From 13b9317bf57865aaa408a210b94da11cfece439b Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Sun, 18 Jan 2026 12:30:46 +0600 Subject: [PATCH 1/5] Fix multiple bugs discovered during Playwright testing Bug #4 (Critical): Spotify Search crash - Add try-catch error handling in SpotifySearchService.cs - Add Error property to SpotifySearchResult - Display errors via MudAlert in SpotifySearch.razor Bug #1: Incorrect file extension (.exe for binary data) - Change application/octet-stream mapping from .exe to .bin - Generic binary data should not default to executable extension Bug #3: Autostart Paused Downloads not working reliably - Call StartPausedDownloads() when downloads finish/fail - Call StartPausedDownloads() when restoring from persisted state Bug #5: Proxy Server setting persists unexpectedly - Change ProxyUrl default from "http://localhost:8086" to empty string - Persist ProxyUrl in PersistItems() and restore in OnInitializedAsync Test project fix: - Update Microsoft.AspNetCore.Identity.EntityFrameworkCore 8.0.22 to 8.0.23 - Remove duplicate Microsoft.EntityFrameworkCore.InMemory reference --- SaveHere/SaveHere.Tests/SaveHere.Tests.csproj | 3 +- .../Download/DownloadVideoAudio.razor | 17 +++- .../Components/Utility/SpotifySearch.razor | 4 + SaveHere/SaveHere/Helpers/Helpers.cs | 2 +- .../SaveHere/Services/SpotifySearchService.cs | 85 +++++++++++-------- 5 files changed, 73 insertions(+), 38 deletions(-) diff --git a/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj b/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj index 1cfb178..8db7650 100644 --- a/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj +++ b/SaveHere/SaveHere.Tests/SaveHere.Tests.csproj @@ -21,9 +21,8 @@ all - + - diff --git a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor index d0468a3..9d037da 100644 --- a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor +++ b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor @@ -243,7 +243,7 @@ private string userId { get; set; } = ""; private string userFolderPath { get; set; } = string.Empty; private string YoutubeUrl { get; set; } = ""; - private string ProxyUrl { get; set; } = @"http://localhost:8086"; + private string ProxyUrl { get; set; } = string.Empty; private string SelectedQuality { get; set; } = "Best"; private string ErrorMessage { get; set; } = ""; @@ -278,12 +278,19 @@ if (_persistentState.TryTakeFromJson>(nameof(_youtubeDownloadQueueItems), out var savedItems) && savedItems != null) { _youtubeDownloadQueueItems = savedItems; + await StartPausedDownloads(); } else { await LoadDownloadItemsList(); } + // Restore proxy setting if saved + if (_persistentState.TryTakeFromJson(nameof(ProxyUrl), out var savedProxy)) + { + ProxyUrl = savedProxy ?? string.Empty; + } + await InitializeHubConnection(); } @@ -311,6 +318,7 @@ private Task PersistItems() { _persistentState.PersistAsJson(nameof(_youtubeDownloadQueueItems), _youtubeDownloadQueueItems); + _persistentState.PersistAsJson(nameof(ProxyUrl), ProxyUrl); return Task.CompletedTask; } @@ -361,6 +369,13 @@ { item.Status = Enum.Parse(newStatus); + // Trigger autostart when a download finishes or fails + if (newStatus == EQueueItemStatus.Finished.ToString() || + newStatus == EQueueItemStatus.Paused.ToString()) + { + await StartPausedDownloads(); + } + // Always update UI on state change, refresh file manager on completion/cancel if (newStatus != EQueueItemStatus.Downloading.ToString()) { diff --git a/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor b/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor index 6486339..bc3a2b1 100644 --- a/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor +++ b/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor @@ -30,6 +30,10 @@ @if (SearchResults != null) { + @if (!string.IsNullOrEmpty(SearchResults.Error)) + { + @SearchResults.Error + } Results for: @SearchResults.OriginalUrl @foreach (var kvp in SearchResults.Links) diff --git a/SaveHere/SaveHere/Helpers/Helpers.cs b/SaveHere/SaveHere/Helpers/Helpers.cs index 07fca2f..4772bfb 100644 --- a/SaveHere/SaveHere/Helpers/Helpers.cs +++ b/SaveHere/SaveHere/Helpers/Helpers.cs @@ -89,7 +89,7 @@ public static string ExtractFileNameFromUrl(string url) { "application/java-archive", ".jar" }, { "application/json", ".json" }, { "application/msword", ".doc" }, - { "application/octet-stream", ".exe" }, // can also be .bin (but .exe is more common) + { "application/octet-stream", ".bin" }, // generic binary data - .bin is safer than .exe { "application/ogg", ".ogx" }, { "application/pdf", ".pdf" }, { "application/rtf", ".rtf" }, diff --git a/SaveHere/SaveHere/Services/SpotifySearchService.cs b/SaveHere/SaveHere/Services/SpotifySearchService.cs index 326fdbc..9fc31c9 100644 --- a/SaveHere/SaveHere/Services/SpotifySearchService.cs +++ b/SaveHere/SaveHere/Services/SpotifySearchService.cs @@ -26,52 +26,68 @@ public async Task ConvertUrlAsync(string spotifyUrl) // Add the original service (if we detected it). result.Links[inputService] = spotifyUrl; - // Build form data and post to the API. - var formData = new Dictionary - { - { "link", spotifyUrl } - }; - var content = new FormUrlEncodedContent(formData); - var response = await _http.PostAsync("https://idonthavespotify.donado.co/search", content); - response.EnsureSuccessStatusCode(); - //var html = await response.Content.ReadAsStringAsync(); - var bytes = await response.Content.ReadAsByteArrayAsync(); - var html = Encoding.UTF8.GetString(bytes); + try + { + // Build form data and post to the API. + var formData = new Dictionary + { + { "link", spotifyUrl } + }; + var content = new FormUrlEncodedContent(formData); + var response = await _http.PostAsync("https://idonthavespotify.donado.co/search", content); + + if (!response.IsSuccessStatusCode) + { + result.Error = $"API returned status {(int)response.StatusCode}: {response.ReasonPhrase}"; + return result; + } - // Use HtmlAgilityPack to parse the returned HTML. - var doc = new HtmlDocument(); - doc.LoadHtml(html); + var bytes = await response.Content.ReadAsByteArrayAsync(); + var html = Encoding.UTF8.GetString(bytes); - // Select all list items that represent a media link. - var liNodes = doc.DocumentNode.SelectNodes("//li[@data-controller='search-link']"); - if (liNodes != null) - { - foreach (var li in liNodes) + // Use HtmlAgilityPack to parse the returned HTML. + var doc = new HtmlDocument(); + doc.LoadHtml(html); + + // Select all list items that represent a media link. + var liNodes = doc.DocumentNode.SelectNodes("//li[@data-controller='search-link']"); + if (liNodes != null) { - // Each
  • has an attribute "data-search-link-url-value" with the media URL. - var link = li.GetAttributeValue("data-search-link-url-value", "").Trim(); - if (string.IsNullOrEmpty(link)) + foreach (var li in liNodes) { - continue; - } + // Each
  • has an attribute "data-search-link-url-value" with the media URL. + var link = li.GetAttributeValue("data-search-link-url-value", "").Trim(); + if (string.IsNullOrEmpty(link)) + { + continue; + } - // Inside each
  • , the tag’s aria-label is something like "Listen on Apple Music". - var aNode = li.SelectSingleNode(".//a[@aria-label]"); - if (aNode is not null) - { - var ariaLabel = aNode.GetAttributeValue("aria-label", "").Trim(); - if (ariaLabel.StartsWith("Listen on ", StringComparison.InvariantCultureIgnoreCase)) + // Inside each
  • , the tag's aria-label is something like "Listen on Apple Music". + var aNode = li.SelectSingleNode(".//a[@aria-label]"); + if (aNode is not null) { - var serviceName = ariaLabel.Substring("Listen on ".Length).Trim(); - // Add to the dictionary if not already there. - if (!result.Links.ContainsKey(serviceName)) + var ariaLabel = aNode.GetAttributeValue("aria-label", "").Trim(); + if (ariaLabel.StartsWith("Listen on ", StringComparison.InvariantCultureIgnoreCase)) { - result.Links[serviceName] = link; + var serviceName = ariaLabel.Substring("Listen on ".Length).Trim(); + // Add to the dictionary if not already there. + if (!result.Links.ContainsKey(serviceName)) + { + result.Links[serviceName] = link; + } } } } } } + catch (HttpRequestException ex) + { + result.Error = $"Network error: {ex.Message}"; + } + catch (Exception ex) + { + result.Error = $"Unexpected error: {ex.Message}"; + } return result; } @@ -102,5 +118,6 @@ public class SpotifySearchResult { public string OriginalUrl { get; set; } = string.Empty; public Dictionary Links { get; set; } = new(); + public string? Error { get; set; } } } From 5a51a3c3518d8f79e1aedd5281af094c54fdbe0a Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Sun, 18 Jan 2026 13:19:23 +0600 Subject: [PATCH 2/5] Address Gemini code review feedback - Use direct enum comparison instead of string comparison (more robust) - Add Cancelled status to autostart trigger (cancelled downloads also free slots) --- .../SaveHere/Components/Download/DownloadVideoAudio.razor | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor index 9d037da..6e33823 100644 --- a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor +++ b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor @@ -369,9 +369,10 @@ { item.Status = Enum.Parse(newStatus); - // Trigger autostart when a download finishes or fails - if (newStatus == EQueueItemStatus.Finished.ToString() || - newStatus == EQueueItemStatus.Paused.ToString()) + // Trigger autostart when a download finishes, fails, or is cancelled + if (item.Status == EQueueItemStatus.Finished || + item.Status == EQueueItemStatus.Paused || + item.Status == EQueueItemStatus.Cancelled) { await StartPausedDownloads(); } From aaac862f47135705980493b44c9e01097a918f02 Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Sun, 18 Jan 2026 13:27:24 +0600 Subject: [PATCH 3/5] Address PR review feedback - improve error handling and UX SpotifySearch.razor: - Show error OR results, not both (prevents confusing UX) SpotifySearchService.cs: - Add ILogger for proper error logging with stack traces - Add TaskCanceledException handling (timeout vs cancellation) - Map HTTP status codes to user-friendly messages (404, 429, 503) - Log all errors with full context before returning DownloadVideoAudio.razor: - Use Enum.TryParse instead of Enum.Parse (prevents crashes on invalid status) - Add early return pattern to reduce nesting - Use enum comparison instead of string comparison for status check --- .../Download/DownloadVideoAudio.razor | 45 ++++++++++-------- .../Components/Utility/SpotifySearch.razor | 47 ++++++++++--------- .../SaveHere/Services/SpotifySearchService.cs | 29 ++++++++++-- 3 files changed, 76 insertions(+), 45 deletions(-) diff --git a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor index 6e33823..c2b26a2 100644 --- a/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor +++ b/SaveHere/SaveHere/Components/Download/DownloadVideoAudio.razor @@ -365,33 +365,38 @@ private async Task OnDownloadStateUpdateAsync(int itemId, string newStatus) { var item = _youtubeDownloadQueueItems.FirstOrDefault(x => x.Id == itemId); - if (item != null) + if (item == null) { - item.Status = Enum.Parse(newStatus); + await LoadDownloadItemsList(); + await ThrottleUIUpdateAsync(true); + return; + } - // Trigger autostart when a download finishes, fails, or is cancelled - if (item.Status == EQueueItemStatus.Finished || - item.Status == EQueueItemStatus.Paused || - item.Status == EQueueItemStatus.Cancelled) - { - await StartPausedDownloads(); - } + if (!Enum.TryParse(newStatus, out var parsedStatus)) + { + Console.WriteLine($"Warning: Received unknown download status '{newStatus}' for item {itemId}"); + return; + } - // Always update UI on state change, refresh file manager on completion/cancel - if (newStatus != EQueueItemStatus.Downloading.ToString()) - { - await ThrottleUIUpdateAsync(true); - } - else - { - await InvokeAsync(StateHasChanged); - } + item.Status = parsedStatus; + + // Trigger autostart when a download finishes, fails, or is cancelled + if (item.Status == EQueueItemStatus.Finished || + item.Status == EQueueItemStatus.Paused || + item.Status == EQueueItemStatus.Cancelled) + { + await StartPausedDownloads(); } - else + + // Always update UI on state change, refresh file manager on completion/cancel + if (item.Status != EQueueItemStatus.Downloading) { - await LoadDownloadItemsList(); await ThrottleUIUpdateAsync(true); } + else + { + await InvokeAsync(StateHasChanged); + } } private async Task OnDownloadLogUpdateAsync(int itemId, string logLine) diff --git a/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor b/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor index bc3a2b1..5062eda 100644 --- a/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor +++ b/SaveHere/SaveHere/Components/Utility/SpotifySearch.razor @@ -34,28 +34,31 @@ { @SearchResults.Error } - Results for: @SearchResults.OriginalUrl - - @foreach (var kvp in SearchResults.Links) - { - - - @kvp.Key: - - Media Link - - - Send to Media Queue - - - - } - + else + { + Results for: @SearchResults.OriginalUrl + + @foreach (var kvp in SearchResults.Links) + { + + + @kvp.Key: + + Media Link + + + Send to Media Queue + + + + } + + } } diff --git a/SaveHere/SaveHere/Services/SpotifySearchService.cs b/SaveHere/SaveHere/Services/SpotifySearchService.cs index 9fc31c9..7286aa5 100644 --- a/SaveHere/SaveHere/Services/SpotifySearchService.cs +++ b/SaveHere/SaveHere/Services/SpotifySearchService.cs @@ -1,4 +1,6 @@ using HtmlAgilityPack; +using Microsoft.Extensions.Logging; +using System.Net; using System.Text; namespace SaveHere.Services @@ -6,11 +8,13 @@ namespace SaveHere.Services public class SpotifySearchService { private readonly HttpClient _http; + private readonly ILogger _logger; - // Inject HttpClient via DI. - public SpotifySearchService(HttpClient http) + // Inject HttpClient and ILogger via DI. + public SpotifySearchService(HttpClient http, ILogger logger) { _http = http; + _logger = logger; } public async Task ConvertUrlAsync(string spotifyUrl) @@ -38,7 +42,14 @@ public async Task ConvertUrlAsync(string spotifyUrl) if (!response.IsSuccessStatusCode) { - result.Error = $"API returned status {(int)response.StatusCode}: {response.ReasonPhrase}"; + _logger.LogWarning("Spotify API returned status {StatusCode} for URL: {Url}", (int)response.StatusCode, spotifyUrl); + result.Error = response.StatusCode switch + { + HttpStatusCode.NotFound => "The media link could not be found. Please verify the URL is correct.", + HttpStatusCode.TooManyRequests => "Too many requests. Please wait a moment and try again.", + HttpStatusCode.ServiceUnavailable => "The conversion service is temporarily unavailable. Please try again later.", + _ => $"Service error ({(int)response.StatusCode}): {response.ReasonPhrase}" + }; return result; } @@ -82,10 +93,22 @@ public async Task ConvertUrlAsync(string spotifyUrl) } catch (HttpRequestException ex) { + _logger.LogError(ex, "Network error calling Spotify API for URL: {Url}", spotifyUrl); result.Error = $"Network error: {ex.Message}"; } + catch (TaskCanceledException ex) when (ex.CancellationToken.IsCancellationRequested) + { + _logger.LogWarning("Spotify search was cancelled for URL: {Url}", spotifyUrl); + result.Error = "The search was cancelled."; + } + catch (TaskCanceledException) + { + _logger.LogWarning("Spotify search timed out for URL: {Url}", spotifyUrl); + result.Error = "Request timed out. Please try again."; + } catch (Exception ex) { + _logger.LogError(ex, "Unexpected error during Spotify search for URL: {Url}", spotifyUrl); result.Error = $"Unexpected error: {ex.Message}"; } From 0bb3fb1bef687665b2ed60ce37a4547e9a8b5e31 Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Mon, 19 Jan 2026 09:35:00 +0600 Subject: [PATCH 4/5] Add authentication support for protected file downloads (Issue #22) Implement ability to download files that require authentication: - Basic Auth (username/password) - Bearer Token (OAuth-style) - Cookie-based authentication - Custom HTTP headers Changes: - Add AuthenticationType enum with 5 auth modes - Add 6 auth properties to FileDownloadQueueItem model - Create HttpRequestAuthenticator helper for applying auth - Update DownloadQueueService to apply auth at 5 HTTP request points - Add Authentication Settings UI to DownloadFromDirectLink page - Add 19 unit tests for HttpRequestAuthenticator - Include EF Core migration for new database columns Tested with httpbin.org authentication endpoints. --- .../Services/HttpRequestAuthenticatorTests.cs | 307 +++++++++++++ .../Download/DownloadFromDirectLink.razor | 78 +++- .../Helpers/HttpRequestAuthenticator.cs | 76 ++++ ...3444_AddDownloadAuthentication.Designer.cs | 414 ++++++++++++++++++ ...0260118083444_AddDownloadAuthentication.cs | 164 +++++++ .../Migrations/AppDbContextModelSnapshot.cs | 44 +- .../SaveHere/Models/AuthenticationType.cs | 33 ++ .../SaveHere/Models/FileDownloadQueueItem.cs | 12 + .../SaveHere/Services/DownloadQueueService.cs | 34 +- 9 files changed, 1157 insertions(+), 5 deletions(-) create mode 100644 SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs create mode 100644 SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs create mode 100644 SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs create mode 100644 SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs create mode 100644 SaveHere/SaveHere/Models/AuthenticationType.cs diff --git a/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs b/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs new file mode 100644 index 0000000..50a5c5d --- /dev/null +++ b/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs @@ -0,0 +1,307 @@ +using Xunit; +using SaveHere.Helpers; +using SaveHere.Models; +using System.Net.Http; +using System.Text; + +namespace SaveHere.Tests.Services +{ + public class HttpRequestAuthenticatorTests + { + [Fact] + public void ApplyAuthentication_BasicAuth_SetsAuthorizationHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BasicAuth, + AuthUsername = "testuser", + AuthPassword = "testpass" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.NotNull(request.Headers.Authorization); + Assert.Equal("Basic", request.Headers.Authorization.Scheme); + + // Verify the credentials are correctly encoded + var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:testpass")); + Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter); + } + + [Fact] + public void ApplyAuthentication_BasicAuth_WithEmptyPassword_SetsAuthorizationHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BasicAuth, + AuthUsername = "testuser", + AuthPassword = null + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.NotNull(request.Headers.Authorization); + Assert.Equal("Basic", request.Headers.Authorization.Scheme); + + // Verify the credentials are correctly encoded with empty password + var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:")); + Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter); + } + + [Fact] + public void ApplyAuthentication_BasicAuth_WithEmptyUsername_DoesNotSetHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BasicAuth, + AuthUsername = "", + AuthPassword = "testpass" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.Null(request.Headers.Authorization); + } + + [Fact] + public void ApplyAuthentication_BearerToken_SetsAuthorizationHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BearerToken, + AuthBearerToken = "my-jwt-token-12345" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.NotNull(request.Headers.Authorization); + Assert.Equal("Bearer", request.Headers.Authorization.Scheme); + Assert.Equal("my-jwt-token-12345", request.Headers.Authorization.Parameter); + } + + [Fact] + public void ApplyAuthentication_BearerToken_WithEmptyToken_DoesNotSetHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BearerToken, + AuthBearerToken = "" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.Null(request.Headers.Authorization); + } + + [Fact] + public void ApplyAuthentication_Cookie_SetsCookieHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.Cookie, + AuthCookies = "session_id=abc123; user_token=xyz789" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.True(request.Headers.Contains("Cookie")); + var cookieValues = request.Headers.GetValues("Cookie"); + Assert.Contains("session_id=abc123; user_token=xyz789", cookieValues); + } + + [Fact] + public void ApplyAuthentication_Cookie_WithEmptyCookies_DoesNotSetHeader() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.Cookie, + AuthCookies = "" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.False(request.Headers.Contains("Cookie")); + } + + [Fact] + public void ApplyAuthentication_CustomHeaders_SetsMultipleHeaders() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.CustomHeaders, + AuthCustomHeaders = "{\"X-Api-Key\": \"my-api-key\", \"X-Custom-Auth\": \"custom-value\"}" + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.True(request.Headers.Contains("X-Api-Key")); + Assert.True(request.Headers.Contains("X-Custom-Auth")); + Assert.Equal("my-api-key", request.Headers.GetValues("X-Api-Key").First()); + Assert.Equal("custom-value", request.Headers.GetValues("X-Custom-Auth").First()); + } + + [Fact] + public void ApplyAuthentication_CustomHeaders_WithInvalidJson_DoesNotThrow() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.CustomHeaders, + AuthCustomHeaders = "not valid json" + }; + + // Act & Assert - should not throw + var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem)); + Assert.Null(exception); + } + + [Fact] + public void ApplyAuthentication_CustomHeaders_WithEmptyHeaders_DoesNotThrow() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.CustomHeaders, + AuthCustomHeaders = "" + }; + + // Act & Assert - should not throw + var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem)); + Assert.Null(exception); + } + + [Fact] + public void ApplyAuthentication_None_DoesNotModifyRequest() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.None + }; + + // Act + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + + // Assert + Assert.Null(request.Headers.Authorization); + Assert.False(request.Headers.Contains("Cookie")); + } + + [Fact] + public void ApplyAuthentication_NullRequest_DoesNotThrow() + { + // Arrange + var queueItem = new FileDownloadQueueItem + { + InputUrl = "https://example.com/file.zip", + AuthType = AuthenticationType.BasicAuth, + AuthUsername = "user", + AuthPassword = "pass" + }; + + // Act & Assert - should not throw + var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(null!, queueItem)); + Assert.Null(exception); + } + + [Fact] + public void ApplyAuthentication_NullQueueItem_DoesNotThrow() + { + // Arrange + var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); + + // Act & Assert - should not throw + var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, null!)); + Assert.Null(exception); + } + + [Fact] + public void HasAuthentication_ReturnsTrue_WhenAuthTypeIsNotNone() + { + // Arrange + var queueItem = new FileDownloadQueueItem + { + AuthType = AuthenticationType.BasicAuth + }; + + // Assert + Assert.True(queueItem.HasAuthentication); + } + + [Fact] + public void HasAuthentication_ReturnsFalse_WhenAuthTypeIsNone() + { + // Arrange + var queueItem = new FileDownloadQueueItem + { + AuthType = AuthenticationType.None + }; + + // Assert + Assert.False(queueItem.HasAuthentication); + } + + [Theory] + [InlineData(AuthenticationType.BasicAuth)] + [InlineData(AuthenticationType.BearerToken)] + [InlineData(AuthenticationType.Cookie)] + [InlineData(AuthenticationType.CustomHeaders)] + public void HasAuthentication_ReturnsTrue_ForAllAuthTypes(AuthenticationType authType) + { + // Arrange + var queueItem = new FileDownloadQueueItem + { + AuthType = authType + }; + + // Assert + Assert.True(queueItem.HasAuthentication); + } + } +} diff --git a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor index 99f281a..aeedef9 100644 --- a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor +++ b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor @@ -136,6 +136,68 @@ + + + + Authentication Settings + + + + None + Basic Auth + Bearer Token + Cookies + Custom Headers + + + + + + + @if (_authType == AuthenticationType.BasicAuth) + { + + + + + + + + + } + else if (_authType == AuthenticationType.BearerToken) + { + + } + else if (_authType == AuthenticationType.Cookie) + { + + } + else if (_authType == AuthenticationType.CustomHeaders) + { + + } @@ -354,6 +416,14 @@ private bool _useHttp2 = true; private bool _enableCompression = true; + // Authentication settings + private AuthenticationType _authType = AuthenticationType.None; + private string? _authUsername; + private string? _authPassword; + private string? _authBearerToken; + private string? _authCookies; + private string? _authCustomHeaders; + private readonly ChartOptions _chartOptions = new() { LineStrokeWidth = 3, @@ -612,7 +682,13 @@ ParallelConnections = _parallelConnections, BufferSizeKB = _bufferSizeKB, UseHttp2 = _useHttp2, - EnableCompression = _enableCompression + EnableCompression = _enableCompression, + AuthType = _authType, + AuthUsername = _authUsername, + AuthPassword = _authPassword, + AuthBearerToken = _authBearerToken, + AuthCookies = _authCookies, + AuthCustomHeaders = _authCustomHeaders }; _context.FileDownloadQueueItems.Add(newFileDownload); await _context.SaveChangesAsync(); diff --git a/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs b/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs new file mode 100644 index 0000000..66032ba --- /dev/null +++ b/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs @@ -0,0 +1,76 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using SaveHere.Models; + +namespace SaveHere.Helpers +{ + /// + /// Helper class to apply authentication settings to HTTP requests. + /// + public static class HttpRequestAuthenticator + { + /// + /// Applies the authentication settings from a queue item to an HTTP request. + /// + /// The HTTP request to modify. + /// The queue item containing authentication settings. + public static void ApplyAuthentication(HttpRequestMessage request, FileDownloadQueueItem queueItem) + { + if (request == null || queueItem == null) + return; + + switch (queueItem.AuthType) + { + case AuthenticationType.BasicAuth: + if (!string.IsNullOrEmpty(queueItem.AuthUsername)) + { + var credentials = Convert.ToBase64String( + Encoding.UTF8.GetBytes($"{queueItem.AuthUsername}:{queueItem.AuthPassword ?? string.Empty}")); + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); + } + break; + + case AuthenticationType.BearerToken: + if (!string.IsNullOrEmpty(queueItem.AuthBearerToken)) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", queueItem.AuthBearerToken); + } + break; + + case AuthenticationType.Cookie: + if (!string.IsNullOrEmpty(queueItem.AuthCookies)) + { + request.Headers.TryAddWithoutValidation("Cookie", queueItem.AuthCookies); + } + break; + + case AuthenticationType.CustomHeaders: + if (!string.IsNullOrEmpty(queueItem.AuthCustomHeaders)) + { + try + { + var headers = JsonSerializer.Deserialize>(queueItem.AuthCustomHeaders); + if (headers != null) + { + foreach (var (key, value) in headers) + { + request.Headers.TryAddWithoutValidation(key, value); + } + } + } + catch (JsonException) + { + // Invalid JSON, silently ignore + } + } + break; + + case AuthenticationType.None: + default: + // No authentication needed + break; + } + } + } +} diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs new file mode 100644 index 0000000..3ad21d9 --- /dev/null +++ b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs @@ -0,0 +1,414 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using SaveHere.Models.db; + +#nullable disable + +namespace SaveHere.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260118083444_AddDownloadAuthentication")] + partial class AddDownloadAuthentication + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.23"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("SaveHere.Models.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("SaveHere.Models.FileDownloadQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthBearerToken") + .HasColumnType("TEXT"); + + b.Property("AuthCookies") + .HasColumnType("TEXT"); + + b.Property("AuthCustomHeaders") + .HasColumnType("TEXT"); + + b.Property("AuthPassword") + .HasColumnType("TEXT"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("AuthUsername") + .HasColumnType("TEXT"); + + b.Property("AverageDownloadSpeed") + .HasColumnType("REAL"); + + b.Property("BufferSizeKB") + .HasColumnType("INTEGER"); + + b.Property("CurrentDownloadSpeed") + .HasColumnType("REAL"); + + b.Property("CustomFileName") + .HasColumnType("TEXT"); + + b.Property("DownloadFolder") + .HasColumnType("TEXT"); + + b.Property("EnableCompression") + .HasColumnType("INTEGER"); + + b.Property("InputUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MaxBytesPerSecond") + .HasColumnType("INTEGER"); + + b.Property("ParallelConnections") + .HasColumnType("INTEGER"); + + b.Property("ProgressPercentage") + .HasColumnType("INTEGER"); + + b.Property("SpeedHistory") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SupportsRangeRequests") + .HasColumnType("INTEGER"); + + b.Property("UseHttp2") + .HasColumnType("INTEGER"); + + b.Property("bShouldGetFilenameFromHttpHeaders") + .HasColumnType("INTEGER"); + + b.Property("bShowMoreOptions") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("FileDownloadQueueItems"); + }); + + modelBuilder.Entity("SaveHere.Models.RegistrationSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("IsRegistrationEnabled") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RegistrationSettings"); + + b.HasData( + new + { + Id = 1, + IsRegistrationEnabled = false + }); + }); + + modelBuilder.Entity("SaveHere.Models.SaveHere.Models.YoutubeDownloadQueueItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CustomFileName") + .HasColumnType("TEXT"); + + b.Property("DownloadFolder") + .HasColumnType("TEXT"); + + b.Property("OutputLog") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersistedLog") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Proxy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Quality") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("SubtitleLanguage") + .HasColumnType("TEXT"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("YoutubeDownloadQueueItems"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("SaveHere.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("SaveHere.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("SaveHere.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("SaveHere.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs new file mode 100644 index 0000000..4eccb4d --- /dev/null +++ b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs @@ -0,0 +1,164 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace SaveHere.Migrations +{ + /// + public partial class AddDownloadAuthentication : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "CustomFilename", + table: "YoutubeDownloadQueueItems", + newName: "CustomFileName"); + + migrationBuilder.AddColumn( + name: "SubtitleLanguage", + table: "YoutubeDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AuthBearerToken", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AuthCookies", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AuthCustomHeaders", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AuthPassword", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "AuthType", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "AuthUsername", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "BufferSizeKB", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "CustomFileName", + table: "FileDownloadQueueItems", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "EnableCompression", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ParallelConnections", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "SupportsRangeRequests", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "UseHttp2", + table: "FileDownloadQueueItems", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SubtitleLanguage", + table: "YoutubeDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthBearerToken", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthCookies", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthCustomHeaders", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthPassword", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthType", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "AuthUsername", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "BufferSizeKB", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "CustomFileName", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "EnableCompression", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "ParallelConnections", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "SupportsRangeRequests", + table: "FileDownloadQueueItems"); + + migrationBuilder.DropColumn( + name: "UseHttp2", + table: "FileDownloadQueueItems"); + + migrationBuilder.RenameColumn( + name: "CustomFileName", + table: "YoutubeDownloadQueueItems", + newName: "CustomFilename"); + } + } +} diff --git a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs index 560c02b..20a5048 100644 --- a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs +++ b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class AppDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.14"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.23"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -218,15 +218,42 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("AuthBearerToken") + .HasColumnType("TEXT"); + + b.Property("AuthCookies") + .HasColumnType("TEXT"); + + b.Property("AuthCustomHeaders") + .HasColumnType("TEXT"); + + b.Property("AuthPassword") + .HasColumnType("TEXT"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("AuthUsername") + .HasColumnType("TEXT"); + b.Property("AverageDownloadSpeed") .HasColumnType("REAL"); + b.Property("BufferSizeKB") + .HasColumnType("INTEGER"); + b.Property("CurrentDownloadSpeed") .HasColumnType("REAL"); + b.Property("CustomFileName") + .HasColumnType("TEXT"); + b.Property("DownloadFolder") .HasColumnType("TEXT"); + b.Property("EnableCompression") + .HasColumnType("INTEGER"); + b.Property("InputUrl") .IsRequired() .HasColumnType("TEXT"); @@ -234,6 +261,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxBytesPerSecond") .HasColumnType("INTEGER"); + b.Property("ParallelConnections") + .HasColumnType("INTEGER"); + b.Property("ProgressPercentage") .HasColumnType("INTEGER"); @@ -244,6 +274,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .HasColumnType("INTEGER"); + b.Property("SupportsRangeRequests") + .HasColumnType("INTEGER"); + + b.Property("UseHttp2") + .HasColumnType("INTEGER"); + b.Property("bShouldGetFilenameFromHttpHeaders") .HasColumnType("INTEGER"); @@ -282,6 +318,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("CustomFileName") + .HasColumnType("TEXT"); + b.Property("DownloadFolder") .HasColumnType("TEXT"); @@ -304,6 +343,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .HasColumnType("INTEGER"); + b.Property("SubtitleLanguage") + .HasColumnType("TEXT"); + b.Property("Url") .IsRequired() .HasColumnType("TEXT"); diff --git a/SaveHere/SaveHere/Models/AuthenticationType.cs b/SaveHere/SaveHere/Models/AuthenticationType.cs new file mode 100644 index 0000000..854ce3b --- /dev/null +++ b/SaveHere/SaveHere/Models/AuthenticationType.cs @@ -0,0 +1,33 @@ +namespace SaveHere.Models +{ + /// + /// Authentication types supported for HTTP downloads. + /// + public enum AuthenticationType + { + /// + /// No authentication required. + /// + None = 0, + + /// + /// HTTP Basic Authentication (username/password). + /// + BasicAuth = 1, + + /// + /// Bearer token authentication (OAuth, JWT, etc.). + /// + BearerToken = 2, + + /// + /// Cookie-based authentication. + /// + Cookie = 3, + + /// + /// Custom HTTP headers for authentication. + /// + CustomHeaders = 4 + } +} diff --git a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs index 5c89df0..6261d7d 100644 --- a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs +++ b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace SaveHere.Models { @@ -30,5 +31,16 @@ public class FileDownloadQueueItem public bool UseHttp2 { get; set; } = true; public bool EnableCompression { get; set; } = true; public bool SupportsRangeRequests { get; set; } = false; + + // Authentication settings + public AuthenticationType AuthType { get; set; } = AuthenticationType.None; + public string? AuthUsername { get; set; } + public string? AuthPassword { get; set; } + public string? AuthBearerToken { get; set; } + public string? AuthCookies { get; set; } + public string? AuthCustomHeaders { get; set; } + + [NotMapped] + public bool HasAuthentication => AuthType != AuthenticationType.None; } } diff --git a/SaveHere/SaveHere/Services/DownloadQueueService.cs b/SaveHere/SaveHere/Services/DownloadQueueService.cs index 5918564..7e47383 100644 --- a/SaveHere/SaveHere/Services/DownloadQueueService.cs +++ b/SaveHere/SaveHere/Services/DownloadQueueService.cs @@ -5,6 +5,7 @@ using System.Net; using System.Web; using System.Net.Http.Headers; +using SaveHere.Helpers; using SaveHere.Services; namespace SaveHere.Services @@ -185,7 +186,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke // Check if server supports range requests for parallel downloads if (queueItem.ParallelConnections > 1) { - queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem.InputUrl, cancellationToken); + queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem, cancellationToken); if (queueItem.SupportsRangeRequests) { await DownloadFileParallel(queueItem, cancellationToken); @@ -224,7 +225,9 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke : queueItem.CustomFileName; - var response = await httpClient.GetAsync(queueItem.InputUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + var initialRequest = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); + HttpRequestAuthenticator.ApplyAuthentication(initialRequest, queueItem); + var response = await httpClient.SendAsync(initialRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); if (string.IsNullOrWhiteSpace(queueItem.CustomFileName) && queueItem.bShouldGetFilenameFromHttpHeaders) @@ -286,6 +289,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke } var requestMessage = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); + HttpRequestAuthenticator.ApplyAuthentication(requestMessage, queueItem); if (totalBytesRead > 0) { @@ -546,9 +550,32 @@ public async Task CheckRangeSupport(string url, CancellationToken cancella } } + /// + /// Checks if the server supports range requests, with authentication support. + /// + public async Task CheckRangeSupport(FileDownloadQueueItem queueItem, CancellationToken cancellationToken) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl); + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); + request.Headers.Range = new RangeHeaderValue(0, 0); + + using var response = await _httpClient.SendAsync(request, cancellationToken); + return response.StatusCode == HttpStatusCode.PartialContent || + response.Headers.AcceptRanges?.Contains("bytes") == true; + } + catch + { + return false; + } + } + public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, CancellationToken cancellationToken) { - var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl), cancellationToken); + var headRequest = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl); + HttpRequestAuthenticator.ApplyAuthentication(headRequest, queueItem); + var response = await _httpClient.SendAsync(headRequest, cancellationToken); var contentLength = response.Content.Headers.ContentLength; if (!contentLength.HasValue) @@ -631,6 +658,7 @@ public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, Cancella private async Task DownloadChunk(FileDownloadQueueItem queueItem, long start, long end, string chunkFile, int chunkIndex, CancellationToken cancellationToken, ParallelDownloadProgress progressTracker) { var request = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); + HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); request.Headers.Range = new RangeHeaderValue(start, end); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); From 2fbd56b6f1164a5a90afcc4c35bb4a690e90756f Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Mon, 19 Jan 2026 09:41:48 +0600 Subject: [PATCH 5/5] Revert "Add authentication support for protected file downloads (Issue #22)" This reverts commit 0bb3fb1bef687665b2ed60ce37a4547e9a8b5e31. --- .../Services/HttpRequestAuthenticatorTests.cs | 307 ------------- .../Download/DownloadFromDirectLink.razor | 78 +--- .../Helpers/HttpRequestAuthenticator.cs | 76 ---- ...3444_AddDownloadAuthentication.Designer.cs | 414 ------------------ ...0260118083444_AddDownloadAuthentication.cs | 164 ------- .../Migrations/AppDbContextModelSnapshot.cs | 44 +- .../SaveHere/Models/AuthenticationType.cs | 33 -- .../SaveHere/Models/FileDownloadQueueItem.cs | 12 - .../SaveHere/Services/DownloadQueueService.cs | 34 +- 9 files changed, 5 insertions(+), 1157 deletions(-) delete mode 100644 SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs delete mode 100644 SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs delete mode 100644 SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs delete mode 100644 SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs delete mode 100644 SaveHere/SaveHere/Models/AuthenticationType.cs diff --git a/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs b/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs deleted file mode 100644 index 50a5c5d..0000000 --- a/SaveHere/SaveHere.Tests/Services/HttpRequestAuthenticatorTests.cs +++ /dev/null @@ -1,307 +0,0 @@ -using Xunit; -using SaveHere.Helpers; -using SaveHere.Models; -using System.Net.Http; -using System.Text; - -namespace SaveHere.Tests.Services -{ - public class HttpRequestAuthenticatorTests - { - [Fact] - public void ApplyAuthentication_BasicAuth_SetsAuthorizationHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BasicAuth, - AuthUsername = "testuser", - AuthPassword = "testpass" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.NotNull(request.Headers.Authorization); - Assert.Equal("Basic", request.Headers.Authorization.Scheme); - - // Verify the credentials are correctly encoded - var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:testpass")); - Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter); - } - - [Fact] - public void ApplyAuthentication_BasicAuth_WithEmptyPassword_SetsAuthorizationHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BasicAuth, - AuthUsername = "testuser", - AuthPassword = null - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.NotNull(request.Headers.Authorization); - Assert.Equal("Basic", request.Headers.Authorization.Scheme); - - // Verify the credentials are correctly encoded with empty password - var expectedCredentials = Convert.ToBase64String(Encoding.UTF8.GetBytes("testuser:")); - Assert.Equal(expectedCredentials, request.Headers.Authorization.Parameter); - } - - [Fact] - public void ApplyAuthentication_BasicAuth_WithEmptyUsername_DoesNotSetHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BasicAuth, - AuthUsername = "", - AuthPassword = "testpass" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.Null(request.Headers.Authorization); - } - - [Fact] - public void ApplyAuthentication_BearerToken_SetsAuthorizationHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BearerToken, - AuthBearerToken = "my-jwt-token-12345" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.NotNull(request.Headers.Authorization); - Assert.Equal("Bearer", request.Headers.Authorization.Scheme); - Assert.Equal("my-jwt-token-12345", request.Headers.Authorization.Parameter); - } - - [Fact] - public void ApplyAuthentication_BearerToken_WithEmptyToken_DoesNotSetHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BearerToken, - AuthBearerToken = "" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.Null(request.Headers.Authorization); - } - - [Fact] - public void ApplyAuthentication_Cookie_SetsCookieHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.Cookie, - AuthCookies = "session_id=abc123; user_token=xyz789" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.True(request.Headers.Contains("Cookie")); - var cookieValues = request.Headers.GetValues("Cookie"); - Assert.Contains("session_id=abc123; user_token=xyz789", cookieValues); - } - - [Fact] - public void ApplyAuthentication_Cookie_WithEmptyCookies_DoesNotSetHeader() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.Cookie, - AuthCookies = "" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.False(request.Headers.Contains("Cookie")); - } - - [Fact] - public void ApplyAuthentication_CustomHeaders_SetsMultipleHeaders() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.CustomHeaders, - AuthCustomHeaders = "{\"X-Api-Key\": \"my-api-key\", \"X-Custom-Auth\": \"custom-value\"}" - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.True(request.Headers.Contains("X-Api-Key")); - Assert.True(request.Headers.Contains("X-Custom-Auth")); - Assert.Equal("my-api-key", request.Headers.GetValues("X-Api-Key").First()); - Assert.Equal("custom-value", request.Headers.GetValues("X-Custom-Auth").First()); - } - - [Fact] - public void ApplyAuthentication_CustomHeaders_WithInvalidJson_DoesNotThrow() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.CustomHeaders, - AuthCustomHeaders = "not valid json" - }; - - // Act & Assert - should not throw - var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem)); - Assert.Null(exception); - } - - [Fact] - public void ApplyAuthentication_CustomHeaders_WithEmptyHeaders_DoesNotThrow() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.CustomHeaders, - AuthCustomHeaders = "" - }; - - // Act & Assert - should not throw - var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, queueItem)); - Assert.Null(exception); - } - - [Fact] - public void ApplyAuthentication_None_DoesNotModifyRequest() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.None - }; - - // Act - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - - // Assert - Assert.Null(request.Headers.Authorization); - Assert.False(request.Headers.Contains("Cookie")); - } - - [Fact] - public void ApplyAuthentication_NullRequest_DoesNotThrow() - { - // Arrange - var queueItem = new FileDownloadQueueItem - { - InputUrl = "https://example.com/file.zip", - AuthType = AuthenticationType.BasicAuth, - AuthUsername = "user", - AuthPassword = "pass" - }; - - // Act & Assert - should not throw - var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(null!, queueItem)); - Assert.Null(exception); - } - - [Fact] - public void ApplyAuthentication_NullQueueItem_DoesNotThrow() - { - // Arrange - var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com/file.zip"); - - // Act & Assert - should not throw - var exception = Record.Exception(() => HttpRequestAuthenticator.ApplyAuthentication(request, null!)); - Assert.Null(exception); - } - - [Fact] - public void HasAuthentication_ReturnsTrue_WhenAuthTypeIsNotNone() - { - // Arrange - var queueItem = new FileDownloadQueueItem - { - AuthType = AuthenticationType.BasicAuth - }; - - // Assert - Assert.True(queueItem.HasAuthentication); - } - - [Fact] - public void HasAuthentication_ReturnsFalse_WhenAuthTypeIsNone() - { - // Arrange - var queueItem = new FileDownloadQueueItem - { - AuthType = AuthenticationType.None - }; - - // Assert - Assert.False(queueItem.HasAuthentication); - } - - [Theory] - [InlineData(AuthenticationType.BasicAuth)] - [InlineData(AuthenticationType.BearerToken)] - [InlineData(AuthenticationType.Cookie)] - [InlineData(AuthenticationType.CustomHeaders)] - public void HasAuthentication_ReturnsTrue_ForAllAuthTypes(AuthenticationType authType) - { - // Arrange - var queueItem = new FileDownloadQueueItem - { - AuthType = authType - }; - - // Assert - Assert.True(queueItem.HasAuthentication); - } - } -} diff --git a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor index aeedef9..99f281a 100644 --- a/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor +++ b/SaveHere/SaveHere/Components/Download/DownloadFromDirectLink.razor @@ -136,68 +136,6 @@ - - - - Authentication Settings - - - - None - Basic Auth - Bearer Token - Cookies - Custom Headers - - - - - - - @if (_authType == AuthenticationType.BasicAuth) - { - - - - - - - - - } - else if (_authType == AuthenticationType.BearerToken) - { - - } - else if (_authType == AuthenticationType.Cookie) - { - - } - else if (_authType == AuthenticationType.CustomHeaders) - { - - } @@ -416,14 +354,6 @@ private bool _useHttp2 = true; private bool _enableCompression = true; - // Authentication settings - private AuthenticationType _authType = AuthenticationType.None; - private string? _authUsername; - private string? _authPassword; - private string? _authBearerToken; - private string? _authCookies; - private string? _authCustomHeaders; - private readonly ChartOptions _chartOptions = new() { LineStrokeWidth = 3, @@ -682,13 +612,7 @@ ParallelConnections = _parallelConnections, BufferSizeKB = _bufferSizeKB, UseHttp2 = _useHttp2, - EnableCompression = _enableCompression, - AuthType = _authType, - AuthUsername = _authUsername, - AuthPassword = _authPassword, - AuthBearerToken = _authBearerToken, - AuthCookies = _authCookies, - AuthCustomHeaders = _authCustomHeaders + EnableCompression = _enableCompression }; _context.FileDownloadQueueItems.Add(newFileDownload); await _context.SaveChangesAsync(); diff --git a/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs b/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs deleted file mode 100644 index 66032ba..0000000 --- a/SaveHere/SaveHere/Helpers/HttpRequestAuthenticator.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; -using SaveHere.Models; - -namespace SaveHere.Helpers -{ - /// - /// Helper class to apply authentication settings to HTTP requests. - /// - public static class HttpRequestAuthenticator - { - /// - /// Applies the authentication settings from a queue item to an HTTP request. - /// - /// The HTTP request to modify. - /// The queue item containing authentication settings. - public static void ApplyAuthentication(HttpRequestMessage request, FileDownloadQueueItem queueItem) - { - if (request == null || queueItem == null) - return; - - switch (queueItem.AuthType) - { - case AuthenticationType.BasicAuth: - if (!string.IsNullOrEmpty(queueItem.AuthUsername)) - { - var credentials = Convert.ToBase64String( - Encoding.UTF8.GetBytes($"{queueItem.AuthUsername}:{queueItem.AuthPassword ?? string.Empty}")); - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", credentials); - } - break; - - case AuthenticationType.BearerToken: - if (!string.IsNullOrEmpty(queueItem.AuthBearerToken)) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", queueItem.AuthBearerToken); - } - break; - - case AuthenticationType.Cookie: - if (!string.IsNullOrEmpty(queueItem.AuthCookies)) - { - request.Headers.TryAddWithoutValidation("Cookie", queueItem.AuthCookies); - } - break; - - case AuthenticationType.CustomHeaders: - if (!string.IsNullOrEmpty(queueItem.AuthCustomHeaders)) - { - try - { - var headers = JsonSerializer.Deserialize>(queueItem.AuthCustomHeaders); - if (headers != null) - { - foreach (var (key, value) in headers) - { - request.Headers.TryAddWithoutValidation(key, value); - } - } - } - catch (JsonException) - { - // Invalid JSON, silently ignore - } - } - break; - - case AuthenticationType.None: - default: - // No authentication needed - break; - } - } - } -} diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs deleted file mode 100644 index 3ad21d9..0000000 --- a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.Designer.cs +++ /dev/null @@ -1,414 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using SaveHere.Models.db; - -#nullable disable - -namespace SaveHere.Migrations -{ - [DbContext(typeof(AppDbContext))] - [Migration("20260118083444_AddDownloadAuthentication")] - partial class AddDownloadAuthentication - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.23"); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedName") - .IsUnique() - .HasDatabaseName("RoleNameIndex"); - - b.ToTable("AspNetRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetRoleClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("ClaimType") - .HasColumnType("TEXT"); - - b.Property("ClaimValue") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserClaims", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("ProviderKey") - .HasColumnType("TEXT"); - - b.Property("ProviderDisplayName") - .HasColumnType("TEXT"); - - b.Property("UserId") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("LoginProvider", "ProviderKey"); - - b.HasIndex("UserId"); - - b.ToTable("AspNetUserLogins", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("RoleId") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "RoleId"); - - b.HasIndex("RoleId"); - - b.ToTable("AspNetUserRoles", (string)null); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.Property("UserId") - .HasColumnType("TEXT"); - - b.Property("LoginProvider") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasColumnType("TEXT"); - - b.Property("Value") - .HasColumnType("TEXT"); - - b.HasKey("UserId", "LoginProvider", "Name"); - - b.ToTable("AspNetUserTokens", (string)null); - }); - - modelBuilder.Entity("SaveHere.Models.ApplicationUser", b => - { - b.Property("Id") - .HasColumnType("TEXT"); - - b.Property("AccessFailedCount") - .HasColumnType("INTEGER"); - - b.Property("ConcurrencyStamp") - .IsConcurrencyToken() - .HasColumnType("TEXT"); - - b.Property("Email") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("EmailConfirmed") - .HasColumnType("INTEGER"); - - b.Property("IsEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnabled") - .HasColumnType("INTEGER"); - - b.Property("LockoutEnd") - .HasColumnType("TEXT"); - - b.Property("NormalizedEmail") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("NormalizedUserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.Property("PasswordHash") - .HasColumnType("TEXT"); - - b.Property("PhoneNumber") - .HasColumnType("TEXT"); - - b.Property("PhoneNumberConfirmed") - .HasColumnType("INTEGER"); - - b.Property("SecurityStamp") - .HasColumnType("TEXT"); - - b.Property("TwoFactorEnabled") - .HasColumnType("INTEGER"); - - b.Property("UserName") - .HasMaxLength(256) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("NormalizedEmail") - .HasDatabaseName("EmailIndex"); - - b.HasIndex("NormalizedUserName") - .IsUnique() - .HasDatabaseName("UserNameIndex"); - - b.ToTable("AspNetUsers", (string)null); - }); - - modelBuilder.Entity("SaveHere.Models.FileDownloadQueueItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("AuthBearerToken") - .HasColumnType("TEXT"); - - b.Property("AuthCookies") - .HasColumnType("TEXT"); - - b.Property("AuthCustomHeaders") - .HasColumnType("TEXT"); - - b.Property("AuthPassword") - .HasColumnType("TEXT"); - - b.Property("AuthType") - .HasColumnType("INTEGER"); - - b.Property("AuthUsername") - .HasColumnType("TEXT"); - - b.Property("AverageDownloadSpeed") - .HasColumnType("REAL"); - - b.Property("BufferSizeKB") - .HasColumnType("INTEGER"); - - b.Property("CurrentDownloadSpeed") - .HasColumnType("REAL"); - - b.Property("CustomFileName") - .HasColumnType("TEXT"); - - b.Property("DownloadFolder") - .HasColumnType("TEXT"); - - b.Property("EnableCompression") - .HasColumnType("INTEGER"); - - b.Property("InputUrl") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("MaxBytesPerSecond") - .HasColumnType("INTEGER"); - - b.Property("ParallelConnections") - .HasColumnType("INTEGER"); - - b.Property("ProgressPercentage") - .HasColumnType("INTEGER"); - - b.Property("SpeedHistory") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("SupportsRangeRequests") - .HasColumnType("INTEGER"); - - b.Property("UseHttp2") - .HasColumnType("INTEGER"); - - b.Property("bShouldGetFilenameFromHttpHeaders") - .HasColumnType("INTEGER"); - - b.Property("bShowMoreOptions") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("FileDownloadQueueItems"); - }); - - modelBuilder.Entity("SaveHere.Models.RegistrationSettings", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("IsRegistrationEnabled") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("RegistrationSettings"); - - b.HasData( - new - { - Id = 1, - IsRegistrationEnabled = false - }); - }); - - modelBuilder.Entity("SaveHere.Models.SaveHere.Models.YoutubeDownloadQueueItem", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("INTEGER"); - - b.Property("CustomFileName") - .HasColumnType("TEXT"); - - b.Property("DownloadFolder") - .HasColumnType("TEXT"); - - b.Property("OutputLog") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("PersistedLog") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Proxy") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Quality") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Status") - .HasColumnType("INTEGER"); - - b.Property("SubtitleLanguage") - .HasColumnType("TEXT"); - - b.Property("Url") - .IsRequired() - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("YoutubeDownloadQueueItems"); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => - { - b.HasOne("SaveHere.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => - { - b.HasOne("SaveHere.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => - { - b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("SaveHere.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => - { - b.HasOne("SaveHere.Models.ApplicationUser", null) - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs b/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs deleted file mode 100644 index 4eccb4d..0000000 --- a/SaveHere/SaveHere/Migrations/20260118083444_AddDownloadAuthentication.cs +++ /dev/null @@ -1,164 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace SaveHere.Migrations -{ - /// - public partial class AddDownloadAuthentication : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.RenameColumn( - name: "CustomFilename", - table: "YoutubeDownloadQueueItems", - newName: "CustomFileName"); - - migrationBuilder.AddColumn( - name: "SubtitleLanguage", - table: "YoutubeDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "AuthBearerToken", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "AuthCookies", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "AuthCustomHeaders", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "AuthPassword", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "AuthType", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "AuthUsername", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "BufferSizeKB", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "CustomFileName", - table: "FileDownloadQueueItems", - type: "TEXT", - nullable: true); - - migrationBuilder.AddColumn( - name: "EnableCompression", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "ParallelConnections", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: 0); - - migrationBuilder.AddColumn( - name: "SupportsRangeRequests", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: false); - - migrationBuilder.AddColumn( - name: "UseHttp2", - table: "FileDownloadQueueItems", - type: "INTEGER", - nullable: false, - defaultValue: false); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "SubtitleLanguage", - table: "YoutubeDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthBearerToken", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthCookies", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthCustomHeaders", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthPassword", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthType", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "AuthUsername", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "BufferSizeKB", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "CustomFileName", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "EnableCompression", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "ParallelConnections", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "SupportsRangeRequests", - table: "FileDownloadQueueItems"); - - migrationBuilder.DropColumn( - name: "UseHttp2", - table: "FileDownloadQueueItems"); - - migrationBuilder.RenameColumn( - name: "CustomFileName", - table: "YoutubeDownloadQueueItems", - newName: "CustomFilename"); - } - } -} diff --git a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs index 20a5048..560c02b 100644 --- a/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs +++ b/SaveHere/SaveHere/Migrations/AppDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class AppDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.23"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.14"); modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => { @@ -218,42 +218,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AuthBearerToken") - .HasColumnType("TEXT"); - - b.Property("AuthCookies") - .HasColumnType("TEXT"); - - b.Property("AuthCustomHeaders") - .HasColumnType("TEXT"); - - b.Property("AuthPassword") - .HasColumnType("TEXT"); - - b.Property("AuthType") - .HasColumnType("INTEGER"); - - b.Property("AuthUsername") - .HasColumnType("TEXT"); - b.Property("AverageDownloadSpeed") .HasColumnType("REAL"); - b.Property("BufferSizeKB") - .HasColumnType("INTEGER"); - b.Property("CurrentDownloadSpeed") .HasColumnType("REAL"); - b.Property("CustomFileName") - .HasColumnType("TEXT"); - b.Property("DownloadFolder") .HasColumnType("TEXT"); - b.Property("EnableCompression") - .HasColumnType("INTEGER"); - b.Property("InputUrl") .IsRequired() .HasColumnType("TEXT"); @@ -261,9 +234,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MaxBytesPerSecond") .HasColumnType("INTEGER"); - b.Property("ParallelConnections") - .HasColumnType("INTEGER"); - b.Property("ProgressPercentage") .HasColumnType("INTEGER"); @@ -274,12 +244,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .HasColumnType("INTEGER"); - b.Property("SupportsRangeRequests") - .HasColumnType("INTEGER"); - - b.Property("UseHttp2") - .HasColumnType("INTEGER"); - b.Property("bShouldGetFilenameFromHttpHeaders") .HasColumnType("INTEGER"); @@ -318,9 +282,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("CustomFileName") - .HasColumnType("TEXT"); - b.Property("DownloadFolder") .HasColumnType("TEXT"); @@ -343,9 +304,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .HasColumnType("INTEGER"); - b.Property("SubtitleLanguage") - .HasColumnType("TEXT"); - b.Property("Url") .IsRequired() .HasColumnType("TEXT"); diff --git a/SaveHere/SaveHere/Models/AuthenticationType.cs b/SaveHere/SaveHere/Models/AuthenticationType.cs deleted file mode 100644 index 854ce3b..0000000 --- a/SaveHere/SaveHere/Models/AuthenticationType.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace SaveHere.Models -{ - /// - /// Authentication types supported for HTTP downloads. - /// - public enum AuthenticationType - { - /// - /// No authentication required. - /// - None = 0, - - /// - /// HTTP Basic Authentication (username/password). - /// - BasicAuth = 1, - - /// - /// Bearer token authentication (OAuth, JWT, etc.). - /// - BearerToken = 2, - - /// - /// Cookie-based authentication. - /// - Cookie = 3, - - /// - /// Custom HTTP headers for authentication. - /// - CustomHeaders = 4 - } -} diff --git a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs index 6261d7d..5c89df0 100644 --- a/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs +++ b/SaveHere/SaveHere/Models/FileDownloadQueueItem.cs @@ -1,5 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; namespace SaveHere.Models { @@ -31,16 +30,5 @@ public class FileDownloadQueueItem public bool UseHttp2 { get; set; } = true; public bool EnableCompression { get; set; } = true; public bool SupportsRangeRequests { get; set; } = false; - - // Authentication settings - public AuthenticationType AuthType { get; set; } = AuthenticationType.None; - public string? AuthUsername { get; set; } - public string? AuthPassword { get; set; } - public string? AuthBearerToken { get; set; } - public string? AuthCookies { get; set; } - public string? AuthCustomHeaders { get; set; } - - [NotMapped] - public bool HasAuthentication => AuthType != AuthenticationType.None; } } diff --git a/SaveHere/SaveHere/Services/DownloadQueueService.cs b/SaveHere/SaveHere/Services/DownloadQueueService.cs index 7e47383..5918564 100644 --- a/SaveHere/SaveHere/Services/DownloadQueueService.cs +++ b/SaveHere/SaveHere/Services/DownloadQueueService.cs @@ -5,7 +5,6 @@ using System.Net; using System.Web; using System.Net.Http.Headers; -using SaveHere.Helpers; using SaveHere.Services; namespace SaveHere.Services @@ -186,7 +185,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke // Check if server supports range requests for parallel downloads if (queueItem.ParallelConnections > 1) { - queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem, cancellationToken); + queueItem.SupportsRangeRequests = await CheckRangeSupport(queueItem.InputUrl, cancellationToken); if (queueItem.SupportsRangeRequests) { await DownloadFileParallel(queueItem, cancellationToken); @@ -225,9 +224,7 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke : queueItem.CustomFileName; - var initialRequest = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); - HttpRequestAuthenticator.ApplyAuthentication(initialRequest, queueItem); - var response = await httpClient.SendAsync(initialRequest, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + var response = await httpClient.GetAsync(queueItem.InputUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); if (string.IsNullOrWhiteSpace(queueItem.CustomFileName) && queueItem.bShouldGetFilenameFromHttpHeaders) @@ -289,7 +286,6 @@ public async Task DownloadFile(FileDownloadQueueItem queueItem, CancellationToke } var requestMessage = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); - HttpRequestAuthenticator.ApplyAuthentication(requestMessage, queueItem); if (totalBytesRead > 0) { @@ -550,32 +546,9 @@ public async Task CheckRangeSupport(string url, CancellationToken cancella } } - /// - /// Checks if the server supports range requests, with authentication support. - /// - public async Task CheckRangeSupport(FileDownloadQueueItem queueItem, CancellationToken cancellationToken) - { - try - { - var request = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl); - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); - request.Headers.Range = new RangeHeaderValue(0, 0); - - using var response = await _httpClient.SendAsync(request, cancellationToken); - return response.StatusCode == HttpStatusCode.PartialContent || - response.Headers.AcceptRanges?.Contains("bytes") == true; - } - catch - { - return false; - } - } - public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, CancellationToken cancellationToken) { - var headRequest = new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl); - HttpRequestAuthenticator.ApplyAuthentication(headRequest, queueItem); - var response = await _httpClient.SendAsync(headRequest, cancellationToken); + var response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, queueItem.InputUrl), cancellationToken); var contentLength = response.Content.Headers.ContentLength; if (!contentLength.HasValue) @@ -658,7 +631,6 @@ public async Task DownloadFileParallel(FileDownloadQueueItem queueItem, Cancella private async Task DownloadChunk(FileDownloadQueueItem queueItem, long start, long end, string chunkFile, int chunkIndex, CancellationToken cancellationToken, ParallelDownloadProgress progressTracker) { var request = new HttpRequestMessage(HttpMethod.Get, queueItem.InputUrl); - HttpRequestAuthenticator.ApplyAuthentication(request, queueItem); request.Headers.Range = new RangeHeaderValue(start, end); using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);