Full Code of TypNull/Tubifarry for AI

master f6ed443d1819 cached
282 files
1.6 MB
356.8k tokens
2440 symbols
1 requests
Download .txt
Showing preview only (1,742K chars total). Download the full file or copy to clipboard to get everything.
Repository: TypNull/Tubifarry
Branch: master
Commit: f6ed443d1819
Files: 282
Total size: 1.6 MB

Directory structure:
gitextract_31q9a7rf/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── documentation_improvement.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── branch-detection.yml
│       ├── compilation.yml
│       ├── dotnet-setup.yml
│       ├── git-info.yml
│       ├── main.yml
│       ├── metadata.yml
│       ├── packaging.yml
│       ├── publishing.yml
│       └── release-notes.yml
├── .gitignore
├── .gitmodules
├── CONTRIBUTION.md
├── Directory.Build.props
├── LICENSE.txt
├── NuGet.config
├── README.md
├── Tubifarry/
│   ├── Blocklisting/
│   │   ├── BaseBlocklist.cs
│   │   └── Blocklists.cs
│   ├── Core/
│   │   ├── Model/
│   │   │   ├── AlbumData.cs
│   │   │   ├── ApiCircuitBreaker.cs
│   │   │   ├── AudioMetadataHandler.cs
│   │   │   ├── FileCache.cs
│   │   │   ├── PlaylistItem.cs
│   │   │   └── TrustedSessionException.cs
│   │   ├── Records/
│   │   │   ├── Lyric.cs
│   │   │   ├── MappingAgent.cs
│   │   │   ├── MusicBrainzData.cs
│   │   │   └── YouTubeSession.cs
│   │   ├── Replacements/
│   │   │   ├── ExtendedHttpIndexerBase.cs
│   │   │   ├── FlexibleHttpDispatcher.cs
│   │   │   └── IIndexerRequestGenerator.cs
│   │   ├── Telemetry/
│   │   │   ├── ISearchContextBuffer.cs
│   │   │   ├── ISentryHelper.cs
│   │   │   ├── NoopSentryHelper.cs
│   │   │   ├── SearchContextBuffer.cs
│   │   │   ├── SentryEventFilter.cs
│   │   │   ├── SentryHelper.cs
│   │   │   ├── SlskdBufferedContext.cs
│   │   │   ├── SlskdSentryEvents.cs
│   │   │   ├── SlskdTrackingService.cs
│   │   │   ├── TubifarrySentry.cs
│   │   │   └── TubifarrySentryTarget.cs
│   │   └── Utilities/
│   │       ├── AudioFormat.cs
│   │       ├── CacheService.cs
│   │       ├── CookieManager.cs
│   │       ├── DynamicSchemaInjector.cs
│   │       ├── DynamicStateSettings.cs
│   │       ├── FileInfoParser.cs
│   │       ├── IndexerParserHelper.cs
│   │       ├── JsonConverters.cs
│   │       ├── LazyRequestChain.cs
│   │       ├── PermissionTester.cs
│   │       ├── PluginSettings.cs
│   │       ├── ReleaseFormatter.cs
│   │       ├── RepositorySettingsResolver.cs
│   │       └── UserAgentValidator.cs
│   ├── Debug.targets
│   ├── Download/
│   │   ├── Base/
│   │   │   ├── BaseDownloadManager.cs
│   │   │   ├── BaseDownloadOptions.cs
│   │   │   ├── BaseDownloadRequest.cs
│   │   │   └── BaseHttpClient.cs
│   │   └── Clients/
│   │       ├── DABMusic/
│   │       │   ├── DABMusicClient.cs
│   │       │   ├── DABMusicDownloadManager.cs
│   │       │   ├── DABMusicDownloadOptions.cs
│   │       │   ├── DABMusicDownloadRequest.cs
│   │       │   └── DABMusicProviderSettings.cs
│   │       ├── Lucida/
│   │       │   ├── ILucidaRateLimiter.cs
│   │       │   ├── LucidaClient.cs
│   │       │   ├── LucidaDownloadManager.cs
│   │       │   ├── LucidaDownloadOptions.cs
│   │       │   ├── LucidaDownloadRequest.cs
│   │       │   ├── LucidaInitiationResult.cs
│   │       │   ├── LucidaMetadataExtractor.cs
│   │       │   ├── LucidaProviderSettings.cs
│   │       │   ├── LucidaRateLimitException.cs
│   │       │   ├── LucidaRateLimiter.cs
│   │       │   ├── LucidaTokenExtractor.cs
│   │       │   └── LucidaWorkerState.cs
│   │       ├── Soulseek/
│   │       │   ├── ISlskdApiClient.cs
│   │       │   ├── ISlskdDownloadManager.cs
│   │       │   ├── Models/
│   │       │   │   ├── DownloadKey.cs
│   │       │   │   ├── SlskdDownloadDirectory.cs
│   │       │   │   ├── SlskdDownloadFile.cs
│   │       │   │   ├── SlskdDownloadItem.cs
│   │       │   │   └── SlskdFileState.cs
│   │       │   ├── SlskdApiClient.cs
│   │       │   ├── SlskdClient.cs
│   │       │   ├── SlskdDownloadManager.cs
│   │       │   ├── SlskdProviderSettings.cs
│   │       │   ├── SlskdRetryHandler.cs
│   │       │   └── SlskdStatusResolver.cs
│   │       ├── SubSonic/
│   │       │   ├── SubSonicClient.cs
│   │       │   ├── SubSonicDownloadManager.cs
│   │       │   ├── SubSonicDownloadOptions.cs
│   │       │   ├── SubSonicDownloadRequest.cs
│   │       │   └── SubSonicProviderSettings.cs
│   │       ├── TripleTriple/
│   │       │   ├── TripleTripleClient.cs
│   │       │   ├── TripleTripleDownloadManager.cs
│   │       │   ├── TripleTripleDownloadOptions.cs
│   │       │   ├── TripleTripleDownloadRequest.cs
│   │       │   └── TripleTripleProviderSettings.cs
│   │       └── YouTube/
│   │           ├── SponsorBlock.cs
│   │           ├── TrustedSessionHelper.cs
│   │           ├── YouTubeDownloadOptions.cs
│   │           ├── YouTubeDownloadRequest.cs
│   │           ├── YoutubeClient.cs
│   │           ├── YoutubeDownloadManager.cs
│   │           └── YoutubeProviderSettings.cs
│   ├── ILRepack.targets
│   ├── ImportLists/
│   │   ├── ArrStack/
│   │   │   ├── ArrMedia.cs
│   │   │   ├── ArrSoundtrackImport.cs
│   │   │   ├── ArrSoundtrackImportParser.cs
│   │   │   ├── ArrSoundtrackImportSettings.cs
│   │   │   └── ArrSoundtrackRequestGenerator.cs
│   │   ├── LastFmRecommendation/
│   │   │   ├── LastFmRecomendRequestGenerator.cs
│   │   │   ├── LastFmRecommend.cs
│   │   │   ├── LastFmRecommendParser.cs
│   │   │   ├── LastFmRecommendSettings.cs
│   │   │   └── LastFmRecords.cs
│   │   ├── ListenBrainz/
│   │   │   ├── ListenBrainzCFRecommendations/
│   │   │   │   ├── ListenBrainzCFRecommendationsImportList.cs
│   │   │   │   ├── ListenBrainzCFRecommendationsParser.cs
│   │   │   │   ├── ListenBrainzCFRecommendationsRequestGenerator.cs
│   │   │   │   └── ListenBrainzCFRecommendationsSettings.cs
│   │   │   ├── ListenBrainzCreatedForPlaylist/
│   │   │   │   ├── ListenBrainzCreatedForPlaylistImportList.cs
│   │   │   │   ├── ListenBrainzCreatedForPlaylistParser.cs
│   │   │   │   ├── ListenBrainzCreatedForPlaylistRequestGenerator.cs
│   │   │   │   └── ListenBrainzCreatedForPlaylistSettings.cs
│   │   │   ├── ListenBrainzPlaylist/
│   │   │   │   ├── ListenBrainzPlaylistImportList.cs
│   │   │   │   ├── ListenBrainzPlaylistParser.cs
│   │   │   │   ├── ListenBrainzPlaylistRequestGenerator.cs
│   │   │   │   └── ListenBrainzPlaylistSettings.cs
│   │   │   ├── ListenBrainzRecords.cs
│   │   │   └── ListenBrainzUserStats/
│   │   │       ├── ListenBrainzUserStatsImportList.cs
│   │   │       ├── ListenBrainzUserStatsParser.cs
│   │   │       ├── ListenBrainzUserStatsRequestGenerator.cs
│   │   │       └── ListenBrainzUserStatsSettings.cs
│   │   └── Spotify/
│   │       ├── SpotifyUserPlaylistImport.cs
│   │       └── SpotifyUserPlaylistImportSettings.cs
│   ├── Indexers/
│   │   ├── DABMusic/
│   │   │   ├── DABMusicIndexer.cs
│   │   │   ├── DABMusicIndexerSettings.cs
│   │   │   ├── DABMusicParser.cs
│   │   │   ├── DABMusicRecords.cs
│   │   │   ├── DABMusicRequestGenerator.cs
│   │   │   └── DABMusicSessionHelper.cs
│   │   ├── DownloadProtocols.cs
│   │   ├── Lucida/
│   │   │   ├── LucidaIndexer.cs
│   │   │   ├── LucidaIndexerSettings.cs
│   │   │   ├── LucidaRecords.cs
│   │   │   ├── LucidaRequestGenerator.cs
│   │   │   ├── LucidaRequestParser.cs
│   │   │   └── LucidaServiceHelper.cs
│   │   ├── Soulseek/
│   │   │   ├── ISlskdItemsParser.cs
│   │   │   ├── Search/
│   │   │   │   ├── Core/
│   │   │   │   │   ├── ISearchStrategy.cs
│   │   │   │   │   ├── QueryAnalyzer.cs
│   │   │   │   │   ├── SearchContext.cs
│   │   │   │   │   └── SearchPipeline.cs
│   │   │   │   ├── Strategies/
│   │   │   │   │   ├── BaseSearchStrategy.cs
│   │   │   │   │   ├── FallbackStrategy.cs
│   │   │   │   │   ├── SpecialCaseStrategy.cs
│   │   │   │   │   ├── TemplateSearchStrategy.cs
│   │   │   │   │   └── VariationStrategy.cs
│   │   │   │   ├── Templates/
│   │   │   │   │   └── TemplateEngine.cs
│   │   │   │   └── Transformers/
│   │   │   │       ├── QueryBuilder.cs
│   │   │   │       └── QueryNormalizer.cs
│   │   │   ├── SlsdkRecords.cs
│   │   │   ├── SlskdIndexer.cs
│   │   │   ├── SlskdIndexerParser.cs
│   │   │   ├── SlskdItemsParser.cs
│   │   │   ├── SlskdRequestGenerator.cs
│   │   │   ├── SlskdSettings.cs
│   │   │   └── SlskdTextProcessor.cs
│   │   ├── Spotify/
│   │   │   ├── SpotifyIndexerSettings.cs
│   │   │   ├── SpotifyParser.cs
│   │   │   ├── SpotifyRequestGenerator.cs
│   │   │   ├── SpotifyToYouTubeEnricher.cs
│   │   │   └── TubifarryIndexer.cs
│   │   ├── SubSonic/
│   │   │   ├── SubSonicAuthHelper.cs
│   │   │   ├── SubSonicIndexer.cs
│   │   │   ├── SubSonicIndexerParser.cs
│   │   │   ├── SubSonicIndexerSettings.cs
│   │   │   ├── SubSonicRecords.cs
│   │   │   └── SubSonicRequestGenerator.cs
│   │   ├── TripleTriple/
│   │   │   ├── TripleTripleIndexer.cs
│   │   │   ├── TripleTripleIndexerSettings.cs
│   │   │   ├── TripleTripleParser.cs
│   │   │   ├── TripleTripleRecords.cs
│   │   │   └── TripleTripleRequestGenerator.cs
│   │   └── YouTube/
│   │       ├── YoutubeIndexer.cs
│   │       ├── YoutubeIndexerSettings.cs
│   │       ├── YoutubeParser.cs
│   │       └── YoutubeRequestGenerator.cs
│   ├── Metadata/
│   │   ├── Converter/
│   │   │   ├── AudioConverter.cs
│   │   │   ├── AudioConverterSettings.cs
│   │   │   └── BitrateRules.cs
│   │   ├── Lyrics/
│   │   │   ├── LyricEnhancerSettings.cs
│   │   │   ├── LyricsEnhancer.cs
│   │   │   ├── LyricsHelper.cs
│   │   │   ├── LyricsProviders.cs
│   │   │   └── TrackFileRepositoryHelper.cs
│   │   ├── Proxy/
│   │   │   ├── MetadataProvider/
│   │   │   │   ├── AlbumMapper.cs
│   │   │   │   ├── CustomLidarr/
│   │   │   │   │   ├── CustomLidarrMetadataProxy.cs
│   │   │   │   │   ├── CustomLidarrMetadataProxySettings.cs
│   │   │   │   │   ├── CustomLidarrProxy.cs
│   │   │   │   │   └── ICustomLidarrProxy.cs
│   │   │   │   ├── Deezer/
│   │   │   │   │   ├── DeezerAPIService.cs
│   │   │   │   │   ├── DeezerAuthService.cs
│   │   │   │   │   ├── DeezerMappingHelper.cs
│   │   │   │   │   ├── DeezerMetadataProxy.cs
│   │   │   │   │   ├── DeezerMetadataProxySettings.cs
│   │   │   │   │   ├── DeezerProxy.cs
│   │   │   │   │   ├── DeezerRecords.cs
│   │   │   │   │   └── IDeezerProxy.cs
│   │   │   │   ├── Discogs/
│   │   │   │   │   ├── DiscogsAPIService.cs
│   │   │   │   │   ├── DiscogsMappingHelper.cs
│   │   │   │   │   ├── DiscogsMetadataProxy.cs
│   │   │   │   │   ├── DiscogsMetadataProxySettings.cs
│   │   │   │   │   ├── DiscogsProxy.cs
│   │   │   │   │   ├── DiscogsRecords.cs
│   │   │   │   │   └── IDiscogsProxy.cs
│   │   │   │   ├── Lastfm/
│   │   │   │   │   ├── ILastfmProxy.cs
│   │   │   │   │   ├── LastfmApiService.cs
│   │   │   │   │   ├── LastfmImageScraper.cs
│   │   │   │   │   ├── LastfmMappingHelper.cs
│   │   │   │   │   ├── LastfmMetadataProxy.cs
│   │   │   │   │   ├── LastfmMetadataProxySettings.cs
│   │   │   │   │   ├── LastfmProxy.cs
│   │   │   │   │   ├── LastfmRecordConverter.cs
│   │   │   │   │   └── LastfmRecords.cs
│   │   │   │   ├── MetadataProviderWrapper.cs
│   │   │   │   ├── Mixed/
│   │   │   │   │   ├── AdaptiveThresholdConfig.cs
│   │   │   │   │   ├── AdaptiveThresholdManager.cs
│   │   │   │   │   ├── ISupportMetadataMixing.cs
│   │   │   │   │   ├── MixedMetadataProxy.cs
│   │   │   │   │   ├── MixedMetadataProxySettings.cs
│   │   │   │   │   ├── ProxyDecisionHandler.cs
│   │   │   │   │   └── ProxyMetrics.cs
│   │   │   │   └── SkyHook/
│   │   │   │       ├── SkyHookMetdadataProxy.cs
│   │   │   │       └── SykHookMetadataProxySettings.cs
│   │   │   ├── MixedProxyBase.cs
│   │   │   ├── ProxyAttribute.cs
│   │   │   ├── ProxyBase.cs
│   │   │   ├── ProxyService.cs
│   │   │   ├── ProxyServiceStarter.cs
│   │   │   ├── ProxyWrapperBase.cs
│   │   │   └── RecommendArtists/
│   │   │       ├── LastFmSimilarArtistsService.cs
│   │   │       ├── SimilarArtistsProxy.cs
│   │   │       └── SimilarArtistsProxySettings.cs
│   │   └── ScheduledTasks/
│   │       ├── IProvideScheduledTask.cs
│   │       ├── ScheduledTaskBase.cs
│   │       ├── ScheduledTaskService.cs
│   │       ├── ScheduledTaskServiceStarter.cs
│   │       └── SearchSniper/
│   │           ├── SearchSniperRepositoryHelper.cs
│   │           ├── SearchSniperTask.cs
│   │           └── SearchSniperTaskSettings.cs
│   ├── Notifications/
│   │   ├── FlareSolverr/
│   │   │   ├── FlareDetector.cs
│   │   │   ├── FlareRecords.cs
│   │   │   ├── FlareSolverrHttpInterceptor.cs
│   │   │   ├── FlareSolverrNotification.cs
│   │   │   ├── FlareSolverrService.cs
│   │   │   ├── FlareSolverrSettings.cs
│   │   │   └── IFlareSolverrService.cs
│   │   ├── PlaylistExport/
│   │   │   ├── PlaylistExportNotification.cs
│   │   │   ├── PlaylistExportService.cs
│   │   │   └── PlaylistExportSettings.cs
│   │   ├── QueueCleaner/
│   │   │   ├── ImportFailureNotificationService.cs
│   │   │   ├── QueueCleaner.cs
│   │   │   └── QueueCleanerSettings.cs
│   │   └── YouTubeProxy/
│   │       ├── YouTubeProxyNotification.cs
│   │       ├── YouTubeProxyService.cs
│   │       └── YouTubeProxySettings.cs
│   ├── Plugin.cs
│   ├── PluginInfo.targets
│   ├── PluginKeys.targets
│   ├── PreBuild.targets
│   └── Tubifarry.csproj
├── Tubifarry.sln
└── stylecop.json

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

================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following 
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
# 
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the 
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: "Bug Report"
description: Report a bug or unexpected behavior in Tubifarry
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
  - type: markdown
    attributes:
      value: |
        # Thank you for taking the time to report an issue!
        Your feedback helps us make Tubifarry better for everyone.
  
  - type: input
    id: tubifarry-version
    attributes:
      label: Tubifarry Version
      description: |
        What version of Tubifarry are you running?
        Find this in: System → Plugin.

        Format must be: v1.8.1 (not "latest" or "newest")
      placeholder: v1.8.1
    validations:
      required: true

  - type: input
    id: lidarr-version
    attributes:
      label: Lidarr Version
      description: |
        What version of Lidarr are you using?
        Find this in: System → Status

        Format must be: v2.14.2 (not "latest")
      placeholder: v2.14.2
    validations:
      required: true

  - type: dropdown
    id: os
    attributes:
      label: Operating System
      options:
        - Windows
        - macOS
        - Linux
        - Docker
        - Other (specify in additional context)
    validations:
      required: true

  - type: textarea
    id: bug-description
    attributes:
      label: Bug Description
      description: A clear and concise description of what the bug is and the specific feature that's not working.
      placeholder: |
        Example: "I was trying to... and then... The search function doesn't work as expected because..."
        
        Describe what you were trying to do and what went wrong:
    validations:
      required: true

  - type: textarea
    id: reproduction
    attributes:
      label: Steps to Reproduce
      description: Provide exact steps so we can recreate the issue
      placeholder: |
        1. Go to '...'
        2. Click on '....'
        3. Scroll down to '....'
        4. See error
    validations:
      required: true

  - type: textarea
    id: expected
    attributes:
      label: Expected Behavior
      description: What did you expect to happen?
      placeholder: I expected to see...
    validations:
      required: true

  - type: markdown
    attributes:
      value: |
        ## How to Enable Trace Logging
        
        1. Go to Lidarr → Settings → General → Logging
        2. Set Log Level to **Trace** (not Debug, not Info)
        3. Click Save
        4. Reproduce your exact problem
        5. Go to System → Logs → Files tab
        6. Download the most recent log file
        7. Paste the relevant section below
        
        If your logs do not contain the word **`trace`**, your issue will be closed

  - type: textarea
    id: logs
    attributes:
      label: Trace Log Files
      description: |
        Paste only the relevant trace logs here. Do not delete the details tags.
      value: |
        <details>
        <summary>Logs</summary>

        ```plaintext
        Keep the plaintext markers intact and paste your logs in here...
        ```
        
        </details>
    validations:
      required: true

  - type: checkboxes
    id: log-checklist
    attributes:
      label: Log Checklist
      options:
        - label: I have enabled Trace logging in Lidarr before reproducing the issue
          required: true

  - type: textarea
    id: screenshots
    attributes:
      label: Screenshots
      description: If applicable, add screenshots to help explain your problem.
      value: |
        <details>
        <summary>Screenshots</summary>
        
        <!-- Paste your screenshots here... -->
        
        </details>
    validations:
      required: false

  - type: textarea
    id: additional-context
    attributes:
      label: Additional Context
      description: Any other information that might help us resolve the issue
    validations:
      required: false

================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
# .github/ISSUE_TEMPLATE/config.yml
blank_issues_enabled: false

contact_links:
  - name: "Questions & Support"
    url: https://github.com/TypNull/Tubifarry/discussions
    about: Ask questions or get help with Tubifarry in our Discussions forum.
  
  - name: "Documentation"
    url: https://github.com/TypNull/Tubifarry/wiki
    about: Check out the official documentation for Tubifarry.

================================================
FILE: .github/ISSUE_TEMPLATE/documentation_improvement.yml
================================================
name: "Documentation Improvement"
description: Report issues or suggest improvements for Tubifarry documentation
title: "[DOCS] "
labels: ["documentation"]
assignees: []
body:
  - type: markdown
    attributes:
      value: |
        ## Documentation Improvement Request
        Thank you for helping us improve our documentation!
  
  - type: dropdown
    id: doc-type
    attributes:
      label: Documentation Type
      description: What type of documentation needs improvement?
      options:
        - Installation Guide
        - User Manual
        - API Reference
        - FAQ
        - Tutorial
        - Wiki Page
        - README
        - Other (specify in description)
    validations:
      required: true
  
  - type: input
    id: doc-location
    attributes:
      label: Documentation Location
      description: URL or path to the documentation that needs improvement
      placeholder: https://github.com/TypNull/Tubifarry/wiki/Installation or /docs/installation.md
    validations:
      required: true
  
  - type: textarea
    id: current-state
    attributes:
      label: Current State
      description: Describe what is currently in the documentation and why it needs improvement
      placeholder: The current documentation is missing information about...
    validations:
      required: true
  
  - type: textarea
    id: suggested-changes
    attributes:
      label: Suggested Changes
      description: Describe your suggested changes or additions
      placeholder: |
        I suggest adding a section about...
        The following information should be included...
    validations:
      required: true
  
  - type: textarea
    id: proposed-text
    attributes:
      label: Proposed Text
      description: If you have specific wording in mind, please provide it here
      value: |
        <!-- Do not remove the <details> tags, add your proposed text between them -->
        <details>
        <summary>Click to expand proposed text</summary>
        
        ## New Section Title
        
        Here is my suggested text for this section...
        
        </details>
    validations:
      required: false
  
  - type: textarea
    id: screenshots
    attributes:
      label: Screenshots or Examples
      description: If applicable, add screenshots or examples to illustrate your point
      value: |
        <!-- Do not remove the <details> tags, add your screenshots between them -->
        <details>
        <summary>Click to expand screenshots</summary>
        
        Upload or paste your screenshots here...
        
        </details>
    validations:
      required: false
  
  - type: checkboxes
    id: contribution
    attributes:
      label: Contribution
      options:
        - label: I am willing to submit a pull request with these changes
          required: false

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: "Feature Request"
description: Suggest an idea or improvement for Tubifarry
title: "[FEATURE] "
labels: ["enhancement"]
assignees: []
body:
  - type: markdown
    attributes:
      value: |
        ## Thanks for taking the time to suggest a feature!
        
        Please provide detailed information about your feature idea.
        Check existing issues first to avoid duplicates.
  - type: textarea
    id: feature-request
    attributes:
      label: Feature Request
      description: Describe what you'd like to see added and why it would be valuable.
      placeholder: |
        I would like Tubifarry to support [specific feature] because [reason].
        
        This would improve the experience by...
        
        The feature should work like [detailed description].
    validations:
      required: true
  - type: dropdown
    id: importance
    attributes:
      label: Priority Level
      description: How important is this feature for your workflow?
      options:
        - Nice to have (low priority)
        - Would improve my experience (medium priority)
        - Critical to my use case (high priority)
    validations:
      required: true
  - type: textarea
    id: mockups
    attributes:
      label: Visual Designs or Mockups
      description: If you have any mockups, wireframes, or designs, please share them here.
      value: |
        <details>
        <summary>Mockups</summary>
        
        <!-- Paste your mockups here... -->
        
        </details>
    validations:
      required: false
  - type: dropdown
    id: contribution
    attributes:
      label: Are you willing to contribute to this feature?
      description: Let us know if you'd be interested in implementing this feature
      options:
        - No, I'm just suggesting the idea
        - I might contribute, but I'd need guidance
        - Yes, I'd be willing to implement this feature
    validations:
      required: true
  - type: textarea
    id: technical
    attributes:
      label: Technical Details (Optional)
      description: If you have technical expertise or ideas for implementation, feel free to share.
      value: |
        <details>
        <summary>Technical Details</summary>
        
        <!-- Add your technical implementation ideas here... -->
        
        </details>
    validations:
      required: false
  - type: checkboxes
    id: prerequisites
    attributes:
      label: Prerequisites
      options:
        - label: I have searched existing issues to ensure this is not a duplicate
          required: true

================================================
FILE: .github/workflows/branch-detection.yml
================================================
name: Detect Branch from Tag or Reference

on:
  workflow_call:
    inputs:
      github_ref:
        required: true
        type: string
        description: "The GitHub reference (usually github.ref)"
    outputs:
      branch_name:
        description: "The detected branch name"
        value: ${{ jobs.detect.outputs.branch_name }}

jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      branch_name: ${{ steps.detect_branch.outputs.branch_name }}
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Ensure all Git refs are available
        run: |
          git fetch --prune --unshallow || git fetch --prune
          git fetch --tags --force
          
          echo "Available remote branches:"
          git branch -r
          echo "Available tags:"
          git tag -l

      - name: Detect Branch
        id: detect_branch
        run: |
          # Enhanced branch detection from tag names
          if [[ "${{ inputs.github_ref }}" == refs/tags/* ]]; then
            TAG_NAME=${{ inputs.github_ref }}
            TAG_NAME=${TAG_NAME#refs/tags/}
            DEFAULT_BRANCH=$(git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
            
            # Check if tag contains branch info in format v1.2.3-branch-name
            if [[ "$TAG_NAME" == v*-* ]]; then
              # Extract potential branch name after the dash
              TAG_BRANCH=${TAG_NAME#*-}
              echo "Detected potential branch suffix in tag: $TAG_BRANCH"
              
              # List all available branches and look for best match
              AVAILABLE_BRANCHES=$(git branch -r | grep -v HEAD | sed -e 's/ *origin\///g')
              echo "Available branches:"
              echo "$AVAILABLE_BRANCHES"
              
              # First, try exact match
              if echo "$AVAILABLE_BRANCHES" | grep -q "^$TAG_BRANCH$"; then
                BRANCH_NAME="$TAG_BRANCH"
                echo "Found exact branch match: $BRANCH_NAME"
              # Then try to match longer branch names that contain the tag suffix
              elif echo "$AVAILABLE_BRANCHES" | grep -q "$TAG_BRANCH"; then
                BRANCH_NAME=$(echo "$AVAILABLE_BRANCHES" | grep "$TAG_BRANCH" | head -n 1)
                echo "Found branch containing tag suffix: $BRANCH_NAME"
              fi
            fi
            
            # If no branch name from tag, try to find branch that contains this tag
            if [ -z "$BRANCH_NAME" ]; then
              echo "No branch found from tag suffix, trying git branch --contains..."
              CONTAINING_BRANCHES=$(git branch -r --contains $TAG_NAME | grep -v HEAD | sed -e 's/ *origin\///g')
              echo "Branches containing this tag: $CONTAINING_BRANCHES"
              
              # Prioritize master/main when multiple branches contain the commit
              if echo "$CONTAINING_BRANCHES" | grep -qE "^(master|main)$"; then
                BRANCH_NAME=$(echo "$CONTAINING_BRANCHES" | grep -E "^(master|main)$" | head -n 1)
                echo "Prioritizing default branch: $BRANCH_NAME"
              elif echo "$CONTAINING_BRANCHES" | grep -q "^$DEFAULT_BRANCH$"; then
                BRANCH_NAME="$DEFAULT_BRANCH"
                echo "Using repository default branch: $BRANCH_NAME"
              else
                BRANCH_NAME=$(echo "$CONTAINING_BRANCHES" | head -n 1)
                echo "Using first available branch: $BRANCH_NAME"
              fi
            fi
            
            # Fallback to the default branch if no branch is found
            if [ -z "$BRANCH_NAME" ]; then
              BRANCH_NAME=$DEFAULT_BRANCH
              echo "Tag not associated with branch, using default branch: $BRANCH_NAME"
            fi
          else
            # When triggered by a push, use GITHUB_REF directly
            BRANCH_NAME=${{ inputs.github_ref }}
            BRANCH_NAME=${BRANCH_NAME#refs/heads/}
          fi
          
          echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
          echo "Detected branch: $BRANCH_NAME"

================================================
FILE: .github/workflows/compilation.yml
================================================
name: Compile Project

on:
  workflow_call:
    inputs:
      dotnet_version:
        required: true
        type: string
        description: ".NET SDK version to use"
      plugin_name:
        required: true
        type: string
        description: "Name of the plugin"
      package_version:
        required: true
        type: string
        description: "Version number for the package"
      framework:
        required: true
        type: string
        description: "Framework version extracted from project file" 
      git_commit:
        required: true
        type: string
        description: "Git commit SHA used for the build"
      git_branch:
        required: true
        type: string
        description: "Git branch name"
      git_tag:
        required: false
        type: string
        description: "Git tag if available"
        default: ""
      repo_url:
        required: true
        type: string
        description: "Base repository URL"
      branch_repo_url:
        required: true
        type: string
        description: "Repository URL with branch path"
      repo_owner:
        required: true
        type: string
        description: "Repository owner"
      minimum_lidarr_version:
        required: false
        type: string
        description: "Minimum Lidarr version required"
        default: ""
    secrets:
      SPOTIFY_CLIENT_ID:
        required: false
        description: "Spotify API Client ID"
      SPOTIFY_CLIENT_SECRET:
        required: false
        description: "Spotify API Client Secret"
      YOUTUBE_SECRET:
        required: false
        description: "YouTube API Secret"
      SENTRY_DSN:
        required: false
        description: "Sentry DSN for error reporting"
    outputs:
      build_status:
        description: "Status of the build"
        value: ${{ jobs.compile.outputs.build_status }}
      plugin_output_path:
        description: "Path to the built plugin output directory"
        value: ${{ jobs.compile.outputs.plugin_output_path }}

jobs:
  compile:
    runs-on: ubuntu-latest
    outputs:
      build_status: ${{ steps.build_result.outputs.status }}
      plugin_output_path: ${{ steps.find_output.outputs.plugin_path }}
    
    steps:
      - name: Checkout with submodules
        uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          fetch-depth: 0

      - name: Setup .NET Core
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ inputs.dotnet_version }}

      # Add NuGet package caching
      - name: Cache NuGet packages
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

      - name: Create global.json
        run: |
          echo '{"sdk":{"version": "${{ inputs.dotnet_version }}"}}' > ./global.json

      - name: Create Release Notes
        id: release_notes
        run: |
          mkdir -p ./Properties
          
          cat > ./Properties/release_notes.txt << EOL
          Version: ${{ inputs.package_version }}
          Branch: ${{ inputs.git_branch }}
          Commit: ${{ inputs.git_commit }}
          Framework: ${{ inputs.framework }}
          Repository: ${{ inputs.branch_repo_url }}
          Minimum Lidarr Version: ${{ inputs.minimum_lidarr_version }}
          Built at: $(date -u +"%Y-%m-%d %H:%M:%S UTC")
          
          Plugin Info:
          Name: ${{ inputs.plugin_name }}
          Owner: ${{ inputs.repo_owner }}
          EOL
          
          echo "release_notes=./Properties/release_notes.txt" >> $GITHUB_OUTPUT

      - name: Display Build Parameters
        run: |
          echo "Plugin Name: ${{ inputs.plugin_name }}"
          echo "Package Version: ${{ inputs.package_version }}"
          echo "Git Branch: ${{ inputs.git_branch }}"
          echo "Git Commit: ${{ inputs.git_commit }}"
          echo "Git Tag: ${{ inputs.git_tag }}"
          echo "Repo URL: ${{ inputs.repo_url }}"
          echo "Branch Repo URL: ${{ inputs.branch_repo_url }}"
          echo "Repo Owner: ${{ inputs.repo_owner }}"
          echo "Release Notes Path: ${{ steps.release_notes.outputs.release_notes }}"
          echo "Minimum Lidarr Version: ${{ inputs.minimum_lidarr_version }}"
          echo "Spotify Client ID is configured: ${{ secrets.SPOTIFY_CLIENT_ID != '' }}"
          echo "Spotify Client Secret is configured: ${{ secrets.SPOTIFY_CLIENT_SECRET != '' }}"
          echo "YouTube Secret is configured: ${{ secrets.YOUTUBE_SECRET != '' }}"
          echo "Sentry DSN is configured: ${{ secrets.SENTRY_DSN != '' }}"

      - name: Build with package version and metadata
        id: build_step
        env:
          SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
          SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
          YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
          SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
        run: |
          # Restore NuGet packages first
          dotnet restore *.sln
          
          # Then build with all parameters
          dotnet build *.sln -c Release -f ${{ inputs.framework }} \
            -p:Version=${{ inputs.package_version }} \
            -p:AssemblyVersion=${{ inputs.package_version }} \
            -p:FileVersion=${{ inputs.package_version }} \
            -p:Branch="${{ inputs.git_branch }}" \
            -p:GitCommit="${{ inputs.git_commit }}" \
            -p:GitTag="${{ inputs.git_tag }}" \
            -p:RepoUrl="${{ inputs.branch_repo_url }}" \
            -p:Author="${{ inputs.repo_owner }}" \
            -p:ReleaseNotesFile="${{ steps.release_notes.outputs.release_notes }}" \
            -p:MinimumLidarrVersion="${{ inputs.minimum_lidarr_version }}" \
            -p:SpotifyClientId=$SPOTIFY_CLIENT_ID \
            -p:SpotifyClientSecret=$SPOTIFY_CLIENT_SECRET \
            -p:YouTubeSecret=$YOUTUBE_SECRET \
            -p:SentryDsn=$SENTRY_DSN \
            -p:CI="true" \
            -v:n

      - name: Find Plugin Output Directory
        id: find_output
        run: |
          # More thorough search for the plugin output directory
          echo "Searching for plugin output directories..."
          find . -name "_plugins" -type d
          
          # Find the plugin output directory
          PLUGIN_OUTPUT_DIR=$(find . -type d -path "*/_plugins/*/${{ inputs.plugin_name }}" | head -n 1)
          if [ -z "$PLUGIN_OUTPUT_DIR" ]; then
            echo "Trying alternate search pattern..."
            PLUGIN_OUTPUT_DIR=$(find . -type d -path "*/_plugins/*" -name "${{ inputs.plugin_name }}" | head -n 1)
          fi
          
          # If still not found, use the first plugin directory we can find
          if [ -z "$PLUGIN_OUTPUT_DIR" ]; then
            echo "Plugin directory not found by name. Using first available plugin directory..."
            FIRST_PLUGINS_DIR=$(find . -type d -path "*/_plugins/*" | sort | head -n 1)
            if [ -n "$FIRST_PLUGINS_DIR" ]; then
              PLUGIN_OUTPUT_DIR=$FIRST_PLUGINS_DIR
              echo "Using plugin directory: $PLUGIN_OUTPUT_DIR"
            else
              echo "::error::No plugin directories found at all! Build may have failed to produce plugin files."
              exit 1
            fi
          fi
          
          echo "Plugin output directory: $PLUGIN_OUTPUT_DIR"
          
          # Check if directory contains any files at all
          FILES_COUNT=$(find "$PLUGIN_OUTPUT_DIR" -type f | wc -l)
          if [ "$FILES_COUNT" -eq 0 ]; then
            echo "::error::Directory is empty! Build may have failed to produce plugin files."
            exit 1
          fi
          
          echo "Found $FILES_COUNT files in $PLUGIN_OUTPUT_DIR"
          echo "plugin_path=$PLUGIN_OUTPUT_DIR" >> $GITHUB_OUTPUT
          
          # Copy release notes to output directory
          cp ${{ steps.release_notes.outputs.release_notes }} $PLUGIN_OUTPUT_DIR/release_notes.txt

      - name: Record Build Result
        id: build_result
        run: |
          echo "status=success" >> $GITHUB_OUTPUT

      # Upload the actual plugin files
      - name: Upload Plugin Artifact
        uses: actions/upload-artifact@v4
        with:
          name: plugin-output
          path: ${{ steps.find_output.outputs.plugin_path }}
          retention-days: 1

      - name: Upload Build Log
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: build-logs
          path: |
            ./**/*.binlog
            ./**/*.log
          retention-days: 1
          if-no-files-found: warn

================================================
FILE: .github/workflows/dotnet-setup.yml
================================================
name: Setup .NET Environment

on:
  workflow_call:
    inputs:
      dotnet_version:
        required: true
        type: string
        description: ".NET SDK version to use"
      framework:
        required: false
        type: string
        description: "Framework version"
        default: "net6.0"
    outputs:
      submodules_ready:
        description: "Whether submodules were initialized successfully"
        value: ${{ jobs.setup.outputs.submodules_ready }}

jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      submodules_ready: ${{ steps.check_submodules.outputs.ready }}
    
    steps:
      - name: Checkout with submodules
        uses: actions/checkout@v4
        with:
          submodules: 'recursive'
          fetch-depth: 0

      - name: Setup .NET Core
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ inputs.dotnet_version }}

      # Add NuGet package caching
      - name: Cache NuGet packages
        uses: actions/cache@v3
        with:
          path: ~/.nuget/packages
          key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
          restore-keys: |
            ${{ runner.os }}-nuget-

      - name: Create global.json
        run: |
          echo '{"sdk":{"version": "${{ inputs.dotnet_version }}"}}' > ./global.json

      - name: Initialize and verify submodules
        id: check_submodules
        run: |
          echo "Listing Submodules directory:"
          ls -la Submodules/ || echo "Submodules directory not found"
          
          if [ -d "Submodules/Lidarr" ]; then
            echo "Listing Lidarr submodule:"
            ls -la Submodules/Lidarr/
            
            echo "Listing Lidarr source directory:"
            ls -la Submodules/Lidarr/src/ || echo "Lidarr src directory not found"
          else
            echo "Lidarr submodule not found"
          fi
          
          echo "Checking .gitmodules file:"
          cat .gitmodules || echo ".gitmodules file not found"
          
          # Initialize submodules if they weren't checked out properly
          if [ ! -d "Submodules/Lidarr/src" ]; then
            echo "Manually initializing submodules..."
            git submodule update --init --recursive
          fi
          
          # Verify that submodules are now properly initialized
          if [ -d "Submodules/Lidarr/src" ]; then
            echo "ready=true" >> $GITHUB_OUTPUT
            echo "Submodules successfully initialized"
          else
            echo "ready=false" >> $GITHUB_OUTPUT
            echo "Warning: Submodules could not be initialized"
          fi

================================================
FILE: .github/workflows/git-info.yml
================================================
name: Extract Git Information

on:
  workflow_call:
    inputs:
      tag_ref:
        required: true
        type: string
        description: "The tag reference (usually github.ref)"
      branch_name:
        required: true
        type: string
        description: "Branch name (passed from main workflow)"
    outputs:
      git_commit:
        description: "Git commit SHA used for the build"
        value: ${{ jobs.git_info.outputs.git_commit }}
      git_branch:
        description: "Git branch name"
        value: ${{ jobs.git_info.outputs.git_branch }}
      git_tag:
        description: "Git tag if available"
        value: ${{ jobs.git_info.outputs.git_tag }}
      repo_url:
        description: "Repository URL (base URL without branch)"
        value: ${{ jobs.git_info.outputs.repo_url }}
      branch_repo_url:
        description: "Repository URL with branch path for non-master branches"
        value: ${{ jobs.git_info.outputs.branch_repo_url }}
      repo_owner:
        description: "Repository owner"
        value: ${{ jobs.git_info.outputs.repo_owner }}
      commit_messages:
        description: "Commit messages since last release"
        value: ${{ jobs.git_info.outputs.commit_messages }}
      is_latestrelease:
        description: "Whether this should be the newest stable release"
        value: ${{ jobs.git_info.outputs.is_latestrelease }}
      plugin_input_format:
        description: "Plugin input format: name@owner#branch"
        value: ${{ jobs.git_info.outputs.plugin_input_format }}

jobs:
  git_info:
    runs-on: ubuntu-latest
    outputs:
      git_commit: ${{ steps.extract_git_info.outputs.git_commit }}
      git_branch: ${{ steps.extract_git_info.outputs.git_branch }}
      git_tag: ${{ steps.extract_git_info.outputs.git_tag }}
      repo_url: ${{ steps.extract_git_info.outputs.repo_url }}
      branch_repo_url: ${{ steps.extract_git_info.outputs.branch_repo_url }}
      repo_owner: ${{ steps.extract_git_info.outputs.repo_owner }}
      commit_messages: ${{ steps.get_commit_messages.outputs.commit_messages }}
      is_latestrelease: ${{ steps.check_latest_release.outputs.is_latestrelease }}
      plugin_input_format: ${{ steps.extract_git_info.outputs.plugin_input_format }}
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch tags
        run: |
          git fetch --tags --force

      - name: Extract Git Information
        id: extract_git_info
        run: |
          # Extract commit hash
          GIT_COMMIT=$(git rev-parse --short HEAD)
          echo "git_commit=$GIT_COMMIT" >> $GITHUB_OUTPUT
          echo "Git commit: $GIT_COMMIT"
          
          # Use branch name passed from main workflow
          BRANCH_NAME="${{ inputs.branch_name }}"
          echo "git_branch=$BRANCH_NAME" >> $GITHUB_OUTPUT
          echo "Using branch name: $BRANCH_NAME"
          
          # Extract tag name
          GIT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
          echo "git_tag=$GIT_TAG" >> $GITHUB_OUTPUT
          echo "Git tag: $GIT_TAG"
          
          # Extract repository URL
          REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}"
          echo "repo_url=$REPO_URL" >> $GITHUB_OUTPUT
          echo "Repository URL: $REPO_URL"
          
          # Create branch-specific repository URL (master/main uses base URL)
          if [[ "$BRANCH_NAME" != "master" && "$BRANCH_NAME" != "main" ]]; then
            BRANCH_REPO_URL="$REPO_URL/tree/$BRANCH_NAME"
            echo "Branch-specific URL: $BRANCH_REPO_URL"
          else
            BRANCH_REPO_URL="$REPO_URL"
            echo "Using base URL for master/main branch: $BRANCH_REPO_URL"
          fi
          echo "branch_repo_url=$BRANCH_REPO_URL" >> $GITHUB_OUTPUT
          
          # Extract repository owner
          REPO_OWNER=$(echo "$GITHUB_REPOSITORY" | cut -d'/' -f1)
          echo "repo_owner=$REPO_OWNER" >> $GITHUB_OUTPUT
          echo "Repository owner: $REPO_OWNER"
          
          # Generate plugin input format: name@owner#branch (or name@owner if master/main)
          REPO_NAME=$(basename $GITHUB_REPOSITORY)
          if [[ "$BRANCH_NAME" != "master" && "$BRANCH_NAME" != "main" ]]; then
            PLUGIN_INPUT="$REPO_NAME@$REPO_OWNER#$BRANCH_NAME"
          else
            PLUGIN_INPUT="$REPO_NAME@$REPO_OWNER"
          fi
          echo "plugin_input_format=$PLUGIN_INPUT" >> $GITHUB_OUTPUT
          echo "Plugin input format: $PLUGIN_INPUT"

      - name: Check if Latest Release
        id: check_latest_release
        run: |
          BRANCH_NAME="${{ inputs.branch_name }}"
          CURRENT_TAG="${{ inputs.tag_ref }}"
          CURRENT_TAG="${CURRENT_TAG#refs/tags/}"
          VERSION_NUMBER="${CURRENT_TAG#v}"
          
          # Only master/main with non-0.x versions are latest releases
          if [[ ("$BRANCH_NAME" == "master" || "$BRANCH_NAME" == "main") && ! "$VERSION_NUMBER" =~ ^0\. ]]; then
            echo "is_latestrelease=true" >> $GITHUB_OUTPUT
          else
            echo "is_latestrelease=false" >> $GITHUB_OUTPUT
          fi
          
          echo "Branch: $BRANCH_NAME"
          echo "Version: $VERSION_NUMBER"
          echo "Is latest release: $(cat $GITHUB_OUTPUT | grep is_latestrelease | cut -d= -f2)"

      - name: Get Commit Messages
        id: get_commit_messages
        run: |
          # Get the current tag from inputs.tag_ref
          CURRENT_TAG="${{ inputs.tag_ref }}"
          CURRENT_TAG="${CURRENT_TAG#refs/tags/}"
          echo "Current tag: $CURRENT_TAG"
          
          # Get a list of all tags sorted by creation date (newest first)
          ALL_TAGS=$(git tag --sort=-creatordate)
          echo "All tags:"
          echo "$ALL_TAGS"
          
          # Find the previous tag (the tag after the current one in the sorted list)
          PREVIOUS_TAG=""
          FOUND_CURRENT=false
          
          for tag in $ALL_TAGS; do
            if $FOUND_CURRENT; then
              PREVIOUS_TAG=$tag
              break
            fi
            
            if [ "$tag" = "$CURRENT_TAG" ]; then
              FOUND_CURRENT=true
            fi
          done
          
          echo "Previous tag found: $PREVIOUS_TAG"
          
          # Get commit messages between tags or all commits if no previous tag
          if [ -z "$PREVIOUS_TAG" ]; then
            echo "No previous tag found. Retrieving all commit messages."
            COMMIT_MESSAGES=$(git log --pretty=format:"- %h %s" -n 20)
          else
            echo "Getting commits between $PREVIOUS_TAG and $CURRENT_TAG"
            COMMIT_MESSAGES=$(git log --pretty=format:"- %h %s" $PREVIOUS_TAG..$CURRENT_TAG)
          fi
          
          # Check if we got any commit messages
          if [ -z "$COMMIT_MESSAGES" ]; then
            echo "No commit messages found!"
            COMMIT_MESSAGES="- No significant changes detected between releases."
          fi
          
          # Debug: Show the commit messages
          echo "Commit messages:"
          echo "$COMMIT_MESSAGES"
          
          # No need to base64 encode - we'll pass it directly
          echo "commit_messages<<EOF" >> $GITHUB_OUTPUT
          echo "$COMMIT_MESSAGES" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

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

on:   
  push:     
    tags:       
      - 'v*'  

concurrency:   
  group: ${{ github.workflow }}-${{ github.ref }}   
  cancel-in-progress: true  

permissions:   
  contents: write  

env:   
  DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true   
  DOTNET_CLI_TELEMETRY_OPTOUT: true  

jobs:   
  # Detect branch from tag or reference
  branch-detection:     
    uses: ./.github/workflows/branch-detection.yml     
    with:       
      github_ref: ${{ github.ref }}    

  # Extract metadata from the project   
  metadata:     
    uses: ./.github/workflows/metadata.yml
    needs: [branch-detection]
    with:       
      override_plugin_name: 'Tubifarry'
      branch_name: ${{ needs.branch-detection.outputs.branch_name }}

  # Extract git-specific information   
  git-info:     
    uses: ./.github/workflows/git-info.yml
    needs: [branch-detection]
    with:       
      tag_ref: ${{ github.ref }}
      branch_name: ${{ needs.branch-detection.outputs.branch_name }}

  # Setup .NET environment and submodules   
  dotnet-setup:     
    uses: ./.github/workflows/dotnet-setup.yml     
    with:       
      dotnet_version: '8.0.404'       
      framework: ${{ needs.metadata.outputs.framework }}     
    needs: [metadata]      

  # Compile the project   
  compilation:     
    uses: ./.github/workflows/compilation.yml     
    with:       
      dotnet_version: '8.0.404'       
      plugin_name: ${{ needs.metadata.outputs.plugin_name }}       
      package_version: ${{ needs.metadata.outputs.package_version }}       
      framework: ${{ needs.metadata.outputs.framework }}       
      git_commit: ${{ needs.git-info.outputs.git_commit }}       
      git_branch: ${{ needs.git-info.outputs.git_branch }}       
      git_tag: ${{ needs.git-info.outputs.git_tag }}       
      repo_url: ${{ needs.git-info.outputs.repo_url }}       
      branch_repo_url: ${{ needs.git-info.outputs.branch_repo_url }}       
      repo_owner: ${{ needs.git-info.outputs.repo_owner }}       
      minimum_lidarr_version: ${{ needs.metadata.outputs.minimum_lidarr_version }}     
    secrets:
      SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }}
      SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }}
      YOUTUBE_SECRET: ${{ secrets.YOUTUBE_SECRET }}
      SENTRY_DSN: ${{ secrets.SENTRY_DSN }}     
    needs: [metadata, git-info, dotnet-setup]      

  # Package the artifacts   
  packaging:     
    uses: ./.github/workflows/packaging.yml     
    with:       
      plugin_name: ${{ needs.metadata.outputs.plugin_name }}       
      package_version: ${{ needs.metadata.outputs.package_version }}       
      build_suffix: ${{ needs.metadata.outputs.build_suffix }}       
      build_status: ${{ needs.compilation.outputs.build_status }}     
    needs: [metadata, compilation]      

  # Generate release notes   
  release-notes:     
    uses: ./.github/workflows/release-notes.yml     
    with:       
      plugin_name: ${{ needs.metadata.outputs.plugin_name }}       
      package_version: ${{ needs.metadata.outputs.package_version }}       
      build_suffix: ${{ needs.metadata.outputs.build_suffix }}       
      minimum_lidarr_version: ${{ needs.metadata.outputs.minimum_lidarr_version }}       
      commit_messages: ${{ needs.git-info.outputs.commit_messages }}       
      git_commit: ${{ needs.git-info.outputs.git_commit }}       
      branch_repo_url: ${{ needs.git-info.outputs.branch_repo_url }}
      git_branch: ${{ needs.git-info.outputs.git_branch }}
      plugin_input_format: ${{ needs.git-info.outputs.plugin_input_format }}     
    needs: [metadata, git-info]      

  # Publish the release   
  publishing:     
    uses: ./.github/workflows/publishing.yml     
    with:       
      plugin_name: ${{ needs.metadata.outputs.plugin_name }}       
      package_version: ${{ needs.metadata.outputs.package_version }}       
      build_suffix: ${{ needs.metadata.outputs.build_suffix }}       
      is_prerelease: ${{ needs.metadata.outputs.is_prerelease }}       
      is_latestrelease: ${{ needs.git-info.outputs.is_latestrelease }}       
      release_notes_id: ${{ needs.release-notes.outputs.release_notes_id }}       
      git_commit: ${{ needs.git-info.outputs.git_commit }}       
      branch_repo_url: ${{ needs.git-info.outputs.branch_repo_url }}
      git_branch: ${{ needs.git-info.outputs.git_branch }}     
    needs: [metadata, git-info, compilation, packaging, release-notes]     
    if: needs.compilation.outputs.build_status == 'success'

================================================
FILE: .github/workflows/metadata.yml
================================================
name: Extract Project Metadata

on:
  workflow_call:
    inputs:
      override_plugin_name:
        required: false
        type: string
        description: "Override the plugin name if different from repository name"
        default: ""
      branch_name:
        required: true
        type: string
        description: "Branch name (passed from main workflow)"
    outputs:
      plugin_name:
        description: "The name of the plugin"
        value: ${{ jobs.metadata.outputs.plugin_name }}
      package_version:
        description: "The version of the package"
        value: ${{ jobs.metadata.outputs.package_version }}
      build_suffix:
        description: "Build suffix based on framework and branch name"
        value: ${{ jobs.metadata.outputs.build_suffix }}
      minimum_lidarr_version:
        description: "Minimum Lidarr version required"
        value: ${{ jobs.metadata.outputs.minimum_lidarr_version }}
      is_prerelease:
        description: "Whether this is a prerelease"
        value: ${{ jobs.metadata.outputs.is_prerelease }}
      framework:
        description: "Framework version extracted from project file"
        value: ${{ jobs.metadata.outputs.framework }}

jobs:
  metadata:
    runs-on: ubuntu-latest
    outputs:
      plugin_name: ${{ steps.extract_repo_name.outputs.plugin_name }}
      package_version: ${{ steps.extract_version.outputs.package_version }}
      build_suffix: ${{ steps.build_info.outputs.build_suffix }}
      branch_name: ${{ inputs.branch_name }}
      framework: ${{ steps.extract_framework.outputs.framework }}
      minimum_lidarr_version: ${{ steps.fetch_minimum_lidarr_version.outputs.minimum_lidarr_version }}
      is_prerelease: ${{ steps.extract_version.outputs.is_prerelease }}
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Extract repository name or use override
        id: extract_repo_name
        run: |
          # Check if override_plugin_name is provided
          if [ -n "${{ inputs.override_plugin_name }}" ]; then
            PLUGIN_NAME="${{ inputs.override_plugin_name }}"
            echo "Using provided plugin name override: $PLUGIN_NAME"
          else
            # Fall back to repository name
            PLUGIN_NAME=$(basename $GITHUB_REPOSITORY)
            echo "Using repository name as plugin name: $PLUGIN_NAME"
          fi
          echo "plugin_name=$PLUGIN_NAME" >> $GITHUB_OUTPUT

      - name: Extract framework from project file
        id: extract_framework
        run: |
          # Get plugin name 
          PLUGIN_NAME="${{ steps.extract_repo_name.outputs.plugin_name }}"
          echo "Looking for project file: $PLUGIN_NAME/$PLUGIN_NAME.csproj"
          
          # Check if the main project file exists
          if [ -f "$PLUGIN_NAME/$PLUGIN_NAME.csproj" ]; then
            PROJECT_FILE="$PLUGIN_NAME/$PLUGIN_NAME.csproj"
          else
            echo "Main project file not found, searching for any .csproj file..."
            # Find the first .csproj file in the repo
            PROJECT_FILE=$(find . -name "*.csproj" | head -n 1)
          fi
          
          if [ -n "$PROJECT_FILE" ]; then
            echo "Found project file: $PROJECT_FILE"
            # Extract the TargetFramework using grep and sed
            FRAMEWORK=$(grep -o '<TargetFramework>.*</TargetFramework>' "$PROJECT_FILE" | sed 's/<TargetFramework>\(.*\)<\/TargetFramework>/\1/')
            echo "Extracted framework: $FRAMEWORK"
          else
            echo "No .csproj files found, defaulting to net6.0"
            FRAMEWORK="net6.0"
          fi
          
          # If framework is still empty, default to net6.0
          if [ -z "$FRAMEWORK" ]; then
            echo "Framework not found in project file, defaulting to net6.0"
            FRAMEWORK="net6.0"
          fi
          
          echo "Using framework: $FRAMEWORK"
          echo "framework=$FRAMEWORK" >> $GITHUB_OUTPUT

      - name: Extract version from tag
        id: extract_version
        run: |
          TAG_VERSION=${GITHUB_REF#refs/tags/v}
          # Remove branch suffix if present
          if [[ "$TAG_VERSION" == *-* ]]; then
            VERSION_ONLY=${TAG_VERSION%%-*}
            echo "Extracted version without branch suffix: $VERSION_ONLY"
            TAG_VERSION=$VERSION_ONLY
          fi
          echo "Extracted version: $TAG_VERSION"
          echo "package_version=$TAG_VERSION" >> $GITHUB_OUTPUT
          if [[ "$TAG_VERSION" == 0.* ]]; then
            echo "is_prerelease=true" >> $GITHUB_OUTPUT
          else
            echo "is_prerelease=false" >> $GITHUB_OUTPUT
          fi

      - name: Determine build suffix
        id: build_info
        run: |
          # Get the framework from the extract_framework step
          FRAMEWORK="${{ steps.extract_framework.outputs.framework }}"
          
          # Build suffix is just the framework
          BUILD_SUFFIX="$FRAMEWORK"
          
          echo "Framework: $FRAMEWORK"
          echo "Build suffix: $BUILD_SUFFIX"
          echo "build_suffix=$BUILD_SUFFIX" >> $GITHUB_OUTPUT

      - name: Fetch Minimum Lidarr Version from Submodule
        id: fetch_minimum_lidarr_version
        run: |
          if [ -f "Submodules/Lidarr/azure-pipelines.yml" ]; then
            RAW_YAML=$(cat Submodules/Lidarr/azure-pipelines.yml)
          else
            echo "Error: Submodules/Lidarr/azure-pipelines.yml not found!"
            exit 1
          fi
          MAJOR_VERSION=$(echo "$RAW_YAML" | grep "majorVersion:" | head -n 1 | sed "s/.*majorVersion: *'\([^']*\)'.*/\1/")
          echo "Extracted majorVersion: $MAJOR_VERSION"
          DOT_COUNT=$(echo "$MAJOR_VERSION" | awk -F. '{print NF-1}')
          if [ "$DOT_COUNT" -eq 2 ]; then
            MINIMUM_LIDARR_VERSION="${MAJOR_VERSION}.0"
          else
            MINIMUM_LIDARR_VERSION="$MAJOR_VERSION"
          fi
          echo "Minimum Lidarr Version: $MINIMUM_LIDARR_VERSION"
          echo "minimum_lidarr_version=$MINIMUM_LIDARR_VERSION" >> $GITHUB_OUTPUT

================================================
FILE: .github/workflows/packaging.yml
================================================
name: Package Plugin

on:
  workflow_call:
    inputs:
      plugin_name:
        required: true
        type: string
        description: "Name of the plugin"
      package_version:
        required: true
        type: string
        description: "Version number for the package"
      build_suffix:
        required: true
        type: string
        description: "Build suffix based on framework and branch"
      build_status:
        required: true
        type: string
        description: "Status of the build step"
    outputs:
      package_path:
        description: "Path to the final package"
        value: ${{ jobs.package.outputs.package_path }}

jobs:
  package:
    runs-on: ubuntu-latest
    outputs:
      package_path: ${{ steps.zip_plugin.outputs.package_path }}
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        
      - name: Download built plugin
        uses: actions/download-artifact@v4
        with:
          name: plugin-output
          path: ./plugin-output
          # Don't continue on error - must have plugin to package

      - name: Verify plugin artifacts are available
        run: |
          echo "Contents of the downloaded plugin output directory:"
          find ./plugin-output -type f | sort
          
          # Fail if no plugin files found
          PLUGIN_FILES_COUNT=$(find ./plugin-output -name "*.${{ inputs.plugin_name }}.*" -o -name "*.Plugin.${{ inputs.plugin_name }}.*" | wc -l)
          if [ "$PLUGIN_FILES_COUNT" -eq 0 ]; then
            echo "::error::No plugin files found in downloaded artifacts! Build may have failed to produce plugin files."
            exit 1
          fi
          
          echo "Found $PLUGIN_FILES_COUNT plugin files."

      - name: Zip Plugin
        id: zip_plugin
        run: |
          cd ./plugin-output
          PACKAGE_NAME="${{ inputs.plugin_name }}-v${{ inputs.package_version }}.${{ inputs.build_suffix }}.zip"
          
          # List files that will be included in the package
          echo "Files to be packaged:"
          find . -name "*.${{ inputs.plugin_name }}.*" -o -name "*.Plugin.${{ inputs.plugin_name }}.*"
          
          # Create zip with all plugin files and release notes
          find . -name "*.${{ inputs.plugin_name }}.*" -o -name "*.Plugin.${{ inputs.plugin_name }}.*" | xargs zip -r $PACKAGE_NAME
          
          # Add release notes to the zip if it exists
          if [ -f "release_notes.txt" ]; then
            zip -r $PACKAGE_NAME release_notes.txt
          fi
          
          # Verify the zip was created and has content
          if [ ! -f "$PACKAGE_NAME" ] || [ $(stat -c%s "$PACKAGE_NAME") -eq 0 ]; then
            echo "::error::Failed to create package or package is empty!"
            exit 1
          fi
          
          echo "Created package: $PACKAGE_NAME"
          echo "Contents of zip file:"
          unzip -l $PACKAGE_NAME
          
          echo "package_path=./plugin-output/$PACKAGE_NAME" >> $GITHUB_OUTPUT

      - name: Upload Package Artifact
        uses: actions/upload-artifact@v4
        with:
          name: plugin-package
          path: ${{ steps.zip_plugin.outputs.package_path }}
          retention-days: 7

================================================
FILE: .github/workflows/publishing.yml
================================================
name: Publish GitHub Release

on:
  workflow_call:
    inputs:
      plugin_name:
        required: true
        type: string
        description: "Name of the plugin"
      package_version:
        required: true
        type: string
        description: "Version number for the release"
      build_suffix:
        required: true
        type: string
        description: "Build suffix based on framework and branch"
      is_prerelease:
        required: true
        type: string
        description: "Whether this is a pre-release"
      is_latestrelease:
        required: true
        type: string
        description: "Whether this should be the newest stable release"
      release_notes_id:
        required: true
        type: string
        description: "ID of the generated release notes"
      git_commit:
        required: true
        type: string
        description: "Git commit SHA used for the build"
      branch_repo_url:
        required: false
        type: string
        description: "Repository URL with branch path"
        default: ""
      git_branch:
        required: true
        type: string
        description: "Git branch name for target_commitish"
    outputs:
      release_url:
        description: "URL of the created release"
        value: ${{ jobs.publish.outputs.release_url }}

jobs:
  publish:
    runs-on: ubuntu-latest
    outputs:
      release_url: ${{ steps.create_release.outputs.url }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: plugin-package
          path: ./artifacts

      - name: Download release notes
        uses: actions/download-artifact@v4
        with:
          name: ${{ inputs.release_notes_id }}
          path: ./notes

      - name: Verify artifacts
        id: verify_artifacts
        run: |
          echo "Artifacts directory contents:"
          find ./artifacts -type f | sort
          
          # The artifact should follow the naming convention with framework from csproj
          ARTIFACT_PATH="./artifacts/${{ inputs.plugin_name }}-v${{ inputs.package_version }}.${{ inputs.build_suffix }}.zip"
          
          if [ ! -f "$ARTIFACT_PATH" ]; then
            echo "::error::Artifact not found at expected path: $ARTIFACT_PATH"
            exit 1
          fi
          echo "artifact_path=$ARTIFACT_PATH" >> $GITHUB_OUTPUT
          FILESIZE=$(stat -c%s "$ARTIFACT_PATH")
          echo "Artifact size: $FILESIZE bytes"
          echo "artifact_size=$FILESIZE" >> $GITHUB_OUTPUT
          
          # Check release notes
          if [ ! -f "./notes/release_notes.md" ]; then
            echo "::error::Release notes not found at expected path: ./notes/release_notes.md"
            exit 1
          fi
          echo "release_notes_path=./notes/release_notes.md" >> $GITHUB_OUTPUT

      - name: Create GitHub Release
        id: create_release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref }}
          name: "🚀 Release ${{ inputs.package_version }}"
          body_path: ${{ steps.verify_artifacts.outputs.release_notes_path }}
          prerelease: ${{ inputs.is_prerelease == 'true' }}
          make_latest: ${{ inputs.is_latestrelease == 'true' }}
          target_commitish: ${{ inputs.git_branch }}
          files: |
            ${{ steps.verify_artifacts.outputs.artifact_path }}
          fail_on_unmatched_files: true
        env:
          GITHUB_TOKEN: ${{ github.token }}

      - name: Verify release was created
        run: |
          echo "Release URL: ${{ steps.create_release.outputs.url }}"
          if [ -z "${{ steps.create_release.outputs.url }}" ]; then
            echo "::warning::Release URL is empty, there might have been an issue with release creation"
          else
            echo "Release successfully created"
          fi

      - name: Notify of successful release
        if: success()
        run: |
          echo "::notice::🎉 Successfully published release v${{ inputs.package_version }} for ${{ inputs.plugin_name }}"
          
          # Include branch-specific URL in notification if available
          if [ -n "${{ inputs.branch_repo_url }}" ]; then
            echo "Repository: ${{ inputs.branch_repo_url }}"
          fi
          
          echo "Release available at: ${{ steps.create_release.outputs.url }}"

================================================
FILE: .github/workflows/release-notes.yml
================================================
name: Generate Release Notes

on:
  workflow_call:
    inputs:
      plugin_name:
        required: true
        type: string
        description: "Name of the plugin"
      package_version:
        required: true
        type: string
        description: "Version number for the release"
      build_suffix:
        required: true
        type: string
        description: "Build suffix based on framework and branch"
      minimum_lidarr_version:
        required: true
        type: string
        description: "Minimum required Lidarr version"
      commit_messages:
        required: true
        type: string
        description: "Commit messages since last release"
      git_commit:
        required: true
        type: string
        description: "Git commit SHA used for the build"
      branch_repo_url:
        required: true
        type: string
        description: "Repository URL with branch path"
      git_branch:
        required: true
        type: string
        description: "Current git branch"
      plugin_input_format:
        required: true
        type: string
        description: "Plugin input format: name@owner#branch"
    outputs:
      release_notes_id:
        description: "ID of the generated release notes"
        value: ${{ jobs.generate.outputs.release_notes_id }}

jobs:
  generate:
    runs-on: ubuntu-latest
    outputs:
      release_notes_id: ${{ steps.create_notes.outputs.release_notes_id }}
    steps:
      - name: Generate Release Notes
        id: create_notes
        run: |
          # Generate a unique ID for this release notes
          NOTES_ID="release-notes-${{ github.run_id }}"
          echo "release_notes_id=$NOTES_ID" >> $GITHUB_OUTPUT
          
          # Print the commit messages for debugging
          echo "Commit messages:"
          echo "${{ inputs.commit_messages }}"
          
          # Use the git_branch input directly
          BRANCH_NAME="${{ inputs.git_branch }}"
          echo "Using branch from git-info: $BRANCH_NAME"
          
          # Set default branch
          DEFAULT_BRANCH="master"
          
          # Generate release notes in markdown
          cat > release_notes.md << EOL
          
          ## 📝 What's New:
          ${{ inputs.commit_messages }}
          
          ---
          
          ## 📥 Installation Notes:
          - In Lidarr, navigate to **System -> Plugins**
          - Enter: \`${{ inputs.plugin_input_format }}\`
          
          ---
          
          ## 📦 Package Information
          **Version:** ${{ inputs.package_version }}
          **.NET Version:** ${{ inputs.build_suffix }}
          **Minimum Lidarr Version:** ${{ inputs.minimum_lidarr_version }}
          **Commit:** ${{ inputs.git_commit }}
          EOL
          
          # Debug: Show what was generated
          echo "Generated release notes:"
          cat release_notes.md

      - name: Upload Release Notes
        uses: actions/upload-artifact@v4
        with:
          name: ${{ steps.create_notes.outputs.release_notes_id }}
          path: release_notes.md
          retention-days: 1

================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Mono auto generated files
mono_crash.*

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
_plugins/
_tests/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# ASP.NET Scaffolding
ScaffoldingReadMe.txt

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb

# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/

# Ionide (cross platform F# VS Code tools) working folder
.ionide/

# Fody - auto-generated XML schema
FodyWeavers.xsd
/_plugins/SpotiarrTube
/_plugins/Tubifarry
/enc_temp_folder/f7b6bdaa49df014838e55dd376c78df
/_plugins/UnitTest
/enc_temp_folder


================================================
FILE: .gitmodules
================================================
[submodule "Submodules/Lidarr"]
	path = Submodules/Lidarr
	url = https://github.com/Lidarr/Lidarr/


================================================
FILE: CONTRIBUTION.md
================================================
# Contributing to Tubifarry

Thank you for your interest in contributing to Tubifarry! This guide
will help you set up your development environment and understand our
contribution process. 
Tubifarry is a plugin for Lidarr that extends its functionality.


## Table of Contents

- [Prerequisites](#prerequisites)
- [Understanding the Architecture](#understanding-the-architecture)
- [Automatic Setup (Recommended)](#automatic-setup-recommended)
- [Build Automation Explained](#build-automation-explained)
- [Manual Setup (Fallback)](#manual-setup-fallback)
- [Development Workflow](#development-workflow)
- [Contributing Code](#contributing-code)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Troubleshooting](#troubleshooting)
- [Getting Help](#getting-help)


## Prerequisites

Before you begin, ensure you have the following installed on your system:

### Required Software

- **Visual Studio 2022** or higher (Community edition is free)
  - Download: https://visualstudio.com/downloads/
  - Must include .NET 8 SDK (VS 2022 V17.0+)
- **Git** - Version control system
- **Node.js 20.x** - JavaScript runtime for frontend development
  - Download: https://nodejs.org/
  - ⚠️ **Important**: Versions 18.x, 16.x, or 21.x will NOT work
- **Yarn** - Package manager for Node.js dependencies
  - Included with Node 20+ (enable with `corepack enable`)
  - For other versions: `npm i -g corepack`

### Optional but Recommended

- **Rider** by JetBrains (alternative to Visual Studio)
- **VS Code** or similar text editor for quick edits

### System Requirements

- **Windows**: Windows 10/11
- **Linux**: Any modern distribution
- **macOS**: 10.14+ (Mojave or newer)
- **RAM**: Minimum 8GB (16GB recommended)
- **Storage**: At least 5GB free space for the full build

## Understanding the Architecture

Tubifarry is built as a plugin that integrates with Lidarr's core functionality:

- **Lidarr Core**: The base music management application (included as a Git submodule)
- **Tubifarry Plugin**: Your plugin code that extends Lidarr
- **Frontend**: React-based UI (part of Lidarr)
- **Backend**: C# .NET 8 code (both Lidarr and Tubifarry)

### Important Notes About the Build Process

1. **Lidarr Submodule**: Tubifarry includes Lidarr as a Git submodule, which means it contains a reference to a specific version of Lidarr
2. **Force Push Warning**: The Lidarr develop branch is regularly rebased and force-pushed. This is normal and expected behavior
3. **Build Order Matters**: You must build Lidarr completely before building Tubifarry
4. **First Build Takes Time**: The initial setup can take 5-10 minutes, but subsequent builds are much faster


## Automatic Setup (Recommended)

The project includes build automation that handles most of the setup process automatically. This is the fastest way to get started.

### Step 1: Clone the Repository

```bash
git clone https://github.com/TypNull/Tubifarry.git
```

Or if you've forked the repository:
```bash
git clone https://github.com/YOUR-USERNAME/Tubifarry.git
```

### Step 2: Open and Build

1. **Open** `Tubifarry.sln` in Visual Studio

2. **Build the solution** by pressing `Ctrl + Shift + B` (or `Build` → `Build Solution`)
   
   ⏳ **Wait patiently** - This first build will take some time depending on your internet connection and PC performance. The build automation will:
   - Initialize and clone all Git submodules (Lidarr source code)
   - Install Node.js dependencies via Yarn
   - Build the Lidarr frontend
   
   You can monitor progress in the **Output** window (`View` → `Output`).
   
   ⚠️ You will see errors on this build.
   
### Step 3: Restart Visual Studio

After the first build completes:

1. **Close** Visual Studio completely
2. **Reopen** Visual Studio
3. **Open** `Tubifarry.sln` again

This ensures Visual Studio properly recognizes all the newly generated files and references.

### Step 4: Build Again

1. **Build the solution** again (`Ctrl + Shift + B`)
   
   ⚠️ You may see some errors on this build - **these can be ignored**.

2. **Build once more** (`Ctrl + Shift + B`)
   
   The build should now complete successfully.

### Step 5: Configure Startup Project

1. In **Solution Explorer**, navigate to: `Submodules` → `Lidarr` → find `Lidarr.Console`
2. **Right-click** on `Lidarr.Console`
3. Select **"Set as Startup Project"**

### Step 6: Run

Press `F5` or click the **Start** button to run Lidarr.

---

**That's it!** If everything worked, you're ready to start developing. If you encountered errors, see the [Manual Setup](#manual-setup-fallback) or [Troubleshooting](#troubleshooting) sections below.


## Build Automation Explained

The project uses several MSBuild `.targets` files to automate the setup and build process. Understanding these can help with troubleshooting.

### PreBuild.targets - Automatic Initialization

This file handles automatic setup before the main build starts:

**Submodule Initialization (`InitializeSubmodules` target)**
- Checks if the Lidarr submodule exists by looking for `Submodules/Lidarr/src/NzbDrone.Core/Lidarr.Core.csproj`
- If missing, automatically runs `git submodule update --init --recursive`

**Frontend Build (`LidarrFrontendBuild` target)**
- Checks if the Lidarr frontend has been built by looking for `Submodules/Lidarr/_output/UI/`
- If missing, automatically runs:
  - `yarn install` - Downloads all Node.js dependencies
  - `yarn build` - Compiles the React frontend
- This can take several minutes on first run

### ILRepack.targets - Assembly Merging

After the build completes, this file merges all plugin dependencies into a single DLL:

### Debug.targets - Automatic Plugin Deployment

In Debug configuration, this file automatically deploys your plugin:

- After ILRepack finishes, copies the plugin files to Lidarr's plugin directory:
  - `C:\ProgramData\Lidarr\plugins\AUTHOR\Tubifarry\`
- Allows immediate testing without manual file copying

### Build Order

The targets execute in this order:
1. `InitializeSubmodules` → Ensures Lidarr source exists
2. `LidarrFrontendBuild` → Ensures frontend is compiled
3. `BeforeBuild` → Standard .NET build preparation
4. `Build` → Compiles Tubifarry code
5. `CopySystemTextJson` → Copies required runtime DLL
6. `ILRepacker` → Merges all assemblies
7. `PostBuild` (Debug only) → Deploys to plugin folder


## Manual Setup (Fallback)

If the automatic setup fails or you prefer manual control, follow these steps.

### Step 1: Fork and Clone

1. **Fork the Repository**
   - Go to https://github.com/TypNull/Tubifarry
   - Click the "Fork" button in the top right
   - This creates your own copy of the repository

2. **Clone Your Fork**
   ```bash
   git clone https://github.com/YOUR-USERNAME/Tubifarry.git
   cd Tubifarry
   ```

### Step 2: Initialize Git Submodules

The Tubifarry repository includes Lidarr as a submodule. You need to initialize and download it:

```bash
git submodule update --init --recursive
```

This command downloads the Lidarr source code into the submodule directory. This may take a few minutes depending on your internet connection.

### Step 3: Verify Submodule

Check that the Lidarr submodule was properly initialized:

```bash
cd Submodules/Lidarr/
git status
```

You should see the Lidarr repository files.

### Step 4: Build Lidarr Frontend

The frontend must be built before proceeding to the backend.

1. **Navigate to the Lidarr submodule directory**
   ```bash
   cd Submodules/Lidarr/
   ```

2. **Install Node dependencies**
   ```bash
   yarn install
   ```
   This downloads all required JavaScript packages. First time takes 2-5 minutes.

3. **Build the frontend** (or start the watcher for development)
   ```bash
   yarn build
   ```
   
   Or for active development with hot-reload:
   ```bash
   yarn start
   ```
   **Important**: If using `yarn start`, keep this terminal window open.

### Step 5: Build Lidarr Backend

1. **Open the Lidarr solution in Visual Studio**
   - Navigate to the Lidarr submodule directory
   - Open `Lidarr.sln` in Visual Studio 2022

2. **Configure the startup project**
   - Right-click on `Lidarr.Console` in Solution Explorer
   - Select "Set as Startup Project"

3. **Build the solution**
   - Click `Build` → `Build Solution` (or press `Ctrl+Shift+B`)
   - Wait for the build to complete (first build takes a bit time)
   - Watch the Output window for any errors

### Step 6: Build Tubifarry Plugin

Now that Lidarr is fully built, you can build the Tubifarry plugin.

1. **Navigate back to the Tubifarry root directory**
   ```bash
   cd [path-to-Tubifarry-root]
   ```

2. **Open the Tubifarry solution**
   - Open `Tubifarry.sln` in Visual Studio (in a new instance or after closing Lidarr solution)

3. **Wait for dependencies to load**
   - Visual Studio will restore NuGet packages automatically
   - Wait until the status bar shows "Ready"

4. **Build the Tubifarry solution**
   - Click `Build` → `Build Solution`
   - The plugin will automatically copy to the correct directory on Windows in Debug mode

### Step 7: Manual Plugin Deployment (if needed)

If the automatic deployment doesn't work, manually copy the plugin:

1. Find the built plugin at: `Tubifarry/bin/Debug/net8.0/Lidarr.Plugin.Tubifarry.dll`
2. Copy to: `C:\ProgramData\Lidarr\plugins\AUTHOR\Tubifarry\`
3. Also copy the `.pdb` and `.deps.json` files if they exist


## Development Workflow

### Daily Development Process

Once you've completed the initial setup, your typical workflow will be:

1. **Make your changes** to Tubifarry code in Visual Studio

2. **Build Tubifarry** to test your changes (`Ctrl + Shift + B`)

3. **Run and test**
   - Ensure `Lidarr.Console` is set as the startup project
   - Press `F5` to start debugging
   - Lidarr will start with your plugin loaded
   - Access the UI at http://localhost:8686

### Hot Reload (Optional)

For faster frontend iteration:
1. Keep `yarn start` running in the Lidarr submodule directory
2. Frontend changes will automatically refresh in the browser


## Contributing Code

### Before You Start

- **Check existing issues**: Look at [GitHub Issues](https://github.com/TypNull/Tubifarry/issues) to see if your feature/bug is already being worked on
- **Create an issue**: If your idea isn't already tracked, create a new issue to discuss it
- **Ask questions**: Join our Discord or comment on the issue if you need clarification

### Code Guidelines

1. **Code Style**
   - Use 4 spaces for indentation (not tabs)
   - Follow C# naming conventions
   - Keep methods focused and reasonably sized
   - Add XML documentation comments for public APIs

2. **Commit Guidelines**
   - Make meaningful commits with clear messages
   - Use \*nix line endings (LF, not CRLF)
   - Commit message format:
     - `New: Add Spotify playlist import feature`
     - `Fixed: YouTube download timeout issue`
     - `Improved: Error handling in Slskd client`

3. **Code Quality**
   - Test your changes thoroughly
   - Handle errors gracefully
   - Add logging for debugging purposes
   - Don't leave commented-out code

### Branching Strategy

1. **Create a feature branch** from `develop` (not from `master`)
   ```bash
   git checkout develop
   git pull origin develop
   git checkout -b feature/your-feature-name
   ```

2. **Use descriptive branch names**
   - ✅ Good: `feature/spotify-auth`, `fix/youtube-timeout`, `improve/error-messages`
   - ❌ Bad: `patch`, `updates`, `my-branch`

3. **Keep your branch updated**
   ```bash
   git checkout develop
   git pull origin develop
   git checkout feature/your-feature-name
   git rebase develop
   ```

## Pull Request Guidelines

### Before Submitting

- [ ] Code builds without errors
- [ ] You've tested the changes locally
- [ ] Code follows the style guidelines
- [ ] Commits are clean and well-organized (consider squashing if needed)
- [ ] Branch is up to date with `develop`

### Creating a Pull Request

1. **Push your branch** to your fork
   ```bash
   git push origin feature/your-feature-name
   ```

2. **Open a Pull Request** on GitHub
   - Go to your fork on GitHub
   - Click "Compare & pull request"
   - Target the `develop` branch (not `master`)

3. **Fill out the PR template**
   - Describe what your PR does
   - Reference any related issues (e.g., "Fixes #123")
   - Add screenshots if it's a UI change
   - List any breaking changes

4. **Respond to feedback**
   - Maintainers may request changes
   - Make the requested updates in your branch
   - Push the changes (they'll automatically update the PR)

### PR Review Process

- We aim to review PRs within a few days
- If it's been longer, feel free to ping us on Discord
- Be patient and respectful during code review
- All PRs require approval before merging

### Important Rules

- ⛔ **Never** make PRs to `master` - they will be closed
- ⛔ **Don't** merge `develop` into your feature branch - use rebase instead
- ✅ **Do** create one PR per feature/bugfix
- ✅ **Do** ask questions if you're unsure

## Troubleshooting

### Common Build Errors

#### "Could not load file or assembly 'Lidarr.Core'"

**Cause**: Lidarr backend wasn't fully built before building Tubifarry.

**Solution**:
1. Ensure Lidarr frontend is built (`Submodules/Lidarr/_output/UI/` exists)
2. Open and build `Lidarr.sln` first
3. Then build Tubifarry

#### "Node version not supported" or "Yarn command not found"

**Cause**: Wrong Node.js version or Yarn not enabled.

**Solution**:
```bash
# Check Node version
node --version  # Should be 20.x

# Enable Yarn
corepack enable

# Verify Yarn is available
yarn --version
```

#### Frontend changes not appearing

**Cause**: Frontend not rebuilt or browser cache.

**Solution**:
1. Navigate to Lidarr submodule directory
2. Run `yarn build` (or `yarn start` for development)
3. Refresh your browser with `Ctrl+F5` (hard refresh)

#### "Submodule not initialized" or empty Lidarr directory

**Cause**: Git submodules weren't initialized.

**Solution**:
```bash
git submodule update --init --recursive
```

#### Build succeeds but plugin doesn't appear in Lidarr

**Cause**: Plugin DLL not copied to correct location.

**Solution**:
1. Check if you're building in **Debug** configuration (automatic copy only works in Debug)
2. Check your build output directory: `Tubifarry/bin/Debug/net8.0/`
3. Manually copy plugin files to: `C:\ProgramData\Lidarr\plugins\AUTHOR\Tubifarry\`
4. Restart Lidarr
5. Check Lidarr logs for plugin loading errors

#### ILRepack errors

**Cause**: Missing or incompatible assemblies.

**Solution**:
1. Clean the solution (`Build` → `Clean Solution`)
2. Delete the `bin` and `obj` folders in the Tubifarry project
3. Rebuild

### Performance Issues

#### First build is very slow

This is normal! The first build includes:
- Downloading all NuGet packages
- Downloading all Node packages
- Building the frontend (webpack compilation)
- Building everything from scratch

Subsequent builds will be much faster (usually under 1 minute).

#### Visual Studio is slow or freezes

**Solutions**:
- Close unnecessary programs to free up RAM
- Disable unnecessary VS extensions
- Clear VS cache: `Tools` → `Options` → `Projects and Solutions` → `Build and Run` → check "Only build startup projects"
- Consider using Rider instead (lighter weight)

### Getting More Help

If you're still stuck:

1. **Check existing issues**: https://github.com/TypNull/Tubifarry/issues
2. **Search Discord**: Past questions may have been answered
3. **Ask on Discord**: Include:
   - What you're trying to do
   - What error you're seeing
   - What you've already tried
   - Your OS and software versions

## Getting Help

### Support Channels

- **GitHub Issues**: For bug reports and feature requests
- **Servarr Discord**: For development questions and discussions

### Tips for Getting Help

When asking for help, please include:

1. **What you're trying to accomplish**
2. **What error you're encountering** (exact error message)
3. **What you've already tried**
4. **Your environment**:
   - Operating System
   - Visual Studio version
   - .NET SDK version (`dotnet --version`)
   - Node.js version (`node --version`)

---

## Quick Reference

### First-Time Setup Checklist

- [ ] Install Visual Studio 2022 with .NET 8 SDK
- [ ] Install Git
- [ ] Install Node.js 20.x
- [ ] Enable Yarn (`corepack enable`)
- [ ] Fork Tubifarry repository
- [ ] Clone your fork
- [ ] Initialize submodules (`git submodule update --init --recursive`)
- [ ] Build Lidarr frontend (`cd Submodules/Lidarr && yarn install && yarn build`)
- [ ] Build Lidarr backend (open `Lidarr.sln`, build)
- [ ] Build Tubifarry plugin (open `Tubifarry.sln`, build)
- [ ] Test by running Lidarr.Console

### Before Submitting a PR

- [ ] Code builds successfully
- [ ] Changes tested locally
- [ ] Commits are clean with good messages
- [ ] Branch rebased with latest `develop`
- [ ] PR targets `develop` (not `master`)
- [ ] PR description is complete

---

Thank you for contributing to Tubifarry! Your contributions help make
music management better for everyone.



================================================
FILE: Directory.Build.props
================================================
<Project>
	<!-- Common to all Lidarr Projects -->
	<PropertyGroup>
	<!--	<TreatWarningsAsErrors>true</TreatWarningsAsErrors> -->

		<PlatformTarget>AnyCPU</PlatformTarget>
		<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
		
		<LidarrRootDir>$(MSBuildThisFileDirectory)</LidarrRootDir>

		<!-- Specifies the type of output -->
		<LidarrOutputType>Library</LidarrOutputType>
		<LidarrOutputType Condition="$(MSBuildProjectName.Contains('.Test'))">Test</LidarrOutputType>
		<LidarrOutputType Condition="'$(MSBuildProjectName)'=='ServiceInstall'">Exe</LidarrOutputType>
		<LidarrOutputType Condition="'$(MSBuildProjectName)'=='ServiceUninstall'">Exe</LidarrOutputType>
		<LidarrOutputType Condition="'$(MSBuildProjectName)'=='Lidarr'">Exe</LidarrOutputType>
		<LidarrOutputType Condition="'$(MSBuildProjectName)'=='Lidarr.Console'">Exe</LidarrOutputType>
		<LidarrOutputType Condition="'$(MSBuildProjectName)'=='Lidarr.Update'">Update</LidarrOutputType>

		<!-- Specifies whether it's one of our own libraries -->
		<LidarrProject>false</LidarrProject>
		<LidarrProject Condition="$(MSBuildProjectName.StartsWith('Lidarr'))">true</LidarrProject>
		<LidarrProject Condition="$(MSBuildProjectName.StartsWith('ServiceInstall'))">true</LidarrProject>
		<LidarrProject Condition="$(MSBuildProjectName.StartsWith('ServiceUninstall'))">true</LidarrProject>

		<PluginProject>true</PluginProject>
	</PropertyGroup>

	<Target Name="PrintOutputPath" BeforeTargets="Build">
		<Message Text="PluginProject: $(PluginProject)" Importance="high" />
		<Message Text="OutputPath: $(OutputPath)" Importance="high" />
		<Message Text="MSBuildProjectName: $(MSBuildProjectName)" Importance="high" />
		<Message Text="LidarrRootDir: $(LidarrRootDir)" Importance="high" />
	</Target>

	<PropertyGroup>
		<Configuration Condition="'$(Configuration)'==''">Release</Configuration>
		<!-- Centralize intermediate and default outputs -->
		<BaseIntermediateOutputPath>$(LidarrRootDir)_temp\obj\$(MSBuildProjectName)\</BaseIntermediateOutputPath>
		<IntermediateOutputPath>$(LidarrRootDir)_temp\obj\$(MSBuildProjectName)\$(Configuration)\</IntermediateOutputPath>
		<OutputPath>$(LidarrRootDir)_temp\bin\$(Configuration)\$(MSBuildProjectName)\</OutputPath>

		<!-- Output to _output and _tests respectively -->
		<OutputPath Condition="'$(LidarrProject)'=='true'">$(LidarrRootDir)_output\</OutputPath>
		<OutputPath Condition="'$(PluginProject)'=='true'">$(LidarrRootDir)_plugins\$(TargetFramework)\$(MSBuildProjectName)</OutputPath>
		<OutputPath Condition="'$(LidarrOutputType)'=='Test'">$(LidarrRootDir)_tests\</OutputPath>
		<OutputPath Condition="'$(LidarrOutputType)'=='Update'">$(LidarrRootDir)_output\Lidarr.Update\</OutputPath>

		<!-- Paths relative to project file for better readability -->
		<BaseIntermediateOutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(BaseIntermediateOutputPath)'))</BaseIntermediateOutputPath>
		<IntermediateOutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)'))</IntermediateOutputPath>
		<OutputPath>$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '$(OutputPath)'))</OutputPath>

		<DebugSymbols>true</DebugSymbols>
	</PropertyGroup>

	<!-- Test projects need bindingRedirects -->
	<PropertyGroup Condition="'$(LidarrOutputType)'=='Test'">
		<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
		<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
		<SelfContained>false</SelfContained>
	</PropertyGroup>

	<!-- Set the AssemblyConfiguration attribute for projects -->
	<ItemGroup Condition="'$(LidarrProject)'=='true'">
		<AssemblyAttribute Include="System.Reflection.AssemblyConfigurationAttribute">
			<_Parameter1>$(AssemblyConfiguration)</_Parameter1>
		</AssemblyAttribute>
	</ItemGroup>

	<PropertyGroup>
		<AppendTargetFrameworkToOutputPath Condition="'$(PluginProject)'=='true'">false</AppendTargetFrameworkToOutputPath>
		<!-- For now keep the NzbDrone namespace -->
		<RootNamespace Condition="'$(LidarrProject)'=='true'">$(MSBuildProjectName.Replace('Lidarr','NzbDrone'))</RootNamespace>
	</PropertyGroup>

	<!-- Allow building net framework using mono -->
	<ItemGroup>
		<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0">
			<PrivateAssets>all</PrivateAssets>
			<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
		</PackageReference>
	</ItemGroup>

</Project>

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

Copyright (c) 2026 TypNull

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" />
    <add key="Taglib" value="https://pkgs.dev.azure.com/Lidarr/Lidarr/_packaging/Taglib/nuget/v3/index.json" />
    <add key="dotnet-bsd-crossbuild" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/dotnet-bsd-crossbuild/nuget/v3/index.json" />
    <add key="Mono.Posix.NETStandard" value="https://pkgs.dev.azure.com/Servarr/Servarr/_packaging/Mono.Posix.NETStandard/nuget/v3/index.json" />
  </packageSources>
</configuration>


================================================
FILE: README.md
================================================
# Tubifarry for Lidarr 🎶
![Downloads](https://img.shields.io/github/downloads/TypNull/Tubifarry/total)  ![GitHub release (latest by date)](https://img.shields.io/github/v/release/TypNull/Tubifarry)  ![GitHub last commit](https://img.shields.io/github/last-commit/TypNull/Tubifarry)  ![License](https://img.shields.io/github/license/TypNull/Tubifarry)  ![GitHub stars](https://img.shields.io/github/stars/TypNull/Tubifarry)

Tubifarry is a plugin for **Lidarr** that adds multiple music sources to your library management. It uses **Spotify's catalog** as an [indexer](https://wiki.servarr.com/en/lidarr/supported#indexers) to search for music, then downloads the actual audio files from **YouTube**. Tubifarry also supports **Slskd**, the Soulseek client, as both an **indexer** and **downloader**, allowing you to tap into the vast music collection available on the Soulseek network. 🛠️

Additionally, Tubifarry supports fetching soundtracks from **Sonarr** (series) and **Radarr** (movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This makes it easy to manage and download soundtracks for your favorite movies and TV shows. 🎬🎵
For further customization, Codec Tinker lets you convert audio files between formats using FFmpeg, helping you optimize your library.⚙️

> **Note**: Some details in this documentation may vary from the current implementation.

---

## Table of Contents 📑

1. [Installation 🚀](#installation-)
2. [Soulseek (Slskd) Setup 🎧](#soulseek-slskd-setup-)
3. [YouTube Downloader Setup 🎥](#youtube-downloader-setup-)
4. [WebClients 📻](#web-clients-)
5. [Fetching Soundtracks 🎬🎵](#fetching-soundtracks-from-sonarr-and-radarr-)
6. [Queue Cleaner 🧹](#queue-cleaner-)
7. [Codec Tinker 🎛️](#codec-tinker-️)
8. [Lyrics Fetcher 📜](#lyrics-fetcher-)
9. [Search Sniper 🏹](#search-sniper-)
10. [Custom Metadata Sources 🧩](#custom-metadata-sources-)
11. [Similar Artists 🧷](#similar-artists-)
12. [Troubleshooting 🛠️](#troubleshooting-%EF%B8%8F)

----

## Installation 🚀
Follow the steps below to get started.

- In Lidarr, go to `System -> Plugins`.
- Paste `https://github.com/TypNull/Tubifarry` into the GitHub URL box and click **Install**.

---

### Soulseek (Slskd) Setup 🎧
Tubifarry supports **Slskd**, the Soulseek client, as both an **indexer** and **downloader**. Follow the steps below to configure it.

#### **Setting Up the Soulseek Indexer**:
1. Navigate to `Settings -> Indexers` and click **Add**.
2. Select `Slskd` from the list of indexers.
3. Configure the following settings:
   - **URL**: The URL of your Slskd instance (e.g., `http://localhost:5030`).
   - **API Key**: The API key for your Slskd instance (found in Slskd's settings under 'Options').
   - **Include Only Audio Files**: Enable to filter search results to audio files only.

#### **Setting Up the Soulseek Download Client**:
1. Go to `Settings -> Download Clients` and click **Add**.
2. Select `Slskd` from the list of download clients.
3. The download path is fetched from slskd, if it does not match use `Remote Path` settings.

---

### YouTube Downloader Setup 🎥 
> #### YouTube Bot Detection ⚠️
> YouTube actively detects and blocks automated downloaders. To bypass this, configure the Trusted Session Generator and provide cookie authentication (see setup steps below).

The YouTube downloader extracts audio from YouTube and converts them to audio files using FFmpeg. 

#### **Configure the Indexer**:
1. Navigate to `Settings -> Indexers` and click **Add**.
2. In the modal, select `Tubifarry` (located under **Other** at the bottom).

#### **Setting Up the YouTube Download Client**:
1. Go to `Settings -> Download Clients` and click **Add**.
2. Select `Youtube` from the list of download clients.
3. Set the download path and adjust other settings as needed.
4. **Optional**: If using FFmpeg, ensure the FFmpeg path is correctly configured.

#### **FFmpeg and Audio Conversion**:
1. **FFmpeg**: Required to extract audio from YouTube. The plugin will attempt to download FFmpeg automatically if not found. Without FFmpeg, files will be downloaded in their original format, which Lidarr may cannot properly import.
   - Ensure FFmpeg is in your system PATH or specify its location in settings
   - Used for extracting audio tracks and converting between formats

2. **Audio Quality**: YouTube provides audio at various bitrates:
   - Standard quality: 128kbps AAC (free users)
   - High quality: 256kbps AAC (YouTube Premium required)
   - The plugin can convert to other formats (MP3, Opus) using FFmpeg

---

### Web Clients 📻

Tubifarry supports multiple web clients. These are web services that provide music. Some work better than others and Tubifarry is not responsible for the uptime or stability of these services.

##### Supported Clients
- **Lucida** - A music downloading service that supports multiple sources.
- **DABmusic** - A high-resolution audio streaming platform.
- **T2Tunes** - A music downloading service that supports AmazonMusic
- **Subsonic** - A music streaming API standard with broad compatibility

All clients share the same base architecture, making it relatively straightforward to add new ones. The Subsonic Indexer and Client is a generic client, making it possible for any online service to connect with it. The Subsonic specifications are documented on the [API page](https://www.subsonic.org/pages/api.jsp).

If you have a suggestion to add a web client and the service does not want to support Subsonic as a generic indexer, please open a feature request.

---

### Fetching Soundtracks from Sonarr and Radarr 🎬🎵
Tubifarry also supports fetching soundtracks from **Sonarr** (for TV series) and **Radarr** (for movies) and adding them to Lidarr using the **Arr-Soundtracks** import list feature. This allows you to easily manage and download soundtracks for your favorite movies and TV shows.

To enable this feature:
1. **Set Up the Import List**:
   - Navigate to `Settings -> Import Lists` in Lidarr.
   - Add a new import list and select the option for **Arr-Soundtracks**.
   - Configure the settings to match your Sonarr and Radarr instances.
   - Provide a cache path to store responses from MusicBrainz for faster lookups.

2. **Enjoy Soundtracks**:
   - Once configured, Tubifarry will automatically fetch soundtracks from your Sonarr and Radarr libraries and add them to Lidarr for download and management.

---

### Queue Cleaner 🧹

The **Queue Cleaner** automatically handles downloads that fail to import into your library. When Lidarr can't import a download (due to missing tracks, incorrect metadata, etc.), Queue Cleaner can rename files based on their embedded tags, retry the import, blocklist the release, or remove the files.

1. **Key Options**:
   - *Blocklist*: Choose to remove, blocklist, or both for failed imports.
   - *Rename*: Automatically rename album folders and tracks using available metadata.
   - *Clean Imports*: Decide when to clean—when tracks are missing, metadata is incomplete, or always.
   - *Retry Finding Release*: Automatically retry searching for a release if the import fails.

2. **How to Enable**:
   - Navigate to `Settings -> Connect` in Lidarr.
   - Add a new connection and select the **Queue Cleaner**.
   - Configure the settings to match your needs.

---

### Codec Tinker 🎛️

**Codec Tinker** automatically converts audio files between formats using FFmpeg when they're imported into your library. You can set up rules to convert specific formats (e.g., convert all WAV files to FLAC, or convert high-bitrate AAC to MP3). Note: Lossy formats (MP3, AAC) cannot be converted to lossless formats (FLAC, WAV) as quality cannot be restored.

#### How to Enable Codec Tinker

1. Go to `Settings > Metadata` in Lidarr.
2. Open the **Codec Tinker** MetadataConsumer.
3. Toggle the switch to enable the feature.

#### How to Use Codec Tinker

1. **Set Default Conversion Settings**
   - **Target Format**:
     Choose the default format for conversions (e.g., FLAC, Opus, MP3).

   - **Custom Conversion Rules**:
     Define rules like `wav -> flac`, `AAC>=256k -> MP3:300k` or `all -> alac` for more specific conversions.

   - **Custom Conversion Rules On Artists**:
     Define tags like `opus-192` for one specific conversion on all albums of an artist.

   **Note**: Lossy formats (e.g., MP3, AAC) cannot be converted to non-lossy formats (e.g., FLAC, WAV).

2. **Enable Format-Specific Conversion**
   Toggle checkboxes or use custom rules to enable conversion for specific formats:
   - **Convert MP3**, **Convert FLAC**, etc.

---

###  Lyrics Fetcher 📜

**Lyrics Fetcher** automatically downloads lyrics for your music files. It fetches synchronized lyrics from LRCLIB and plain lyrics from Genius. Lyrics can be saved as separate .lrc files and embedded directly into the audio files' metadata.

#### How to Enable Lyrics Fetcher

1. Go to `Settings > Metadata` in Lidarr.
2. Open the **Lyrics Fetcher** MetadataConsumer.
3. Toggle the switch to enable the feature.

#### How to Use Lyrics Fetcher

You can configure the following options:

- **Create LRC Files**: Enables creating external `.lrc` files that contain time-synced lyrics.
- **Embed Lyrics in Audio Files**: Instead of (or in addition to) creating separate LRC files, this option embeds the lyrics directly into the audio file's.
- **Overwrite Existing LRC Files**: When enabled, this will replace any existing LRC files with newly downloaded ones.

---

### Search Sniper 🏹

**Search Sniper** automatically triggers searches for missing albums in your wanted list. Instead of searching for everything at once, which can overload indexers, it randomly selects a few albums from your wanted list at regular intervals and searches for them. It keeps track of what has been searched recently to avoid repeating searches too often. 
Search Sniper can be triggered manually from the Tasks tab.

#### How to Enable Search Sniper
1. Go to `Settings > Metadata` in Lidarr.
2. Open the **Search Sniper** option.
3. Configure the following options:
   - **Picks Per Interval**: How many items to search each cycle
   - **Min Refresh Interval**: How often to run searches
   - **Cache Type**: Memory or Permanent
   - **Cache Retention Time**: Days to keep cache
   - **Pause When Queued**: Stop when queue reaches this number
   - **Search Options**: Enable at least one - Missing albums, Missing tracks, or Cutoff not met

---

###  Custom Metadata Sources 🧩

Tubifarry can fetch metadata from additional sources beyond MusicBrainz, including **Discogs**, **Deezer**, and **Last.fm**. These sources can provide additional artist information, album details, and cover art when MusicBrainz data is incomplete. The MetaMix feature intelligently combines data from multiple sources to create more complete metadata profiles.

#### How to Enable Individual Metadata Sources

1. Go to `Settings > Metadata` in Lidarr.
2. Open a specific **Metadata Source**.
3. Toggle the switch to enable the feature.
4. Configure the required settings:
   - **User Agent**: Set a custom identifier that follows the format "Name/Version" to help the metadata service identify your requests properly.
   - **API Key**: Enter your personal access token or API key for the service.
   - **Caching Method**: Choose between:
     - **Memory Caching**: Faster but less persistent (only recommended if your system has been running stably for 5+ days)
     - **Permanent Caching**: More reliable but requires disk storage
   - **Cache Directory**: If using Permanent caching, specify a folder where metadata can be stored to reduce API calls.

#### How to Enable Multiple Metadata Sources

MetaMix is an advanced feature that intelligently combines metadata from multiple sources to create more complete artist profiles. It can fill gaps in one source with information from another, resulting in a more comprehensive music library.

1. Go to `Settings > Metadata` in Lidarr. 
2. Open the **MetaMix** settings.
3. Configure the following options:
   - **Priority Rules**: Establish a hierarchy among your metadata sources. For example, set MusicBrainz as primary and Discogs as secondary. Lower numbers indicate higher priority.
   - **Dynamic Threshold**: Controls how aggressively MetaMix switches between sources:
     - Higher values make MetaMix more willing to use lower-priority sources
     - Lower values make MetaMix stick more closely to your primary source
   - **Multi-Source Population**: When enabled, missing album information from your primary source will be automatically supplemented with data from secondary sources.

The feature currently works best with artists that are properly linked across different metadata systems. Which is typically the case on MusicBrainz.

---

### Similar Artists 🧷

**Similar Artists** lets you discover related artists using Last.fm's
recommendation data directly in Lidarr's search. Search for an artist
with the `~` prefix and get back a list of similar musicians ready to
be added to your library.

#### How to Enable Similar Artists

1. Go to `Settings > Metadata` in Lidarr.
2. Enable these three metadata sources:
   - **Similar Artists** - Enter your Last.fm API key
   - **Lidarr Default** - Required to handle normal searches
   - **MetaMix** - Required to coordinate the search
3. Optional: Adjust result limit, enable image fetching, and configure caching.

**Examples:**
- `similar:Pink Floyd`
- `~20244d07-534f-4eff-b4d4-930878889970`

---

## Troubleshooting 🛠️

- **Slskd Download Path Permissions**:
  Ensure Lidarr has read/write access to the Slskd download path. Verify folder permissions and confirm the user running Lidarr has the necessary access. For Docker setups, double-check that the volume is correctly mounted and permissions are properly configured.

- **FFmpeg Issues (Optional)**:
  If you're using FFmpeg and songs fail to process, ensure FFmpeg is installed correctly and accessible in your system's PATH. If issues persist, try reinstalling FFmpeg or downloading it manually.

- **Metadata Issues**:
  If metadata isn't being added to downloaded files, confirm the files are in a supported format. If using FFmpeg, check that it's extracting audio to compatible formats like AAC embedded in MP4 containers. Review debug logs for further details.

- **No Release Found**:
  If no release is found, YouTube may flag the plugin as a bot. To avoid this and access higher-quality audio, use a combination of cookies and the Trusted Session Generator:
  1. Install the **cookies.txt** extension for your browser:
     - [Chrome](https://chrome.google.com/webstore/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)
     - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/cookies-txt/)
  2. Log in to YouTube and save the `cookies.txt` file in a folder accessible by Lidarr.
  3. In Lidarr, go to **Indexer and Downloader Settings** and provide the path to the `cookies.txt` file.
  4. **Trusted Session Generator**: Creates authentication tokens that mimic regular browser sessions to bypass YouTube's bot detection.
     - It generates tokens locally, which requires Node.js installed and available in your system's PATH
	 - It can generate tokens using the [bgutil-ytdlp-pot-provider](https://github.com/Brainicism/bgutil-ytdlp-pot-provider)
  
  The combination of cookies and trusted sessions significantly improves success rates when downloading from YouTube, and can help access higher quality audio streams.

- **No Lyrics Imported**:
  To save `.lrc` files (lyric files), navigate to **Media Management > Advanced Settings > Import Extra Files** and add `lrc` to the list of supported file types. This ensures lyric files are imported and saved alongside your music files.

- **Unsupported Formats**: Verify custom rules and target formats.

--- 

## Acknowledgments 🙌
Special thanks to [**trevTV**](https://github.com/TrevTV) for laying the groundwork with his plugins. Additionally, thanks to [**IcySnex**](https://github.com/IcySnex) for providing the YouTube API. 🎉

---

## Contributing 🤝
If you'd like to contribute to Tubifarry, feel free to open issues or submit pull requests on the [GitHub repository](https://github.com/TypNull/Tubifarry). Your feedback and contributions are highly appreciated!

---

## License 📄
Tubifarry is licensed under the MIT License. See the [LICENSE](https://github.com/TypNull/Tubifarry/blob/master/LICENSE.txt) file for more details.

---

Enjoy seamless music downloads with Tubifarry! 🎧


================================================
FILE: Tubifarry/Blocklisting/BaseBlocklist.cs
================================================
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Download;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;

namespace Tubifarry.Blocklisting
{
    public abstract class BaseBlocklist<TProtocol>(IBlocklistRepository blocklistRepository) : IBlocklistForProtocol where TProtocol : IDownloadProtocol
    {
        private readonly IBlocklistRepository _blocklistRepository = blocklistRepository;

        public string Protocol => typeof(TProtocol).Name;

        public bool IsBlocklisted(int artistId, ReleaseInfo release) => _blocklistRepository.BlocklistedByTorrentInfoHash(artistId, release.Guid).Any(b => BaseBlocklist<TProtocol>.SameRelease(b, release));

        public Blocklist GetBlocklist(DownloadFailedEvent message) => new()
        {
            ArtistId = message.ArtistId,
            AlbumIds = message.AlbumIds,
            SourceTitle = message.SourceTitle,
            Quality = message.Quality,
            Date = DateTime.UtcNow,
            PublishedDate = DateTime.TryParse(message.Data.GetValueOrDefault("publishedDate") ?? string.Empty, out DateTime publishedDate) ? publishedDate : null,
            Size = long.Parse(message.Data.GetValueOrDefault("size", "0")),
            Indexer = message.Data.GetValueOrDefault("indexer"),
            Protocol = message.Data.GetValueOrDefault("protocol"),
            Message = message.Message,
            TorrentInfoHash = message.Data.GetValueOrDefault("guid")
        };

        private static bool SameRelease(Blocklist item, ReleaseInfo release) => release.Guid.IsNotNullOrWhiteSpace() ? release.Guid.Equals(item.TorrentInfoHash) : item.Indexer.Equals(release.Indexer, StringComparison.InvariantCultureIgnoreCase);
    }
}

================================================
FILE: Tubifarry/Blocklisting/Blocklists.cs
================================================
using NzbDrone.Core.Blocklisting;
using NzbDrone.Core.Indexers;

namespace Tubifarry.Blocklisting
{
    public class YoutubeBlocklist(IBlocklistRepository blocklistRepository) : BaseBlocklist<YoutubeDownloadProtocol>(blocklistRepository)
    { }

    public class SoulseekBlocklist(IBlocklistRepository blocklistRepository) : BaseBlocklist<SoulseekDownloadProtocol>(blocklistRepository)
    { }

    public class QobuzBlocklist(IBlocklistRepository blocklistRepository) : BaseBlocklist<QobuzDownloadProtocol>(blocklistRepository)
    { }

    public class LucidaBlocklist(IBlocklistRepository blocklistRepository) : BaseBlocklist<LucidaDownloadProtocol>(blocklistRepository)
    { }

    public class SubSonicBlocklist(IBlocklistRepository blocklistRepository) : BaseBlocklist<SubSonicDownloadProtocol>(blocklistRepository)
    { }
}

================================================
FILE: Tubifarry/Core/Model/AlbumData.cs
================================================
using NzbDrone.Core.Parser.Model;
using System.Text.RegularExpressions;
using Tubifarry.Core.Utilities;

namespace Tubifarry.Core.Model
{
    /// <summary>
    /// Contains combined information about an album, search parameters, and search results.
    /// </summary>
    public partial class AlbumData(string name, string downloadProtocol)
    {
        public string? Guid { get; set; }
        public string IndexerName { get; } = name;

        // Mixed
        public string AlbumId { get; set; } = string.Empty;

        // Properties from AlbumInfo
        public string AlbumName { get; set; } = string.Empty;

        public string ArtistName { get; set; } = string.Empty;
        public string InfoUrl { get; set; } = string.Empty;
        public string ReleaseDate { get; set; } = string.Empty;
        public DateTime ReleaseDateTime { get; set; }
        public string ReleaseDatePrecision { get; set; } = string.Empty;
        public int TotalTracks { get; set; }
        public bool ExplicitContent { get; set; }
        public string CustomString { get; set; } = string.Empty;
        public string CoverResolution { get; set; } = string.Empty;

        // Properties from YoutubeSearchResults
        public int Bitrate { get; set; }

        public int BitDepth { get; set; }
        public long Duration { get; set; }

        // Soulseek
        public long? Size { get; set; }

        public int Priotity { get; set; }
        public List<string>? ExtraInfo { get; set; }

        public string DownloadProtocol { get; set; } = downloadProtocol;

        // Not used
        public AudioFormat Codec { get; set; } = AudioFormat.AAC;

        /// <summary>
        /// Converts AlbumData into a ReleaseInfo object.
        /// </summary>
        public ReleaseInfo ToReleaseInfo() => new()
        {
            Guid = Guid ?? $"{IndexerName}-{AlbumId}-{Codec}-{Bitrate}-{BitDepth}",
            Artist = ArtistName,
            Album = AlbumName,
            DownloadUrl = AlbumId,
            InfoUrl = InfoUrl,
            PublishDate = ReleaseDateTime == DateTime.MinValue ? DateTime.UtcNow : ReleaseDateTime,
            DownloadProtocol = DownloadProtocol,
            Title = ConstructTitle(),
            Codec = Codec.ToString(),
            Resolution = CoverResolution,
            Source = CustomString,
            Container = Bitrate.ToString(),
            Size = Size ?? (Duration > 0 ? Duration : TotalTracks * 300) * Bitrate * 1000 / 8
        };

        /// <summary>
        /// Parses the release date based on the precision.
        /// </summary>
        public void ParseReleaseDate() => ReleaseDateTime = ReleaseDatePrecision switch
        {
            "year" => new DateTime(int.Parse(ReleaseDate), 1, 1),
            "month" => DateTime.ParseExact(ReleaseDate, "yyyy-MM", System.Globalization.CultureInfo.InvariantCulture),
            "day" => DateTime.ParseExact(ReleaseDate, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture),
            _ => throw new FormatException($"Unsupported release_date_precision: {ReleaseDatePrecision}"),
        };

        /// <summary>
        /// Constructs a title string for the album in a format optimized for parsing.
        /// </summary>
        /// <returns>A formatted title string.</returns>
        private string ConstructTitle()
        {
            string normalizedAlbumName = NormalizeAlbumName(AlbumName);

            string title = $"{ArtistName} - {normalizedAlbumName}";

            if (ReleaseDateTime != DateTime.MinValue)
                title += $" ({ReleaseDateTime.Year})";

            if (ExplicitContent)
                title += " [Explicit]";

            int calculatedBitrate = Bitrate;
            if (calculatedBitrate <= 0 && Size.HasValue && Duration > 0)
                calculatedBitrate = (int)(Size.Value * 8 / (Duration * 1000));

            if (AudioFormatHelper.IsLossyFormat(Codec) && calculatedBitrate != 0)
                title += $" [{Codec} {calculatedBitrate}kbps]";
            else if (!AudioFormatHelper.IsLossyFormat(Codec) && BitDepth != 0)
                title += $" [{Codec} {BitDepth}bit]";
            else
                title += $" [{Codec}]";

            if (ExtraInfo?.Count > 0)
                title += string.Concat(ExtraInfo.Where(info => !string.IsNullOrEmpty(info)).Select(info => $" [{info}]"));

            title += " [WEB]";
            return title;
        }

        /// <summary>
        /// Normalizes the album name to handle featuring artists and other parentheses.
        /// </summary>
        /// <param name="albumName">The album name to normalize.</param>
        /// <returns>The normalized album name.</returns>
        private static string NormalizeAlbumName(string albumName)
        {
            if (FeatRegex().IsMatch(albumName)) // TODO ISMatch vs Match
            {
                Match match = FeatRegex().Match(albumName);
                string featuringArtist = albumName[(match.Index + match.Length)..].Trim();

                albumName = $"{albumName[..match.Index].Trim()} (feat. {featuringArtist})";
            }
            return FeatReplaceRegex().Replace(albumName, match => $"{{{match.Value.Trim('(', ')')}}}");
        }

        [GeneratedRegex(@"(?i)\b(feat\.|ft\.|featuring)\b", RegexOptions.IgnoreCase, "de-DE")]
        private static partial Regex FeatRegex();

        [GeneratedRegex(@"\((?!feat\.)[^)]*\)")]
        private static partial Regex FeatReplaceRegex();
    }
}

================================================
FILE: Tubifarry/Core/Model/ApiCircuitBreaker.cs
================================================
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;

namespace Tubifarry.Core.Model
{
    public interface ICircuitBreaker
    {
        bool IsOpen { get; }

        void RecordSuccess();

        void RecordFailure();

        void Reset();
    }

    public class ApiCircuitBreaker(int failureThreshold = 5, int resetTimeoutMinutes = 5) : ICircuitBreaker
    {
        private int _failureCount;
        private DateTime _lastFailure = DateTime.MinValue;
        private readonly int _failureThreshold = failureThreshold;
        private readonly TimeSpan _resetTimeout = TimeSpan.FromMinutes(resetTimeoutMinutes);
        private readonly object _lock = new();

        public bool IsOpen
        {
            get
            {
                lock (_lock)
                {
                    if (_failureCount >= _failureThreshold)
                    {
                        if (DateTime.UtcNow - _lastFailure > _resetTimeout)
                        {
                            Reset();
                            return false;
                        }
                        return true;
                    }
                    return false;
                }
            }
        }

        public void RecordSuccess()
        {
            lock (_lock) { _failureCount = Math.Max(0, _failureCount - 1); }
        }

        public void RecordFailure()
        {
            lock (_lock)
            {
                _failureCount++;
                _lastFailure = DateTime.UtcNow;
            }
        }

        public void Reset()
        {
            lock (_lock) { _failureCount = 0; }
        }
    }

    public static class CircuitBreakerFactory
    {
        private static readonly ConditionalWeakTable<Type, ICircuitBreaker> _typeBreakers = [];
        private static readonly ConcurrentDictionary<string, WeakReference<ICircuitBreaker>> _namedBreakers = new();

        private static readonly object _cleanupLock = new();
        private static DateTime _lastCleanup = DateTime.UtcNow;
        private static readonly TimeSpan _cleanupInterval = TimeSpan.FromMinutes(15);

        /// <summary>
        /// Gets a circuit breaker for a specific type.
        /// </summary>
        public static ICircuitBreaker GetBreaker<T>() => GetBreaker(typeof(T));

        /// <summary>
        /// Gets a circuit breaker for a specific object.
        /// </summary>
        public static ICircuitBreaker GetBreaker(object obj) => GetBreaker(obj.GetType());

        /// <summary>
        /// Gets a circuit breaker for a specific type.
        /// </summary>
        public static ICircuitBreaker GetBreaker(Type type)
        {
            if (!_typeBreakers.TryGetValue(type, out ICircuitBreaker? breaker))
            {
                breaker = new ApiCircuitBreaker();
                _typeBreakers.Add(type, breaker);
            }
            return breaker;
        }

        /// <summary>
        /// Gets a circuit breaker by name.
        /// </summary>
        public static ICircuitBreaker GetBreaker(string name)
        {
            CleanupIfNeeded();

            if (_namedBreakers.TryGetValue(name, out WeakReference<ICircuitBreaker>? weakRef) && weakRef.TryGetTarget(out ICircuitBreaker? breaker))
                return breaker;

            breaker = new ApiCircuitBreaker();
            _namedBreakers[name] = new WeakReference<ICircuitBreaker>(breaker);
            return breaker;
        }

        /// <summary>
        /// Gets a circuit breaker with custom configuration.
        /// </summary>
        public static ICircuitBreaker GetCustomBreaker<T>(int failureThreshold, int resetTimeoutMinutes)
        {
            Type type = typeof(T);
            if (!_typeBreakers.TryGetValue(type, out ICircuitBreaker? breaker))
            {
                breaker = new ApiCircuitBreaker(failureThreshold, resetTimeoutMinutes);
                _typeBreakers.Add(type, breaker);
            }
            return breaker;
        }

        private static void CleanupIfNeeded()
        {
            lock (_cleanupLock)
            {
                if (DateTime.UtcNow - _lastCleanup > _cleanupInterval)
                {
                    CleanupNamedBreakers();
                    _lastCleanup = DateTime.UtcNow;
                }
            }
        }

        private static void CleanupNamedBreakers()
        {
            foreach (KeyValuePair<string, WeakReference<ICircuitBreaker>> kvp in _namedBreakers)
            {
                if (!kvp.Value.TryGetTarget(out _))
                    _namedBreakers.TryRemove(kvp.Key, out _);
            }
        }
    }
}

================================================
FILE: Tubifarry/Core/Model/AudioMetadataHandler.cs
================================================
using NLog;
using NzbDrone.Common.Instrumentation;
using NzbDrone.Core.Music;
using Tubifarry.Core.Records;
using Tubifarry.Core.Utilities;
using Xabe.FFmpeg;
using Xabe.FFmpeg.Downloader;

namespace Tubifarry.Core.Model
{
    internal class AudioMetadataHandler
    {
        private readonly Logger? _logger;
        private static bool? _isFFmpegInstalled = null;

        public string TrackPath { get; private set; }
        public Lyric? Lyric { get; set; }
        public byte[]? AlbumCover { get; set; }
        public bool UseID3v2_3 { get; set; }

        public AudioMetadataHandler(string originalPath)
        {
            TrackPath = originalPath;
            _logger = NzbDroneLogger.GetLogger(this);
        }

        /// <summary>
        /// Base codec parameters that don't change with bitrate settings
        /// </summary>
        private static readonly Dictionary<AudioFormat, string[]> BaseConversionParameters = new()
        {
            { AudioFormat.AAC,    new[] { "-codec:a aac", "-movflags +faststart", "-aac_coder twoloop" } },
            { AudioFormat.MP3,    new[] { "-codec:a libmp3lame" } },
            { AudioFormat.Opus,   new[] { "-codec:a libopus", "-vbr on", "-application audio", "-vn" } },
            { AudioFormat.Vorbis, new[] { "-codec:a libvorbis" } },
            { AudioFormat.FLAC,   new[] { "-codec:a flac", "-compression_level 8" } },
            { AudioFormat.ALAC,   new[] { "-codec:a alac" } },
            { AudioFormat.WAV,    new[] { "-codec:a pcm_s16le", "-ar 44100" } },
            { AudioFormat.MP4,    new[] { "-codec:a aac", "-movflags +faststart", "-aac_coder twoloop" } },
            { AudioFormat.AIFF,   new[] { "-codec:a pcm_s16be" } },
            { AudioFormat.OGG,    new[] { "-codec:a libvorbis" } },
            { AudioFormat.AMR,    new[] { "-codec:a libopencore_amrnb", "-ar 8000" } },
            { AudioFormat.WMA,    new[] { "-codec:a wmav2" } }
        };

        /// <summary>
        /// Format-specific bitrate/quality parameter templates
        /// </summary>
        private static readonly Dictionary<AudioFormat, Func<int, string[]>> QualityParameters = new()
        {
            {
                AudioFormat.AAC,
                bitrate => bitrate < 256
                    ? [$"-b:a {bitrate}k"]
                    : ["-q:a 2"] // 2 is highest quality for AAC
            },

            {
                AudioFormat.MP3,
                bitrate => {
                    int qualityLevel = bitrate switch {
                        >= 220 => 0,   // V0 (~220-260kbps avg)
                        >= 190 => 1,   // V1 (~190-250kbps)
                        >= 170 => 2,   // V2 (~170-210kbps)
                        >= 150 => 3,   // V3 (~150-195kbps)
                        >= 130 => 4,   // V4 (~130-175kbps)
                        >= 115 => 5,   // V5 (~115-155kbps)
                        >= 100 => 6,   // V6 (~100-140kbps)
                        >= 85 => 7,    // V7 (~85-125kbps)
                        >= 65 => 8,    // V8 (~65-105kbps)
                        _ => 9         // V9 (~45-85kbps)
                    };
                    return [$"-q:a {qualityLevel}"];
                }
            },

            {
                AudioFormat.Opus,
                bitrate => [$"-b:a {bitrate}k", "-compression_level 10"]
            },

            {
                AudioFormat.Vorbis,
                bitrate => [$"-q:a {AudioFormatHelper.MapBitrateToVorbisQuality(bitrate)}"]
            },

            { AudioFormat.MP4, bitrate => [$"-b:a {bitrate}k"] },
            {
                AudioFormat.OGG,
                bitrate => [$"-q:a {AudioFormatHelper.MapBitrateToVorbisQuality(bitrate)}"]
            },
            { AudioFormat.AMR, bitrate => [$"-ab {bitrate}k"]},
            { AudioFormat.WMA, bitrate => [$"-b:a {bitrate}k"]}
        };

        private static readonly Dictionary<AudioFormat, Func<int, string[]>> CBRQualityParameters = new()
        {
            {
                AudioFormat.MP3,
                bitrate => ["-b:a", $"{bitrate}k"]
            },
            {
                AudioFormat.AAC,
                bitrate => ["-b:a", $"{bitrate}k"]
            },
            {
                AudioFormat.Opus,
                bitrate => ["-b:a", $"{bitrate}k", "-vbr", "off"]
            },
            {
                AudioFormat.MP4,
                bitrate => ["-b:a", $"{bitrate}k"]
            },
            {
                AudioFormat.AMR,
                bitrate => ["-ab", $"{bitrate}k"]
            },
            {
                AudioFormat.WMA,
                bitrate => ["-b:a", $"{bitrate}k"]
            }
        };

        private static readonly Dictionary<AudioFormat, Func<int, string[]>> BitDepthParameters = new()
        {
            {
                AudioFormat.FLAC,
                bitDepth => bitDepth switch
                {
                    16 => ["-sample_fmt", "s16"],
                    24 => ["-sample_fmt", "s32", "-bits_per_raw_sample", "24"],
                    32 => ["-sample_fmt", "s32"],
                    _ => []
                }
            },
            {
                AudioFormat.WAV,
                bitDepth => bitDepth switch
                {
                    16 => ["-codec:a", "pcm_s16le"],
                    24 => ["-codec:a", "pcm_s24le"],
                    32 => ["-codec:a", "pcm_s32le"],
                    _ => []
                }
            },
            {
                AudioFormat.AIFF,
                bitDepth => bitDepth switch
                {
                    16 => ["-codec:a", "pcm_s16be"],
                    24 => ["-codec:a", "pcm_s24be"],
                    32 => ["-codec:a", "pcm_s32be"],
                    _ => []
                }
            }
        };

        private static readonly string[] ExtractionParameters =
        [
            "-codec:a copy",
            "-vn",
            "-movflags +faststart"
        ];

        private static readonly string[] VideoFormats =
        [
            "matroska", "webm",
            "mov", "mp4", "m4a",
            "avi",
            "asf", "wmv", "wma",
            "flv", "f4v",
            "3gp", "3g2",
            "mxf",
            "ts", "m2ts"
        ];

        private static readonly HashSet<string> CoverArtCodecs = new(StringComparer.OrdinalIgnoreCase)
        {
            "mjpeg", "png", "bmp", "gif", "webp", "jpeg", "jpg", "tiff", "tif"
        };

        private async Task<byte[]?> TryExtractCoverArtAsync()
        {
            try
            {
                using TagLib.File file = TagLib.File.Create(TrackPath);
                byte[]? data = file.Tag.Pictures?.FirstOrDefault()?.Data?.Data;
                if (data?.Length > 0)
                    return data;
            }
            catch { }

            try
            {
                IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(TrackPath);
                IVideoStream? coverStream = mediaInfo.VideoStreams
                    .FirstOrDefault(vs => CoverArtCodecs.Contains(vs.Codec ?? ""));

                if (coverStream == null)
                    return null;

                string tempCoverPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.jpg");
                try
                {
                    IConversion conversion = FFmpeg.Conversions.New()
                        .AddParameter($"-i \"{TrackPath}\"")
                        .AddParameter("-an -vcodec copy")
                        .SetOutput(tempCoverPath);

                    await conversion.Start();

                    if (File.Exists(tempCoverPath))
                        return await File.ReadAllBytesAsync(tempCoverPath);
                }
                finally
                {
                    if (File.Exists(tempCoverPath))
                        File.Delete(tempCoverPath);
                }
            }
            catch { }

            return null;
        }

        /// <summary>
        /// Converts audio to the specified format with optional bitrate control.
        /// </summary>
        /// <param name="audioFormat">Target audio format</param>
        /// <param name="targetBitrate">Optional target bitrate in kbps</param>
        /// <returns>True if conversion succeeded, false otherwise</returns>
        public async Task<bool> TryConvertToFormatAsync(AudioFormat audioFormat, int? targetBitrate = null, int? targetBitDepth = null, bool useCBR = false)
        {
            _logger?.Trace($"Converting {Path.GetFileName(TrackPath)} to {audioFormat}" +
                          (targetBitrate.HasValue ? $" at {targetBitrate}kbps" :
                           targetBitDepth.HasValue ? $" at {targetBitDepth}-bit" : ""));

            if (!CheckFFmpegInstalled())
                return false;

            if (!await TryExtractAudioFromVideoAsync())
                return false;

            _logger?.Trace($"Looking up audio format: {audioFormat}");

            if (audioFormat == AudioFormat.Unknown)
                return true;

            if (!BaseConversionParameters.ContainsKey(audioFormat))
                return false;

            string finalOutputPath = Path.ChangeExtension(TrackPath, AudioFormatHelper.GetFileExtensionForFormat(audioFormat));
            string tempOutputPath = Path.ChangeExtension(TrackPath, $".converted{AudioFormatHelper.GetFileExtensionForFormat(audioFormat)}");

            try
            {
                if (File.Exists(tempOutputPath))
                    File.Delete(tempOutputPath);

                byte[]? preservedCoverArt = AlbumCover?.Length > 0 ? AlbumCover : await TryExtractCoverArtAsync();

                IConversion conversion = await FFmpeg.Conversions.FromSnippet.Convert(TrackPath, tempOutputPath);

                foreach (string parameter in BaseConversionParameters[audioFormat])
                    conversion.AddParameter(parameter);

                if (AudioFormatHelper.IsLossyFormat(audioFormat))
                {
                    int bitrate = targetBitrate ?? AudioFormatHelper.GetDefaultBitrate(audioFormat);
                    bitrate = AudioFormatHelper.ClampBitrate(audioFormat, bitrate);

                    string[] qualityParams;
                    string mode;

                    if (useCBR && CBRQualityParameters.ContainsKey(audioFormat))
                    {
                        qualityParams = CBRQualityParameters[audioFormat](bitrate);
                        mode = "CBR";
                    }
                    else if (QualityParameters.ContainsKey(audioFormat))
                    {
                        qualityParams = QualityParameters[audioFormat](bitrate);
                        mode = "VBR";
                    }
                    else
                    {
                        qualityParams = [$"-b:a {bitrate}k"];
                        mode = "fallback";
                    }

                    foreach (string param in qualityParams)
                        conversion.AddParameter(param);

                    _logger?.Trace($"Applied {mode} quality parameters for {audioFormat} at {bitrate}kbps: {string.Join(", ", qualityParams)}");
                }

                if (!AudioFormatHelper.IsLossyFormat(audioFormat) &&
                    BitDepthParameters.ContainsKey(audioFormat) &&
                    targetBitDepth.HasValue)
                {
                    string[] bitDepthParams = BitDepthParameters[audioFormat](targetBitDepth.Value);
                    foreach (string param in bitDepthParams)
                        conversion.AddParameter(param);

                    _logger?.Trace($"Applied bit depth parameters for {audioFormat}: {targetBitDepth}-bit ({string.Join(", ", bitDepthParams)})");
                }

                _logger?.Trace($"Starting FFmpeg conversion");
                await conversion.Start();

                if (File.Exists(TrackPath))
                    File.Delete(TrackPath);

                File.Move(tempOutputPath, finalOutputPath, true);
                TrackPath = finalOutputPath;

                if (preservedCoverArt?.Length > 0)
                {
                    try
                    {
                        using TagLib.File destFile = TagLib.File.Create(TrackPath);
                        destFile.Tag.Pictures = [new TagLib.Picture(new TagLib.ByteVector(preservedCoverArt))
                        {
                            Type = TagLib.PictureType.FrontCover,
                            Description = "Album Cover"
                        }];
                        destFile.Save();
                        _logger?.Trace("Re-embedded cover art into converted file");
                    }
                    catch (Exception ex)
                    {
                        _logger?.Warn(ex, "Failed to re-embed cover art after conversion, cover art may be missing");
                    }
                }

                return true;
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to convert file to {audioFormat}: {TrackPath}");
                return false;
            }
        }

        public async Task<bool> IsVideoContainerAsync()
        {
            try
            {
                IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(TrackPath);

                bool hasRealVideo = mediaInfo.VideoStreams.Any(vs =>
                    !CoverArtCodecs.Contains(vs.Codec ?? "") &&
                    !(vs.Duration.TotalSeconds < 1 && vs.Framerate <= 1));

                if (hasRealVideo)
                    return true;

                string probeResult = await Probe.New().Start($"-v error -show_entries format=format_name -of default=noprint_wrappers=1:nokey=1 \"{TrackPath}\"");
                string formatName = probeResult?.Trim().ToLower() ?? "";
                return VideoFormats.Any(container => formatName.Contains(container));
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to check file header: {TrackPath}");
                return false;
            }
        }

        public async Task<bool> TryExtractAudioFromVideoAsync()
        {
            if (!CheckFFmpegInstalled())
                return false;

            bool isVideo = await IsVideoContainerAsync();
            if (!isVideo)
                return await EnsureFileExtAsync();

            _logger?.Trace($"Extracting audio from video file: {Path.GetFileName(TrackPath)}");

            try
            {
                IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(TrackPath);
                IAudioStream? audioStream = mediaInfo.AudioStreams.FirstOrDefault();

                if (audioStream == null)
                {
                    _logger?.Trace("No audio stream found in video file");
                    return false;
                }

                string codec = audioStream.Codec.ToLower();
                string finalOutputPath = Path.ChangeExtension(TrackPath, AudioFormatHelper.GetFileExtensionForCodec(codec));
                string tempOutputPath = Path.ChangeExtension(TrackPath, $".extracted{AudioFormatHelper.GetFileExtensionForCodec(codec)}");

                if (File.Exists(tempOutputPath))
                    File.Delete(tempOutputPath);

                IConversion conversion = await FFmpeg.Conversions.FromSnippet.ExtractAudio(TrackPath, tempOutputPath);
                foreach (string parameter in ExtractionParameters)
                    conversion.AddParameter(parameter);

                await conversion.Start();

                if (File.Exists(TrackPath))
                    File.Delete(TrackPath);

                File.Move(tempOutputPath, finalOutputPath, true);
                TrackPath = finalOutputPath;
                await EnsureFileExtAsync();

                _logger?.Trace($"Successfully extracted audio to {Path.GetFileName(TrackPath)}");
                return true;
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to extract audio from video: {TrackPath}");
                return false;
            }
        }


        /// <summary>
        /// Decrypts an encrypted audio file using FFmpeg with the provided decryption key.
        /// </summary>
        /// <param name="decryptionKey">The hex decryption key for the encrypted content.</param>
        /// <param name="codec">The audio codec of the content (e.g., "flac", "opus", "eac3").</param>
        /// <param name="token">Cancellation token.</param>
        /// <returns>True if decryption was successful, false otherwise.</returns>
        public async Task<bool> TryDecryptAsync(string decryptionKey, string? codec, CancellationToken token = default)
        {
            if (string.IsNullOrEmpty(decryptionKey))
                return true;

            if (!CheckFFmpegInstalled())
                return false;

            _logger?.Trace($"Decrypting file: {Path.GetFileName(TrackPath)}");

            try
            {
                AudioFormat format = AudioFormatHelper.GetAudioFormatFromCodec(codec ?? "aac");
                string extension = AudioFormatHelper.GetFileExtensionForFormat(format);
                string outputPath = Path.ChangeExtension(TrackPath, extension);
                string tempOutput = Path.ChangeExtension(TrackPath, $".dec{extension}");

                if (File.Exists(tempOutput))
                    File.Delete(tempOutput);

                IConversion conversion = FFmpeg.Conversions.New()
                    .AddParameter($"-decryption_key {decryptionKey}")
                    .AddParameter($"-i \"{TrackPath}\"")
                    .AddParameter("-c copy")
                    .SetOutput(tempOutput);

                await conversion.Start(token);

                if (File.Exists(TrackPath))
                    File.Delete(TrackPath);

                File.Move(tempOutput, outputPath, true);
                TrackPath = outputPath;

                _logger?.Trace($"Successfully decrypted: {Path.GetFileName(TrackPath)}");
                return true;
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to decrypt file: {TrackPath}");
                return false;
            }
        }

        public async Task<bool> TryCreateLrcFileAsync(CancellationToken token)
        {
            if (Lyric?.SyncedLyrics == null)
                return false;
            try
            {
                string lrcContent = string.Join(Environment.NewLine, Lyric.SyncedLyrics
                    .Where(lyric => !string.IsNullOrEmpty(lyric.LrcTimestamp) && !string.IsNullOrEmpty(lyric.Line))
                    .Select(lyric => $"{lyric.LrcTimestamp} {lyric.Line}"));

                string lrcPath = Path.ChangeExtension(TrackPath, ".lrc");
                await File.WriteAllTextAsync(lrcPath, lrcContent, token);
                _logger?.Trace($"Created LRC file with {Lyric.SyncedLyrics.Count} synced lyrics");
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to create LRC file: {Path.ChangeExtension(TrackPath, ".lrc")}");
                return false;
            }
            return true;
        }

        /// <summary>
        /// Ensures the file extension matches the actual audio codec.
        /// </summary>
        /// <returns>True if the file extension is correct or was successfully corrected; otherwise, false.</returns>
        public async Task<bool> EnsureFileExtAsync()
        {
            try
            {
                IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(TrackPath);
                string codec = mediaInfo.AudioStreams.FirstOrDefault()?.Codec.ToLower() ?? string.Empty;
                if (string.IsNullOrEmpty(codec))
                    return false;

                string correctExtension = AudioFormatHelper.GetFileExtensionForCodec(codec);
                string currentExtension = Path.GetExtension(TrackPath);

                if (!string.Equals(currentExtension, correctExtension, StringComparison.OrdinalIgnoreCase))
                {
                    string newPath = Path.ChangeExtension(TrackPath, correctExtension);
                    _logger?.Trace($"Correcting file extension from {currentExtension} to {correctExtension} for codec {codec}");
                    File.Move(TrackPath, newPath);
                    TrackPath = newPath;
                }
                return true;
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to ensure correct file extension: {TrackPath}");
                return false;
            }
        }

        public bool TryEmbedMetadata(Album albumInfo, Track trackInfo)
        {
            _logger?.Trace($"Embedding metadata for track: {trackInfo?.Title}");
            try
            {
                using TagLib.File file = TagLib.File.Create(TrackPath);

                if (UseID3v2_3)
                {
                    TagLib.Id3v2.Tag.DefaultVersion = 3;
                    TagLib.Id3v2.Tag.ForceDefaultVersion = true;
                }
                else
                {
                    TagLib.Id3v2.Tag.DefaultVersion = 4;
                    TagLib.Id3v2.Tag.ForceDefaultVersion = false;
                }

                if (!string.IsNullOrEmpty(trackInfo?.Title))
                    file.Tag.Title = trackInfo.Title;

                if (trackInfo?.AbsoluteTrackNumber > 0)
                    file.Tag.Track = (uint)trackInfo.AbsoluteTrackNumber;

                if (!string.IsNullOrEmpty(albumInfo?.Title))
                    file.Tag.Album = albumInfo.Title;

                if (albumInfo?.ReleaseDate?.Year > 0)
                    file.Tag.Year = (uint)albumInfo.ReleaseDate.Value.Year;

                if (albumInfo?.AlbumReleases?.Value?.FirstOrDefault()?.TrackCount > 0)
                    file.Tag.TrackCount = (uint)albumInfo.AlbumReleases.Value[0].TrackCount;

                if (trackInfo?.MediumNumber > 0)
                    file.Tag.Disc = (uint)trackInfo.MediumNumber;

                string? albumArtistName = albumInfo?.Artist?.Value?.Name;
                string? trackArtistName = trackInfo?.Artist?.Value?.Name;

                if (!string.IsNullOrEmpty(albumArtistName))
                    file.Tag.AlbumArtists = new[] { albumArtistName };

                if (!string.IsNullOrEmpty(trackArtistName))
                    file.Tag.Performers = new[] { trackArtistName };

                if (albumInfo?.AlbumReleases?.Value?.FirstOrDefault()?.Label?.Any() == true)
                    file.Tag.Copyright = albumInfo.AlbumReleases.Value[0].Label.FirstOrDefault();

                if (albumInfo?.Genres?.Any() == true)
                {
                    string[] validGenres = albumInfo.Genres.Where(g => !string.IsNullOrEmpty(g)).ToArray();
                    if (validGenres.Length > 0)
                        file.Tag.Genres = validGenres;
                }

                if (trackInfo?.Explicit == true)
                    file.Tag.Comment = "EXPLICIT";

                if (!string.IsNullOrEmpty(trackInfo?.ForeignRecordingId) &&
                    file.GetTag(TagLib.TagTypes.Id3v2) is TagLib.Id3v2.Tag id3v2Tag)
                {
                    TagLib.Id3v2.UserTextInformationFrame mbFrame = TagLib.Id3v2.UserTextInformationFrame.Get(id3v2Tag, "MusicBrainz Recording Id", true);
                    mbFrame.Text = [trackInfo.ForeignRecordingId];
                }

                try
                {
                    if (AlbumCover?.Length > 0)
                    {
                        TagLib.Picture picture = new(new TagLib.ByteVector(AlbumCover))
                        {
                            Type = TagLib.PictureType.FrontCover,
                            Description = "Album Cover"
                        };
                        file.Tag.Pictures = [picture];
                    }
                }
                catch (Exception ex)
                {
                    _logger?.Error(ex, "Failed to embed album cover");
                }

                file.Save();
                return true;
            }
            catch (TagLib.CorruptFileException ex)
            {
                _logger?.Error(ex, $"File is corrupted or has incorrect extension: {TrackPath}");
                return false;
            }
            catch (TagLib.UnsupportedFormatException ex)
            {
                _logger?.Error(ex, $"File format does not support metadata embedding: {TrackPath}");
                return false;
            }
            catch (Exception ex)
            {
                _logger?.Error(ex, $"Failed to embed metadata in track: {TrackPath}");
                return false;
            }
        }

        /// <summary>
        /// Checks if the specified audio format is supported for encoding by FFmpeg.
        /// </summary>
        /// <param name="format">The audio format to check</param>
        /// <returns>True if the format can be used as a conversion target, false otherwise</returns>
        public static bool IsTargetFormatSupportedForEncoding(AudioFormat format) => BaseConversionParameters.ContainsKey(format);

        ///// <summary>
        ///// Checks if a given audio format supports embedded metadata tags.
        ///// </summary>
        ///// <param name="format">The audio format to check</param>
        ///// <returns>True if the format supports metadata tagging, false otherwise</returns>
        public static bool SupportsMetadataEmbedding(AudioFormat format) => format switch
        {
            // Formats that DO NOT support metadata embedding
            AudioFormat.AC3 or AudioFormat.EAC3 or AudioFormat.MIDI => false,

            // Formats that DO support metadata embedding
            AudioFormat.AAC or AudioFormat.MP3 or AudioFormat.Opus or AudioFormat.Vorbis or
            AudioFormat.FLAC or AudioFormat.WAV or AudioFormat.MP4 or AudioFormat.AIFF or
            AudioFormat.OGG or AudioFormat.WMA or AudioFormat.ALAC or AudioFormat.APE => true,

            // Unknown formats - assume they might support it
            _ => true
        };

        /// <summary>
        /// Gets the actual audio codec from a file using FFmpeg and returns the corresponding AudioFormat.
        /// </summary>
        /// <param name="filePath">Path to the audio file</param>
        /// <returns>AudioFormat enum value or AudioFormat.Unknown if codec is not supported or detection fails</returns>
        public static async Task<AudioFormat> GetSupportedCodecAsync(string filePath)
        {
            try
            {
                IMediaInfo mediaInfo = await FFmpeg.GetMediaInfo(filePath);
                IAudioStream? audioStream = mediaInfo.AudioStreams.FirstOrDefault();

                if (audioStream == null)
                {
                    NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Debug("No audio stream found in file: {0}", filePath);
                    return AudioFormat.Unknown;
                }

                string codec = audioStream.Codec.ToLower();
                AudioFormat format = AudioFormatHelper.GetAudioFormatFromCodec(codec);

                NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Trace("Detected codec '{0}' as format '{1}' for file: {2}", codec, format, filePath);
                return format;
            }
            catch (Exception ex)
            {
                NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Error(ex, "Failed to detect codec for file: {0}", filePath);
                return AudioFormat.Unknown;
            }
        }

        public static bool CheckFFmpegInstalled()
        {
            if (_isFFmpegInstalled.HasValue)
                return _isFFmpegInstalled.Value;

            bool isInstalled = false;

            if (!string.IsNullOrEmpty(FFmpeg.ExecutablesPath) && Directory.Exists(FFmpeg.ExecutablesPath))
            {
                string[] ffmpegPatterns = ["ffmpeg", "ffmpeg.exe", "ffmpeg.bin"];
                string[] files = Directory.GetFiles(FFmpeg.ExecutablesPath);
                if (files.Any(file => ffmpegPatterns.Contains(Path.GetFileName(file), StringComparer.OrdinalIgnoreCase) && IsExecutable(file)))
                {
                    isInstalled = true;
                }
            }

            if (!isInstalled)
            {
                string? ffmpegEnv = Environment.GetEnvironmentVariable("FFMPEG");
                if (!string.IsNullOrEmpty(ffmpegEnv))
                {
                    string dir = File.Exists(ffmpegEnv) ? Path.GetDirectoryName(ffmpegEnv)! : ffmpegEnv;
                    if (Directory.Exists(dir))
                    {
                        string[] ffmpegPatterns = ["ffmpeg", "ffmpeg.exe", "ffmpeg.bin"];
                        if (Directory.GetFiles(dir).Any(file => ffmpegPatterns.Contains(Path.GetFileName(file), StringComparer.OrdinalIgnoreCase) && IsExecutable(file)))
                        {
                            FFmpeg.SetExecutablesPath(dir);
                            isInstalled = true;
                        }
                    }
                }
            }

            if (!isInstalled)
            {
                foreach (string path in Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [])
                {
                    if (Directory.Exists(path))
                    {
                        string[] ffmpegPatterns = ["ffmpeg", "ffmpeg.exe", "ffmpeg.bin"];
                        string[] files = Directory.GetFiles(path);

                        if (files.Any(file => ffmpegPatterns.Contains(Path.GetFileName(file), StringComparer.OrdinalIgnoreCase) && IsExecutable(file)))
                        {
                            FFmpeg.SetExecutablesPath(path);
                            isInstalled = true;
                            break;
                        }
                    }
                }
            }

            if (!isInstalled)
                NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Trace("FFmpeg not found in configured path or system PATH");

            _isFFmpegInstalled = isInstalled;
            return isInstalled;
        }

        private static bool IsExecutable(string filePath)
        {
            try
            {
                using FileStream stream = File.OpenRead(filePath);
                byte[] magicNumber = new byte[4];
                stream.Read(magicNumber, 0, 4);

                // Windows PE
                if (magicNumber[0] == 0x4D && magicNumber[1] == 0x5A)
                    return true;

                // Linux ELF
                if (magicNumber[0] == 0x7F && magicNumber[1] == 0x45 &&
                    magicNumber[2] == 0x4C && magicNumber[3] == 0x46)
                    return true;

                // macOS Mach-O (32-bit: 0xFEEDFACE, 64-bit: 0xFEEDFACF)
                if (magicNumber[0] == 0xFE && magicNumber[1] == 0xED &&
                    magicNumber[2] == 0xFA &&
                    (magicNumber[3] == 0xCE || magicNumber[3] == 0xCF))
                    return true;

                // Universal Binary (fat_header)
                if (magicNumber[0] == 0xCA && magicNumber[1] == 0xFE &&
                    magicNumber[2] == 0xBA && magicNumber[3] == 0xBE)
                    return true;
            }
            catch { }
            return false;
        }

        public static void ResetFFmpegInstallationCheck() => _isFFmpegInstalled = null;

        public static Task InstallFFmpeg(string path)
        {
            NzbDroneLogger.GetLogger(typeof(AudioMetadataHandler)).Trace($"Installing FFmpeg to: {path}");
            ResetFFmpegInstallationCheck();
            FFmpeg.SetExecutablesPath(path);
            return CheckFFmpegInstalled() ? Task.CompletedTask : FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official, path);
        }
    }
}

================================================
FILE: Tubifarry/Core/Model/FileCache.cs
================================================
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace Tubifarry.Core.Model
{
    public class FileCache
    {
        private readonly string _cacheDirectory;
        private static readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };

        public FileCache(string cacheDirectory)
        {
            if (!Directory.Exists(cacheDirectory))
                Directory.CreateDirectory(cacheDirectory);
            _cacheDirectory = cacheDirectory;

            CleanupOldCacheFiles();
        }

        /// <summary>
        /// Deletes any JSON files stored directly in the root cache directory,
        /// as these are from the old file naming scheme.
        /// </summary>
        private void CleanupOldCacheFiles()
        {
            try
            {
                foreach (string file in Directory.GetFiles(_cacheDirectory, "*.json", SearchOption.TopDirectoryOnly))
                    File.Delete(file);
            }
            catch
            { }
        }

        /// <summary>
        /// Retrieves cached data for the given key if available and not expired.
        /// </summary>
        public async Task<T?> GetAsync<T>(string cacheKey)
        {
            string cacheFilePath = GetCacheFilePath(cacheKey);

            if (!File.Exists(cacheFilePath))
                return default;

            string json = await File.ReadAllTextAsync(cacheFilePath);
            CachedData<T>? cachedData = JsonSerializer.Deserialize<CachedData<T>>(json);

            if (cachedData == null || DateTime.UtcNow - cachedData.CreatedAt > cachedData.ExpirationDuration)
            {
                try
                {
                    File.Delete(cacheFilePath);
                }
                catch { }
                return default;
            }

            return cachedData.Data;
        }

        /// <summary>
        /// Caches the provided data with the specified expiration duration.
        /// </summary>
        public async Task SetAsync<T>(string cacheKey, T data, TimeSpan expirationDuration)
        {
            string cacheFilePath = GetCacheFilePath(cacheKey);

            string directory = Path.GetDirectoryName(cacheFilePath)!;
            if (!Directory.Exists(directory))
                Directory.CreateDirectory(directory);

            CachedData<T> cachedData = new()
            {
                Data = data,
                CreatedAt = DateTime.UtcNow,
                ExpirationDuration = expirationDuration
            };

            string json = JsonSerializer.Serialize(cachedData, _jsonOptions);
            await File.WriteAllTextAsync(cacheFilePath, json);
        }

        /// <summary>
        /// Checks whether a valid cache file exists for the given key.
        /// </summary>
        public bool IsCacheValid(string cacheKey, TimeSpan expirationDuration)
        {
            string cacheFilePath = GetCacheFilePath(cacheKey);

            if (!File.Exists(cacheFilePath))
                return false;

            string json = File.ReadAllText(cacheFilePath);
            CachedData<object>? cachedData = JsonSerializer.Deserialize<CachedData<object>>(json);

            return cachedData != null && DateTime.UtcNow - cachedData.CreatedAt <= expirationDuration;
        }

        /// <summary>
        /// Computes the file path for a given cache key using a SHA256 hash and sharding.
        /// </summary>
        private string GetCacheFilePath(string cacheKey)
        {
            byte[] hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(cacheKey));
            string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();

            string subdirectory = hashString[..2];
            string fileName = $"{hashString}.json";

            return Path.Combine(_cacheDirectory, subdirectory, fileName);
        }

        /// <summary>
        /// Ensures that the cache directory is writable and path length is valid.
        /// </summary>
        public void CheckDirectory()
        {
            try
            {
                if (!Directory.Exists(_cacheDirectory))
                    Directory.CreateDirectory(_cacheDirectory);

                string testFile = Path.Combine(_cacheDirectory, "test.tmp");
                File.WriteAllText(testFile, "test");
                File.Delete(testFile);

                int maxPath = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 255 : 4040;
                int maxCachePathLength = _cacheDirectory.Length + 40;
                if (maxCachePathLength >= maxPath)
                    throw new PathTooLongException($"Cache path exceeds OS limits ({maxCachePathLength} characters). Use a shorter base directory.");
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException($"Cache directory validation failed: {ex.Message}", ex);
            }
        }
    }

    public class CachedData<T>
    {
        public T? Data { get; set; }
        public DateTime CreatedAt { get; set; }
        public TimeSpan ExpirationDuration { get; set; }
    }
}

================================================
FILE: Tubifarry/Core/Model/PlaylistItem.cs
================================================
namespace Tubifarry.Core.Model;

public record PlaylistItem(
    string ArtistMusicBrainzId,
    string? AlbumMusicBrainzId,
    string ArtistName,
    string? AlbumTitle,
    string? TrackTitle = null,
    string? ForeignRecordingId = null);

public record PlaylistSnapshot(
    string ListName,
    List<PlaylistItem> Items,
    DateTime FetchedAt);

public interface IPlaylistTrackSource
{
    List<PlaylistItem> FetchTrackLevelItems();
}


================================================
FILE: Tubifarry/Core/Model/TrustedSessionException.cs
================================================
namespace Tubifarry.Core.Model
{
    /// <summary>
    /// Exceptions specific to the YouTube trusted session authentication process
    /// </summary>
    public class TrustedSessionException : Exception
    {
        public TrustedSessionException(string message) : base(message)
        {
        }

        public TrustedSessionException(string message, Exception innerException) : base(message, innerException)
        {
        }

        public TrustedSessionException()
        { }
    }
}

================================================
FILE: Tubifarry/Core/Records/Lyric.cs
================================================
using DownloadAssistant.Base;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NzbDrone.Core.Parser.Model;
using System.Text.RegularExpressions;

namespace Tubifarry.Core.Records
{
    public record Lyric(string? PlainLyrics, List<SyncLine>? SyncedLyrics)
    {
        public static async Task<Lyric?> FetchLyricsFromLRCLIBAsync(string instance, ReleaseInfo releaseInfo, string trackName, int duration = 0, CancellationToken token = default)
        {
            string requestUri = $"{instance}/api/get?artist_name={Uri.EscapeDataString(releaseInfo.Artist)}&track_name={Uri.EscapeDataString(trackName)}&album_name={Uri.EscapeDataString(releaseInfo.Album)}{(duration != 0 ? $"&duration={duration}" : "")}";
            HttpResponseMessage response = await HttpGet.HttpClient.GetAsync(requestUri, token);
            if (!response.IsSuccessStatusCode) return null;
            JObject json = JObject.Parse(await response.Content.ReadAsStringAsync(token));
            return new Lyric(json["plainLyrics"]?.ToString() ?? string.Empty, SyncLine.ParseSyncedLyrics(json["syncedLyrics"]?.ToString() ?? string.Empty));
        }
    }

    public partial record class SyncLine
    {
        [JsonProperty("lrc_timestamp")]
        public string? LrcTimestamp { get; init; }

        [JsonProperty("milliseconds")]
        public string? Milliseconds { get; init; }

        [JsonProperty("duration")]
        public string? Duration { get; init; }

        [JsonProperty("line")]
        public string? Line { get; init; }

        public static List<SyncLine> ParseSyncedLyrics(string syncedLyrics)
        {
            List<SyncLine> lyric = [];
            string[] array = syncedLyrics.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
            for (int i = 0; i < array.Length; i++)
            {
                Match match = TagRegex().Match(array[i]);
                if (match.Success)
                {
                    string value = match.Groups[1].Value;
                    string line = match.Groups[2].Value.Trim();
                    double totalMilliseconds = TimeSpan.ParseExact(value, "mm\\:ss\\.ff", null).TotalMilliseconds;
                    lyric.Add(new SyncLine
                    {
                        LrcTimestamp = "[" + value + "]",
                        Line = line,
                        Milliseconds = totalMilliseconds.ToString()
                    });
                }
            }
            return lyric;
        }

        [GeneratedRegex("\\[(\\d{2}:\\d{2}\\.\\d{2})\\](.*)")]
        private static partial Regex TagRegex();
    }
}

================================================
FILE: Tubifarry/Core/Records/MappingAgent.cs
================================================
namespace Tubifarry.Core.Records
{
    public record MappingAgent
    {
        public string UserAgent { get; set; } = Tubifarry.UserAgent;

        public static T? MapAgent<T>(T? mappingAgent, string userAgent) where T : MappingAgent
        {
            if (mappingAgent != null)
                mappingAgent.UserAgent = userAgent;
            return mappingAgent;
        }

        public static IEnumerable<T>? MapAgent<T>(IEnumerable<T>? mappingAgent, string userAgent) where T : MappingAgent
        {
            if (mappingAgent != null)
            {
                foreach (T mapping in mappingAgent)
                    mapping.UserAgent = userAgent;
            }

            return mappingAgent;
        }

        public static List<T>? MapAgent<T>(List<T>? mappingAgent, string userAgent) where T : MappingAgent
        {
            if (mappingAgent != null)
            {
                foreach (T mapping in mappingAgent)
                    mapping.UserAgent = userAgent;
            }

            return mappingAgent;
        }
    }
}

================================================
FILE: Tubifarry/Core/Records/MusicBrainzData.cs
================================================
using System.Xml.Linq;

namespace Tubifarry.Core.Records
{
    public record MusicBrainzSearchItem(string? Title, string? AlbumId, string? Artist, string? ArtistId, DateTime ReleaseDate)
    {
        public static MusicBrainzSearchItem FromXml(XElement release, XNamespace ns)
        {
            XElement? artistCredit = release.Element(ns + "artist-credit")?.Element(ns + "name-credit")?.Element(ns + "artist");
            XElement? releaseGroup = release.Element(ns + "release-group");

            return new MusicBrainzSearchItem(release.Element(ns + "title")?.Value, releaseGroup?.Attribute("id")?.Value, artistCredit?.Element(ns + "name")?.Value,
                artistCredit?.Attribute("id")?.Value, DateTime.TryParse(release.Element(ns + "date")?.Value, out DateTime date) ? date : DateTime.MinValue);
        }
    }

    public record MusicBrainzAlbumItem(string? AlbumId, string? Title, string? Type, string? PrimaryType, List<string> SecondaryTypes, string? Artist, string? ArtistId, DateTime ReleaseDate)
    {
        public static MusicBrainzAlbumItem? FromXml(XElement releaseGroup, XNamespace ns)
        {
            if (releaseGroup == null)
                return null;

            List<string> secondaryTypes = new();
            XElement? secondaryTypeList = releaseGroup.Element(ns + "secondary-type-list");
            if (secondaryTypeList != null)
            {
                secondaryTypes = secondaryTypeList.Elements(ns + "secondary-type")
                    .Select(e => e.Value)
                    .Where(v => !string.IsNullOrWhiteSpace(v))
                    .ToList();
            }

            return new MusicBrainzAlbumItem(
                releaseGroup.Attribute("id")?.Value,
                releaseGroup.Element(ns + "title")?.Value,
                releaseGroup.Attribute("type")?.Value,
                releaseGroup.Element(ns + "primary-type")?.Value,
                secondaryTypes,
                releaseGroup.Element(ns + "artist-credit")?.Element(ns + "name-credit")?.Element(ns + "artist")?.Element(ns + "name")?.Value,
                releaseGroup.Element(ns + "artist-credit")?.Element(ns + "name-credit")?.Element(ns + "artist")?.Attribute("id")?.Value,
                DateTime.TryParse(releaseGroup.Element(ns + "first-release-date")?.Value, out DateTime date) ? date : DateTime.MinValue);
        }
    }
}

================================================
FILE: Tubifarry/Core/Records/YouTubeSession.cs
================================================
using System.Net;

namespace Tubifarry.Core.Records
{
    /// <summary>
    /// Represents session token data for transportation and caching
    /// </summary>
    public record SessionTokens(
        string PoToken,
        string VisitorData,
        DateTime ExpiryUtc,
        string Source = "Unknown")
    {
        /// <summary>
        /// Checks if the tokens are still valid (not expired)
        /// </summary>
        public bool IsValid => !IsEmpty && DateTime.UtcNow < ExpiryUtc;

        /// <summary>
        /// Checks if the tokens are empty
        /// </summary>
        public bool IsEmpty => string.IsNullOrEmpty(PoToken) || string.IsNullOrEmpty(VisitorData);

        /// <summary>
        /// Gets the remaining time until expiry
        /// </summary>
        public TimeSpan TimeUntilExpiry => ExpiryUtc - DateTime.UtcNow;
    }

    /// <summary>
    /// Represents client session configuration and state
    /// </summary>
    public record ClientSessionInfo(
        SessionTokens? Tokens,
        Cookie[]? Cookies,
        string GeographicalLocation = "US")
    {
        /// <summary>
        /// Checks if the session has valid authentication data
        /// </summary>
        public bool HasValidTokens => Tokens?.IsValid == true;

        /// <summary>
        /// Checks if the session has cookies
        /// </summary>
        public bool HasCookies => Cookies?.Length > 0;

        /// <summary>
        /// Gets a summary of the authentication methods available
        /// </summary>
        public string AuthenticationSummary =>
            $"Tokens: {(HasValidTokens ? "Valid" : "Invalid/Missing")}, " +
            $"Cookies: {(HasCookies ? $"{Cookies!.Length} cookies" : "None")}";

        /// <summary>
        /// Compares this session info with another to detect if authentication has changed
        /// </summary>
        public bool IsEquivalentTo(ClientSessionInfo? other)
        {
            if (other == null) return false;

            return Tokens?.PoToken == other.Tokens?.PoToken &&
                   Tokens?.VisitorData == other.Tokens?.VisitorData &&
                   GeographicalLocation == other.GeographicalLocation &&
                   CookiesAreEquivalent(Cookies, other.Cookies);
        }

        private static bool CookiesAreEquivalent(Cookie[]? cookies1, Cookie[]? cookies2)
        {
            if (cookies1 == null && cookies2 == null) return true;
            if (cookies1 == null || cookies2 == null) return false;
            if (cookies1.Length != cookies2.Length) return false;

            return cookies1.Zip(cookies2).All(pair =>
                pair.First.Name == pair.Second.Name &&
                pair.First.Value == pair.Second.Value);
        }
    }
}

================================================
FILE: Tubifarry/Core/Replacements/ExtendedHttpIndexerBase.cs
================================================
using FluentValidation.Results;
using NLog;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.Configuration;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Indexers.Exceptions;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
using System.Net;
using Sentry;
using Tubifarry.Core.Telemetry;
using Tubifarry.Core.Utilities;

namespace Tubifarry.Core.Replacements
{
    /// <summary>
    /// Enhanced generic base class for HTTP indexers with advanced search and result handling
    /// </summary>
    public abstract class ExtendedHttpIndexerBase<TSettings, TIndexerPageableRequest> : IndexerBase<TSettings>
        where TSettings : IIndexerSettings, new()
        where TIndexerPageableRequest : IndexerPageableRequest
    {
        protected const int MaxNumResultsPerQuery = 1000;

        protected readonly IHttpClient _httpClient;
        protected readonly ISentryHelper _sentry;
        protected new readonly Logger _logger = null!;

        public override bool SupportsRss { get; }
        public override bool SupportsSearch { get; }

        public abstract int PageSize { get; }
        public abstract TimeSpan RateLimit { get; }

        public abstract IIndexerRequestGenerator<TIndexerPageableRequest> GetExtendedRequestGenerator();

        public abstract IParseIndexerResponse GetParser();

        protected ExtendedHttpIndexerBase(
            IHttpClient httpClient,
            IIndexerStatusService indexerStatusService,
            IConfigService configService,
            IParsingService parsingService,
            ISentryHelper sentry,
            Logger logger)
            : base(indexerStatusService, configService, parsingService, logger)
        {
            _httpClient = httpClient;
            _sentry = sentry;
            _logger = logger;
        }

        // Concrete implementation of inherited abstract methods
        public override async Task<IList<ReleaseInfo>> FetchRecent()
        {
            if (!SupportsRss)
                return Array.Empty<ReleaseInfo>();

            return await FetchReleases(g => g.GetRecentRequests(), true);
        }

        public override async Task<IList<ReleaseInfo>> Fetch(AlbumSearchCriteria searchCriteria)
        {
            if (!SupportsSearch)
                return Array.Empty<ReleaseInfo>();

            return await FetchReleases(g => g.GetSearchRequests(searchCriteria));
        }

        public override async Task<IList<ReleaseInfo>> Fetch(ArtistSearchCriteria searchCriteria)
        {
            if (!SupportsSearch) Array.Empty<ReleaseInfo>();

            return await FetchReleases(g => g.GetSearchRequests(searchCriteria));
        }

        public override HttpRequest GetDownloadRequest(string link) => new(link);

        protected virtual async Task<IList<ReleaseInfo>> FetchReleases(
            Func<IIndexerRequestGenerator<TIndexerPageableRequest>, IndexerPageableRequestChain<TIndexerPageableRequest>> pageableRequestChainSelector,
            bool isRecent = false)
        {
            var span = _sentry.StartSpan("indexer.fetch");
            _sentry.SetSpanTag(span, "indexer.type", GetType().Name);
            _sentry.SetSpanTag(span, "indexer.is_recent", isRecent.ToString());

            List<ReleaseInfo> releases = [];
            string url = string.Empty;
            TimeSpan minimumBackoff = TimeSpan.FromHours(1);

            try
            {
                IIndexerRequestGenerator<TIndexerPageableRequest> generator = GetExtendedRequestGenerator();
                IParseIndexerResponse parser = GetParser();

                IndexerPageableRequestChain<TIndexerPageableRequest> pageableRequestChain = pageableRequestChainSelector(generator);

                bool fullyUpdated = false;
                ReleaseInfo? lastReleaseInfo = null;
                if (isRecent)
                {
                    lastReleaseInfo = _indexerStatusService.GetLastRssSyncReleaseInfo(Definition.Id);
                }

                for (int i = 0; i < pageableRequestChain.Tiers; i++)
                {
                    IEnumerable<TIndexerPageableRequest> pageableRequests = pageableRequestChain.GetTier(i);

                    List<ReleaseInfo> tierReleases = [];

                    foreach (TIndexerPageableRequest pageableRequest in pageableRequests)
                    {
                        List<ReleaseInfo> pagedReleases = [];

                        foreach (IndexerRequest? request in pageableRequest)
                        {
                            url = request.Url.FullUri;

                            IList<ReleaseInfo> page = await FetchPage(request, parser);

                            pagedReleases.AddRange(page);

                            if (ShouldStopFetchingPages(isRecent, page, lastReleaseInfo, pagedReleases, ref fullyUpdated))
                                break;

                            if (!IsFullPage(page))
                                break;
                        }

                        tierReleases.AddRange(pagedReleases.Where(IsValidRelease));
                    }

                    releases.AddRange(tierReleases);

                    if (pageableRequestChain.AreTierResultsUsable(i, tierReleases.Count))
                    {
                        _logger.Debug($"Tier {i + 1} found {tierReleases.Count} usable results out of total {releases.Count} results. Stopping search.");
                        break;
                    }
                    else if (tierReleases.Count != 0)
                    {
                        _logger.Debug($"Tier {i + 1} found {tierReleases.Count} results out of total {releases.Count}, but doesn't meet usability criteria. Trying next tier.");
                    }
                    else
                    {
                        _logger.Debug($"Tier {i + 1} found no results. Total results so far: {releases.Count}. Trying next tier.");
                    }
                }

                if (isRecent && !releases.Empty())
                    UpdateRssSyncStatus(releases, lastReleaseInfo, fullyUpdated);

                _indexerStatusService.RecordSuccess(Definition.Id);
                _sentry.SetSpanData(span, "result.count", releases.Count);
                _sentry.FinishSpan(span, SpanStatus.Ok);
            }
            catch (Exception ex)
            {
                HandleException(ex, url, minimumBackoff);
                _sentry.FinishSpan(span, ex);
            }

            return CleanupReleases(releases, isRecent);
        }

        protected virtual bool ShouldStopFetchingPages(bool isRecent, IList<ReleaseInfo> page, ReleaseInfo? lastReleaseInfo, List<ReleaseInfo> pagedReleases, ref bool fullyUpdated)
        {
            if (!isRecent)
                return pagedReleases.Count >= MaxNumResultsPerQuery;

            if (!page.Any())
                return false;

            if (lastReleaseInfo == null)
            {
                fullyUpdated = true;
                return true;
            }

            DateTime oldestReleaseDate = page.Min(v => v.PublishDate);
            if (oldestReleaseDate < lastReleaseInfo.PublishDate || page.Any(v => v.DownloadUrl == lastReleaseInfo.DownloadUrl))
            {
                fullyUpdated = true;
                return true;
            }

            if (pagedReleases.Count >= MaxNumResultsPerQuery && oldestReleaseDate < DateTime.UtcNow - TimeSpan.FromHours(24))
            {
                fullyUpdated = false;
                return true;
            }

            return false;
        }

        protected virtual bool IsFullPage(IList<ReleaseInfo> page) => PageSize != 0 && page.Count >= PageSize;

        protected virtual bool IsValidRelease(ReleaseInfo release)
        {
            if (release.Title.IsNullOrWhiteSpace())
            {
                _logger.Trace("Invalid Release: '{0}' from indexer: {1}. No title provided.", release.InfoUrl, Definition.Name);
                return false;
            }

            if (release.DownloadUrl.IsNullOrWhiteSpace())
            {
                _logger.Trace("Invalid Release: '{0}' from indexer: {1}. No Download URL provided.", release.Title, Definition.Name);
                return false;
            }

            return true;
        }

        private void UpdateRssSyncStatus(List<ReleaseInfo> releases, ReleaseInfo? lastReleaseInfo, bool fullyUpdated)
        {
            List<ReleaseInfo> ordered = [.. releases.OrderByDescending(v => v.PublishDate)];

            if (!fullyUpdated && lastReleaseInfo != null)
            {
                DateTime gapStart = lastReleaseInfo.PublishDate;
                DateTime gapEnd = ordered[^1].PublishDate;
                _logger.Warn("Indexer {0} rss sync didn't cover the period between {1} and {2} UTC. Search may be required.", Definition.Name, gapStart, gapEnd);
            }

            lastReleaseInfo = ordered[0];
            _indexerStatusService.UpdateRssSyncStatus(Definition.Id, lastReleaseInfo);
        }

        private void HandleException(Exception ex, string url, TimeSpan minimumBackoff)
        {
            switch (ex)
            {
                case WebException webException:
                    HandleWebException(webException, url);
                    break;

                case TooManyRequestsException tooManyRequestsEx:
                    HandleTooManyRequestsException(tooManyRequestsEx, minimumBackoff);
                    break;

                case HttpException httpException:
                    HandleHttpException(httpException, url);
                    break;

                case RequestLimitReachedException requestLimitEx:
                    HandleRequestLimitReachedException(requestLimitEx, minimumBackoff);
                    break;

                case ApiKeyException:
                    _indexerStatusService.RecordFailure(Definition.Id);
                    _logger.Warn("Invalid API Key for {0} {1}", this, url);
                    break;

                case IndexerException indexerEx:
                    _indexerStatusService.RecordFailure(Definition.Id);
                    _logger.Warn(indexerEx, "{0}", url);
                    break;

                case TaskCanceledException taskCancelledEx:
                    _indexerStatusService.RecordFailure(Definition.Id);
                    _logger.Warn(taskCancelledEx, "Unable to connect to indexer, possibly due to a timeout. {0}", url);
                    break;

                default:
                    _indexerStatusService.RecordFailure(Definition.Id);
                    ex.WithData("FeedUrl", url);
                    _logger.Error(ex, "An error occurred while processing feed. {0}", url);
                    break;
            }
        }

        private void HandleWebException(WebException webException, string url)
        {
            if (webException.Status is WebExceptionStatus.NameResolutionFailure or WebExceptionStatus.ConnectFailure)
                _indexerStatusService.RecordConnectionFailure(Definition.Id);
            else
                _indexerStatusService.RecordFailure(Definition.Id);

            if (webException.Message.Contains("502") || webException.Message.Contains("503") || webException.Message.Contains("504") || webException.Message.Contains("timed out"))
                _logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message);
            else
                _logger.Warn("{0} {1} {2}", this, url, webException.Message);
        }

        private void HandleHttpException(HttpException ex, string url)
        {
            _indexerStatusService.RecordFailure(Definition.Id);
            if (ex.Response.HasHttpServerError)
                _logger.Warn("Unable to connect to {0} at [{1}]. Indexer's server is unavailable. Try again later. {2}", this, url, ex.Message);
            else
                _logger.Warn("{0} {1}", this, ex.Message);
        }

        private void HandleTooManyRequestsException(TooManyRequestsException ex, TimeSpan minimumBackoff)
        {
            TimeSpan retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : minimumBackoff;
            _indexerStatusService.RecordFailure(Definition.Id, retryTime);

            _logger.Warn("API Request Limit reached for {0}. Disabled for {1}", this, retryTime);
        }

        private void HandleRequestLimitReachedException(RequestLimitReachedException ex, TimeSpan minimumBackoff)
        {
            TimeSpan retryTime = ex.RetryAfter != TimeSpan.Zero ? ex.RetryAfter : minimumBackoff;
            _indexerStatusService.RecordFailure(Definition.Id, retryTime);

            _logger.Warn("API Request Limit reached for {0}. Disabled for {1}", this, retryTime);
        }

        protected virtual async Task<IList<ReleaseInfo>> FetchPage(IndexerRequest request, IParseIndexerResponse parser)
        {
            IndexerResponse response = await FetchIndexerResponse(request);

            try
            {
                return [.. parser.ParseResponse(response)];
            }
            catch (Exception ex)
            {
                ex.WithData(response.HttpResponse, 128 * 1024);
                _logger.Trace("Unexpected Response content ({0} bytes): {1}", response.HttpResponse.ResponseData.Length, response.HttpResponse.Content);
                throw;
            }
        }

        protected virtual async Task<IndexerResponse> FetchIndexerResponse(IndexerRequest request)
        {
            _logger.Debug("Downloading Feed " + request.HttpRequest.ToString(false));

            if (request.HttpRequest.RateLimit < RateLimit)
                request.HttpRequest.RateLimit = RateLimit;

            request.HttpRequest.RateLimitKey = Definition.Id.ToString();

            HttpResponse response = await _httpClient.ExecuteAsync(request.HttpRequest);

            return new IndexerResponse(request, response);
        }

        protected override async Task Test(List<ValidationFailure> failures) => failures.AddIfNotNull(await TestConnection());

        protected virtual async Task<ValidationFailure> TestConnection()
        {
            try
            {
                IParseIndexerResponse parser = GetParser();
                IIndexerRequestGenerator<TIndexerPageableRequest> generator = GetExtendedRequestGenerator();
                IndexerRequest? firstRequest = generator.GetRecentRequests().GetAllTiers().FirstOrDefault()?.FirstOrDefault();

                if (firstRequest == null)
                {
                    return new ValidationFailure(string.Empty, "No rss feed query available. This may be an issue with the indexer or your indexer category settings.");
                }

                IList<ReleaseInfo> releases = await FetchPage(firstRequest, parser);

                if (releases.Empty())
                {
                    return new ValidationFailure(string.Empty, "Query successful, but no results in the configured categories were returned from your indexer. This may be an issue with the indexer or your indexer category settings.");
                }
            }
            catch (Exception ex)
            {
                _logger.Warn(ex, "Unable to connect to indexer");
                return new ValidationFailure(string.Empty, $"Unable to connect to indexer: {ex.Message}");
            }

            return null!;
        }
    }
}

================================================
FILE: Tubifarry/Core/Replacements/FlexibleHttpDispatcher.cs
================================================
using NLog;
using NzbDrone.Common.Cache;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Common.Http.Dispatchers;
using NzbDrone.Common.Http.Proxy;
using System.Net;
using Tubifarry.Core.Utilities;

namespace Tubifarry.Core.Replacements
{
    public class FlexibleHttpDispatcher : ManagedHttpDispatcher, IHttpDispatcher
    {
        public const string UA_PARAM = "x-user-agent";

        public FlexibleHttpDispatcher(IHttpProxySettingsProvider proxySettingsProvider,
            IUserAgentValidator userAgentValidator,
            ICreateManagedWebProxy createManagedWebProxy,
            ICertificateValidationService certificateValidationService,
            IUserAgentBuilder userAgentBuilder,
            ICacheManager cacheManager,
            Logger logger)
            : base(proxySettingsProvider, createManagedWebProxy, certificateValidationService,
                  userAgentBuilder, cacheManager, logger)
        {
            userAgentValidator.AddAllowedPattern(@"^[^/]+/\d+(\.\d+)*$");
            foreach (string pattern in blacklisted)
                userAgentValidator.AddBlacklistPattern(pattern);
        }

        private readonly string[] blacklisted =
        [
            // Fuzzy matching for Lidarr (catches variations like Lidar, Lidaar, etc.)
            ".*[a]r+.*",
            ".*[e]rr+.*",
            // Fuzzy matching for Tubifarry (catches variations like Tubifary, Tubiferry, etc.)
            ".*t[uo]b?[iey]?.*",
            // Additional fuzzy blocking patterns
            ".*.[fbvd][ae]r+.*",
            ".*b[o0]t.*",
            ".*cr[ae]wl[ae]r.*",
            ".*pr[o0]xy.*",
            ".*scr[ae]p[ae]r.*"
        ];

        async Task<HttpResponse> IHttpDispatcher.GetResponseAsync(HttpRequest request, CookieContainer cookies)
        {
            ExtractUserAgentFromUrl(request);
            return await GetResponseAsync(request, cookies);
        }

        private static void ExtractUserAgentFromUrl(HttpRequest request)
        {
            if (request.Url.Query.IsNullOrWhiteSpace()) return;

            string[] parts = request.Url.Query.Split('&');
            string? uaPart = parts.FirstOrDefault(p => p.StartsWith($"{UA_PARAM}="));

            if (uaPart != null)
            {
                string userAgent = Uri.UnescapeDataString(uaPart.Split('=')[1]);
                request.Headers.Set("User-Agent", userAgent);
                request.Url = request.Url.SetQuery(string.Join("&", parts.Where(p => p != uaPart)));
            }
        }

        protected override void AddRequestHeaders(HttpRequestMessage webRequest, HttpHeader headers)
        {
            string userAgent = headers.GetSingleValue("User-Agent");

            HttpHeader filtered = [];
            foreach (KeyValuePair<string, string> h in headers.Where(h => h.Key != "User-Agent"))
                filtered.Add(h.Key, h.Value);

            base.AddRequestHeaders(webRequest, filtered);

            if (userAgent != null)
            {
                webRequest.Headers.UserAgent.Clear();
                webRequest.Headers.UserAgent.ParseAdd(userAgent);
            }
        }
    }
}

================================================
FILE: Tubifarry/Core/Replacements/IIndexerRequestGenerator.cs
================================================
using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using Tubifarry.Core.Utilities;

namespace Tubifarry.Core.Replacements
{
    /// <summary>
    /// Generic indexer request generator interface that supports different request chain types
    /// </summary>
    public interface IIndexerRequestGenerator<TIndexerPageableRequest>
            where TIndexerPageableRequest : IndexerPageableRequest
    {
        /// <summary>
        /// Gets requests for recent releases
        /// </summary>
        IndexerPageableRequestChain<TIndexerPageableRequest> GetRecentRequests();

        /// <summary>
        /// Gets search requests for an album
        /// </summary>
        IndexerPageableRequestChain<TIndexerPageableRequest> GetSearchRequests(AlbumSearchCriteria searchCriteria);

        /// <summary>
        /// Gets search requests for an artist
        /// </summary>
        IndexerPageableRequestChain<TIndexerPageableRequest> GetSearchRequests(ArtistSearchCriteria searchCriteria);
    }
}

================================================
FILE: Tubifarry/Core/Telemetry/ISearchContextBuffer.cs
================================================
namespace Tubifarry.Core.Telemetry
{
    public interface ISearchContextBuffer
    {
        void LogSearch(string searchId, string query, string? artist, string? album, string strategy, int resultCount);

        void LogSearchSettings(string searchId, int trackCountFilter, bool normalizedSearch, bool appendYear, bool handleVolumeVariations, bool useFallbackSearch, bool useTrackFallback, int minimumResults, bool hasTemplates);

        void LogExpectedTracks(string searchId, List<string> trackNames, int expectedCount);

        void LogParseResult(string searchId, string folderPath, string regexMatchType, int fuzzyArtistScore, int fuzzyAlbumScore, int fuzzyArtistTokenSort, int fuzzyAlbumTokenSort, int priority, string codec, int bitrate, int bitDepth, int trackCountExpected, int trackCountActual, string username, bool hasFreeSlot, int queueLength, List<string>? directoryFiles, bool isInteractive);

        void UpdateSearchResultCount(string searchId, int actualResultCount);

        void LogGrab(string searchId, string downloadId, bool isInteractive);

        SlskdBufferedContext? GetContext(string downloadId);

        SlskdBufferedContext? GetAndRemoveContext(string downloadId);

        void AddBreadcrumb(string downloadId, string message);

        void RecordImport(string albumKey);

        bool WasRecentlyImported(string albumKey, out int daysSinceImport);
    }
}


================================================
FILE: Tubifarry/Core/Telemetry/ISentryHelper.cs
================================================
namespace Tubifarry.Core.Telemetry
{
    public interface ISentryHelper
    {
        bool IsEnabled { get; }

        // Span operations
        ISpan? StartSpan(string operation, string? description = null);
        void FinishSpan(ISpan? span, SpanStatus status = SpanStatus.Ok);
        void FinishSpan(ISpan? span, Exception ex);
        void SetSpanData(ISpan? span, string key, object? value);
        void SetSpanTag(ISpan? span, string key, string value);

        // Event operations
        void AddBreadcrumb(string? message, string? category = null);
        void CaptureException(Exception ex, string? message = null);
        void CaptureEvent(string message, string[] fingerprint, Dictionary<string, string>? tags = null, Dictionary<string, object>? extras = null, SentryLevel level = SentryLevel.Warning);

        // Context buffer operations (routed to ISearchContextBuffer)
        void LogSearch(string searchId, string query, string? artist, string? album, string strategy, int resultCount);
        void LogSearchSettings(string searchId, int trackCountFilter, bool normalizedSearch, bool appendYear, bool handleVolumeVariations, bool useFallbackSearch, bool useTrackFallback, int minimumResults, bool hasTemplates);
        void LogExpectedTracks(string searchId, List<string> trackNames, int expectedCount);
        void LogParseResult(string searchId, string folderPath, string regexMatchType, int fuzzyArtistScore, int fuzzyAlbumScore, int fuzzyArtistTokenSort, int fuzzyAlbumTokenSort, int priority, string codec, int bitrate, int bitDepth, int trackCountExpected, int trackCountActual, string username, bool hasFreeSlot, int queueLength, List<string>? directoryFiles, bool isInteractive);
        void UpdateSearchResultCount(string searchId, int actualResultCount);
        void LogGrab(string searchId, string downloadId, bool isInteractive);
        SlskdBufferedContext? GetContext(string downloadId);
        SlskdBufferedContext? GetAndRemoveContext(string downloadId);
        void AddContextBreadcrumb(string downloadId, string message);
        void RecordImport(string albumKey);
        bool WasRecentlyImported(string albumKey, out int daysSinceImport);
    }
}


================================================
FILE: Tubifarry/Core/Telemetry/NoopSentryHelper.cs
================================================
namespace Tubifarry.Core.Telemetry
{
    public class NoopSentryHelper : ISentryHelper
    {
        public bool IsEnabled => false;

        public ISpan? StartSpan(string operation, string? description = null) => null;

        public void FinishSpan(ISpan? span, SpanStatus status = SpanStatus.Ok) { }

        public void FinishSpan(ISpan? span, Exception ex) { }

        public void SetSpanData(ISpan? span, string key, object? value) { }

        public void SetSpanTag(ISpan? span, string key, string value) { }

        public void AddBreadcrumb(string? message, string? category = null) { }

        public void CaptureException(Exception ex, string? message = null) { }

        public void CaptureEvent(string message, string[] fingerprint, Dictionary<string, string>? tags = null, Dictionary<string, object>? extras = null, SentryLevel level = SentryLevel.Warning) { }

        public void LogSearch(string searchId, string query, string? artist, string? album, string strategy, int resultCount) { }

        public void LogSearchSettings(string searchId, int trackCountFilter, bool normalizedSearch, bool appendYear, bool handleVolumeVariations, bool useFallbackSearch, bool useTrackFallback, int minimumResults, bool hasTemplates) { }

        public void LogExpectedTracks(string searchId, List<string> trackNames, int expectedCount) { }

        public void LogParseResult(string searchId, string folderPath, string regexMatchType, int fuzzyArtistScore, int fuzzyAlbumScore, int fuzzyArtistTokenSort, int fuzzyAlbumTokenSort, int priority, string codec, int bitrate, int bitDepth, int trackCountExpected, int trackCountActual, string username, bool hasFreeSlot, int queueLength, List<string>? directoryFiles, bool isInteractive) { }

        public void UpdateSearchResultCount(string searchId, int actualResultCount) { }

        public void LogGrab(string searchId, string downloadId, bool isInteractive) { }

        public SlskdBufferedContext? GetContext(string downloadId) => null;

        public SlskdBufferedContext? GetAndRemoveContext(string downloadId) => null;

        public void AddContextBreadcrumb(string downloadId, string message) { }

        public void RecordImport(string albumKey) { }

        public bool WasRecentlyImported(string albumKey, out int daysSinceImport) { daysSinceImport = 0; return false; }
    }
}


================================================
FILE: Tubifarry/Core/Telemetry/SearchContextBuffer.cs
================================================
#if !MASTER_BRANCH
using NzbDrone.Common.Instrumentation;
using System.Collections.Concurrent;

namespace Tubifarry.Core.Telemetry
{
    public class SearchContextBuffer : ISearchContextBuffer
    {
        private readonly ConcurrentDictionary<string, SlskdBufferedContext> _contextBySearchId = new();
        private readonly ConcurrentDictionary<string, SlskdBufferedContext> _contextByDownloadId = new();
        private readonly ConcurrentDictionary<string, DateTime> _recentImports = new();
        private static readonly TimeSpan ContextExpiry = TimeSpan.FromHours(1);
        private static readonly TimeSpan ImportTrackingExpiry = TimeSpan.FromDays(8);
        private DateTime _lastCleanup = DateTime.UtcNow;

        public void LogSearch(string searchId, string query, string? artist, string? album, string strategy, int resultCount)
        {
            CleanupIfNeeded();

            SlskdBufferedContext context = new()
            {
                SearchId = searchId,
                SearchQuery = query,
                Artist = artist,
                Album = album,
                Strategy = strategy,
                TotalResults = resultCount
            };
            context.Breadcrumbs.Add($"Search: '{query}' via {strategy} → {resultCount} results");

            _contextBySearchId[searchId] = context;
        }

        public void LogSearchSettings(string searchId, int trackCountFilter, bool normalizedSearch, bool appendYear, bool handleVolumeVariations, bool useFallbackSearch, bool useTrackFallback, int minimumResults, bool hasTemplates)
        {
            if (_contextBySearchId.TryGetValue(searchId, out SlskdBufferedContext? context))
            {
                context.SettingsTrackCountFilter = trackCountFilter;
                context.SettingsNormalizedSearch = normalizedSearch;
                context.SettingsAppendYear = appendYear;
                context.SettingsHandleVolumeVariations = handleVolumeVariations;
                context.SettingsUseFallbackSearch = useFallbackSearch;
                context.SettingsUseTrackFallback = useTrackFallback;
                context.SettingsMinimumResults = minimumResults;
                context.SettingsHasTemplates = hasTemplates;
            }
        }

        public void LogExpectedTracks(string searchId, List<string> trackNames, int expectedCount)
        {
            if (_contextBySearchId.TryGetValue(searchId, out SlskdBufferedContext? context))
            {
                context.ExpectedTracks = trackNames;
                context.ExpectedTrackCount = expectedCount;
            }
        }

        public void LogParseResult(
            string searchId,
            string folderPath,
            string regexMatchType,
            int fuzzyArtistScore,
            int fuzzyAlbumScore,
            int fuzzyArtistTokenSort,
            int fuzzyAlbumTokenSort,
            int priority,
            string codec,
            int bitrate,
            int bitDepth,
            int trackCountExpected,
            int trackCountActual,
            string username,
            bool hasFreeSlot,
            int queueLength,
            List<string>? directoryFiles,
            bool isInteractive)
        {
            if (!_contextBySearchId.TryGetValue(searchId, out SlskdBufferedContext? context))
            {
                context = new SlskdBufferedContext { SearchId = searchId, CreatedAt = DateTime.UtcNow };
                _contextBySearchId[searchId] = context;
            }

            context.FolderPath = folderPath;
            context.RegexMatchType = regexMatchType;
            context.FuzzyArtistScore = fuzzyArtistScore;
            context.FuzzyAlbumScore = fuzzyAlbumScore;
            context.FuzzyArtistTokenSort = fuzzyArtistTokenSort;
            context.FuzzyAlbumTokenSort = fuzzyAlbumTokenSort;
            context.Priority = priority;
            context.Codec = codec;
            context.Bitrate = bitrate;
            context.BitDepth = bitDepth;
            context.TrackCountExpected = trackCountExpected;
            context.TrackCountActual = trackCountActual;
            context.Username = username;
            context.HasFreeSlot = hasFreeSlot;
            context.QueueLength = queueLength;
            context.DirectoryFiles = directoryFiles;
            context.IsInteractive = isInteractive;

            context.AllCandidates.Add(new ParseCandidate
            {
                FolderName = Path.GetFileName(folderPath.TrimEnd('\\', '/')),
                FullPath = folderPath,
                RegexMatchType = regexMatchType,
                FuzzyArtist = fuzzyArtistScore,
                FuzzyAlbum = fuzzyAlbumScore,
                Priority = priority,
                TrackCount = trackCountActual,
                Codec = codec,
                Username = username,
                WasGrabbed = false
            });
        }

        public void UpdateSearchResultCount(string searchId, int actualResultCount)
        {
            if (_contextBySearchId.TryGetValue(searchId, out SlskdBufferedContext? context))
            {
                context.TotalResults = actualResultCount;
                int idx = context.Breadcrumbs.FindIndex(b => b.StartsWith("Search:"));
                if (idx >= 0)
                    context.Breadcrumbs[idx] = $"Search: '{context.SearchQuery}' via {context.Strategy} → {actualResultCount} results";
            }
        }

        public void LogGrab(string searchId, string downloadId, bool isInteractive)
        {
            NzbDroneLogger.GetLogger(this).Debug($"[SearchContextBuffer] LogGrab: searchId={searchId} knownSearchIds=[{string.Join(",", _contextBySearchId.Keys)}]");
            if (_contextBySearchId.TryRemove(searchId, out SlskdBufferedContext? context))
            {
                context.DownloadId = downloadId;
                context.IsInteractive = isInteractive;

                // Mark the grabbed candidate
                ParseCandidate? grabbed = context.AllCandidates.FirstOrDefault(c => c.FullPath == context.FolderPath);
                if (grabbed != null)
                    grabbed.WasGrabbed = true;

                // Calculate selection analysis
                if (context.AllCandidates.Count > 0)
                {
 
Download .txt
gitextract_31q9a7rf/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   ├── documentation_improvement.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── branch-detection.yml
│       ├── compilation.yml
│       ├── dotnet-setup.yml
│       ├── git-info.yml
│       ├── main.yml
│       ├── metadata.yml
│       ├── packaging.yml
│       ├── publishing.yml
│       └── release-notes.yml
├── .gitignore
├── .gitmodules
├── CONTRIBUTION.md
├── Directory.Build.props
├── LICENSE.txt
├── NuGet.config
├── README.md
├── Tubifarry/
│   ├── Blocklisting/
│   │   ├── BaseBlocklist.cs
│   │   └── Blocklists.cs
│   ├── Core/
│   │   ├── Model/
│   │   │   ├── AlbumData.cs
│   │   │   ├── ApiCircuitBreaker.cs
│   │   │   ├── AudioMetadataHandler.cs
│   │   │   ├── FileCache.cs
│   │   │   ├── PlaylistItem.cs
│   │   │   └── TrustedSessionException.cs
│   │   ├── Records/
│   │   │   ├── Lyric.cs
│   │   │   ├── MappingAgent.cs
│   │   │   ├── MusicBrainzData.cs
│   │   │   └── YouTubeSession.cs
│   │   ├── Replacements/
│   │   │   ├── ExtendedHttpIndexerBase.cs
│   │   │   ├── FlexibleHttpDispatcher.cs
│   │   │   └── IIndexerRequestGenerator.cs
│   │   ├── Telemetry/
│   │   │   ├── ISearchContextBuffer.cs
│   │   │   ├── ISentryHelper.cs
│   │   │   ├── NoopSentryHelper.cs
│   │   │   ├── SearchContextBuffer.cs
│   │   │   ├── SentryEventFilter.cs
│   │   │   ├── SentryHelper.cs
│   │   │   ├── SlskdBufferedContext.cs
│   │   │   ├── SlskdSentryEvents.cs
│   │   │   ├── SlskdTrackingService.cs
│   │   │   ├── TubifarrySentry.cs
│   │   │   └── TubifarrySentryTarget.cs
│   │   └── Utilities/
│   │       ├── AudioFormat.cs
│   │       ├── CacheService.cs
│   │       ├── CookieManager.cs
│   │       ├── DynamicSchemaInjector.cs
│   │       ├── DynamicStateSettings.cs
│   │       ├── FileInfoParser.cs
│   │       ├── IndexerParserHelper.cs
│   │       ├── JsonConverters.cs
│   │       ├── LazyRequestChain.cs
│   │       ├── PermissionTester.cs
│   │       ├── PluginSettings.cs
│   │       ├── ReleaseFormatter.cs
│   │       ├── RepositorySettingsResolver.cs
│   │       └── UserAgentValidator.cs
│   ├── Debug.targets
│   ├── Download/
│   │   ├── Base/
│   │   │   ├── BaseDownloadManager.cs
│   │   │   ├── BaseDownloadOptions.cs
│   │   │   ├── BaseDownloadRequest.cs
│   │   │   └── BaseHttpClient.cs
│   │   └── Clients/
│   │       ├── DABMusic/
│   │       │   ├── DABMusicClient.cs
│   │       │   ├── DABMusicDownloadManager.cs
│   │       │   ├── DABMusicDownloadOptions.cs
│   │       │   ├── DABMusicDownloadRequest.cs
│   │       │   └── DABMusicProviderSettings.cs
│   │       ├── Lucida/
│   │       │   ├── ILucidaRateLimiter.cs
│   │       │   ├── LucidaClient.cs
│   │       │   ├── LucidaDownloadManager.cs
│   │       │   ├── LucidaDownloadOptions.cs
│   │       │   ├── LucidaDownloadRequest.cs
│   │       │   ├── LucidaInitiationResult.cs
│   │       │   ├── LucidaMetadataExtractor.cs
│   │       │   ├── LucidaProviderSettings.cs
│   │       │   ├── LucidaRateLimitException.cs
│   │       │   ├── LucidaRateLimiter.cs
│   │       │   ├── LucidaTokenExtractor.cs
│   │       │   └── LucidaWorkerState.cs
│   │       ├── Soulseek/
│   │       │   ├── ISlskdApiClient.cs
│   │       │   ├── ISlskdDownloadManager.cs
│   │       │   ├── Models/
│   │       │   │   ├── DownloadKey.cs
│   │       │   │   ├── SlskdDownloadDirectory.cs
│   │       │   │   ├── SlskdDownloadFile.cs
│   │       │   │   ├── SlskdDownloadItem.cs
│   │       │   │   └── SlskdFileState.cs
│   │       │   ├── SlskdApiClient.cs
│   │       │   ├── SlskdClient.cs
│   │       │   ├── SlskdDownloadManager.cs
│   │       │   ├── SlskdProviderSettings.cs
│   │       │   ├── SlskdRetryHandler.cs
│   │       │   └── SlskdStatusResolver.cs
│   │       ├── SubSonic/
│   │       │   ├── SubSonicClient.cs
│   │       │   ├── SubSonicDownloadManager.cs
│   │       │   ├── SubSonicDownloadOptions.cs
│   │       │   ├── SubSonicDownloadRequest.cs
│   │       │   └── SubSonicProviderSettings.cs
│   │       ├── TripleTriple/
│   │       │   ├── TripleTripleClient.cs
│   │       │   ├── TripleTripleDownloadManager.cs
│   │       │   ├── TripleTripleDownloadOptions.cs
│   │       │   ├── TripleTripleDownloadRequest.cs
│   │       │   └── TripleTripleProviderSettings.cs
│   │       └── YouTube/
│   │           ├── SponsorBlock.cs
│   │           ├── TrustedSessionHelper.cs
│   │           ├── YouTubeDownloadOptions.cs
│   │           ├── YouTubeDownloadRequest.cs
│   │           ├── YoutubeClient.cs
│   │           ├── YoutubeDownloadManager.cs
│   │           └── YoutubeProviderSettings.cs
│   ├── ILRepack.targets
│   ├── ImportLists/
│   │   ├── ArrStack/
│   │   │   ├── ArrMedia.cs
│   │   │   ├── ArrSoundtrackImport.cs
│   │   │   ├── ArrSoundtrackImportParser.cs
│   │   │   ├── ArrSoundtrackImportSettings.cs
│   │   │   └── ArrSoundtrackRequestGenerator.cs
│   │   ├── LastFmRecommendation/
│   │   │   ├── LastFmRecomendRequestGenerator.cs
│   │   │   ├── LastFmRecommend.cs
│   │   │   ├── LastFmRecommendParser.cs
│   │   │   ├── LastFmRecommendSettings.cs
│   │   │   └── LastFmRecords.cs
│   │   ├── ListenBrainz/
│   │   │   ├── ListenBrainzCFRecommendations/
│   │   │   │   ├── ListenBrainzCFRecommendationsImportList.cs
│   │   │   │   ├── ListenBrainzCFRecommendationsParser.cs
│   │   │   │   ├── ListenBrainzCFRecommendationsRequestGenerator.cs
│   │   │   │   └── ListenBrainzCFRecommendationsSettings.cs
│   │   │   ├── ListenBrainzCreatedForPlaylist/
│   │   │   │   ├── ListenBrainzCreatedForPlaylistImportList.cs
│   │   │   │   ├── ListenBrainzCreatedForPlaylistParser.cs
│   │   │   │   ├── ListenBrainzCreatedForPlaylistRequestGenerator.cs
│   │   │   │   └── ListenBrainzCreatedForPlaylistSettings.cs
│   │   │   ├── ListenBrainzPlaylist/
│   │   │   │   ├── ListenBrainzPlaylistImportList.cs
│   │   │   │   ├── ListenBrainzPlaylistParser.cs
│   │   │   │   ├── ListenBrainzPlaylistRequestGenerator.cs
│   │   │   │   └── ListenBrainzPlaylistSettings.cs
│   │   │   ├── ListenBrainzRecords.cs
│   │   │   └── ListenBrainzUserStats/
│   │   │       ├── ListenBrainzUserStatsImportList.cs
│   │   │       ├── ListenBrainzUserStatsParser.cs
│   │   │       ├── ListenBrainzUserStatsRequestGenerator.cs
│   │   │       └── ListenBrainzUserStatsSettings.cs
│   │   └── Spotify/
│   │       ├── SpotifyUserPlaylistImport.cs
│   │       └── SpotifyUserPlaylistImportSettings.cs
│   ├── Indexers/
│   │   ├── DABMusic/
│   │   │   ├── DABMusicIndexer.cs
│   │   │   ├── DABMusicIndexerSettings.cs
│   │   │   ├── DABMusicParser.cs
│   │   │   ├── DABMusicRecords.cs
│   │   │   ├── DABMusicRequestGenerator.cs
│   │   │   └── DABMusicSessionHelper.cs
│   │   ├── DownloadProtocols.cs
│   │   ├── Lucida/
│   │   │   ├── LucidaIndexer.cs
│   │   │   ├── LucidaIndexerSettings.cs
│   │   │   ├── LucidaRecords.cs
│   │   │   ├── LucidaRequestGenerator.cs
│   │   │   ├── LucidaRequestParser.cs
│   │   │   └── LucidaServiceHelper.cs
│   │   ├── Soulseek/
│   │   │   ├── ISlskdItemsParser.cs
│   │   │   ├── Search/
│   │   │   │   ├── Core/
│   │   │   │   │   ├── ISearchStrategy.cs
│   │   │   │   │   ├── QueryAnalyzer.cs
│   │   │   │   │   ├── SearchContext.cs
│   │   │   │   │   └── SearchPipeline.cs
│   │   │   │   ├── Strategies/
│   │   │   │   │   ├── BaseSearchStrategy.cs
│   │   │   │   │   ├── FallbackStrategy.cs
│   │   │   │   │   ├── SpecialCaseStrategy.cs
│   │   │   │   │   ├── TemplateSearchStrategy.cs
│   │   │   │   │   └── VariationStrategy.cs
│   │   │   │   ├── Templates/
│   │   │   │   │   └── TemplateEngine.cs
│   │   │   │   └── Transformers/
│   │   │   │       ├── QueryBuilder.cs
│   │   │   │       └── QueryNormalizer.cs
│   │   │   ├── SlsdkRecords.cs
│   │   │   ├── SlskdIndexer.cs
│   │   │   ├── SlskdIndexerParser.cs
│   │   │   ├── SlskdItemsParser.cs
│   │   │   ├── SlskdRequestGenerator.cs
│   │   │   ├── SlskdSettings.cs
│   │   │   └── SlskdTextProcessor.cs
│   │   ├── Spotify/
│   │   │   ├── SpotifyIndexerSettings.cs
│   │   │   ├── SpotifyParser.cs
│   │   │   ├── SpotifyRequestGenerator.cs
│   │   │   ├── SpotifyToYouTubeEnricher.cs
│   │   │   └── TubifarryIndexer.cs
│   │   ├── SubSonic/
│   │   │   ├── SubSonicAuthHelper.cs
│   │   │   ├── SubSonicIndexer.cs
│   │   │   ├── SubSonicIndexerParser.cs
│   │   │   ├── SubSonicIndexerSettings.cs
│   │   │   ├── SubSonicRecords.cs
│   │   │   └── SubSonicRequestGenerator.cs
│   │   ├── TripleTriple/
│   │   │   ├── TripleTripleIndexer.cs
│   │   │   ├── TripleTripleIndexerSettings.cs
│   │   │   ├── TripleTripleParser.cs
│   │   │   ├── TripleTripleRecords.cs
│   │   │   └── TripleTripleRequestGenerator.cs
│   │   └── YouTube/
│   │       ├── YoutubeIndexer.cs
│   │       ├── YoutubeIndexerSettings.cs
│   │       ├── YoutubeParser.cs
│   │       └── YoutubeRequestGenerator.cs
│   ├── Metadata/
│   │   ├── Converter/
│   │   │   ├── AudioConverter.cs
│   │   │   ├── AudioConverterSettings.cs
│   │   │   └── BitrateRules.cs
│   │   ├── Lyrics/
│   │   │   ├── LyricEnhancerSettings.cs
│   │   │   ├── LyricsEnhancer.cs
│   │   │   ├── LyricsHelper.cs
│   │   │   ├── LyricsProviders.cs
│   │   │   └── TrackFileRepositoryHelper.cs
│   │   ├── Proxy/
│   │   │   ├── MetadataProvider/
│   │   │   │   ├── AlbumMapper.cs
│   │   │   │   ├── CustomLidarr/
│   │   │   │   │   ├── CustomLidarrMetadataProxy.cs
│   │   │   │   │   ├── CustomLidarrMetadataProxySettings.cs
│   │   │   │   │   ├── CustomLidarrProxy.cs
│   │   │   │   │   └── ICustomLidarrProxy.cs
│   │   │   │   ├── Deezer/
│   │   │   │   │   ├── DeezerAPIService.cs
│   │   │   │   │   ├── DeezerAuthService.cs
│   │   │   │   │   ├── DeezerMappingHelper.cs
│   │   │   │   │   ├── DeezerMetadataProxy.cs
│   │   │   │   │   ├── DeezerMetadataProxySettings.cs
│   │   │   │   │   ├── DeezerProxy.cs
│   │   │   │   │   ├── DeezerRecords.cs
│   │   │   │   │   └── IDeezerProxy.cs
│   │   │   │   ├── Discogs/
│   │   │   │   │   ├── DiscogsAPIService.cs
│   │   │   │   │   ├── DiscogsMappingHelper.cs
│   │   │   │   │   ├── DiscogsMetadataProxy.cs
│   │   │   │   │   ├── DiscogsMetadataProxySettings.cs
│   │   │   │   │   ├── DiscogsProxy.cs
│   │   │   │   │   ├── DiscogsRecords.cs
│   │   │   │   │   └── IDiscogsProxy.cs
│   │   │   │   ├── Lastfm/
│   │   │   │   │   ├── ILastfmProxy.cs
│   │   │   │   │   ├── LastfmApiService.cs
│   │   │   │   │   ├── LastfmImageScraper.cs
│   │   │   │   │   ├── LastfmMappingHelper.cs
│   │   │   │   │   ├── LastfmMetadataProxy.cs
│   │   │   │   │   ├── LastfmMetadataProxySettings.cs
│   │   │   │   │   ├── LastfmProxy.cs
│   │   │   │   │   ├── LastfmRecordConverter.cs
│   │   │   │   │   └── LastfmRecords.cs
│   │   │   │   ├── MetadataProviderWrapper.cs
│   │   │   │   ├── Mixed/
│   │   │   │   │   ├── AdaptiveThresholdConfig.cs
│   │   │   │   │   ├── AdaptiveThresholdManager.cs
│   │   │   │   │   ├── ISupportMetadataMixing.cs
│   │   │   │   │   ├── MixedMetadataProxy.cs
│   │   │   │   │   ├── MixedMetadataProxySettings.cs
│   │   │   │   │   ├── ProxyDecisionHandler.cs
│   │   │   │   │   └── ProxyMetrics.cs
│   │   │   │   └── SkyHook/
│   │   │   │       ├── SkyHookMetdadataProxy.cs
│   │   │   │       └── SykHookMetadataProxySettings.cs
│   │   │   ├── MixedProxyBase.cs
│   │   │   ├── ProxyAttribute.cs
│   │   │   ├── ProxyBase.cs
│   │   │   ├── ProxyService.cs
│   │   │   ├── ProxyServiceStarter.cs
│   │   │   ├── ProxyWrapperBase.cs
│   │   │   └── RecommendArtists/
│   │   │       ├── LastFmSimilarArtistsService.cs
│   │   │       ├── SimilarArtistsProxy.cs
│   │   │       └── SimilarArtistsProxySettings.cs
│   │   └── ScheduledTasks/
│   │       ├── IProvideScheduledTask.cs
│   │       ├── ScheduledTaskBase.cs
│   │       ├── ScheduledTaskService.cs
│   │       ├── ScheduledTaskServiceStarter.cs
│   │       └── SearchSniper/
│   │           ├── SearchSniperRepositoryHelper.cs
│   │           ├── SearchSniperTask.cs
│   │           └── SearchSniperTaskSettings.cs
│   ├── Notifications/
│   │   ├── FlareSolverr/
│   │   │   ├── FlareDetector.cs
│   │   │   ├── FlareRecords.cs
│   │   │   ├── FlareSolverrHttpInterceptor.cs
│   │   │   ├── FlareSolverrNotification.cs
│   │   │   ├── FlareSolverrService.cs
│   │   │   ├── FlareSolverrSettings.cs
│   │   │   └── IFlareSolverrService.cs
│   │   ├── PlaylistExport/
│   │   │   ├── PlaylistExportNotification.cs
│   │   │   ├── PlaylistExportService.cs
│   │   │   └── PlaylistExportSettings.cs
│   │   ├── QueueCleaner/
│   │   │   ├── ImportFailureNotificationService.cs
│   │   │   ├── QueueCleaner.cs
│   │   │   └── QueueCleanerSettings.cs
│   │   └── YouTubeProxy/
│   │       ├── YouTubeProxyNotification.cs
│   │       ├── YouTubeProxyService.cs
│   │       └── YouTubeProxySettings.cs
│   ├── Plugin.cs
│   ├── PluginInfo.targets
│   ├── PluginKeys.targets
│   ├── PreBuild.targets
│   └── Tubifarry.csproj
├── Tubifarry.sln
└── stylecop.json
Download .txt
Showing preview only (264K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (2440 symbols across 253 files)

FILE: Tubifarry/Blocklisting/BaseBlocklist.cs
  class BaseBlocklist (line 9) | public abstract class BaseBlocklist<TProtocol>(IBlocklistRepository bloc...
    method IsBlocklisted (line 15) | public bool IsBlocklisted(int artistId, ReleaseInfo release) => _block...
    method GetBlocklist (line 17) | public Blocklist GetBlocklist(DownloadFailedEvent message) => new()
    method SameRelease (line 32) | private static bool SameRelease(Blocklist item, ReleaseInfo release) =...

FILE: Tubifarry/Blocklisting/Blocklists.cs
  class YoutubeBlocklist (line 6) | public class YoutubeBlocklist(IBlocklistRepository blocklistRepository) ...
  class SoulseekBlocklist (line 9) | public class SoulseekBlocklist(IBlocklistRepository blocklistRepository)...
  class QobuzBlocklist (line 12) | public class QobuzBlocklist(IBlocklistRepository blocklistRepository) : ...
  class LucidaBlocklist (line 15) | public class LucidaBlocklist(IBlocklistRepository blocklistRepository) :...
  class SubSonicBlocklist (line 18) | public class SubSonicBlocklist(IBlocklistRepository blocklistRepository)...

FILE: Tubifarry/Core/Model/AlbumData.cs
  class AlbumData (line 10) | public partial class AlbumData(string name, string downloadProtocol)
    method ToReleaseInfo (line 51) | public ReleaseInfo ToReleaseInfo() => new()
    method ParseReleaseDate (line 71) | public void ParseReleaseDate() => ReleaseDateTime = ReleaseDatePrecisi...
    method ConstructTitle (line 83) | private string ConstructTitle()
    method NormalizeAlbumName (line 118) | private static string NormalizeAlbumName(string albumName)
    method FeatRegex (line 130) | [GeneratedRegex(@"(?i)\b(feat\.|ft\.|featuring)\b", RegexOptions.Ignor...
    method FeatReplaceRegex (line 133) | [GeneratedRegex(@"\((?!feat\.)[^)]*\)")]

FILE: Tubifarry/Core/Model/ApiCircuitBreaker.cs
  type ICircuitBreaker (line 6) | public interface ICircuitBreaker
    method RecordSuccess (line 10) | void RecordSuccess();
    method RecordFailure (line 12) | void RecordFailure();
    method Reset (line 14) | void Reset();
  class ApiCircuitBreaker (line 17) | public class ApiCircuitBreaker(int failureThreshold = 5, int resetTimeou...
    method RecordSuccess (line 45) | public void RecordSuccess()
    method RecordFailure (line 50) | public void RecordFailure()
    method Reset (line 59) | public void Reset()
  class CircuitBreakerFactory (line 65) | public static class CircuitBreakerFactory
    method GetBreaker (line 77) | public static ICircuitBreaker GetBreaker<T>() => GetBreaker(typeof(T));
    method GetBreaker (line 82) | public static ICircuitBreaker GetBreaker(object obj) => GetBreaker(obj...
    method GetBreaker (line 87) | public static ICircuitBreaker GetBreaker(Type type)
    method GetBreaker (line 100) | public static ICircuitBreaker GetBreaker(string name)
    method GetCustomBreaker (line 115) | public static ICircuitBreaker GetCustomBreaker<T>(int failureThreshold...
    method CleanupIfNeeded (line 126) | private static void CleanupIfNeeded()
    method CleanupNamedBreakers (line 138) | private static void CleanupNamedBreakers()

FILE: Tubifarry/Core/Model/AudioMetadataHandler.cs
  class AudioMetadataHandler (line 11) | internal class AudioMetadataHandler
    method AudioMetadataHandler (line 21) | public AudioMetadataHandler(string originalPath)
    method TryExtractCoverArtAsync (line 182) | private async Task<byte[]?> TryExtractCoverArtAsync()
    method TryConvertToFormatAsync (line 232) | public async Task<bool> TryConvertToFormatAsync(AudioFormat audioForma...
    method IsVideoContainerAsync (line 345) | public async Task<bool> IsVideoContainerAsync()
    method TryExtractAudioFromVideoAsync (line 369) | public async Task<bool> TryExtractAudioFromVideoAsync()
    method TryDecryptAsync (line 429) | public async Task<bool> TryDecryptAsync(string decryptionKey, string? ...
    method TryCreateLrcFileAsync (line 473) | public async Task<bool> TryCreateLrcFileAsync(CancellationToken token)
    method EnsureFileExtAsync (line 499) | public async Task<bool> EnsureFileExtAsync()
    method TryEmbedMetadata (line 527) | public bool TryEmbedMetadata(Album albumInfo, Track trackInfo)
    method IsTargetFormatSupportedForEncoding (line 634) | public static bool IsTargetFormatSupportedForEncoding(AudioFormat form...
    method SupportsMetadataEmbedding (line 641) | public static bool SupportsMetadataEmbedding(AudioFormat format) => fo...
    method GetSupportedCodecAsync (line 660) | public static async Task<AudioFormat> GetSupportedCodecAsync(string fi...
    method CheckFFmpegInstalled (line 686) | public static bool CheckFFmpegInstalled()
    method IsExecutable (line 747) | private static bool IsExecutable(string filePath)
    method ResetFFmpegInstallationCheck (line 779) | public static void ResetFFmpegInstallationCheck() => _isFFmpegInstalle...
    method InstallFFmpeg (line 781) | public static Task InstallFFmpeg(string path)

FILE: Tubifarry/Core/Model/FileCache.cs
  class FileCache (line 8) | public class FileCache
    method FileCache (line 13) | public FileCache(string cacheDirectory)
    method CleanupOldCacheFiles (line 26) | private void CleanupOldCacheFiles()
    method GetAsync (line 40) | public async Task<T?> GetAsync<T>(string cacheKey)
    method SetAsync (line 66) | public async Task SetAsync<T>(string cacheKey, T data, TimeSpan expira...
    method IsCacheValid (line 88) | public bool IsCacheValid(string cacheKey, TimeSpan expirationDuration)
    method GetCacheFilePath (line 104) | private string GetCacheFilePath(string cacheKey)
    method CheckDirectory (line 118) | public void CheckDirectory()
  class CachedData (line 141) | public class CachedData<T>

FILE: Tubifarry/Core/Model/PlaylistItem.cs
  type PlaylistItem (line 3) | public record PlaylistItem(
  type PlaylistSnapshot (line 11) | public record PlaylistSnapshot(
  type IPlaylistTrackSource (line 16) | public interface IPlaylistTrackSource
    method FetchTrackLevelItems (line 18) | List<PlaylistItem> FetchTrackLevelItems();

FILE: Tubifarry/Core/Model/TrustedSessionException.cs
  class TrustedSessionException (line 6) | public class TrustedSessionException : Exception
    method TrustedSessionException (line 8) | public TrustedSessionException(string message) : base(message)
    method TrustedSessionException (line 12) | public TrustedSessionException(string message, Exception innerExceptio...
    method TrustedSessionException (line 16) | public TrustedSessionException()

FILE: Tubifarry/Core/Records/Lyric.cs
  type Lyric (line 9) | public record Lyric(string? PlainLyrics, List<SyncLine>? SyncedLyrics)
  type SyncLine (line 21) | public partial record class SyncLine

FILE: Tubifarry/Core/Records/MappingAgent.cs
  type MappingAgent (line 3) | public record MappingAgent

FILE: Tubifarry/Core/Records/MusicBrainzData.cs
  type MusicBrainzSearchItem (line 5) | public record MusicBrainzSearchItem(string? Title, string? AlbumId, stri...
  type MusicBrainzAlbumItem (line 17) | public record MusicBrainzAlbumItem(string? AlbumId, string? Title, strin...

FILE: Tubifarry/Core/Records/YouTubeSession.cs
  type SessionTokens (line 8) | public record SessionTokens(
  type ClientSessionInfo (line 33) | public record ClientSessionInfo(

FILE: Tubifarry/Core/Replacements/ExtendedHttpIndexerBase.cs
  class ExtendedHttpIndexerBase (line 21) | public abstract class ExtendedHttpIndexerBase<TSettings, TIndexerPageabl...
    method GetExtendedRequestGenerator (line 37) | public abstract IIndexerRequestGenerator<TIndexerPageableRequest> GetE...
    method GetParser (line 39) | public abstract IParseIndexerResponse GetParser();
    method ExtendedHttpIndexerBase (line 41) | protected ExtendedHttpIndexerBase(
    method FetchRecent (line 56) | public override async Task<IList<ReleaseInfo>> FetchRecent()
    method Fetch (line 64) | public override async Task<IList<ReleaseInfo>> Fetch(AlbumSearchCriter...
    method Fetch (line 72) | public override async Task<IList<ReleaseInfo>> Fetch(ArtistSearchCrite...
    method GetDownloadRequest (line 79) | public override HttpRequest GetDownloadRequest(string link) => new(link);
    method FetchReleases (line 81) | protected virtual async Task<IList<ReleaseInfo>> FetchReleases(
    method ShouldStopFetchingPages (line 168) | protected virtual bool ShouldStopFetchingPages(bool isRecent, IList<Re...
    method IsFullPage (line 198) | protected virtual bool IsFullPage(IList<ReleaseInfo> page) => PageSize...
    method IsValidRelease (line 200) | protected virtual bool IsValidRelease(ReleaseInfo release)
    method UpdateRssSyncStatus (line 217) | private void UpdateRssSyncStatus(List<ReleaseInfo> releases, ReleaseIn...
    method HandleException (line 232) | private void HandleException(Exception ex, string url, TimeSpan minimu...
    method HandleWebException (line 275) | private void HandleWebException(WebException webException, string url)
    method HandleHttpException (line 288) | private void HandleHttpException(HttpException ex, string url)
    method HandleTooManyRequestsException (line 297) | private void HandleTooManyRequestsException(TooManyRequestsException e...
    method HandleRequestLimitReachedException (line 305) | private void HandleRequestLimitReachedException(RequestLimitReachedExc...
    method FetchPage (line 313) | protected virtual async Task<IList<ReleaseInfo>> FetchPage(IndexerRequ...
    method FetchIndexerResponse (line 329) | protected virtual async Task<IndexerResponse> FetchIndexerResponse(Ind...
    method Test (line 343) | protected override async Task Test(List<ValidationFailure> failures) =...
    method TestConnection (line 345) | protected virtual async Task<ValidationFailure> TestConnection()

FILE: Tubifarry/Core/Replacements/FlexibleHttpDispatcher.cs
  class FlexibleHttpDispatcher (line 12) | public class FlexibleHttpDispatcher : ManagedHttpDispatcher, IHttpDispat...
    method FlexibleHttpDispatcher (line 16) | public FlexibleHttpDispatcher(IHttpProxySettingsProvider proxySettings...
    method GetResponseAsync (line 46) | async Task<HttpResponse> IHttpDispatcher.GetResponseAsync(HttpRequest ...
    method ExtractUserAgentFromUrl (line 52) | private static void ExtractUserAgentFromUrl(HttpRequest request)
    method AddRequestHeaders (line 67) | protected override void AddRequestHeaders(HttpRequestMessage webReques...

FILE: Tubifarry/Core/Replacements/IIndexerRequestGenerator.cs
  type IIndexerRequestGenerator (line 10) | public interface IIndexerRequestGenerator<TIndexerPageableRequest>
    method GetRecentRequests (line 16) | IndexerPageableRequestChain<TIndexerPageableRequest> GetRecentRequests();
    method GetSearchRequests (line 21) | IndexerPageableRequestChain<TIndexerPageableRequest> GetSearchRequests...
    method GetSearchRequests (line 26) | IndexerPageableRequestChain<TIndexerPageableRequest> GetSearchRequests...

FILE: Tubifarry/Core/Telemetry/ISearchContextBuffer.cs
  type ISearchContextBuffer (line 3) | public interface ISearchContextBuffer
    method LogSearch (line 5) | void LogSearch(string searchId, string query, string? artist, string? ...
    method LogSearchSettings (line 7) | void LogSearchSettings(string searchId, int trackCountFilter, bool nor...
    method LogExpectedTracks (line 9) | void LogExpectedTracks(string searchId, List<string> trackNames, int e...
    method LogParseResult (line 11) | void LogParseResult(string searchId, string folderPath, string regexMa...
    method UpdateSearchResultCount (line 13) | void UpdateSearchResultCount(string searchId, int actualResultCount);
    method LogGrab (line 15) | void LogGrab(string searchId, string downloadId, bool isInteractive);
    method GetContext (line 17) | SlskdBufferedContext? GetContext(string downloadId);
    method GetAndRemoveContext (line 19) | SlskdBufferedContext? GetAndRemoveContext(string downloadId);
    method AddBreadcrumb (line 21) | void AddBreadcrumb(string downloadId, string message);
    method RecordImport (line 23) | void RecordImport(string albumKey);
    method WasRecentlyImported (line 25) | bool WasRecentlyImported(string albumKey, out int daysSinceImport);

FILE: Tubifarry/Core/Telemetry/ISentryHelper.cs
  type ISentryHelper (line 3) | public interface ISentryHelper
    method StartSpan (line 8) | ISpan? StartSpan(string operation, string? description = null);
    method FinishSpan (line 9) | void FinishSpan(ISpan? span, SpanStatus status = SpanStatus.Ok);
    method FinishSpan (line 10) | void FinishSpan(ISpan? span, Exception ex);
    method SetSpanData (line 11) | void SetSpanData(ISpan? span, string key, object? value);
    method SetSpanTag (line 12) | void SetSpanTag(ISpan? span, string key, string value);
    method AddBreadcrumb (line 15) | void AddBreadcrumb(string? message, string? category = null);
    method CaptureException (line 16) | void CaptureException(Exception ex, string? message = null);
    method CaptureEvent (line 17) | void CaptureEvent(string message, string[] fingerprint, Dictionary<str...
    method LogSearch (line 20) | void LogSearch(string searchId, string query, string? artist, string? ...
    method LogSearchSettings (line 21) | void LogSearchSettings(string searchId, int trackCountFilter, bool nor...
    method LogExpectedTracks (line 22) | void LogExpectedTracks(string searchId, List<string> trackNames, int e...
    method LogParseResult (line 23) | void LogParseResult(string searchId, string folderPath, string regexMa...
    method UpdateSearchResultCount (line 24) | void UpdateSearchResultCount(string searchId, int actualResultCount);
    method LogGrab (line 25) | void LogGrab(string searchId, string downloadId, bool isInteractive);
    method GetContext (line 26) | SlskdBufferedContext? GetContext(string downloadId);
    method GetAndRemoveContext (line 27) | SlskdBufferedContext? GetAndRemoveContext(string downloadId);
    method AddContextBreadcrumb (line 28) | void AddContextBreadcrumb(string downloadId, string message);
    method RecordImport (line 29) | void RecordImport(string albumKey);
    method WasRecentlyImported (line 30) | bool WasRecentlyImported(string albumKey, out int daysSinceImport);

FILE: Tubifarry/Core/Telemetry/NoopSentryHelper.cs
  class NoopSentryHelper (line 3) | public class NoopSentryHelper : ISentryHelper
    method StartSpan (line 7) | public ISpan? StartSpan(string operation, string? description = null) ...
    method FinishSpan (line 9) | public void FinishSpan(ISpan? span, SpanStatus status = SpanStatus.Ok)...
    method FinishSpan (line 11) | public void FinishSpan(ISpan? span, Exception ex) { }
    method SetSpanData (line 13) | public void SetSpanData(ISpan? span, string key, object? value) { }
    method SetSpanTag (line 15) | public void SetSpanTag(ISpan? span, string key, string value) { }
    method AddBreadcrumb (line 17) | public void AddBreadcrumb(string? message, string? category = null) { }
    method CaptureException (line 19) | public void CaptureException(Exception ex, string? message = null) { }
    method CaptureEvent (line 21) | public void CaptureEvent(string message, string[] fingerprint, Diction...
    method LogSearch (line 23) | public void LogSearch(string searchId, string query, string? artist, s...
    method LogSearchSettings (line 25) | public void LogSearchSettings(string searchId, int trackCountFilter, b...
    method LogExpectedTracks (line 27) | public void LogExpectedTracks(string searchId, List<string> trackNames...
    method LogParseResult (line 29) | public void LogParseResult(string searchId, string folderPath, string ...
    method UpdateSearchResultCount (line 31) | public void UpdateSearchResultCount(string searchId, int actualResultC...
    method LogGrab (line 33) | public void LogGrab(string searchId, string downloadId, bool isInterac...
    method GetContext (line 35) | public SlskdBufferedContext? GetContext(string downloadId) => null;
    method GetAndRemoveContext (line 37) | public SlskdBufferedContext? GetAndRemoveContext(string downloadId) =>...
    method AddContextBreadcrumb (line 39) | public void AddContextBreadcrumb(string downloadId, string message) { }
    method RecordImport (line 41) | public void RecordImport(string albumKey) { }
    method WasRecentlyImported (line 43) | public bool WasRecentlyImported(string albumKey, out int daysSinceImpo...

FILE: Tubifarry/Core/Telemetry/SearchContextBuffer.cs
  class SearchContextBuffer (line 7) | public class SearchContextBuffer : ISearchContextBuffer
    method LogSearch (line 16) | public void LogSearch(string searchId, string query, string? artist, s...
    method LogSearchSettings (line 34) | public void LogSearchSettings(string searchId, int trackCountFilter, b...
    method LogExpectedTracks (line 49) | public void LogExpectedTracks(string searchId, List<string> trackNames...
    method LogParseResult (line 58) | public void LogParseResult(
    method UpdateSearchResultCount (line 117) | public void UpdateSearchResultCount(string searchId, int actualResultC...
    method LogGrab (line 128) | public void LogGrab(string searchId, string downloadId, bool isInterac...
    method GetContext (line 163) | public SlskdBufferedContext? GetContext(string downloadId)
    method GetAndRemoveContext (line 169) | public SlskdBufferedContext? GetAndRemoveContext(string downloadId)
    method AddBreadcrumb (line 176) | public void AddBreadcrumb(string downloadId, string message)
    method RecordImport (line 182) | public void RecordImport(string albumKey)
    method WasRecentlyImported (line 188) | public bool WasRecentlyImported(string albumKey, out int daysSinceImport)
    method CleanupIfNeeded (line 203) | private void CleanupIfNeeded()

FILE: Tubifarry/Core/Telemetry/SentryEventFilter.cs
  class SentryEventFilter (line 12) | public static class SentryEventFilter
    method FilterEvent (line 43) | public static SentryEvent? FilterEvent(SentryEvent? sentryEvent, Sentr...
    method EnrichFingerprint (line 79) | private static void EnrichFingerprint(SentryEvent sentryEvent, Excepti...
    method ClassifyException (line 103) | private static (string? Operation, string Component) ClassifyException...

FILE: Tubifarry/Core/Telemetry/SentryHelper.cs
  class SentryHelper (line 4) | public class SentryHelper : ISentryHelper
    method SentryHelper (line 8) | public SentryHelper(ISearchContextBuffer contextBuffer) => _contextBuf...
    method StartSpan (line 12) | public ISpan? StartSpan(string operation, string? description = null)
    method FinishSpan (line 24) | public void FinishSpan(ISpan? span, SpanStatus status = SpanStatus.Ok)
    method FinishSpan (line 27) | public void FinishSpan(ISpan? span, Exception ex)
    method SetSpanData (line 44) | public void SetSpanData(ISpan? span, string key, object? value)
    method SetSpanTag (line 50) | public void SetSpanTag(ISpan? span, string key, string value)
    method AddBreadcrumb (line 53) | public void AddBreadcrumb(string? message, string? category = null)
    method CaptureException (line 59) | public void CaptureException(Exception ex, string? message = null)
    method CaptureEvent (line 67) | public void CaptureEvent(string message, string[] fingerprint, Diction...
    method LogSearch (line 86) | public void LogSearch(string searchId, string query, string? artist, s...
    method LogSearchSettings (line 89) | public void LogSearchSettings(string searchId, int trackCountFilter, b...
    method LogExpectedTracks (line 92) | public void LogExpectedTracks(string searchId, List<string> trackNames...
    method LogParseResult (line 95) | public void LogParseResult(string searchId, string folderPath, string ...
    method UpdateSearchResultCount (line 98) | public void UpdateSearchResultCount(string searchId, int actualResultC...
    method LogGrab (line 101) | public void LogGrab(string searchId, string downloadId, bool isInterac...
    method GetContext (line 104) | public SlskdBufferedContext? GetContext(string downloadId)
    method GetAndRemoveContext (line 107) | public SlskdBufferedContext? GetAndRemoveContext(string downloadId)
    method AddContextBreadcrumb (line 110) | public void AddContextBreadcrumb(string downloadId, string message)
    method RecordImport (line 113) | public void RecordImport(string albumKey)
    method WasRecentlyImported (line 116) | public bool WasRecentlyImported(string albumKey, out int daysSinceImport)

FILE: Tubifarry/Core/Telemetry/SlskdBufferedContext.cs
  class SlskdBufferedContext (line 3) | public class SlskdBufferedContext
  class ParseCandidate (line 59) | public class ParseCandidate

FILE: Tubifarry/Core/Telemetry/SlskdSentryEvents.cs
  class SlskdSentryEvents (line 4) | public static class SlskdSentryEvents
    type ImportFailureReason (line 6) | public enum ImportFailureReason
    type DownloadFailureReason (line 15) | public enum DownloadFailureReason
    method EmitImportFailed (line 24) | public static void EmitImportFailed(
    method EmitImportSuccess (line 130) | public static void EmitImportSuccess(
    method EmitUserReplaced (line 182) | public static void EmitUserReplaced(
    method EmitDownloadFailed (line 217) | public static void EmitDownloadFailed(
    method CategorizeDownloadError (line 264) | public static DownloadFailureReason CategorizeDownloadError(string? er...

FILE: Tubifarry/Core/Telemetry/SlskdTrackingService.cs
  class SlskdTrackingService (line 11) | public class SlskdTrackingService(ISentryHelper sentry, Logger logger) :
    method Handle (line 20) | public void Handle(AlbumGrabbedEvent message)
    method Handle (line 43) | public void Handle(AlbumImportIncompleteEvent message)
    method Handle (line 70) | public void Handle(TrackImportedEvent message)
    method Handle (line 119) | public void Handle(DownloadFailedEvent message)
    method IsSlskdDownload (line 136) | private static bool IsSlskdDownload(TrackedDownload trackedDownload)
    method DetermineFailureReason (line 146) | private static SlskdSentryEvents.ImportFailureReason DetermineFailureR...
    method ExtractStatusMessages (line 169) | private static List<string> ExtractStatusMessages(TrackedDownload trac...
    method ExtractSearchIdFromInfoUrl (line 173) | private static string? ExtractSearchIdFromInfoUrl(string infoUrl)
    method GetAlbumKey (line 181) | private static string GetAlbumKey(TrackImportedEvent message) => $"{me...
    method DetermineReplacementSource (line 183) | private static string DetermineReplacementSource(TrackImportedEvent me...

FILE: Tubifarry/Core/Telemetry/TubifarrySentry.cs
  class TubifarrySentry (line 9) | public static class TubifarrySentry
    method Initialize (line 18) | public static void Initialize()
    method GetEnvironment (line 67) | private static string GetEnvironment()
    method ConfigureDefaultScope (line 78) | private static void ConfigureDefaultScope()
    method Shutdown (line 102) | public static void Shutdown()
    method ConfigureScope (line 118) | public static void ConfigureScope(Action<Scope> configure)

FILE: Tubifarry/Core/Telemetry/TubifarrySentryTarget.cs
  class TubifarrySentryTarget (line 7) | [Target("TubifarrySentry")]
    method Write (line 34) | protected override void Write(LogEventInfo logEvent)
    method CaptureEvent (line 70) | private void CaptureEvent(LogEventInfo logEvent)

FILE: Tubifarry/Core/Utilities/AudioFormat.cs
  type AudioFormat (line 5) | public enum AudioFormat
  class AudioFormatHelper (line 26) | internal static class AudioFormatHelper
    method GetFileExtensionForCodec (line 93) | public static string GetFileExtensionForCodec(string codec) => codec s...
    method GetAudioFormatFromCodec (line 111) | public static AudioFormat GetAudioFormatFromCodec(string codec) => cod...
    method GetFileExtensionForFormat (line 134) | public static string GetFileExtensionForFormat(AudioFormat format) => ...
    method ConvertOptionToAudioFormat (line 158) | public static AudioFormat ConvertOptionToAudioFormat(ReEncodeOptions r...
    method IsLossyFormat (line 170) | public static bool IsLossyFormat(AudioFormat format) => _lossyFormats....
    method GetAudioCodecFromExtension (line 175) | public static AudioFormat GetAudioCodecFromExtension(string extension)...
    method GetDefaultBitrate (line 198) | public static int GetDefaultBitrate(AudioFormat format) =>
    method ClampBitrate (line 204) | public static int ClampBitrate(AudioFormat format, int requestedBitrate)
    method MapBitrateToVorbisQuality (line 210) | public static int MapBitrateToVorbisQuality(int targetBitrate) => Vorb...
    method RoundToStandardBitrate (line 217) | public static int RoundToStandardBitrate(int bitrateKbps) => _standard...

FILE: Tubifarry/Core/Utilities/CacheService.cs
  class CacheService (line 9) | public class CacheService
    method CacheService (line 33) | public CacheService() => _logger = NzbDroneLogger.GetLogger(this);
    method GetAsync (line 49) | public async Task<TData?> GetAsync<TData>(string key)
    method SetAsync (line 59) | public async Task SetAsync<TData>(string key, TData value)
    method FetchAndCacheAsync (line 76) | public async Task<TData> FetchAndCacheAsync<TData>(string key, Func<Ta...
    method UpdateAsync (line 91) | public async Task<TData> UpdateAsync<TData>(string key, Func<TData?, T...
    method IsExpired (line 99) | private bool IsExpired(CachedData<object> entry) =>
  type CacheType (line 106) | public enum CacheType

FILE: Tubifarry/Core/Utilities/CookieManager.cs
  class CookieManager (line 5) | internal static class CookieManager
    method ParseCookieFile (line 7) | internal static Cookie[] ParseCookieFile(string filePath)

FILE: Tubifarry/Core/Utilities/DynamicSchemaInjector.cs
  class DynamicSchemaInjector (line 7) | public static class DynamicSchemaInjector
    method InjectDynamic (line 12) | public static void InjectDynamic<TSettings>(IEnumerable<FieldMapping> ...

FILE: Tubifarry/Core/Utilities/DynamicStateSettings.cs
  class DynamicStateSettings (line 9) | public abstract class DynamicStateSettings : IProviderConfig
    method GetAllBoolStates (line 14) | protected Dictionary<string, bool> GetAllBoolStates() =>
    method GetBoolState (line 18) | public bool GetBoolState(string key) =>
    method SetBoolState (line 21) | public void SetBoolState(string key, bool value)
    method Validate (line 28) | public abstract NzbDroneValidationResult Validate();
    method BuildMappings (line 30) | public static FieldMapping[] BuildMappings<TSettings>(IEnumerable<Dyna...
  type DynamicFieldDefinition (line 93) | public record DynamicFieldDefinition(

FILE: Tubifarry/Core/Utilities/FileInfoParser.cs
  class FileInfoParser (line 8) | public partial class FileInfoParser
    method FileInfoParser (line 16) | public FileInfoParser(string filePath)
    method ParseFilename (line 24) | private void ParseFilename(string filename)
    method ExtractMatchResults (line 49) | private void ExtractMatchResults(Match match)
    method TrackArtistTitleTagPattern1 (line 99) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<artist>[a-z0-9,\(\)\...
    method TrackArtistTagTitlePattern1 (line 102) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<artist>[a-z0-9,\(\)\...
    method TrackArtistTitlePattern1 (line 105) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<artist>[a-z0-9,\(\)\...
    method ArtistTagTrackTitlePattern1 (line 108) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tag...
    method ArtistTrackTitleTagPattern1 (line 111) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tra...
    method ArtistTrackTitlePattern1 (line 114) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tra...
    method ArtistTitleTagPattern1 (line 117) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tit...
    method ArtistTagTitlePattern1 (line 120) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tag...
    method ArtistTitlePattern1 (line 123) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.&'']+)(?<sep>[\s_-]+)(?<tit...
    method TrackTitlePattern1 (line 126) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<title>[a-z0-9,\(\)\....
    method TrackTagTitlePattern1 (line 129) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<tag>[a-z0-9,\(\)\.&'...
    method TrackTitleTagPattern1 (line 132) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s_-]+)(?<title>[a-z0-9,\(\)\....
    method TrackArtistTitleTagPattern2 (line 136) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<artist>[a-z0-9,\(\)\....
    method TrackArtistTagTitlePattern2 (line 139) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<artist>[a-z0-9,\(\)\....
    method TrackArtistTitlePattern2 (line 142) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<artist>[a-z0-9,\(\)\....
    method ArtistTagTrackTitlePattern2 (line 145) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<ta...
    method ArtistTrackTitleTagPattern2 (line 148) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<tr...
    method ArtistTrackTitlePattern2 (line 151) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<tr...
    method ArtistTitleTagPattern2 (line 154) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<ti...
    method ArtistTagTitlePattern2 (line 157) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<ta...
    method ArtistTitlePattern2 (line 160) | [GeneratedRegex(@"^(?<artist>[a-z0-9,\(\)\.\&''_]+)(?<sep>[\s-]+)(?<ti...
    method TrackTitlePattern2 (line 163) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<title>[a-z0-9,\(\)\.\...
    method TrackTagTitlePattern2 (line 166) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<tag>[a-z0-9,\(\)\.\&'...
    method TrackTitleTagPattern2 (line 169) | [GeneratedRegex(@"^(?<track>\d+)(?<sep>[\s-]+)(?<title>[a-z0-9,\(\)\.\...
    method TitleOnlyPattern (line 173) | [GeneratedRegex(@"^(?<title>.+)$", RegexOptions.IgnoreCase)]

FILE: Tubifarry/Core/Utilities/IndexerParserHelper.cs
  class IndexerParserHelper (line 10) | public static class IndexerParserHelper
    method ProcessItems (line 31) | public static void ProcessItems<T>(
    method DetermineFormat (line 54) | public static AudioFormat DetermineFormat(
    method EstimateSize (line 89) | public static long EstimateSize(
    method GetQualityInfo (line 114) | public static (AudioFormat Format, int Bitrate, int BitDepth) GetQuali...

FILE: Tubifarry/Core/Utilities/JsonConverters.cs
  class StringConverter (line 9) | public class StringConverter : JsonConverter<string>
    method Read (line 11) | public override string Read(ref Utf8JsonReader reader, Type typeToConv...
    method Write (line 24) | public override void Write(Utf8JsonWriter writer, string value, JsonSe...
  class FloatConverter (line 30) | public class FloatConverter : JsonConverter<float>
    method Read (line 32) | public override float Read(ref Utf8JsonReader reader, Type typeToConve...
    method Write (line 43) | public override void Write(Utf8JsonWriter writer, float value, JsonSer...
  class BooleanConverter (line 49) | public class BooleanConverter : JsonConverter<bool>
    method Read (line 51) | public override bool Read(ref Utf8JsonReader reader, Type typeToConver...
    method Write (line 63) | public override void Write(Utf8JsonWriter writer, bool value, JsonSeri...
  class UnixTimestampConverter (line 69) | internal class UnixTimestampConverter : JsonConverter<DateTime?>
    method Read (line 73) | public override DateTime? Read(ref Utf8JsonReader reader, Type typeToC...
    method Write (line 91) | public override void Write(Utf8JsonWriter writer, DateTime? value, Jso...

FILE: Tubifarry/Core/Utilities/LazyRequestChain.cs
  class LazyIndexerPageableRequest (line 11) | public class LazyIndexerPageableRequest(Func<IEnumerable<IndexerRequest>...
    method AreResultsUsable (line 15) | public bool AreResultsUsable(int resultsCount) => MinimumResultsThresh...
    class LazyEnumerable (line 20) | private class LazyEnumerable(Func<IEnumerable<IndexerRequest>> factory...
      method GetEnumerator (line 24) | public IEnumerator<IndexerRequest> GetEnumerator() => _factory().Get...
      method GetEnumerator (line 26) | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
  class IndexerPageableRequestChain (line 33) | public class IndexerPageableRequestChain<TRequest> where TRequest : Inde...
    method IndexerPageableRequestChain (line 38) | public IndexerPageableRequestChain() => _chains = [[]];
    method GetAllTiers (line 42) | public virtual IEnumerable<TRequest> GetAllTiers() => _chains.SelectMa...
    method GetTier (line 44) | public virtual IEnumerable<TRequest> GetTier(int index) => index < _ch...
    method Add (line 46) | public virtual void Add(IEnumerable<IndexerRequest> request)
    method AddTier (line 57) | public virtual void AddTier(IEnumerable<IndexerRequest> request)
    method AddTier (line 63) | public virtual void AddTier()
    method AreTierResultsUsable (line 74) | public virtual bool AreTierResultsUsable(int tierIndex, int resultsCou...
    method ToStandardChain (line 79) | public virtual IndexerPageableRequestChain ToStandardChain()
  class LazyIndexerPageableRequestChain (line 106) | public class LazyIndexerPageableRequestChain(int defaultThreshold = 0) :...
    method AddFactory (line 114) | public void AddFactory(Func<IEnumerable<IndexerRequest>> requestFactor...
    method AddTierFactory (line 125) | public void AddTierFactory(Func<IEnumerable<IndexerRequest>> requestFa...
    method AreTierResultsUsable (line 131) | public override bool AreTierResultsUsable(int tierIndex, int resultsCo...
    method Add (line 145) | public override void Add(IEnumerable<IndexerRequest> request)
    method AddTier (line 151) | public override void AddTier(IEnumerable<IndexerRequest> request)
  class SearchTierGenerator (line 161) | public static class SearchTierGenerator
    method CreateConditionalTier (line 166) | public static Func<IEnumerable<IndexerRequest>> CreateConditionalTier(...
    method CreateTier (line 171) | public static Func<IEnumerable<IndexerRequest>> CreateTier(Func<IEnume...

FILE: Tubifarry/Core/Utilities/PermissionTester.cs
  class PermissionTester (line 7) | public static class PermissionTester
    method TestReadWritePermissions (line 9) | public static ValidationFailure? TestReadWritePermissions(string direc...
    method TestExistance (line 47) | public static ValidationFailure? TestExistance(string directoryPath, I...
    method TestExecutePermissions (line 63) | public static ValidationFailure? TestExecutePermissions(string directo...
    method TestAllPermissions (line 78) | public static List<ValidationFailure> TestAllPermissions(string direct...

FILE: Tubifarry/Core/Utilities/PluginSettings.cs
  type IPluginSettings (line 10) | public interface IPluginSettings
    method GetValue (line 12) | T GetValue<T>(string key, T? defaultValue = default);
    method SetValue (line 14) | void SetValue<T>(string key, T value);
    method HasKey (line 16) | bool HasKey(string key);
    method RemoveKey (line 18) | void RemoveKey(string key);
    method Save (line 20) | void Save();
    method Load (line 22) | void Load();
  class SettingChangedEventArgs (line 27) | public class SettingChangedEventArgs(string key, object? oldValue, objec...
  class PluginSettings (line 34) | public class PluginSettings : IPluginSettings
    method PluginSettings (line 45) | public PluginSettings(IAppFolderInfo appFolderInfo, Logger logger, boo...
    method GetValue (line 62) | public T GetValue<T>(string key, T? defaultValue = default)
    method TryDeserialize (line 70) | private T? TryDeserialize<T>(string json, T? defaultValue)
    method SetValue (line 82) | public void SetValue<T>(string key, T value)
    method HasKey (line 100) | public bool HasKey(string key)
    method RemoveKey (line 108) | public void RemoveKey(string key)
    method Save (line 125) | public void Save()
    method SaveInternal (line 133) | private void SaveInternal()
    method Load (line 153) | public void Load()
    method GetInt (line 185) | public int GetInt(string key, int defaultValue = 0) => GetValue(key, d...
    method GetBool (line 187) | public bool GetBool(string key, bool defaultValue = false) => GetValue...
    method GetString (line 189) | public string GetString(string key, string defaultValue = "") => GetVa...
    method GetDouble (line 191) | public double GetDouble(string key, double defaultValue = 0.0) => GetV...
    method SetValues (line 193) | public void SetValues<T>(Dictionary<string, T> values)
    method Clear (line 212) | public void Clear()
    method OnSettingChanged (line 223) | protected virtual void OnSettingChanged(string key, object? oldValue, ...
    method ObfuscateString (line 226) | private static string ObfuscateString(string input) =>
    method DeobfuscateString (line 233) | private static string DeobfuscateString(string input) =>

FILE: Tubifarry/Core/Utilities/ReleaseFormatter.cs
  class ReleaseFormatter (line 8) | public partial class ReleaseFormatter(ReleaseInfo releaseInfo, Artist ar...
    method BuildTrackFilename (line 14) | public string BuildTrackFilename(string? pattern, Track track, Album a...
    method BuildAlbumFilename (line 22) | public string BuildAlbumFilename(string? pattern, Album album)
    method BuildArtistFolderName (line 30) | public string BuildArtistFolderName(string? pattern)
    method GetTokenHandlers (line 38) | private Dictionary<string, Func<string>> GetTokenHandlers(Track? track...
    method ReplaceTokens (line 79) | private static string ReplaceTokens(string pattern, Dictionary<string,...
    method CleanFileName (line 85) | private string CleanFileName(string fileName)
    method CleanTitle (line 118) | private static string CleanTitle(string? title)
    method TitleThe (line 124) | private static string TitleThe(string? title)
    method CleanTitleThe (line 130) | private static string CleanTitleThe(string? title) => CleanTitle(Title...
    method TitleFirstCharacter (line 132) | private static string TitleFirstCharacter(string? title)
    method FormatTrackNumber (line 138) | private static string FormatTrackNumber(string? trackNumber, string? f...
    method ReplaceTokensRegex (line 146) | [GeneratedRegex(@"\{([^}]+)\}")]
    method TitleTheRegex (line 149) | [GeneratedRegex(@"^(The|A|An)\s+(.+)$", RegexOptions.IgnoreCase, "de-D...
    method ColonReplaceRegex (line 152) | [GeneratedRegex(@":\s*")]

FILE: Tubifarry/Core/Utilities/RepositorySettingsResolver.cs
  type IRepositorySettingsResolver (line 7) | public interface IRepositorySettingsResolver
    method Resolve (line 9) | TSettings Resolve<TRepository, TSettings>(string implementationName)
  class RepositorySettingsResolver (line 14) | public class RepositorySettingsResolver(Lazy<IServiceProvider> servicePr...
    method Resolve (line 19) | public TSettings Resolve<TRepository, TSettings>(string implementation...

FILE: Tubifarry/Core/Utilities/UserAgentValidator.cs
  type IUserAgentValidator (line 8) | public interface IUserAgentValidator
    method IsAllowed (line 15) | bool IsAllowed(string userAgent);
    method Parse (line 22) | IEnumerable<UserAgentProduct> Parse(string userAgent);
    method AddAllowedPattern (line 28) | void AddAllowedPattern(string pattern);
    method AddBlacklistPattern (line 34) | void AddBlacklistPattern(string pattern);
  type UserAgentProduct (line 40) | public record UserAgentProduct(string Name, string Version)
  class UserAgentValidator (line 48) | public partial class UserAgentValidator : IUserAgentValidator
    method UserAgentValidator (line 62) | public UserAgentValidator(IEnumerable<string>? allowed = null, IEnumer...
    method IsAllowed (line 70) | public bool IsAllowed(string userAgent)
    method Parse (line 81) | public IEnumerable<UserAgentProduct> Parse(string userAgent) => userAg...
    method AddAllowedPattern (line 87) | public void AddAllowedPattern(string pattern) => AddPattern(pattern, _...
    method AddBlacklistPattern (line 90) | public void AddBlacklistPattern(string pattern) => AddPattern(pattern,...
    method IsValidFormat (line 92) | private bool IsValidFormat(string ua)
    method AddPattern (line 101) | private static void AddPattern(string p, HashSet<string> exact, List<R...
    method TokenPattern (line 112) | [GeneratedRegex(@"^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$")]

FILE: Tubifarry/Download/Base/BaseDownloadManager.cs
  type IBaseDownloadManager (line 13) | public interface IBaseDownloadManager<TDownloadRequest, TOptions, TClient>
    method Download (line 17) | Task<string> Download(RemoteAlbum remoteAlbum, IIndexer indexer, Namin...
    method GetItems (line 19) | IEnumerable<DownloadClientItem> GetItems();
    method RemoveItem (line 21) | void RemoveItem(DownloadClientItem item);
  class BaseDownloadManager (line 27) | public abstract class BaseDownloadManager<TDownloadRequest, TOptions, TC...
    method CreateDownloadRequest (line 38) | protected abstract Task<TDownloadRequest> CreateDownloadRequest(Remote...
    method Download (line 43) | public virtual async Task<string> Download(RemoteAlbum remoteAlbum, II...
    method GetItems (line 63) | public virtual IEnumerable<DownloadClientItem> GetItems() => _queue.Se...
    method RemoveItem (line 68) | public virtual void RemoveItem(DownloadClientItem item)

FILE: Tubifarry/Download/Base/BaseDownloadOptions.cs
  type BaseDownloadOptions (line 11) | public record BaseDownloadOptions : RequestOptions<string, string>

FILE: Tubifarry/Download/Base/BaseDownloadRequest.cs
  class BaseDownloadRequest (line 19) | public abstract partial class BaseDownloadRequest<TOptions> : Request<TO...
    method BaseDownloadRequest (line 62) | protected BaseDownloadRequest(RemoteAlbum remoteAlbum, TOptions? optio...
    method ProcessDownloadAsync (line 84) | protected abstract Task ProcessDownloadAsync(CancellationToken token);
    method GetAlbumTitle (line 89) | protected virtual string GetAlbumTitle() => ReleaseInfo.Album ?? Relea...
    method SanitizeFileName (line 94) | protected static string SanitizeFileName(string fileName) => string.Is...
    method LogAndAppendMessage (line 99) | protected void LogAndAppendMessage(string message, LogLevel logLevel)
    method CreateClientItem (line 108) | protected virtual DownloadClientItem CreateClientItem() => new()
    method GetRemainingTime (line 120) | protected virtual TimeSpan? GetRemainingTime()
    method GetDistinctMessages (line 144) | protected virtual string GetDistinctMessages() => string.Join(Environm...
    method GetRemainingSize (line 149) | protected virtual long GetRemainingSize()
    method GetDownloadItemStatus (line 168) | public virtual DownloadItemStatus GetDownloadItemStatus() => State switch
    method BuildTrackFilename (line 183) | protected string BuildTrackFilename(Track track, Album album, string e...
    method Start (line 185) | public override void Start() => throw new NotImplementedException();
    method Pause (line 187) | public override void Pause() => throw new NotImplementedException();
    method RunRequestAsync (line 189) | protected override Task<RequestReturn> RunRequestAsync() => throw new ...
    method FileNameSanitizerRegex (line 191) | [GeneratedRegex(@"[\\/:\*\?""<>\|]", RegexOptions.Compiled)]

FILE: Tubifarry/Download/Base/BaseHttpClient.cs
  class BaseHttpClient (line 11) | public class BaseHttpClient
    method BaseHttpClient (line 23) | public BaseHttpClient(string baseUrl, IEnumerable<IHttpRequestIntercep...
    method CreateRequest (line 41) | public HttpRequestMessage CreateRequest(HttpMethod method, string url)
    method SendAsync (line 60) | public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage re...
    method ConvertToNzbRequest (line 93) | private static HttpRequest ConvertToNzbRequest(HttpRequestMessage http...
    method ApplyNzbRequestToHttpRequestMessage (line 144) | private static void ApplyNzbRequestToHttpRequestMessage(HttpRequest nz...
    method ConvertToNzbResponse (line 187) | private static async Task<HttpResponse> ConvertToNzbResponse(HttpReque...
    method ConvertToHttpResponseMessage (line 213) | private static HttpResponseMessage ConvertToHttpResponseMessage(HttpRe...
    method GetStringAsync (line 239) | public async Task<string> GetStringAsync(string url, CancellationToken...
    method GetAsync (line 258) | public async Task<HttpResponseMessage> GetAsync(string url, Cancellati...
    method PostAsync (line 278) | public async Task<HttpResponseMessage> PostAsync(HttpRequestMessage re...
    method PostAsync (line 304) | public async Task<HttpResponseMessage> PostAsync(string url, HttpConte...

FILE: Tubifarry/Download/Clients/DABMusic/DABMusicClient.cs
  class DABMusicClient (line 22) | public class DABMusicClient : DownloadClientBase<DABMusicProviderSettings>
    method DABMusicClient (line 29) | public DABMusicClient(
    method Download (line 51) | public override Task<string> Download(RemoteAlbum remoteAlbum, IIndexe...
    method GetItems (line 53) | public override IEnumerable<DownloadClientItem> GetItems() => _downloa...
    method RemoveItem (line 55) | public override void RemoveItem(DownloadClientItem item, bool deleteData)
    method GetStatus (line 62) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 68) | protected override void Test(List<ValidationFailure> failures)

FILE: Tubifarry/Download/Clients/DABMusic/DABMusicDownloadManager.cs
  type IDABMusicDownloadManager (line 12) | public interface IDABMusicDownloadManager : IBaseDownloadManager<DABMusi...
  class DABMusicDownloadManager (line 15) | public class DABMusicDownloadManager : BaseDownloadManager<DABMusicDownl...
    method DABMusicDownloadManager (line 20) | public DABMusicDownloadManager(IDABMusicSessionManager sessionManager,...
    method CreateDownloadRequest (line 26) | protected override async Task<DABMusicDownloadRequest> CreateDownloadR...

FILE: Tubifarry/Download/Clients/DABMusic/DABMusicDownloadOptions.cs
  type DABMusicDownloadOptions (line 5) | public record DABMusicDownloadOptions : BaseDownloadOptions

FILE: Tubifarry/Download/Clients/DABMusic/DABMusicDownloadRequest.cs
  class DABMusicDownloadRequest (line 19) | public class DABMusicDownloadRequest : BaseDownloadRequest<DABMusicDownl...
    method DABMusicDownloadRequest (line 32) | public DABMusicDownloadRequest(RemoteAlbum remoteAlbum, IDABMusicSessi...
    method ProcessDownloadAsync (line 59) | protected override async Task ProcessDownloadAsync(CancellationToken t...
    method ProcessSingleTrackAsync (line 69) | private async Task ProcessSingleTrackAsync(string trackId, Cancellatio...
    method ProcessAlbumAsync (line 101) | private async Task ProcessAlbumAsync(string albumId, CancellationToken...
    method RequestAsync (line 141) | private async Task<string> RequestAsync(string url, CancellationToken ...
    method GetTrackAsync (line 156) | private async Task<DABMusicTrack> GetTrackAsync(string trackId, Cancel...
    method GetAlbumAsync (line 175) | private async Task<DABMusicAlbum> GetAlbumAsync(string albumId, Cancel...
    method GetStreamUrlAsync (line 203) | private async Task<string> GetStreamUrlAsync(string trackId, Cancellat...
    method DownloadAlbumCoverAsync (line 219) | private async Task DownloadAlbumCoverAsync(string? coverUrl, Cancellat...
    method InitiateDownload (line 239) | private void InitiateDownload(string streamUrl, string fileName, DABMu...
    method PostProcessTrackAsync (line 284) | private async Task<bool> PostProcessTrackAsync(DABMusicTrack trackInfo...
    method CreateAlbumFromDABData (line 315) | private Album CreateAlbumFromDABData(DABMusicAlbum? albumInfo)
    method CreateTrackFromDABData (line 344) | private Track CreateTrackFromDABData(DABMusicTrack trackInfo, DABMusic...

FILE: Tubifarry/Download/Clients/DABMusic/DABMusicProviderSettings.cs
  class DABMusicProviderSettingsValidator (line 9) | public class DABMusicProviderSettingsValidator : AbstractValidator<DABMu...
    method DABMusicProviderSettingsValidator (line 11) | public DABMusicProviderSettingsValidator()
  class DABMusicProviderSettings (line 53) | public class DABMusicProviderSettings : IProviderConfig
    method Validate (line 78) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Download/Clients/Lucida/ILucidaRateLimiter.cs
  type ILucidaRateLimiter (line 3) | public interface ILucidaRateLimiter
    method WaitForAvailableWorkerAsync (line 25) | Task<string> WaitForAvailableWorkerAsync(CancellationToken cancellatio...
    method MarkWorkerRateLimited (line 27) | void MarkWorkerRateLimited(string workerName);
    method ReleaseWorker (line 29) | void ReleaseWorker(string workerName);
    method EnsureWorkerRegistered (line 31) | void EnsureWorkerRegistered(string workerName);
    method IsRateLimitedResponse (line 33) | bool IsRateLimitedResponse(string responseContent);
    method GetWorkerStates (line 35) | IReadOnlyDictionary<string, LucidaWorkerState> GetWorkerStates();
    method AcquirePollingSlotAsync (line 37) | Task AcquirePollingSlotAsync(string workerName, CancellationToken canc...
    method ReleasePollingSlot (line 39) | void ReleasePollingSlot(string workerName);

FILE: Tubifarry/Download/Clients/Lucida/LucidaClient.cs
  class LucidaClient (line 18) | public class LucidaClient : DownloadClientBase<LucidaProviderSettings>
    method LucidaClient (line 24) | public LucidaClient(
    method Download (line 44) | public override Task<string> Download(RemoteAlbum remoteAlbum, IIndexe...
    method GetItems (line 46) | public override IEnumerable<DownloadClientItem> GetItems() => _downloa...
    method RemoveItem (line 48) | public override void RemoveItem(DownloadClientItem item, bool deleteData)
    method GetStatus (line 55) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 61) | protected override void Test(List<ValidationFailure> failures)
    method TestWorkerHealth (line 78) | private void TestWorkerHealth()

FILE: Tubifarry/Download/Clients/Lucida/LucidaDownloadManager.cs
  type ILucidaDownloadManager (line 15) | public interface ILucidaDownloadManager : IBaseDownloadManager<LucidaDow...
  class LucidaDownloadManager (line 20) | public class LucidaDownloadManager(
    method CreateDownloadRequest (line 26) | protected override async Task<LucidaDownloadRequest> CreateDownloadReq...

FILE: Tubifarry/Download/Clients/Lucida/LucidaDownloadOptions.cs
  type LucidaDownloadOptions (line 5) | public record LucidaDownloadOptions : BaseDownloadOptions

FILE: Tubifarry/Download/Clients/Lucida/LucidaDownloadRequest.cs
  class LucidaDownloadRequest (line 19) | public class LucidaDownloadRequest : BaseDownloadRequest<LucidaDownloadO...
    method LucidaDownloadRequest (line 25) | public LucidaDownloadRequest(RemoteAlbum remoteAlbum, LucidaDownloadOp...
    method ProcessDownloadAsync (line 52) | protected override async Task ProcessDownloadAsync(CancellationToken t...
    method ProcessSingleTrackAsync (line 62) | private async Task ProcessSingleTrackAsync(string downloadUrl, Cancell...
    method ProcessAlbumAsync (line 95) | private async Task ProcessAlbumAsync(string downloadUrl, CancellationT...
    method InitiateDownload (line 130) | private void InitiateDownload(string url, string primaryToken, string ...
    method InitiateDownloadWithRetryAsync (line 200) | private async Task<LucidaInitiationResult> InitiateDownloadWithRetryAs...
    method InitiateDownloadRequestAsync (line 278) | private async Task<LucidaInitiationResult> InitiateDownloadRequestAsync(
    method PollForCompletionAsync (line 337) | private async Task<bool> PollForCompletionAsync(string handoffId, stri...
    method ExtractDomainFromUrl (line 416) | private static string ExtractDomainFromUrl(string url)
    method CreateAlbumFromLucidaData (line 423) | private Album CreateAlbumFromLucidaData(LucidaAlbumModel? albumInfo)
    method CreateTrackFromLucidaData (line 449) | private Track CreateTrackFromLucidaData(LucidaTrackModel trackInfo, Lu...

FILE: Tubifarry/Download/Clients/Lucida/LucidaInitiationResult.cs
  type LucidaInitiationResult (line 3) | public sealed record LucidaInitiationResult

FILE: Tubifarry/Download/Clients/Lucida/LucidaMetadataExtractor.cs
  class LucidaMetadataExtractor (line 16) | public static partial class LucidaMetadataExtractor
    method ExtractAlbumMetadataAsync (line 26) | public static async Task<LucidaAlbumModel> ExtractAlbumMetadataAsync(B...
    method ExtractViaApiAsync (line 79) | private static async Task<LucidaAlbumModel?> ExtractViaApiAsync(BaseHt...
    method ExtractViaHtmlAsync (line 136) | private static async Task<LucidaAlbumModel?> ExtractViaHtmlAsync(BaseH...
    method EnrichWithCsrfTokensAsync (line 195) | private static async Task EnrichWithCsrfTokensAsync(
    method ExtractInfoFromHtml (line 245) | private static LucidaInfo? ExtractInfoFromHtml(string html)
    method ExtractJsonArrayFromHtml (line 266) | private static string ExtractJsonArrayFromHtml(string html)
    method ParseDataArray (line 334) | private static LucidaInfo? ParseDataArray(string jsonArray)
    method ExecuteJintOnDataArray (line 367) | private static LucidaInfo? ExecuteJintOnDataArray(string jsonArray)
    method ConvertToAlbum (line 407) | private static LucidaAlbumModel ConvertToAlbum(LucidaInfo info)
    method ConvertSingleTrackToAlbum (line 472) | private static LucidaAlbumModel ConvertSingleTrackToAlbum(LucidaInfo i...
    method ApplyTokensToAlbum (line 529) | private static void ApplyTokensToAlbum(LucidaAlbumModel album, LucidaT...
    method IsCharIndexed404 (line 545) | private static bool IsCharIndexed404(string body)

FILE: Tubifarry/Download/Clients/Lucida/LucidaProviderSettings.cs
  class LucidaProviderSettingsValidator (line 9) | public class LucidaProviderSettingsValidator : AbstractValidator<LucidaP...
    method LucidaProviderSettingsValidator (line 11) | public LucidaProviderSettingsValidator()
  class LucidaProviderSettings (line 54) | public class LucidaProviderSettings : IProviderConfig
    method Validate (line 76) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Download/Clients/Lucida/LucidaRateLimitException.cs
  class LucidaRateLimitException (line 3) | public class LucidaRateLimitException : Exception
    method LucidaRateLimitException (line 6) | public LucidaRateLimitException(string message, string? workerName = n...
    method LucidaRateLimitException (line 9) | public LucidaRateLimitException(string message, Exception innerExcepti...

FILE: Tubifarry/Download/Clients/Lucida/LucidaRateLimiter.cs
  class LucidaRateLimiter (line 8) | public sealed class LucidaRateLimiter : ILucidaRateLimiter
    method LucidaRateLimiter (line 18) | public LucidaRateLimiter(Logger logger)
    method EnsureWorkerRegistered (line 26) | public void EnsureWorkerRegistered(string workerName)
    method WaitForAvailableWorkerAsync (line 34) | public async Task<string> WaitForAvailableWorkerAsync(CancellationToke...
    method MarkWorkerRateLimited (line 65) | public void MarkWorkerRateLimited(string workerName)
    method ReleaseWorker (line 78) | public void ReleaseWorker(string workerName)
    method IsRateLimitedResponse (line 100) | public bool IsRateLimitedResponse(string responseContent)
    method GetWorkerStates (line 120) | public IReadOnlyDictionary<string, LucidaWorkerState> GetWorkerStates()
    method AcquirePollingSlotAsync (line 143) | public async Task AcquirePollingSlotAsync(string workerName, Cancellat...
    method ReleasePollingSlot (line 155) | public void ReleasePollingSlot(string workerName)
    method PickBestWorker (line 171) | private string? PickBestWorker()
    method EnforceGlobalDelayAsync (line 208) | private async Task EnforceGlobalDelayAsync(CancellationToken cancellat...
    method DecodeJsonEncodedHtml (line 231) | private static string DecodeJsonEncodedHtml(string jsonEncoded)
    class WorkerSlot (line 248) | private sealed class WorkerSlot
      method WorkerSlot (line 259) | public WorkerSlot(string name)

FILE: Tubifarry/Download/Clients/Lucida/LucidaTokenExtractor.cs
  class LucidaTokenExtractor (line 10) | public static partial class LucidaTokenExtractor
    method ExtractTokensAsync (line 14) | public static async Task<LucidaTokens> ExtractTokensAsync(BaseHttpClie...
    method ExtractTokensFromHtml (line 29) | public static LucidaTokens ExtractTokensFromHtml(string html)
    method DoubleBase64Decode (line 67) | private static string DoubleBase64Decode(string encoded)
    method NormalizeBase64 (line 89) | private static string NormalizeBase64(string input)
    method TrackTokenRegex (line 102) | [GeneratedRegex(@"\b""?token""?\s*:\s*""([A-Za-z0-9+/=_-]{16,})""\s*,\...

FILE: Tubifarry/Download/Clients/Lucida/LucidaWorkerState.cs
  type LucidaWorkerState (line 3) | public sealed record LucidaWorkerState

FILE: Tubifarry/Download/Clients/Soulseek/ISlskdApiClient.cs
  class SlskdUserTransfers (line 6) | public class SlskdUserTransfers
  class SlskdEventRecord (line 12) | public class SlskdEventRecord
  type ISlskdApiClient (line 20) | public interface ISlskdApiClient
    method EnqueueDownloadAsync (line 22) | Task<(List<string> Enqueued, List<string> Failed)> EnqueueDownloadAsyn...
    method GetAllTransfersAsync (line 23) | Task<List<SlskdUserTransfers>> GetAllTransfersAsync(SlskdProviderSetti...
    method GetUserTransfersAsync (line 24) | Task<SlskdUserTransfers?> GetUserTransfersAsync(SlskdProviderSettings ...
    method GetTransferAsync (line 25) | Task<SlskdDownloadFile?> GetTransferAsync(SlskdProviderSettings settin...
    method GetQueuePositionAsync (line 26) | Task<int?> GetQueuePositionAsync(SlskdProviderSettings settings, strin...
    method DeleteTransferAsync (line 27) | Task DeleteTransferAsync(SlskdProviderSettings settings, string userna...
    method DeleteAllCompletedAsync (line 28) | Task DeleteAllCompletedAsync(SlskdProviderSettings settings);
    method GetDownloadPathAsync (line 29) | Task<string?> GetDownloadPathAsync(SlskdProviderSettings settings);
    method TestConnectionAsync (line 30) | Task<ValidationFailure?> TestConnectionAsync(SlskdProviderSettings set...
    method GetEventsAsync (line 31) | Task<(List<SlskdEventRecord> Events, int TotalCount)> GetEventsAsync(S...

FILE: Tubifarry/Download/Clients/Soulseek/ISlskdDownloadManager.cs
  type ISlskdDownloadManager (line 7) | public interface ISlskdDownloadManager
    method DownloadAsync (line 9) | Task<string> DownloadAsync(RemoteAlbum remoteAlbum, int definitionId, ...
    method GetItems (line 10) | IEnumerable<DownloadClientItem> GetItems(int definitionId, SlskdProvid...
    method RemoveItem (line 11) | void RemoveItem(DownloadClientItem clientItem, bool deleteData, int de...

FILE: Tubifarry/Download/Clients/Soulseek/Models/DownloadKey.cs
  type DownloadKey (line 3) | public readonly struct DownloadKey<TOuterKey, TInnerKey>(TOuterKey outer...
    method Equals (line 10) | public override readonly bool Equals(object? obj) =>
    method GetHashCode (line 15) | public override readonly int GetHashCode() =>

FILE: Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadDirectory.cs
  type SlskdDownloadDirectory (line 6) | public record SlskdDownloadDirectory(string Directory, int FileCount, Li...

FILE: Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadFile.cs
  type SlskdDownloadFile (line 6) | public record SlskdDownloadFile(

FILE: Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadItem.cs
  class SlskdDownloadItem (line 11) | public class SlskdDownloadItem
    method SlskdDownloadItem (line 42) | public SlskdDownloadItem(ReleaseInfo releaseInfo)
    method GetStableMD5Id (line 51) | public static string GetStableMD5Id(IEnumerable<string?> filenames)
    method CompareFileStates (line 58) | private void CompareFileStates(SlskdDownloadDirectory? newDirectory)
    method GetFullFolderPath (line 90) | public OsPath GetFullFolderPath(OsPath downloadPath) => new(Path.Combine(

FILE: Tubifarry/Download/Clients/Soulseek/Models/SlskdFileState.cs
  type TransferStates (line 5) | [Flags]
  class SlskdFileState (line 23) | public class SlskdFileState(SlskdDownloadFile file)
    method GetStatus (line 32) | public DownloadItemStatus GetStatus()
    method GetStatus (line 40) | public static DownloadItemStatus GetStatus(string stateStr)
    method GetStatus (line 47) | public static DownloadItemStatus GetStatus(TransferStates state) => st...
    method UpdateFile (line 62) | public void UpdateFile(SlskdDownloadFile file)
    method UpdateMaxRetryCount (line 72) | public void UpdateMaxRetryCount(int maxRetryCount) => MaxRetryCount = ...
    method IncrementAttempt (line 74) | public void IncrementAttempt()

FILE: Tubifarry/Download/Clients/Soulseek/SlskdApiClient.cs
  class SlskdApiClient (line 10) | public class SlskdApiClient(IHttpClient httpClient) : ISlskdApiClient
    method EnqueueDownloadAsync (line 14) | public async Task<(List<string> Enqueued, List<string> Failed)> Enqueu...
    method GetAllTransfersAsync (line 39) | public async Task<List<SlskdUserTransfers>> GetAllTransfersAsync(Slskd...
    method GetUserTransfersAsync (line 64) | public async Task<SlskdUserTransfers?> GetUserTransfersAsync(SlskdProv...
    method GetTransferAsync (line 84) | public async Task<SlskdDownloadFile?> GetTransferAsync(SlskdProviderSe...
    method GetQueuePositionAsync (line 96) | public async Task<int?> GetQueuePositionAsync(SlskdProviderSettings se...
    method DeleteTransferAsync (line 115) | public async Task DeleteTransferAsync(SlskdProviderSettings settings, ...
    method DeleteAllCompletedAsync (line 122) | public async Task DeleteAllCompletedAsync(SlskdProviderSettings settin...
    method GetDownloadPathAsync (line 126) | public async Task<string?> GetDownloadPathAsync(SlskdProviderSettings ...
    method TestConnectionAsync (line 141) | public async Task<ValidationFailure?> TestConnectionAsync(SlskdProvide...
    method GetEventsAsync (line 191) | public async Task<(List<SlskdEventRecord> Events, int TotalCount)> Get...
    method BuildRequest (line 223) | private HttpRequest BuildRequest(SlskdProviderSettings settings, strin...

FILE: Tubifarry/Download/Clients/Soulseek/SlskdClient.cs
  class SlskdClient (line 14) | public class SlskdClient : DownloadClientBase<SlskdProviderSettings>
    method SlskdClient (line 22) | public SlskdClient(
    method Download (line 36) | public override async Task<string> Download(RemoteAlbum remoteAlbum, I...
    method GetItems (line 39) | public override IEnumerable<DownloadClientItem> GetItems()
    method RemoveItem (line 49) | public override void RemoveItem(DownloadClientItem clientItem, bool de...
    method GetStatus (line 52) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 58) | protected override void Test(List<ValidationFailure> failures) =>
    method GetRemoteToLocal (line 61) | private OsPath GetRemoteToLocal() =>

FILE: Tubifarry/Download/Clients/Soulseek/SlskdDownloadManager.cs
  class SlskdEventTypes (line 17) | internal static class SlskdEventTypes
  class SlskdDownloadManager (line 23) | public class SlskdDownloadManager : ISlskdDownloadManager
    method SlskdDownloadManager (line 45) | public SlskdDownloadManager(
    method DownloadAsync (line 64) | public async Task<string> DownloadAsync(RemoteAlbum remoteAlbum, int d...
    method GetItems (line 98) | public IEnumerable<DownloadClientItem> GetItems(int definitionId, Slsk...
    method RemoveItem (line 146) | public void RemoveItem(DownloadClientItem clientItem, bool deleteData,...
    method RefreshAsync (line 164) | private async Task RefreshAsync(int definitionId, SlskdProviderSetting...
    method PollTransfersAsync (line 188) | private async Task PollTransfersAsync(int definitionId, SlskdProviderS...
    method ProcessUserTransfers (line 222) | private void ProcessUserTransfers(
    method PollEventsAsync (line 256) | private async Task PollEventsAsync(int definitionId, SlskdProviderSett...
    method HandleEventAsync (line 277) | private async Task HandleEventAsync(int definitionId, SlskdProviderSet...
    method EmitCompletionSpan (line 312) | private void EmitCompletionSpan(SlskdDownloadItem item, SlskdStatusRes...
    method SubscribeStateChanges (line 336) | private void SubscribeStateChanges(SlskdDownloadItem item, int definit...
    method RemoveItemFilesAsync (line 345) | private async Task RemoveItemFilesAsync(SlskdDownloadItem item, SlskdP...
    method CleanStaleDirectoriesAsync (line 391) | private async Task CleanStaleDirectoriesAsync(string directoryPath, Sl...
    method CreateReleaseInfoFromDirectory (line 430) | private ReleaseInfo CreateReleaseInfoFromDirectory(string username, Sl...
    method ExtractUsernameFromPath (line 441) | private static string ExtractUsernameFromPath(string path)
    method ParseFilesFromSource (line 447) | private static List<(string Filename, long Size)> ParseFilesFromSource...
    method GetItem (line 458) | private SlskdDownloadItem? GetItem(int definitionId, string id) =>
    method GetItemsForDef (line 462) | private IEnumerable<SlskdDownloadItem> GetItemsForDef(int definitionId...
    method AddItem (line 467) | private void AddItem(int definitionId, SlskdDownloadItem item) =>
    method RemoveItemFromDict (line 470) | private void RemoveItemFromDict(int definitionId, string id) =>
    method GetActiveUsernames (line 473) | private HashSet<string> GetActiveUsernames(int definitionId) =>

FILE: Tubifarry/Download/Clients/Soulseek/SlskdProviderSettings.cs
  class SlskdProviderSettingsValidator (line 9) | internal class SlskdProviderSettingsValidator : AbstractValidator<SlskdP...
    method SlskdProviderSettingsValidator (line 11) | public SlskdProviderSettingsValidator()
  class SlskdProviderSettings (line 37) | public partial class SlskdProviderSettings : IProviderConfig
    method GetTimeout (line 74) | public TimeSpan? GetTimeout() => Timeout == null ? null : TimeSpan.Fro...
    method Validate (line 76) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
    method HostRegex (line 78) | [GeneratedRegex(@"^(?:https?:\/\/)?([^\/:\?]+)(?::\d+)?(?:\/|$)", Rege...

FILE: Tubifarry/Download/Clients/Soulseek/SlskdRetryHandler.cs
  class SlskdRetryHandler (line 9) | public class SlskdRetryHandler(ISlskdApiClient apiClient, ISentryHelper ...
    method OnFileStateChanged (line 15) | public void OnFileStateChanged(SlskdDownloadItem? item, SlskdFileState...
    method RetryDownloadAsync (line 28) | private async Task RetryDownloadAsync(SlskdDownloadItem item, SlskdFil...
    method ExtractUsernameFromPath (line 66) | private static string ExtractUsernameFromPath(string path)

FILE: Tubifarry/Download/Clients/Soulseek/SlskdStatusResolver.cs
  class SlskdStatusResolver (line 6) | public static class SlskdStatusResolver
    type DownloadStatus (line 8) | public record DownloadStatus(
    method Resolve (line 16) | public static DownloadStatus Resolve(SlskdDownloadItem item, TimeSpan?...

FILE: Tubifarry/Download/Clients/SubSonic/SubSonicClient.cs
  class SubSonicClient (line 23) | public class SubSonicClient : DownloadClientBase<SubSonicProviderSettings>
    method SubSonicClient (line 29) | public SubSonicClient(
    method Download (line 49) | public override Task<string> Download(RemoteAlbum remoteAlbum, IIndexe...
    method GetItems (line 52) | public override IEnumerable<DownloadClientItem> GetItems()
    method RemoveItem (line 55) | public override void RemoveItem(DownloadClientItem item, bool deleteData)
    method GetStatus (line 62) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 68) | protected override void Test(List<ValidationFailure> failures)

FILE: Tubifarry/Download/Clients/SubSonic/SubSonicDownloadManager.cs
  type ISubSonicDownloadManager (line 11) | public interface ISubSonicDownloadManager : IBaseDownloadManager<SubSoni...
  class SubSonicDownloadManager (line 17) | public class SubSonicDownloadManager(IEnumerable<IHttpRequestInterceptor...
    method CreateDownloadRequest (line 21) | protected override async Task<SubSonicDownloadRequest> CreateDownloadR...
    method ExtractIdFromUrl (line 65) | private static string ExtractIdFromUrl(string url)

FILE: Tubifarry/Download/Clients/SubSonic/SubSonicDownloadOptions.cs
  type SubSonicDownloadOptions (line 8) | public record SubSonicDownloadOptions : BaseDownloadOptions

FILE: Tubifarry/Download/Clients/SubSonic/SubSonicDownloadRequest.cs
  class SubSonicDownloadRequest (line 21) | public class SubSonicDownloadRequest : BaseDownloadRequest<SubSonicDownl...
    method SubSonicDownloadRequest (line 26) | public SubSonicDownloadRequest(RemoteAlbum remoteAlbum, SubSonicDownlo...
    method ProcessDownloadAsync (line 56) | protected override async Task ProcessDownloadAsync(CancellationToken t...
    method ProcessSingleTrackAsync (line 66) | private async Task ProcessSingleTrackAsync(string songId, Cancellation...
    method ProcessAlbumAsync (line 80) | private async Task ProcessAlbumAsync(string albumId, CancellationToken...
    method GetSongAsync (line 111) | private async Task<SubSonicSearchSong> GetSongAsync(string songId, Can...
    method GetAlbumAsync (line 131) | private async Task<SubSonicAlbumFull> GetAlbumAsync(string albumId, Ca...
    method TryGetAlbumAsync (line 151) | private async Task<SubSonicAlbumFull?> TryGetAlbumAsync(string albumId...
    method TryDownloadAlbumCoverAsync (line 164) | private async Task TryDownloadAlbumCoverAsync(string? coverArtId, Canc...
    method ExecuteApiRequestAsync (line 185) | private async Task<string> ExecuteApiRequestAsync(string endpoint, str...
    method ValidateApiResponse (line 196) | private static void ValidateApiResponse(SubSonicItemResponse? response)
    method BuildApiUrl (line 206) | private string BuildApiUrl(string endpoint, string id)
    method BuildStreamUrl (line 215) | private string BuildStreamUrl(string songId)
    method BuildCoverArtUrl (line 231) | private string BuildCoverArtUrl(string coverArtId)
    method AppendStandardApiParameters (line 240) | private void AppendStandardApiParameters(StringBuilder urlBuilder)
    method QueueTrackDownload (line 246) | private void QueueTrackDownload(SubSonicSearchSong track, Cancellation...
    method CreateDownloadRequest (line 263) | private LoadRequest CreateDownloadRequest(string streamUrl, string fil...
    method CreatePostProcessRequest (line 280) | private OwnRequest CreatePostProcessRequest(SubSonicSearchSong track, ...
    method PostProcessTrackAsync (line 292) | private async Task<bool> PostProcessTrackAsync(SubSonicSearchSong trac...
    method CreateAlbumFromSubSonicData (line 333) | private Album CreateAlbumFromSubSonicData(SubSonicSearchSong track, Su...
    method DetermineReleaseDate (line 359) | private DateTime DetermineReleaseDate(SubSonicAlbumFull? albumInfo, Su...
    method CreateTrackFromSubSonicData (line 369) | private static Track CreateTrackFromSubSonicData(SubSonicSearchSong tr...

FILE: Tubifarry/Download/Clients/SubSonic/SubSonicProviderSettings.cs
  class SubSonicProviderSettingsValidator (line 9) | public class SubSonicProviderSettingsValidator : AbstractValidator<SubSo...
    method SubSonicProviderSettingsValidator (line 11) | public SubSonicProviderSettingsValidator()
  class SubSonicProviderSettings (line 62) | public class SubSonicProviderSettings : IProviderConfig
    method Validate (line 99) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type PreferredFormatEnum (line 105) | public enum PreferredFormatEnum

FILE: Tubifarry/Download/Clients/TripleTriple/TripleTripleClient.cs
  class TripleTripleClient (line 17) | public class TripleTripleClient : DownloadClientBase<TripleTripleProvide...
    method TripleTripleClient (line 23) | public TripleTripleClient(
    method Download (line 43) | public override Task<string> Download(RemoteAlbum remoteAlbum, IIndexe...
    method GetItems (line 45) | public override IEnumerable<DownloadClientItem> GetItems() => _downloa...
    method RemoveItem (line 47) | public override void RemoveItem(DownloadClientItem item, bool deleteData)
    method GetStatus (line 54) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 60) | protected override void Test(List<ValidationFailure> failures)

FILE: Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadManager.cs
  type ITripleTripleDownloadManager (line 12) | public interface ITripleTripleDownloadManager : IBaseDownloadManager<Tri...
  class TripleTripleDownloadManager (line 14) | public class TripleTripleDownloadManager : BaseDownloadManager<TripleTri...
    method TripleTripleDownloadManager (line 18) | public TripleTripleDownloadManager(IEnumerable<IHttpRequestInterceptor...
    method CreateDownloadRequest (line 23) | protected override Task<TripleTripleDownloadRequest> CreateDownloadReq...

FILE: Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadOptions.cs
  type TripleTripleDownloadOptions (line 6) | public record TripleTripleDownloadOptions : BaseDownloadOptions

FILE: Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadRequest.cs
  class TripleTripleDownloadRequest (line 18) | public class TripleTripleDownloadRequest : BaseDownloadRequest<TripleTri...
    method TripleTripleDownloadRequest (line 24) | public TripleTripleDownloadRequest(RemoteAlbum remoteAlbum, TripleTrip...
    method ProcessDownloadAsync (line 50) | protected override async Task ProcessDownloadAsync(CancellationToken t...
    method ProcessSingleTrackAsync (line 62) | private async Task ProcessSingleTrackAsync(string asin, CancellationTo...
    method ProcessAlbumAsync (line 84) | private async Task ProcessAlbumAsync(string asin, CancellationToken to...
    method GetMediaAsync (line 132) | private async Task<TripleTripleMediaResponse?> GetMediaAsync(string as...
    method GetAlbumMetadataAsync (line 149) | private async Task<TripleTripleAlbumInfo?> GetAlbumMetadataAsync(strin...
    method GetAlbumMediaAsync (line 165) | private async Task<List<TripleTripleMediaResponse>?> GetAlbumMediaAsyn...
    method RequestAsync (line 181) | private async Task<string> RequestAsync(string url, CancellationToken ...
    method DownloadAlbumCoverAsync (line 191) | private async Task DownloadAlbumCoverAsync(string? coverUrl, Cancellat...
    method BuildCoverUrl (line 211) | private string BuildCoverUrl(string? template)
    method InitiateDownload (line 222) | private void InitiateDownload(TripleTripleMediaResponse media, string ...
    method PostProcessTrackAsync (line 267) | private async Task<bool> PostProcessTrackAsync(TripleTripleMediaRespon...
    method ParseSyncedLyrics (line 317) | private static Lyric? ParseSyncedLyrics(string syncedLyrics)
    method CreateAlbumFromMedia (line 338) | private Album CreateAlbumFromMedia(TripleTripleMediaResponse media) =>...
    method CreateAlbumFromMetadata (line 353) | private Album CreateAlbumFromMetadata(TripleTripleAlbumInfo albumInfo)...
    method CreateTrackFromMedia (line 368) | private Track CreateTrackFromMedia(TripleTripleMediaResponse media) =>...
    method CreateTrackFromAlbum (line 378) | private Track CreateTrackFromAlbum(TripleTripleTrackInfo track, Triple...

FILE: Tubifarry/Download/Clients/TripleTriple/TripleTripleProviderSettings.cs
  class TripleTripleProviderSettingsValidator (line 10) | public class TripleTripleProviderSettingsValidator : AbstractValidator<T...
    method TripleTripleProviderSettingsValidator (line 12) | public TripleTripleProviderSettingsValidator()
  class TripleTripleProviderSettings (line 45) | public class TripleTripleProviderSettings : IProviderConfig
    method TripleTripleProviderSettings (line 49) | public TripleTripleProviderSettings()
    method Validate (line 93) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Download/Clients/YouTube/SponsorBlock.cs
  class SponsorBlock (line 10) | public class SponsorBlock
    method SponsorBlock (line 17) | public SponsorBlock(string filePath, string videoId, string apiEndpoin...
    method LookupAndTrimAsync (line 36) | public async Task<bool> LookupAndTrimAsync(CancellationToken cancellat...
    method FetchNonMusicSegmentsAsync (line 64) | private async Task<List<SponsorSegment>> FetchNonMusicSegmentsAsync(Ca...
    method TrimSegmentsAsync (line 99) | private async Task<bool> TrimSegmentsAsync(List<SponsorSegment> segmen...
    type SponsorSegment (line 212) | private record SponsorSegment(

FILE: Tubifarry/Download/Clients/YouTube/TrustedSessionHelper.cs
  class TrustedSessionHelper (line 21) | public class TrustedSessionHelper
    method GetTrustedSessionTokensAsync (line 50) | public static async Task<SessionTokens> GetTrustedSessionTokensAsync(s...
    method GetVideoBoundTokensAsync (line 100) | public static async Task<SessionTokens> GetVideoBoundTokensAsync(strin...
    method CreateSessionInfoAsync (line 130) | public static async Task<ClientSessionInfo> CreateSessionInfoAsync(str...
    method CreateAuthenticatedClient (line 152) | public static YouTubeMusicClient CreateAuthenticatedClient(ClientSessi...
    method CreateAuthenticatedClientAsync (line 176) | public static async Task<YouTubeMusicClient> CreateAuthenticatedClient...
    method UpdateClientWithVideoBoundTokensAsync (line 185) | public static async Task UpdateClientWithVideoBoundTokensAsync(YouTube...
    method LoadCookies (line 195) | public static Cookie[]? LoadCookies(string cookiePath)
    method GetTokensFromWebServiceAsync (line 224) | private static async Task<SessionTokens> GetTokensFromWebServiceAsync(...
    method ParseResponse (line 262) | private static SessionTokens ParseResponse(string responseJson, string...
    method GetOrCreateVisitorDataAsync (line 292) | private static async Task<string> GetOrCreateVisitorDataAsync(Cancella...
    method GetTokensFromLocalGeneratorAsync (line 329) | private static async Task<SessionTokens> GetTokensFromLocalGeneratorAs...
    method ValidateAuthenticationSettingsAsync (line 385) | public static async Task ValidateAuthenticationSettingsAsync(string? t...
    method IsNodeJsAvailable (line 437) | private static bool IsNodeJsAvailable() => NodeJsAvailable.Value;
    method ClearCache (line 439) | public static void ClearCache()
    method GetCachedTokens (line 447) | public static SessionTokens? GetCachedTokens() => _cachedTokens;
    class YouTubeSessionGeneratorLogger (line 452) | private class YouTubeSessionGeneratorLogger : Microsoft.Extensions.Log...
      method YouTubeSessionGeneratorLogger (line 456) | public YouTubeSessionGeneratorLogger(Logger nlogLogger) => _nlogLogg...
      method BeginScope (line 458) | public IDisposable? BeginScope<TState>(TState state) where TState : ...
      method IsEnabled (line 460) | public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel...
      method Log (line 462) | public void Log<TState>(Microsoft.Extensions.Logging.LogLevel logLev...

FILE: Tubifarry/Download/Clients/YouTube/YouTubeDownloadOptions.cs
  type YouTubeDownloadOptions (line 9) | public record YouTubeDownloadOptions : BaseDownloadOptions

FILE: Tubifarry/Download/Clients/YouTube/YouTubeDownloadRequest.cs
  class YouTubeDownloadRequest (line 22) | public class YouTubeDownloadRequest : BaseDownloadRequest<YouTubeDownloa...
    method YouTubeDownloadRequest (line 24) | public YouTubeDownloadRequest(RemoteAlbum remoteAlbum, YouTubeDownload...
    method ProcessDownloadAsync (line 50) | protected override async Task ProcessDownloadAsync(CancellationToken t...
    method ProcessAlbumAsync (line 56) | private async Task ProcessAlbumAsync(string downloadUrl, CancellationT...
    method AddTrackDownloadRequest (line 110) | private void AddTrackDownloadRequest(AlbumInfo albumInfo, AlbumSong tr...
    method PostProcessTrackAsync (line 161) | private async Task<bool> PostProcessTrackAsync(AlbumInfo albumInfo, Al...
    method CreateAlbumFromYouTubeData (line 202) | private Album CreateAlbumFromYouTubeData(AlbumInfo albumInfo) => new()
    method CreateTrackFromYouTubeData (line 218) | private Track CreateTrackFromYouTubeData(AlbumSong trackInfo, AlbumInf...
    method TryDownloadCoverAsync (line 231) | private async Task<byte[]?> TryDownloadCoverAsync(AlbumInfo albumInfo,...
    method ApplyRandomDelayAsync (line 258) | private async Task ApplyRandomDelayAsync(CancellationToken token)
    method TryUpdateVideoBoundTokensAsync (line 267) | private async Task TryUpdateVideoBoundTokensAsync(string videoId, Canc...

FILE: Tubifarry/Download/Clients/YouTube/YoutubeClient.cs
  class YoutubeClient (line 20) | public class YoutubeClient : DownloadClientBase<YoutubeProviderSettings>
    method YoutubeClient (line 25) | public YoutubeClient(IYoutubeDownloadManager dlManager, IConfigService...
    method Download (line 38) | public override Task<string> Download(RemoteAlbum remoteAlbum, IIndexe...
    method GetItems (line 40) | public override IEnumerable<DownloadClientItem> GetItems() => _dlManag...
    method RemoveItem (line 42) | public override void RemoveItem(DownloadClientItem item, bool deleteData)
    method GetStatus (line 49) | public override DownloadClientInfo GetStatus() => new()
    method Test (line 55) | protected override void Test(List<ValidationFailure> failures)
    method TestFFmpeg (line 74) | public async Task<ValidationFailure> TestFFmpeg()

FILE: Tubifarry/Download/Clients/YouTube/YoutubeDownloadManager.cs
  type IYoutubeDownloadManager (line 15) | public interface IYoutubeDownloadManager : IBaseDownloadManager<YouTubeD...
  class YoutubeDownloadManager (line 20) | public class YoutubeDownloadManager : BaseDownloadManager<YouTubeDownloa...
    method YoutubeDownloadManager (line 26) | public YoutubeDownloadManager(Logger logger) : base(logger)
    method CreateDownloadRequest (line 31) | protected override async Task<YouTubeDownloadRequest> CreateDownloadRe...
    method UpdateClientAsync (line 66) | private async Task UpdateClientAsync(YoutubeClient provider)

FILE: Tubifarry/Download/Clients/YouTube/YoutubeProviderSettings.cs
  class YoutubeProviderSettingsValidator (line 10) | public class YoutubeProviderSettingsValidator : AbstractValidator<Youtub...
    method YoutubeProviderSettingsValidator (line 12) | public YoutubeProviderSettingsValidator()
  class YoutubeProviderSettings (line 76) | public class YoutubeProviderSettings : IProviderConfig
    method Validate (line 116) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type ReEncodeOptions (line 119) | public enum ReEncodeOptions

FILE: Tubifarry/ImportLists/ArrStack/ArrMedia.cs
  type ArrMedia (line 9) | internal record class ArrMedia
  type ArrTag (line 41) | internal record ArrTag

FILE: Tubifarry/ImportLists/ArrStack/ArrSoundtrackImport.cs
  class ArrSoundtrackImport (line 17) | public class ArrSoundtrackImport : HttpImportListBase<ArrSoundtrackImpor...
    method ArrSoundtrackImport (line 36) | public ArrSoundtrackImport(IHttpClient httpClient, IImportListStatusSe...
    method GetRequestGenerator (line 40) | public override IImportListRequestGenerator GetRequestGenerator() => _...
    method GetParser (line 42) | public override IParseImportListResponse GetParser() => _parser ??= ne...
    method Test (line 44) | protected override void Test(List<ValidationFailure> failures)
    method TestArrConnection (line 52) | private ValidationFailure? TestArrConnection()
    method TestCacheDirectory (line 94) | private ValidationFailure? TestCacheDirectory()
    method GetDefinition (line 125) | private ImportListDefinition GetDefinition(string name, ArrSoundtrackI...
    method GetSettings (line 133) | private static ArrSoundtrackImportSettings GetSettings(string baseUrl,...

FILE: Tubifarry/ImportLists/ArrStack/ArrSoundtrackImportParser.cs
  class ArrSoundtrackImportParser (line 22) | internal partial class ArrSoundtrackImportParser : IParseImportListResponse
    method ArrSoundtrackImportParser (line 45) | public ArrSoundtrackImportParser(ArrSoundtrackImportSettings settings,...
    method ParseResponse (line 59) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ParseResponseAsync (line 61) | public async Task<List<ImportListItemInfo>> ParseResponseAsync(ImportL...
    method ResolveTagIdsAsync (line 97) | private async Task<HashSet<int>?> ResolveTagIdsAsync()
    method DeserializeMediaItemsAsync (line 136) | private static async IAsyncEnumerable<ArrMedia?> DeserializeMediaItems...
    method ProcessMediaItem (line 145) | private async Task<List<ImportListItemInfo>> ProcessMediaItem(ArrMedia...
    method FetchSoundtracksForMedia (line 158) | private async Task<MediaProcessingResult> FetchSoundtracksForMedia(Arr...
    method SearchMusicBrainzSoundtracks (line 209) | private async Task<List<MusicBrainzSearchItem>> SearchMusicBrainzSound...
    method BuildSearchUrl (line 230) | private string BuildSearchUrl(string title)
    method ParseSearchResponse (line 253) | private List<MusicBrainzSearchItem> ParseSearchResponse(string xmlCont...
    method FetchAlbumDetails (line 271) | private async Task<MusicBrainzAlbumItem?> FetchAlbumDetails(string alb...
    method ParseAlbumDetails (line 290) | private MusicBrainzAlbumItem? ParseAlbumDetails(string xmlContent, str...
    method IsGoodMatch (line 309) | private bool IsGoodMatch(MusicBrainzAlbumItem album, string originalTi...
    method ContainsMovieNameAndSoundtrack (line 333) | private static bool ContainsMovieNameAndSoundtrack(string releaseTitle...
    method NormalizeTitle (line 342) | private static string NormalizeTitle(string title)
    method EscapeLuceneQuery (line 361) | private static string EscapeLuceneQuery(string query)
    method CreateImportItem (line 374) | private static ImportListItemInfo CreateImportItem(MusicBrainzSearchIt...
    method GenerateMediaCacheKey (line 383) | private static string GenerateMediaCacheKey(ArrMedia media) => $"media...
    method GenerateAlbumDetailsCacheKey (line 385) | private static string GenerateAlbumDetailsCacheKey(string albumId) => ...
    method GenerateStringHash (line 387) | private static string GenerateStringHash(string input)
    class MediaProcessingResult (line 400) | private class MediaProcessingResult
    method NormalizeTitleEmptyRegex (line 408) | [GeneratedRegex(@"[^a-zA-Z0-9\s]")]
    method NormalizeTitleSpaceRegex (line 411) | [GeneratedRegex(@"\s+")]

FILE: Tubifarry/ImportLists/ArrStack/ArrSoundtrackImportSettings.cs
  class ArrSoundtrackImportSettingsValidator (line 9) | public class ArrSoundtrackImportSettingsValidator : AbstractValidator<Ar...
    method ArrSoundtrackImportSettingsValidator (line 11) | public ArrSoundtrackImportSettingsValidator()
  class ArrSoundtrackImportSettings (line 70) | public class ArrSoundtrackImportSettings : IImportListSettings
    method Validate (line 104) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/ImportLists/ArrStack/ArrSoundtrackRequestGenerator.cs
  class ArrSoundtrackRequestGenerator (line 9) | internal class ArrSoundtrackRequestGenerator(ArrSoundtrackImportSettings...
    method GetListItems (line 13) | public ImportListPageableRequestChain GetListItems()
    method GetPagedRequests (line 20) | private IEnumerable<ImportListRequest> GetPagedRequests()

FILE: Tubifarry/ImportLists/LastFmRecommendation/LastFmRecomendRequestGenerator.cs
  class LastFmRecomendRequestGenerator (line 7) | public class LastFmRecomendRequestGenerator(LastFmRecommendSettings sett...
    method GetListItems (line 11) | public virtual ImportListPageableRequestChain GetListItems()
    method GetPagedRequests (line 20) | private IEnumerable<ImportListRequest> GetPagedRequests()

FILE: Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommend.cs
  class LastFmRecommend (line 14) | internal class LastFmRecommend : HttpImportListBase<LastFmRecommendSetti...
    method LastFmRecommend (line 24) | public LastFmRecommend(IHttpClient httpClient, IImportListStatusServic...
    method GetRequestGenerator (line 26) | public override IImportListRequestGenerator GetRequestGenerator() => n...
    method GetParser (line 28) | public override IParseImportListResponse GetParser() => new LastFmReco...
    method Test (line 30) | protected override void Test(List<ValidationFailure> failures)
    method TestConnection (line 35) | protected override ValidationFailure? TestConnection()
    method IsJsonContentType (line 82) | private static bool IsJsonContentType(string mediaType) => mediaType.E...

FILE: Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommendParser.cs
  class LastFmRecommendParser (line 13) | internal class LastFmRecommendParser : IParseImportListResponse
    method LastFmRecommendParser (line 20) | public LastFmRecommendParser(LastFmRecommendSettings settings, IHttpCl...
    method ParseResponse (line 27) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ProcessTopAlbumsResponse (line 58) | private List<ImportListItemInfo> ProcessTopAlbumsResponse(LastFmTopRes...
    method ProcessTopArtistsResponse (line 100) | private List<ImportListItemInfo> ProcessTopArtistsResponse(LastFmTopRe...
    method ProcessTopTracksResponse (line 136) | private List<ImportListItemInfo> ProcessTopTracksResponse(LastFmTopRes...
    method GroupAndSortSimilarArtists (line 176) | private static List<(LastFmArtist artist, int count, int bestRank)> Gr...
    method FetchTopAlbumsForArtist (line 208) | private List<LastFmAlbum> FetchTopAlbumsForArtist(LastFmArtist artist)
    method ConvertAlbumToImportListItems (line 216) | private static ImportListItemInfo ConvertAlbumToImportListItems(LastFm...
    method BuildRequest (line 224) | private HttpRequest BuildRequest(string method, Dictionary<string, str...
    method FetchImportListResponse (line 241) | protected virtual ImportListResponse FetchImportListResponse(HttpReque...
    method PreProcess (line 247) | protected virtual bool PreProcess(ImportListResponse importListResponse)

FILE: Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommendSettings.cs
  class LastFmRecommendSettingsValidator (line 9) | public class LastFmRecommendSettingsValidator : AbstractValidator<LastFm...
    method LastFmRecommendSettingsValidator (line 11) | public LastFmRecommendSettingsValidator()
  class LastFmRecommendSettings (line 35) | public class LastFmRecommendSettings : IImportListSettings
    method LastFmRecommendSettings (line 39) | public LastFmRecommendSettings()
    method Validate (line 70) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type LastFmRecommendMethodList (line 73) | public enum LastFmRecommendMethodList

FILE: Tubifarry/ImportLists/LastFmRecommendation/LastFmRecords.cs
  type LastFmTopResponse (line 6) | public record LastFmTopResponse(
  type LastFmTrackList (line 12) | public record LastFmTrackList(
  type LastFmTrack (line 16) | public record LastFmTrack(
  type LastFmSimilarArtistsResponse (line 23) | public record LastFmSimilarArtistsResponse(
  type LastFmSimilarTracksResponse (line 27) | public record LastFmSimilarTracksResponse(
  type LastFmTopAlbumsResponse (line 31) | public record LastFmTopAlbumsResponse(

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsImportList.cs
  class ListenBrainzCFRecommendationsImportList (line 14) | public class ListenBrainzCFRecommendationsImportList : HttpImportListBas...
    method ListenBrainzCFRecommendationsImportList (line 22) | public ListenBrainzCFRecommendationsImportList(IHttpClient httpClient,
    method GetRequestGenerator (line 29) | public override IImportListRequestGenerator GetRequestGenerator() =>
    method GetParser (line 32) | public override IParseImportListResponse GetParser() =>
    method IsValidRelease (line 35) | protected override bool IsValidRelease(ImportListItemInfo release) =>
    method Test (line 40) | protected override void Test(List<ValidationFailure> failures) =>
    method TestConnection (line 43) | protected override ValidationFailure TestConnection()

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsParser.cs
  class ListenBrainzCFRecommendationsParser (line 11) | public class ListenBrainzCFRecommendationsParser : IParseImportListResponse
    method ListenBrainzCFRecommendationsParser (line 15) | public ListenBrainzCFRecommendationsParser()
    method ParseResponse (line 20) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ParseRecordingRecommendations (line 38) | private List<ImportListItemInfo> ParseRecordingRecommendations(string ...
    method GetJsonOptions (line 61) | private static JsonSerializerOptions GetJsonOptions() => new()
    method PreProcess (line 66) | private bool PreProcess(ImportListResponse importListResponse)

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsRequestGenerator.cs
  class ListenBrainzCFRecommendationsRequestGenerator (line 6) | public class ListenBrainzCFRecommendationsRequestGenerator : IImportList...
    method ListenBrainzCFRecommendationsRequestGenerator (line 11) | public ListenBrainzCFRecommendationsRequestGenerator(ListenBrainzCFRec...
    method GetListItems (line 13) | public virtual ImportListPageableRequestChain GetListItems()
    method GetPagedRequests (line 20) | private IEnumerable<ImportListRequest> GetPagedRequests()
    method CreateRequest (line 29) | private ImportListRequest CreateRequest(int offset, int count)

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsSettings.cs
  class ListenBrainzCFRecommendationsSettingsValidator (line 8) | public class ListenBrainzCFRecommendationsSettingsValidator : AbstractVa...
    method ListenBrainzCFRecommendationsSettingsValidator (line 10) | public ListenBrainzCFRecommendationsSettingsValidator()
  class ListenBrainzCFRecommendationsSettings (line 27) | public class ListenBrainzCFRecommendationsSettings : IImportListSettings
    method ListenBrainzCFRecommendationsSettings (line 31) | public ListenBrainzCFRecommendationsSettings()
    method Validate (line 52) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistImportList.cs
  class ListenBrainzCreatedForPlaylistImportList (line 14) | public class ListenBrainzCreatedForPlaylistImportList : HttpImportListBa...
    method ListenBrainzCreatedForPlaylistImportList (line 22) | public ListenBrainzCreatedForPlaylistImportList(IHttpClient httpClient,
    method GetRequestGenerator (line 29) | public override IImportListRequestGenerator GetRequestGenerator() =>
    method GetParser (line 32) | public override IParseImportListResponse GetParser() =>
    method IsValidRelease (line 35) | protected override bool IsValidRelease(ImportListItemInfo release) =>
    method Test (line 40) | protected override void Test(List<ValidationFailure> failures) =>
    method TestConnection (line 43) | protected override ValidationFailure TestConnection()

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistParser.cs
  class ListenBrainzCreatedForPlaylistParser (line 12) | public class ListenBrainzCreatedForPlaylistParser : IParseImportListResp...
    method ListenBrainzCreatedForPlaylistParser (line 20) | public ListenBrainzCreatedForPlaylistParser(ListenBrainzCreatedForPlay...
    method ParseResponse (line 30) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ParseCreatedForPlaylists (line 48) | private IList<ImportListItemInfo> ParseCreatedForPlaylists(string cont...
    method IsTargetPlaylistType (line 92) | private bool IsTargetPlaylistType(PlaylistInfo playlist, string target...
    method ExtractPlaylistMbid (line 115) | private static string ExtractPlaylistMbid(string identifier) =>
    method FetchPlaylistItems (line 118) | private List<ImportListItemInfo> FetchPlaylistItems(string mbid)
    method ExtractAlbumInfo (line 167) | private ImportListItemInfo? ExtractAlbumInfo(TrackData track)
    method ExtractArtistMbid (line 193) | private string? ExtractArtistMbid(TrackData track)
    method PreProcess (line 224) | private static bool PreProcess(ImportListResponse importListResponse)

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistRequestGenerator.cs
  class ListenBrainzCreatedForPlaylistRequestGenerator (line 6) | public class ListenBrainzCreatedForPlaylistRequestGenerator : IImportLis...
    method ListenBrainzCreatedForPlaylistRequestGenerator (line 11) | public ListenBrainzCreatedForPlaylistRequestGenerator(ListenBrainzCrea...
    method GetListItems (line 13) | public virtual ImportListPageableRequestChain GetListItems()
    method GetPagedRequests (line 20) | private IEnumerable<ImportListRequest> GetPagedRequests() => (List<Imp...
    method CreateRequest (line 25) | private ImportListRequest CreateRequest(int offset, int count)
    method GetPlaylistTypeName (line 44) | public string GetPlaylistTypeName() => _settings.PlaylistType switch

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistSettings.cs
  class ListenBrainzCreatedForPlaylistSettingsValidator (line 8) | public class ListenBrainzCreatedForPlaylistSettingsValidator : AbstractV...
    method ListenBrainzCreatedForPlaylistSettingsValidator (line 10) | public ListenBrainzCreatedForPlaylistSettingsValidator()
  class ListenBrainzCreatedForPlaylistSettings (line 22) | public class ListenBrainzCreatedForPlaylistSettings : IImportListSettings
    method ListenBrainzCreatedForPlaylistSettings (line 26) | public ListenBrainzCreatedForPlaylistSettings()
    method Validate (line 47) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type ListenBrainzPlaylistType (line 50) | public enum ListenBrainzPlaylistType

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistImportList.cs
  class ListenBrainzPlaylistImportList (line 16) | public class ListenBrainzPlaylistImportList : HttpImportListBase<ListenB...
    method ListenBrainzPlaylistImportList (line 26) | public ListenBrainzPlaylistImportList(IHttpClient httpClient,
    method GetRequestGenerator (line 33) | public override IImportListRequestGenerator GetRequestGenerator() =>
    method GetParser (line 36) | public override IParseImportListResponse GetParser() =>
    method IsValidRelease (line 39) | protected override bool IsValidRelease(ImportListItemInfo release) =>
    method TestConnection (line 44) | protected override ValidationFailure TestConnection()
    method RequestAction (line 72) | public override object RequestAction(string action, IDictionary<string...
    method FetchAvailablePlaylists (line 104) | private List<dynamic> FetchAvailablePlaylists()
    method ExtractPlaylistMbid (line 161) | private static string ExtractPlaylistMbid(string identifier) =>
    method GetJsonOptions (line 164) | private static JsonSerializerOptions GetJsonOptions() => new()
    method FetchTrackLevelItems (line 169) | public List<PlaylistItem> FetchTrackLevelItems()

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistParser.cs
  class ListenBrainzPlaylistParser (line 12) | public class ListenBrainzPlaylistParser : IParseImportListResponse
    method ListenBrainzPlaylistParser (line 16) | public ListenBrainzPlaylistParser(ListenBrainzPlaylistSettings settings)
    method ParseResponse (line 21) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ParsePlaylistTracks (line 39) | private List<ImportListItemInfo> ParsePlaylistTracks(string content)
    method ExtractAlbumInfo (line 61) | private ImportListItemInfo? ExtractAlbumInfo(TrackData track)
    method ParseTrackLevelItems (line 87) | public List<PlaylistItem> ParseTrackLevelItems(string content)
    method ToPlaylistItem (line 109) | private PlaylistItem? ToPlaylistItem(TrackData track)
    method ExtractArtistMbid (line 128) | private string? ExtractArtistMbid(TrackData track)
    method GetJsonOptions (line 159) | private static JsonSerializerOptions GetJsonOptions() => new()
    method PreProcess (line 164) | private static bool PreProcess(ImportListResponse importListResponse)

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistRequestGenerator.cs
  class ListenBrainzPlaylistRequestGenerator (line 6) | public class ListenBrainzPlaylistRequestGenerator(ListenBrainzPlaylistSe...
    method GetListItems (line 10) | public virtual ImportListPageableRequestChain GetListItems()
    method CreatePlaylistRequest (line 30) | public ImportListRequest CreatePlaylistRequest(string playlistId)
    method GetEndpointUrl (line 46) | public string GetEndpointUrl()
    method CreateDiscoveryRequest (line 58) | public ImportListRequest CreateDiscoveryRequest(int count = 100, int o...

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistSettings.cs
  class ListenBrainzPlaylistSettingsValidator (line 8) | public class ListenBrainzPlaylistSettingsValidator : AbstractValidator<L...
    method ListenBrainzPlaylistSettingsValidator (line 10) | public ListenBrainzPlaylistSettingsValidator()
  class ListenBrainzPlaylistSettings (line 18) | public class ListenBrainzPlaylistSettings : IImportListSettings
    method ListenBrainzPlaylistSettings (line 22) | public ListenBrainzPlaylistSettings()
    method Validate (line 42) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type ListenBrainzPlaylistEndpointType (line 45) | public enum ListenBrainzPlaylistEndpointType

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzRecords.cs
  type ArtistStatsResponse (line 7) | public record ArtistStatsResponse(
  type ArtistStatsPayload (line 10) | public record ArtistStatsPayload(
  type ArtistStat (line 16) | public record ArtistStat(
  type ReleaseStatsResponse (line 21) | public record ReleaseStatsResponse(
  type ReleaseStatsPayload (line 24) | public record ReleaseStatsPayload(
  type ReleaseStat (line 30) | public record ReleaseStat(
  type ReleaseGroupStatsResponse (line 37) | public record ReleaseGroupStatsResponse(
  type ReleaseGroupStatsPayload (line 40) | public record ReleaseGroupStatsPayload(
  type ReleaseGroupStat (line 46) | public record ReleaseGroupStat(
  type RecordingRecommendationResponse (line 54) | public record RecordingRecommendationResponse(
  type RecordingRecommendationPayload (line 57) | public record RecordingRecommendationPayload(
  type RecordingRecommendation (line 63) | public record RecordingRecommendation(
  type PlaylistsResponse (line 68) | public record PlaylistsResponse(
  type PlaylistInfo (line 71) | public record PlaylistInfo(
  type PlaylistData (line 74) | public record PlaylistData(
  type PlaylistResponse (line 79) | public record PlaylistResponse(
  type PlaylistResponseData (line 82) | public record PlaylistResponseData(
  type TrackData (line 85) | public record TrackData(

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsImportList.cs
  class ListenBrainzUserStatsImportList (line 14) | public class ListenBrainzUserStatsImportList : HttpImportListBase<Listen...
    method ListenBrainzUserStatsImportList (line 22) | public ListenBrainzUserStatsImportList(IHttpClient httpClient,
    method GetRequestGenerator (line 29) | public override IImportListRequestGenerator GetRequestGenerator() =>
    method GetParser (line 32) | public override IParseImportListResponse GetParser() =>
    method IsValidRelease (line 35) | protected override bool IsValidRelease(ImportListItemInfo release) =>
    method Test (line 40) | protected override void Test(List<ValidationFailure> failures) =>
    method TestConnection (line 43) | protected override ValidationFailure TestConnection()

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsParser.cs
  class ListenBrainzUserStatsParser (line 11) | public class ListenBrainzUserStatsParser : IParseImportListResponse
    method ListenBrainzUserStatsParser (line 17) | public ListenBrainzUserStatsParser(ListenBrainzUserStatsSettings setti...
    method ParseResponse (line 23) | public IList<ImportListItemInfo> ParseResponse(ImportListResponse impo...
    method ParseArtistStats (line 48) | private List<ImportListItemInfo> ParseArtistStats(string content)
    method ParseReleaseStats (line 70) | private List<ImportListItemInfo> ParseReleaseStats(string content)
    method ParseReleaseGroupStats (line 94) | private List<ImportListItemInfo> ParseReleaseGroupStats(string content)
    method PreProcess (line 117) | private bool PreProcess(ImportListResponse importListResponse)

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsRequestGenerator.cs
  class ListenBrainzUserStatsRequestGenerator (line 6) | public class ListenBrainzUserStatsRequestGenerator(ListenBrainzUserStats...
    method GetListItems (line 11) | public virtual ImportListPageableRequestChain GetListItems()
    method GetPagedRequests (line 18) | private IEnumerable<ImportListRequest> GetPagedRequests()
    method CreateRequest (line 27) | private ImportListRequest CreateRequest(int offset, int count)
    method GetEndpoint (line 48) | private string GetEndpoint() => _settings.StatType switch
    method GetTimeRange (line 56) | private string GetTimeRange() => _settings.Range switch

FILE: Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsSettings.cs
  class ListenBrainzUserStatsSettingsValidator (line 8) | public class ListenBrainzUserStatsSettingsValidator : AbstractValidator<...
    method ListenBrainzUserStatsSettingsValidator (line 10) | public ListenBrainzUserStatsSettingsValidator()
  class ListenBrainzUserStatsSettings (line 27) | public class ListenBrainzUserStatsSettings : IImportListSettings
    method ListenBrainzUserStatsSettings (line 31) | public ListenBrainzUserStatsSettings()
    method Validate (line 60) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type ListenBrainzStatType (line 63) | public enum ListenBrainzStatType
  type ListenBrainzTimeRange (line 75) | public enum ListenBrainzTimeRange

FILE: Tubifarry/ImportLists/Spotify/SpotifyUserPlaylistImport.cs
  class SpotifyUserPlaylistImport (line 16) | public class SpotifyUserPlaylistImport : SpotifyImportListBase<SpotifyUs...
    method SpotifyUserPlaylistImport (line 27) | public SpotifyUserPlaylistImport(
    method Fetch (line 51) | public override IList<SpotifyImportListItemInfo> Fetch(SpotifyWebAPI api)
    method ProcessPlaylists (line 91) | private void ProcessPlaylists(SpotifyWebAPI api, Paging<SimplePlaylist...
    method ProcessPlaylistWithCache (line 137) | private void ProcessPlaylistWithCache(SpotifyWebAPI api, SimplePlaylis...
    method ProcessPlaylistTracks (line 156) | private void ProcessPlaylistTracks(SpotifyWebAPI api, Paging<PlaylistT...
    method FetchTrackLevelItems (line 172) | public List<PlaylistItem> FetchTrackLevelItems()
    method CollectTrackItems (line 204) | private void CollectTrackItems(SpotifyWebAPI api, Paging<SimplePlaylis...
    method AppendTrackItems (line 223) | private void AppendTrackItems(SpotifyWebAPI api, Paging<PlaylistTrack>...
    class CachedPlaylistData (line 256) | private class CachedPlaylistData
    method GenerateCacheKey (line 262) | private static string GenerateCacheKey(string playlistId, string usern...
    method GetUserPlaylistsWithRetry (line 270) | private Paging<SimplePlaylist> GetUserPlaylistsWithRetry(SpotifyWebAPI...
    method GetPlaylistTracksWithRetry (line 292) | private Paging<PlaylistTrack> GetPlaylistTracksWithRetry(SpotifyWebAPI...
    method GetNextPageWithRetry (line 314) | private Paging<T> GetNextPageWithRetry<T>(SpotifyWebAPI api, Paging<T>...
    method Throttle (line 336) | private static void Throttle()
    method CalculateRateLimitDelay (line 355) | private static int CalculateRateLimitDelay(int retryCount)
    method ParsePlaylistTrack (line 363) | private SpotifyImportListItemInfo? ParsePlaylistTrack(PlaylistTrack pl...

FILE: Tubifarry/ImportLists/Spotify/SpotifyUserPlaylistImportSettings.cs
  class SpotifyUserPlaylistImportSettingsValidator (line 7) | public class SpotifyUserPlaylistImportSettingsValidator : SpotifySetting...
    method SpotifyUserPlaylistImportSettingsValidator (line 9) | public SpotifyUserPlaylistImportSettingsValidator() : base()
  class SpotifyUserPlaylistImportSettings (line 25) | public class SpotifyUserPlaylistImportSettings : SpotifySettingsBase<Spo...

FILE: Tubifarry/Indexers/DABMusic/DABMusicIndexer.cs
  class DABMusicIndexer (line 11) | public class DABMusicIndexer : HttpIndexerBase<DABMusicIndexerSettings>
    method DABMusicIndexer (line 26) | public DABMusicIndexer(
    method Test (line 42) | protected override async Task Test(List<ValidationFailure> failures)
    method GetRequestGenerator (line 64) | public override IIndexerRequestGenerator GetRequestGenerator()
    method GetParser (line 70) | public override IParseIndexerResponse GetParser() => _parser;

FILE: Tubifarry/Indexers/DABMusic/DABMusicIndexerSettings.cs
  class DABMusicIndexerSettingsValidator (line 8) | public class DABMusicIndexerSettingsValidator : AbstractValidator<DABMus...
    method DABMusicIndexerSettingsValidator (line 10) | public DABMusicIndexerSettingsValidator()
  class DABMusicIndexerSettings (line 37) | public class DABMusicIndexerSettings : IIndexerSettings
    method DABMusicIndexerSettings (line 41) | public DABMusicIndexerSettings()
    method Validate (line 68) | public NzbDroneValidationResult Validate() => new(_validator.Validate(...

FILE: Tubifarry/Indexers/DABMusic/DABMusicParser.cs
  type IDABMusicParser (line 11) | public interface IDABMusicParser : IParseIndexerResponse
  class DABMusicParser (line 14) | public class DABMusicParser : IDABMusicParser
    method DABMusicParser (line 18) | public DABMusicParser(Logger logger) => _logger = logger;
    method ParseResponse (line 20) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method CreateAlbumData (line 42) | private static AlbumData CreateAlbumData(DABMusicAlbum album)
    method CreateTrackData (line 64) | private static AlbumData CreateTrackData(DABMusicTrack track)
    method SanitizeForUrl (line 87) | private static string SanitizeForUrl(string input) => input.ToLowerInv...
    method GetQuality (line 95) | private static (AudioFormat Format, int Bitrate, int BitDepth) GetQual...

FILE: Tubifarry/Indexers/DABMusic/DABMusicRecords.cs
  type DABMusicRequestData (line 11) | public record DABMusicRequestData(
  type DABMusicSearchResponse (line 19) | public record DABMusicSearchResponse(
  type DABMusicPagination (line 27) | public record DABMusicPagination(
  type DABMusicAlbumDetailsResponse (line 37) | public record DABMusicAlbumDetailsResponse(
  type DABMusicStreamResponse (line 43) | public record DABMusicStreamResponse(
  type DABMusicAlbum (line 53) | public record DABMusicAlbum(
  type DABMusicTrack (line 73) | public record DABMusicTrack(
  type DABMusicAudioQuality (line 101) | public record DABMusicAudioQuality(
  type DABMusicImages (line 109) | public record DABMusicImages(

FILE: Tubifarry/Indexers/DABMusic/DABMusicRequestGenerator.cs
  type IDABMusicRequestGenerator (line 9) | public interface IDABMusicRequestGenerator : IIndexerRequestGenerator
    method SetSetting (line 11) | public void SetSetting(DABMusicIndexerSettings settings);
  class DABMusicRequestGenerator (line 17) | public class DABMusicRequestGenerator(Logger logger, IDABMusicSessionMan...
    method GetRecentRequests (line 23) | public IndexerPageableRequestChain GetRecentRequests() => new();
    method GetSearchRequests (line 25) | public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriter...
    method GetSearchRequests (line 32) | public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCrite...
    method SetSetting (line 34) | public void SetSetting(DABMusicIndexerSettings settings) => _settings ...
    method Generate (line 36) | private IndexerPageableRequestChain Generate(string query, bool isSingle)
    method CreateRequest (line 60) | private IndexerRequest CreateRequest(string url, string baseUrl, strin...

FILE: Tubifarry/Indexers/DABMusic/DABMusicSessionHelper.cs
  type DABMusicSession (line 8) | public record DABMusicSession(string SessionCookie, DateTime ExpiryUtc, ...
  type DABMusicLoginRequest (line 14) | public record DABMusicLoginRequest(string Email, string Password);
  type DABMusicUser (line 15) | public record DABMusicUser(int Id, string Username, string Email);
  type IDABMusicSessionManager (line 17) | public interface IDABMusicSessionManager
    method GetOrCreateSession (line 19) | DABMusicSession? GetOrCreateSession(string baseUrl, string email, stri...
    method InvalidateSession (line 21) | void InvalidateSession(string email);
    method HasValidSession (line 23) | bool HasValidSession(string email);
  class DABMusicSessionHelper (line 26) | public class DABMusicSessionHelper(IHttpClient httpClient, Logger logger...
    method GetOrCreateSession (line 34) | public DABMusicSession? GetOrCreateSession(string baseUrl, string emai...
    method InvalidateSession (line 52) | public void InvalidateSession(string email)
    method HasValidSession (line 58) | public bool HasValidSession(string email) => _sessions.TryGetValue(ema...
    method Login (line 60) | private DABMusicSession? Login(string baseUrl, string email, string pa...
    method ExtractSessionCookie (line 102) | private string? ExtractSessionCookie(HttpResponse response)

FILE: Tubifarry/Indexers/DownloadProtocols.cs
  class YoutubeDownloadProtocol (line 3) | public class YoutubeDownloadProtocol : IDownloadProtocol { }
  class SoulseekDownloadProtocol (line 4) | public class SoulseekDownloadProtocol : IDownloadProtocol { }
  class LucidaDownloadProtocol (line 5) | public class LucidaDownloadProtocol : IDownloadProtocol { }
  class QobuzDownloadProtocol (line 6) | public class QobuzDownloadProtocol : IDownloadProtocol { }
  class SubSonicDownloadProtocol (line 7) | public class SubSonicDownloadProtocol : IDownloadProtocol { }
  class AmazonMusicDownloadProtocol (line 8) | public class AmazonMusicDownloadProtocol : IDownloadProtocol { }

FILE: Tubifarry/Indexers/Lucida/LucidaIndexer.cs
  class LucidaIndexer (line 12) | public partial class LucidaIndexer : HttpIndexerBase<LucidaIndexerSettings>
    method LucidaIndexer (line 29) | public LucidaIndexer(
    method Test (line 43) | protected override async Task Test(List<ValidationFailure> failures)
    method GetRequestGenerator (line 112) | public override IIndexerRequestGenerator GetRequestGenerator()
    method GetParser (line 118) | public override IParseIndexerResponse GetParser() => _parser;
    method LucidaHeaderRegex (line 120) | [GeneratedRegex("<title>.*?(Lucida|Music).*?</title>", RegexOptions.Ig...

FILE: Tubifarry/Indexers/Lucida/LucidaIndexerSettings.cs
  class LucidaIndexerSettingsValidator (line 8) | public class LucidaIndexerSettingsValidator : AbstractValidator<LucidaIn...
    method LucidaIndexerSettingsValidator (line 10) | public LucidaIndexerSettingsValidator()
  class LucidaIndexerSettings (line 33) | public class LucidaIndexerSettings : IIndexerSettings
    method LucidaIndexerSettings (line 49) | public LucidaIndexerSettings()
    method Validate (line 94) | public NzbDroneValidationResult Validate() => new(_validator.Validate(...

FILE: Tubifarry/Indexers/Lucida/LucidaRecords.cs
  type LucidaRequestData (line 11) | public record LucidaRequestData(
  type LucidaSearchResults (line 20) | public record LucidaSearchResults(
  type LucidaResultsContainer (line 26) | public record LucidaResultsContainer(
  type LucidaResultsData (line 33) | public record LucidaResultsData(
  type LucidaDataWrapper (line 42) | public record LucidaDataWrapper(
  type DataContainer (line 49) | public record DataContainer(
  type LucidaAlbum (line 63) | public record LucidaAlbum(
  type LucidaTrack (line 81) | public record LucidaTrack(
  type LucidaAlbumReference (line 103) | public record LucidaAlbumReference(
  type LucidaArtist (line 120) | public record LucidaArtist(
  type LucidaArtwork (line 132) | public record LucidaArtwork(
  type ServiceCountry (line 146) | public record ServiceCountry(
  type CountryResponse (line 156) | public record CountryResponse(
  type LucidaInfo (line 170) | public record LucidaInfo(
  type LucidaArtistInfo (line 211) | public record LucidaArtistInfo(
  type LucidaArtworkInfo (line 223) | public record LucidaArtworkInfo(
  type LucidaAlbumRef (line 235) | public record LucidaAlbumRef(
  type LucidaTrackInfo (line 248) | public record LucidaTrackInfo(
  type LucidaStats (line 277) | public record LucidaStats(
  class LucidaTrackModel (line 290) | public class LucidaTrackModel
    method GetBestCoverArtUrl (line 329) | public string GetBestCoverArtUrl()
    method FormatDuration (line 340) | public string FormatDuration()
  class LucidaAlbumModel (line 351) | public class LucidaAlbumModel
    method GetBestCoverArtUrl (line 385) | public string GetBestCoverArtUrl()
    method GetTotalDurationMs (line 396) | public long GetTotalDurationMs() => Tracks.Sum(t => t.DurationMs);
    method FormatTotalDuration (line 398) | public string FormatTotalDuration()
  type LucidaTokens (line 413) | public record LucidaTokens(string Primary, string Fallback, long Expiry)
  type LucidaDownloadRequestInfo (line 428) | public record LucidaDownloadRequestInfo(
  type LucidaAccountInfo (line 455) | public record LucidaAccountInfo(
  type LucidaUploadInfo (line 462) | public record LucidaUploadInfo(
  type LucidaTokenData (line 469) | public record LucidaTokenData(
  type LucidaDownloadResponse (line 477) | public record LucidaDownloadResponse(
  type LucidaStatsResponse (line 491) | public record LucidaStatsResponse(
  type LucidaStatusResponse (line 498) | public record LucidaStatusResponse(

FILE: Tubifarry/Indexers/Lucida/LucidaRequestGenerator.cs
  type ILucidaRequestGenerator (line 9) | public interface ILucidaRequestGenerator : IIndexerRequestGenerator
    method SetSetting (line 11) | public void SetSetting(LucidaIndexerSettings settings);
  class LucidaRequestGenerator (line 17) | public class LucidaRequestGenerator : ILucidaRequestGenerator
    method LucidaRequestGenerator (line 23) | public LucidaRequestGenerator(IHttpClient httpClient, Logger logger) =...
    method GetRecentRequests (line 25) | public IndexerPageableRequestChain GetRecentRequests() => new();
    method GetSearchRequests (line 27) | public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriter...
    method GetSearchRequests (line 31) | public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCrite...
    method SetSetting (line 33) | public void SetSetting(LucidaIndexerSettings settings) => _settings = ...
    method Generate (line 35) | private IndexerPageableRequestChain Generate(string query, bool isSingle)

FILE: Tubifarry/Indexers/Lucida/LucidaRequestParser.cs
  type ILucidaParser (line 13) | public interface ILucidaParser : IParseIndexerResponse
  class LucidaParser (line 16) | public partial class LucidaParser(Logger logger) : ILucidaParser
    method ParseResponse (line 22) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method GetRequestData (line 41) | private LucidaRequestData? GetRequestData(IndexerResponse indexerRespo...
    method ExtractSearchResults (line 54) | private (List<LucidaAlbum>? Albums, List<LucidaTrack>? Tracks) Extract...
    method ExtractWithJintToRecords (line 93) | private static (List<LucidaAlbum>? Albums, List<LucidaTrack>? Tracks) ...
    method ProcessResults (line 137) | private void ProcessResults(List<LucidaAlbum>? albums, List<LucidaTrac...
    method TryAdd (line 154) | private void TryAdd(Func<AlbumData> factory, List<ReleaseInfo> list, s...
    method CreateAlbumData (line 166) | private AlbumData CreateAlbumData(LucidaAlbum album, LucidaRequestData...
    method CreateTrackData (line 189) | private AlbumData CreateTrackData(LucidaTrack track, LucidaRequestData...
    method ProcessReleaseDate (line 212) | private static void ProcessReleaseDate(AlbumData albumData, string? re...
    method NormalizeJsonData (line 239) | private static string NormalizeJsonData(string js)
    method ReleaseDateDayRegex (line 248) | [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$")]
    method ReleaseDateYearRegex (line 251) | [GeneratedRegex("^\\d{4}$")]
    method ReleaseDateYear2Regex (line 254) | [GeneratedRegex("\\b(\\d{4})\\b")]
    method Data1Regex (line 257) | [GeneratedRegex(@"data\s*=\s*(\[(?:[^\[\]]|\[(?:[^\[\]]|\[(?:[^\[\]]|\...
    method Data2Regex (line 260) | [GeneratedRegex(@"__INITIAL_DATA__\s*=\s*({.+?});", RegexOptions.Compi...

FILE: Tubifarry/Indexers/Lucida/LucidaServiceHelper.cs
  class LucidaServiceHelper (line 14) | public static class LucidaServiceHelper
    method GetServicesAsync (line 47) | public static Task<Dictionary<string, List<ServiceCountry>>> GetServic...
    method HasAvailableServices (line 59) | public static bool HasAvailableServices(string baseUrl)
    method GetAvailableServices (line 72) | public static Dictionary<string, List<ServiceCountry>> GetAvailableSer...
    method GetServiceDisplayName (line 83) | public static string GetServiceDisplayName(string serviceValue)
    method GetServiceKey (line 91) | public static string? GetServiceKey(string displayName)
    method GetServiceQuality (line 97) | public static (AudioFormat Format, int Bitrate, int BitDepth) GetServi...
    method ClearCache (line 104) | public static void ClearCache(string baseUrl) => _cache.TryRemove(base...
    method ClearAllCaches (line 109) | public static void ClearAllCaches() => _cache.Clear();
    method FetchServicesAsync (line 111) | private static async Task<Dictionary<string, List<ServiceCountry>>> Fe...

FILE: Tubifarry/Indexers/Soulseek/ISlskdItemsParser.cs
  type ISlskdItemsParser (line 5) | public interface ISlskdItemsParser
    method ParseFolderName (line 7) | SlskdFolderData ParseFolderName(string folderPath);
    method CreateAlbumData (line 8) | AlbumData CreateAlbumData(string searchId, IGrouping<string, SlskdFile...

FILE: Tubifarry/Indexers/Soulseek/Search/Core/ISearchStrategy.cs
  type ISearchStrategy (line 3) | public interface ISearchStrategy
    method IsEnabled (line 8) | bool IsEnabled(SlskdSettings settings);
    method CanExecute (line 9) | bool CanExecute(SearchContext context, QueryType queryType);
    method GetQuery (line 10) | string? GetQuery(SearchContext context, QueryType queryType);
  class SearchStrategyBase (line 13) | public abstract class SearchStrategyBase : ISearchStrategy
    method IsEnabled (line 19) | public virtual bool IsEnabled(SlskdSettings settings) => true;
    method CanExecute (line 20) | public abstract bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 21) | public abstract string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Core/QueryAnalyzer.cs
  class QueryAnalyzer (line 5) | public static partial class QueryAnalyzer
    method Analyze (line 16) | public static QueryType Analyze(SearchContext context)
    method IsVariousArtists (line 44) | public static bool IsVariousArtists(string? artist)
    method IsSelfTitled (line 51) | public static bool IsSelfTitled(string? artist, string? album)
    method IsShortName (line 80) | public static bool IsShortName(string? album)
    method NeedsTypeDisambiguation (line 87) | public static bool NeedsTypeDisambiguation(SearchContext context)
    method HasVolumeReference (line 113) | public static bool HasVolumeReference(string? album) =>
    method HasStandaloneRomanNumeral (line 116) | public static bool HasStandaloneRomanNumeral(string? album)
    method NeedsNormalization (line 134) | public static bool NeedsNormalization(string? artist, string? album) =>
    method HasSpecialCharacters (line 138) | private static bool HasSpecialCharacters(string? text) =>
    method HasPunctuation (line 141) | private static bool HasPunctuation(string? text) =>
    method NormalizeName (line 144) | private static string NormalizeName(string name)
    method ArticleRegex (line 151) | [GeneratedRegex(@"\b(the|a|an)\s+", RegexOptions.IgnoreCase | RegexOpt...
    method CollapseWhitespaceRegex (line 154) | [GeneratedRegex(@"\s+")]
    method PunctuationRegex (line 157) | [GeneratedRegex(@"[^\w\s]", RegexOptions.Compiled)]
    method SpecialCharRegex (line 160) | [GeneratedRegex(@"[àáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓ...
    method StandaloneRomanRegex (line 163) | [GeneratedRegex(@"\b([IVXLCDM]{1,4})\b", RegexOptions.Compiled)]
    method VolumeRegex (line 166) | [GeneratedRegex(@"\b(?:Vol(?:ume)?\.?)\s*([0-9]+|[IVXLCDM]+)\b", Regex...

FILE: Tubifarry/Indexers/Soulseek/Search/Core/SearchContext.cs
  type QueryType (line 7) | [Flags]
  type SearchContext (line 20) | public sealed record SearchContext
  type SearchQuery (line 82) | public sealed record SearchQuery
  type SearchTier (line 106) | public enum SearchTier

FILE: Tubifarry/Indexers/Soulseek/Search/Core/SearchPipeline.cs
  type ISlskdSearchChain (line 8) | public interface ISlskdSearchChain
    method BuildChain (line 10) | LazyIndexerPageableRequestChain BuildChain(SearchContext context, Sear...
  class SearchPipeline (line 13) | public sealed class SearchPipeline : ISlskdSearchChain
    method SearchPipeline (line 18) | public SearchPipeline(IEnumerable<ISearchStrategy> strategies, Logger ...
    method BuildChain (line 30) | public LazyIndexerPageableRequestChain BuildChain(SearchContext contex...
    method ApplyNormalization (line 62) | private SearchContext ApplyNormalization(SearchContext context, QueryT...
    method ExecuteStrategy (line 75) | private IEnumerable<IndexerRequest> ExecuteStrategy(

FILE: Tubifarry/Indexers/Soulseek/Search/Strategies/BaseSearchStrategy.cs
  class BaseSearchStrategy (line 6) | public sealed class BaseSearchStrategy : SearchStrategyBase
    method CanExecute (line 12) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 25) | public override string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Strategies/FallbackStrategy.cs
  class WildcardStrategy (line 6) | public sealed class WildcardStrategy : SearchStrategyBase
    method IsEnabled (line 12) | public override bool IsEnabled(SlskdSettings settings) => settings.Use...
    method CanExecute (line 14) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 21) | public override string? GetQuery(SearchContext context, QueryType quer...
  class PartialAlbumStrategy (line 40) | public sealed class PartialAlbumStrategy : SearchStrategyBase
    method IsEnabled (line 46) | public override bool IsEnabled(SlskdSettings settings) => settings.Use...
    method CanExecute (line 48) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 52) | public override string? GetQuery(SearchContext context, QueryType quer...
  class AliasStrategy (line 62) | public sealed class AliasStrategy : SearchStrategyBase
    method IsEnabled (line 70) | public override bool IsEnabled(SlskdSettings settings) => settings.Use...
    method CanExecute (line 72) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 77) | public override string? GetQuery(SearchContext context, QueryType quer...
  class TrackFallbackStrategy (line 91) | public sealed class TrackFallbackStrategy : SearchStrategyBase
    method IsEnabled (line 99) | public override bool IsEnabled(SlskdSettings settings) => settings.Use...
    method CanExecute (line 101) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 105) | public override string? GetQuery(SearchContext context, QueryType quer...
  class DistinctiveAlbumStrategy (line 121) | public sealed class DistinctiveAlbumStrategy : SearchStrategyBase
    method IsEnabled (line 127) | public override bool IsEnabled(SlskdSettings settings) => settings.Use...
    method CanExecute (line 129) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 133) | public override string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Strategies/SpecialCaseStrategy.cs
  class VariousArtistsStrategy (line 6) | public sealed class VariousArtistsStrategy : SearchStrategyBase
    method CanExecute (line 12) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 16) | public override string? GetQuery(SearchContext context, QueryType quer...
  class SelfTitledStrategy (line 28) | public sealed class SelfTitledStrategy : SearchStrategyBase
    method CanExecute (line 34) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 39) | public override string? GetQuery(SearchContext context, QueryType quer...
  class ShortNameStrategy (line 51) | public sealed class ShortNameStrategy : SearchStrategyBase
    method CanExecute (line 57) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 62) | public override string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Strategies/TemplateSearchStrategy.cs
  class TemplateSearchStrategy (line 12) | public sealed class TemplateSearchStrategy : SearchStrategyBase
    method IsEnabled (line 18) | public override bool IsEnabled(SlskdSettings settings) =>
    method CanExecute (line 21) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 24) | public override string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Strategies/VariationStrategy.cs
  class VolumeVariationStrategy (line 6) | public sealed class VolumeVariationStrategy : SearchStrategyBase
    method IsEnabled (line 12) | public override bool IsEnabled(SlskdSettings settings) => settings.Han...
    method CanExecute (line 14) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 18) | public override string? GetQuery(SearchContext context, QueryType quer...
  class RomanNumeralVariationStrategy (line 28) | public sealed class RomanNumeralVariationStrategy : SearchStrategyBase
    method IsEnabled (line 34) | public override bool IsEnabled(SlskdSettings settings) => settings.Han...
    method CanExecute (line 36) | public override bool CanExecute(SearchContext context, QueryType query...
    method GetQuery (line 40) | public override string? GetQuery(SearchContext context, QueryType quer...

FILE: Tubifarry/Indexers/Soulseek/Search/Templates/TemplateEngine.cs
  class TemplateEngine (line 9) | public static partial class TemplateEngine
    method ParseTemplates (line 18) | public static IReadOnlyList<string> ParseTemplates(string? templateCon...
    method ValidateTemplates (line 31) | public static IReadOnlyList<string> ValidateTemplates(string? template...
    method ValidatePath (line 57) | private static string? ValidatePath(string path)
    method Apply (line 89) | public static string? Apply(string template, object? context)
    method ResolvePath (line 116) | private static object? ResolvePath(object? obj, string path)
    method GetValue (line 144) | private static object? GetValue(object obj, string name)
    method GetIndexed (line 157) | private static object? GetIndexed(object? col, int idx) => col switch
    method GetCachedProperty (line 165) | private static PropertyInfo? GetCachedProperty(Type type, string name)
    method GetCachedField (line 169) | private static FieldInfo? GetCachedField(Type type, string name)
    method ClearCaches (line 173) | public static void ClearCaches()
    method CreatePlaceholderRegex (line 179) | [GeneratedRegex(@"\{\{([^}]+)\}\}", RegexOptions.Compiled)]
    method CreateIndexerRegex (line 182) | [GeneratedRegex(@"^(\w+)\[(\d+)\]$", RegexOptions.Compiled)]
    method MultiSpaceRegex (line 185) | [GeneratedRegex(@"\s{2,}", RegexOptions.Compiled)]

FILE: Tubifarry/Indexers/Soulseek/Search/Transformers/QueryBuilder.cs
  class QueryBuilder (line 5) | public static partial class QueryBuilder
    method Build (line 18) | public static string Build(params string?[] parts) =>
    method DeduplicateTerms (line 21) | public static string DeduplicateTerms(string? text)
    method BuildTrimmed (line 57) | public static string BuildTrimmed(string? text)
    method BuildPartial (line 71) | public static string? BuildPartial(string? text)
    method ExtractDistinctive (line 103) | public static string ExtractDistinctive(string? text, int maxWords = 3)
    method ConvertVolumeFormat (line 129) | public static string? ConvertVolumeFormat(string? album)
    method ConvertRomanNumeral (line 153) | public static string? ConvertRomanNumeral(string? album)
    method ConvertNumber (line 195) | private static string ConvertNumber(string number)
    method ParenthesesRegex (line 210) | [GeneratedRegex(@"\([^)]*\)|\[[^\]]*\]", RegexOptions.Compiled)]
    method StandaloneRomanRegex (line 212) | [GeneratedRegex(@"\b([IVXLCDM]{1,4})\b", RegexOptions.Compiled)]
    method VolumeRegex (line 214) | [GeneratedRegex(@"\b(Vol(?:ume)?\.?)\s*([0-9]+|[IVXLCDM]+)\b", RegexOp...

FILE: Tubifarry/Indexers/Soulseek/Search/Transformers/QueryNormalizer.cs
  class QueryNormalizer (line 8) | public static partial class QueryNormalizer
    method Normalize (line 10) | public static SearchContext Normalize(SearchContext context)
    method NormalizeText (line 31) | public static string NormalizeText(string? input)
    method PunctuationRegex (line 61) | [GeneratedRegex(@"[^\w\s\-&]", RegexOptions.Compiled)]
    method PlusRegex (line 64) | [GeneratedRegex(@"\+")]
    method WhitespaceRegex (line 67) | [GeneratedRegex(@"\s+")]

FILE: Tubifarry/Indexers/Soulseek/SlsdkRecords.cs
  type SlskdSearchResponse (line 7) | public record SlskdSearchResponse(
  type SlskdLockedFile (line 20) | public record SlskdLockedFile(
  type SlskdFileData (line 24) | public record SlskdFileData(
  type SlskdFolderData (line 53) | public record SlskdFolderData(
  type SlskdSearchData (line 131) | public record SlskdSearchData(
  type SlskdDirectoryApiResponse (line 143) | public record SlskdDirectoryApiResponse(
  type SlskdDirectoryApiFile (line 147) | public record SlskdDirectoryApiFile(

FILE: Tubifarry/Indexers/Soulseek/SlskdIndexer.cs
  class SlskdIndexer (line 21) | public class SlskdIndexer : ExtendedHttpIndexerBase<SlskdSettings, LazyI...
    method SlskdIndexer (line 35) | public SlskdIndexer(IHttpClient httpClient, Lazy<IIndexerFactory> inde...
    method CleanupReleases (line 42) | protected override IList<ReleaseInfo> CleanupReleases(IEnumerable<Rele...
    method Test (line 59) | protected override async Task Test(List<ValidationFailure> failures) =...
    method GetExtendedRequestGenerator (line 61) | public override IIndexerRequestGenerator<LazyIndexerPageableRequest> G...
    method GetParser (line 63) | public override IParseIndexerResponse GetParser() => _parseIndexerResp...
    method TestConnection (line 65) | protected override async Task<ValidationFailure> TestConnection()

FILE: Tubifarry/Indexers/Soulseek/SlskdIndexerParser.cs
  class SlskdIndexerParser (line 20) | public class SlskdIndexerParser : IParseIndexerResponse, IHandle<AlbumGr...
    method SlskdIndexerParser (line 40) | public SlskdIndexerParser(SlskdIndexer indexer, Lazy<IIndexerFactory> ...
    method ParseResponse (line 53) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method TryExpandDirectory (line 139) | private IGrouping<string, SlskdFileData>? TryExpandDirectory(SlskdSear...
    method RemoveSearch (line 172) | public void RemoveSearch(string searchId, bool delay = false)
    method Handle (line 195) | public void Handle(AlbumGrabbedEvent message)
    method Handle (line 203) | public void Handle(ApplicationShutdownRequested message)
    method InvalidIgnoreCache (line 215) | public static void InvalidIgnoreCache(string path) => _ignoreListCache...
    method GetRateLimitedUsers (line 217) | private HashSet<string> GetRateLimitedUsers()
    method GetGrabCounts (line 245) | private Dictionary<string, int> GetGrabCounts()
    method GetQueuedCounts (line 282) | private Dictionary<string, int> GetQueuedCounts()
    method ExtractUsernameFromUrl (line 311) | private static string? ExtractUsernameFromUrl(string? url)
    method ExecuteRemovalAsync (line 323) | private async Task ExecuteRemovalAsync(SlskdSettings settings, string ...
    method GetIgnoredUsers (line 339) | private HashSet<string>? GetIgnoredUsers(string? ignoreListPath)

FILE: Tubifarry/Indexers/Soulseek/SlskdItemsParser.cs
  class SlskdItemsParser (line 13) | public partial class SlskdItemsParser : ISlskdItemsParser
    method SlskdItemsParser (line 47) | public SlskdItemsParser(ISentryHelper sentry, Logger logger)
    method ParseFolderName (line 53) | public SlskdFolderData ParseFolderName(string folderPath)
    method CreateAlbumData (line 81) | public AlbumData CreateAlbumData(string searchId, IGrouping<string, Sl...
    method SplitPathIntoComponents (line 172) | private static string[] SplitPathIntoComponents(string path) => path.S...
    method ParseFromRegexPatterns (line 174) | private static (string? artist, string? album, string? year) ParseFrom...
    method GetArtistFromParentFolder (line 217) | private static string? GetArtistFromParentFolder(string[] pathComponents)
    method CheckVolumeSeriesMatch (line 227) | private bool CheckVolumeSeriesMatch(string directoryPath, string? sear...
    method NormalizeVolume (line 255) | public string NormalizeVolume(string volume)
    method NormalizeRomanNumeral (line 277) | private static string NormalizeRomanNumeral(string roman)
    method ConvertRomanToNumber (line 291) | private int ConvertRomanToNumber(string roman)
    method NormalizeString (line 317) | public static string NormalizeString(string input)
    method CleanComponent (line 328) | private static string CleanComponent(string component)
    method ExtractExplicitTag (line 336) | private bool ExtractExplicitTag(string path)
    method ExtractEdition (line 353) | private static string? ExtractEdition(string path)
    method ExtractYearFromPath (line 359) | private static string? ExtractYearFromPath(string path)
    method TryMatchRegex (line 365) | private static Match? TryMatchRegex(string input, Regex regex)
    method IsFuzzyArtistMatch (line 371) | private static bool IsFuzzyArtistMatch(string dirNameNorm, string sear...
    method IsFuzzyAlbumMatch (line 379) | private static bool IsFuzzyAlbumMatch(string dirNameNorm, string searc...
    method GetCachedFuzzyScores (line 392) | private static (int Partial, int TokenSort) GetCachedFuzzyScores(strin...
    method DetermineRegexMatchType (line 417) | public static string DetermineRegexMatchType(string folderPath)
    method DetermineFinalArtist (line 437) | private static string DetermineFinalArtist(bool isArtistMatch, SlskdFo...
    method DetermineFinalAlbum (line 446) | private static string DetermineFinalAlbum(bool isAlbumMatch, SlskdFold...
    method AnalyzeAudioQuality (line 459) | private (AudioFormat Codec, int? BitRate, int? BitDepth, int? SampleRa...
    method GetMostCommonExtension (line 480) | public static string? GetMostCommonExtension(IEnumerable<SlskdFileData...
    method FormatQualityInfo (line 499) | private static string FormatQualityInfo(AudioFormat codec, int? bitRat...
    method CleanComponentRegex (line 510) | [GeneratedRegex(@"(?ix)
    method VolumeRegex (line 523) | [GeneratedRegex(@"(?ix)
    method AlbumYearRegex (line 529) | [GeneratedRegex(@"^(?<album>[^(\[]+)(?:\s*[\(\[](?<year>19\d{2}|20\d{2...
    method YearArtistAlbumRegex (line 532) | [GeneratedRegex(@"^(?<year>19\d{2}|20\d{2})\s*-\s*(?<artist>[^-]+)\s*-...
    method ArtistAlbumYearRegex (line 535) | [GeneratedRegex(@"^(?<artist>[^-]+)\s*-\s*(?<album>[^(\[]+)(?:\s*[\(\[...
    method YearExtractionRegex (line 538) | [GeneratedRegex(@"(?<year>19\d{2}|20\d{2})", RegexOptions.ExplicitCapt...
    method ExplicitTagRegex (line 541) | [GeneratedRegex(@"(?:^|\s|\(|\[)(?<negation>Non-?|Not\s+)?Explicit(?:\...
    method EditionRegex (line 544) | [GeneratedRegex(@"[\(\[](?<edition>(?:(?:Super\s+)?Deluxe|Limited|Spec...
    method RemoveWordsRegex (line 547) | [GeneratedRegex(@"\b(?:the|a|an|feat|featuring|ft|presents|pres|with|a...
    method VolumeRangeRegex (line 550) | [GeneratedRegex(@"(\d+)(?:[-to&]\s*\d+)?", RegexOptions.ExplicitCaptur...
    method RomanNumeralRegex (line 553) | [GeneratedRegex(@"^M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|...
    method ReduceWhitespaceRegex (line 556) | [GeneratedRegex(@"\s+", RegexOptions.Compiled)]
    method RemoveNonAlphanumericRegex (line 559) | [GeneratedRegex(@"[^\w\s$-]", RegexOptions.Compiled)]
    method NormalizeCharactersRegex (line 562) | [GeneratedRegex(@"[._/]+", RegexOptions.Compiled)]

FILE: Tubifarry/Indexers/Soulseek/SlskdRequestGenerator.cs
  class SlskdRequestGenerator (line 19) | internal class SlskdRequestGenerator : IIndexerRequestGenerator<LazyInde...
    method SlskdRequestGenerator (line 31) | public SlskdRequestGenerator(SlskdIndexer indexer, ISlskdSearchChain s...
    method GetRecentRequests (line 40) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetRece...
    method GetSearchRequests (line 42) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method GetSearchRequests (line 72) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method ExecuteSearch (line 102) | private IEnumerable<IndexerRequest> ExecuteSearch(SearchQuery query)
    method GetRequestsAsync (line 142) | private async Task<IndexerRequest?> GetRequestsAsync(SearchQuery query...
    method CreateSearchData (line 188) | private dynamic CreateSearchData(string searchText) => new
    method CreateSearchRequest (line 201) | private HttpRequest CreateSearchRequest(dynamic searchData)
    method ExecuteSearchAsync (line 213) | private async Task ExecuteSearchAsync(HttpRequest searchRequest, strin...
    method CreateResultRequest (line 219) | private HttpRequest CreateResultRequest(string searchId, SearchQuery q...
    method WaitOnSearchCompletionAsync (line 255) | private async Task WaitOnSearchCompletionAsync(string searchId, TimeSp...
    method GetSearchResultsAsync (line 294) | private async Task<JsonNode?> GetSearchResultsAsync(string searchId)
    method CalculateQuadraticDelay (line 310) | private static double CalculateQuadraticDelay(double progress)
    method GetPrimaryAlbumType (line 320) | private static PrimaryAlbumType GetPrimaryAlbumType(string? albumType)
    method ExpandDirectory (line 331) | public async Task<IGrouping<string, SlskdFileData>?> ExpandDirectory(s...

FILE: Tubifarry/Indexers/Soulseek/SlskdSettings.cs
  class SlskdSettingsValidator (line 10) | internal class SlskdSettingsValidator : AbstractValidator<SlskdSettings>
    method SlskdSettingsValidator (line 12) | public SlskdSettingsValidator()
  class SlskdSettings (line 97) | public class SlskdSettings : IIndexerSettings
    method Validate (line 179) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type GrabLimitIntervalType (line 183) | public enum GrabLimitIntervalType
  type TrackCountFilterType (line 195) | public enum TrackCountFilterType

FILE: Tubifarry/Indexers/Soulseek/SlskdTextProcessor.cs
  class SlskdTextProcessor (line 10) | public static partial class SlskdTextProcessor
    method BuildSearchText (line 25) | public static string BuildSearchText(string? artist, string? album)
    method ShouldNormalizeCharacters (line 28) | public static bool ShouldNormalizeCharacters(string? artist, string? a...
    method ShouldStripPunctuation (line 36) | public static bool ShouldStripPunctuation(string? artist, string? album)
    method IsVariousArtists (line 44) | public static bool IsVariousArtists(string artist)
    method ContainsVolumeReference (line 47) | public static bool ContainsVolumeReference(string album)
    method ShouldGenerateRomanVariations (line 50) | public static bool ShouldGenerateRomanVariations(string album)
    method StripPunctuation (line 59) | public static string StripPunctuation(string? input)
    method NormalizeSpecialCharacters (line 67) | public static string NormalizeSpecialCharacters(string? input)
    method GenerateVolumeVariations (line 84) | public static IEnumerable<string> GenerateVolumeVariations(string album)
    method GenerateRomanNumeralVariations (line 117) | public static IEnumerable<string> GenerateRomanNumeralVariations(strin...
    method GetDirectoryFromFilename (line 132) | public static string GetDirectoryFromFilename(string? filename)
    method ParseListContent (line 140) | public static HashSet<string> ParseListContent(string content)
    method StripPunctuationRegex (line 152) | [GeneratedRegex(@"\s+")]

FILE: Tubifarry/Indexers/Spotify/SpotifyIndexerSettings.cs
  class SpotifyIndexerSettingsValidator (line 8) | public class SpotifyIndexerSettingsValidator : AbstractValidator<Spotify...
    method SpotifyIndexerSettingsValidator (line 10) | public SpotifyIndexerSettingsValidator()
  class SpotifyIndexerSettings (line 48) | public class SpotifyIndexerSettings : YouTubeIndexerSettings
    method Validate (line 73) | public override NzbDroneValidationResult Validate() => new(Validator.V...

FILE: Tubifarry/Indexers/Spotify/SpotifyParser.cs
  type ISpotifyParser (line 9) | public interface ISpotifyParser : IParseIndexerResponse
    method UpdateSettings (line 11) | void UpdateSettings(SpotifyIndexerSettings settings);
  class SpotifyParser (line 17) | internal class SpotifyParser(Logger logger, ISpotifyToYouTubeEnricher en...
    method UpdateSettings (line 23) | public void UpdateSettings(SpotifyIndexerSettings settings)
    method ParseResponse (line 29) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method ParseAlbumItems (line 68) | private List<AlbumData> ParseAlbumItems(JsonElement itemsElement)
    method ExtractAlbumInfo (line 89) | private static AlbumData ExtractAlbumInfo(JsonElement album) => new("S...
    method ExtractArtistName (line 103) | private static string ExtractArtistName(JsonElement album)
    method ExtractAlbumArtUrl (line 110) | private static string ExtractAlbumArtUrl(JsonElement album)
    method ExtractCoverResolution (line 117) | private static string ExtractCoverResolution(JsonElement album)

FILE: Tubifarry/Indexers/Spotify/SpotifyRequestGenerator.cs
  type ISpotifyRequestGenerator (line 13) | public interface ISpotifyRequestGenerator : IIndexerRequestGenerator<Laz...
    method StartTokenRequest (line 15) | void StartTokenRequest();
    method TokenIsExpired (line 17) | bool TokenIsExpired();
    method RequestNewToken (line 19) | bool RequestNewToken();
    method UpdateSettings (line 21) | void UpdateSettings(SpotifyIndexerSettings settings);
  class SpotifyRequestGenerator (line 24) | internal class SpotifyRequestGenerator(Logger logger) : ISpotifyRequestG...
    method UpdateSettings (line 37) | public void UpdateSettings(SpotifyIndexerSettings settings) => _settin...
    method GetRecentRequests (line 42) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetRece...
    method GetRecentReleaseRequests (line 58) | private IEnumerable<IndexerRequest> GetRecentReleaseRequests()
    method GetSearchRequests (line 71) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method GetSearchRequests (line 108) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method GetAllPagesForQuery (line 130) | private IEnumerable<IndexerRequest> GetAllPagesForQuery(string searchQ...
    method GetRequests (line 139) | private IEnumerable<IndexerRequest> GetRequests(string searchQuery, st...
    method HandleToken (line 152) | private void HandleToken()
    method TokenIsExpired (line 160) | public bool TokenIsExpired() => DateTime.Now >= _tokenExpiry;
    method RequestNewToken (line 162) | public bool RequestNewToken() => DateTime.Now >= _tokenExpiry.AddMinut...
    method StartTokenRequest (line 164) | public void StartTokenRequest()

FILE: Tubifarry/Indexers/Spotify/SpotifyToYouTubeEnricher.cs
  type ISpotifyToYouTubeEnricher (line 15) | public interface ISpotifyToYouTubeEnricher
    method UpdateSettings (line 17) | void UpdateSettings(SpotifyIndexerSettings settings);
    method EnrichWithYouTubeData (line 19) | List<AlbumData> EnrichWithYouTubeData(List<AlbumData> albums);
    method EnrichSingleAlbumAsync (line 21) | Task<AlbumData?> EnrichSingleAlbumAsync(AlbumData albumData);
  class SpotifyToYouTubeEnricher (line 27) | internal class SpotifyToYouTubeEnricher(Logger logger) : ISpotifyToYouTu...
    method UpdateSettings (line 37) | public void UpdateSettings(SpotifyIndexerSettings settings)
    method EnrichWithYouTubeData (line 57) | public List<AlbumData> EnrichWithYouTubeData(List<AlbumData> albums)
    method EnrichSingleAlbumAsync (line 92) | public async Task<AlbumData?> EnrichSingleAlbumAsync(AlbumData albumData)
    method IsAlbumMatch (line 193) | private bool IsAlbumMatch(AlbumData spotifyAlbum, AlbumSearchResult yt...
    method IsTrackCountValid (line 226) | private bool IsTrackCountValid(int spotifyTrackCount, int ytTrackCount)
    method NormalizeString (line 235) | private static string NormalizeString(string input)
    method AreNamesSimilar (line 247) | private static bool AreNamesSimilar(string name1, string name2, bool e...
    method SettingsEqual (line 260) | private static bool SettingsEqual(SpotifyIndexerSettings? settings1, S...

FILE: Tubifarry/Indexers/Spotify/TubifarryIndexer.cs
  class TubifarryIndexer (line 16) | internal class TubifarryIndexer : ExtendedHttpIndexerBase<SpotifyIndexer...
    method TubifarryIndexer (line 35) | public TubifarryIndexer(
    method Test (line 50) | protected override async Task Test(List<ValidationFailure> failures)
    method UpdateComponentSettings (line 71) | private void UpdateComponentSettings()
    method GetExtendedRequestGenerator (line 77) | public override IIndexerRequestGenerator<LazyIndexerPageableRequest> G...
    method GetParser (line 83) | public override IParseIndexerResponse GetParser()

FILE: Tubifarry/Indexers/SubSonic/SubSonicAuthHelper.cs
  class SubSonicAuthHelper (line 9) | public static class SubSonicAuthHelper
    method GenerateToken (line 14) | public static (string Salt, string Token) GenerateToken(string password)
    method AppendAuthParameters (line 21) | public static void AppendAuthParameters(StringBuilder urlBuilder, stri...
    method GenerateSaltFromAssembly (line 41) | private static string GenerateSaltFromAssembly() =>
    method CalculateMd5Hash (line 44) | private static string CalculateMd5Hash(string input)

FILE: Tubifarry/Indexers/SubSonic/SubSonicIndexer.cs
  class SubSonicIndexer (line 14) | public class SubSonicIndexer : HttpIndexerBase<SubSonicIndexerSettings>
    method SubSonicIndexer (line 30) | public SubSonicIndexer(
    method Test (line 44) | protected override async Task Test(List<ValidationFailure> failures)
    method GetRequestGenerator (line 111) | public override IIndexerRequestGenerator GetRequestGenerator()
    method GetParser (line 117) | public override IParseIndexerResponse GetParser()

FILE: Tubifarry/Indexers/SubSonic/SubSonicIndexerParser.cs
  type ISubSonicParser (line 12) | public interface ISubSonicParser : IParseIndexerResponse
    method SetSettings (line 14) | void SetSettings(SubSonicIndexerSettings settings);
  class SubSonicIndexerParser (line 17) | public class SubSonicIndexerParser(Logger logger, IHttpClient httpClient...
    method SetSettings (line 29) | public void SetSettings(SubSonicIndexerSettings settings) => _settings...
    method ParseResponse (line 31) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method ParseSearch3Response (line 54) | private void ParseSearch3Response(string responseContent, List<Release...
    method ValidateResponse (line 82) | private bool ValidateResponse(SubSonicResponse response)
    method ProcessAlbums (line 95) | private void ProcessAlbums(List<SubSonicSearchAlbum>? albums, List<Rel...
    method ProcessSongs (line 122) | private void ProcessSongs(List<SubSonicSearchSong>? songs, List<Releas...
    method FetchFullAlbum (line 133) | private SubSonicAlbumFull? FetchFullAlbum(string albumId)
    method BuildAlbumUrl (line 155) | private string BuildAlbumUrl(string albumId)
    method CreateHttpRequest (line 167) | private HttpRequest CreateHttpRequest(string url)
    method ExecuteRequest (line 177) | private HttpResponse? ExecuteRequest(HttpRequest request)
    method ParseAlbumResponse (line 190) | private SubSonicAlbumFull? ParseAlbumResponse(string content, string a...
    method CreateAlbumData (line 211) | private AlbumData CreateAlbumData(SubSonicAlbumFull album)
    method CalculateTotalAlbumSize (line 247) | private static long CalculateTotalAlbumSize(List<SubSonicSearchSong> s...
    method CreateTrackData (line 265) | private AlbumData CreateTrackData(SubSonicSearchSong song)
    method BuildInfoUrl (line 291) | private string BuildInfoUrl(string type, string id) =>

FILE: Tubifarry/Indexers/SubSonic/SubSonicIndexerSettings.cs
  class SubSonicIndexerSettingsValidator (line 8) | public class SubSonicIndexerSettingsValidator : AbstractValidator<SubSon...
    method SubSonicIndexerSettingsValidator (line 10) | public SubSonicIndexerSettingsValidator()
  class SubSonicIndexerSettings (line 45) | public class SubSonicIndexerSettings : IIndexerSettings
    method Validate (line 73) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Indexers/SubSonic/SubSonicRecords.cs
  type SubSonicResponseWrapper (line 9) | internal record SubSonicResponseWrapper(
  type SubSonicResponse (line 15) | internal record SubSonicResponse(
  type SubSonicPingResponse (line 24) | internal record SubSonicPingResponse(
  type SubSonicPingData (line 30) | internal record SubSonicPingData(
  type SubSonicError (line 38) | internal record SubSonicError(
  type SubSonicSearchResponse (line 45) | internal record SubSonicSearchResponse(
  type SubSonicSearchArtist (line 53) | internal record SubSonicSearchArtist(
  type SubSonicSearchAlbum (line 62) | internal record SubSonicSearchAlbum(
  type SubSonicAlbumFull (line 81) | internal record SubSonicAlbumFull(
  type SubSonicAlbumResponseWrapper (line 101) | internal record SubSonicAlbumResponseWrapper(
  type SubSonicSongResponseWrapper (line 107) | internal record SubSonicSongResponseWrapper(
  type SubSonicItemResponse (line 113) | internal record SubSonicItemResponse(
  type SubSonicSearchSong (line 123) | internal record SubSonicSearchSong(

FILE: Tubifarry/Indexers/SubSonic/SubSonicRequestGenerator.cs
  type ISubSonicRequestGenerator (line 9) | public interface ISubSonicRequestGenerator : IIndexerRequestGenerator
    method SetSetting (line 11) | void SetSetting(SubSonicIndexerSettings settings);
  class SubSonicRequestGenerator (line 14) | public class SubSonicRequestGenerator(Logger logger) : ISubSonicRequestG...
    method GetRecentRequests (line 19) | public IndexerPageableRequestChain GetRecentRequests() => new();
    method GetSearchRequests (line 21) | public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriter...
    method GetSearchRequests (line 30) | public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCrite...
    method SetSetting (line 35) | public void SetSetting(SubSonicIndexerSettings settings) => _settings ...
    method Generate (line 37) | private IndexerPageableRequestChain Generate(string query, bool isSingle)
    method BuildSearch3Url (line 70) | private string BuildSearch3Url(string baseUrl, string query, bool isSi...
    method CreateRequest (line 82) | private IndexerRequest CreateRequest(string url, string contentType)

FILE: Tubifarry/Indexers/TripleTriple/TripleTripleIndexer.cs
  class TripleTripleIndexer (line 13) | public class TripleTripleIndexer : HttpIndexerBase<TripleTripleIndexerSe...
    method TripleTripleIndexer (line 28) | public TripleTripleIndexer(
    method Test (line 44) | protected override async Task Test(List<ValidationFailure> failures)
    method GetRequestGenerator (line 74) | public override IIndexerRequestGenerator GetRequestGenerator()
    method GetParser (line 80) | public override IParseIndexerResponse GetParser() => _parser;

FILE: Tubifarry/Indexers/TripleTriple/TripleTripleIndexerSettings.cs
  class TripleTripleIndexerSettingsValidator (line 8) | public class TripleTripleIndexerSettingsValidator : AbstractValidator<Tr...
    method TripleTripleIndexerSettingsValidator (line 10) | public TripleTripleIndexerSettingsValidator()
  class TripleTripleIndexerSettings (line 33) | public class TripleTripleIndexerSettings : IIndexerSettings
    method TripleTripleIndexerSettings (line 37) | public TripleTripleIndexerSettings()
    method Validate (line 61) | public NzbDroneValidationResult Validate() => new(_validator.Validate(...
  type TripleTripleCountry (line 64) | public enum TripleTripleCountry
  type TripleTripleCodec (line 100) | public enum TripleTripleCodec

FILE: Tubifarry/Indexers/TripleTriple/TripleTripleParser.cs
  type ITripleTripleParser (line 10) | public interface ITripleTripleParser : IParseIndexerResponse { }
  class TripleTripleParser (line 12) | public class TripleTripleParser(Logger logger) : ITripleTripleParser
    method ParseResponse (line 14) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method CreateAlbumRelease (line 72) | private AlbumData CreateAlbumRelease(TripleTripleDocument album)
    method CreateTrackRelease (line 97) | private AlbumData CreateTrackRelease(TripleTripleDocument track)
    method GetQualityForCodec (line 122) | private static (AudioFormat Format, int Bitrate, int BitDepth) GetQual...

FILE: Tubifarry/Indexers/TripleTriple/TripleTripleRecords.cs
  type TripleTripleStatusResponse (line 5) | public record TripleTripleStatusResponse(
  type TripleTripleSearchResponse (line 8) | public record TripleTripleSearchResponse(
  type TripleTripleResult (line 11) | public record TripleTripleResult(
  type TripleTripleSearchHit (line 14) | public record TripleTripleSearchHit(
  type TripleTripleDocument (line 17) | public record TripleTripleDocument(
  type TripleTripleArt (line 38) | public record TripleTripleArt(
  type TripleTripleMediaResponse (line 42) | public record TripleTripleMediaResponse(
  type TripleTripleTags (line 56) | public record TripleTripleTags(
  type TripleTripleStreamInfo (line 74) | public record TripleTripleStreamInfo(
  type TripleTripleLyrics (line 82) | public record TripleTripleLyrics(
  type TripleTripleAlbumMetadata (line 86) | public record TripleTripleAlbumMetadata(
  type TripleTripleAlbumInfo (line 89) | public record TripleTripleAlbumInfo(
  type TripleTripleArtistInfo (line 103) | public record TripleTripleArtistInfo(
  type TripleTripleTrackInfo (line 107) | public record TripleTripleTrackInfo(
  type TripleTripleRequestData (line 117) | public record TripleTripleRequestData(

FILE: Tubifarry/Indexers/TripleTriple/TripleTripleRequestGenerator.cs
  type ITripleTripleRequestGenerator (line 9) | public interface ITripleTripleRequestGenerator : IIndexerRequestGenerator
    method SetSetting (line 11) | void SetSetting(TripleTripleIndexerSettings settings);
  class TripleTripleRequestGenerator (line 14) | public class TripleTripleRequestGenerator : ITripleTripleRequestGenerator
    method TripleTripleRequestGenerator (line 19) | public TripleTripleRequestGenerator(Logger logger) => _logger = logger;
    method GetRecentRequests (line 21) | public IndexerPageableRequestChain GetRecentRequests() => new();
    method GetSearchRequests (line 23) | public IndexerPageableRequestChain GetSearchRequests(AlbumSearchCriter...
    method GetSearchRequests (line 30) | public IndexerPageableRequestChain GetSearchRequests(ArtistSearchCrite...
    method SetSetting (line 32) | public void SetSetting(TripleTripleIndexerSettings settings) => _setti...
    method Generate (line 34) | private IndexerPageableRequestChain Generate(string query, bool isSingle)

FILE: Tubifarry/Indexers/YouTube/YoutubeIndexer.cs
  class YouTubeIndexer (line 16) | internal class YouTubeIndexer : ExtendedHttpIndexerBase<YouTubeIndexerSe...
    method YouTubeIndexer (line 39) | public YouTubeIndexer(IHttpClient httpClient,
    method Test (line 51) | protected override async Task Test(List<ValidationFailure> failures)
    method GetExtendedRequestGenerator (line 66) | public override IIndexerRequestGenerator<LazyIndexerPageableRequest> G...
    method GetParser (line 68) | public override IParseIndexerResponse GetParser() => _parser;

FILE: Tubifarry/Indexers/YouTube/YoutubeIndexerSettings.cs
  class YouTubeIndexerSettingsValidator (line 9) | public class YouTubeIndexerSettingsValidator : AbstractValidator<YouTube...
    method YouTubeIndexerSettingsValidator (line 11) | public YouTubeIndexerSettingsValidator()
  class YouTubeIndexerSettings (line 27) | public class YouTubeIndexerSettings : IIndexerSettings
    method Validate (line 42) | public virtual NzbDroneValidationResult Validate() => new(Validator.Va...

FILE: Tubifarry/Indexers/YouTube/YoutubeParser.cs
  class YouTubeParser (line 22) | internal class YouTubeParser : IParseIndexerResponse
    method YouTubeParser (line 44) | public YouTubeParser(YouTubeIndexer indexer)
    method ParseResponse (line 50) | public IList<ReleaseInfo> ParseResponse(IndexerResponse indexerResponse)
    method TryParseWithDelegate (line 79) | private Page<SearchResult>? TryParseWithDelegate(JObject jsonResponse)
    method ProcessSearchResults (line 103) | private void ProcessSearchResults(IReadOnlyList<SearchResult> searchRe...
    method EnrichAlbumWithYouTubeDataAsync (line 132) | private async Task EnrichAlbumWithYouTubeDataAsync(AlbumData albumData)
    method UpdateClient (line 186) | private void UpdateClient()
    method ExtractAlbumInfo (line 194) | private static AlbumData ExtractAlbumInfo(AlbumSearchResult album) => ...

FILE: Tubifarry/Indexers/YouTube/YoutubeRequestGenerator.cs
  class YouTubeRequestGenerator (line 17) | internal class YouTubeRequestGenerator : IIndexerRequestGenerator<LazyIn...
    method YouTubeRequestGenerator (line 26) | public YouTubeRequestGenerator(YouTubeIndexer indexer)
    method GetRecentRequests (line 32) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetRece...
    method GetSearchRequests (line 38) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method GetSearchRequests (line 66) | public IndexerPageableRequestChain<LazyIndexerPageableRequest> GetSear...
    method UpdateTokens (line 77) | private void UpdateTokens()
    method GetRequests (line 84) | private IEnumerable<IndexerRequest> GetRequests(string searchQuery, Se...
    method ToParams (line 125) | public static string? ToParams(SearchCategory? value) =>

FILE: Tubifarry/Metadata/Converter/AudioConverter.cs
  class AudioConverter (line 13) | public class AudioConverter(Logger logger, Lazy<ITagService> tagService)...
    method FindMetadataFile (line 20) | public override MetadataFile FindMetadataFile(Artist artist, string pa...
    method ArtistMetadata (line 22) | public override MetadataFileResult ArtistMetadata(Artist artist) => de...
    method AlbumMetadata (line 24) | public override MetadataFileResult AlbumMetadata(Artist artist, Album ...
    method ArtistImages (line 26) | public override List<ImageFileResult> ArtistImages(Artist artist) => d...
    method AlbumImages (line 28) | public override List<ImageFileResult> AlbumImages(Artist artist, Album...
    method TrackImages (line 30) | public override List<ImageFileResult> TrackImages(Artist artist, Track...
    method TrackMetadata (line 32) | public override MetadataFileResult TrackMetadata(Artist artist, TrackF...
    method ConvertTrack (line 41) | private async Task ConvertTrack(TrackFile trackFile)
    method PerformConversion (line 58) | private async Task PerformConversion(TrackFile trackFile, ConversionRe...
    method GetTrackBitrateAsync (line 70) | private async Task<int?> GetTrackBitrateAsync(string filePath)
    method GetTrackBitDepthAsync (line 89) | private async Task<int?> GetTrackBitDepthAsync(string filePath)
    method ShouldBlockConversion (line 130) | private ConversionResult ShouldBlockConversion(ConversionRule rule, Au...
    method GetTargetConversionForTrack (line 167) | private async Task<ConversionResult> GetTargetConversionForTrack(Audio...
    method ShouldConvertTrack (line 211) | private async Task<bool> ShouldConvertTrack(TrackFile trackFile)
    method GetArtistTagRule (line 234) | private ConversionRule? GetArtistTagRule(TrackFile trackFile)
    method MatchesAnyCustomRule (line 250) | private bool MatchesAnyCustomRule(AudioFormat trackFormat, int? curren...
    method IsRuleMatching (line 253) | private bool IsRuleMatching(ConversionRule rule, AudioFormat trackForm...
    method GetTrackAudioFormatAsync (line 265) | private async Task<AudioFormat> GetTrackAudioFormatAsync(string trackP...
    method LogConversionPlan (line 289) | private void LogConversionPlan(AudioFormat sourceFormat, int? sourceBi...
    method FormatDescriptionWithBitrate (line 297) | private static string FormatDescriptionWithBitrate(AudioFormat format,...
    method IsFormatEnabledForConversion (line 300) | private bool IsFormatEnabledForConversion(AudioFormat format) => forma...

FILE: Tubifarry/Metadata/Converter/AudioConverterSettings.cs
  class AudioConverterSettingsValidator (line 12) | public class AudioConverterSettingsValidator : AbstractValidator<AudioCo...
    method AudioConverterSettingsValidator (line 14) | public AudioConverterSettingsValidator()
    method IsValidConversionRule (line 41) | private bool IsValidConversionRule(KeyValuePair<string, string> rule)
    method IsValidLossyConversion (line 49) | private bool IsValidLossyConversion(KeyValuePair<string, string> rule)
    method IsValidCBRUsage (line 65) | private bool IsValidCBRUsage(KeyValuePair<string, string> rule)
    method IsValidStaticConversion (line 77) | private static bool IsValidStaticConversion(AudioConverterSettings set...
    method TestFFmpeg (line 80) | private static async Task<bool> TestFFmpeg(string ffmpegPath)
  class AudioConverterSettings (line 106) | public class AudioConverterSettings : IProviderConfig
    method Validate (line 137) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  type TargetAudioFormat (line 140) | public enum TargetAudioFormat

FILE: Tubifarry/Metadata/Converter/BitrateRules.cs
  class ConversionResult (line 9) | public class ConversionResult
    method Blocked (line 17) | public static ConversionResult Blocked() => new() { IsBlocked = true, ...
    method Success (line 19) | public static ConversionResult Success(AudioFormat format, int? bitrat...
    method FromRule (line 28) | public static ConversionResult FromRule(ConversionRule rule) => new()
  class ConversionRule (line 38) | public class ConversionRule
    method MatchesBitrate (line 57) | public bool MatchesBitrate(int? currentBitrate)
    method MatchesFormat (line 67) | public bool MatchesFormat(AudioFormat trackFormat)
    method HasBitrateConstraints (line 81) | private bool HasBitrateConstraints() => SourceBitrateOperator.HasValue...
    method EvaluateBitrateCondition (line 83) | private bool EvaluateBitrateCondition(int currentBitrate)
    method GetOperatorSymbol (line 100) | private string GetOperatorSymbol() => SourceBitrateOperator.HasValue ?...
    method ToString (line 102) | public override string ToString() => $"{FormatSourcePart()}->{FormatTa...
    method FormatSourcePart (line 104) | private string FormatSourcePart()
    method FormatTargetPart (line 121) | private string FormatTargetPart()
  class OperatorSymbols (line 136) | public static class OperatorSymbols
    method GetSymbol (line 145) | public static string GetSymbol(ComparisonOperator op)
    method FromSymbol (line 159) | public static ComparisonOperator? FromSymbol(string symbol)
  type ComparisonOperator (line 174) | public enum ComparisonOperator
  class RuleParser (line 184) | public static partial class RuleParser
    method TryParseRule (line 192) | public static bool TryParseRule(string sourceKey, string targetValue, ...
    method TryParseArtistTag (line 206) | public static bool TryParseArtistTag(string tagLabel, out ConversionRu...
    method ParseSourcePart (line 248) | private static bool ParseSourcePart(string sourceKey, ConversionRule r...
    method ParseSourceFormat (line 282) | private static bool ParseSourceFormat(string formatName, ConversionRul...
    method ParseSourceBitrateConstraints (line 315) | private static bool ParseSourceBitrateConstraints(string operatorStr, ...
    method ParseTargetPart (line 335) | private static bool ParseTargetPart(string targetValue, ConversionRule...
    method ParseTargetFormat (line 366) | private static bool ParseTargetFormat(string formatName, ConversionRul...
    method ParseTargetBitrate (line 384) | private static bool ParseTargetBitrate(string bitrateStr, ConversionRu...
    method SourceFormatRegex (line 416) | [GeneratedRegex(@"^([a-zA-Z0-9]+)(?:([!<>=]{1,2})(\d+))?$", RegexOptio...
    method TargetFormatRegex (line 419) | [GeneratedRegex(@"^([a-zA-Z0-9]+)(?::(\d+)k?)?(?::(cbr))?$", RegexOpti...
    method ArtistTagRegex (line 422) | [GeneratedRegex(@"^([a-zA-Z]+)(?:-(\d+)k?)?$", RegexOptions.Compiled)]

FILE: Tubifarry/Metadata/Lyrics/LyricEnhancerSettings.cs
  class LyricsEnhancerSettingsValidator (line 10) | public class LyricsEnhancerSettingsValidator : AbstractValidator<LyricsE...
    method LyricsEnhancerSettingsValidator (line 12) | public LyricsEnhancerSettingsValidator()
  class LyricsEnhancerSettings (line 42) | public class LyricsEnhancerSettings : IProviderConfig
    method LyricsEnhancerSettings (line 76) | public LyricsEnhancerSettings() => Instance = this;
    method Validate (line 80) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...
  class LyricsUpdateCommand (line 86) | public class LyricsUpdateCommand : Command
    method SetCompletionMessage (line 95) | public void SetCompletionMessage(string message) => _completionMessage...
  type LyricOptions (line 98) | public enum LyricOptions

FILE: Tubifarry/Metadata/Lyrics/LyricsEnhancer.cs
  class LyricsEnhancer (line 20) | public class LyricsEnhancer : ScheduledTaskBase<LyricsEnhancerSettings>,...
    method LyricsEnhancer (line 35) | public LyricsEnhancer(
    method Execute (line 68) | public void Execute(LyricsUpdateCommand message)
    method ProcessTrackBatch (line 128) | private ProcessingResult ProcessTrackBatch(List<TrackFile> batch)
    method SyncExistingLrcFile (line 176) | private void SyncExistingLrcFile(Artist artist, TrackFile trackFile)
    method WriteLrcFile (line 184) | private void WriteLrcFile(Artist artist, TrackFile trackFile, Metadata...
    method EmbedLyrics (line 192) | private void EmbedLyrics(Lyric lyric, TrackFile trackFile)
    method GetFilenameAfterMove (line 221) | public override string GetFilenameAfterMove(Artist artist, TrackFile t...
    method TrackMetadata (line 230) | public override MetadataFileResult TrackMetadata(Artist artist, TrackF...
    method ProcessTrackLyricsAsync (line 255) | private async Task<MetadataFileResult> ProcessTrackLyricsAsync(Artist ...
    method FetchLyricsAsync (line 283) | private async Task<Lyric?> FetchLyricsAsync((string Artist, string Tit...
    method CreateLrcFileContent (line 304) | private string? CreateLrcFileContent(Lyric lyric, (string Artist, stri...
    class ProcessingResult (line 319) | private class ProcessingResult

FILE: Tubifarry/Metadata/Lyrics/LyricsHelper.cs
  class LyricsHelper (line 14) | public static class LyricsHelper
    method ScoreAndSelectBestMatch (line 16) | public static JToken? ScoreAndSelectBestMatch(List<JToken> artistMatch...
    method CreateRawLrcContent (line 63) | public static string? CreateRawLrcContent(List<SyncLine>? syncedLyrics)
    method CreateLrcFileContent (line 76) | public static string? CreateLrcFileContent(Lyric lyric, string artistN...
    method GetLyricsForEmbedding (line 101) | public static string? GetLyricsForEmbedding(Lyric lyric, LyricOptions ...
    method GetLyricsForLrcFile (line 110) | public static string? GetLyricsForLrcFile(Lyric lyric, LyricOptions op...
    method EmbedLyricsInAudioFile (line 119) | public static bool EmbedLyricsInAudioFile(string filePath, string lyri...
    method ExtractTrackInfo (line 139) | public static (string Artist, string Title, string Album, int Duration...
    method LrcFileExistsOnDisk (line 167) | public static bool LrcFileExistsOnDisk(string trackFilePath, IDiskProv...

FILE: Tubifarry/Metadata/Lyrics/LyricsProviders.cs
  class LyricsProviders (line 11) | public partial class LyricsProviders
    method LyricsProviders (line 17) | public LyricsProviders(HttpClient httpClient, Logger logger, LyricsEnh...
    method FetchFromLrcLibAsync (line 26) | public async Task<Lyric?> FetchFromLrcLibAsync(string artistName, stri...
    method FetchFromGeniusAsync (line 77) | public async Task<Lyric?> FetchFromGeniusAsync(string artistName, stri...
    method SearchSongOnGeniusAsync (line 105) | private async Task<JToken?> SearchSongOnGeniusAsync(string artistName,...
    method ExtractLyricsFromGeniusPageAsync (line 151) | private async Task<string?> ExtractLyricsFromGeniusPageAsync(string so...
    method ExtractLyricsFromHtml (line 178) | private string? ExtractLyricsFromHtml(string html)
    method DataLyricsContainerRegex (line 207) | [GeneratedRegex(@"<div[^>]*data-lyrics-container[^>]*>(.*?)<\/div>", R...
    method ClassicLyricsClassRegex (line 210) | [GeneratedRegex(@"<div[^>]*class=""[^""]*lyrics[^""]*""[^>]*>(.*?)<\/d...
    method LyricsRootIdRegex (line 213) | [GeneratedRegex(@"<div[^>]*id=""lyrics-root[^""]*""[^>]*>(.*?)<\/div>"...
    method BrTagRegex (line 216) | [GeneratedRegex(@"<br[^>]*>", RegexOptions.Compiled)]
    method ItalicTagRegex (line 219) | [GeneratedRegex(@"</?i[^>]*>", RegexOptions.Compiled)]
    method BoldTagRegex (line 222) | [GeneratedRegex(@"</?b[^>]*>", RegexOptions.Compiled)]
    method AnchorTagRegex (line 225) | [GeneratedRegex(@"</?a[^>]*>", RegexOptions.Compiled)]
    method AllHtmlTagsRegex (line 228) | [GeneratedRegex(@"<[^>]*>", RegexOptions.Compiled)]

FILE: Tubifarry/Metadata/Lyrics/TrackFileRepositoryHelper.cs
  class TrackFileRepositoryHelper (line 17) | public sealed class TrackFileRepositoryHelper : BasicRepository<TrackFile>
    method TrackFileRepositoryHelper (line 24) | public TrackFileRepositoryHelper(
    method GetTracksWithoutLrcFilesCount (line 42) | public int GetTracksWithoutLrcFilesCount()
    method GetTracksWithoutLrcFilesBatch (line 75) | public List<TrackFile> GetTracksWithoutLrcFilesBatch(int offset, int l...
    method CreateAndUpsertLyricFile (line 114) | public LyricFile? CreateAndUpsertLyricFile(Artist artist, TrackFile tr...
    method Builder (line 151) | public new SqlBuilder Builder() => base.Builder();
    method Query (line 156) | public new List<TrackFile> Query(SqlBuilder builder) => base.Query(bui...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/AlbumMapper.cs
  class AlbumMapper (line 10) | public static class AlbumMapper
    method GetLinkNameFromUrl (line 70) | public static string GetLinkNameFromUrl(string url)
    method DetermineSecondaryTypesFromTitle (line 102) | public static List<SecondaryAlbumType> DetermineSecondaryTypesFromTitl...
    method FilterAlbums (line 147) | public static List<Album> FilterAlbums(IEnumerable<Album> albums, int ...
    method MapAlbumTypes (line 167) | public static void MapAlbumTypes(IEnumerable<string>? formatDescriptio...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/CustomLidarr/CustomLidarrMetadataProxy.cs
  class CustomLidarrMetadataProxy (line 13) | [Proxy(ProxyMode.Public)]
    method CustomLidarrMetadataProxy (line 29) | public CustomLidarrMetadataProxy(IConfigService configService, ILidarr...
    method SearchForNewAlbum (line 36) | public List<Album> SearchForNewAlbum(string title, string artist) =>
    method SearchForNewArtist (line 39) | public List<Artist> SearchForNewArtist(string title) =>
    method SearchForNewEntity (line 42) | public List<object> SearchForNewEntity(string title) =>
    method GetAlbumInfo (line 45) | public Tuple<string, Album, List<ArtistMetadata>> GetAlbumInfo(string ...
    method GetArtistInfo (line 48) | public Artist GetArtistInfo(string lidarrId, int metadataProfileId) =>
    method GetChangedAlbums (line 51) | public HashSet<string> GetChangedAlbums(DateTime startTime) =>
    method GetChangedArtists (line 54) | public HashSet<string> GetChangedArtists(DateTime startTime) =>
    method SearchForNewAlbumByRecordingIds (line 57) | public List<Album> SearchForNewAlbumByRecordingIds(List<string> record...
    method GetRequestBuilder (line 60) | public IHttpRequestBuilderFactory GetRequestBuilder()
    method CanHandleSearch (line 76) | public MetadataSupportLevel CanHandleSearch(string? albumTitle, string...
    method CanHandleIRecordingIds (line 87) | public MetadataSupportLevel CanHandleIRecordingIds(params string[] rec...
    method CanHandleChanged (line 92) | public MetadataSupportLevel CanHandleChanged()
    method SupportsLink (line 104) | public string? SupportsLink(List<Links> links)
    method CanHandleId (line 125) | public MetadataSupportLevel CanHandleId(string id)

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/CustomLidarr/CustomLidarrMetadataProxySettings.cs
  class CustomLidarrMetadataProxySettingsValidator (line 9) | public class CustomLidarrMetadataProxySettingsValidator : AbstractValida...
    method CustomLidarrMetadataProxySettingsValidator (line 13) | public CustomLidarrMetadataProxySettingsValidator()
    method NotBeRestrictedDomain (line 30) | private static bool NotBeRestrictedDomain(string url) => !_restrictedD...
  class CustomLidarrMetadataProxySettings (line 34) | public class CustomLidarrMetadataProxySettings : IProviderConfig
    method CustomLidarrMetadataProxySettings (line 47) | public CustomLidarrMetadataProxySettings() => Instance = this;
    method Validate (line 51) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/CustomLidarr/CustomLidarrProxy.cs
  class CustomLidarrProxy (line 18) | public partial class CustomLidarrProxy : ICustomLidarrProxy
    method CustomLidarrProxy (line 30) | public CustomLidarrProxy(IHttpClient httpClient,
    method GetChangedArtists (line 45) | public HashSet<string> GetChangedArtists(CustomLidarrMetadataProxySett...
    method GetArtistInfo (line 65) | public Artist GetArtistInfo(CustomLidarrMetadataProxySettings settings...
    method GetChangedAlbums (line 107) | public HashSet<string> GetChangedAlbums(CustomLidarrMetadataProxySetti...
    method GetChangedAlbumsUncached (line 112) | private HashSet<string> GetChangedAlbumsUncached(CustomLidarrMetadataP...
    method FilterAlbums (line 132) | public IEnumerable<AlbumResource> FilterAlbums(IEnumerable<AlbumResour...
    method GetAlbumInfo (line 145) | public Tuple<string, Album, List<ArtistMetadata>> GetAlbumInfo(CustomL...
    method SearchNewArtist (line 182) | public List<Artist> SearchNewArtist(CustomLidarrMetadataProxySettings ...
    method SearchNewAlbum (line 244) | public List<Album> SearchNewAlbum(CustomLidarrMetadataProxySettings se...
    method SearchNewAlbumByRecordingIds (line 311) | public List<Album> SearchNewAlbumByRecordingIds(CustomLidarrMetadataPr...
    method SearchNewEntity (line 328) | public List<object> SearchNewEntity(CustomLidarrMetadataProxySettings ...
    method ExtractMbid (line 380) | public string? ExtractMbid(string? query)
    method MapSearchResult (line 391) | private Artist MapSearchResult(ArtistResource resource)
    method MapSearchResult (line 402) | private Album? MapSearchResult(AlbumResource resource)
    method MapSearchResult (line 424) | private object? MapSearchResult(EntityResource resource)
    method MapAlbum (line 440) | private static Album MapAlbum(AlbumResource resource, Dictionary<strin...
    method MapRelease (line 485) | private static AlbumRelease MapRelease(ReleaseResource resource, Dicti...
    method MapMedium (line 526) | private static Medium MapMedium(MediumResource resource) => new()
    method MapTrack (line 533) | private static Track MapTrack(TrackResource resource, Dictionary<strin...
    method MapArtistMetadata (line 547) | private static ArtistMetadata MapArtistMetadata(ArtistResource resourc...
    method MapArtistStatus (line 563) | private static ArtistStatusType MapArtistStatus(string status)
    method MapRatings (line 578) | private static Ratings MapRatings(RatingResource rating)
    method MapImage (line 592) | private static MediaCover MapImage(ImageResource arg)
    method MapLink (line 601) | private static Links MapLink(LinkResource arg) => new()
    method MapCoverType (line 607) | private static MediaCoverTypes MapCoverType(string coverType) => cover...
    method MapSecondaryTypes (line 618) | public static SecondaryAlbumType MapSecondaryTypes(string albumType) =...
    method IsMbidQuery (line 634) | public static bool IsMbidQuery(string? query) => MusicBrainzRegex().Is...
    method GetRequestBuilder (line 636) | private static IHttpRequestBuilderFactory GetRequestBuilder(CustomLida...
    method MusicBrainzRegex (line 637) | [GeneratedRegex(@"\b(?:lidarr:|lidarrid:|mbid:|cl:|clid:|customlidarri...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/CustomLidarr/ICustomLidarrProxy.cs
  type ICustomLidarrProxy (line 5) | public interface ICustomLidarrProxy
    method SearchNewAlbum (line 7) | List<Album> SearchNewAlbum(CustomLidarrMetadataProxySettings settings,...
    method SearchNewArtist (line 9) | List<Artist> SearchNewArtist(CustomLidarrMetadataProxySettings setting...
    method SearchNewEntity (line 11) | List<object> SearchNewEntity(CustomLidarrMetadataProxySettings setting...
    method SearchNewAlbumByRecordingIds (line 13) | List<Album> SearchNewAlbumByRecordingIds(CustomLidarrMetadataProxySett...
    method GetAlbumInfo (line 15) | Tuple<string, Album, List<ArtistMetadata>> GetAlbumInfo(CustomLidarrMe...
    method GetArtistInfo (line 17) | Artist GetArtistInfo(CustomLidarrMetadataProxySettings settings, strin...
    method GetChangedAlbums (line 19) | HashSet<string> GetChangedAlbums(CustomLidarrMetadataProxySettings set...
    method GetChangedArtists (line 21) | HashSet<string> GetChangedArtists(CustomLidarrMetadataProxySettings se...
    method ExtractMbid (line 23) | string? ExtractMbid(string? query);

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerAPIService.cs
  class DeezerApiService (line 11) | public class DeezerApiService
    method DeezerApiService (line 31) | public DeezerApiService(IHttpClient httpClient, string userAgent)
    method BuildRequest (line 43) | private HttpRequestBuilder BuildRequest(string endpoint)
    method ExecuteRequestWithRetryAsync (line 59) | private async Task<JsonElement> ExecuteRequestWithRetryAsync(HttpReque...
    method HandleErrorResponse (line 106) | private void HandleErrorResponse(HttpResponse response)
    method FetchPaginatedResultsAsync (line 127) | private async Task<List<T>?> FetchPaginatedResultsAsync<T>(HttpRequest...
    method GetPaginatedDataAsync (line 161) | private async Task<List<T>?> GetPaginatedDataAsync<T>(string endpoint,...
    method GetAlbumAsync (line 167) | public async Task<DeezerAlbum?> GetAlbumAsync(long albumId)
    method GetArtistAsync (line 174) | public async Task<DeezerArtist?> GetArtistAsync(long artistId)
    method GetChartAsync (line 180) | public async Task<DeezerChart?> GetChartAsync(int chartId = 0)
    method GetEditorialAsync (line 186) | public async Task<DeezerEditorial?> GetEditorialAsync(long editorialId)
    method GetGenreAsync (line 192) | public async Task<DeezerGenre?> GetGenreAsync(long genreId)
    method ListGenresAsync (line 198) | public async Task<List<DeezerGenre>?> ListGenresAsync()
    method GetPlaylistAsync (line 204) | public async Task<DeezerPlaylist?> GetPlaylistAsync(long playlistId)
    method GetPodcastAsync (line 210) | public async Task<DeezerPodcast?> GetPodcastAsync(long podcastId)
    method GetRadioAsync (line 216) | public async Task<DeezerRadio?> GetRadioAsync(long radioId)
    method GetTrackAsync (line 222) | public async Task<DeezerTrack?> GetTrackAsync(long trackId)
    method GetUserAsync (line 228) | public async Task<DeezerUser?> GetUserAsync(long? userId = null)
    method GetOptionsAsync (line 235) | public async Task<DeezerOptions?> GetOptionsAsync()
    method SearchAsync (line 253) | public async Task<List<T>?> SearchAsync<T>(DeezerSearchParameter searc...
    method BuildAdvancedSearchQuery (line 265) | private static string BuildAdvancedSearchQuery(DeezerSearchParameter s...
    method SearchAsync (line 289) | public async Task<List<DeezerSearchItem>?> SearchAsync(DeezerSearchPar...
    method GetChartDataAsync (line 305) | public async Task<List<T>?> GetChartDataAsync<T>(int? maxPages = null)...
    method GetArtistDataAsync (line 322) | public async Task<List<T>?> GetArtistDataAsync<T>(long artistId, int? ...
    method GetAlbumDataAsync (line 333) | public async Task<List<T>?> GetAlbumDataAsync<T>(long albumId, int? ma...
    method GetRadioListsAsync (line 342) | public Task<List<DeezerRadio>?> GetRadioListsAsync(int? maxPages = nul...
    method GetRadioTopAsync (line 345) | public Task<List<DeezerRadio>?> GetRadioTopAsync(int? maxPages = null) =>
    method GetRadioGenresAsync (line 348) | public Task<List<DeezerGenre>?> GetRadioGenresAsync(int? maxPages = nu...
    method GetPlaylistRadioAsync (line 351) | public Task<List<DeezerRadio>?> GetPlaylistRadioAsync(long playlistId,...
    method GetRadiosAsync (line 354) | public Task<List<DeezerRadio>?> GetRadiosAsync(int? maxPages = null) =>
    method GetPlaylistTracksAsync (line 357) | public Task<List<DeezerTrack>?> GetPlaylistTracksAsync(long playlistId...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerAuthService.cs
  class DeezerAuthService (line 12) | public class DeezerAuthService
    method DeezerAuthService (line 17) | public DeezerAuthService(IHttpClient httpClient)
    method AuthenticateAsync (line 26) | public async Task<string?> AuthenticateAsync(string appId, string appS...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerMappingHelper.cs
  class DeezerMappingHelper (line 9) | public static class DeezerMappingHelper
    method MapAlbumFromDeezerAlbum (line 16) | public static Album MapAlbumFromDeezerAlbum(DeezerAlbum dAlbum, Artist...
    method MapArtistFromDeezerArtist (line 94) | public static Artist MapArtistFromDeezerArtist(DeezerArtist dArtist)
    method GetArtistImages (line 127) | private static List<MediaCover> GetArtistImages(DeezerArtist artist)
    method MapTrack (line 138) | public static Track MapTrack(DeezerTrack dTrack, Album album, AlbumRel...
    method MergeAlbums (line 158) | public static Album MergeAlbums(Album existingAlbum, Album mappedAlbum)

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerMetadataProxy.cs
  class DeezerMetadataProxy (line 10) | [Proxy(ProxyMode.Public)]
    method DeezerMetadataProxy (line 24) | public DeezerMetadataProxy(IDeezerProxy deezerProxy, Logger logger)
    method SearchForNewAlbum (line 30) | public List<Album> SearchForNewAlbum(string title, string artist) => _...
    method SearchForNewArtist (line 32) | public List<Artist> SearchForNewArtist(string title) => _deezerProxy.S...
    method SearchForNewEntity (line 34) | public List<object> SearchForNewEntity(string title) => _deezerProxy.S...
    method GetAlbumInfo (line 36) | public Tuple<string, Album, List<ArtistMetadata>> GetAlbumInfo(string ...
    method GetArtistInfo (line 38) | public Artist GetArtistInfo(string lidarrId, int metadataProfileId) =>...
    method GetChangedAlbums (line 40) | public HashSet<string> GetChangedAlbums(DateTime startTime)
    method GetChangedArtists (line 46) | public HashSet<string> GetChangedArtists(DateTime startTime)
    method SearchForNewAlbumByRecordingIds (line 52) | public List<Album> SearchForNewAlbumByRecordingIds(List<string> record...
    method CanHandleSearch (line 58) | public MetadataSupportLevel CanHandleSearch(string? albumTitle, string...
    method CanHandleId (line 69) | public MetadataSupportLevel CanHandleId(string id)
    method CanHandleIRecordingIds (line 77) | public MetadataSupportLevel CanHandleIRecordingIds(params string[] rec...
    method CanHandleChanged (line 82) | public MetadataSupportLevel CanHandleChanged()
    method SupportsLink (line 87) | public string? SupportsLink(List<Links> links)
    method DeezerRegex (line 105) | [GeneratedRegex(@"deezer\.com\/(?:album|artist|track)\/(\d+)", RegexOp...
    method FormatRegex (line 108) | [GeneratedRegex(@"^\s*\w+:\s*\w+", RegexOptions.Compiled)]

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerMetadataProxySettings.cs
  class DeezerMetadataProxySettingsValidator (line 10) | public class DeezerMetadataProxySettingsValidator : AbstractValidator<De...
    method DeezerMetadataProxySettingsValidator (line 12) | public DeezerMetadataProxySettingsValidator()
  class DeezerMetadataProxySettings (line 44) | public class DeezerMetadataProxySettings : IProviderConfig
    method DeezerMetadataProxySettings (line 65) | public DeezerMetadataProxySettings() => Instance = this;
    method Validate (line 69) | public NzbDroneValidationResult Validate() => new(Validator.Validate(t...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerProxy.cs
  class DeezerProxy (line 10) | public class DeezerProxy : IDeezerProxy
    method DeezerProxy (line 20) | public DeezerProxy(Logger logger, IHttpClient httpClient, IArtistServi...
    method UpdateCache (line 30) | private void UpdateCache(DeezerMetadataProxySettings settings)
    method CachedSearchAsync (line 36) | private async Task<List<TReturn>> CachedSearchAsync<TReturn, TSearch>(...
    method GetArtistAlbumsAsync (line 52) | public async Task<List<Album>> GetArtistAlbumsAsync(DeezerMetadataProx...
    method SearchNewAlbum (line 74) | public List<Album> SearchNewAlbum(DeezerMetadataProxySettings settings...
    method SearchNewArtist (line 98) | public List<Artist> SearchNewArtist(DeezerMetadataProxySettings settin...
    method SearchNewEntity (line 109) | public List<object> SearchNewEntity(DeezerMetadataProxySettings settin...
    method GetAlbumInfoAsync (line 164) | public async Task<Tuple<string, Album, List<ArtistMetadata>>> GetAlbum...
    method EnsureAllTracksAsync (line 192) | private async Task<DeezerAlbum> EnsureAllTracksAsync(DeezerAlbum album...
    method GetArtistInfoAsync (line 205) | public async Task<Artist> GetArtistInfoAsync(DeezerMetadataProxySettin...
    method IsDeezerIdQuery (line 228) | public static bool IsDeezerIdQuery(string? query) => query?.StartsWith...
    method SanitizeToUnicode (line 230) | private static string SanitizeToUnicode(string input) => string.IsNull...
    method RemoveIdentifier (line 232) | private static string RemoveIdentifier(string input) => input.EndsWith...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/DeezerRecords.cs
  type ExplicitContent (line 6) | public enum ExplicitContent
  type DeezerAlbum (line 18) | public record DeezerAlbum(
  type DeezerFallbackAlbum (line 50) | public record DeezerFallbackAlbum(
  type DeezerArtist (line 55) | public record DeezerArtist(
  type DeezerChart (line 72) | public record DeezerChart(
  type DeezerEditorial (line 82) | public record DeezerEditorial(
  type DeezerEpisode (line 92) | public record DeezerEpisode(
  type DeezerGenresWrapper (line 110) | public record DeezerGenresWrapper(
  type DeezerGenre (line 114) | public record DeezerGenre(
  type DeezerPlaylist (line 121) | public record DeezerPlaylist(
  type DeezerOptions (line 144) | public record DeezerOptions(
  type DeezerPodcast (line 159) | public record DeezerPodcast(
  type DeezerRadio (line 174) | public record DeezerRadio(
  type DeezerTrackWrapper (line 188) | public record DeezerTrackWrapper(
  type DeezerTrack (line 192) | public record DeezerTrack(
  type DeezerUser (line 223) | public record DeezerUser(
  type DeezerSearchParameter (line 252) | public record DeezerSearchParameter(
  type DeezerSearchItem (line 284) | public record DeezerSearchItem(

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Deezer/IDeezerProxy.cs
  type IDeezerProxy (line 5) | public interface IDeezerProxy
    method SearchNewAlbum (line 7) | List<Album> SearchNewAlbum(DeezerMetadataProxySettings settings, strin...
    method SearchNewArtist (line 9) | List<Artist> SearchNewArtist(DeezerMetadataProxySettings settings, str...
    method SearchNewEntity (line 11) | List<object> SearchNewEntity(DeezerMetadataProxySettings settings, str...
    method GetAlbumInfoAsync (line 13) | Task<Tuple<string, Album, List<ArtistMetadata>>> GetAlbumInfoAsync(Dee...
    method GetArtistInfoAsync (line 15) | Task<Artist> GetArtistInfoAsync(DeezerMetadataProxySettings settings, ...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Discogs/DiscogsAPIService.cs
  class DiscogsApiService (line 11) | public class DiscogsApiService
    method DiscogsApiService (line 31) | public DiscogsApiService(IHttpClient httpClient, string userAgent)
    method GetReleaseAsync (line 39) | public async Task<DiscogsRelease?> GetReleaseAsync(int releaseId, stri...
    method GetMasterReleaseAsync (line 48) | public async Task<DiscogsMasterRelease?> GetMasterReleaseAsync(int mas...
    method GetMasterVersionsAsync (line 55) | public async Task<List<DiscogsMasterReleaseVersion>> GetMasterVersions...
    method GetArtistAsync (line 68) | public async Task<DiscogsArtist?> GetArtistAsync(int artistId)
    method GetArtistReleasesAsync (line 74) | public async Task<List<DiscogsArtistRelease>> GetArtistReleasesAsync(i...
    method GetLabelAsync (line 82) | public async Task<DiscogsLabel?> GetLabelAsync(int labelId)
    method GetLabelReleasesAsync (line 88) | public async Task<List<DiscogsLabelRelease>> GetLabelReleasesAsync(int...
    method SearchAsync (line 93) | public async Task<List<DiscogsSearchItem>> SearchAsync(DiscogsSearchPa...
    method GetReleaseStatsAsync (line 100) | public async Task<DiscogsStats?> GetReleaseStatsAsync(int releaseId)
    method GetCommunityRatingAsync (line 106) | public async Task<DiscogsRating?> GetCommunityRatingAsync(int releaseId)
    method BuildRequest (line 112) | private HttpRequestBuilder BuildRequest(string? endpoint)
    method ExecuteRequestWithRetryAsync (line 125) | private async Task<JsonElement> ExecuteRequestWithRetryAsync(HttpReque...
    method WaitForRateLimit (line 175) | private async Task WaitForRateLimit()
    method UpdateRateLimitTracking (line 191) | private void UpdateRateLimitTracking(HttpResponse response)
    method FetchPaginatedResultsAsync (line 210) | private async Task<List<T>?> FetchPaginatedResultsAsync<T>(HttpRequest...
    method AddSearchParams (line 246) | private static void AddSearchParams(HttpRequestBuilder requestBuilder,...
    method HandleErrorResponse (line 252) | private void HandleErrorResponse(HttpResponse response)
    method AddQueryParamIfNotNull (line 267) | private static HttpRequestBuilder AddQueryParamIfNotNull(HttpRequestBu...

FILE: Tubifarry/Metadata/Proxy/MetadataProvider/Discogs/DiscogsMappingHelper.cs
  class DiscogsMappingHelper (line 11) | public static partial class DiscogsMappingHelper
    method MapFormat (line 40) | private static string MapFormat(string discogsFormat)
    method ParseReleaseDate (line 50) | public static DateTime? ParseReleaseDate(DiscogsRelease release)
    method ParseReleaseDate (line 60) | public static DateTime? ParseReleaseDate(int? year) => year > 0 ? new ...
    method DetermineMedia (line 65) | private static List<Medium> DetermineMedia(List<DiscogsTrack>? trackli...
    method ParseTrackPosition (line 91) | private static DiscogsTrackPosition ParseTrackPosition(string? positio...
    method ParseDuration (line 151) | public static int ParseDuration(string duration)
    method MapImage (line 166) | public static MediaCover? MapImage(DiscogsImage img, bool isArtist) =>...
    method MapCoverType (line 176) | public static MediaCoverTypes MapCoverType(string? type, bool isArtist)
    method MapAlbumTypes (line 200) | public static void MapAlbumTypes(DiscogsRelease release, Album album)
    method MapAlbumTypes (line 225) | public static void MapAlbumTypes(DiscogsArtistRelease release, Album a...
    method InferTypeFromPhysicalFormat (line 241) | private static string? InferTypeFromPhysicalFormat(List<string?>? phys...
    method InferTypeFromMetadata (line 266) | private static string InferTypeFromMetadata(int trackCount, int totalD...
    method MapAlbumFromMasterRelease (line 291) | public static Album MapAlbumFromMasterRelease(DiscogsMasterRelease mas...
    method MapAlbumFromRelease (line 336) | public static Album MapAlbumFromRelease(DiscogsRelease release)
    method MapTrack (line 381) | public static Track MapTrack(DiscogsTrack t, DiscogsMasterRelease mast...
    method MapTrack (line 409) | public static Track MapTrack(DiscogsTrack t, DiscogsRelease release, A...
    method MapAlbumFromArtistRelease (line 437) | public static Album MapAlbumFromArtistRelease(DiscogsArtistRelease rel...
    method MapArtistFromDiscogsArtist (line 474) | public static Artist MapArtistFromDiscogsArtist(DiscogsArtist discogsA...
    method MergeAlbums (line 494) | public static Album MergeAlbums(Album existingAlbum, Album mappedAlbum)
    method MapTracks (line 506) | public static List<Track> MapTracks(object releaseForTracks, Album alb...
    method FilterTracklist (line 523) | private static List<DiscogsTrack> FilterTracklist(List<DiscogsTrack>? ...
    method BuildArtistOverview (line 531) | private static string BuildArtistOverview(DiscogsArtist discogsArtist)
    method MapArtistFromSearchItem (line 556) | public static Artist MapArtistFromSearchItem(DiscogsSearchItem searchI...
    method MapDiscogsMember (line 570) | private static Member MapDiscogsMember(DiscogsMember discogsMember) =>...
    method ComputeCommunityRating (line 572) | public static Ratings ComputeCommunityRating(DiscogsCommunityInfo? com...
    method PositionPattern (line 597) | [GeneratedRegex(@"^(?:[A-Za-z]+(?=\d))?(?<letters>[A-Za-z]+)?(?<fir
Condensed preview — 282 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,758K chars).
[
  {
    "path": ".gitattributes",
    "chars": 2518,
    "preview": "###############################################################################\n# Set default behavior to automatically "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 3857,
    "preview": "name: \"Bug Report\"\ndescription: Report a bug or unexpected behavior in Tubifarry\ntitle: \"[BUG] \"\nlabels: [\"bug\"]\nassigne"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 390,
    "preview": "# .github/ISSUE_TEMPLATE/config.yml\nblank_issues_enabled: false\n\ncontact_links:\n  - name: \"Questions & Support\"\n    url:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/documentation_improvement.yml",
    "chars": 2835,
    "preview": "name: \"Documentation Improvement\"\ndescription: Report issues or suggest improvements for Tubifarry documentation\ntitle: "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 2573,
    "preview": "name: \"Feature Request\"\ndescription: Suggest an idea or improvement for Tubifarry\ntitle: \"[FEATURE] \"\nlabels: [\"enhancem"
  },
  {
    "path": ".github/workflows/branch-detection.yml",
    "chars": 4117,
    "preview": "name: Detect Branch from Tag or Reference\n\non:\n  workflow_call:\n    inputs:\n      github_ref:\n        required: true\n   "
  },
  {
    "path": ".github/workflows/compilation.yml",
    "chars": 8692,
    "preview": "name: Compile Project\n\non:\n  workflow_call:\n    inputs:\n      dotnet_version:\n        required: true\n        type: strin"
  },
  {
    "path": ".github/workflows/dotnet-setup.yml",
    "chars": 2622,
    "preview": "name: Setup .NET Environment\n\non:\n  workflow_call:\n    inputs:\n      dotnet_version:\n        required: true\n        type"
  },
  {
    "path": ".github/workflows/git-info.yml",
    "chars": 7363,
    "preview": "name: Extract Git Information\n\non:\n  workflow_call:\n    inputs:\n      tag_ref:\n        required: true\n        type: stri"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 4552,
    "preview": "name: Build Plugin  \n\non:   \n  push:     \n    tags:       \n      - 'v*'  \n\nconcurrency:   \n  group: ${{ github.workflow "
  },
  {
    "path": ".github/workflows/metadata.yml",
    "chars": 6112,
    "preview": "name: Extract Project Metadata\n\non:\n  workflow_call:\n    inputs:\n      override_plugin_name:\n        required: false\n   "
  },
  {
    "path": ".github/workflows/packaging.yml",
    "chars": 3245,
    "preview": "name: Package Plugin\n\non:\n  workflow_call:\n    inputs:\n      plugin_name:\n        required: true\n        type: string\n  "
  },
  {
    "path": ".github/workflows/publishing.yml",
    "chars": 4450,
    "preview": "name: Publish GitHub Release\n\non:\n  workflow_call:\n    inputs:\n      plugin_name:\n        required: true\n        type: s"
  },
  {
    "path": ".github/workflows/release-notes.yml",
    "chars": 3107,
    "preview": "name: Generate Release Notes\n\non:\n  workflow_call:\n    inputs:\n      plugin_name:\n        required: true\n        type: s"
  },
  {
    "path": ".gitignore",
    "chars": 6370,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": ".gitmodules",
    "chars": 99,
    "preview": "[submodule \"Submodules/Lidarr\"]\n\tpath = Submodules/Lidarr\n\turl = https://github.com/Lidarr/Lidarr/\n"
  },
  {
    "path": "CONTRIBUTION.md",
    "chars": 17016,
    "preview": "# Contributing to Tubifarry\n\nThank you for your interest in contributing to Tubifarry! This guide\nwill help you set up y"
  },
  {
    "path": "Directory.Build.props",
    "chars": 4469,
    "preview": "<Project>\n\t<!-- Common to all Lidarr Projects -->\n\t<PropertyGroup>\n\t<!--\t<TreatWarningsAsErrors>true</TreatWarningsAsErr"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1064,
    "preview": "MIT License\n\nCopyright (c) 2026 TypNull\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof"
  },
  {
    "path": "NuGet.config",
    "chars": 599,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<configuration>\n  <packageSources>\n    <clear />\n    <add key=\"nuget.org\" value=\""
  },
  {
    "path": "README.md",
    "chars": 16501,
    "preview": "# Tubifarry for Lidarr 🎶\n![Downloads](https://img.shields.io/github/downloads/TypNull/Tubifarry/total)  ![GitHub release"
  },
  {
    "path": "Tubifarry/Blocklisting/BaseBlocklist.cs",
    "chars": 1748,
    "preview": "using NzbDrone.Common.Extensions;\nusing NzbDrone.Core.Blocklisting;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.In"
  },
  {
    "path": "Tubifarry/Blocklisting/Blocklists.cs",
    "chars": 833,
    "preview": "using NzbDrone.Core.Blocklisting;\nusing NzbDrone.Core.Indexers;\n\nnamespace Tubifarry.Blocklisting\n{\n    public class You"
  },
  {
    "path": "Tubifarry/Core/Model/AlbumData.cs",
    "chars": 5506,
    "preview": "using NzbDrone.Core.Parser.Model;\nusing System.Text.RegularExpressions;\nusing Tubifarry.Core.Utilities;\n\nnamespace Tubi"
  },
  {
    "path": "Tubifarry/Core/Model/ApiCircuitBreaker.cs",
    "chars": 4662,
    "preview": "using System.Collections.Concurrent;\nusing System.Runtime.CompilerServices;\n\nnamespace Tubifarry.Core.Model\n{\n    publi"
  },
  {
    "path": "Tubifarry/Core/Model/AudioMetadataHandler.cs",
    "chars": 32159,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Music;\nusing Tubifarry.Core.Records;\nusing Tubifa"
  },
  {
    "path": "Tubifarry/Core/Model/FileCache.cs",
    "chars": 5189,
    "preview": "using System.Runtime.InteropServices;\nusing System.Security.Cryptography;\nusing System.Text;\nusing System.Text.Json;\n\nn"
  },
  {
    "path": "Tubifarry/Core/Model/PlaylistItem.cs",
    "chars": 442,
    "preview": "namespace Tubifarry.Core.Model;\n\npublic record PlaylistItem(\n    string ArtistMusicBrainzId,\n    string? AlbumMusicBrain"
  },
  {
    "path": "Tubifarry/Core/Model/TrustedSessionException.cs",
    "chars": 498,
    "preview": "namespace Tubifarry.Core.Model\n{\n    /// <summary>\n    /// Exceptions specific to the YouTube trusted session authentic"
  },
  {
    "path": "Tubifarry/Core/Records/Lyric.cs",
    "chars": 2616,
    "preview": "using DownloadAssistant.Base;\nusing Newtonsoft.Json;\nusing Newtonsoft.Json.Linq;\nusing NzbDrone.Core.Parser.Model;\nusin"
  },
  {
    "path": "Tubifarry/Core/Records/MappingAgent.cs",
    "chars": 1064,
    "preview": "namespace Tubifarry.Core.Records\n{\n    public record MappingAgent\n    {\n        public string UserAgent { get; set; } ="
  },
  {
    "path": "Tubifarry/Core/Records/MusicBrainzData.cs",
    "chars": 2374,
    "preview": "using System.Xml.Linq;\n\nnamespace Tubifarry.Core.Records\n{\n    public record MusicBrainzSearchItem(string? Title, strin"
  },
  {
    "path": "Tubifarry/Core/Records/YouTubeSession.cs",
    "chars": 2753,
    "preview": "using System.Net;\n\nnamespace Tubifarry.Core.Records\n{\n    /// <summary>\n    /// Represents session token data for trans"
  },
  {
    "path": "Tubifarry/Core/Replacements/ExtendedHttpIndexerBase.cs",
    "chars": 15521,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/Core/Replacements/FlexibleHttpDispatcher.cs",
    "chars": 3173,
    "preview": "using NLog;\nusing NzbDrone.Common.Cache;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone.C"
  },
  {
    "path": "Tubifarry/Core/Replacements/IIndexerRequestGenerator.cs",
    "chars": 1026,
    "preview": "using NzbDrone.Core.Indexers;\nusing NzbDrone.Core.IndexerSearch.Definitions;\nusing Tubifarry.Core.Utilities;\n\nnamespace"
  },
  {
    "path": "Tubifarry/Core/Telemetry/ISearchContextBuffer.cs",
    "chars": 1397,
    "preview": "namespace Tubifarry.Core.Telemetry\n{\n    public interface ISearchContextBuffer\n    {\n        void LogSearch(string searc"
  },
  {
    "path": "Tubifarry/Core/Telemetry/ISentryHelper.cs",
    "chars": 2203,
    "preview": "namespace Tubifarry.Core.Telemetry\n{\n    public interface ISentryHelper\n    {\n        bool IsEnabled { get; }\n\n        /"
  },
  {
    "path": "Tubifarry/Core/Telemetry/NoopSentryHelper.cs",
    "chars": 2357,
    "preview": "namespace Tubifarry.Core.Telemetry\n{\n    public class NoopSentryHelper : ISentryHelper\n    {\n        public bool IsEnabl"
  },
  {
    "path": "Tubifarry/Core/Telemetry/SearchContextBuffer.cs",
    "chars": 9995,
    "preview": "#if !MASTER_BRANCH\nusing NzbDrone.Common.Instrumentation;\nusing System.Collections.Concurrent;\n\nnamespace Tubifarry.Core"
  },
  {
    "path": "Tubifarry/Core/Telemetry/SentryEventFilter.cs",
    "chars": 3953,
    "preview": "#if !MASTER_BRANCH\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Net.Http;\nusing Syste"
  },
  {
    "path": "Tubifarry/Core/Telemetry/SentryHelper.cs",
    "chars": 5585,
    "preview": "#if !MASTER_BRANCH\nnamespace Tubifarry.Core.Telemetry\n{\n    public class SentryHelper : ISentryHelper\n    {\n        priv"
  },
  {
    "path": "Tubifarry/Core/Telemetry/SlskdBufferedContext.cs",
    "chars": 2848,
    "preview": "namespace Tubifarry.Core.Telemetry\n{\n    public class SlskdBufferedContext\n    {\n        // Search phase\n        public "
  },
  {
    "path": "Tubifarry/Core/Telemetry/SlskdSentryEvents.cs",
    "chars": 13231,
    "preview": "#if !MASTER_BRANCH\nnamespace Tubifarry.Core.Telemetry\n{\n    public static class SlskdSentryEvents\n    {\n        public e"
  },
  {
    "path": "Tubifarry/Core/Telemetry/SlskdTrackingService.cs",
    "chars": 7741,
    "preview": "#if !MASTER_BRANCH\nusing NLog;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Download.TrackedDownloads;\nusing NzbDro"
  },
  {
    "path": "Tubifarry/Core/Telemetry/TubifarrySentry.cs",
    "chars": 3961,
    "preview": "#if !MASTER_BRANCH\nusing NzbDrone.Common;\nusing NzbDrone.Common.EnvironmentInfo;\nusing System.Diagnostics;\nusing System."
  },
  {
    "path": "Tubifarry/Core/Telemetry/TubifarrySentryTarget.cs",
    "chars": 4102,
    "preview": "#if !MASTER_BRANCH\nusing NLog;\nusing NLog.Targets;\n\nnamespace Tubifarry.Core.Telemetry\n{\n    [Target(\"TubifarrySentry\")]"
  },
  {
    "path": "Tubifarry/Core/Utilities/AudioFormat.cs",
    "chars": 8442,
    "preview": "using Tubifarry.Download.Clients.YouTube;\n\nnamespace Tubifarry.Core.Utilities\n{\n    public enum AudioFormat\n    {\n     "
  },
  {
    "path": "Tubifarry/Core/Utilities/CacheService.cs",
    "chars": 4122,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Annotations;\nusing System.Collections.Concurrent"
  },
  {
    "path": "Tubifarry/Core/Utilities/CookieManager.cs",
    "chars": 2004,
    "preview": "using System.Net;\n\nnamespace Tubifarry.Core.Utilities\n{\n    internal static class CookieManager\n    {\n        internal "
  },
  {
    "path": "Tubifarry/Core/Utilities/DynamicSchemaInjector.cs",
    "chars": 1393,
    "preview": "using Lidarr.Http.ClientSchema;\nusing NzbDrone.Core.ThingiProvider;\nusing System.Reflection;\n\nnamespace Tubifarry.Core.U"
  },
  {
    "path": "Tubifarry/Core/Utilities/DynamicStateSettings.cs",
    "chars": 3716,
    "preview": "using Lidarr.Http.ClientSchema;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core"
  },
  {
    "path": "Tubifarry/Core/Utilities/FileInfoParser.cs",
    "chars": 9458,
    "preview": "using System.Text.RegularExpressions;\n\nnamespace Tubifarry.Core.Utilities\n{\n    /// <summary>\n    /// Was used for Slskd"
  },
  {
    "path": "Tubifarry/Core/Utilities/IndexerParserHelper.cs",
    "chars": 5498,
    "preview": "using NzbDrone.Core.Parser.Model;\nusing System.Text.Json;\nusing Tubifarry.Core.Model;\n\nnamespace Tubifarry.Core.Utilitie"
  },
  {
    "path": "Tubifarry/Core/Utilities/JsonConverters.cs",
    "chars": 4214,
    "preview": "using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Tubifarry.Core.Utilities\n{\n    /// <summary>\n "
  },
  {
    "path": "Tubifarry/Core/Utilities/LazyRequestChain.cs",
    "chars": 6794,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Indexers;\nusing System.Collections;\n\nnamespace T"
  },
  {
    "path": "Tubifarry/Core/Utilities/PermissionTester.cs",
    "chars": 4060,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\n\nnamespace Tubifarry.Core.Utilities\n{\n   "
  },
  {
    "path": "Tubifarry/Core/Utilities/PluginSettings.cs",
    "chars": 7692,
    "preview": "using NLog;\nusing NzbDrone.Common.EnvironmentInfo;\nusing NzbDrone.Common.Extensions;\nusing System.Collections.Concurren"
  },
  {
    "path": "Tubifarry/Core/Utilities/ReleaseFormatter.cs",
    "chars": 7407,
    "preview": "using NzbDrone.Core.Music;\nusing NzbDrone.Core.Organizer;\nusing NzbDrone.Core.Parser.Model;\nusing System.Text.RegularEx"
  },
  {
    "path": "Tubifarry/Core/Utilities/RepositorySettingsResolver.cs",
    "chars": 1480,
    "preview": "using Microsoft.Extensions.DependencyInjection;\nusing NLog;\nusing NzbDrone.Core.ThingiProvider;\n\nnamespace Tubifarry.Co"
  },
  {
    "path": "Tubifarry/Core/Utilities/UserAgentValidator.cs",
    "chars": 4637,
    "preview": "using System.Text.RegularExpressions;\n\nnamespace Tubifarry.Core.Utilities\n{\n    /// <summary>\n    /// Interface for Use"
  },
  {
    "path": "Tubifarry/Debug.targets",
    "chars": 1175,
    "preview": "<Project xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n\t<!-- Copy the files to the plugin folder -->\n\t<Ta"
  },
  {
    "path": "Tubifarry/Download/Base/BaseDownloadManager.cs",
    "chars": 3460,
    "preview": "using NLog;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Organizer;\nusing NzbDrone.C"
  },
  {
    "path": "Tubifarry/Download/Base/BaseDownloadOptions.cs",
    "chars": 2747,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Organizer;\nusing Requests.Options;\n\nnames"
  },
  {
    "path": "Tubifarry/Download/Base/BaseDownloadRequest.cs",
    "chars": 8803,
    "preview": "using DownloadAssistant.Requests;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Instrumentation;\nusing "
  },
  {
    "path": "Tubifarry/Download/Base/BaseHttpClient.cs",
    "chars": 14100,
    "preview": "using DownloadAssistant.Base;\nusing NzbDrone.Common.Http;\n\nnamespace Tubifarry.Download.Base\n{\n    /// <summary>\n    ///"
  },
  {
    "path": "Tubifarry/Download/Clients/DABMusic/DABMusicClient.cs",
    "chars": 5120,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core"
  },
  {
    "path": "Tubifarry/Download/Clients/DABMusic/DABMusicDownloadManager.cs",
    "chars": 2687,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core"
  },
  {
    "path": "Tubifarry/Download/Clients/DABMusic/DABMusicDownloadOptions.cs",
    "chars": 520,
    "preview": "using Tubifarry.Download.Base;\n\nnamespace Tubifarry.Download.Clients.DABMusic\n{\n    public record DABMusicDownloadOptio"
  },
  {
    "path": "Tubifarry/Download/Clients/DABMusic/DABMusicDownloadRequest.cs",
    "chars": 14729,
    "preview": "using DownloadAssistant.Options;\nusing DownloadAssistant.Requests;\nusing NLog;\nusing NzbDrone.Core.Datastore;\nusing NzbD"
  },
  {
    "path": "Tubifarry/Download/Clients/DABMusic/DABMusicProviderSettings.cs",
    "chars": 3783,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Valida"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/ILucidaRateLimiter.cs",
    "chars": 1251,
    "preview": "namespace Tubifarry.Download.Clients.Lucida;\n\npublic interface ILucidaRateLimiter\n{\n    #region Constants\n    public con"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaClient.cs",
    "chars": 4620,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaDownloadManager.cs",
    "chars": 2693,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaDownloadOptions.cs",
    "chars": 421,
    "preview": "using Tubifarry.Download.Base;\n\nnamespace Tubifarry.Download.Clients.Lucida\n{\n    public record LucidaDownloadOptions :"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaDownloadRequest.cs",
    "chars": 21440,
    "preview": "using DownloadAssistant.Options;\nusing DownloadAssistant.Requests;\nusing NLog;\nusing NzbDrone.Core.Music;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaInitiationResult.cs",
    "chars": 199,
    "preview": "namespace Tubifarry.Download.Clients.Lucida;\n\npublic sealed record LucidaInitiationResult\n{\n    public required string H"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaMetadataExtractor.cs",
    "chars": 18670,
    "preview": "using Jint;\nusing Jint.Native;\nusing NLog;\nusing NzbDrone.Common.Instrumentation;\nusing System.Net;\nusing System.Text.Js"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaProviderSettings.cs",
    "chars": 3607,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Validat"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaRateLimitException.cs",
    "chars": 478,
    "preview": "namespace Tubifarry.Download.Clients.Lucida\n{\n    public class LucidaRateLimitException : Exception\n    {\n        public"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaRateLimiter.cs",
    "chars": 8654,
    "preview": "using NLog;\nusing System.Collections.Concurrent;\nusing System.Text;\nusing System.Text.Json;\n\nnamespace Tubifarry.Downloa"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaTokenExtractor.cs",
    "chars": 3732,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing Tubifa"
  },
  {
    "path": "Tubifarry/Download/Clients/Lucida/LucidaWorkerState.cs",
    "chars": 386,
    "preview": "namespace Tubifarry.Download.Clients.Lucida;\n\npublic sealed record LucidaWorkerState\n{\n    public string Name { get; ini"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/ISlskdApiClient.cs",
    "chars": 1631,
    "preview": "using FluentValidation.Results;\nusing Tubifarry.Download.Clients.Soulseek.Models;\n\nnamespace Tubifarry.Download.Clients."
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/ISlskdDownloadManager.cs",
    "chars": 525,
    "preview": "using NzbDrone.Common.Disk;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Parser.Model;\n\nnamespace Tubifarry.Downloa"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/Models/DownloadKey.cs",
    "chars": 983,
    "preview": "namespace Tubifarry.Download.Clients.Soulseek.Models;\n\npublic readonly struct DownloadKey<TOuterKey, TInnerKey>(TOuterKe"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadDirectory.cs",
    "chars": 1422,
    "preview": "using System.Text.Json;\nusing Tubifarry.Indexers.Soulseek;\n\nnamespace Tubifarry.Download.Clients.Soulseek.Models;\n\npubli"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadFile.cs",
    "chars": 4096,
    "preview": "using System.Text.Json;\nusing Tubifarry.Indexers.Soulseek;\n\nnamespace Tubifarry.Download.Clients.Soulseek.Models;\n\npubli"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/Models/SlskdDownloadItem.cs",
    "chars": 3651,
    "preview": "using NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Download;\nusing NzbDr"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/Models/SlskdFileState.cs",
    "chars": 2785,
    "preview": "using NzbDrone.Core.Download;\n\nnamespace Tubifarry.Download.Clients.Soulseek.Models;\n\n[Flags]\npublic enum TransferStates"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdApiClient.cs",
    "chars": 9956,
    "preview": "using FluentValidation.Results;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Download.Clients;\nusing System.Net;\nusin"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdClient.cs",
    "chars": 2420,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdDownloadManager.cs",
    "chars": 20093,
    "preview": "using NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Download;\nusing NzbDr"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdProviderSettings.cs",
    "chars": 3973,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Valida"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdRetryHandler.cs",
    "chars": 2865,
    "preview": "using NLog;\nusing NzbDrone.Core.Download;\nusing System.Text.Json;\nusing Tubifarry.Core.Telemetry;\nusing Tubifarry.Downlo"
  },
  {
    "path": "Tubifarry/Download/Clients/Soulseek/SlskdStatusResolver.cs",
    "chars": 5287,
    "preview": "using NzbDrone.Core.Download;\nusing Tubifarry.Download.Clients.Soulseek.Models;\n\nnamespace Tubifarry.Download.Clients.So"
  },
  {
    "path": "Tubifarry/Download/Clients/SubSonic/SubSonicClient.cs",
    "chars": 6460,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Download/Clients/SubSonic/SubSonicDownloadManager.cs",
    "chars": 3834,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Download/Clients/SubSonic/SubSonicDownloadOptions.cs",
    "chars": 1460,
    "preview": "using Tubifarry.Download.Base;\n\nnamespace Tubifarry.Download.Clients.SubSonic\n{\n    /// <summary>\n    /// Download optio"
  },
  {
    "path": "Tubifarry/Download/Clients/SubSonic/SubSonicDownloadRequest.cs",
    "chars": 15565,
    "preview": "using DownloadAssistant.Options;\nusing DownloadAssistant.Requests;\nusing NLog;\nusing NzbDrone.Core.Datastore;\nusing NzbD"
  },
  {
    "path": "Tubifarry/Download/Clients/SubSonic/SubSonicProviderSettings.cs",
    "chars": 5804,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Validat"
  },
  {
    "path": "Tubifarry/Download/Clients/TripleTriple/TripleTripleClient.cs",
    "chars": 4161,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadManager.cs",
    "chars": 2715,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadOptions.cs",
    "chars": 1014,
    "preview": "using Tubifarry.Download.Base;\nusing Tubifarry.Indexers.TripleTriple;\n\nnamespace Tubifarry.Download.Clients.TripleTriple"
  },
  {
    "path": "Tubifarry/Download/Clients/TripleTriple/TripleTripleDownloadRequest.cs",
    "chars": 17100,
    "preview": "using DownloadAssistant.Options;\nusing DownloadAssistant.Requests;\nusing NLog;\nusing NzbDrone.Core.Datastore;\nusing NzbD"
  },
  {
    "path": "Tubifarry/Download/Clients/TripleTriple/TripleTripleProviderSettings.cs",
    "chars": 4644,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Validat"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/SponsorBlock.cs",
    "chars": 9590,
    "preview": "using NLog;\nusing System.Globalization;\nusing System.Net;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/TrustedSessionHelper.cs",
    "chars": 20835,
    "preview": "using DownloadAssistant.Base;\nusing NLog;\nusing NzbDrone.Common.Instrumentation;\nusing System.Net;\nusing System.Net.Http"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/YouTubeDownloadOptions.cs",
    "chars": 2188,
    "preview": "using Tubifarry.Download.Base;\nusing YouTubeMusicAPI.Client;\n\nnamespace Tubifarry.Download.Clients.YouTube\n{\n    /// <s"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/YouTubeDownloadRequest.cs",
    "chars": 12674,
    "preview": "using DownloadAssistant.Base;\nusing DownloadAssistant.Options;\nusing DownloadAssistant.Requests;\nusing NLog;\nusing NzbDr"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/YoutubeClient.cs",
    "chars": 4228,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Disk;\nusing NzbDrone.Common.Extensions;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/YoutubeDownloadManager.cs",
    "chars": 3178,
    "preview": "using NLog;\nusing NzbDrone.Core.Download;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Organizer;\nusing NzbDrone.C"
  },
  {
    "path": "Tubifarry/Download/Clients/YouTube/YoutubeProviderSettings.cs",
    "chars": 7906,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ThingiProvider;\nusing NzbDrone.Core.Valida"
  },
  {
    "path": "Tubifarry/ILRepack.targets",
    "chars": 1580,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<Project xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n\t<Target "
  },
  {
    "path": "Tubifarry/ImportLists/ArrStack/ArrMedia.cs",
    "chars": 1479,
    "preview": "using System.Text.Json.Serialization;\n\nnamespace Tubifarry.ImportLists.ArrStack\n{\n    /// <summary>\n    /// Represents a"
  },
  {
    "path": "Tubifarry/ImportLists/ArrStack/ArrSoundtrackImport.cs",
    "chars": 5944,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/ImportLists/ArrStack/ArrSoundtrackImportParser.cs",
    "chars": 17249,
    "preview": "using FuzzySharp;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Imp"
  },
  {
    "path": "Tubifarry/ImportLists/ArrStack/ArrSoundtrackImportSettings.cs",
    "chars": 5985,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.Validation"
  },
  {
    "path": "Tubifarry/ImportLists/ArrStack/ArrSoundtrackRequestGenerator.cs",
    "chars": 983,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\n\nnamespace Tubifarry.ImportLists.ArrStack\n{\n    /// <summar"
  },
  {
    "path": "Tubifarry/ImportLists/LastFmRecommendation/LastFmRecomendRequestGenerator.cs",
    "chars": 1960,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.ImportLists.LastFm;\n\nnamespace Tubifar"
  },
  {
    "path": "Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommend.cs",
    "chars": 4063,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommendParser.cs",
    "chars": 12314,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.ImportLists;\nusing N"
  },
  {
    "path": "Tubifarry/ImportLists/LastFmRecommendation/LastFmRecommendSettings.cs",
    "chars": 3737,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.ImportLis"
  },
  {
    "path": "Tubifarry/ImportLists/LastFmRecommendation/LastFmRecords.cs",
    "chars": 1224,
    "preview": "using NzbDrone.Core.ImportLists.LastFm;\nusing System.Text.Json.Serialization;\n\nnamespace Tubifarry.ImportLists.LastFmRe"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsImportList.cs",
    "chars": 3762,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsParser.cs",
    "chars": 2956,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.ImportLists.Exce"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsRequestGenerator.cs",
    "chars": 1931,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\n\nnamespace Tubifarry.ImportLists.ListenBrainz.ListenBrainzC"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCFRecommendations/ListenBrainzCFRecommendationsSettings.cs",
    "chars": 2267,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.Validatio"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistImportList.cs",
    "chars": 3569,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistParser.cs",
    "chars": 9012,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.ImportLists;\nusing Nz"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistRequestGenerator.cs",
    "chars": 2170,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\n\nnamespace Tubifarry.ImportLists.ListenBrainz.ListenBrainzC"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzCreatedForPlaylist/ListenBrainzCreatedForPlaylistSettings.cs",
    "chars": 2609,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.Validatio"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistImportList.cs",
    "chars": 7575,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistParser.cs",
    "chars": 6159,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.ImportLists.Exce"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistRequestGenerator.cs",
    "chars": 2896,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\n\nnamespace Tubifarry.ImportLists.ListenBrainz.ListenBrainzP"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzPlaylist/ListenBrainzPlaylistSettings.cs",
    "chars": 2120,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.Validation"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzRecords.cs",
    "chars": 4511,
    "preview": "using System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace Tubifarry.ImportLists.ListenBrainz\n{\n    // Use"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsImportList.cs",
    "chars": 3732,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsParser.cs",
    "chars": 5338,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.ImportLists.Exce"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsRequestGenerator.cs",
    "chars": 2594,
    "preview": "using NzbDrone.Common.Http;\nusing NzbDrone.Core.ImportLists;\n\nnamespace Tubifarry.ImportLists.ListenBrainz.ListenBrainzU"
  },
  {
    "path": "Tubifarry/ImportLists/ListenBrainz/ListenBrainzUserStats/ListenBrainzUserStatsSettings.cs",
    "chars": 3283,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists;\nusing NzbDrone.Core.Validatio"
  },
  {
    "path": "Tubifarry/ImportLists/Spotify/SpotifyUserPlaylistImport.cs",
    "chars": 15275,
    "preview": "using NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbD"
  },
  {
    "path": "Tubifarry/ImportLists/Spotify/SpotifyUserPlaylistImportSettings.cs",
    "chars": 2458,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.ImportLists.Spotify;\n\nnamespace Tubifarry."
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicIndexer.cs",
    "chars": 2722,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicIndexerSettings.cs",
    "chars": 3067,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\n\n"
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicParser.cs",
    "chars": 4531,
    "preview": "using NLog;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Parser.Model;\nusing System.Text;\nusing System.Text.Json;\nu"
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicRecords.cs",
    "chars": 5120,
    "preview": "using System.Text.Json.Serialization;\nusing Tubifarry.Core.Utilities;\n\nnamespace Tubifarry.Indexers.DABMusic\n{\n    #reg"
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicRequestGenerator.cs",
    "chars": 3630,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Serializer;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.C"
  },
  {
    "path": "Tubifarry/Indexers/DABMusic/DABMusicSessionHelper.cs",
    "chars": 5204,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing System.Net;\nusing System.Text.Json;\n\nnamespace Tubifarry.Indexers.DABMusic"
  },
  {
    "path": "Tubifarry/Indexers/DownloadProtocols.cs",
    "chars": 430,
    "preview": "namespace NzbDrone.Core.Indexers\n{\n    public class YoutubeDownloadProtocol : IDownloadProtocol { }\n    public class Sou"
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaIndexer.cs",
    "chars": 4859,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDro"
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaIndexerSettings.cs",
    "chars": 4643,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\n"
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaRecords.cs",
    "chars": 20879,
    "preview": "using System.Text.Json.Serialization;\nusing Tubifarry.Core.Utilities;\n\nnamespace Tubifarry.Indexers.Lucida\n{\n    #region"
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaRequestGenerator.cs",
    "chars": 4274,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Serializer;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone."
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaRequestParser.cs",
    "chars": 10493,
    "preview": "using Jint;\nusing NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Parser"
  },
  {
    "path": "Tubifarry/Indexers/Lucida/LucidaServiceHelper.cs",
    "chars": 6718,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing Requests;\nusing System.Collections.Concurrent;\nusing System.Net;\nusing Sy"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/ISlskdItemsParser.cs",
    "chars": 394,
    "preview": "using Tubifarry.Core.Model;\n\nnamespace Tubifarry.Indexers.Soulseek\n{\n    public interface ISlskdItemsParser\n    {\n      "
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Core/ISearchStrategy.cs",
    "chars": 763,
    "preview": "namespace Tubifarry.Indexers.Soulseek.Search.Core;\n\npublic interface ISearchStrategy\n{\n    string Name { get; }\n    Sear"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Core/QueryAnalyzer.cs",
    "chars": 5915,
    "preview": "using System.Text.RegularExpressions;\n\nnamespace Tubifarry.Indexers.Soulseek.Search.Core;\n\npublic static partial class Q"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Core/SearchContext.cs",
    "chars": 3577,
    "preview": "using NzbDrone.Core.Indexers;\nusing NzbDrone.Core.IndexerSearch.Definitions;\nusing NzbDrone.Core.Music;\n\nnamespace Tubif"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Core/SearchPipeline.cs",
    "chars": 3462,
    "preview": "using NLog;\nusing NzbDrone.Core.Indexers;\nusing Tubifarry.Core.Utilities;\nusing Tubifarry.Indexers.Soulseek.Search.Trans"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Strategies/BaseSearchStrategy.cs",
    "chars": 1249,
    "preview": "using Tubifarry.Indexers.Soulseek.Search.Core;\nusing Tubifarry.Indexers.Soulseek.Search.Transformers;\n\nnamespace Tubifar"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Strategies/FallbackStrategy.cs",
    "chars": 5392,
    "preview": "using Tubifarry.Indexers.Soulseek.Search.Core;\nusing Tubifarry.Indexers.Soulseek.Search.Transformers;\n\nnamespace Tubifar"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Strategies/SpecialCaseStrategy.cs",
    "chars": 2621,
    "preview": "using Tubifarry.Indexers.Soulseek.Search.Core;\nusing Tubifarry.Indexers.Soulseek.Search.Transformers;\n\nnamespace Tubifar"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Strategies/TemplateSearchStrategy.cs",
    "chars": 1579,
    "preview": "using Tubifarry.Indexers.Soulseek.Search.Core;\nusing Tubifarry.Indexers.Soulseek.Search.Templates;\nusing Tubifarry.Index"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Strategies/VariationStrategy.cs",
    "chars": 1836,
    "preview": "using Tubifarry.Indexers.Soulseek.Search.Core;\nusing Tubifarry.Indexers.Soulseek.Search.Transformers;\n\nnamespace Tubifar"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Templates/TemplateEngine.cs",
    "chars": 6498,
    "preview": "using NzbDrone.Core.IndexerSearch.Definitions;\nusing System.Collections;\nusing System.Collections.Concurrent;\nusing Syst"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Transformers/QueryBuilder.cs",
    "chars": 7467,
    "preview": "using System.Text.RegularExpressions;\n\nnamespace Tubifarry.Indexers.Soulseek.Search.Transformers;\n\npublic static partial"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/Search/Transformers/QueryNormalizer.cs",
    "chars": 2323,
    "preview": "using System.Globalization;\nusing System.Text;\nusing System.Text.RegularExpressions;\nusing Tubifarry.Indexers.Soulseek.S"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlsdkRecords.cs",
    "chars": 6890,
    "preview": "using System.Text.Json;\nusing System.Text.Json.Serialization;\nusing Tubifarry.Core.Utilities;\n\nnamespace Tubifarry.Inde"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdIndexer.cs",
    "chars": 5317,
    "preview": "using FluentValidation.Results;\nusing Newtonsoft.Json;\nusing NLog;\nusing NzbDrone.Common.Extensions;\nusing NzbDrone.Comm"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdIndexerParser.cs",
    "chars": 16747,
    "preview": "using FuzzySharp;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Do"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdItemsParser.cs",
    "chars": 26041,
    "preview": "using FuzzySharp;\nusing Newtonsoft.Json;\nusing NLog;\nusing NzbDrone.Core.Indexers;\nusing System.Collections.Concurrent;\n"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdRequestGenerator.cs",
    "chars": 17210,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Common.Serializer;\nusing N"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdSettings.cs",
    "chars": 10789,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\nu"
  },
  {
    "path": "Tubifarry/Indexers/Soulseek/SlskdTextProcessor.cs",
    "chars": 7577,
    "preview": "using System.Globalization;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace Tubifarry.Indexers.Soul"
  },
  {
    "path": "Tubifarry/Indexers/Spotify/SpotifyIndexerSettings.cs",
    "chars": 4049,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Validation;\nusing Tubifarry.Indexers.YouTub"
  },
  {
    "path": "Tubifarry/Indexers/Spotify/SpotifyParser.cs",
    "chars": 6021,
    "preview": "using NLog;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Parser.Model;\nusing System.Text.Json;\nusing Tubifarry.Core"
  },
  {
    "path": "Tubifarry/Indexers/Spotify/SpotifyRequestGenerator.cs",
    "chars": 9181,
    "preview": "using DownloadAssistant.Base;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core."
  },
  {
    "path": "Tubifarry/Indexers/Spotify/SpotifyToYouTubeEnricher.cs",
    "chars": 11531,
    "preview": "using FuzzySharp;\nusing NLog;\nusing Tubifarry.Core.Model;\nusing Tubifarry.Core.Records;\nusing Tubifarry.Core.Utilities;\n"
  },
  {
    "path": "Tubifarry/Indexers/Spotify/TubifarryIndexer.cs",
    "chars": 3537,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicAuthHelper.cs",
    "chars": 1934,
    "preview": "using System.Text;\n\nnamespace Tubifarry.Indexers.SubSonic\n{\n    /// <summary>\n    /// Helper class for SubSonic authenti"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicIndexer.cs",
    "chars": 4938,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicIndexerParser.cs",
    "chars": 10026,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Parser.Model;\nusing System.Tex"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicIndexerSettings.cs",
    "chars": 3562,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\n\n"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicRecords.cs",
    "chars": 6842,
    "preview": "using System.Text.Json.Serialization;\nusing Tubifarry.Core.Utilities;\n\nnamespace Tubifarry.Indexers.SubSonic\n{\n    /// <"
  },
  {
    "path": "Tubifarry/Indexers/SubSonic/SubSonicRequestGenerator.cs",
    "chars": 3545,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.IndexerSearch.Definitions;\nusi"
  },
  {
    "path": "Tubifarry/Indexers/TripleTriple/TripleTripleIndexer.cs",
    "chars": 3367,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Indexers/TripleTriple/TripleTripleIndexerSettings.cs",
    "chars": 4396,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\n\n"
  },
  {
    "path": "Tubifarry/Indexers/TripleTriple/TripleTripleParser.cs",
    "chars": 5816,
    "preview": "using NLog;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Parser.Model;\nusing System.Text.Json;\nusing Tubifarry.Core"
  },
  {
    "path": "Tubifarry/Indexers/TripleTriple/TripleTripleRecords.cs",
    "chars": 6548,
    "preview": "using System.Text.Json.Serialization;\n\nnamespace Tubifarry.Indexers.TripleTriple\n{\n    public record TripleTripleStatusR"
  },
  {
    "path": "Tubifarry/Indexers/TripleTriple/TripleTripleRequestGenerator.cs",
    "chars": 3464,
    "preview": "using NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Serializer;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.C"
  },
  {
    "path": "Tubifarry/Indexers/YouTube/YoutubeIndexer.cs",
    "chars": 3215,
    "preview": "using FluentValidation.Results;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Core.Configuration;\nusing NzbDron"
  },
  {
    "path": "Tubifarry/Indexers/YouTube/YoutubeIndexerSettings.cs",
    "chars": 2450,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Indexers;\nusing NzbDrone.Core.Validation;\n"
  },
  {
    "path": "Tubifarry/Indexers/YouTube/YoutubeParser.cs",
    "chars": 9076,
    "preview": "using Newtonsoft.Json.Linq;\nusing NLog;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Core.Indexers;\nusing NzbDr"
  },
  {
    "path": "Tubifarry/Indexers/YouTube/YoutubeRequestGenerator.cs",
    "chars": 5955,
    "preview": "using Newtonsoft.Json;\nusing NLog;\nusing NzbDrone.Common.Http;\nusing NzbDrone.Common.Instrumentation;\nusing NzbDrone.Cor"
  },
  {
    "path": "Tubifarry/Metadata/Converter/AudioConverter.cs",
    "chars": 13888,
    "preview": "using NLog;\nusing NzbDrone.Core.Extras.Metadata;\nusing NzbDrone.Core.Extras.Metadata.Files;\nusing NzbDrone.Core.MediaFi"
  },
  {
    "path": "Tubifarry/Metadata/Converter/AudioConverterSettings.cs",
    "chars": 7035,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Extras.Metadata;\nusing NzbDrone.Core.Thing"
  },
  {
    "path": "Tubifarry/Metadata/Converter/BitrateRules.cs",
    "chars": 15809,
    "preview": "using NLog;\nusing NzbDrone.Common.Instrumentation;\nusing System.Text.RegularExpressions;\nusing Tubifarry.Core.Model;\nusi"
  },
  {
    "path": "Tubifarry/Metadata/Lyrics/LyricEnhancerSettings.cs",
    "chars": 5582,
    "preview": "using FluentValidation;\nusing NzbDrone.Core.Annotations;\nusing NzbDrone.Core.Extras.Metadata;\nusing NzbDrone.Core.Messa"
  }
]

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

About this extraction

This page contains the full source code of the TypNull/Tubifarry GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 282 files (1.6 MB), approximately 356.8k tokens, and a symbol index with 2440 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!