Full Code of Tyrrrz/YoutubeDownloader for AI

prime 9dbf1219d3b0 cached
103 files
255.6 KB
54.9k tokens
268 symbols
1 requests
Download .txt
Showing preview only (282K chars total). Download the full file or copy to clipboard to get everything.
Repository: Tyrrrz/YoutubeDownloader
Branch: prime
Commit: 9dbf1219d3b0
Files: 103
Total size: 255.6 KB

Directory structure:
gitextract_i1gh213a/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── Directory.Build.props
├── Directory.Packages.props
├── License.txt
├── NuGet.config
├── Readme.md
├── YoutubeDownloader/
│   ├── .gitignore
│   ├── App.axaml
│   ├── App.axaml.cs
│   ├── Converters/
│   │   ├── EqualityConverter.cs
│   │   ├── MarkdownToInlinesConverter.cs
│   │   ├── VideoQualityPreferenceToStringConverter.cs
│   │   ├── VideoToHighestQualityThumbnailUrlStringConverter.cs
│   │   └── VideoToLowestQualityThumbnailUrlStringConverter.cs
│   ├── Download-FFmpeg.ps1
│   ├── Framework/
│   │   ├── DialogManager.cs
│   │   ├── DialogViewModelBase.cs
│   │   ├── SnackbarManager.cs
│   │   ├── ThemeVariant.cs
│   │   ├── UserControl.cs
│   │   ├── ViewManager.cs
│   │   ├── ViewModelBase.cs
│   │   ├── ViewModelManager.cs
│   │   └── Window.cs
│   ├── Localization/
│   │   ├── Language.cs
│   │   ├── LocalizationManager.English.cs
│   │   ├── LocalizationManager.French.cs
│   │   ├── LocalizationManager.German.cs
│   │   ├── LocalizationManager.Spanish.cs
│   │   ├── LocalizationManager.Ukrainian.cs
│   │   └── LocalizationManager.cs
│   ├── Program.cs
│   ├── Publish-MacOSBundle.ps1
│   ├── Services/
│   │   ├── SettingsService.AuthCookiesEncryptionConverter.cs
│   │   ├── SettingsService.cs
│   │   └── UpdateService.cs
│   ├── StartOptions.cs
│   ├── Utils/
│   │   ├── Disposable.cs
│   │   ├── DisposableCollector.cs
│   │   ├── Extensions/
│   │   │   ├── AvaloniaExtensions.cs
│   │   │   ├── DirectoryExtensions.cs
│   │   │   ├── DisposableExtensions.cs
│   │   │   ├── EnvironmentExtensions.cs
│   │   │   ├── NotifyPropertyChangedExtensions.cs
│   │   │   ├── PathExtensions.cs
│   │   │   └── ProcessExtensions.cs
│   │   ├── NativeMethods.cs
│   │   └── ResizableSemaphore.cs
│   ├── ViewModels/
│   │   ├── Components/
│   │   │   ├── DashboardViewModel.cs
│   │   │   ├── DownloadStatus.cs
│   │   │   └── DownloadViewModel.cs
│   │   ├── Dialogs/
│   │   │   ├── AuthSetupViewModel.cs
│   │   │   ├── DownloadMultipleSetupViewModel.cs
│   │   │   ├── DownloadSingleSetupViewModel.cs
│   │   │   ├── MessageBoxViewModel.cs
│   │   │   └── SettingsViewModel.cs
│   │   └── MainViewModel.cs
│   ├── Views/
│   │   ├── Components/
│   │   │   ├── DashboardView.axaml
│   │   │   └── DashboardView.axaml.cs
│   │   ├── Dialogs/
│   │   │   ├── AuthSetupView.axaml
│   │   │   ├── AuthSetupView.axaml.cs
│   │   │   ├── DownloadMultipleSetupView.axaml
│   │   │   ├── DownloadMultipleSetupView.axaml.cs
│   │   │   ├── DownloadSingleSetupView.axaml
│   │   │   ├── DownloadSingleSetupView.axaml.cs
│   │   │   ├── MessageBoxView.axaml
│   │   │   ├── MessageBoxView.axaml.cs
│   │   │   ├── SettingsView.axaml
│   │   │   └── SettingsView.axaml.cs
│   │   ├── MainView.axaml
│   │   └── MainView.axaml.cs
│   ├── YoutubeDownloader.csproj
│   └── app.manifest
├── YoutubeDownloader.Core/
│   ├── Downloading/
│   │   ├── FFmpeg.cs
│   │   ├── FileNameTemplate.cs
│   │   ├── VideoDownloadOption.cs
│   │   ├── VideoDownloadPreference.cs
│   │   ├── VideoDownloader.cs
│   │   └── VideoQualityPreference.cs
│   ├── Resolving/
│   │   ├── QueryResolver.cs
│   │   ├── QueryResult.cs
│   │   └── QueryResultKind.cs
│   ├── Tagging/
│   │   ├── MediaFile.cs
│   │   ├── MediaTagInjector.cs
│   │   ├── MusicBrainzClient.cs
│   │   └── MusicBrainzRecording.cs
│   ├── Utils/
│   │   ├── Extensions/
│   │   │   ├── AsyncCollectionExtensions.cs
│   │   │   ├── CollectionExtensions.cs
│   │   │   ├── GenericExtensions.cs
│   │   │   ├── PathExtensions.cs
│   │   │   ├── StringExtensions.cs
│   │   │   └── YoutubeExtensions.cs
│   │   ├── Http.cs
│   │   ├── ThrottleLock.cs
│   │   └── Url.cs
│   └── YoutubeDownloader.Core.csproj
├── YoutubeDownloader.sln
├── favicon.icns
└── global.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.yml
================================================
name: 🐛 Bug report
description: Report broken functionality.
labels: [bug]

body:
  - type: markdown
    attributes:
      value: |
        - Avoid generic or vague titles such as "Something's not working" or "A couple of problems" — be as descriptive as possible.
        - Keep your issue focused on one single problem. If you have multiple bug reports, please create a separate issue for each of them.
        - Issues should represent **complete and actionable** work items. If you are unsure about something or have a question, please start a [discussion](https://github.com/Tyrrrz/YoutubeDownloader/discussions/new) instead.
        - Remember that **YoutubeDownloader** is an open-source project funded by the community. If you find it useful, **please consider [donating](https://tyrrrz.me/donate) to support its development**.

        ___

  - type: input
    attributes:
      label: Version
      description: Which version of the application does this bug affect? Make sure you're not using an outdated version.
      placeholder: v1.0.0
    validations:
      required: true

  - type: input
    attributes:
      label: Platform
      description: Which platform do you experience this bug on?
      placeholder: Windows 11
    validations:
      required: true

  - type: textarea
    attributes:
      label: Steps to reproduce
      description: >
        Minimum steps required to reproduce the bug, including prerequisites, application settings, video URL(s), or other relevant items.
        The information provided in this field must be readily actionable, meaning that anyone should be able to reproduce the bug by following these steps.
      placeholder: |
        Video or playlist URL: ...

        Download settings:
        - ...

        Application settings:
        - ...

        Steps:
        - Step 1
        - Step 2
        - Step 3
    validations:
      required: true

  - type: textarea
    attributes:
      label: Details
      description: Clear and thorough explanation of the bug, including any additional information you may find relevant.
      placeholder: |
        - Expected behavior: ...
        - Actual behavior: ...
    validations:
      required: true

  - type: checkboxes
    attributes:
      label: Checklist
      description: Quick list of checks to ensure that everything is in order.
      options:
        - label: I have looked through existing issues to make sure that this bug has not been reported before
          required: true
        - label: I have provided a descriptive title for this issue
          required: true
        - label: I have made sure that this bug is reproducible on the latest version of the application
          required: true
        - label: I have provided all the information needed to reproduce this bug as efficiently as possible
          required: true
        - label: I have sponsored this project
          required: false
        - label: I have not read any of the above and just checked all the boxes to submit the issue
          required: false

  - type: markdown
    attributes:
      value: |
        If you are struggling to provide actionable reproduction steps, or if something else is preventing you from creating a complete bug report, please start a [discussion](https://github.com/Tyrrrz/YoutubeDownloader/discussions/new) instead.


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
  - name: ⚠ Feature request
    url: https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md
    about: Sorry, but this project is in maintenance mode and no longer accepts new feature requests.
  - name: 📖 Documentation
    url: https://github.com/Tyrrrz/YoutubeDownloader/wiki
    about: Find usage guides and frequently asked questions.
  - name: 🗨 Discussions
    url: https://github.com/Tyrrrz/YoutubeDownloader/discussions/new
    about: Ask and answer questions.
  - name: 💬 Discord server
    url: https://discord.gg/2SUWKFnHSm
    about: Chat with the project community.


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: monthly
    labels:
      - enhancement
    groups:
      actions:
        patterns:
          - "*"
  - package-ecosystem: nuget
    directory: "/"
    schedule:
      interval: monthly
    labels:
      - enhancement
    groups:
      nuget:
        patterns:
          - "*"


================================================
FILE: .github/workflows/main.yml
================================================
name: main

on:
  workflow_dispatch:
  push:
    branches:
      - prime
    tags:
      - "*"
  pull_request:
    branches:
      - prime

env:
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
  DOTNET_NOLOGO: true
  DOTNET_CLI_TELEMETRY_OPTOUT: true

jobs:
  format:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    permissions:
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install .NET
        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0

      # Build the project separately to discern between build and format errors
      - name: Build
        run: >
          dotnet build
          -p:CSharpier_Bypass=true
          --configuration Release

      - name: Verify formatting
        id: verify
        run: >
          dotnet build
          -t:CSharpierFormat
          --configuration Release
          --no-restore

      - name: Report issues
        if: ${{ failure() && steps.verify.outcome == 'failure' }}
        run: echo "::error title=Bad formatting::Formatting issues detected. Please build the solution locally to fix them."

  pack:
    strategy:
      matrix:
        rid:
          - win-arm64
          - win-x86
          - win-x64
          - linux-arm64
          # Linux x86 is not supported by .NET
          # - linux-x86
          - linux-x64
          - osx-arm64
          - osx-x64
        bundle-ffmpeg:
          - true
          - false
        include:
          - bundle-ffmpeg: true
            artifact-name-base: YoutubeDownloader
          - bundle-ffmpeg: false
            artifact-name-base: YoutubeDownloader.Bare

    runs-on: ${{ startsWith(matrix.rid, 'win-') && 'windows-latest' || startsWith(matrix.rid, 'osx-') && 'macos-latest' || 'ubuntu-latest' }}
    timeout-minutes: 10

    permissions:
      actions: write
      contents: read

    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

      - name: Install .NET
        uses: actions/setup-dotnet@baa11fbfe1d6520db94683bd5c7a3818018e4309 # v5.1.0

      - name: Publish app
        run: >
          dotnet publish YoutubeDownloader
          -p:Version=${{ github.ref_type == 'tag' && github.ref_name || format('999.9.9-ci-{0}', github.sha) }}
          -p:CSharpier_Bypass=true
          -p:EncryptionSalt=${{ secrets.ENCRYPTION_SALT || 'HimalayanPinkSalt' }}
          -p:DownloadFFmpeg=${{ matrix.bundle-ffmpeg }}
          -p:PublishMacOSBundle=${{ startsWith(matrix.rid, 'osx-') }}
          --output YoutubeDownloader/bin/publish
          --configuration Release
          --runtime ${{ matrix.rid }}
          --self-contained

      - name: Upload app binaries
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
        with:
          name: ${{ matrix.artifact-name-base }}.${{ matrix.rid }}
          path: YoutubeDownloader/bin/publish
          if-no-files-found: error

  release:
    if: ${{ github.ref_type == 'tag' }}

    needs:
      - format
      - pack

    runs-on: ubuntu-latest
    timeout-minutes: 10

    permissions:
      contents: write

    steps:
      - name: Create release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: >
          gh release create ${{ github.ref_name }}
          --repo ${{ github.event.repository.full_name }}
          --title ${{ github.ref_name }}
          --generate-notes
          --verify-tag

  deploy:
    needs: release

    strategy:
      matrix:
        rid:
          - win-arm64
          - win-x86
          - win-x64
          - linux-arm64
          # Linux x86 is not supported by .NET
          # - linux-x86
          - linux-x64
          - osx-arm64
          - osx-x64
        bundle-ffmpeg:
          - true
          - false
        include:
          - bundle-ffmpeg: true
            artifact-name-base: YoutubeDownloader
          - bundle-ffmpeg: false
            artifact-name-base: YoutubeDownloader.Bare

    runs-on: ubuntu-latest
    timeout-minutes: 10

    permissions:
      actions: read
      contents: write

    steps:
      - name: Download app binaries
        uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
        with:
          name: ${{ matrix.artifact-name-base }}.${{ matrix.rid }}
          path: YoutubeDownloader/

      - name: Set permissions
        if: ${{ !startsWith(matrix.rid, 'win-') }}
        run: |
          [ -f YoutubeDownloader/YoutubeDownloader ] && chmod +x YoutubeDownloader/YoutubeDownloader || true
          [ -f YoutubeDownloader/ffmpeg ] && chmod +x YoutubeDownloader/ffmpeg || true

          # macOS bundle
          [ -f YoutubeDownloader/YoutubeDownloader.app/Contents/MacOS/YoutubeDownloader ] && chmod +x YoutubeDownloader/YoutubeDownloader.app/Contents/MacOS/YoutubeDownloader || true
          [ -f YoutubeDownloader/YoutubeDownloader.app/Contents/MacOS/ffmpeg ] && chmod +x YoutubeDownloader/YoutubeDownloader.app/Contents/MacOS/ffmpeg || true

      - name: Create package
        # Change into the artifacts directory to avoid including the directory itself in the zip archive
        working-directory: YoutubeDownloader/
        run: zip -r ../${{ matrix.artifact-name-base }}.${{ matrix.rid }}.zip .

      - name: Upload release asset
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: >
          gh release upload ${{ github.ref_name }}
          ${{ matrix.artifact-name-base }}.${{ matrix.rid }}.zip
          --repo ${{ github.event.repository.full_name }}

  notify:
    needs: deploy
    runs-on: ubuntu-latest
    timeout-minutes: 10

    permissions:
      contents: read

    steps:
      - name: Notify Discord
        uses: tyrrrz/action-http-request@1dd7ad841a34b9299f3741f7c7399f9feefdfb08 # 1.1.3
        with:
          url: ${{ secrets.DISCORD_WEBHOOK }}
          method: POST
          headers: |
            Content-Type: application/json; charset=UTF-8
          body: |
            {
              "avatar_url": "https://raw.githubusercontent.com/${{ github.event.repository.full_name }}/${{ github.ref_name }}/favicon.png",
              "content": "[**${{ github.event.repository.name }}**](<${{ github.event.repository.html_url }}>) v${{ github.ref_name }} has been released!"
            }
          retry-count: 5


================================================
FILE: .gitignore
================================================
# User-specific files
.vs/
.idea/
*.suo
*.user

# Build results
bin/
obj/

# Avalonia
.avalonia-build-tasks/

# Test results
TestResults/


================================================
FILE: Directory.Build.props
================================================
<Project>

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Version>999.9.9-dev</Version>
    <Company>Tyrrrz</Company>
    <Copyright>Copyright (C) Oleksii Holub</Copyright>
    <LangVersion>preview</LangVersion>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <ILLinkTreatWarningsAsErrors>false</ILLinkTreatWarningsAsErrors>
  </PropertyGroup>

</Project>

================================================
FILE: Directory.Packages.props
================================================
<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>

  <ItemGroup>
    <PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.6.0" />
    <PackageVersion Include="Avalonia" Version="11.3.0" />
    <PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.0" />
    <PackageVersion Include="Avalonia.Desktop" Version="11.3.0" />
    <PackageVersion Include="Avalonia.Diagnostics" Version="11.3.0" />
    <PackageVersion Include="Cogwheel" Version="2.1.0" />
    <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
    <PackageVersion Include="CSharpier.MsBuild" Version="1.2.6" />
    <PackageVersion Include="Deorcify" Version="1.1.0" />
    <PackageVersion Include="ThisAssembly.Project" Version="2.1.2" />
    <PackageVersion Include="DialogHost.Avalonia" Version="0.10.4" />
    <PackageVersion Include="Gress" Version="2.1.1" />
    <PackageVersion Include="JsonExtensions" Version="1.2.0" />
    <PackageVersion Include="Markdig" Version="1.1.0" />
    <PackageVersion Include="Material.Avalonia" Version="3.9.2" />
    <PackageVersion Include="Material.Avalonia.DataGrid" Version="3.9.2" />
    <PackageVersion Include="Material.Icons.Avalonia" Version="2.2.0" />
    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
    <PackageVersion Include="Onova" Version="2.6.13" />
    <PackageVersion Include="TagLibSharp" Version="2.3.0" />
    <PackageVersion Include="WebView.Avalonia" Version="11.0.0.1" />
    <PackageVersion Include="WebView.Avalonia.Desktop" Version="11.0.0.1" />
    <PackageVersion Include="YoutubeExplode" Version="6.5.7" />
    <PackageVersion Include="YoutubeExplode.Converter" Version="6.5.7" />
  </ItemGroup>
</Project>


================================================
FILE: License.txt
================================================
MIT License

Copyright (c) 2018-2026 Oleksii Holub

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

================================================
FILE: NuGet.config
================================================
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <clear />
    <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
  </packageSources>
  <config>
    <add key="defaultPushSource" value="https://api.nuget.org/v3/index.json" />
  </config>
</configuration>


================================================
FILE: Readme.md
================================================
# YoutubeDownloader

[![Status](https://img.shields.io/badge/status-maintenance-ffd700.svg)](https://github.com/Tyrrrz/.github/blob/prime/docs/project-status.md)
[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://tyrrrz.me/ukraine)
[![Build](https://img.shields.io/github/actions/workflow/status/Tyrrrz/YoutubeDownloader/main.yml?branch=prime)](https://github.com/Tyrrrz/YoutubeDownloader/actions)
[![Release](https://img.shields.io/github/release/Tyrrrz/YoutubeDownloader.svg)](https://github.com/Tyrrrz/YoutubeDownloader/releases)
[![Downloads](https://img.shields.io/github/downloads/Tyrrrz/YoutubeDownloader/total.svg)](https://github.com/Tyrrrz/YoutubeDownloader/releases)
[![Discord](https://img.shields.io/discord/869237470565392384?label=discord)](https://discord.gg/2SUWKFnHSm)
[![Fuck Russia](https://img.shields.io/badge/fuck-russia-e4181c.svg?labelColor=000000)](https://twitter.com/tyrrrz/status/1495972128977571848)

<table>
    <tr>
        <td width="99999" align="center">Development of this project is entirely funded by the community. <b><a href="https://tyrrrz.me/donate">Consider donating to support!</a></b></td>
    </tr>
</table>

<p align="center">
    <img src="favicon.png" alt="Icon" />
</p>

**YoutubeDownloader** is an application that lets you download videos from YouTube.
You can copy-paste URL of any video, playlist or channel and download it directly in a format of your choice.
It also supports searching by keywords, which is helpful if you want to quickly look up and download videos.

> [!NOTE]
> This application uses [**YoutubeExplode**](https://github.com/Tyrrrz/YoutubeExplode) under the hood to interact with YouTube.
> You can [read this article](https://tyrrrz.me/blog/reverse-engineering-youtube-revisited) to learn more about how it works.

## Terms of use<sup>[[?]](https://github.com/Tyrrrz/.github/blob/prime/docs/why-so-political.md)</sup>

By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all the following statements:

- You **condemn Russia and its military aggression against Ukraine**
- You **recognize that Russia is an occupant that unlawfully invaded a sovereign state**
- You **support Ukraine's territorial integrity, including its claims over temporarily occupied territories of Crimea and Donbas**
- You **reject false narratives perpetuated by Russian state propaganda**

To learn more about the war and how you can help, [click here](https://tyrrrz.me/ukraine). Glory to Ukraine! 🇺🇦

## Download

- 🟢 **[Stable release](https://github.com/Tyrrrz/YoutubeDownloader/releases/latest)**
- 🟠 [CI build](https://github.com/Tyrrrz/YoutubeDownloader/actions/workflows/main.yml)

> [!IMPORTANT]
> To launch the app on MacOS, you need to first remove the downloaded file from quarantine.
> You can do that by running the following command in the terminal: `xattr -rd com.apple.quarantine YoutubeDownloader.app`.

> [!NOTE]
> If you're unsure which build is right for your system, consult with [this page](https://useragent.cc) to determine your OS and CPU architecture.

> [!NOTE]
> **YoutubeDownloader** comes bundled with [FFmpeg](https://ffmpeg.org) which is used for processing videos.
> You can also download a version of **YoutubeDownloader** that doesn't include FFmpeg (`YoutubeDownloader.Bare.*` builds) if you prefer to use your own installation.

## Features

- Cross-platform graphical user interface
- Download videos by URL
- Download videos from playlists or channels
- Download videos by search query
- Selectable video quality and format
- Automatically embed audio tracks in alternative languages
- Automatically embed subtitles
- Automatically inject media tags
- Log in with a YouTube account to access private content

## Screenshots

![list](.assets/list.png)
![single](.assets/single.png)
![multiple](.assets/multiple.png)


================================================
FILE: YoutubeDownloader/.gitignore
================================================
/ffmpeg*

================================================
FILE: YoutubeDownloader/App.axaml
================================================
<Application
    x:Class="YoutubeDownloader.App"
    xmlns="https://github.com/avaloniaui"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dialogHostAvalonia="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
    xmlns:framework="clr-namespace:YoutubeDownloader.Framework"
    xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
    xmlns:materialControls="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
    xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
    xmlns:materialStyles="clr-namespace:Material.Styles.Themes;assembly=Material.Styles"
    Name="YoutubeDownloader"
    ActualThemeVariantChanged="Application_OnActualThemeVariantChanged">
    <Application.DataTemplates>
        <framework:ViewManager />
    </Application.DataTemplates>

    <Application.Styles>
        <!--  This theme is used as a stub to pre-load default resources, the actual colors are set through code  -->
        <materialStyles:MaterialTheme
            BaseTheme="Light"
            PrimaryColor="Grey"
            SecondaryColor="DeepOrange" />
        <materialIcons:MaterialIconStyles />
        <dialogHostAvalonia:DialogHostStyles />

        <!--  Combo box  -->
        <Style Selector="ComboBox">
            <Setter Property="FontSize" Value="14" />

            <Style Selector="^ /template/ Panel#PART_RootPanel">
                <Setter Property="Height" Value="22" />
            </Style>

            <Style Selector="^ /template/ ToggleButton">
                <Style Selector="^:checked, ^:unchecked">
                    <Setter Property="Margin" Value="0" />
                    <Setter Property="CornerRadius" Value="0" />

                    <Style Selector="^ ContentPresenter#contentPresenter">
                        <Setter Property="Margin" Value="12,8" />
                    </Style>
                </Style>
            </Style>
        </Style>

        <!--  Context menu  -->
        <Style Selector="ContextMenu">
            <Setter Property="BorderBrush" Value="{DynamicResource MaterialDividerBrush}" />
            <Setter Property="BorderThickness" Value="1" />
        </Style>

        <!--  Data grid  -->
        <Style Selector="DataGrid">
            <Setter Property="BorderBrush" Value="{DynamicResource MaterialDividerBrush}" />
            <Setter Property="AutoGenerateColumns" Value="False" />
            <Setter Property="CanUserReorderColumns" Value="False" />
            <Setter Property="CanUserResizeColumns" Value="False" />
            <Setter Property="CanUserSortColumns" Value="True" />
            <Setter Property="IsReadOnly" Value="True" />
            <Setter Property="SelectionMode" Value="Single" />
        </Style>

        <Style Selector="DataGridColumnHeader">
            <Setter Property="AreSeparatorsVisible" Value="False" />
        </Style>

        <Style Selector="DataGridRow">
            <Style Selector="^:selected /template/ Rectangle#BackgroundRectangle">
                <Setter Property="IsVisible" Value="False" />
            </Style>
            <Style Selector="^:pointerover /template/ Rectangle#BackgroundRectangle">
                <Setter Property="IsVisible" Value="False" />
            </Style>
        </Style>

        <!--  Dialog host  -->
        <Style Selector="dialogHostAvalonia|DialogHost">
            <Setter Property="DialogMargin" Value="0" />
        </Style>

        <Style Selector="dialogHostAvalonia|DialogOverlayPopupHost">
            <Setter Property="Margin" Value="48" />
            <Setter Property="Background" Value="{DynamicResource MaterialPaperBrush}" />
        </Style>

        <!--  Snack bar host  -->
        <Style Selector="materialControls|SnackbarHost">
            <Setter Property="SnackbarHorizontalAlignment" Value="Stretch" />
            <Setter Property="VerticalContentAlignment" Value="Center" />

            <Style Selector="^ /template/ ItemsControl#PART_SnackbarHostItemsContainer materialControls|Card">
                <Setter Property="Background" Value="{DynamicResource MaterialDarkBackgroundBrush}" />
                <Setter Property="Foreground" Value="{DynamicResource MaterialDarkForegroundBrush}" />
            </Style>

            <Style Selector="^ /template/ ItemsControl#PART_SnackbarHostItemsContainer Button">
                <Setter Property="Foreground" Value="{DynamicResource SecondaryHueMidBrush}" />
            </Style>
        </Style>

        <!--  Progress bar  -->
        <Style Selector="ProgressBar">
            <Setter Property="Minimum" Value="0" />
            <Setter Property="Maximum" Value="1" />
            <Setter Property="Foreground" Value="{DynamicResource MaterialSecondaryMidBrush}" />
            <Setter Property="materialAssists:TransitionAssist.DisableTransitions" Value="True" />

            <Style Selector="^:horizontal">
                <Setter Property="MinHeight" Value="0" />
            </Style>
        </Style>

        <!--  Slider  -->
        <Style Selector="Slider">
            <Style Selector="^ /template/ ProgressBar#PART_ProgressLayer">
                <Style Selector="^:horizontal">
                    <Style Selector="^ Panel#PART_InnerPanel">
                        <Setter Property="Height" Value="2" />

                        <Style Selector="^ Border#PART_InactiveState">
                            <Setter Property="Margin" Value="0" />
                            <Setter Property="Height" Value="2" />
                        </Style>

                        <Style Selector="^ Border#PART_Indicator">
                            <Setter Property="Margin" Value="0" />
                        </Style>
                    </Style>
                </Style>
            </Style>

            <Style Selector="^ /template/ Track#PART_Track">
                <Style Selector="^:horizontal">
                    <Setter Property="Margin" Value="4,0" />
                </Style>

                <Style Selector="^ Border#PART_HoverEffect">
                    <Setter Property="Width" Value="24" />
                    <Setter Property="Height" Value="24" />
                </Style>

                <Style Selector="^ Border#PART_ThumbGrip">
                    <Setter Property="Width" Value="12" />
                    <Setter Property="Height" Value="12" />
                </Style>
            </Style>
        </Style>

        <!--  Run  -->
        <Style Selector="Run">
            <Setter Property="BaselineAlignment" Value="Center" />
        </Style>

        <!--  Text box  -->
        <Style Selector="TextBox">
            <Setter Property="FontSize" Value="14" />
        </Style>

        <!--  Toggle switch  -->
        <Style Selector="ToggleSwitch">
            <Setter Property="materialAssists:ToggleSwitchAssist.SwitchThumbOffBackground" Value="{DynamicResource ToggleBackgroundBrush}" />
        </Style>

        <!--  Tooltip  -->
        <Style Selector="ToolTip">
            <Setter Property="TextElement.FontSize" Value="14" />
            <Setter Property="TextElement.FontWeight" Value="Normal" />
            <Setter Property="TextElement.FontStyle" Value="Normal" />
            <Setter Property="TextElement.FontStretch" Value="Normal" />
        </Style>
    </Application.Styles>

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.ThemeDictionaries>
                <ResourceDictionary x:Key="Default">
                    <SolidColorBrush x:Key="ToggleBackgroundBrush" Color="#FFFFFF" />
                </ResourceDictionary>
                <ResourceDictionary x:Key="Dark">
                    <SolidColorBrush x:Key="ToggleBackgroundBrush" Color="#8E8E8E" />
                </ResourceDictionary>
            </ResourceDictionary.ThemeDictionaries>

            <!--  Text box  -->
            <ControlTheme
                x:Key="CompactTextBox"
                BasedOn="{StaticResource {x:Type TextBox}}"
                TargetType="{x:Type TextBox}">
                <Styles>
                    <Style Selector="TextBox">
                        <Setter Property="Height" Value="22" />

                        <Style Selector="^ /template/ Panel#PART_TextFieldPanel">
                            <Setter Property="MinHeight" Value="0" />
                        </Style>

                        <Style Selector="^ /template/ Panel#PART_TextContainer">
                            <Setter Property="Margin" Value="0" />
                        </Style>
                    </Style>
                </Styles>
            </ControlTheme>
        </ResourceDictionary>
    </Application.Resources>
</Application>

================================================
FILE: YoutubeDownloader/App.axaml.cs
================================================
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using AvaloniaWebView;
using Material.Styles.Themes;
using Microsoft.Extensions.DependencyInjection;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;
using YoutubeDownloader.ViewModels;
using YoutubeDownloader.ViewModels.Components;
using YoutubeDownloader.ViewModels.Dialogs;
using YoutubeDownloader.Views;

namespace YoutubeDownloader;

public class App : Application, IDisposable
{
    private readonly DisposableCollector _eventRoot = new();

    private readonly ServiceProvider _services;
    private readonly SettingsService _settingsService;
    private readonly MainViewModel _mainViewModel;

    private bool _isDisposed;

    public App()
    {
        var services = new ServiceCollection();

        // Framework
        services.AddSingleton<DialogManager>();
        services.AddSingleton<SnackbarManager>();
        services.AddSingleton<ViewManager>();
        services.AddSingleton<ViewModelManager>();

        // Localization
        services.AddSingleton<LocalizationManager>();

        // Services
        services.AddSingleton<SettingsService>();
        services.AddSingleton<UpdateService>();

        // View models
        services.AddTransient<MainViewModel>();
        services.AddTransient<DashboardViewModel>();
        services.AddTransient<DownloadViewModel>();
        services.AddTransient<AuthSetupViewModel>();
        services.AddTransient<DownloadMultipleSetupViewModel>();
        services.AddTransient<DownloadSingleSetupViewModel>();
        services.AddTransient<MessageBoxViewModel>();
        services.AddTransient<SettingsViewModel>();

        _services = services.BuildServiceProvider(true);
        _settingsService = _services.GetRequiredService<SettingsService>();
        _mainViewModel = _services.GetRequiredService<ViewModelManager>().CreateMainViewModel();

        // Re-initialize the theme when the user changes it
        _eventRoot.Add(
            _settingsService.WatchProperty(
                o => o.Theme,
                () =>
                {
                    RequestedThemeVariant = _settingsService.Theme switch
                    {
                        ThemeVariant.Light => Avalonia.Styling.ThemeVariant.Light,
                        ThemeVariant.Dark => Avalonia.Styling.ThemeVariant.Dark,
                        _ => Avalonia.Styling.ThemeVariant.Default,
                    };

                    InitializeTheme();
                }
            )
        );
    }

    public override void Initialize()
    {
        base.Initialize();

        AvaloniaXamlLoader.Load(this);
    }

    public override void RegisterServices()
    {
        base.RegisterServices();

        AvaloniaWebViewBuilder.Initialize(config => config.IsInPrivateModeEnabled = true);
    }

    private void InitializeTheme()
    {
        var actualTheme = RequestedThemeVariant?.Key switch
        {
            "Light" => PlatformThemeVariant.Light,
            "Dark" => PlatformThemeVariant.Dark,
            _ => PlatformSettings?.GetColorValues().ThemeVariant ?? PlatformThemeVariant.Light,
        };

        this.LocateMaterialTheme<MaterialThemeBase>().CurrentTheme =
            actualTheme == PlatformThemeVariant.Light
                ? Theme.Create(Theme.Light, Color.Parse("#343838"), Color.Parse("#F9A825"))
                : Theme.Create(Theme.Dark, Color.Parse("#E8E8E8"), Color.Parse("#F9A825"));
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            desktop.MainWindow = new MainView { DataContext = _mainViewModel };

            void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs args)
            {
                if (sender is IControlledApplicationLifetime lifetime)
                    lifetime.Exit -= OnExit;

                Dispose();
            }

            // Although `App.Dispose()` is invoked from `Program.Main(...)`, on some platforms
            // it may be called too late in the shutdown lifecycle. Attach an exit
            // handler to ensure timely disposal as a safeguard.
            // https://github.com/Tyrrrz/YoutubeDownloader/issues/795
            desktop.Exit += OnExit;
        }

        base.OnFrameworkInitializationCompleted();

        // Set up initial custom theme colors
        InitializeTheme();

        // Load settings
        _settingsService.Load();
    }

    private void Application_OnActualThemeVariantChanged(object? sender, EventArgs args) =>
        // Re-initialize the theme when the system theme changes
        InitializeTheme();

    public void Dispose()
    {
        if (_isDisposed)
            return;

        _isDisposed = true;

        _eventRoot.Dispose();
        _services.Dispose();
    }
}


================================================
FILE: YoutubeDownloader/Converters/EqualityConverter.cs
================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;

namespace YoutubeDownloader.Converters;

public class EqualityConverter(bool isInverted) : IValueConverter
{
    public static EqualityConverter IsEqual { get; } = new(false);
    public static EqualityConverter IsNotEqual { get; } = new(true);

    public object? Convert(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => EqualityComparer<object>.Default.Equals(value, parameter) != isInverted;

    public object ConvertBack(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => throw new NotSupportedException();
}


================================================
FILE: YoutubeDownloader/Converters/MarkdownToInlinesConverter.cs
================================================
using System;
using System.Globalization;
using Avalonia.Controls.Documents;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using MarkdownInline = Markdig.Syntax.Inlines.Inline;

namespace YoutubeDownloader.Converters;

public class MarkdownToInlinesConverter : IValueConverter
{
    public static readonly MarkdownToInlinesConverter Instance = new();

    private static readonly MarkdownPipeline MarkdownPipeline = new MarkdownPipelineBuilder()
        .UseEmphasisExtras()
        .Build();

    private static void ProcessInline(
        InlineCollection inlines,
        MarkdownInline markdownInline,
        FontWeight? fontWeight = null,
        FontStyle? fontStyle = null,
        TextDecorationCollection? textDecorations = null
    )
    {
        switch (markdownInline)
        {
            case LiteralInline literal:
            {
                var run = new Run(literal.Content.ToString());

                if (fontWeight is not null)
                    run.FontWeight = fontWeight.Value;
                if (fontStyle is not null)
                    run.FontStyle = fontStyle.Value;
                if (textDecorations is not null)
                    run.TextDecorations = textDecorations;

                inlines.Add(run);
                break;
            }

            case LineBreakInline:
            {
                inlines.Add(new LineBreak());
                break;
            }

            case EmphasisInline emphasis:
            {
                var newWeight = fontWeight;
                var newStyle = fontStyle;
                var newDecorations = textDecorations;

                switch (emphasis.DelimiterChar)
                {
                    case '*' or '_' when emphasis.DelimiterCount == 2:
                        newWeight = FontWeight.SemiBold;
                        break;
                    case '*' or '_':
                        newStyle = FontStyle.Italic;
                        break;
                    case '~':
                        newDecorations = TextDecorations.Strikethrough;
                        break;
                    case '+':
                        newDecorations = TextDecorations.Underline;
                        break;
                }

                foreach (var child in emphasis)
                    ProcessInline(inlines, child, newWeight, newStyle, newDecorations);

                break;
            }

            case ContainerInline container:
            {
                foreach (var child in container)
                    ProcessInline(inlines, child, fontWeight, fontStyle, textDecorations);

                break;
            }
        }
    }

    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        var inlines = new InlineCollection();
        if (value is not string { Length: > 0 } text)
            return inlines;

        var isFirstParagraph = true;
        foreach (var block in Markdown.Parse(text, MarkdownPipeline))
        {
            if (block is not ParagraphBlock { Inline: not null } paragraph)
                continue;

            if (!isFirstParagraph)
            {
                // Insert a blank line between paragraphs
                inlines.Add(new LineBreak());
                inlines.Add(new LineBreak());
            }

            isFirstParagraph = false;

            foreach (var markdownInline in paragraph.Inline)
                ProcessInline(inlines, markdownInline);
        }

        return inlines;
    }

    public object? ConvertBack(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => throw new NotSupportedException();
}


================================================
FILE: YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs
================================================
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using YoutubeDownloader.Core.Downloading;

namespace YoutubeDownloader.Converters;

public class VideoQualityPreferenceToStringConverter : IValueConverter
{
    public static VideoQualityPreferenceToStringConverter Instance { get; } = new();

    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
    {
        if (value is VideoQualityPreference preference)
            return preference.GetDisplayName();

        return default(string);
    }

    public object ConvertBack(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => throw new NotSupportedException();
}


================================================
FILE: YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlStringConverter.cs
================================================
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using YoutubeExplode.Common;
using YoutubeExplode.Videos;

namespace YoutubeDownloader.Converters;

public class VideoToHighestQualityThumbnailUrlStringConverter : IValueConverter
{
    public static VideoToHighestQualityThumbnailUrlStringConverter Instance { get; } = new();

    public object? Convert(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => value is IVideo video ? video.Thumbnails.TryGetWithHighestResolution()?.Url : null;

    public object ConvertBack(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => throw new NotSupportedException();
}


================================================
FILE: YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlStringConverter.cs
================================================
using System;
using System.Globalization;
using System.Linq;
using Avalonia.Data.Converters;
using YoutubeExplode.Videos;

namespace YoutubeDownloader.Converters;

public class VideoToLowestQualityThumbnailUrlStringConverter : IValueConverter
{
    public static VideoToLowestQualityThumbnailUrlStringConverter Instance { get; } = new();

    public object? Convert(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => value is IVideo video ? video.Thumbnails.MinBy(t => t.Resolution.Area)?.Url : null;

    public object ConvertBack(
        object? value,
        Type targetType,
        object? parameter,
        CultureInfo culture
    ) => throw new NotSupportedException();
}


================================================
FILE: YoutubeDownloader/Download-FFmpeg.ps1
================================================
param (
    [Parameter(Mandatory=$false)]
    [string]$Platform,

    [Parameter(Mandatory=$false)]
    [string]$OutputPath = $PSScriptRoot
)

$ErrorActionPreference = "Stop"

# If the platform is not specified, use the current OS/arch
if (-not $Platform) {
    $arch = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture

    if ($isWindows) {
        $Platform = "windows-$arch"
    } elseif ($isLinux) {
        $Platform = "linux-$arch"
    } elseif ($isMacOS) {
        $Platform = "osx-$arch"
    } else {
        throw "Unsupported platform"
    }
}

# Normalize platform identifier
$Platform = $Platform.ToLower().Replace("win-", "windows-")

# Identify the FFmpeg filename based on the platform
$fileName = if ($Platform.Contains("windows-")) { "ffmpeg.exe" } else { "ffmpeg" }

# If the output path is an existing directory, append the default file name for the platform
if (Test-Path $OutputPath -PathType Container) {
    $OutputPath = Join-Path $OutputPath $fileName
}

# Delete the existing file if it exists
if (Test-Path $OutputPath) {
    Remove-Item $OutputPath
}

# Download the archive
Write-Host "Downloading FFmpeg for $Platform..."
$http = New-Object System.Net.WebClient
try {
    $http.DownloadFile("https://github.com/Tyrrrz/FFmpegBin/releases/download/7.1.2/ffmpeg-$Platform.zip", "$OutputPath.zip")
} finally {
    $http.Dispose()
}

try {
    # Extract FFmpeg
    Add-Type -Assembly System.IO.Compression.FileSystem
    $zip = [IO.Compression.ZipFile]::OpenRead("$OutputPath.zip")
    try {
        [IO.Compression.ZipFileExtensions]::ExtractToFile($zip.GetEntry($fileName), $OutputPath)
    } finally {
        $zip.Dispose()
    }

    Write-Host "Done downloading FFmpeg."
} finally {
    # Clean up
    Remove-Item "$OutputPath.zip" -Force
}

================================================
FILE: YoutubeDownloader/Framework/DialogManager.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform.Storage;
using DialogHostAvalonia;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.Framework;

public class DialogManager : IDisposable
{
    private readonly SemaphoreSlim _dialogLock = new(1, 1);

    public async Task<T?> ShowDialogAsync<T>(DialogViewModelBase<T> dialog)
    {
        await _dialogLock.WaitAsync();
        try
        {
            await DialogHost.Show(
                dialog,
                // It's fine to await in a void method here because it's an event handler
                // ReSharper disable once AsyncVoidLambda
                async (object _, DialogOpenedEventArgs args) =>
                {
                    await dialog.WaitForCloseAsync();

                    try
                    {
                        args.Session.Close();
                    }
                    catch (InvalidOperationException)
                    {
                        // Dialog host is already processing a close operation
                    }
                }
            );

            // Yield to allow DialogHost to fully reset its state before
            // another dialog is shown (e.g. when dialogs are shown sequentially)
            await Task.Yield();

            return dialog.DialogResult;
        }
        finally
        {
            _dialogLock.Release();
        }
    }

    public async Task<string?> PromptOpenFilePathAsync(
        IReadOnlyList<FilePickerFileType>? fileTypes = null
    )
    {
        var topLevel =
            Application.Current?.ApplicationLifetime?.TryGetTopLevel()
            ?? throw new ApplicationException("Could not find the top-level visual element.");

        var result = await topLevel.StorageProvider.OpenFilePickerAsync(
            new FilePickerOpenOptions { FileTypeFilter = fileTypes, AllowMultiple = false }
        );

        var file = result.FirstOrDefault();
        return file?.TryGetLocalPath() ?? file?.Path.ToString();
    }

    public async Task<string?> PromptSaveFilePathAsync(
        IReadOnlyList<FilePickerFileType>? fileTypes = null,
        string defaultFilePath = ""
    )
    {
        var topLevel =
            Application.Current?.ApplicationLifetime?.TryGetTopLevel()
            ?? throw new ApplicationException("Could not find the top-level visual element.");

        var file = await topLevel.StorageProvider.SaveFilePickerAsync(
            new FilePickerSaveOptions
            {
                FileTypeChoices = fileTypes,
                SuggestedFileName = defaultFilePath,
                DefaultExtension = Path.GetExtension(defaultFilePath).TrimStart('.'),
            }
        );

        return file?.TryGetLocalPath() ?? file?.Path.ToString();
    }

    public async Task<string?> PromptDirectoryPathAsync(string defaultDirPath = "")
    {
        var topLevel =
            Application.Current?.ApplicationLifetime?.TryGetTopLevel()
            ?? throw new ApplicationException("Could not find the top-level visual element.");

        var result = await topLevel.StorageProvider.OpenFolderPickerAsync(
            new FolderPickerOpenOptions
            {
                AllowMultiple = false,
                SuggestedStartLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(
                    defaultDirPath
                ),
            }
        );

        var directory = result.FirstOrDefault();
        if (directory is null)
            return null;

        return directory.TryGetLocalPath() ?? directory.Path.ToString();
    }

    public void Dispose() => _dialogLock.Dispose();
}


================================================
FILE: YoutubeDownloader/Framework/DialogViewModelBase.cs
================================================
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace YoutubeDownloader.Framework;

public abstract partial class DialogViewModelBase<T> : ViewModelBase
{
    private readonly TaskCompletionSource<T> _closeTcs = new(
        TaskCreationOptions.RunContinuationsAsynchronously
    );

    [ObservableProperty]
    public partial T? DialogResult { get; set; }

    [RelayCommand]
    protected void Close(T dialogResult)
    {
        DialogResult = dialogResult;
        _closeTcs.TrySetResult(dialogResult);
    }

    public async Task<T> WaitForCloseAsync() => await _closeTcs.Task;
}

public abstract class DialogViewModelBase : DialogViewModelBase<bool?>;


================================================
FILE: YoutubeDownloader/Framework/SnackbarManager.cs
================================================
using System;
using Avalonia.Threading;
using Material.Styles.Controls;
using Material.Styles.Models;

namespace YoutubeDownloader.Framework;

public class SnackbarManager
{
    private readonly TimeSpan _defaultDuration = TimeSpan.FromSeconds(5);

    public void Notify(string message, TimeSpan? duration = null) =>
        SnackbarHost.Post(
            new SnackbarModel(message, duration ?? _defaultDuration),
            null,
            DispatcherPriority.Normal
        );

    public void Notify(
        string message,
        string actionText,
        Action actionHandler,
        TimeSpan? duration = null
    ) =>
        SnackbarHost.Post(
            new SnackbarModel(
                message,
                duration ?? _defaultDuration,
                new SnackbarButtonModel { Text = actionText, Action = actionHandler }
            ),
            null,
            DispatcherPriority.Normal
        );
}


================================================
FILE: YoutubeDownloader/Framework/ThemeVariant.cs
================================================
namespace YoutubeDownloader.Framework;

public enum ThemeVariant
{
    System,
    Light,
    Dark,
}


================================================
FILE: YoutubeDownloader/Framework/UserControl.cs
================================================
using System;
using Avalonia.Controls;

namespace YoutubeDownloader.Framework;

public class UserControl<TDataContext> : UserControl
{
    public new TDataContext DataContext
    {
        get =>
            base.DataContext is TDataContext dataContext
                ? dataContext
                : throw new InvalidCastException(
                    $"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
                );
        set => base.DataContext = value;
    }
}


================================================
FILE: YoutubeDownloader/Framework/ViewManager.cs
================================================
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using YoutubeDownloader.ViewModels;
using YoutubeDownloader.ViewModels.Components;
using YoutubeDownloader.ViewModels.Dialogs;
using YoutubeDownloader.Views;
using YoutubeDownloader.Views.Components;
using YoutubeDownloader.Views.Dialogs;

namespace YoutubeDownloader.Framework;

public partial class ViewManager
{
    private Control? TryCreateView(ViewModelBase viewModel) =>
        viewModel switch
        {
            MainViewModel => new MainView(),
            DashboardViewModel => new DashboardView(),
            AuthSetupViewModel => new AuthSetupView(),
            DownloadMultipleSetupViewModel => new DownloadMultipleSetupView(),
            DownloadSingleSetupViewModel => new DownloadSingleSetupView(),
            MessageBoxViewModel => new MessageBoxView(),
            SettingsViewModel => new SettingsView(),
            _ => null,
        };

    public Control? TryBindView(ViewModelBase viewModel)
    {
        var view = TryCreateView(viewModel);
        if (view is null)
            return null;

        view.DataContext ??= viewModel;

        return view;
    }
}

public partial class ViewManager : IDataTemplate
{
    bool IDataTemplate.Match(object? data) => data is ViewModelBase;

    Control? ITemplate<object?, Control?>.Build(object? data) =>
        data is ViewModelBase viewModel ? TryBindView(viewModel) : null;
}


================================================
FILE: YoutubeDownloader/Framework/ViewModelBase.cs
================================================
using System;
using CommunityToolkit.Mvvm.ComponentModel;

namespace YoutubeDownloader.Framework;

public abstract class ViewModelBase : ObservableObject, IDisposable
{
    ~ViewModelBase() => Dispose(false);

    protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty);

    protected virtual void Dispose(bool disposing) { }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}


================================================
FILE: YoutubeDownloader/Framework/ViewModelManager.cs
================================================
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Core.Utils.Extensions;
using YoutubeDownloader.ViewModels;
using YoutubeDownloader.ViewModels.Components;
using YoutubeDownloader.ViewModels.Dialogs;
using YoutubeExplode.Videos;

namespace YoutubeDownloader.Framework;

public class ViewModelManager(IServiceProvider services)
{
    public MainViewModel CreateMainViewModel() => services.GetRequiredService<MainViewModel>();

    public DashboardViewModel CreateDashboardViewModel() =>
        services.GetRequiredService<DashboardViewModel>();

    public AuthSetupViewModel CreateAuthSetupViewModel() =>
        services.GetRequiredService<AuthSetupViewModel>();

    public DownloadViewModel CreateDownloadViewModel(
        IVideo video,
        VideoDownloadOption downloadOption,
        string filePath
    )
    {
        var viewModel = services.GetRequiredService<DownloadViewModel>();

        viewModel.Video = video;
        viewModel.DownloadOption = downloadOption;
        viewModel.FilePath = filePath;

        return viewModel;
    }

    public DownloadViewModel CreateDownloadViewModel(
        IVideo video,
        VideoDownloadPreference downloadPreference,
        string filePath
    )
    {
        var viewModel = services.GetRequiredService<DownloadViewModel>();

        viewModel.Video = video;
        viewModel.DownloadPreference = downloadPreference;
        viewModel.FilePath = filePath;

        return viewModel;
    }

    public DownloadMultipleSetupViewModel CreateDownloadMultipleSetupViewModel(
        string title,
        IReadOnlyList<IVideo> availableVideos,
        bool preselectVideos = true
    )
    {
        var viewModel = services.GetRequiredService<DownloadMultipleSetupViewModel>();

        viewModel.Title = title;
        viewModel.AvailableVideos = availableVideos;

        if (preselectVideos)
            viewModel.SelectedVideos.AddRange(availableVideos);

        return viewModel;
    }

    public DownloadSingleSetupViewModel CreateDownloadSingleSetupViewModel(
        IVideo video,
        IReadOnlyList<VideoDownloadOption> availableDownloadOptions
    )
    {
        var viewModel = services.GetRequiredService<DownloadSingleSetupViewModel>();

        viewModel.Video = video;
        viewModel.AvailableDownloadOptions = availableDownloadOptions;

        return viewModel;
    }

    public MessageBoxViewModel CreateMessageBoxViewModel(
        string title,
        string message,
        string? okButtonText,
        string? cancelButtonText
    )
    {
        var viewModel = services.GetRequiredService<MessageBoxViewModel>();

        viewModel.Title = title;
        viewModel.Message = message;
        viewModel.DefaultButtonText = okButtonText;
        viewModel.CancelButtonText = cancelButtonText;

        return viewModel;
    }

    public MessageBoxViewModel CreateMessageBoxViewModel(string title, string message)
    {
        var viewModel = services.GetRequiredService<MessageBoxViewModel>();

        viewModel.Title = title;
        viewModel.Message = message;

        return viewModel;
    }

    public SettingsViewModel CreateSettingsViewModel() =>
        services.GetRequiredService<SettingsViewModel>();
}


================================================
FILE: YoutubeDownloader/Framework/Window.cs
================================================
using System;
using Avalonia.Controls;

namespace YoutubeDownloader.Framework;

public class Window<TDataContext> : Window
{
    public new TDataContext DataContext
    {
        get =>
            base.DataContext is TDataContext dataContext
                ? dataContext
                : throw new InvalidCastException(
                    $"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
                );
        set => base.DataContext = value;
    }
}


================================================
FILE: YoutubeDownloader/Localization/Language.cs
================================================
namespace YoutubeDownloader.Localization;

public enum Language
{
    System,
    English,
    Ukrainian,
    German,
    French,
    Spanish,
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.English.cs
================================================
using System.Collections.Generic;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager
{
    private static readonly IReadOnlyDictionary<string, string> EnglishLocalization =
        new Dictionary<string, string>
        {
            // Dashboard
            [nameof(QueryWatermark)] = "URL or search query",
            [nameof(QueryTooltip)] =
                "Any valid YouTube URL or ID is accepted. Prepend a question mark (?) to perform search by text.",
            [nameof(ProcessQueryTooltip)] = "Process query (Enter)",
            [nameof(AuthTooltip)] = "Authentication",
            [nameof(SettingsTooltip)] = "Settings",
            [nameof(DashboardPlaceholder)] = """
                Copy-paste a **URL** or enter a **search query** to start downloading
                Press **Shift+Enter** to add multiple items
                """,
            [nameof(DownloadsFileColumnHeader)] = "File",
            [nameof(DownloadsStatusColumnHeader)] = "Status",
            [nameof(ContextMenuRemoveSuccessful)] = "Remove successful downloads",
            [nameof(ContextMenuRemoveInactive)] = "Remove inactive downloads",
            [nameof(ContextMenuRestartFailed)] = "Restart failed downloads",
            [nameof(ContextMenuCancelAll)] = "Cancel all downloads",
            [nameof(DownloadStatusEnqueued)] = "Pending...",
            [nameof(DownloadStatusCompleted)] = "Done",
            [nameof(DownloadStatusCanceled)] = "Canceled",
            [nameof(DownloadStatusFailed)] = "Failed",
            [nameof(ClickToCopyErrorTooltip)] = "Note: Click to copy this error message",
            [nameof(ShowFileTooltip)] = "Show file",
            [nameof(PlayTooltip)] = "Play",
            [nameof(CancelDownloadTooltip)] = "Cancel download",
            [nameof(RestartDownloadTooltip)] = "Restart download",
            // Settings
            [nameof(SettingsTitle)] = "Settings",
            [nameof(ThemeLabel)] = "Theme",
            [nameof(ThemeTooltip)] = "Preferred user interface theme",
            [nameof(LanguageLabel)] = "Language",
            [nameof(LanguageTooltip)] = "Preferred display language for the user interface",
            [nameof(AutoUpdateLabel)] = "Auto-update",
            [nameof(AutoUpdateTooltip)] = """
                Perform automatic updates on every launch.
                **Warning:** it's recommended to leave this option enabled to ensure that the app is compatible with the latest version of YouTube.
                """,
            [nameof(PersistAuthLabel)] = "Persist authentication",
            [nameof(PersistAuthTooltip)] = """
                Save authentication cookies to a file so that they can be persisted between sessions.
                **Warning**: although the cookies are stored with encryption, they may still be recovered by an attacker who has access to your system.
                """,
            [nameof(InjectAltLanguagesLabel)] = "Inject alternative languages",
            [nameof(InjectAltLanguagesTooltip)] =
                "Inject audio tracks in alternative languages (if available) into downloaded files",
            [nameof(InjectSubtitlesLabel)] = "Inject subtitles",
            [nameof(InjectSubtitlesTooltip)] =
                "Inject subtitles (if available) into downloaded files",
            [nameof(InjectTagsLabel)] = "Inject media tags",
            [nameof(InjectTagsTooltip)] = "Inject media tags (if available) into downloaded files",
            [nameof(SkipExistingFilesLabel)] = "Skip existing files",
            [nameof(SkipExistingFilesTooltip)] =
                "When downloading multiple videos, skip those that already have matching files in the output directory",
            [nameof(FileNameTemplateLabel)] = "File name template",
            [nameof(FileNameTemplateTooltip)] = """
                Template used for generating file names for downloaded videos.

                Available tokens:
                **$num** — video's position in the list (if applicable)
                **$id** — video ID
                **$title** — video title
                **$author** — video author
                """,
            [nameof(ParallelLimitLabel)] = "Parallel limit",
            [nameof(ParallelLimitTooltip)] = "How many downloads can be active at the same time",
            [nameof(FFmpegPathLabel)] = "FFmpeg path",
            [nameof(FFmpegPathTooltip)] =
                "Path to the FFmpeg executable. Leave empty to use auto-detection.",
            [nameof(FFmpegPathWatermark)] = "Auto-detect",
            [nameof(FFmpegPathResetTooltip)] = "Reset to auto-detection",
            [nameof(FFmpegPathBrowseTooltip)] = "Browse for FFmpeg executable",
            // Auth Setup
            [nameof(AuthenticationTitle)] = "Authentication",
            [nameof(AuthenticatedText)] = "You are currently authenticated",
            [nameof(LogOutButton)] = "Log out",
            [nameof(LoadingText)] = "Loading...",
            // Download Single Setup
            [nameof(CopyMenuItem)] = "Copy",
            [nameof(LiveLabel)] = "Live",
            [nameof(AudioLabel)] = "Audio",
            [nameof(FormatLabel)] = "Format",
            // Download Multiple Setup
            [nameof(ContainerLabel)] = "Container",
            [nameof(VideoQualityLabel)] = "Video quality",
            // Common buttons
            [nameof(CloseButton)] = "CLOSE",
            [nameof(DownloadButton)] = "DOWNLOAD",
            [nameof(CancelButton)] = "CANCEL",
            [nameof(SettingsButton)] = "SETTINGS",
            // Dialog messages
            [nameof(UkraineSupportTitle)] = "Thank you for supporting Ukraine!",
            [nameof(UkraineSupportMessage)] = """
                As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.

                Click LEARN MORE to find ways that you can help.
                """,
            [nameof(LearnMoreButton)] = "LEARN MORE",
            [nameof(UnstableBuildTitle)] = "Unstable build warning",
            [nameof(UnstableBuildMessage)] = """
                You're using a development build of {0}. These builds are not thoroughly tested and may contain bugs.

                Auto-updates are disabled for development builds.

                Click SEE RELEASES if you want to download a stable release instead.
                """,
            [nameof(SeeReleasesButton)] = "SEE RELEASES",
            [nameof(FFmpegMissingTitle)] = "FFmpeg is missing",
            [nameof(FFmpegMissingMessage)] = """
                FFmpeg is required for {0} to work. Please download it and make it available in the application directory or on the system PATH, or configure the location in settings.

                Alternatively, you can also download a version of {0} that has FFmpeg bundled with it. Look for release assets that are NOT marked as *.Bare.

                Click DOWNLOAD to go to the FFmpeg download page.
                """,
            [nameof(FFmpegPathMissingMessage)] = """
                FFmpeg is required for this app to work, but the configured path does not exist:
                {0}

                Please update the FFmpeg path in settings or clear it to use auto-detection.
                """,
            [nameof(FFmpegMissingSearchedLabel)] =
                "Searched for '{0}' in the following directories:",
            [nameof(NothingFoundTitle)] = "Nothing found",
            [nameof(NothingFoundMessage)] =
                "Couldn't find any videos based on the query or URL you provided",
            [nameof(ErrorTitle)] = "Error",
            [nameof(UpdateDownloadingMessage)] = "Downloading update to {0} v{1}...",
            [nameof(UpdateReadyMessage)] =
                "Update has been downloaded and will be installed when you exit",
            [nameof(UpdateInstallNowButton)] = "INSTALL NOW",
            [nameof(UpdateFailedMessage)] = "Failed to perform application update",
        };
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.French.cs
================================================
using System.Collections.Generic;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager
{
    private static readonly IReadOnlyDictionary<string, string> FrenchLocalization = new Dictionary<
        string,
        string
    >
    {
        // Dashboard
        [nameof(QueryWatermark)] = "URL ou requête de recherche",
        [nameof(QueryTooltip)] =
            "Toute URL ou ID YouTube valide est acceptée. Ajoutez un point d'interrogation (?) pour rechercher par texte.",
        [nameof(ProcessQueryTooltip)] = "Traiter la requête (Entrée)",
        [nameof(AuthTooltip)] = "Authentification",
        [nameof(SettingsTooltip)] = "Paramètres",
        [nameof(DashboardPlaceholder)] = """
            Collez une **URL** ou entrez une **requête de recherche** pour commencer
            Appuyez sur **Shift+Entrée** pour ajouter plusieurs éléments
            """,
        [nameof(DownloadsFileColumnHeader)] = "Fichier",
        [nameof(DownloadsStatusColumnHeader)] = "Statut",
        [nameof(ContextMenuRemoveSuccessful)] = "Supprimer les téléchargements réussis",
        [nameof(ContextMenuRemoveInactive)] = "Supprimer les téléchargements inactifs",
        [nameof(ContextMenuRestartFailed)] = "Relancer les téléchargements échoués",
        [nameof(ContextMenuCancelAll)] = "Annuler tous les téléchargements",
        [nameof(DownloadStatusEnqueued)] = "En attente...",
        [nameof(DownloadStatusCompleted)] = "Terminé",
        [nameof(DownloadStatusCanceled)] = "Annulé",
        [nameof(DownloadStatusFailed)] = "Échec",
        [nameof(ClickToCopyErrorTooltip)] = "Note : Cliquez pour copier ce message d'erreur",
        [nameof(ShowFileTooltip)] = "Afficher le fichier",
        [nameof(PlayTooltip)] = "Lire",
        [nameof(CancelDownloadTooltip)] = "Annuler le téléchargement",
        [nameof(RestartDownloadTooltip)] = "Relancer le téléchargement",
        // Settings
        [nameof(SettingsTitle)] = "Paramètres",
        [nameof(ThemeLabel)] = "Thème",
        [nameof(ThemeTooltip)] = "Thème d'interface préféré",
        [nameof(LanguageLabel)] = "Langue",
        [nameof(LanguageTooltip)] = "Langue d'affichage préférée pour l'interface utilisateur",
        [nameof(AutoUpdateLabel)] = "Mise à jour automatique",
        [nameof(AutoUpdateTooltip)] = """
            Effectuer des mises à jour automatiques à chaque démarrage.
            **Avertissement :** il est recommandé de laisser cette option activée pour assurer la compatibilité avec la dernière version de YouTube.
            """,
        [nameof(PersistAuthLabel)] = "Conserver l'authentification",
        [nameof(PersistAuthTooltip)] = """
            Enregistrer les cookies d'authentification dans un fichier pour les conserver entre les sessions.
            **Avertissement** : bien que les cookies soient stockés avec chiffrement, ils peuvent toujours être récupérés par un attaquant ayant accès à votre système.
            """,
        [nameof(InjectAltLanguagesLabel)] = "Injecter les langues alternatives",
        [nameof(InjectAltLanguagesTooltip)] =
            "Injecter des pistes audio en langues alternatives (si disponibles) dans les fichiers téléchargés",
        [nameof(InjectSubtitlesLabel)] = "Injecter les sous-titres",
        [nameof(InjectSubtitlesTooltip)] =
            "Injecter les sous-titres (si disponibles) dans les fichiers téléchargés",
        [nameof(InjectTagsLabel)] = "Injecter les balises média",
        [nameof(InjectTagsTooltip)] =
            "Injecter les balises média (si disponibles) dans les fichiers téléchargés",
        [nameof(SkipExistingFilesLabel)] = "Ignorer les fichiers existants",
        [nameof(SkipExistingFilesTooltip)] =
            "Lors du téléchargement de plusieurs vidéos, ignorer celles qui ont déjà des fichiers correspondants dans le répertoire de sortie",
        [nameof(FileNameTemplateLabel)] = "Modèle de nom de fichier",
        [nameof(FileNameTemplateTooltip)] = """
            Modèle utilisé pour générer les noms de fichiers des vidéos téléchargées.

            Jetons disponibles :
            **$num** — position de la vidéo dans la liste (si applicable)
            **$id** — ID de la vidéo
            **$title** — titre de la vidéo
            **$author** — auteur de la vidéo
            """,
        [nameof(ParallelLimitLabel)] = "Limite parallèle",
        [nameof(ParallelLimitTooltip)] =
            "Combien de téléchargements peuvent être actifs en même temps",
        [nameof(FFmpegPathLabel)] = "Chemin FFmpeg",
        [nameof(FFmpegPathTooltip)] =
            "Chemin vers l'exécutable FFmpeg. Laisser vide pour la détection automatique.",
        [nameof(FFmpegPathWatermark)] = "Auto",
        [nameof(FFmpegPathResetTooltip)] = "Réinitialiser la détection automatique",
        [nameof(FFmpegPathBrowseTooltip)] = "Parcourir l'exécutable FFmpeg",
        // Auth Setup
        [nameof(AuthenticationTitle)] = "Authentification",
        [nameof(AuthenticatedText)] = "Vous êtes actuellement authentifié",
        [nameof(LogOutButton)] = "Se déconnecter",
        [nameof(LoadingText)] = "Chargement...",
        // Download Single Setup
        [nameof(CopyMenuItem)] = "Copier",
        [nameof(LiveLabel)] = "En direct",
        [nameof(AudioLabel)] = "Audio",
        [nameof(FormatLabel)] = "Format",
        // Download Multiple Setup
        [nameof(ContainerLabel)] = "Conteneur",
        [nameof(VideoQualityLabel)] = "Qualité vidéo",
        // Common buttons
        [nameof(CloseButton)] = "FERMER",
        [nameof(DownloadButton)] = "TÉLÉCHARGER",
        [nameof(CancelButton)] = "ANNULER",
        [nameof(SettingsButton)] = "PARAMÈTRES",
        // Dialog messages
        [nameof(UkraineSupportTitle)] = "Merci de soutenir l'Ukraine !",
        [nameof(UkraineSupportMessage)] = """
            Alors que la Russie mène une guerre génocidaire contre mon pays, je suis reconnaissant envers tous ceux qui continuent à soutenir l'Ukraine dans notre combat pour la liberté.

            Cliquez sur EN SAVOIR PLUS pour trouver des moyens d'aider.
            """,
        [nameof(LearnMoreButton)] = "EN SAVOIR PLUS",
        [nameof(UnstableBuildTitle)] = "Avertissement : build instable",
        [nameof(UnstableBuildMessage)] = """
            Vous utilisez une version de développement de {0}. Ces versions ne sont pas rigoureusement testées et peuvent contenir des bugs.

            Les mises à jour automatiques sont désactivées pour les versions de développement.

            Cliquez sur VOIR LES VERSIONS pour télécharger une version stable.
            """,
        [nameof(SeeReleasesButton)] = "VOIR LES VERSIONS",
        [nameof(FFmpegMissingTitle)] = "FFmpeg est manquant",
        [nameof(FFmpegMissingMessage)] = """
            FFmpeg est requis pour que {0} fonctionne. Veuillez le télécharger et le rendre disponible dans le répertoire de l'application ou dans le PATH système, ou configurer son emplacement dans les paramètres.

            Alternativement, vous pouvez télécharger une version de {0} avec FFmpeg intégré. Cherchez les fichiers de version qui ne sont PAS marqués *.Bare.

            Cliquez sur TÉLÉCHARGER pour accéder à la page de téléchargement de FFmpeg.
            """,
        [nameof(FFmpegPathMissingMessage)] = """
            FFmpeg est requis pour cette application, mais le chemin configuré n'existe pas :
            {0}

            Veuillez mettre à jour le chemin FFmpeg dans les paramètres ou le vider pour utiliser la détection automatique.
            """,
        [nameof(FFmpegMissingSearchedLabel)] = "'{0}' recherché dans les répertoires suivants :",
        [nameof(NothingFoundTitle)] = "Rien trouvé",
        [nameof(NothingFoundMessage)] =
            "Impossible de trouver des vidéos correspondant à la requête ou l'URL fournie",
        [nameof(ErrorTitle)] = "Erreur",
        [nameof(UpdateDownloadingMessage)] = "Téléchargement de la mise à jour {0} v{1}...",
        [nameof(UpdateReadyMessage)] =
            "La mise à jour a été téléchargée et sera installée à la fermeture",
        [nameof(UpdateInstallNowButton)] = "INSTALLER MAINTENANT",
        [nameof(UpdateFailedMessage)] = "Échec de la mise à jour de l'application",
    };
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.German.cs
================================================
using System.Collections.Generic;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager
{
    private static readonly IReadOnlyDictionary<string, string> GermanLocalization = new Dictionary<
        string,
        string
    >
    {
        // Dashboard
        [nameof(QueryWatermark)] = "URL oder Suchanfrage",
        [nameof(QueryTooltip)] =
            "Jede gültige YouTube-URL oder -ID wird akzeptiert. Stellen Sie ein Fragezeichen (?) voran, um nach Text zu suchen.",
        [nameof(ProcessQueryTooltip)] = "Anfrage verarbeiten (Enter)",
        [nameof(AuthTooltip)] = "Authentifizierung",
        [nameof(SettingsTooltip)] = "Einstellungen",
        [nameof(DashboardPlaceholder)] = """
            **URL** einfügen oder **Suchanfrage** eingeben um den Download zu starten
            Drücken Sie **Shift+Enter** um mehrere Einträge hinzuzufügen
            """,
        [nameof(DownloadsFileColumnHeader)] = "Datei",
        [nameof(DownloadsStatusColumnHeader)] = "Status",
        [nameof(ContextMenuRemoveSuccessful)] = "Erfolgreiche Downloads entfernen",
        [nameof(ContextMenuRemoveInactive)] = "Inaktive Downloads entfernen",
        [nameof(ContextMenuRestartFailed)] = "Fehlgeschlagene Downloads neu starten",
        [nameof(ContextMenuCancelAll)] = "Alle Downloads abbrechen",
        [nameof(DownloadStatusEnqueued)] = "Ausstehend...",
        [nameof(DownloadStatusCompleted)] = "Fertig",
        [nameof(DownloadStatusCanceled)] = "Abgebrochen",
        [nameof(DownloadStatusFailed)] = "Fehlgeschlagen",
        [nameof(ClickToCopyErrorTooltip)] = "Hinweis: Klicken zum Kopieren der Fehlermeldung",
        [nameof(ShowFileTooltip)] = "Datei anzeigen",
        [nameof(PlayTooltip)] = "Abspielen",
        [nameof(CancelDownloadTooltip)] = "Download abbrechen",
        [nameof(RestartDownloadTooltip)] = "Download neu starten",
        // Settings
        [nameof(SettingsTitle)] = "Einstellungen",
        [nameof(ThemeLabel)] = "Design",
        [nameof(ThemeTooltip)] = "Bevorzugtes Oberflächendesign",
        [nameof(LanguageLabel)] = "Sprache",
        [nameof(LanguageTooltip)] = "Bevorzugte Anzeigesprache für die Benutzeroberfläche",
        [nameof(AutoUpdateLabel)] = "Automatische Updates",
        [nameof(AutoUpdateTooltip)] = """
            Automatische Updates bei jedem Start durchführen.
            **Hinweis:** Es wird empfohlen, diese Option aktiviert zu lassen, um die Kompatibilität mit der neuesten YouTube-Version zu gewährleisten.
            """,
        [nameof(PersistAuthLabel)] = "Authentifizierung speichern",
        [nameof(PersistAuthTooltip)] = """
            Authentifizierungs-Cookies in einer Datei speichern für sitzungsübergreifende Persistenz.
            **Warnung**: Die Cookies werden mit Verschlüsselung gespeichert, können aber dennoch von einem Angreifer mit Zugriff auf Ihr System wiederhergestellt werden.
            """,
        [nameof(InjectAltLanguagesLabel)] = "Alternative Sprachen einbetten",
        [nameof(InjectAltLanguagesTooltip)] =
            "Audiotracks in alternativen Sprachen (falls verfügbar) in heruntergeladene Dateien einbetten",
        [nameof(InjectSubtitlesLabel)] = "Untertitel einbetten",
        [nameof(InjectSubtitlesTooltip)] =
            "Untertitel (falls verfügbar) in heruntergeladene Dateien einbetten",
        [nameof(InjectTagsLabel)] = "Medien-Tags einbetten",
        [nameof(InjectTagsTooltip)] =
            "Medien-Tags (falls verfügbar) in heruntergeladene Dateien einbetten",
        [nameof(SkipExistingFilesLabel)] = "Vorhandene Dateien überspringen",
        [nameof(SkipExistingFilesTooltip)] =
            "Beim Herunterladen mehrerer Videos solche überspringen, für die bereits passende Dateien im Ausgabeverzeichnis vorhanden sind",
        [nameof(FileNameTemplateLabel)] = "Dateinamen-Vorlage",
        [nameof(FileNameTemplateTooltip)] = """
            Vorlage für die Generierung von Dateinamen heruntergeladener Videos.

            Verfügbare Token:
            **$num** — Position des Videos in der Liste (falls zutreffend)
            **$id** — Video-ID
            **$title** — Videotitel
            **$author** — Videoautor
            """,
        [nameof(ParallelLimitLabel)] = "Paralleles Limit",
        [nameof(ParallelLimitTooltip)] = "Wie viele Downloads gleichzeitig aktiv sein können",
        [nameof(FFmpegPathLabel)] = "FFmpeg-Pfad",
        [nameof(FFmpegPathTooltip)] =
            "Pfad zur FFmpeg-Programmdatei. Leer lassen für automatische Erkennung.",
        [nameof(FFmpegPathWatermark)] = "Auto",
        [nameof(FFmpegPathResetTooltip)] = "Zurücksetzen auf automatische Erkennung",
        [nameof(FFmpegPathBrowseTooltip)] = "FFmpeg-Programmdatei suchen",
        // Auth Setup
        [nameof(AuthenticationTitle)] = "Authentifizierung",
        [nameof(AuthenticatedText)] = "Sie sind derzeit authentifiziert",
        [nameof(LogOutButton)] = "Abmelden",
        [nameof(LoadingText)] = "Laden...",
        // Download Single Setup
        [nameof(CopyMenuItem)] = "Kopieren",
        [nameof(LiveLabel)] = "Live",
        [nameof(AudioLabel)] = "Audio",
        [nameof(FormatLabel)] = "Format",
        // Download Multiple Setup
        [nameof(ContainerLabel)] = "Container",
        [nameof(VideoQualityLabel)] = "Videoqualität",
        // Common buttons
        [nameof(CloseButton)] = "SCHLIESSEN",
        [nameof(DownloadButton)] = "HERUNTERLADEN",
        [nameof(CancelButton)] = "ABBRECHEN",
        [nameof(SettingsButton)] = "EINSTELLUNGEN",
        // Dialog messages
        [nameof(UkraineSupportTitle)] = "Danke für Ihre Unterstützung der Ukraine!",
        [nameof(UkraineSupportMessage)] = """
            Während Russland einen Vernichtungskrieg gegen mein Land führt, bin ich jedem dankbar, der weiterhin zur Ukraine in unserem Kampf für die Freiheit steht.

            Klicken Sie auf MEHR ERFAHREN um Wege zu finden, wie Sie helfen können.
            """,
        [nameof(LearnMoreButton)] = "MEHR ERFAHREN",
        [nameof(UnstableBuildTitle)] = "Warnung: Instabiler Build",
        [nameof(UnstableBuildMessage)] = """
            Sie verwenden einen Entwicklungs-Build von {0}. Diese Builds wurden nicht gründlich getestet und können Fehler enthalten.

            Automatische Updates sind für Entwicklungs-Builds deaktiviert.

            Klicken Sie auf RELEASES ANZEIGEN um stattdessen einen stabilen Release herunterzuladen.
            """,
        [nameof(SeeReleasesButton)] = "RELEASES ANZEIGEN",
        [nameof(FFmpegMissingTitle)] = "FFmpeg fehlt",
        [nameof(FFmpegMissingMessage)] = """
            FFmpeg wird benötigt damit {0} funktioniert. Bitte laden Sie es herunter und machen Sie es im Anwendungsverzeichnis oder im System-PATH verfügbar, oder konfigurieren Sie den Speicherort in den Einstellungen.

            Alternativ können Sie auch eine Version von {0} herunterladen, die FFmpeg enthält. Suchen Sie nach Release-Dateien, die NICHT mit *.Bare markiert sind.

            Klicken Sie auf HERUNTERLADEN um zur FFmpeg-Downloadseite zu gelangen.
            """,
        [nameof(FFmpegPathMissingMessage)] = """
            FFmpeg wird für diese App benötigt, aber der konfigurierte Pfad existiert nicht:
            {0}

            Bitte aktualisieren Sie den FFmpeg-Pfad in den Einstellungen oder löschen Sie ihn zur automatischen Erkennung.
            """,
        [nameof(FFmpegMissingSearchedLabel)] = "'{0}' wurde in folgenden Verzeichnissen gesucht:",
        [nameof(NothingFoundTitle)] = "Nichts gefunden",
        [nameof(NothingFoundMessage)] =
            "Es konnten keine Videos basierend auf der angegebenen Anfrage oder URL gefunden werden",
        [nameof(ErrorTitle)] = "Fehler",
        [nameof(UpdateDownloadingMessage)] = "Update für {0} v{1} wird heruntergeladen...",
        [nameof(UpdateReadyMessage)] =
            "Update wurde heruntergeladen und wird beim Beenden installiert",
        [nameof(UpdateInstallNowButton)] = "JETZT INSTALLIEREN",
        [nameof(UpdateFailedMessage)] = "Anwendungsupdate konnte nicht durchgeführt werden",
    };
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.Spanish.cs
================================================
using System.Collections.Generic;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager
{
    private static readonly IReadOnlyDictionary<string, string> SpanishLocalization =
        new Dictionary<string, string>
        {
            // Dashboard
            [nameof(QueryWatermark)] = "URL o consulta de búsqueda",
            [nameof(QueryTooltip)] =
                "Se acepta cualquier URL o ID de YouTube válido. Antepone un signo de interrogación (?) para buscar por texto.",
            [nameof(ProcessQueryTooltip)] = "Procesar consulta (Enter)",
            [nameof(AuthTooltip)] = "Autenticación",
            [nameof(SettingsTooltip)] = "Configuración",
            [nameof(DashboardPlaceholder)] = """
                Pega una **URL** o ingresa una **consulta de búsqueda** para comenzar
                Presiona **Shift+Enter** para agregar múltiples elementos
                """,
            [nameof(DownloadsFileColumnHeader)] = "Archivo",
            [nameof(DownloadsStatusColumnHeader)] = "Estado",
            [nameof(ContextMenuRemoveSuccessful)] = "Eliminar descargas exitosas",
            [nameof(ContextMenuRemoveInactive)] = "Eliminar descargas inactivas",
            [nameof(ContextMenuRestartFailed)] = "Reiniciar descargas fallidas",
            [nameof(ContextMenuCancelAll)] = "Cancelar todas las descargas",
            [nameof(DownloadStatusEnqueued)] = "Pendiente...",
            [nameof(DownloadStatusCompleted)] = "Listo",
            [nameof(DownloadStatusCanceled)] = "Cancelado",
            [nameof(DownloadStatusFailed)] = "Fallido",
            [nameof(ClickToCopyErrorTooltip)] = "Nota: Haz clic para copiar este mensaje de error",
            [nameof(ShowFileTooltip)] = "Mostrar archivo",
            [nameof(PlayTooltip)] = "Reproducir",
            [nameof(CancelDownloadTooltip)] = "Cancelar descarga",
            [nameof(RestartDownloadTooltip)] = "Reiniciar descarga",
            // Settings
            [nameof(SettingsTitle)] = "Configuración",
            [nameof(ThemeLabel)] = "Tema",
            [nameof(ThemeTooltip)] = "Tema de interfaz preferido",
            [nameof(LanguageLabel)] = "Idioma",
            [nameof(LanguageTooltip)] =
                "Idioma de visualización preferido para la interfaz de usuario",
            [nameof(AutoUpdateLabel)] = "Actualización automática",
            [nameof(AutoUpdateTooltip)] = """
                Realizar actualizaciones automáticas en cada inicio.
                **Advertencia:** se recomienda dejar esta opción habilitada para asegurar la compatibilidad con la última versión de YouTube.
                """,
            [nameof(PersistAuthLabel)] = "Conservar autenticación",
            [nameof(PersistAuthTooltip)] = """
                Guardar las cookies de autenticación en un archivo para persistirlas entre sesiones.
                **Advertencia**: aunque las cookies se almacenan con cifrado, un atacante con acceso a su sistema podría recuperarlas.
                """,
            [nameof(InjectAltLanguagesLabel)] = "Insertar idiomas alternativos",
            [nameof(InjectAltLanguagesTooltip)] =
                "Insertar pistas de audio en idiomas alternativos (si están disponibles) en los archivos descargados",
            [nameof(InjectSubtitlesLabel)] = "Insertar subtítulos",
            [nameof(InjectSubtitlesTooltip)] =
                "Insertar subtítulos (si están disponibles) en los archivos descargados",
            [nameof(InjectTagsLabel)] = "Insertar etiquetas multimedia",
            [nameof(InjectTagsTooltip)] =
                "Insertar etiquetas multimedia (si están disponibles) en los archivos descargados",
            [nameof(SkipExistingFilesLabel)] = "Omitir archivos existentes",
            [nameof(SkipExistingFilesTooltip)] =
                "Al descargar múltiples videos, omitir los que ya tengan archivos correspondientes en el directorio de salida",
            [nameof(FileNameTemplateLabel)] = "Plantilla de nombre de archivo",
            [nameof(FileNameTemplateTooltip)] = """
                Plantilla para generar nombres de archivo de los videos descargados.

                Tokens disponibles:
                **$num** — posición del video en la lista (si aplica)
                **$id** — ID del video
                **$title** — título del video
                **$author** — autor del video
                """,
            [nameof(ParallelLimitLabel)] = "Límite paralelo",
            [nameof(ParallelLimitTooltip)] =
                "Cuántas descargas pueden estar activas al mismo tiempo",
            [nameof(FFmpegPathLabel)] = "Ruta de FFmpeg",
            [nameof(FFmpegPathTooltip)] =
                "Ruta al ejecutable de FFmpeg. Dejar vacío para detección automática.",
            [nameof(FFmpegPathWatermark)] = "Auto",
            [nameof(FFmpegPathResetTooltip)] = "Restablecer detección automática",
            [nameof(FFmpegPathBrowseTooltip)] = "Buscar ejecutable de FFmpeg",
            // Auth Setup
            [nameof(AuthenticationTitle)] = "Autenticación",
            [nameof(AuthenticatedText)] = "Actualmente estás autenticado",
            [nameof(LogOutButton)] = "Cerrar sesión",
            [nameof(LoadingText)] = "Cargando...",
            // Download Single Setup
            [nameof(CopyMenuItem)] = "Copiar",
            [nameof(LiveLabel)] = "En vivo",
            [nameof(AudioLabel)] = "Audio",
            [nameof(FormatLabel)] = "Formato",
            // Download Multiple Setup
            [nameof(ContainerLabel)] = "Contenedor",
            [nameof(VideoQualityLabel)] = "Calidad de video",
            // Common buttons
            [nameof(CloseButton)] = "CERRAR",
            [nameof(DownloadButton)] = "DESCARGAR",
            [nameof(CancelButton)] = "CANCELAR",
            [nameof(SettingsButton)] = "AJUSTES",
            // Dialog messages
            [nameof(UkraineSupportTitle)] = "¡Gracias por apoyar a Ucrania!",
            [nameof(UkraineSupportMessage)] = """
                Mientras Rusia libra una guerra genocida contra mi país, estoy agradecido con todos los que continúan apoyando a Ucrania en nuestra lucha por la libertad.

                Haz clic en MÁS INFORMACIÓN para encontrar formas en que puedes ayudar.
                """,
            [nameof(LearnMoreButton)] = "MÁS INFORMACIÓN",
            [nameof(UnstableBuildTitle)] = "Advertencia: versión inestable",
            [nameof(UnstableBuildMessage)] = """
                Estás usando una versión de desarrollo de {0}. Estas versiones no han sido probadas exhaustivamente y pueden contener errores.

                Las actualizaciones automáticas están desactivadas para versiones de desarrollo.

                Haz clic en VER LANZAMIENTOS para descargar una versión estable.
                """,
            [nameof(SeeReleasesButton)] = "VER LANZAMIENTOS",
            [nameof(FFmpegMissingTitle)] = "Falta FFmpeg",
            [nameof(FFmpegMissingMessage)] = """
                FFmpeg es necesario para que {0} funcione. Descárgalo y ponlo disponible en el directorio de la aplicación o en el PATH del sistema, o configura la ubicación en los ajustes.

                Alternativamente, puedes descargar una versión de {0} que incluye FFmpeg. Busca los archivos de lanzamiento que NO estén marcados como *.Bare.

                Haz clic en DESCARGAR para ir a la página de descarga de FFmpeg.
                """,
            [nameof(FFmpegPathMissingMessage)] = """
                FFmpeg es necesario para esta aplicación, pero la ruta configurada no existe:
                {0}

                Por favor, actualiza la ruta de FFmpeg en los ajustes o bórrala para usar la detección automática.
                """,
            [nameof(FFmpegMissingSearchedLabel)] = "Se buscó '{0}' en los siguientes directorios:",
            [nameof(NothingFoundTitle)] = "Nada encontrado",
            [nameof(NothingFoundMessage)] =
                "No se encontraron videos basados en la consulta o URL proporcionada",
            [nameof(ErrorTitle)] = "Error",
            [nameof(UpdateDownloadingMessage)] = "Descargando actualización de {0} v{1}...",
            [nameof(UpdateReadyMessage)] =
                "La actualización se ha descargado y se instalará al salir",
            [nameof(UpdateInstallNowButton)] = "INSTALAR AHORA",
            [nameof(UpdateFailedMessage)] = "Error al realizar la actualización de la aplicación",
        };
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.Ukrainian.cs
================================================
using System.Collections.Generic;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager
{
    private static readonly IReadOnlyDictionary<string, string> UkrainianLocalization =
        new Dictionary<string, string>
        {
            // Dashboard
            [nameof(QueryWatermark)] = "URL або пошуковий запит",
            [nameof(QueryTooltip)] =
                "Приймається будь-який дійсний URL або ID YouTube. Додайте знак питання (?) для пошуку за текстом.",
            [nameof(ProcessQueryTooltip)] = "Виконати запит (Enter)",
            [nameof(AuthTooltip)] = "Автентифікація",
            [nameof(SettingsTooltip)] = "Налаштування",
            [nameof(DashboardPlaceholder)] = """
                Вставте **URL** або введіть **пошуковий запит** для завантаження
                Натисніть **Shift+Enter**, щоб додати декілька елементів
                """,
            [nameof(DownloadsFileColumnHeader)] = "Файл",
            [nameof(DownloadsStatusColumnHeader)] = "Статус",
            [nameof(ContextMenuRemoveSuccessful)] = "Видалити успішні завантаження",
            [nameof(ContextMenuRemoveInactive)] = "Видалити неактивні завантаження",
            [nameof(ContextMenuRestartFailed)] = "Перезапустити невдалі завантаження",
            [nameof(ContextMenuCancelAll)] = "Скасувати всі завантаження",
            [nameof(DownloadStatusEnqueued)] = "В черзі...",
            [nameof(DownloadStatusCompleted)] = "Готово",
            [nameof(DownloadStatusCanceled)] = "Скасовано",
            [nameof(DownloadStatusFailed)] = "Помилка",
            [nameof(ClickToCopyErrorTooltip)] = "Примітка: натисніть, щоб скопіювати повідомлення",
            [nameof(ShowFileTooltip)] = "Показати файл",
            [nameof(PlayTooltip)] = "Відтворити",
            [nameof(CancelDownloadTooltip)] = "Скасувати завантаження",
            [nameof(RestartDownloadTooltip)] = "Перезапустити завантаження",
            // Settings
            [nameof(SettingsTitle)] = "Налаштування",
            [nameof(ThemeLabel)] = "Тема",
            [nameof(ThemeTooltip)] = "Бажана тема інтерфейсу",
            [nameof(LanguageLabel)] = "Мова",
            [nameof(LanguageTooltip)] = "Бажана мова відображення інтерфейсу користувача",
            [nameof(AutoUpdateLabel)] = "Авто-оновлення",
            [nameof(AutoUpdateTooltip)] = """
                Виконувати автоматичні оновлення при кожному запуску.
                **Увага:** рекомендується залишити цю опцію увімкненою для сумісності з останньою версією YouTube.
                """,
            [nameof(PersistAuthLabel)] = "Зберігати автентифікацію",
            [nameof(PersistAuthTooltip)] = """
                Зберігати файли cookie у файлі для збереження між сеансами.
                **Увага**: хоча cookies зберігаються із шифруванням, зловмисник з доступом до вашої системи може їх відновити.
                """,
            [nameof(InjectAltLanguagesLabel)] = "Вставляти альтернативні мови",
            [nameof(InjectAltLanguagesTooltip)] =
                "Вставляти аудіодоріжки альтернативними мовами (якщо доступні) у завантажені файли",
            [nameof(InjectSubtitlesLabel)] = "Вставляти субтитри",
            [nameof(InjectSubtitlesTooltip)] =
                "Вставляти субтитри (якщо доступні) у завантажені файли",
            [nameof(InjectTagsLabel)] = "Вставляти медіатеги",
            [nameof(InjectTagsTooltip)] = "Вставляти медіатеги (якщо доступні) у завантажені файли",
            [nameof(SkipExistingFilesLabel)] = "Пропускати наявні файли",
            [nameof(SkipExistingFilesTooltip)] =
                "При завантаженні кількох відео пропускати ті, для яких вже є відповідні файли",
            [nameof(FileNameTemplateLabel)] = "Шаблон імені файлу",
            [nameof(FileNameTemplateTooltip)] = """
                Шаблон для генерації імен файлів завантажених відео.

                Доступні токени:
                **$num** — позиція відео у списку (якщо застосовно)
                **$id** — ідентифікатор відео
                **$title** — назва відео
                **$author** — автор відео
                """,
            [nameof(ParallelLimitLabel)] = "Ліміт паралелізації",
            [nameof(ParallelLimitTooltip)] = "Скільки завантажень може бути активними одночасно",
            [nameof(FFmpegPathLabel)] = "Шлях FFmpeg",
            [nameof(FFmpegPathTooltip)] =
                "Шлях до виконуваного файлу FFmpeg. Залиште порожнім для автоматичного визначення.",
            [nameof(FFmpegPathWatermark)] = "Авто",
            [nameof(FFmpegPathResetTooltip)] = "Скинути до автоматичного визначення",
            [nameof(FFmpegPathBrowseTooltip)] = "Вибрати файл FFmpeg",
            // Auth Setup
            [nameof(AuthenticationTitle)] = "Автентифікація",
            [nameof(AuthenticatedText)] = "Ви автентифіковані",
            [nameof(LogOutButton)] = "Вийти",
            [nameof(LoadingText)] = "Завантаження...",
            // Download Single Setup
            [nameof(CopyMenuItem)] = "Копіювати",
            [nameof(LiveLabel)] = "Живе",
            [nameof(AudioLabel)] = "Аудіо",
            [nameof(FormatLabel)] = "Формат",
            // Download Multiple Setup
            [nameof(ContainerLabel)] = "Контейнер",
            [nameof(VideoQualityLabel)] = "Якість відео",
            // Common buttons
            [nameof(CloseButton)] = "ЗАКРИТИ",
            [nameof(DownloadButton)] = "ЗАВАНТАЖИТИ",
            [nameof(CancelButton)] = "СКАСУВАТИ",
            [nameof(SettingsButton)] = "НАЛАШТУВАННЯ",
            // Dialog messages
            [nameof(UkraineSupportTitle)] = "Дякуємо за підтримку України!",
            [nameof(UkraineSupportMessage)] = """
                Поки Росія веде геноцидну війну проти моєї країни, я вдячний кожному, хто продовжує підтримувати Україну у нашій боротьбі за свободу.

                Натисніть ДІЗНАТИСЬ БІЛЬШЕ, щоб знайти способи допомогти.
                """,
            [nameof(LearnMoreButton)] = "ДІЗНАТИСЬ БІЛЬШЕ",
            [nameof(UnstableBuildTitle)] = "Попередження про нестабільну збірку",
            [nameof(UnstableBuildMessage)] = """
                Ви використовуєте збірку розробки {0}. Ці збірки не пройшли ретельного тестування та можуть містити помилки.

                Авто-оновлення вимкнено для збірок розробки.

                Натисніть ПЕРЕГЛЯНУТИ РЕЛІЗИ, щоб завантажити стабільний реліз.
                """,
            [nameof(SeeReleasesButton)] = "ПЕРЕГЛЯНУТИ РЕЛІЗИ",
            [nameof(FFmpegMissingTitle)] = "FFmpeg відсутній",
            [nameof(FFmpegMissingMessage)] = """
                FFmpeg потрібен для роботи {0}. Завантажте його та зробіть доступним у каталозі програми або у системному PATH, або вкажіть розташування у налаштуваннях.

                Альтернативно, ви можете завантажити версію {0} з вбудованим FFmpeg. Шукайте ресурси релізу, які НЕ позначені як *.Bare.

                Натисніть ЗАВАНТАЖИТИ, щоб перейти на сторінку завантаження FFmpeg.
                """,
            [nameof(FFmpegPathMissingMessage)] = """
                FFmpeg потрібен для роботи програми, але вказаний шлях не існує:
                {0}

                Будь ласка, оновіть шлях FFmpeg у налаштуваннях або очистіть його для автовизначення.
                """,
            [nameof(FFmpegMissingSearchedLabel)] = "Шукали '{0}' у таких директоріях:",
            [nameof(NothingFoundTitle)] = "Нічого не знайдено",
            [nameof(NothingFoundMessage)] = "Не вдалося знайти відео за вказаним запитом або URL",
            [nameof(ErrorTitle)] = "Помилка",
            [nameof(UpdateDownloadingMessage)] = "Завантаження оновлення {0} v{1}...",
            [nameof(UpdateReadyMessage)] = "Оновлення завантажено та буде встановлено після виходу",
            [nameof(UpdateInstallNowButton)] = "ВСТАНОВИТИ ЗАРАЗ",
            [nameof(UpdateFailedMessage)] = "Не вдалося виконати оновлення програми",
        };
}


================================================
FILE: YoutubeDownloader/Localization/LocalizationManager.cs
================================================
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.ComponentModel;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.Localization;

public partial class LocalizationManager : ObservableObject, IDisposable
{
    private readonly DisposableCollector _eventRoot = new();

    public LocalizationManager(SettingsService settingsService)
    {
        _eventRoot.Add(
            settingsService.WatchProperty(
                o => o.Language,
                () => Language = settingsService.Language,
                true
            )
        );

        _eventRoot.Add(
            this.WatchProperty(
                o => o.Language,
                () =>
                {
                    foreach (var propertyName in EnglishLocalization.Keys)
                        OnPropertyChanged(propertyName);
                }
            )
        );
    }

    [ObservableProperty]
    public partial Language Language { get; set; } = Language.System;

    private string Get([CallerMemberName] string? key = null)
    {
        if (string.IsNullOrWhiteSpace(key))
            return string.Empty;

        var localization = Language switch
        {
            Language.System =>
                CultureInfo.CurrentUICulture.ThreeLetterISOLanguageName.ToLowerInvariant() switch
                {
                    "ukr" => UkrainianLocalization,
                    "deu" => GermanLocalization,
                    "fra" => FrenchLocalization,
                    "spa" => SpanishLocalization,
                    _ => EnglishLocalization,
                },
            Language.Ukrainian => UkrainianLocalization,
            Language.German => GermanLocalization,
            Language.French => FrenchLocalization,
            Language.Spanish => SpanishLocalization,
            _ => EnglishLocalization,
        };

        if (
            localization.TryGetValue(key, out var value)
            // English is used as a fallback
            || EnglishLocalization.TryGetValue(key, out value)
        )
        {
            return value;
        }

        return $"Missing localization for '{key}'";
    }

    public void Dispose() => _eventRoot.Dispose();
}

public partial class LocalizationManager
{
    // ---- Dashboard ----

    public string QueryWatermark => Get();
    public string QueryTooltip => Get();
    public string ProcessQueryTooltip => Get();
    public string AuthTooltip => Get();
    public string SettingsTooltip => Get();
    public string DashboardPlaceholder => Get();
    public string DownloadsFileColumnHeader => Get();
    public string DownloadsStatusColumnHeader => Get();
    public string ContextMenuRemoveSuccessful => Get();
    public string ContextMenuRemoveInactive => Get();
    public string ContextMenuRestartFailed => Get();
    public string ContextMenuCancelAll => Get();
    public string DownloadStatusEnqueued => Get();
    public string DownloadStatusCompleted => Get();
    public string DownloadStatusCanceled => Get();
    public string DownloadStatusFailed => Get();
    public string ClickToCopyErrorTooltip => Get();
    public string ShowFileTooltip => Get();
    public string PlayTooltip => Get();
    public string CancelDownloadTooltip => Get();
    public string RestartDownloadTooltip => Get();

    // ---- Settings ----

    public string SettingsTitle => Get();
    public string ThemeLabel => Get();
    public string ThemeTooltip => Get();
    public string LanguageLabel => Get();
    public string LanguageTooltip => Get();
    public string AutoUpdateLabel => Get();
    public string AutoUpdateTooltip => Get();
    public string PersistAuthLabel => Get();
    public string PersistAuthTooltip => Get();
    public string InjectAltLanguagesLabel => Get();
    public string InjectAltLanguagesTooltip => Get();
    public string InjectSubtitlesLabel => Get();
    public string InjectSubtitlesTooltip => Get();
    public string InjectTagsLabel => Get();
    public string InjectTagsTooltip => Get();
    public string SkipExistingFilesLabel => Get();
    public string SkipExistingFilesTooltip => Get();
    public string FileNameTemplateLabel => Get();
    public string FileNameTemplateTooltip => Get();
    public string ParallelLimitLabel => Get();
    public string ParallelLimitTooltip => Get();
    public string FFmpegPathLabel => Get();
    public string FFmpegPathTooltip => Get();
    public string FFmpegPathWatermark => Get();
    public string FFmpegPathResetTooltip => Get();
    public string FFmpegPathBrowseTooltip => Get();

    // ---- Auth Setup ----

    public string AuthenticationTitle => Get();
    public string AuthenticatedText => Get();
    public string LogOutButton => Get();
    public string LoadingText => Get();

    // ---- Download Single Setup ----

    public string CopyMenuItem => Get();
    public string LiveLabel => Get();
    public string AudioLabel => Get();
    public string FormatLabel => Get();

    // ---- Download Multiple Setup ----

    public string ContainerLabel => Get();
    public string VideoQualityLabel => Get();

    // ---- Common buttons ----

    public string CloseButton => Get();
    public string DownloadButton => Get();
    public string CancelButton => Get();
    public string SettingsButton => Get();

    // ---- Dialog messages ----

    public string UkraineSupportTitle => Get();
    public string UkraineSupportMessage => Get();
    public string LearnMoreButton => Get();
    public string UnstableBuildTitle => Get();
    public string UnstableBuildMessage => Get();
    public string SeeReleasesButton => Get();
    public string FFmpegMissingTitle => Get();
    public string FFmpegMissingMessage => Get();
    public string FFmpegPathMissingMessage => Get();
    public string FFmpegMissingSearchedLabel => Get();
    public string NothingFoundTitle => Get();
    public string NothingFoundMessage => Get();
    public string ErrorTitle => Get();
    public string UpdateDownloadingMessage => Get();
    public string UpdateReadyMessage => Get();
    public string UpdateInstallNowButton => Get();
    public string UpdateFailedMessage => Get();
}


================================================
FILE: YoutubeDownloader/Program.cs
================================================
using System;
using System.Reflection;
using Avalonia;
using Avalonia.WebView.Desktop;
using YoutubeDownloader.Utils;

namespace YoutubeDownloader;

public static class Program
{
    private static Assembly Assembly { get; } = Assembly.GetExecutingAssembly();

    public static string Name { get; } = Assembly.GetName().Name ?? "YoutubeDownloader";

    public static Version Version { get; } = Assembly.GetName().Version ?? new Version(0, 0, 0);

    public static string VersionString { get; } = Version.ToString(3);

    public static bool IsDevelopmentBuild { get; } = Version.Major is <= 0 or >= 999;

    public static string ProjectUrl { get; } = "https://github.com/Tyrrrz/YoutubeDownloader";

    public static string ProjectReleasesUrl { get; } = $"{ProjectUrl}/releases";

    public static AppBuilder BuildAvaloniaApp() =>
        AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace().UseDesktopWebView();

    [STAThread]
    public static int Main(string[] args)
    {
        // Build and run the app
        var builder = BuildAvaloniaApp();

        try
        {
            return builder.StartWithClassicDesktopLifetime(args);
        }
        catch (Exception ex)
        {
            if (OperatingSystem.IsWindows())
                _ = NativeMethods.Windows.MessageBox(0, ex.ToString(), "Fatal Error", 0x10);

            throw;
        }
        finally
        {
            // Clean up after application shutdown
            if (builder.Instance is IDisposable disposableApp)
                disposableApp.Dispose();
        }
    }
}


================================================
FILE: YoutubeDownloader/Publish-MacOSBundle.ps1
================================================
param(
    [Parameter(Mandatory=$true)]
    [string]$PublishDirPath,

    [Parameter(Mandatory=$true)]
    [string]$IconsFilePath,

    [Parameter(Mandatory=$true)]
    [string]$FullVersion,

    [Parameter(Mandatory=$true)]
    [string]$ShortVersion
)

$ErrorActionPreference = "Stop"

# Setup paths
$tempDirPath = Join-Path $PublishDirPath "../publish-macos-app-temp"
$bundleName = "YoutubeDownloader.app"
$bundleDirPath = Join-Path $tempDirPath $bundleName
$contentsDirPath = Join-Path $bundleDirPath "Contents"
$macosDirPath = Join-Path $contentsDirPath "MacOS"
$resourcesDirPath = Join-Path $contentsDirPath "Resources"

try {
    # Initialize the bundle's directory structure
    New-Item -Path $bundleDirPath -ItemType Directory -Force
    New-Item -Path $contentsDirPath -ItemType Directory -Force
    New-Item -Path $macosDirPath -ItemType Directory -Force
    New-Item -Path $resourcesDirPath -ItemType Directory -Force

    # Copy icons into the .app's Resources folder
    Copy-Item -Path $IconsFilePath -Destination (Join-Path $resourcesDirPath "AppIcon.icns") -Force

    # Generate the Info.plist metadata file with the app information
    $plistContent = @"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>CFBundleDisplayName</key>
    <string>YoutubeDownloader</string>
    <key>CFBundleName</key>
    <string>YoutubeDownloader</string>
    <key>CFBundleExecutable</key>
    <string>YoutubeDownloader</string>
    <key>NSHumanReadableCopyright</key>
    <string>© Oleksii Holub</string>
    <key>CFBundleIdentifier</key>
    <string>me.Tyrrrz.YoutubeDownloader</string>
    <key>CFBundleSpokenName</key>
    <string>YoutubeDownloader</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon</string>
    <key>CFBundleIconName</key>
    <string>AppIcon</string>
    <key>CFBundleVersion</key>
    <string>$FullVersion</string>
    <key>CFBundleShortVersionString</key>
    <string>$ShortVersion</string>
    <key>NSHighResolutionCapable</key>
    <true />
    <key>CFBundlePackageType</key>
    <string>APPL</string>
  </dict>
</plist>
"@

    Set-Content -Path (Join-Path $contentsDirPath "Info.plist") -Value $plistContent

    # Delete the previous bundle if it exists
    if (Test-Path (Join-Path $PublishDirPath $bundleName)) {
        Remove-Item -Path (Join-Path $PublishDirPath $bundleName) -Recurse -Force
    }

    # Move all files from the publish directory into the MacOS directory
    Get-ChildItem -Path $PublishDirPath | ForEach-Object {
        Move-Item -Path $_.FullName -Destination $macosDirPath -Force
    }

    # Move the final bundle into the publish directory for upload
    Move-Item -Path $bundleDirPath -Destination $PublishDirPath -Force
}
finally {
    # Clean up the temporary directory
    Remove-Item -Path $tempDirPath -Recurse -Force
}

================================================
FILE: YoutubeDownloader/Services/SettingsService.AuthCookiesEncryptionConverter.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.Services;

public partial class SettingsService
{
    private class AuthCookiesEncryptionConverter : JsonConverter<IReadOnlyList<Cookie>?>
    {
        private static readonly Lazy<byte[]> Key = new(() =>
            Rfc2898DeriveBytes.Pbkdf2(
                Encoding.UTF8.GetBytes(Environment.TryGetMachineId() ?? string.Empty),
                Encoding.UTF8.GetBytes(ThisAssembly.Project.EncryptionSalt),
                600_000,
                HashAlgorithmName.SHA256,
                16
            )
        );

        public override IReadOnlyList<Cookie>? Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options
        )
        {
            if (reader.TokenType != JsonTokenType.String)
                return null;

            var value = reader.GetString();
            if (string.IsNullOrWhiteSpace(value))
                return null;

            try
            {
                var encryptedData = Convert.FromHexString(value);
                var cookieData = new byte[encryptedData.AsSpan(28).Length];

                // Layout: nonce (12 bytes) | tag (16 bytes) | cipher
                using var aes = new AesGcm(Key.Value, 16);
                aes.Decrypt(
                    encryptedData.AsSpan(0, 12),
                    encryptedData.AsSpan(28),
                    encryptedData.AsSpan(12, 16),
                    cookieData
                );

                return JsonSerializer
                    .Deserialize<IReadOnlyList<CookieData>>(cookieData)
                    ?.Select(c => new Cookie(c.Name, c.Value, c.Path, c.Domain))
                    .ToArray();
            }
            catch (Exception ex)
                when (ex
                        is FormatException
                            or CryptographicException
                            or ArgumentException
                            or IndexOutOfRangeException
                            or JsonException
                )
            {
                return null;
            }
        }

        public override void Write(
            Utf8JsonWriter writer,
            IReadOnlyList<Cookie>? value,
            JsonSerializerOptions options
        )
        {
            if (value is null || value.Count == 0)
            {
                writer.WriteNullValue();
                return;
            }

            var cookieData = Encoding.UTF8.GetBytes(
                JsonSerializer.Serialize(
                    value.Select(c => new CookieData(c.Name, c.Value, c.Path, c.Domain))
                )
            );

            var encryptedData = new byte[28 + cookieData.Length];

            // Nonce
            RandomNumberGenerator.Fill(encryptedData.AsSpan(0, 12));

            // Layout: nonce (12 bytes) | tag (16 bytes) | cipher
            using var aes = new AesGcm(Key.Value, 16);
            aes.Encrypt(
                encryptedData.AsSpan(0, 12),
                cookieData,
                encryptedData.AsSpan(28),
                encryptedData.AsSpan(12, 16)
            );

            writer.WriteStringValue(Convert.ToHexStringLower(encryptedData));
        }

        private record CookieData(string Name, string Value, string Path, string Domain);
    }
}


================================================
FILE: YoutubeDownloader/Services/SettingsService.cs
================================================
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using Cogwheel;
using CommunityToolkit.Mvvm.ComponentModel;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using Container = YoutubeExplode.Videos.Streams.Container;

namespace YoutubeDownloader.Services;

[ObservableObject]
public partial class SettingsService()
    : SettingsBase(StartOptions.Current.SettingsPath, SerializerContext.Default)
{
    [ObservableProperty]
    public partial bool IsUkraineSupportMessageEnabled { get; set; } = true;

    [ObservableProperty]
    public partial ThemeVariant Theme { get; set; }

    [ObservableProperty]
    public partial Language Language { get; set; }

    [ObservableProperty]
    public partial bool IsAutoUpdateEnabled { get; set; } = true;

    [ObservableProperty]
    public partial bool IsAuthPersisted { get; set; } = true;

    [ObservableProperty]
    public partial string? FFmpegFilePath { get; set; }

    [ObservableProperty]
    public partial bool ShouldInjectLanguageSpecificAudioStreams { get; set; } = true;

    [ObservableProperty]
    public partial bool ShouldInjectSubtitles { get; set; } = true;

    [ObservableProperty]
    public partial bool ShouldInjectTags { get; set; } = true;

    [ObservableProperty]
    public partial bool ShouldSkipExistingFiles { get; set; }

    [ObservableProperty]
    public partial string FileNameTemplate { get; set; } = "$title";

    [ObservableProperty]
    public partial int ParallelLimit { get; set; } = 2;

    [ObservableProperty]
    [JsonConverter(typeof(AuthCookiesEncryptionConverter))]
    public partial IReadOnlyList<Cookie>? LastAuthCookies { get; set; }

    [ObservableProperty]
    [JsonConverter(typeof(ContainerJsonConverter))]
    public partial Container LastContainer { get; set; } = Container.Mp4;

    [ObservableProperty]
    public partial VideoQualityPreference LastVideoQualityPreference { get; set; } =
        VideoQualityPreference.Highest;

    public override void Save()
    {
        // Clear the cookies if they are not supposed to be persisted
        var lastAuthCookies = LastAuthCookies;
        if (!IsAuthPersisted)
            LastAuthCookies = null;

        base.Save();

        LastAuthCookies = lastAuthCookies;
    }
}

public partial class SettingsService
{
    private class ContainerJsonConverter : JsonConverter<Container>
    {
        public override Container Read(
            ref Utf8JsonReader reader,
            Type typeToConvert,
            JsonSerializerOptions options
        )
        {
            Container? result = null;

            if (reader.TokenType == JsonTokenType.StartObject)
            {
                while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
                {
                    if (
                        reader.TokenType == JsonTokenType.PropertyName
                        && reader.GetString() == "Name"
                        && reader.Read()
                        && reader.TokenType == JsonTokenType.String
                    )
                    {
                        var name = reader.GetString();
                        if (!string.IsNullOrWhiteSpace(name))
                            result = new Container(name);
                    }
                }
            }

            return result
                ?? throw new InvalidOperationException(
                    $"Invalid JSON for type '{typeToConvert.FullName}'."
                );
        }

        public override void Write(
            Utf8JsonWriter writer,
            Container value,
            JsonSerializerOptions options
        )
        {
            writer.WriteStartObject();
            writer.WriteString("Name", value.Name);
            writer.WriteEndObject();
        }
    }
}

public partial class SettingsService
{
    [JsonSerializable(typeof(SettingsService))]
    private partial class SerializerContext : JsonSerializerContext;
}


================================================
FILE: YoutubeDownloader/Services/UpdateService.cs
================================================
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Onova;
using Onova.Exceptions;
using Onova.Services;
using YoutubeDownloader.Core.Downloading;

namespace YoutubeDownloader.Services;

public class UpdateService(SettingsService settingsService) : IDisposable
{
    private readonly IUpdateManager? _updateManager = OperatingSystem.IsWindows()
        ? new UpdateManager(
            new GithubPackageResolver(
                "Tyrrrz",
                "YoutubeDownloader",
                // Examples:
                // YoutubeDownloader.win-arm64.zip
                // YoutubeDownloader.win-x64.zip
                // YoutubeDownloader.linux-x64.zip
                // YoutubeDownloader.Bare.linux-x64.zip
                FFmpeg.IsBundled()
                    ? $"YoutubeDownloader.{RuntimeInformation.RuntimeIdentifier}.zip"
                    : $"YoutubeDownloader.Bare.{RuntimeInformation.RuntimeIdentifier}.zip"
            ),
            new ZipPackageExtractor()
        )
        : null;

    private Version? _updateVersion;
    private bool _updatePrepared;
    private bool _updaterLaunched;

    public async Task<Version?> CheckForUpdatesAsync()
    {
        if (_updateManager is null)
            return null;

        if (!settingsService.IsAutoUpdateEnabled)
            return null;

        var check = await _updateManager.CheckForUpdatesAsync();
        return check.CanUpdate ? check.LastVersion : null;
    }

    public async Task PrepareUpdateAsync(Version version)
    {
        if (_updateManager is null)
            return;

        if (!settingsService.IsAutoUpdateEnabled)
            return;

        try
        {
            await _updateManager.PrepareUpdateAsync(_updateVersion = version);
            _updatePrepared = true;
        }
        catch (UpdaterAlreadyLaunchedException)
        {
            // Ignore race conditions
        }
        catch (LockFileNotAcquiredException)
        {
            // Ignore race conditions
        }
    }

    public void FinalizeUpdate(bool needRestart)
    {
        if (_updateManager is null)
            return;

        if (!settingsService.IsAutoUpdateEnabled)
            return;

        if (_updateVersion is null || !_updatePrepared || _updaterLaunched)
            return;

        try
        {
            _updateManager.LaunchUpdater(_updateVersion, needRestart);
            _updaterLaunched = true;
        }
        catch (UpdaterAlreadyLaunchedException)
        {
            // Ignore race conditions
        }
        catch (LockFileNotAcquiredException)
        {
            // Ignore race conditions
        }
    }

    public void Dispose() => _updateManager?.Dispose();
}


================================================
FILE: YoutubeDownloader/StartOptions.cs
================================================
using System;
using System.IO;

namespace YoutubeDownloader;

public partial class StartOptions
{
    public required string SettingsPath { get; init; }
}

public partial class StartOptions
{
    public static StartOptions Current { get; } =
        new()
        {
            SettingsPath =
                Environment.GetEnvironmentVariable("YOUTUBEDOWNLOADER_SETTINGS_PATH") is { } path
                && !string.IsNullOrWhiteSpace(path)
                    ? Path.EndsInDirectorySeparator(path) || Directory.Exists(path)
                        ? Path.Combine(path, "Settings.dat")
                        : path
                    : Path.Combine(AppContext.BaseDirectory, "Settings.dat"),
        };
}


================================================
FILE: YoutubeDownloader/Utils/Disposable.cs
================================================
using System;

namespace YoutubeDownloader.Utils;

internal class Disposable(Action dispose) : IDisposable
{
    public static IDisposable Create(Action dispose) => new Disposable(dispose);

    public void Dispose() => dispose();
}


================================================
FILE: YoutubeDownloader/Utils/DisposableCollector.cs
================================================
using System;
using System.Collections.Generic;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.Utils;

internal class DisposableCollector : IDisposable
{
    private readonly object _lock = new();
    private readonly List<IDisposable> _items = [];

    public void Add(IDisposable item)
    {
        lock (_lock)
        {
            _items.Add(item);
        }
    }

    public void Dispose()
    {
        lock (_lock)
        {
            _items.DisposeAll();
            _items.Clear();
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/AvaloniaExtensions.cs
================================================
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.VisualTree;

namespace YoutubeDownloader.Utils.Extensions;

internal static class AvaloniaExtensions
{
    extension(IApplicationLifetime lifetime)
    {
        public Window? TryGetMainWindow() =>
            lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime
                ? desktopLifetime.MainWindow
                : null;

        public TopLevel? TryGetTopLevel() =>
            lifetime.TryGetMainWindow()
            ?? (lifetime as ISingleViewApplicationLifetime)?.MainView?.GetVisualRoot() as TopLevel;

        public bool TryShutdown(int exitCode = 0)
        {
            if (lifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
            {
                return desktopLifetime.TryShutdown(exitCode);
            }

            if (lifetime is IControlledApplicationLifetime controlledLifetime)
            {
                controlledLifetime.Shutdown(exitCode);
                return true;
            }

            return false;
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/DirectoryExtensions.cs
================================================
using System.IO;

namespace YoutubeDownloader.Utils.Extensions;

internal static class DirectoryExtensions
{
    extension(Directory)
    {
        public static void CreateDirectoryForFile(string filePath)
        {
            var dirPath = Path.GetDirectoryName(filePath);
            if (string.IsNullOrWhiteSpace(dirPath))
                return;

            Directory.CreateDirectory(dirPath);
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/DisposableExtensions.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;

namespace YoutubeDownloader.Utils.Extensions;

internal static class DisposableExtensions
{
    extension(IEnumerable<IDisposable> disposables)
    {
        public void DisposeAll()
        {
            var exceptions = default(List<Exception>);

            foreach (var disposable in disposables)
            {
                try
                {
                    disposable.Dispose();
                }
                catch (Exception ex)
                {
                    (exceptions ??= []).Add(ex);
                }
            }

            if (exceptions?.Any() == true)
                throw new AggregateException(exceptions);
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/EnvironmentExtensions.cs
================================================
using System;
using System.IO;

namespace YoutubeDownloader.Utils.Extensions;

internal static class EnvironmentExtensions
{
    extension(Environment)
    {
        public static string? TryGetMachineId()
        {
            // Windows: stable GUID written during OS installation
            if (OperatingSystem.IsWindows())
            {
                try
                {
                    using var regKey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(
                        @"SOFTWARE\Microsoft\Cryptography"
                    );
                    if (
                        regKey?.GetValue("MachineGuid") is string guid
                        && !string.IsNullOrWhiteSpace(guid)
                    )
                        return guid;
                }
                catch { }
            }
            else
            {
                // Unix: /etc/machine-id (set once by systemd at first boot)
                foreach (var path in new[] { "/etc/machine-id", "/var/lib/dbus/machine-id" })
                {
                    try
                    {
                        var id = File.ReadAllText(path).Trim();
                        if (!string.IsNullOrWhiteSpace(id))
                            return id;
                    }
                    catch { }
                }
            }

            // Last-resort fallback
            try
            {
                return Environment.MachineName;
            }
            catch
            {
                return null;
            }
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/NotifyPropertyChangedExtensions.cs
================================================
using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Reflection;

namespace YoutubeDownloader.Utils.Extensions;

internal static class NotifyPropertyChangedExtensions
{
    extension<TOwner>(TOwner owner)
        where TOwner : INotifyPropertyChanged
    {
        public IDisposable WatchProperty<TProperty>(
            Expression<Func<TOwner, TProperty>> propertyExpression,
            Action callback,
            bool watchInitialValue = false
        )
        {
            var memberExpression = propertyExpression.Body as MemberExpression;
            if (memberExpression?.Member is not PropertyInfo property)
                throw new ArgumentException("Provided expression must reference a property.");

            void OnPropertyChanged(object? sender, PropertyChangedEventArgs args)
            {
                if (
                    string.IsNullOrWhiteSpace(args.PropertyName)
                    || string.Equals(args.PropertyName, property.Name, StringComparison.Ordinal)
                )
                {
                    callback();
                }
            }

            owner.PropertyChanged += OnPropertyChanged;

            if (watchInitialValue)
                callback();

            return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);
        }

        public IDisposable WatchAllProperties(Action callback, bool watchInitialValues = false)
        {
            void OnPropertyChanged(object? sender, PropertyChangedEventArgs args) => callback();
            owner.PropertyChanged += OnPropertyChanged;

            if (watchInitialValues)
                callback();

            return Disposable.Create(() => owner.PropertyChanged -= OnPropertyChanged);
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/PathExtensions.cs
================================================
using System.IO;

namespace YoutubeDownloader.Utils.Extensions;

internal static class PathExtensions
{
    extension(Path)
    {
        public static string EnsureUniqueFilePath(string baseFilePath, int maxRetries = 100)
        {
            if (!File.Exists(baseFilePath))
                return baseFilePath;

            var baseDirPath = Path.GetDirectoryName(baseFilePath);
            var baseFileNameWithoutExtension = Path.GetFileNameWithoutExtension(baseFilePath);
            var baseFileExtension = Path.GetExtension(baseFilePath);

            for (var i = 1; i <= maxRetries; i++)
            {
                var fileName = $"{baseFileNameWithoutExtension} ({i}){baseFileExtension}";
                var filePath = !string.IsNullOrWhiteSpace(baseDirPath)
                    ? Path.Combine(baseDirPath, fileName)
                    : fileName;

                if (!File.Exists(filePath))
                    return filePath;
            }

            return baseFilePath;
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/Extensions/ProcessExtensions.cs
================================================
using System.Collections.Generic;
using System.Diagnostics;

namespace YoutubeDownloader.Utils.Extensions;

internal static class ProcessExtensions
{
    extension(Process)
    {
        public static void Start(string path, IReadOnlyList<string>? arguments = null)
        {
            using var process = new Process();
            process.StartInfo = new ProcessStartInfo(path);

            if (arguments is not null)
            {
                foreach (var argument in arguments)
                    process.StartInfo.ArgumentList.Add(argument);
            }

            process.Start();
        }

        public static void StartShellExecute(string path, IReadOnlyList<string>? arguments = null)
        {
            using var process = new Process();
            process.StartInfo = new ProcessStartInfo(path) { UseShellExecute = true };

            if (arguments is not null)
            {
                foreach (var argument in arguments)
                    process.StartInfo.ArgumentList.Add(argument);
            }

            process.Start();
        }
    }
}


================================================
FILE: YoutubeDownloader/Utils/NativeMethods.cs
================================================
using System.Runtime.InteropServices;

namespace YoutubeDownloader.Utils;

internal static class NativeMethods
{
    public static class Windows
    {
        [DllImport("user32.dll", SetLastError = true)]
        public static extern int MessageBox(nint hWnd, string text, string caption, uint type);
    }
}


================================================
FILE: YoutubeDownloader/Utils/ResizableSemaphore.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace YoutubeDownloader.Utils;

// Like a regular semaphore, but the max count can be changed at any point
internal partial class ResizableSemaphore : IDisposable
{
    private readonly object _lock = new();
    private readonly Queue<TaskCompletionSource> _waiters = new();
    private readonly CancellationTokenSource _cts = new();

    private bool _isDisposed;
    private int _maxCount = int.MaxValue;
    private int _count;

    public int MaxCount
    {
        get
        {
            lock (_lock)
            {
                return _maxCount;
            }
        }
        set
        {
            lock (_lock)
            {
                _maxCount = value;
                Refresh();
            }
        }
    }

    private void Refresh()
    {
        lock (_lock)
        {
            // Provide access to pending waiters, as long as max count allows
            while (_count < MaxCount && _waiters.TryDequeue(out var waiter))
            {
                // Don't increment the count if the waiter has already been
                // completed before (most likely by getting canceled).
                if (waiter.TrySetResult())
                    _count++;
            }
        }
    }

    public async Task<IDisposable> AcquireAsync(CancellationToken cancellationToken = default)
    {
        if (_isDisposed)
            throw new ObjectDisposedException(GetType().Name);

        var waiter = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

        await using (_cts.Token.Register(() => waiter.TrySetCanceled(_cts.Token)))
        await using (cancellationToken.Register(() => waiter.TrySetCanceled(cancellationToken)))
        {
            // Add the waiter to the queue
            lock (_lock)
            {
                _waiters.Enqueue(waiter);
                Refresh();
            }

            // Wait until this waiter has been given access
            await waiter.Task;

            return new AcquiredAccess(this);
        }
    }

    private void Release()
    {
        lock (_lock)
        {
            _count--;
            Refresh();
        }
    }

    public void Dispose()
    {
        if (!_isDisposed)
        {
            _cts.Cancel();
            _cts.Dispose();
        }

        _isDisposed = true;
    }
}

internal partial class ResizableSemaphore
{
    private class AcquiredAccess(ResizableSemaphore semaphore) : IDisposable
    {
        private bool _isDisposed;

        public void Dispose()
        {
            if (!_isDisposed)
            {
                semaphore.Release();
            }

            _isDisposed = true;
        }
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Gress;
using Gress.Completable;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Core.Resolving;
using YoutubeDownloader.Core.Tagging;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;
using YoutubeExplode.Exceptions;

namespace YoutubeDownloader.ViewModels.Components;

public partial class DashboardViewModel : ViewModelBase
{
    private readonly ViewModelManager _viewModelManager;
    private readonly SnackbarManager _snackbarManager;
    private readonly DialogManager _dialogManager;
    private readonly SettingsService _settingsService;

    private readonly DisposableCollector _eventRoot = new();
    private readonly ResizableSemaphore _downloadSemaphore = new();
    private readonly AutoResetProgressMuxer _progressMuxer;

    public DashboardViewModel(
        ViewModelManager viewModelManager,
        SnackbarManager snackbarManager,
        DialogManager dialogManager,
        LocalizationManager localizationManager,
        SettingsService settingsService
    )
    {
        _viewModelManager = viewModelManager;
        _snackbarManager = snackbarManager;
        _dialogManager = dialogManager;
        LocalizationManager = localizationManager;
        _settingsService = settingsService;

        _progressMuxer = Progress.CreateMuxer().WithAutoReset();

        _eventRoot.Add(
            _settingsService.WatchProperty(
                o => o.ParallelLimit,
                () => _downloadSemaphore.MaxCount = _settingsService.ParallelLimit,
                true
            )
        );

        _eventRoot.Add(
            Progress.WatchProperty(
                o => o.Current,
                () => OnPropertyChanged(nameof(IsProgressIndeterminate))
            )
        );
    }

    public LocalizationManager LocalizationManager { get; }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]
    [NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
    [NotifyCanExecuteChangedFor(nameof(ShowAuthSetupCommand))]
    [NotifyCanExecuteChangedFor(nameof(ShowSettingsCommand))]
    public partial bool IsBusy { get; set; }

    public ProgressContainer<Percentage> Progress { get; } = new();

    public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(ProcessQueryCommand))]
    public partial string? Query { get; set; }

    public ObservableCollection<DownloadViewModel> Downloads { get; } = [];

    private bool CanShowAuthSetup() => !IsBusy;

    [RelayCommand(CanExecute = nameof(CanShowAuthSetup))]
    private async Task ShowAuthSetupAsync() =>
        await _dialogManager.ShowDialogAsync(_viewModelManager.CreateAuthSetupViewModel());

    private bool CanShowSettings() => !IsBusy;

    [RelayCommand(CanExecute = nameof(CanShowSettings))]
    private async Task ShowSettingsAsync() =>
        await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());

    private async void EnqueueDownload(DownloadViewModel download, int position = 0)
    {
        Downloads.Insert(position, download);
        var progress = _progressMuxer.CreateInput();

        try
        {
            using var downloader = new VideoDownloader(_settingsService.LastAuthCookies);
            var tagInjector = new MediaTagInjector();

            using var access = await _downloadSemaphore.AcquireAsync(download.CancellationToken);

            download.Status = DownloadStatus.Started;

            var downloadOption =
                download.DownloadOption
                ?? await downloader.GetBestDownloadOptionAsync(
                    download.Video!.Id,
                    download.DownloadPreference!,
                    _settingsService.ShouldInjectLanguageSpecificAudioStreams,
                    download.CancellationToken
                );

            await downloader.DownloadVideoAsync(
                download.FilePath!,
                download.Video!,
                downloadOption,
                _settingsService.ShouldInjectSubtitles,
                _settingsService.FFmpegFilePath,
                download.Progress.Merge(progress),
                download.CancellationToken
            );

            if (_settingsService.ShouldInjectTags)
            {
                try
                {
                    await tagInjector.InjectTagsAsync(
                        download.FilePath!,
                        download.Video!,
                        download.CancellationToken
                    );
                }
                catch
                {
                    // Media tagging is not critical
                }
            }

            download.Status = DownloadStatus.Completed;
        }
        catch (Exception ex)
        {
            try
            {
                // Delete the incompletely downloaded file
                if (!string.IsNullOrWhiteSpace(download.FilePath))
                    File.Delete(download.FilePath);
            }
            catch
            {
                // Ignore
            }

            download.Status =
                ex is OperationCanceledException ? DownloadStatus.Canceled : DownloadStatus.Failed;

            // Short error message for YouTube-related errors, full for others
            download.ErrorMessage = ex is YoutubeExplodeException ? ex.Message : ex.ToString();
        }
        finally
        {
            progress.ReportCompletion();
            download.Dispose();
        }
    }

    private bool CanProcessQuery() => !IsBusy && !string.IsNullOrWhiteSpace(Query);

    [RelayCommand(CanExecute = nameof(CanProcessQuery))]
    private async Task ProcessQueryAsync()
    {
        if (string.IsNullOrWhiteSpace(Query))
            return;

        IsBusy = true;

        // Small weight so as to not offset any existing download operations
        var progress = _progressMuxer.CreateInput(0.01);

        try
        {
            using var resolver = new QueryResolver(_settingsService.LastAuthCookies);

            // Split queries by newlines
            var queries = Query.Split(
                '\n',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries
            );

            // Process individual queries
            var queryResults = new List<QueryResult>();
            foreach (var (i, query) in queries.Index())
            {
                try
                {
                    queryResults.Add(await resolver.ResolveAsync(query));
                }
                // If it's not the only query in the list, don't interrupt the process
                // and report the error via an async notification instead of a sync dialog.
                // https://github.com/Tyrrrz/YoutubeDownloader/issues/563
                catch (YoutubeExplodeException ex)
                    when (ex is VideoUnavailableException or PlaylistUnavailableException
                        && queries.Length > 1
                    )
                {
                    _snackbarManager.Notify(ex.Message);
                }

                progress.Report(Percentage.FromFraction((i + 1.0) / queries.Length));
            }

            // Aggregate results
            var queryResult = QueryResult.Aggregate(queryResults);

            // Single video result
            if (queryResult.Videos.Count == 1)
            {
                var video = queryResult.Videos.Single();

                using var downloader = new VideoDownloader(_settingsService.LastAuthCookies);

                var downloadOptions = await downloader.GetDownloadOptionsAsync(
                    video.Id,
                    _settingsService.ShouldInjectLanguageSpecificAudioStreams
                );

                var download = await _dialogManager.ShowDialogAsync(
                    _viewModelManager.CreateDownloadSingleSetupViewModel(video, downloadOptions)
                );

                if (download is null)
                    return;

                EnqueueDownload(download);

                Query = "";
            }
            // Multiple videos
            else if (queryResult.Videos.Count > 1)
            {
                var downloads = await _dialogManager.ShowDialogAsync(
                    _viewModelManager.CreateDownloadMultipleSetupViewModel(
                        queryResult.Title,
                        queryResult.Videos,
                        // Pre-select videos if they come from a single query and not from search
                        queryResult.Kind
                            is not QueryResultKind.Search
                                and not QueryResultKind.Aggregate
                    )
                );

                if (downloads is null)
                    return;

                foreach (var download in downloads)
                    EnqueueDownload(download);

                Query = "";
            }
            // No videos found
            else
            {
                await _dialogManager.ShowDialogAsync(
                    _viewModelManager.CreateMessageBoxViewModel(
                        LocalizationManager.NothingFoundTitle,
                        LocalizationManager.NothingFoundMessage
                    )
                );
            }
        }
        catch (Exception ex)
        {
            await _dialogManager.ShowDialogAsync(
                _viewModelManager.CreateMessageBoxViewModel(
                    LocalizationManager.ErrorTitle,
                    // Short error message for YouTube-related errors, full for others
                    ex is YoutubeExplodeException
                        ? ex.Message
                        : ex.ToString()
                )
            );
        }
        finally
        {
            progress.ReportCompletion();
            IsBusy = false;
        }
    }

    private void RemoveDownload(DownloadViewModel download)
    {
        Downloads.Remove(download);
        download.CancelCommand.Execute(null);
        download.Dispose();
    }

    [RelayCommand]
    private void RemoveSuccessfulDownloads()
    {
        foreach (var download in Downloads.ToArray())
        {
            if (download.Status == DownloadStatus.Completed)
                RemoveDownload(download);
        }
    }

    [RelayCommand]
    private void RemoveInactiveDownloads()
    {
        foreach (var download in Downloads.ToArray())
        {
            if (
                download.Status
                is DownloadStatus.Completed
                    or DownloadStatus.Failed
                    or DownloadStatus.Canceled
            )
                RemoveDownload(download);
        }
    }

    [RelayCommand]
    private void RestartDownload(DownloadViewModel download)
    {
        var position = Math.Max(0, Downloads.IndexOf(download));
        RemoveDownload(download);

        var newDownload = download.DownloadOption is not null
            ? _viewModelManager.CreateDownloadViewModel(
                download.Video!,
                download.DownloadOption,
                download.FilePath!
            )
            : _viewModelManager.CreateDownloadViewModel(
                download.Video!,
                download.DownloadPreference!,
                download.FilePath!
            );

        EnqueueDownload(newDownload, position);
    }

    [RelayCommand]
    private void RestartFailedDownloads()
    {
        foreach (var download in Downloads.ToArray())
        {
            if (download.Status == DownloadStatus.Failed)
                RestartDownload(download);
        }
    }

    [RelayCommand]
    private void CancelAllDownloads()
    {
        foreach (var download in Downloads)
            download.CancelCommand.Execute(null);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            CancelAllDownloads();

            _eventRoot.Dispose();
            _downloadSemaphore.Dispose();
        }

        base.Dispose(disposing);
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Components/DownloadStatus.cs
================================================
namespace YoutubeDownloader.ViewModels.Components;

public enum DownloadStatus
{
    Enqueued,
    Started,
    Completed,
    Failed,
    Canceled,
}


================================================
FILE: YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Gress;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;
using YoutubeExplode.Videos;

namespace YoutubeDownloader.ViewModels.Components;

public partial class DownloadViewModel : ViewModelBase
{
    private readonly ViewModelManager _viewModelManager;
    private readonly DialogManager _dialogManager;

    private readonly DisposableCollector _eventRoot = new();
    private readonly CancellationTokenSource _cancellationTokenSource = new();

    private bool _isDisposed;

    public DownloadViewModel(
        ViewModelManager viewModelManager,
        DialogManager dialogManager,
        LocalizationManager localizationManager
    )
    {
        _viewModelManager = viewModelManager;
        _dialogManager = dialogManager;
        LocalizationManager = localizationManager;

        _eventRoot.Add(
            Progress.WatchProperty(
                o => o.Current,
                () => OnPropertyChanged(nameof(IsProgressIndeterminate))
            )
        );
    }

    public LocalizationManager LocalizationManager { get; }

    [ObservableProperty]
    public partial IVideo? Video { get; set; }

    [ObservableProperty]
    public partial VideoDownloadOption? DownloadOption { get; set; }

    [ObservableProperty]
    public partial VideoDownloadPreference? DownloadPreference { get; set; }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(FileName))]
    public partial string? FilePath { get; set; }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsCanceledOrFailed))]
    [NotifyCanExecuteChangedFor(nameof(CancelCommand))]
    [NotifyCanExecuteChangedFor(nameof(ShowFileCommand))]
    [NotifyCanExecuteChangedFor(nameof(OpenFileCommand))]
    public partial DownloadStatus Status { get; set; } = DownloadStatus.Enqueued;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(CopyErrorMessageCommand))]
    public partial string? ErrorMessage { get; set; }

    public CancellationToken CancellationToken => _cancellationTokenSource.Token;

    public string? FileName => Path.GetFileName(FilePath);

    public ProgressContainer<Percentage> Progress { get; } = new();

    public bool IsProgressIndeterminate => Progress.Current.Fraction is <= 0 or >= 1;

    public bool IsCanceledOrFailed => Status is DownloadStatus.Canceled or DownloadStatus.Failed;

    private bool CanCancel() => Status is DownloadStatus.Enqueued or DownloadStatus.Started;

    [RelayCommand(CanExecute = nameof(CanCancel))]
    private void Cancel()
    {
        if (_isDisposed)
            return;

        _cancellationTokenSource.Cancel();
    }

    private bool CanShowFile() =>
        Status == DownloadStatus.Completed
        // This only works on Windows currently
        && OperatingSystem.IsWindows();

    [RelayCommand(CanExecute = nameof(CanShowFile))]
    private async Task ShowFileAsync()
    {
        if (string.IsNullOrWhiteSpace(FilePath))
            return;

        try
        {
            // Navigate to the file in Windows Explorer
            Process.Start("explorer", ["/select,", FilePath]);
        }
        catch (Exception ex)
        {
            await _dialogManager.ShowDialogAsync(
                _viewModelManager.CreateMessageBoxViewModel(
                    LocalizationManager.ErrorTitle,
                    ex.Message
                )
            );
        }
    }

    private bool CanOpenFile() => Status == DownloadStatus.Completed;

    [RelayCommand(CanExecute = nameof(CanOpenFile))]
    private async Task OpenFileAsync()
    {
        if (string.IsNullOrWhiteSpace(FilePath))
            return;

        try
        {
            Process.StartShellExecute(FilePath);
        }
        catch (Exception ex)
        {
            await _dialogManager.ShowDialogAsync(
                _viewModelManager.CreateMessageBoxViewModel(
                    LocalizationManager.ErrorTitle,
                    ex.Message
                )
            );
        }
    }

    [RelayCommand]
    private async Task CopyErrorMessageAsync()
    {
        if (string.IsNullOrWhiteSpace(ErrorMessage))
            return;

        if (Application.Current?.ApplicationLifetime?.TryGetTopLevel()?.Clipboard is { } clipboard)
            await clipboard.SetTextAsync(ErrorMessage);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _eventRoot.Dispose();
            _cancellationTokenSource.Dispose();

            _isDisposed = true;
        }

        base.Dispose(disposing);
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.ViewModels.Dialogs;

public class AuthSetupViewModel : DialogViewModelBase
{
    private readonly SettingsService _settingsService;
    private readonly DisposableCollector _eventRoot = new();

    public AuthSetupViewModel(
        LocalizationManager localizationManager,
        SettingsService settingsService
    )
    {
        LocalizationManager = localizationManager;
        _settingsService = settingsService;

        _eventRoot.Add(
            _settingsService.WatchProperty(
                o => o.LastAuthCookies,
                () =>
                {
                    OnPropertyChanged(nameof(Cookies));
                    OnPropertyChanged(nameof(IsAuthenticated));
                }
            )
        );
    }

    public LocalizationManager LocalizationManager { get; }

    public IReadOnlyList<Cookie>? Cookies
    {
        get => _settingsService.LastAuthCookies;
        set => _settingsService.LastAuthCookies = value;
    }

    public bool IsAuthenticated =>
        Cookies?.Any() == true
        &&
        // None of the '__SECURE' cookies should be expired
        Cookies
            .Where(c => c.Name.StartsWith("__SECURE", StringComparison.OrdinalIgnoreCase))
            .All(c => !c.Expired && c.Expires.ToUniversalTime() > DateTime.UtcNow);

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _eventRoot.Dispose();
        }

        base.Dispose(disposing);
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils.Extensions;
using YoutubeDownloader.ViewModels.Components;
using YoutubeExplode.Videos;
using YoutubeExplode.Videos.Streams;

namespace YoutubeDownloader.ViewModels.Dialogs;

public partial class DownloadMultipleSetupViewModel(
    ViewModelManager viewModelManager,
    DialogManager dialogManager,
    LocalizationManager localizationManager,
    SettingsService settingsService
) : DialogViewModelBase<IReadOnlyList<DownloadViewModel>>
{
    public LocalizationManager LocalizationManager { get; } = localizationManager;

    [ObservableProperty]
    public partial string? Title { get; set; }

    [ObservableProperty]
    public partial IReadOnlyList<IVideo>? AvailableVideos { get; set; }

    [ObservableProperty]
    public partial Container SelectedContainer { get; set; } = Container.Mp4;

    [ObservableProperty]
    public partial VideoQualityPreference SelectedVideoQualityPreference { get; set; } =
        VideoQualityPreference.Highest;

    public ObservableCollection<IVideo> SelectedVideos { get; } = [];

    public IReadOnlyList<Container> AvailableContainers { get; } =
    [Container.Mp4, Container.WebM, Container.Mp3, new("ogg")];

    public IReadOnlyList<VideoQualityPreference> AvailableVideoQualityPreferences { get; } =
        // Without .AsEnumerable(), the below line throws a compile-time error starting with .NET SDK v9.0.200
        Enum.GetValues<VideoQualityPreference>().AsEnumerable().Reverse().ToArray();

    [RelayCommand]
    private void Initialize()
    {
        SelectedContainer = settingsService.LastContainer;
        SelectedVideoQualityPreference = settingsService.LastVideoQualityPreference;
        SelectedVideos.CollectionChanged += (_, _) => ConfirmCommand.NotifyCanExecuteChanged();
    }

    [RelayCommand]
    private async Task CopyTitleAsync()
    {
        if (Application.Current?.ApplicationLifetime?.TryGetTopLevel()?.Clipboard is { } clipboard)
            await clipboard.SetTextAsync(Title);
    }

    private bool CanConfirm() => SelectedVideos.Any();

    [RelayCommand(CanExecute = nameof(CanConfirm))]
    private async Task ConfirmAsync()
    {
        var dirPath = await dialogManager.PromptDirectoryPathAsync();
        if (string.IsNullOrWhiteSpace(dirPath))
            return;

        var downloads = new List<DownloadViewModel>();
        for (var i = 0; i < SelectedVideos.Count; i++)
        {
            var video = SelectedVideos[i];

            var baseFilePath = Path.Combine(
                dirPath,
                FileNameTemplate.Apply(
                    settingsService.FileNameTemplate,
                    video,
                    SelectedContainer,
                    (i + 1).ToString().PadLeft(SelectedVideos.Count.ToString().Length, '0')
                )
            );

            if (settingsService.ShouldSkipExistingFiles && File.Exists(baseFilePath))
                continue;

            var filePath = Path.EnsureUniqueFilePath(baseFilePath);

            // Download does not start immediately, so lock in the file path to avoid conflicts
            Directory.CreateDirectoryForFile(filePath);
            await File.WriteAllBytesAsync(filePath, []);

            downloads.Add(
                viewModelManager.CreateDownloadViewModel(
                    video,
                    new VideoDownloadPreference(SelectedContainer, SelectedVideoQualityPreference),
                    filePath
                )
            );
        }

        settingsService.LastContainer = SelectedContainer;
        settingsService.LastVideoQualityPreference = SelectedVideoQualityPreference;

        Close(downloads);
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs
================================================
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils.Extensions;
using YoutubeDownloader.ViewModels.Components;
using YoutubeExplode.Videos;

namespace YoutubeDownloader.ViewModels.Dialogs;

public partial class DownloadSingleSetupViewModel(
    ViewModelManager viewModelManager,
    DialogManager dialogManager,
    LocalizationManager localizationManager,
    SettingsService settingsService
) : DialogViewModelBase<DownloadViewModel>
{
    public LocalizationManager LocalizationManager { get; } = localizationManager;

    [ObservableProperty]
    public partial IVideo? Video { get; set; }

    [ObservableProperty]
    public partial IReadOnlyList<VideoDownloadOption>? AvailableDownloadOptions { get; set; }

    [ObservableProperty]
    public partial VideoDownloadOption? SelectedDownloadOption { get; set; }

    [RelayCommand]
    private void Initialize()
    {
        SelectedDownloadOption = AvailableDownloadOptions?.FirstOrDefault(o =>
            o.Container == settingsService.LastContainer
        );
    }

    [RelayCommand]
    private async Task CopyTitleAsync()
    {
        if (Application.Current?.ApplicationLifetime?.TryGetTopLevel()?.Clipboard is { } clipboard)
            await clipboard.SetTextAsync(Video?.Title);
    }

    [RelayCommand]
    private async Task ConfirmAsync()
    {
        if (Video is null || SelectedDownloadOption is null)
            return;

        var container = SelectedDownloadOption.Container;

        var filePath = await dialogManager.PromptSaveFilePathAsync(
            [
                new FilePickerFileType($"{container.Name} file")
                {
                    Patterns = [$"*.{container.Name}"],
                },
            ],
            FileNameTemplate.Apply(settingsService.FileNameTemplate, Video, container)
        );

        if (string.IsNullOrWhiteSpace(filePath))
            return;

        // Download does not start immediately, so lock in the file path to avoid conflicts
        Directory.CreateDirectoryForFile(filePath);
        await File.WriteAllBytesAsync(filePath, []);

        settingsService.LastContainer = container;

        Close(viewModelManager.CreateDownloadViewModel(Video, SelectedDownloadOption, filePath));
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/Dialogs/MessageBoxViewModel.cs
================================================
using CommunityToolkit.Mvvm.ComponentModel;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;

namespace YoutubeDownloader.ViewModels.Dialogs;

public partial class MessageBoxViewModel : DialogViewModelBase
{
    public MessageBoxViewModel(LocalizationManager localizationManager)
    {
        LocalizationManager = localizationManager;

        DefaultButtonText = LocalizationManager.CloseButton;
        CancelButtonText = LocalizationManager.CancelButton;
    }

    public LocalizationManager LocalizationManager { get; }

    [ObservableProperty]
    public partial string? Title { get; set; }

    [ObservableProperty]
    public partial string? Message { get; set; }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsDefaultButtonVisible))]
    [NotifyPropertyChangedFor(nameof(ButtonsCount))]
    public partial string? DefaultButtonText { get; set; }

    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsCancelButtonVisible))]
    [NotifyPropertyChangedFor(nameof(ButtonsCount))]
    public partial string? CancelButtonText { get; set; }

    public bool IsDefaultButtonVisible => !string.IsNullOrWhiteSpace(DefaultButtonText);

    public bool IsCancelButtonVisible => !string.IsNullOrWhiteSpace(CancelButtonText);

    public int ButtonsCount => (IsDefaultButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);
}


================================================
FILE: YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.Input;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils;
using YoutubeDownloader.Utils.Extensions;

namespace YoutubeDownloader.ViewModels.Dialogs;

public partial class SettingsViewModel : DialogViewModelBase
{
    private readonly DialogManager _dialogManager;
    private readonly SettingsService _settingsService;

    private readonly DisposableCollector _eventRoot = new();

    public SettingsViewModel(
        DialogManager dialogManager,
        LocalizationManager localizationManager,
        SettingsService settingsService
    )
    {
        _dialogManager = dialogManager;
        LocalizationManager = localizationManager;
        _settingsService = settingsService;

        _eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));
    }

    public LocalizationManager LocalizationManager { get; }

    public IReadOnlyList<ThemeVariant> AvailableThemes { get; } = Enum.GetValues<ThemeVariant>();

    public ThemeVariant Theme
    {
        get => _settingsService.Theme;
        set => _settingsService.Theme = value;
    }

    public IReadOnlyList<Language> AvailableLanguages { get; } = Enum.GetValues<Language>();

    public Language Language
    {
        get => _settingsService.Language;
        set => _settingsService.Language = value;
    }

    public bool IsAutoUpdateEnabled
    {
        get => _settingsService.IsAutoUpdateEnabled;
        set => _settingsService.IsAutoUpdateEnabled = value;
    }

    public bool IsAuthPersisted
    {
        get => _settingsService.IsAuthPersisted;
        set => _settingsService.IsAuthPersisted = value;
    }

    public string? FFmpegFilePath
    {
        get => _settingsService.FFmpegFilePath;
        set => _settingsService.FFmpegFilePath = !string.IsNullOrWhiteSpace(value) ? value : null;
    }

    public bool ShouldInjectLanguageSpecificAudioStreams
    {
        get => _settingsService.ShouldInjectLanguageSpecificAudioStreams;
        set => _settingsService.ShouldInjectLanguageSpecificAudioStreams = value;
    }

    public bool ShouldInjectSubtitles
    {
        get => _settingsService.ShouldInjectSubtitles;
        set => _settingsService.ShouldInjectSubtitles = value;
    }

    public bool ShouldInjectTags
    {
        get => _settingsService.ShouldInjectTags;
        set => _settingsService.ShouldInjectTags = value;
    }

    public bool ShouldSkipExistingFiles
    {
        get => _settingsService.ShouldSkipExistingFiles;
        set => _settingsService.ShouldSkipExistingFiles = value;
    }

    public string FileNameTemplate
    {
        get => _settingsService.FileNameTemplate;
        set => _settingsService.FileNameTemplate = value;
    }

    public int ParallelLimit
    {
        get => _settingsService.ParallelLimit;
        set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
    }

    [RelayCommand]
    private async Task BrowseFFmpegFilePathAsync()
    {
        var fileTypes = OperatingSystem.IsWindows()
            ? new[]
            {
                new FilePickerFileType("FFmpeg executable") { Patterns = ["*.exe"] },
                FilePickerFileTypes.All,
            }
            : null;

        var filePath = await _dialogManager.PromptOpenFilePathAsync(fileTypes);

        if (string.IsNullOrWhiteSpace(filePath))
            return;

        FFmpegFilePath = filePath;
    }

    [RelayCommand]
    private void ResetFFmpegFilePath() => FFmpegFilePath = null;

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            _eventRoot.Dispose();
        }

        base.Dispose(disposing);
    }
}


================================================
FILE: YoutubeDownloader/ViewModels/MainViewModel.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using CommunityToolkit.Mvvm.Input;
using YoutubeDownloader.Core.Downloading;
using YoutubeDownloader.Framework;
using YoutubeDownloader.Localization;
using YoutubeDownloader.Services;
using YoutubeDownloader.Utils.Extensions;
using YoutubeDownloader.ViewModels.Components;

namespace YoutubeDownloader.ViewModels;

public partial class MainViewModel(
    ViewModelManager viewModelManager,
    DialogManager dialogManager,
    SnackbarManager snackbarManager,
    LocalizationManager localizationManager,
    SettingsService settingsService,
    UpdateService updateService
) : ViewModelBase
{
    public string Title { get; } = $"{Program.Name} v{Program.VersionString}";

    public DashboardViewModel Dashboard { get; } = viewModelManager.CreateDashboardViewModel();

    private async Task ShowUkraineSupportMessageAsync()
    {
        if (!settingsService.IsUkraineSupportMessageEnabled)
            return;

        var dialog = viewModelManager.CreateMessageBoxViewModel(
            localizationManager.UkraineSupportTitle,
            localizationManager.UkraineSupportMessage,
            localizationManager.LearnMoreButton,
            localizationManager.CloseButton
        );

        // Disable this message in the future
        settingsService.IsUkraineSupportMessageEnabled = false;
        settingsService.Save();

        if (await dialogManager.ShowDialogAsync(dialog) == true)
            Process.StartShellExecute("https://tyrrrz.me/ukraine?source=youtubedownloader");
    }

    private async Task ShowDevelopmentBuildMessageAsync()
    {
        if (!Program.IsDevelopmentBuild)
            return;

        // If debugging, the user is likely a developer
        if (Debugger.IsAttached)
            return;

        var dialog = viewModelManager.CreateMessageBoxViewModel(
            localizationManager.UnstableBuildTitle,
            string.Format(localizationManager.UnstableBuildMessage, Program.Name),
            localizationManager.SeeReleasesButton,
            localizationManager.CloseButton
        );

        if (await dialogManager.ShowDialogAsync(dialog) == true)
            Process.StartShellExecute(Program.ProjectReleasesUrl);
    }

    private async Task ShowFFmpegMissingMessageAsync()
    {
        if (settingsService.FFmpegFilePath is { } ffmpegFilePath)
        {
            // Explicit path set — only show the dialog if the file is missing
            if (File.Exists(ffmpegFilePath))
                return;

            var dialog = viewModelManager.CreateMessageBoxViewModel(
                localizationManager.FFmpegMissingTitle,
                string.Format(localizationManager.FFmpegPathMissingMessage, ffmpegFilePath),
                localizationManager.SettingsButton,
                localizationManager.CloseButton
            );

            if (await dialogManager.ShowDialogAsync(dialog) == true)
                await dialogManager.ShowDialogAsync(viewModelManager.CreateSettingsViewModel());
        }
        else
        {
            // No explicit path — fall back to auto-detection check
            if (FFmpeg.TryGetCliFilePath() is not null)
                return;

            var dialog = viewModelManager.CreateMessageBoxViewModel(
                localizationManager.FFmpegMissingTitle,
                $"""
                {string.Format(localizationManager.FFmpegMissingMessage, Program.Name)}

                ――――――――――――――――――――――――――――――――――――――――――

                {string.Format(localizationManager.FFmpegMissingSearchedLabel, FFmpeg.CliFileName)}
                {string.Join(
                    Environment.NewLine,
                    FFmpeg.GetProbeDirectoryPaths().Distinct(StringComparer.Ordinal).Select(d =>
                        $"- {d}"
                    )
                )}
                """,
                localizationManager.DownloadButton,
                localizationManager.CloseButton
            );

            if (await dialogManager.ShowDialogAsync(dialog) == true)
                Process.StartShellExecute("https://ffmpeg.org/download.html");
        }

        if (Application.Current?.ApplicationLifetime?.TryShutdown(3) != true)
            Environment.Exit(3);
    }

    private async Task CheckForUpdatesAsync()
    {
        try
        {
            var updateVersion = await updateService.CheckForUpdatesAsync();
            if (updateVersion is null)
                return;

            snackbarManager.Notify(
                string.Format(
                    localizationManager.UpdateDownloadingMessage,
                    Program.Name,
                    updateVersion
                )
            );
            await updateService.PrepareUpdateAsync(updateVersion);

            snackbarManager.Notify(
                localizationManager.UpdateReadyMessage,
                localizationManager.UpdateInstallNowButton,
                () =>
                {
                    updateService.FinalizeUpdate(true);

                    if (Application.Current?.ApplicationLifetime?.TryShutdown(2) != true)
                        Environment.Exit(2);
                }
            );
        }
        catch
        {
            // Failure to update shouldn't crash the application
            snackbarManager.Notify(localizationManager.UpdateFailedMessage);
        }
    }

    [RelayCommand]
    private async Task InitializeAsync()
    {
        await ShowUkraineSupportMessageAsync();
        await ShowDevelopmentBuildMessageAsync();
        await ShowFFmpegMissingMessageAsync();
        await CheckForUpdatesAsync();
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Save settings
            settingsService.Save();

            // Finalize pending updates
            updateService.FinalizeUpdate(false);
        }

        base.Dispose(disposing);
    }
}


================================================
FILE: YoutubeDownloader/Views/Components/DashboardView.axaml
================================================
<UserControl
    x:Class="YoutubeDownloader.Views.Components.DashboardView"
    xmlns="https://github.com/avaloniaui"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
    xmlns:components="clr-namespace:YoutubeDownloader.ViewModels.Components"
    xmlns:converters="clr-namespace:YoutubeDownloader.Converters"
    xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
    xmlns:materialStyles="clr-namespace:Material.Styles.Controls;assembly=Material.Styles"
    x:Name="UserControl"
    x:DataType="components:DashboardViewModel"
    Loaded="UserControl_OnLoaded">
    <DockPanel>
        <!--  Header  -->
        <StackPanel
            Background="{DynamicResource MaterialDarkBackgroundBrush}"
            DockPanel.Dock="Top"
            Orientation="Vertical">
            <Grid Margin="12,12,8,12" ColumnDefinitions="*,Auto,Auto">
                <!--  Query  -->
                <materialStyles:Card Grid.Column="0">
                    <TextBox
                        x:Name="QueryTextBox"
                        AcceptsReturn="True"
                        FontSize="16"
                        MaxLines="4"
                        ScrollViewer.HorizontalScrollBarVisibility="Hidden"
                        Text="{Binding Query}"
                        Theme="{DynamicResource SoloTextBox}"
                        ToolTip.Tip="{Binding LocalizationManager.QueryTooltip}"
                        Watermark="{Binding LocalizationManager.QueryWatermark}">
                        <TextBox.InnerLeftContent>
                            <materialIcons:MaterialIcon
                                Width="24"
                                Height="24"
                                Margin="4,0,8,0"
                                Kind="Search" />
                        </TextBox.InnerLeftContent>
                        <TextBox.InnerRightContent>
                            <Button
                                x:Name="ProcessQueryButton"
                                Margin="8,0,0,0"
                                Padding="4"
                                Command="{Binding ProcessQueryCommand}"
                                IsDefault="True"
                                Theme="{DynamicResource MaterialFlatButton}"
                                ToolTip.Tip="{Binding LocalizationManager.ProcessQueryTooltip}">
                                <materialIcons:MaterialIcon
                                    Width="24"
                                    Height="24"
                                    Kind="ArrowRight" />
                            </Button>
                        </TextBox.InnerRightContent>
                    </TextBox>
                </materialStyles:Card>

                <!--  Auth button  -->
                <Button
                    Grid.Column="1"
                    Margin="8,0,0,0"
                    Padding="8"
                    VerticalAlignment="Center"
                    Command="{Binding ShowAuthSetupCommand}"
                    Foreground="{DynamicResource MaterialDarkForegroundBrush}"
                    IsVisible="{OnPlatform False,
                                           Windows=True}"
                    Theme="{DynamicResource MaterialFlatButton}"
                    ToolTip.Tip="{Binding LocalizationManager.AuthTooltip}">
                    <materialIcons:MaterialIcon
                        Width="24"
                        Height="24"
                        Kind="AccountKey" />
                </Button>

                <!--  Settings button  -->
                <Button
                    Grid.Column="2"
                    Margin="8,0,0,0"
                    Padding="8"
                    VerticalAlignment="Center"
                    Command="{Binding ShowSettingsCommand}"
                    Foreground="{DynamicResource MaterialDarkForegroundBrush}"
                    Theme="{DynamicResource MaterialFlatButton}"
                    ToolTip.Tip="{Binding LocalizationManager.SettingsTooltip}">
                    <materialIcons:MaterialIcon
                        Width="24"
                        Height="24"
                        Kind="Settings" />
                </Button>
            </Grid>

            <!--  Progress  -->
            <ProgressBar
                Height="2"
                Background="Transparent"
                IsIndeterminate="{Binding IsProgressIndeterminate}"
                Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />
        </StackPanel>

        <!--  Body  -->
        <Panel Background="{DynamicResource MaterialCardBackgroundBrush}" DockPanel.Dock="Bottom">
            <!--  Placeholder  -->
            <StackPanel
                Margin="8,32,8,8"
                HorizontalAlignment="Center"
                IsVisible="{Binding !Downloads.Count}"
                Orientation="Vertical">
                <materialIcons:MaterialIcon
                    Width="256"
                    Height="256"
                    HorizontalAlignment="Center"
                    Foreground="{DynamicResource MaterialDividerBrush}"
                    Kind="Youtube" />

                <TextBlock
                    HorizontalAlignment="Center"
                    FontSize="18"
                    FontWeight="Light"
                    Inlines="{Binding LocalizationManager.DashboardPlaceholder, Converter={x:Static converters:MarkdownToInlinesConverter.Instance}}"
                    LineSpacing="8"
                    TextAlignment="Center"
                    TextWrapping="Wrap" />
            </StackPanel>

            <!--  Downloads  -->
            <DataGrid
                ColumnWidth="Auto"
                HorizontalScrollBarVisibility="Disabled"
                IsVisible="{Binding !!Downloads.Count}"
                ItemsSource="{Binding Downloads}"
                VerticalScrollBarVisibility="Visible">
                <DataGrid.ContextMenu>
                    <ContextMenu>
                        <MenuItem Command="{Binding RemoveSuccessfulDownloadsCommand}" Header="{Binding LocalizationManager.ContextMenuRemoveSuccessful}" />
                        <MenuItem Command="{Binding RemoveInactiveDownloadsCommand}" Header="{Binding LocalizationManager.ContextMenuRemoveInactive}" />
                        <Separator />
                        <MenuItem Command="{Binding RestartFailedDownloadsCommand}" Header="{Binding LocalizationManager.ContextMenuRestartFailed}" />
                        <Separator />
                        <MenuItem Command="{Binding CancelAllDownloadsCommand}" Header="{Binding LocalizationManager.ContextMenuCancelAll}" />
                    </ContextMenu>
                </DataGrid.ContextMenu>
                <DataGrid.Columns>
                    <!--  Thumbnail  -->
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Image
                                    Width="48"
                                    Height="48"
                                    asyncImageLoader:ImageLoader.Source="{Binding Video, Converter={x:Static converters:VideoToLowestQualityThumbnailUrlStringConverter.Instance}}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

                    <!--  File name  -->
                    <DataGridTemplateColumn
                        Width="*"
                        Header="{Binding #UserControl.DataContext.LocalizationManager.DownloadsFileColumnHeader}"
                        SortMemberPath="FileName">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock
                                    FontSize="14"
                                    Foreground="{DynamicResource MaterialBodyBrush}"
                                    Text="{Binding FileName}"
                                    TextTrimming="CharacterEllipsis"
                                    ToolTip.Tip="{Binding FileName}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

                    <!--  Status  -->
                    <DataGridTemplateColumn
                        MinWidth="100"
                        Header="{Binding #UserControl.DataContext.LocalizationManager.DownloadsStatusColumnHeader}"
                        SortMemberPath="Progress.Current.Fraction">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Grid ColumnDefinitions="Auto,Auto">
                                    <!--  Progress  -->
                                    <ProgressBar
                                        Grid.Column="0"
                                        Margin="0,0,6,0"
                                        IsIndeterminate="{Binding IsProgressIndeterminate}"
                                        IsVisible="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Started}}"
                                        Theme="{DynamicResource MaterialCircularProgressBar}"
                                        Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />

                                    <!--  Status  -->
                                    <TextBlock
                                        x:Name="StatusTextBlock"
                                        Grid.Column="1"
                                        Classes.canceled="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Canceled}}"
                                        Classes.completed="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Completed}}"
                                        Classes.enqueued="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Enqueued}}"
                                        Classes.failed="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Failed}}"
                                        Classes.started="{Binding Status, Converter={x:Static converters:EqualityConverter.IsEqual}, ConverterParameter={x:Static components:DownloadStatus.Started}}"
                                        PointerReleased="StatusTextBlock_OnPointerReleased"
                                        TextTrimming="CharacterEllipsis">
                                        <TextBlock.Resources>
                                            <ResourceDictionary>
                                                <ResourceDictionary.ThemeDictionaries>
                                                    <ResourceDictionary x:Key="Default">
                                                        <SolidColorBrush x:Key="SuccessBrush" Color="DarkGreen" />
                                                        <SolidColorBrush x:Key="CanceledBrush" Color="DarkOrange" />
                                                        <SolidColorBrush x:Key="FailedBrush" Color="DarkRed" />
                                                    </ResourceDictionary>
                                                    <ResourceDictionary x:Key="Dark">
                                                        <SolidColorBrush x:Key="SuccessBrush" Color="LightGreen" />
                                                        <SolidColorBrush x:Key="CanceledBrush" Color="Orange" />
                                                        <SolidColorBrush x:Key="FailedBrush" Color="OrangeRed" />
                                                    </ResourceDictionary>
                                                </ResourceDictionary.ThemeDictionaries>
                                            </ResourceDictionary>
                                        </TextBlock.Resources>
                                        <TextBlock.Styles>
                                            <Style Selector="TextBlock">
                                                <Style Selector="^.enqueued">
                                                    <Setter Property="Opacity" Value="0.7" />
                                                    <Setter Property="Text" Value="{Binding LocalizationManager.DownloadStatusEnqueued}" />
                                                </Style>
                                                <Style Selector="^.started">
                                                    <Setter Property="Text" Value="{Binding Progress.Current}" />
                                                </Style>
                                                <Style Selector="^.completed">
                                                    <Setter Property="Foreground" Value="{DynamicResource SuccessBrush}" />
                                                    <Setter Property="Text" Value="{Binding LocalizationManager.DownloadStatusCompleted}" />
                                                </Style>
                                                <Style Selector="^.canceled">
                                                    <Setter Property="Foreground" Value="{DynamicResource CanceledBrush}" />
                                                    <Setter Property="Text" Value="{Binding LocalizationManager.DownloadStatusCanceled}" />
                                                </Style>
                                                <Style Selector="^.failed">
                                                    <Setter Property="Foreground" Value="{DynamicResource FailedBrush}" />
                                                    <Setter Property="Text" Value="{Binding LocalizationManager.DownloadStatusFailed}" />
                                                    <Setter Property="ToolTip.Tip">
                                                        <Template>
                                                            <TextBlock>
                                                                <Run FontWeight="SemiBold" Text="{Binding LocalizationManager.ClickToCopyErrorTooltip}" />
                                                                <LineBreak />
                                                                <LineBreak />
                                                                <Run Text="{Binding ErrorMessage}" />
                                                            </TextBlock>
                                                        </Template>
                                                    </Setter>
                                                    <Setter Property="Cursor" Value="Hand" />
                                                </Style>
                                            </Style>
                                        </TextBlock.Styles>
                                    </TextBlock>
                                </Grid>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>

                    <!--  Buttons  -->
                    <DataGridTemplateColumn MinWidth="100">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
                                    <!--  Show file  -->
                                    <Button
                                        Padding="4"
                                        VerticalAlignment="Center"
                                        Command="{Binding ShowFileCommand}"
                                        IsVisible="{Binding $self.IsEffectivelyEnabled}"
                                        Theme="{DynamicResource MaterialFlatButton}"
                                        ToolTip.Tip="{Binding LocalizationManager.ShowFileTooltip}">
                                        <materialIcons:MaterialIcon
                                            Width="24"
                                            Height="24"
                                            Kind="FileFindOutline" />
                                    </Button>

                                    <!--  Open file  -->
                                    <Button
                                        Padding="4"
                                        VerticalAlignment="Center"
                                        Command="{Binding OpenFileCommand}"
                                        IsVisible="{Binding $self.IsEffectivelyEnabled}"
                                        Theme="{DynamicResource MaterialFlatButton}"
                                        ToolTip.Tip="{Binding LocalizationManager.PlayTooltip}">
                                        <materialIcons:MaterialIcon
                                            Width="24"
                                            Height="24"
                                            Kind="PlayCircleOutline" />
                                    </Button>

                                    <!--  Cancel download  -->
                                    <Button
                                        Padding="4"
                                        VerticalAlignment="Center"
                                        Command="{Binding CancelCommand}"
                                        IsVisible="{Binding $self.IsEffectivelyEnabled}"
                                        Theme="{DynamicResource MaterialFlatButton}"
                                        ToolTip.Tip="{Binding LocalizationManager.CancelDownloadTooltip}">
                                        <materialIcons:MaterialIcon
                                            Width="24"
                                            Height="24"
                                            Kind="CloseCircleOutline" />
                                    </Button>

                                    <!--  Restart download  -->
                                    <Button
                                        Padding="4"
                                        VerticalAlignment="Center"
                                        Command="{Binding $parent[UserControl].((components:DashboardViewModel)DataContext).RestartDownloadCommand}"
                                        CommandParameter="{Binding}"
                                        IsVisible="{Binding IsCanceledOrFailed}"
                                        Theme="{DynamicResource MaterialFlatButton}"
                                        ToolTip.Tip="{Binding LocalizationManager.RestartDownloadTooltip}">
                                        <materialIcons:MaterialIcon
                                            Width="24"
                                            Height="24"
                                            Kind="Restart" />
                                    </Button>
                                </StackPanel>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>
        </Panel>
    </DockPanel>
</UserControl>


================================================
FILE: YoutubeDownloader/Views/Components/DashboardView.axaml.cs
================================================
using Avalonia;
using Avalonia.Input;
using Avalonia.Interactivity;
using YoutubeDownloader.Framework;
using YoutubeDownloader.ViewModels.Components;

namespace YoutubeDownloader.Views.Components;

public partial class DashboardView : UserControl<DashboardViewModel>
{
    public DashboardView()
    {
        InitializeComponent();

        // Bind the event with the tunnel strategy to handle keys that take part in writing text
        QueryTextBox.AddHandler(KeyDownEvent, QueryTextBox_OnKeyDown, RoutingStrategies.Tunnel);
    }

    private void UserControl_OnLoaded(object? sender, RoutedEventArgs args) => QueryTextBox.Focus();

    private void QueryTextBox_OnKeyDown(object? sender, KeyEventArgs args)
    {
        // When pressing Enter without Shift, execute the default button command
        // instead of adding a new line.
        if (args.Key == Key.Enter && args.KeyModifiers != KeyModifiers.Shift)
        {
            args.Handled = true;
            ProcessQueryButton.Command?.Execute(ProcessQueryButton.CommandParameter);
        }
    }

    private void StatusTextBlock_OnPointerReleased(object sender, PointerReleasedEventArgs args)
    {
        if (sender is IDataContextProvider { DataContext: DownloadViewModel dataContext })
            dataContext.CopyErrorMessageCommand.Execute(null);
    }
}


================================================
FILE: YoutubeDownloader/Views/Dialogs/AuthSetupView.axaml
================================================
<UserControl
    x:Class="YoutubeDownloader.Views.Dialogs.AuthSetupView"
    xmlns="https://github.com/avaloniaui"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dialogs="clr-namespace:YoutubeDownloader.ViewModels.Dialogs"
    xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
    Height="450"
    MinWidth="450"
    x:DataType="dialogs:AuthSetupViewModel">
    <Grid RowDefinitions="Auto,*,Auto">
        <!--  Title  -->
        <TextBlock
            Grid.Row="0"
            Margin="16"
            FontSize="19"
            FontWeight="Light"
            Text="{Binding LocalizationManager.AuthenticationTitle}" />

        <!--  Content  -->
        <Border
            Grid.Row="1"
            BorderBrush="{DynamicResource MaterialDividerBrush}"
            BorderThickness="0,1">
            <Panel>
                <!--  Current auth status  -->
                <StackPanel
                    Margin="16"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    IsVisible="{Binding IsAuthenticated}"
                    Orientation="Vertical">
                    <materialIcons:MaterialIcon
                        Width="196"
                        Height="196"
                        HorizontalAlignment="Center"
                        Foreground="{DynamicResource MaterialDividerBrush}"
                        Kind="AccountCheck" />

                    <TextBlock
                        HorizontalAlignment="Center"
                        FontSize="18"
                        TextAlignment="Center"
                        TextWrapping="Wrap">
                        <Run FontWeight="Light" Text="{Binding LocalizationManager.AuthenticatedText}" />
                    </TextBlock>

                    <!--  Log out  -->
                    <Button
                        x:Name="LogOutButton"
                        Margin="16"
                        HorizontalAlignment="Center"
                        Click="LogOutButton_OnClick"
                        Content="{Binding LocalizationManager.LogOutButton}"
                        FontSize="18"
                        Foreground="{DynamicResource MaterialSecondaryMidBrush}"
                        Theme="{DynamicResource MaterialFlatButton}" />
                </StackPanel>

                <!--  Placeholder  -->
                <TextBlock
                    Margin="16"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    FontSize="18"
                    IsVisible="{Binding !IsAuthenticated}"
                    Text="{Binding LocalizationManager.LoadingText}" />

                <!--  Browser  -->
                <WebView
                    x:Name="WebBrowser"
                    IsVisible="{Binding !IsAuthenticated}"
                    Loaded="WebBrowser_OnLoaded"
                    NavigationStarting="WebBrowser_OnNavigationStarting"
                    WebViewCreated="WebBrowser_OnWebViewCreated" />
            </Panel>
        </Border>

        <!--  Close button  -->
        <Button
            Grid.Row="2"
            Margin="16"
            HorizontalAlignment="Stretch"
            Command="{Binding CloseCommand}"
            Content="{Binding LocalizationManager.CloseButton}"
            IsCancel="True"
            IsDefault="True"
            Theme="{DynamicResource MaterialOutlineButton}" />
    </Grid>
</UserControl>


================================================
FILE: YoutubeDownloader/Views/Dialogs/AuthSetupView.axaml.cs
================================================
using System;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.WebView.Windows.Core;
using Microsoft.Web.WebView2.Core;
using WebViewCore.Events;
using YoutubeDownloader.Framework;
using YoutubeDownloader.ViewModels.Dialogs;

namespace YoutubeDownloader.Views.Dialogs;

public partial class AuthSetupView : UserControl<AuthSetupViewModel>
{
    private const string HomePageUrl = "https://www.youtube.com";
    private static readonly string LoginPageUrl =
        $"https://accounts.google.com/ServiceLogin?continue={Uri.EscapeDataString(HomePageUrl)}";

    private CoreWebView2? _coreWebView2;

    public AuthSetupView() => InitializeComponent();

    private void NavigateToLoginPage() => WebBrowser.Url = new Uri(LoginPageUrl);

    private void LogOutButton_OnClick(object sender, RoutedEventArgs args)
    {
        DataContext.Cookies = null;
        NavigateToLoginPage();
    }
Download .txt
gitextract_i1gh213a/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.yml
│   │   └── config.yml
│   ├── dependabot.yml
│   └── workflows/
│       └── main.yml
├── .gitignore
├── Directory.Build.props
├── Directory.Packages.props
├── License.txt
├── NuGet.config
├── Readme.md
├── YoutubeDownloader/
│   ├── .gitignore
│   ├── App.axaml
│   ├── App.axaml.cs
│   ├── Converters/
│   │   ├── EqualityConverter.cs
│   │   ├── MarkdownToInlinesConverter.cs
│   │   ├── VideoQualityPreferenceToStringConverter.cs
│   │   ├── VideoToHighestQualityThumbnailUrlStringConverter.cs
│   │   └── VideoToLowestQualityThumbnailUrlStringConverter.cs
│   ├── Download-FFmpeg.ps1
│   ├── Framework/
│   │   ├── DialogManager.cs
│   │   ├── DialogViewModelBase.cs
│   │   ├── SnackbarManager.cs
│   │   ├── ThemeVariant.cs
│   │   ├── UserControl.cs
│   │   ├── ViewManager.cs
│   │   ├── ViewModelBase.cs
│   │   ├── ViewModelManager.cs
│   │   └── Window.cs
│   ├── Localization/
│   │   ├── Language.cs
│   │   ├── LocalizationManager.English.cs
│   │   ├── LocalizationManager.French.cs
│   │   ├── LocalizationManager.German.cs
│   │   ├── LocalizationManager.Spanish.cs
│   │   ├── LocalizationManager.Ukrainian.cs
│   │   └── LocalizationManager.cs
│   ├── Program.cs
│   ├── Publish-MacOSBundle.ps1
│   ├── Services/
│   │   ├── SettingsService.AuthCookiesEncryptionConverter.cs
│   │   ├── SettingsService.cs
│   │   └── UpdateService.cs
│   ├── StartOptions.cs
│   ├── Utils/
│   │   ├── Disposable.cs
│   │   ├── DisposableCollector.cs
│   │   ├── Extensions/
│   │   │   ├── AvaloniaExtensions.cs
│   │   │   ├── DirectoryExtensions.cs
│   │   │   ├── DisposableExtensions.cs
│   │   │   ├── EnvironmentExtensions.cs
│   │   │   ├── NotifyPropertyChangedExtensions.cs
│   │   │   ├── PathExtensions.cs
│   │   │   └── ProcessExtensions.cs
│   │   ├── NativeMethods.cs
│   │   └── ResizableSemaphore.cs
│   ├── ViewModels/
│   │   ├── Components/
│   │   │   ├── DashboardViewModel.cs
│   │   │   ├── DownloadStatus.cs
│   │   │   └── DownloadViewModel.cs
│   │   ├── Dialogs/
│   │   │   ├── AuthSetupViewModel.cs
│   │   │   ├── DownloadMultipleSetupViewModel.cs
│   │   │   ├── DownloadSingleSetupViewModel.cs
│   │   │   ├── MessageBoxViewModel.cs
│   │   │   └── SettingsViewModel.cs
│   │   └── MainViewModel.cs
│   ├── Views/
│   │   ├── Components/
│   │   │   ├── DashboardView.axaml
│   │   │   └── DashboardView.axaml.cs
│   │   ├── Dialogs/
│   │   │   ├── AuthSetupView.axaml
│   │   │   ├── AuthSetupView.axaml.cs
│   │   │   ├── DownloadMultipleSetupView.axaml
│   │   │   ├── DownloadMultipleSetupView.axaml.cs
│   │   │   ├── DownloadSingleSetupView.axaml
│   │   │   ├── DownloadSingleSetupView.axaml.cs
│   │   │   ├── MessageBoxView.axaml
│   │   │   ├── MessageBoxView.axaml.cs
│   │   │   ├── SettingsView.axaml
│   │   │   └── SettingsView.axaml.cs
│   │   ├── MainView.axaml
│   │   └── MainView.axaml.cs
│   ├── YoutubeDownloader.csproj
│   └── app.manifest
├── YoutubeDownloader.Core/
│   ├── Downloading/
│   │   ├── FFmpeg.cs
│   │   ├── FileNameTemplate.cs
│   │   ├── VideoDownloadOption.cs
│   │   ├── VideoDownloadPreference.cs
│   │   ├── VideoDownloader.cs
│   │   └── VideoQualityPreference.cs
│   ├── Resolving/
│   │   ├── QueryResolver.cs
│   │   ├── QueryResult.cs
│   │   └── QueryResultKind.cs
│   ├── Tagging/
│   │   ├── MediaFile.cs
│   │   ├── MediaTagInjector.cs
│   │   ├── MusicBrainzClient.cs
│   │   └── MusicBrainzRecording.cs
│   ├── Utils/
│   │   ├── Extensions/
│   │   │   ├── AsyncCollectionExtensions.cs
│   │   │   ├── CollectionExtensions.cs
│   │   │   ├── GenericExtensions.cs
│   │   │   ├── PathExtensions.cs
│   │   │   ├── StringExtensions.cs
│   │   │   └── YoutubeExtensions.cs
│   │   ├── Http.cs
│   │   ├── ThrottleLock.cs
│   │   └── Url.cs
│   └── YoutubeDownloader.Core.csproj
├── YoutubeDownloader.sln
├── favicon.icns
└── global.json
Download .txt
SYMBOL INDEX (268 symbols across 76 files)

FILE: YoutubeDownloader.Core/Downloading/FFmpeg.cs
  class FFmpeg (line 8) | public static class FFmpeg
    method GetProbeDirectoryPaths (line 13) | public static IEnumerable<string> GetProbeDirectoryPaths()
    method TryGetCliFilePath (line 60) | public static string? TryGetCliFilePath() =>
    method IsBundled (line 66) | public static bool IsBundled() =>

FILE: YoutubeDownloader.Core/Downloading/FileNameTemplate.cs
  class FileNameTemplate (line 9) | public static class FileNameTemplate
    method Apply (line 11) | public static string Apply(

FILE: YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs
  type VideoDownloadOption (line 9) | public partial record VideoDownloadOption(
  type VideoDownloadOption (line 19) | public partial record VideoDownloadOption

FILE: YoutubeDownloader.Core/Downloading/VideoDownloadPreference.cs
  type VideoDownloadPreference (line 8) | public record VideoDownloadPreference(

FILE: YoutubeDownloader.Core/Downloading/VideoDownloader.cs
  class VideoDownloader (line 16) | public class VideoDownloader(IReadOnlyList<Cookie>? initialCookies = nul...
    method GetDownloadOptionsAsync (line 20) | public async Task<IReadOnlyList<VideoDownloadOption>> GetDownloadOptio...
    method GetBestDownloadOptionAsync (line 30) | public async Task<VideoDownloadOption> GetBestDownloadOptionAsync(
    method DownloadVideoAsync (line 47) | public async Task DownloadVideoAsync(
    method Dispose (line 86) | public void Dispose() => _youtube.Dispose();

FILE: YoutubeDownloader.Core/Downloading/VideoQualityPreference.cs
  type VideoQualityPreference (line 5) | public enum VideoQualityPreference
  class VideoQualityPreferenceExtensions (line 17) | public static class VideoQualityPreferenceExtensions
    method extension (line 19) | extension(VideoQualityPreference preference)

FILE: YoutubeDownloader.Core/Resolving/QueryResolver.cs
  class QueryResolver (line 16) | public class QueryResolver(IReadOnlyList<Cookie>? initialCookies = null)...
    method TryResolvePlaylistAsync (line 21) | private async Task<QueryResult?> TryResolvePlaylistAsync(
    method TryResolveVideoAsync (line 42) | private async Task<QueryResult?> TryResolveVideoAsync(
    method TryResolveChannelAsync (line 54) | private async Task<QueryResult?> TryResolveChannelAsync(
    method ResolveSearchAsync (line 98) | private async Task<QueryResult> ResolveSearchAsync(
    method ResolveAsync (line 110) | public async Task<QueryResult> ResolveAsync(
    method Dispose (line 125) | public void Dispose() => _youtube.Dispose();

FILE: YoutubeDownloader.Core/Resolving/QueryResult.cs
  type QueryResult (line 8) | public record QueryResult(QueryResultKind Kind, string Title, IReadOnlyL...

FILE: YoutubeDownloader.Core/Resolving/QueryResultKind.cs
  type QueryResultKind (line 3) | public enum QueryResultKind

FILE: YoutubeDownloader.Core/Tagging/MediaFile.cs
  class MediaFile (line 7) | internal partial class MediaFile(TagFile file) : IDisposable
    method SetThumbnail (line 9) | public void SetThumbnail(byte[] thumbnailData) =>
    method SetArtist (line 12) | public void SetArtist(string artist) => file.Tag.Performers = [artist];
    method SetArtistSort (line 14) | public void SetArtistSort(string artistSort) => file.Tag.PerformersSor...
    method SetTitle (line 16) | public void SetTitle(string title) => file.Tag.Title = title;
    method SetAlbum (line 18) | public void SetAlbum(string album) => file.Tag.Album = album;
    method SetDescription (line 20) | public void SetDescription(string description) => file.Tag.Description...
    method SetComment (line 22) | public void SetComment(string comment) => file.Tag.Comment = comment;
    method Save (line 24) | public void Save()
    method Dispose (line 30) | public void Dispose() => file.Dispose();
    method Open (line 35) | public static MediaFile Open(string filePath) => new(TagFile.Create(fi...
  class MediaFile (line 33) | internal partial class MediaFile
    method SetThumbnail (line 9) | public void SetThumbnail(byte[] thumbnailData) =>
    method SetArtist (line 12) | public void SetArtist(string artist) => file.Tag.Performers = [artist];
    method SetArtistSort (line 14) | public void SetArtistSort(string artistSort) => file.Tag.PerformersSor...
    method SetTitle (line 16) | public void SetTitle(string title) => file.Tag.Title = title;
    method SetAlbum (line 18) | public void SetAlbum(string album) => file.Tag.Album = album;
    method SetDescription (line 20) | public void SetDescription(string description) => file.Tag.Description...
    method SetComment (line 22) | public void SetComment(string comment) => file.Tag.Comment = comment;
    method Save (line 24) | public void Save()
    method Dispose (line 30) | public void Dispose() => file.Dispose();
    method Open (line 35) | public static MediaFile Open(string filePath) => new(TagFile.Create(fi...

FILE: YoutubeDownloader.Core/Tagging/MediaTagInjector.cs
  class MediaTagInjector (line 11) | public class MediaTagInjector
    method InjectMiscMetadata (line 15) | private void InjectMiscMetadata(MediaFile mediaFile, IVideo video)
    method InjectMusicMetadataAsync (line 32) | private async Task InjectMusicMetadataAsync(
    method InjectThumbnailAsync (line 63) | private async Task InjectThumbnailAsync(
    method InjectTagsAsync (line 84) | public async Task InjectTagsAsync(

FILE: YoutubeDownloader.Core/Tagging/MusicBrainzClient.cs
  class MusicBrainzClient (line 12) | internal class MusicBrainzClient
    method SearchRecordingsAsync (line 17) | public async IAsyncEnumerable<MusicBrainzRecording> SearchRecordingsAs...

FILE: YoutubeDownloader.Core/Tagging/MusicBrainzRecording.cs
  type MusicBrainzRecording (line 3) | internal record MusicBrainzRecording(

FILE: YoutubeDownloader.Core/Utils/Extensions/AsyncCollectionExtensions.cs
  class AsyncCollectionExtensions (line 7) | public static class AsyncCollectionExtensions
    method CollectAsync (line 11) | private async ValueTask<IReadOnlyList<T>> CollectAsync()
    method GetAwaiter (line 21) | public ValueTaskAwaiter<IReadOnlyList<T>> GetAwaiter() =>

FILE: YoutubeDownloader.Core/Utils/Extensions/CollectionExtensions.cs
  class CollectionExtensions (line 5) | public static class CollectionExtensions
    method AddRange (line 9) | public void AddRange(IEnumerable<T> items)

FILE: YoutubeDownloader.Core/Utils/Extensions/GenericExtensions.cs
  class GenericExtensions (line 5) | public static class GenericExtensions
    method Pipe (line 9) | public TOut Pipe<TOut>(Func<TIn, TOut> transform) => transform(input);

FILE: YoutubeDownloader.Core/Utils/Extensions/PathExtensions.cs
  class PathExtensions (line 7) | public static class PathExtensions
    method extension (line 9) | extension(Path)

FILE: YoutubeDownloader.Core/Utils/Extensions/StringExtensions.cs
  class StringExtensions (line 3) | public static class StringExtensions
    method extension (line 5) | extension(string str)

FILE: YoutubeDownloader.Core/Utils/Extensions/YoutubeExtensions.cs
  class YoutubeExtensions (line 6) | public static class YoutubeExtensions
    method extension (line 8) | extension(Thumbnail thumbnail)

FILE: YoutubeDownloader.Core/Utils/Http.cs
  class Http (line 7) | public static class Http

FILE: YoutubeDownloader.Core/Utils/ThrottleLock.cs
  class ThrottleLock (line 7) | public class ThrottleLock(TimeSpan interval) : IDisposable
    method WaitAsync (line 12) | public async Task WaitAsync(CancellationToken cancellationToken = defa...
    method Dispose (line 32) | public void Dispose() => _semaphore.Dispose();

FILE: YoutubeDownloader.Core/Utils/Url.cs
  class Url (line 6) | public static class Url
    method TryExtractFileName (line 8) | public static string? TryExtractFileName(string url) =>

FILE: YoutubeDownloader/App.axaml.cs
  class App (line 22) | public class App : Application, IDisposable
    method App (line 32) | public App()
    method Initialize (line 82) | public override void Initialize()
    method RegisterServices (line 89) | public override void RegisterServices()
    method InitializeTheme (line 96) | private void InitializeTheme()
    method OnFrameworkInitializationCompleted (line 111) | public override void OnFrameworkInitializationCompleted()
    method Application_OnActualThemeVariantChanged (line 141) | private void Application_OnActualThemeVariantChanged(object? sender, E...
    method Dispose (line 145) | public void Dispose()

FILE: YoutubeDownloader/Converters/EqualityConverter.cs
  class EqualityConverter (line 8) | public class EqualityConverter(bool isInverted) : IValueConverter
    method Convert (line 13) | public object? Convert(
    method ConvertBack (line 20) | public object ConvertBack(

FILE: YoutubeDownloader/Converters/MarkdownToInlinesConverter.cs
  class MarkdownToInlinesConverter (line 13) | public class MarkdownToInlinesConverter : IValueConverter
    method ProcessInline (line 21) | private static void ProcessInline(
    method Convert (line 90) | public object? Convert(object? value, Type targetType, object? paramet...
    method ConvertBack (line 118) | public object? ConvertBack(

FILE: YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs
  class VideoQualityPreferenceToStringConverter (line 8) | public class VideoQualityPreferenceToStringConverter : IValueConverter
    method Convert (line 12) | public object? Convert(object? value, Type targetType, object? paramet...
    method ConvertBack (line 20) | public object ConvertBack(

FILE: YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlStringConverter.cs
  class VideoToHighestQualityThumbnailUrlStringConverter (line 9) | public class VideoToHighestQualityThumbnailUrlStringConverter : IValueCo...
    method Convert (line 13) | public object? Convert(
    method ConvertBack (line 20) | public object ConvertBack(

FILE: YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlStringConverter.cs
  class VideoToLowestQualityThumbnailUrlStringConverter (line 9) | public class VideoToLowestQualityThumbnailUrlStringConverter : IValueCon...
    method Convert (line 13) | public object? Convert(
    method ConvertBack (line 20) | public object ConvertBack(

FILE: YoutubeDownloader/Framework/DialogManager.cs
  class DialogManager (line 14) | public class DialogManager : IDisposable
    method ShowDialogAsync (line 18) | public async Task<T?> ShowDialogAsync<T>(DialogViewModelBase<T> dialog)
    method PromptOpenFilePathAsync (line 54) | public async Task<string?> PromptOpenFilePathAsync(
    method PromptSaveFilePathAsync (line 70) | public async Task<string?> PromptSaveFilePathAsync(
    method PromptDirectoryPathAsync (line 91) | public async Task<string?> PromptDirectoryPathAsync(string defaultDirP...
    method Dispose (line 114) | public void Dispose() => _dialogLock.Dispose();

FILE: YoutubeDownloader/Framework/DialogViewModelBase.cs
  class DialogViewModelBase (line 7) | public abstract partial class DialogViewModelBase<T> : ViewModelBase
    method Close (line 16) | [RelayCommand]
    method WaitForCloseAsync (line 23) | public async Task<T> WaitForCloseAsync() => await _closeTcs.Task;

FILE: YoutubeDownloader/Framework/SnackbarManager.cs
  class SnackbarManager (line 8) | public class SnackbarManager
    method Notify (line 12) | public void Notify(string message, TimeSpan? duration = null) =>
    method Notify (line 19) | public void Notify(

FILE: YoutubeDownloader/Framework/ThemeVariant.cs
  type ThemeVariant (line 3) | public enum ThemeVariant

FILE: YoutubeDownloader/Framework/UserControl.cs
  class UserControl (line 6) | public class UserControl<TDataContext> : UserControl

FILE: YoutubeDownloader/Framework/ViewManager.cs
  class ViewManager (line 12) | public partial class ViewManager
    method TryCreateView (line 14) | private Control? TryCreateView(ViewModelBase viewModel) =>
    method TryBindView (line 27) | public Control? TryBindView(ViewModelBase viewModel)
    method Match (line 41) | bool IDataTemplate.Match(object? data) => data is ViewModelBase;
    method Build (line 43) | Control? ITemplate<object?, Control?>.Build(object? data) =>
  class ViewManager (line 39) | public partial class ViewManager : IDataTemplate
    method TryCreateView (line 14) | private Control? TryCreateView(ViewModelBase viewModel) =>
    method TryBindView (line 27) | public Control? TryBindView(ViewModelBase viewModel)
    method Match (line 41) | bool IDataTemplate.Match(object? data) => data is ViewModelBase;
    method Build (line 43) | Control? ITemplate<object?, Control?>.Build(object? data) =>

FILE: YoutubeDownloader/Framework/ViewModelBase.cs
  class ViewModelBase (line 6) | public abstract class ViewModelBase : ObservableObject, IDisposable
    method OnAllPropertiesChanged (line 10) | protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Em...
    method Dispose (line 12) | protected virtual void Dispose(bool disposing) { }
    method Dispose (line 14) | public void Dispose()

FILE: YoutubeDownloader/Framework/ViewModelManager.cs
  class ViewModelManager (line 13) | public class ViewModelManager(IServiceProvider services)
    method CreateMainViewModel (line 15) | public MainViewModel CreateMainViewModel() => services.GetRequiredServ...
    method CreateDashboardViewModel (line 17) | public DashboardViewModel CreateDashboardViewModel() =>
    method CreateAuthSetupViewModel (line 20) | public AuthSetupViewModel CreateAuthSetupViewModel() =>
    method CreateDownloadViewModel (line 23) | public DownloadViewModel CreateDownloadViewModel(
    method CreateDownloadViewModel (line 38) | public DownloadViewModel CreateDownloadViewModel(
    method CreateDownloadMultipleSetupViewModel (line 53) | public DownloadMultipleSetupViewModel CreateDownloadMultipleSetupViewM...
    method CreateDownloadSingleSetupViewModel (line 70) | public DownloadSingleSetupViewModel CreateDownloadSingleSetupViewModel(
    method CreateMessageBoxViewModel (line 83) | public MessageBoxViewModel CreateMessageBoxViewModel(
    method CreateMessageBoxViewModel (line 100) | public MessageBoxViewModel CreateMessageBoxViewModel(string title, str...
    method CreateSettingsViewModel (line 110) | public SettingsViewModel CreateSettingsViewModel() =>

FILE: YoutubeDownloader/Framework/Window.cs
  class Window (line 6) | public class Window<TDataContext> : Window

FILE: YoutubeDownloader/Localization/Language.cs
  type Language (line 3) | public enum Language

FILE: YoutubeDownloader/Localization/LocalizationManager.English.cs
  class LocalizationManager (line 5) | public partial class LocalizationManager

FILE: YoutubeDownloader/Localization/LocalizationManager.French.cs
  class LocalizationManager (line 5) | public partial class LocalizationManager

FILE: YoutubeDownloader/Localization/LocalizationManager.German.cs
  class LocalizationManager (line 5) | public partial class LocalizationManager

FILE: YoutubeDownloader/Localization/LocalizationManager.Spanish.cs
  class LocalizationManager (line 5) | public partial class LocalizationManager

FILE: YoutubeDownloader/Localization/LocalizationManager.Ukrainian.cs
  class LocalizationManager (line 5) | public partial class LocalizationManager

FILE: YoutubeDownloader/Localization/LocalizationManager.cs
  class LocalizationManager (line 11) | public partial class LocalizationManager : ObservableObject, IDisposable
    method LocalizationManager (line 15) | public LocalizationManager(SettingsService settingsService)
    method Get (line 40) | private string Get([CallerMemberName] string? key = null)
    method Dispose (line 75) | public void Dispose() => _eventRoot.Dispose();
  class LocalizationManager (line 78) | public partial class LocalizationManager
    method LocalizationManager (line 15) | public LocalizationManager(SettingsService settingsService)
    method Get (line 40) | private string Get([CallerMemberName] string? key = null)
    method Dispose (line 75) | public void Dispose() => _eventRoot.Dispose();

FILE: YoutubeDownloader/Program.cs
  class Program (line 9) | public static class Program
    method BuildAvaloniaApp (line 25) | public static AppBuilder BuildAvaloniaApp() =>
    method Main (line 28) | [STAThread]

FILE: YoutubeDownloader/Services/SettingsService.AuthCookiesEncryptionConverter.cs
  class SettingsService (line 13) | public partial class SettingsService
    class AuthCookiesEncryptionConverter (line 15) | private class AuthCookiesEncryptionConverter : JsonConverter<IReadOnly...
      method Read (line 27) | public override IReadOnlyList<Cookie>? Read(
      method Write (line 72) | public override void Write(
      type CookieData (line 107) | private record CookieData(string Name, string Value, string Path, st...

FILE: YoutubeDownloader/Services/SettingsService.cs
  class SettingsService (line 15) | [ObservableObject]
    method Save (line 67) | public override void Save()
    class ContainerJsonConverter (line 82) | private class ContainerJsonConverter : JsonConverter<Container>
      method Read (line 84) | public override Container Read(
      method Write (line 116) | public override void Write(
  class SettingsService (line 80) | public partial class SettingsService
    method Save (line 67) | public override void Save()
    class ContainerJsonConverter (line 82) | private class ContainerJsonConverter : JsonConverter<Container>
      method Read (line 84) | public override Container Read(
      method Write (line 116) | public override void Write(
  class SettingsService (line 129) | public partial class SettingsService
    method Save (line 67) | public override void Save()
    class ContainerJsonConverter (line 82) | private class ContainerJsonConverter : JsonConverter<Container>
      method Read (line 84) | public override Container Read(
      method Write (line 116) | public override void Write(

FILE: YoutubeDownloader/Services/UpdateService.cs
  class UpdateService (line 11) | public class UpdateService(SettingsService settingsService) : IDisposable
    method CheckForUpdatesAsync (line 35) | public async Task<Version?> CheckForUpdatesAsync()
    method PrepareUpdateAsync (line 47) | public async Task PrepareUpdateAsync(Version version)
    method FinalizeUpdate (line 70) | public void FinalizeUpdate(bool needRestart)
    method Dispose (line 96) | public void Dispose() => _updateManager?.Dispose();

FILE: YoutubeDownloader/StartOptions.cs
  class StartOptions (line 6) | public partial class StartOptions
  class StartOptions (line 11) | public partial class StartOptions

FILE: YoutubeDownloader/Utils/Disposable.cs
  class Disposable (line 5) | internal class Disposable(Action dispose) : IDisposable
    method Create (line 7) | public static IDisposable Create(Action dispose) => new Disposable(dis...
    method Dispose (line 9) | public void Dispose() => dispose();

FILE: YoutubeDownloader/Utils/DisposableCollector.cs
  class DisposableCollector (line 7) | internal class DisposableCollector : IDisposable
    method Add (line 12) | public void Add(IDisposable item)
    method Dispose (line 20) | public void Dispose()

FILE: YoutubeDownloader/Utils/Extensions/AvaloniaExtensions.cs
  class AvaloniaExtensions (line 7) | internal static class AvaloniaExtensions
    method extension (line 9) | extension(IApplicationLifetime lifetime)

FILE: YoutubeDownloader/Utils/Extensions/DirectoryExtensions.cs
  class DirectoryExtensions (line 5) | internal static class DirectoryExtensions
    method extension (line 7) | extension(Directory)

FILE: YoutubeDownloader/Utils/Extensions/DisposableExtensions.cs
  class DisposableExtensions (line 7) | internal static class DisposableExtensions
    method extension (line 9) | extension(IEnumerable<IDisposable> disposables)

FILE: YoutubeDownloader/Utils/Extensions/EnvironmentExtensions.cs
  class EnvironmentExtensions (line 6) | internal static class EnvironmentExtensions
    method extension (line 8) | extension(Environment)

FILE: YoutubeDownloader/Utils/Extensions/NotifyPropertyChangedExtensions.cs
  class NotifyPropertyChangedExtensions (line 8) | internal static class NotifyPropertyChangedExtensions
    method WatchProperty (line 13) | public IDisposable WatchProperty<TProperty>(
    method WatchAllProperties (line 42) | public IDisposable WatchAllProperties(Action callback, bool watchIniti...

FILE: YoutubeDownloader/Utils/Extensions/PathExtensions.cs
  class PathExtensions (line 5) | internal static class PathExtensions
    method extension (line 7) | extension(Path)

FILE: YoutubeDownloader/Utils/Extensions/ProcessExtensions.cs
  class ProcessExtensions (line 6) | internal static class ProcessExtensions
    method extension (line 8) | extension(Process)

FILE: YoutubeDownloader/Utils/NativeMethods.cs
  class NativeMethods (line 5) | internal static class NativeMethods
    class Windows (line 7) | public static class Windows
      method MessageBox (line 9) | [DllImport("user32.dll", SetLastError = true)]

FILE: YoutubeDownloader/Utils/ResizableSemaphore.cs
  class ResizableSemaphore (line 9) | internal partial class ResizableSemaphore : IDisposable
    method Refresh (line 38) | private void Refresh()
    method AcquireAsync (line 53) | public async Task<IDisposable> AcquireAsync(CancellationToken cancella...
    method Release (line 77) | private void Release()
    method Dispose (line 86) | public void Dispose()
    class AcquiredAccess (line 100) | private class AcquiredAccess(ResizableSemaphore semaphore) : IDisposable
      method Dispose (line 104) | public void Dispose()
  class ResizableSemaphore (line 98) | internal partial class ResizableSemaphore
    method Refresh (line 38) | private void Refresh()
    method AcquireAsync (line 53) | public async Task<IDisposable> AcquireAsync(CancellationToken cancella...
    method Release (line 77) | private void Release()
    method Dispose (line 86) | public void Dispose()
    class AcquiredAccess (line 100) | private class AcquiredAccess(ResizableSemaphore semaphore) : IDisposable
      method Dispose (line 104) | public void Dispose()

FILE: YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs
  class DashboardViewModel (line 23) | public partial class DashboardViewModel : ViewModelBase
    method DashboardViewModel (line 34) | public DashboardViewModel(
    method CanShowAuthSetup (line 85) | private bool CanShowAuthSetup() => !IsBusy;
    method ShowAuthSetupAsync (line 87) | [RelayCommand(CanExecute = nameof(CanShowAuthSetup))]
    method CanShowSettings (line 91) | private bool CanShowSettings() => !IsBusy;
    method ShowSettingsAsync (line 93) | [RelayCommand(CanExecute = nameof(CanShowSettings))]
    method EnqueueDownload (line 97) | private async void EnqueueDownload(DownloadViewModel download, int pos...
    method CanProcessQuery (line 174) | private bool CanProcessQuery() => !IsBusy && !string.IsNullOrWhiteSpac...
    method ProcessQueryAsync (line 176) | [RelayCommand(CanExecute = nameof(CanProcessQuery))]
    method RemoveDownload (line 297) | private void RemoveDownload(DownloadViewModel download)
    method RemoveSuccessfulDownloads (line 304) | [RelayCommand]
    method RemoveInactiveDownloads (line 314) | [RelayCommand]
    method RestartDownload (line 329) | [RelayCommand]
    method RestartFailedDownloads (line 350) | [RelayCommand]
    method CancelAllDownloads (line 360) | [RelayCommand]
    method Dispose (line 367) | protected override void Dispose(bool disposing)

FILE: YoutubeDownloader/ViewModels/Components/DownloadStatus.cs
  type DownloadStatus (line 3) | public enum DownloadStatus

FILE: YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs
  class DownloadViewModel (line 19) | public partial class DownloadViewModel : ViewModelBase
    method DownloadViewModel (line 29) | public DownloadViewModel(
    method CanCancel (line 83) | private bool CanCancel() => Status is DownloadStatus.Enqueued or Downl...
    method Cancel (line 85) | [RelayCommand(CanExecute = nameof(CanCancel))]
    method CanShowFile (line 94) | private bool CanShowFile() =>
    method ShowFileAsync (line 99) | [RelayCommand(CanExecute = nameof(CanShowFile))]
    method CanOpenFile (line 121) | private bool CanOpenFile() => Status == DownloadStatus.Completed;
    method OpenFileAsync (line 123) | [RelayCommand(CanExecute = nameof(CanOpenFile))]
    method CopyErrorMessageAsync (line 144) | [RelayCommand]
    method Dispose (line 154) | protected override void Dispose(bool disposing)

FILE: YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs
  class AuthSetupViewModel (line 13) | public class AuthSetupViewModel : DialogViewModelBase
    method AuthSetupViewModel (line 18) | public AuthSetupViewModel(
    method Dispose (line 54) | protected override void Dispose(bool disposing)

FILE: YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs
  class DownloadMultipleSetupViewModel (line 21) | public partial class DownloadMultipleSetupViewModel(
    method Initialize (line 52) | [RelayCommand]
    method CopyTitleAsync (line 60) | [RelayCommand]
    method CanConfirm (line 67) | private bool CanConfirm() => SelectedVideos.Any();
    method ConfirmAsync (line 69) | [RelayCommand(CanExecute = nameof(CanConfirm))]

FILE: YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs
  class DownloadSingleSetupViewModel (line 19) | public partial class DownloadSingleSetupViewModel(
    method Initialize (line 37) | [RelayCommand]
    method CopyTitleAsync (line 45) | [RelayCommand]
    method ConfirmAsync (line 52) | [RelayCommand]

FILE: YoutubeDownloader/ViewModels/Dialogs/MessageBoxViewModel.cs
  class MessageBoxViewModel (line 7) | public partial class MessageBoxViewModel : DialogViewModelBase
    method MessageBoxViewModel (line 9) | public MessageBoxViewModel(LocalizationManager localizationManager)

FILE: YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs
  class SettingsViewModel (line 14) | public partial class SettingsViewModel : DialogViewModelBase
    method SettingsViewModel (line 21) | public SettingsViewModel(
    method BrowseFFmpegFilePathAsync (line 106) | [RelayCommand]
    method ResetFFmpegFilePath (line 125) | [RelayCommand]
    method Dispose (line 128) | protected override void Dispose(bool disposing)

FILE: YoutubeDownloader/ViewModels/MainViewModel.cs
  class MainViewModel (line 17) | public partial class MainViewModel(
    method ShowUkraineSupportMessageAsync (line 30) | private async Task ShowUkraineSupportMessageAsync()
    method ShowDevelopmentBuildMessageAsync (line 50) | private async Task ShowDevelopmentBuildMessageAsync()
    method ShowFFmpegMissingMessageAsync (line 70) | private async Task ShowFFmpegMissingMessageAsync()
    method CheckForUpdatesAsync (line 121) | private async Task CheckForUpdatesAsync()
    method InitializeAsync (line 157) | [RelayCommand]
    method Dispose (line 166) | protected override void Dispose(bool disposing)

FILE: YoutubeDownloader/Views/Components/DashboardView.axaml.cs
  class DashboardView (line 9) | public partial class DashboardView : UserControl<DashboardViewModel>
    method DashboardView (line 11) | public DashboardView()
    method UserControl_OnLoaded (line 19) | private void UserControl_OnLoaded(object? sender, RoutedEventArgs args...
    method QueryTextBox_OnKeyDown (line 21) | private void QueryTextBox_OnKeyDown(object? sender, KeyEventArgs args)
    method StatusTextBlock_OnPointerReleased (line 32) | private void StatusTextBlock_OnPointerReleased(object sender, PointerR...

FILE: YoutubeDownloader/Views/Dialogs/AuthSetupView.axaml.cs
  class AuthSetupView (line 12) | public partial class AuthSetupView : UserControl<AuthSetupViewModel>
    method AuthSetupView (line 20) | public AuthSetupView() => InitializeComponent();
    method NavigateToLoginPage (line 22) | private void NavigateToLoginPage() => WebBrowser.Url = new Uri(LoginPa...
    method LogOutButton_OnClick (line 24) | private void LogOutButton_OnClick(object sender, RoutedEventArgs args)
    method WebBrowser_OnLoaded (line 30) | private void WebBrowser_OnLoaded(object sender, RoutedEventArgs args) ...
    method WebBrowser_OnWebViewCreated (line 32) | private void WebBrowser_OnWebViewCreated(object sender, WebViewCreated...
    method WebBrowser_OnNavigationStarting (line 53) | private async void WebBrowser_OnNavigationStarting(

FILE: YoutubeDownloader/Views/Dialogs/DownloadMultipleSetupView.axaml.cs
  class DownloadMultipleSetupView (line 7) | public partial class DownloadMultipleSetupView : UserControl<DownloadMul...
    method DownloadMultipleSetupView (line 9) | public DownloadMultipleSetupView() => InitializeComponent();
    method UserControl_OnLoaded (line 11) | private void UserControl_OnLoaded(object? sender, RoutedEventArgs args...

FILE: YoutubeDownloader/Views/Dialogs/DownloadSingleSetupView.axaml.cs
  class DownloadSingleSetupView (line 7) | public partial class DownloadSingleSetupView : UserControl<DownloadSingl...
    method DownloadSingleSetupView (line 9) | public DownloadSingleSetupView() => InitializeComponent();
    method UserControl_OnLoaded (line 11) | private void UserControl_OnLoaded(object? sender, RoutedEventArgs args...

FILE: YoutubeDownloader/Views/Dialogs/MessageBoxView.axaml.cs
  class MessageBoxView (line 6) | public partial class MessageBoxView : UserControl<MessageBoxViewModel>
    method MessageBoxView (line 8) | public MessageBoxView() => InitializeComponent();

FILE: YoutubeDownloader/Views/Dialogs/SettingsView.axaml.cs
  class SettingsView (line 6) | public partial class SettingsView : UserControl<SettingsViewModel>
    method SettingsView (line 8) | public SettingsView() => InitializeComponent();

FILE: YoutubeDownloader/Views/MainView.axaml.cs
  class MainView (line 7) | public partial class MainView : Window<MainViewModel>
    method MainView (line 9) | public MainView() => InitializeComponent();
    method DialogHost_OnLoaded (line 11) | private void DialogHost_OnLoaded(object? sender, RoutedEventArgs args) =>
Condensed preview — 103 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (280K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.yml",
    "chars": 3357,
    "preview": "name: 🐛 Bug report\ndescription: Report broken functionality.\nlabels: [bug]\n\nbody:\n  - type: markdown\n    attributes:\n   "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 635,
    "preview": "blank_issues_enabled: false\ncontact_links:\n  - name: ⚠ Feature request\n    url: https://github.com/Tyrrrz/.github/blob/p"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 385,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    l"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 6432,
    "preview": "name: main\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - prime\n    tags:\n      - \"*\"\n  pull_request:\n    branc"
  },
  {
    "path": ".gitignore",
    "chars": 138,
    "preview": "# User-specific files\n.vs/\n.idea/\n*.suo\n*.user\n\n# Build results\nbin/\nobj/\n\n# Avalonia\n.avalonia-build-tasks/\n\n# Test res"
  },
  {
    "path": "Directory.Build.props",
    "chars": 422,
    "preview": "<Project>\n\n  <PropertyGroup>\n    <TargetFramework>net10.0</TargetFramework>\n    <Version>999.9.9-dev</Version>\n    <Comp"
  },
  {
    "path": "Directory.Packages.props",
    "chars": 1788,
    "preview": "<Project>\n  <PropertyGroup>\n    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>\n  </PropertyGroup>"
  },
  {
    "path": "License.txt",
    "chars": 1074,
    "preview": "MIT License\n\nCopyright (c) 2018-2026 Oleksii Holub\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "NuGet.config",
    "chars": 320,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <add key=\"nuget.org\" value=\""
  },
  {
    "path": "Readme.md",
    "chars": 3932,
    "preview": "# YoutubeDownloader\n\n[![Status](https://img.shields.io/badge/status-maintenance-ffd700.svg)](https://github.com/Tyrrrz/."
  },
  {
    "path": "YoutubeDownloader/.gitignore",
    "chars": 8,
    "preview": "/ffmpeg*"
  },
  {
    "path": "YoutubeDownloader/App.axaml",
    "chars": 8761,
    "preview": "<Application\n    x:Class=\"YoutubeDownloader.App\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://schemas.m"
  },
  {
    "path": "YoutubeDownloader/App.axaml.cs",
    "chars": 5084,
    "preview": "using System;\nusing Avalonia;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.Markup.Xaml;\nusing Avalonia.M"
  },
  {
    "path": "YoutubeDownloader/Converters/EqualityConverter.cs",
    "chars": 750,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\n\nnamespace "
  },
  {
    "path": "YoutubeDownloader/Converters/MarkdownToInlinesConverter.cs",
    "chars": 3793,
    "preview": "using System;\nusing System.Globalization;\nusing Avalonia.Controls.Documents;\nusing Avalonia.Data.Converters;\nusing Avalo"
  },
  {
    "path": "YoutubeDownloader/Converters/VideoQualityPreferenceToStringConverter.cs",
    "chars": 747,
    "preview": "using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing YoutubeDownloader.Core.Downloading;\n\nna"
  },
  {
    "path": "YoutubeDownloader/Converters/VideoToHighestQualityThumbnailUrlStringConverter.cs",
    "chars": 757,
    "preview": "using System;\nusing System.Globalization;\nusing Avalonia.Data.Converters;\nusing YoutubeExplode.Common;\nusing YoutubeExp"
  },
  {
    "path": "YoutubeDownloader/Converters/VideoToLowestQualityThumbnailUrlStringConverter.cs",
    "chars": 745,
    "preview": "using System;\nusing System.Globalization;\nusing System.Linq;\nusing Avalonia.Data.Converters;\nusing YoutubeExplode.Video"
  },
  {
    "path": "YoutubeDownloader/Download-FFmpeg.ps1",
    "chars": 1783,
    "preview": "param (\n    [Parameter(Mandatory=$false)]\n    [string]$Platform,\n\n    [Parameter(Mandatory=$false)]\n    [string]$OutputP"
  },
  {
    "path": "YoutubeDownloader/Framework/DialogManager.cs",
    "chars": 3767,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading;\nusing Syste"
  },
  {
    "path": "YoutubeDownloader/Framework/DialogViewModelBase.cs",
    "chars": 727,
    "preview": "using System.Threading.Tasks;\nusing CommunityToolkit.Mvvm.ComponentModel;\nusing CommunityToolkit.Mvvm.Input;\n\nnamespace"
  },
  {
    "path": "YoutubeDownloader/Framework/SnackbarManager.cs",
    "chars": 931,
    "preview": "using System;\nusing Avalonia.Threading;\nusing Material.Styles.Controls;\nusing Material.Styles.Models;\n\nnamespace Youtub"
  },
  {
    "path": "YoutubeDownloader/Framework/ThemeVariant.cs",
    "chars": 103,
    "preview": "namespace YoutubeDownloader.Framework;\n\npublic enum ThemeVariant\n{\n    System,\n    Light,\n    Dark,\n}\n"
  },
  {
    "path": "YoutubeDownloader/Framework/UserControl.cs",
    "chars": 508,
    "preview": "using System;\nusing Avalonia.Controls;\n\nnamespace YoutubeDownloader.Framework;\n\npublic class UserControl<TDataContext> "
  },
  {
    "path": "YoutubeDownloader/Framework/ViewManager.cs",
    "chars": 1420,
    "preview": "using Avalonia.Controls;\nusing Avalonia.Controls.Templates;\nusing YoutubeDownloader.ViewModels;\nusing YoutubeDownloader"
  },
  {
    "path": "YoutubeDownloader/Framework/ViewModelBase.cs",
    "chars": 446,
    "preview": "using System;\nusing CommunityToolkit.Mvvm.ComponentModel;\n\nnamespace YoutubeDownloader.Framework;\n\npublic abstract clas"
  },
  {
    "path": "YoutubeDownloader/Framework/ViewModelManager.cs",
    "chars": 3331,
    "preview": "using System;\nusing System.Collections.Generic;\nusing Microsoft.Extensions.DependencyInjection;\nusing YoutubeDownloader"
  },
  {
    "path": "YoutubeDownloader/Framework/Window.cs",
    "chars": 498,
    "preview": "using System;\nusing Avalonia.Controls;\n\nnamespace YoutubeDownloader.Framework;\n\npublic class Window<TDataContext> : Win"
  },
  {
    "path": "YoutubeDownloader/Localization/Language.cs",
    "chars": 145,
    "preview": "namespace YoutubeDownloader.Localization;\n\npublic enum Language\n{\n    System,\n    English,\n    Ukrainian,\n    German,\n  "
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.English.cs",
    "chars": 8056,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Localization;\n\npublic partial class LocalizationManager\n{"
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.French.cs",
    "chars": 8272,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Localization;\n\npublic partial class LocalizationManager\n{"
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.German.cs",
    "chars": 8149,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Localization;\n\npublic partial class LocalizationManager\n{"
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.Spanish.cs",
    "chars": 8505,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Localization;\n\npublic partial class LocalizationManager\n{"
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.Ukrainian.cs",
    "chars": 7996,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Localization;\n\npublic partial class LocalizationManager\n{"
  },
  {
    "path": "YoutubeDownloader/Localization/LocalizationManager.cs",
    "chars": 6266,
    "preview": "using System;\nusing System.Globalization;\nusing System.Runtime.CompilerServices;\nusing CommunityToolkit.Mvvm.ComponentMo"
  },
  {
    "path": "YoutubeDownloader/Program.cs",
    "chars": 1572,
    "preview": "using System;\nusing System.Reflection;\nusing Avalonia;\nusing Avalonia.WebView.Desktop;\nusing YoutubeDownloader.Utils;\n\n"
  },
  {
    "path": "YoutubeDownloader/Publish-MacOSBundle.ps1",
    "chars": 2925,
    "preview": "param(\n    [Parameter(Mandatory=$true)]\n    [string]$PublishDirPath,\n\n    [Parameter(Mandatory=$true)]\n    [string]$Icon"
  },
  {
    "path": "YoutubeDownloader/Services/SettingsService.AuthCookiesEncryptionConverter.cs",
    "chars": 3552,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Security.Cryptography;"
  },
  {
    "path": "YoutubeDownloader/Services/SettingsService.cs",
    "chars": 4074,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Net;\nusing System.Text.Json;\nusing System.Text.Json.Serial"
  },
  {
    "path": "YoutubeDownloader/Services/UpdateService.cs",
    "chars": 2727,
    "preview": "using System;\nusing System.Runtime.InteropServices;\nusing System.Threading.Tasks;\nusing Onova;\nusing Onova.Exceptions;\n"
  },
  {
    "path": "YoutubeDownloader/StartOptions.cs",
    "chars": 710,
    "preview": "using System;\nusing System.IO;\n\nnamespace YoutubeDownloader;\n\npublic partial class StartOptions\n{\n    public required st"
  },
  {
    "path": "YoutubeDownloader/Utils/Disposable.cs",
    "chars": 234,
    "preview": "using System;\n\nnamespace YoutubeDownloader.Utils;\n\ninternal class Disposable(Action dispose) : IDisposable\n{\n    public"
  },
  {
    "path": "YoutubeDownloader/Utils/DisposableCollector.cs",
    "chars": 539,
    "preview": "using System;\nusing System.Collections.Generic;\nusing YoutubeDownloader.Utils.Extensions;\n\nnamespace YoutubeDownloader."
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/AvaloniaExtensions.cs",
    "chars": 1094,
    "preview": "using Avalonia.Controls;\nusing Avalonia.Controls.ApplicationLifetimes;\nusing Avalonia.VisualTree;\n\nnamespace YoutubeDow"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/DirectoryExtensions.cs",
    "chars": 420,
    "preview": "using System.IO;\n\nnamespace YoutubeDownloader.Utils.Extensions;\n\ninternal static class DirectoryExtensions\n{\n    extens"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/DisposableExtensions.cs",
    "chars": 738,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace YoutubeDownloader.Utils.Extensions;\n\ninte"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/EnvironmentExtensions.cs",
    "chars": 1559,
    "preview": "using System;\nusing System.IO;\n\nnamespace YoutubeDownloader.Utils.Extensions;\n\ninternal static class EnvironmentExtensio"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/NotifyPropertyChangedExtensions.cs",
    "chars": 1783,
    "preview": "using System;\nusing System.ComponentModel;\nusing System.Linq.Expressions;\nusing System.Reflection;\n\nnamespace YoutubeDo"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/PathExtensions.cs",
    "chars": 1012,
    "preview": "using System.IO;\n\nnamespace YoutubeDownloader.Utils.Extensions;\n\ninternal static class PathExtensions\n{\n    extension(P"
  },
  {
    "path": "YoutubeDownloader/Utils/Extensions/ProcessExtensions.cs",
    "chars": 1088,
    "preview": "using System.Collections.Generic;\nusing System.Diagnostics;\n\nnamespace YoutubeDownloader.Utils.Extensions;\n\ninternal st"
  },
  {
    "path": "YoutubeDownloader/Utils/NativeMethods.cs",
    "chars": 311,
    "preview": "using System.Runtime.InteropServices;\n\nnamespace YoutubeDownloader.Utils;\n\ninternal static class NativeMethods\n{\n    pu"
  },
  {
    "path": "YoutubeDownloader/Utils/ResizableSemaphore.cs",
    "chars": 2772,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace Youtub"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Components/DashboardViewModel.cs",
    "chars": 12431,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.IO;\nusing System.Lin"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Components/DownloadStatus.cs",
    "chars": 152,
    "preview": "namespace YoutubeDownloader.ViewModels.Components;\n\npublic enum DownloadStatus\n{\n    Enqueued,\n    Started,\n    Complet"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Components/DownloadViewModel.cs",
    "chars": 4884,
    "preview": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing Av"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Dialogs/AuthSetupViewModel.cs",
    "chars": 1763,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing YoutubeDownloader.Framework;"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Dialogs/DownloadMultipleSetupViewModel.cs",
    "chars": 4065,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Collections.ObjectModel;\nusing System.IO;\nusing System.Lin"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Dialogs/DownloadSingleSetupViewModel.cs",
    "chars": 2596,
    "preview": "using System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Avalonia;\nusi"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Dialogs/MessageBoxViewModel.cs",
    "chars": 1380,
    "preview": "using CommunityToolkit.Mvvm.ComponentModel;\nusing YoutubeDownloader.Framework;\nusing YoutubeDownloader.Localization;\n\nn"
  },
  {
    "path": "YoutubeDownloader/ViewModels/Dialogs/SettingsViewModel.cs",
    "chars": 3852,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Threading.Tasks;\nusing Avalonia.Platform.Storage;\nusing Co"
  },
  {
    "path": "YoutubeDownloader/ViewModels/MainViewModel.cs",
    "chars": 6005,
    "preview": "using System;\nusing System.Diagnostics;\nusing System.IO;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing Avaloni"
  },
  {
    "path": "YoutubeDownloader/Views/Components/DashboardView.axaml",
    "chars": 19966,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Components.DashboardView\"\n    xmlns=\"https://github.com/avaloniaui\"\n  "
  },
  {
    "path": "YoutubeDownloader/Views/Components/DashboardView.axaml.cs",
    "chars": 1329,
    "preview": "using Avalonia;\nusing Avalonia.Input;\nusing Avalonia.Interactivity;\nusing YoutubeDownloader.Framework;\nusing YoutubeDown"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/AuthSetupView.axaml",
    "chars": 3529,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Dialogs.AuthSetupView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    x"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/AuthSetupView.axaml.cs",
    "chars": 2647,
    "preview": "using System;\nusing System.Linq;\nusing Avalonia.Interactivity;\nusing Avalonia.WebView.Windows.Core;\nusing Microsoft.Web"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/DownloadMultipleSetupView.axaml",
    "chars": 8009,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Dialogs.DownloadMultipleSetupView\"\n    xmlns=\"https://github.com/avalo"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/DownloadMultipleSetupView.axaml.cs",
    "chars": 449,
    "preview": "using Avalonia.Interactivity;\nusing YoutubeDownloader.Framework;\nusing YoutubeDownloader.ViewModels.Dialogs;\n\nnamespace "
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/DownloadSingleSetupView.axaml",
    "chars": 5386,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Dialogs.DownloadSingleSetupView\"\n    xmlns=\"https://github.com/avaloni"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/DownloadSingleSetupView.axaml.cs",
    "chars": 443,
    "preview": "using Avalonia.Interactivity;\nusing YoutubeDownloader.Framework;\nusing YoutubeDownloader.ViewModels.Dialogs;\n\nnamespace "
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/MessageBoxView.axaml",
    "chars": 2680,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Dialogs.MessageBoxView\"\n    xmlns=\"https://github.com/avaloniaui\"\n   "
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/MessageBoxView.axaml.cs",
    "chars": 253,
    "preview": "using YoutubeDownloader.Framework;\nusing YoutubeDownloader.ViewModels.Dialogs;\n\nnamespace YoutubeDownloader.Views.Dialog"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/SettingsView.axaml",
    "chars": 10944,
    "preview": "<UserControl\n    x:Class=\"YoutubeDownloader.Views.Dialogs.SettingsView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xm"
  },
  {
    "path": "YoutubeDownloader/Views/Dialogs/SettingsView.axaml.cs",
    "chars": 247,
    "preview": "using YoutubeDownloader.Framework;\nusing YoutubeDownloader.ViewModels.Dialogs;\n\nnamespace YoutubeDownloader.Views.Dialog"
  },
  {
    "path": "YoutubeDownloader/Views/MainView.axaml",
    "chars": 1163,
    "preview": "<Window\n    x:Class=\"YoutubeDownloader.Views.MainView\"\n    xmlns=\"https://github.com/avaloniaui\"\n    xmlns:x=\"http://sch"
  },
  {
    "path": "YoutubeDownloader/Views/MainView.axaml.cs",
    "chars": 376,
    "preview": "using Avalonia.Interactivity;\nusing YoutubeDownloader.Framework;\nusing YoutubeDownloader.ViewModels;\n\nnamespace YoutubeD"
  },
  {
    "path": "YoutubeDownloader/YoutubeDownloader.csproj",
    "chars": 4820,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>WinExe</OutputType>\n    <ApplicationIcon>..\\favicon."
  },
  {
    "path": "YoutubeDownloader/app.manifest",
    "chars": 686,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<assembly manifestVersion=\"1.0\" xmlns=\"urn:schemas-microsoft-com:asm.v1\">\n  <ass"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/FFmpeg.cs",
    "chars": 2090,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nnamespace YoutubeDownloader.Core.D"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/FileNameTemplate.cs",
    "chars": 1129,
    "preview": "using System;\nusing System.IO;\nusing YoutubeDownloader.Core.Utils.Extensions;\nusing YoutubeExplode.Videos;\nusing Youtub"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/VideoDownloadOption.cs",
    "chars": 6359,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing YoutubeDownloader.Core.Utils.Extensions;\nusing"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/VideoDownloadPreference.cs",
    "chars": 2068,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing YoutubeExplode.Videos.Streams;\n\nnamespace Yout"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/VideoDownloader.cs",
    "chars": 2952,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Net;\nusing System.Threading;\nusing System"
  },
  {
    "path": "YoutubeDownloader.Core/Downloading/VideoQualityPreference.cs",
    "chars": 954,
    "preview": "using System;\n\nnamespace YoutubeDownloader.Core.Downloading;\n\npublic enum VideoQualityPreference\n{\n    // ReSharper dis"
  },
  {
    "path": "YoutubeDownloader.Core/Resolving/QueryResolver.cs",
    "chars": 4559,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net;\nusing System.Threading;\nusing Syst"
  },
  {
    "path": "YoutubeDownloader.Core/Resolving/QueryResult.cs",
    "chars": 1008,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing YoutubeExplode.Videos;\n\nnamespace YoutubeDownl"
  },
  {
    "path": "YoutubeDownloader.Core/Resolving/QueryResultKind.cs",
    "chars": 143,
    "preview": "namespace YoutubeDownloader.Core.Resolving;\n\npublic enum QueryResultKind\n{\n    Video,\n    Playlist,\n    Channel,\n    Se"
  },
  {
    "path": "YoutubeDownloader.Core/Tagging/MediaFile.cs",
    "chars": 1018,
    "preview": "using System;\nusing TagLib;\nusing TagFile = TagLib.File;\n\nnamespace YoutubeDownloader.Core.Tagging;\n\ninternal partial c"
  },
  {
    "path": "YoutubeDownloader.Core/Tagging/MediaTagInjector.cs",
    "chars": 3206,
    "preview": "using System;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing YoutubeDownloader.Core.Uti"
  },
  {
    "path": "YoutubeDownloader.Core/Tagging/MusicBrainzClient.cs",
    "chars": 2351,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Runtime.CompilerServices;\nusing System."
  },
  {
    "path": "YoutubeDownloader.Core/Tagging/MusicBrainzRecording.cs",
    "chars": 164,
    "preview": "namespace YoutubeDownloader.Core.Tagging;\n\ninternal record MusicBrainzRecording(\n    string Artist,\n    string? ArtistS"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/AsyncCollectionExtensions.cs",
    "chars": 629,
    "preview": "using System.Collections.Generic;\nusing System.Runtime.CompilerServices;\nusing System.Threading.Tasks;\n\nnamespace Youtu"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/CollectionExtensions.cs",
    "chars": 324,
    "preview": "using System.Collections.Generic;\n\nnamespace YoutubeDownloader.Core.Utils.Extensions;\n\npublic static class CollectionEx"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/GenericExtensions.cs",
    "chars": 231,
    "preview": "using System;\n\nnamespace YoutubeDownloader.Core.Utils.Extensions;\n\npublic static class GenericExtensions\n{\n    extensio"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/PathExtensions.cs",
    "chars": 476,
    "preview": "using System.IO;\nusing System.Linq;\nusing System.Text;\n\nnamespace YoutubeDownloader.Core.Utils.Extensions;\n\npublic stat"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/StringExtensions.cs",
    "chars": 223,
    "preview": "namespace YoutubeDownloader.Core.Utils.Extensions;\n\npublic static class StringExtensions\n{\n    extension(string str)\n  "
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Extensions/YoutubeExtensions.cs",
    "chars": 322,
    "preview": "using System.IO;\nusing YoutubeExplode.Common;\n\nnamespace YoutubeDownloader.Core.Utils.Extensions;\n\npublic static class "
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Http.cs",
    "chars": 630,
    "preview": "using System.Net.Http;\nusing System.Net.Http.Headers;\nusing System.Reflection;\n\nnamespace YoutubeDownloader.Core.Utils;"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/ThrottleLock.cs",
    "chars": 938,
    "preview": "using System;\nusing System.Threading;\nusing System.Threading.Tasks;\n\nnamespace YoutubeDownloader.Core.Utils;\n\npublic cl"
  },
  {
    "path": "YoutubeDownloader.Core/Utils/Url.cs",
    "chars": 292,
    "preview": "using System.Text.RegularExpressions;\nusing YoutubeDownloader.Core.Utils.Extensions;\n\nnamespace YoutubeDownloader.Core."
  },
  {
    "path": "YoutubeDownloader.Core/YoutubeDownloader.Core.csproj",
    "chars": 395,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n  <ItemGroup>\n    <PackageReference Include=\"CSharpier.MsBuild\" PrivateAssets=\"all\" />"
  },
  {
    "path": "YoutubeDownloader.sln",
    "chars": 2039,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.7.3392"
  },
  {
    "path": "global.json",
    "chars": 80,
    "preview": "{\n  \"sdk\": {\n    \"version\": \"10.0.100\",\n    \"rollForward\": \"latestFeature\"\n  }\n}"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the Tyrrrz/YoutubeDownloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 103 files (255.6 KB), approximately 54.9k tokens, and a symbol index with 268 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!