diff --git a/TuneRipper.Shared/Configuration/ApiSettings.cs b/TuneRipper.Shared/Configuration/ApiSettings.cs index c0b8bc8..4a223a0 100644 --- a/TuneRipper.Shared/Configuration/ApiSettings.cs +++ b/TuneRipper.Shared/Configuration/ApiSettings.cs @@ -5,7 +5,21 @@ public class ApiSettings public const string SectionName = "ApiSettings"; public string? BaseUrl { get; set; } + public string? HttpBaseUrl { get; set; } public string? ApiKey { get; set; } public int TimeoutSeconds { get; set; } = 30; public bool EnableLogging { get; set; } = true; + public int RetryAttempts { get; set; } = 3; + public int RetryDelaySeconds { get; set; } = 2; +} + +public class AppSettings +{ + public const string SectionName = "AppSettings"; + + public string? AppName { get; set; } + public string? Version { get; set; } + public string? Environment { get; set; } + public bool EnableDetailedErrors { get; set; } = true; + public bool DebugMode { get; set; } = false; } \ No newline at end of file diff --git a/TuneRipper/API_USAGE_EXAMPLES.md b/TuneRipper/API_USAGE_EXAMPLES.md new file mode 100644 index 0000000..0118a04 --- /dev/null +++ b/TuneRipper/API_USAGE_EXAMPLES.md @@ -0,0 +1,192 @@ +# TuneRipper API Service Usage Examples + +## Injecting the Service + +In any Razor component, inject the API service: + +```razor +@inject ITuneRipperApiService ApiService +``` + +## Usage Examples + +### 1. Get Video Details + +```csharp +private async Task GetVideoDetailsExample() +{ + var url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ"; + var videoDetails = await ApiService.GetVideoDetailsAsync(url); + + if (videoDetails != null) + { + Console.WriteLine($"Title: {videoDetails.Title}"); + Console.WriteLine($"Duration: {videoDetails.Duration}"); + Console.WriteLine($"Author: {videoDetails.Author}"); + } +} +``` + +### 2. Start Video Processing + +```csharp +private async Task StartProcessingExample() +{ + var request = new ProcessVideoRequest + { + Url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + Format = "mp3", + Quality = "128" + }; + + var response = await ApiService.StartProcessingAsync(request); + + if (response?.Success == true) + { + var jobId = response.FileName; // This contains the job ID + await MonitorProcessing(jobId); + } +} +``` + +### 3. Monitor Processing Status + +```csharp +private async Task MonitorProcessing(string jobId) +{ + while (true) + { + var status = await ApiService.GetProcessingStatusAsync(jobId); + + if (status == null) break; + + Console.WriteLine($"Status: {status.Status} - {status.PercentageComplete}%"); + + if (status.Status == "Completed" || status.Status == "Failed") + { + break; + } + + await Task.Delay(2000); // Wait 2 seconds before checking again + } + + // Get final result + var result = await ApiService.GetProcessingResultAsync(jobId); + if (result?.Success == true) + { + Console.WriteLine($"Download URL: {result.DownloadUrl}"); + } +} +``` + +### 4. Download File + +```csharp +private async Task DownloadFileExample(string jobId) +{ + var fileBytes = await ApiService.DownloadFileAsync(jobId); + + if (fileBytes != null) + { + // In a Blazor component, you might trigger a download + // This would require additional JavaScript interop + Console.WriteLine($"Downloaded {fileBytes.Length} bytes"); + } +} +``` + +### 5. Check API Health + +```csharp +private async Task CheckApiHealthExample() +{ + var isHealthy = await ApiService.IsApiHealthyAsync(); + + if (isHealthy) + { + Console.WriteLine("API is healthy and responding"); + } + else + { + Console.WriteLine("API is not responding or unhealthy"); + } +} +``` + +### 6. Error Handling + +```csharp +private async Task ErrorHandlingExample() +{ + try + { + var videoDetails = await ApiService.GetVideoDetailsAsync("invalid-url"); + + if (videoDetails == null) + { + // Handle case where API returns null (error occurred) + Console.WriteLine("Failed to get video details"); + } + } + catch (Exception ex) + { + // Handle unexpected exceptions + Console.WriteLine($"Error: {ex.Message}"); + } +} +``` + +## Configuration + +The API service automatically uses the configuration from `appsettings.json`: + +```json +{ + "ApiSettings": { + "BaseUrl": "https://localhost:7160", + "HttpBaseUrl": "http://localhost:5112", + "TimeoutSeconds": 30, + "EnableLogging": true, + "RetryAttempts": 3, + "RetryDelaySeconds": 2 + } +} +``` + +## Complete Component Example + +```razor +@page "/example" +@inject ITuneRipperApiService ApiService + +

API Service Example

+ + + +@if (!string.IsNullOrEmpty(message)) +{ +

@message

+} + +@code { + private string message = ""; + + private async Task TestApi() + { + message = "Testing API..."; + + var isHealthy = await ApiService.IsApiHealthyAsync(); + + if (isHealthy) + { + message = "API is healthy!"; + } + else + { + message = "API is not responding"; + } + + StateHasChanged(); + } +} +``` \ No newline at end of file diff --git a/TuneRipper/Extensions/ConfigurationExtensions.cs b/TuneRipper/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..c017ff5 --- /dev/null +++ b/TuneRipper/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; +using TuneRipper.Shared.Configuration; + +namespace TuneRipper.Extensions; + +public static class ConfigurationExtensions +{ + public static ApiSettings GetApiSettings(this IConfiguration configuration) + { + return configuration.GetSection(ApiSettings.SectionName).Get() ?? new ApiSettings(); + } + + public static AppSettings GetAppSettings(this IConfiguration configuration) + { + return configuration.GetSection(AppSettings.SectionName).Get() ?? new AppSettings(); + } +} \ No newline at end of file diff --git a/TuneRipper/Extensions/ServiceCollectionExtensions.cs b/TuneRipper/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..7a3d88e --- /dev/null +++ b/TuneRipper/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,47 @@ +using TuneRipper.Services; +using TuneRipper.Shared.Configuration; + +namespace TuneRipper.Extensions; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddTuneRipperServices( + this IServiceCollection services, + IConfiguration configuration) + { + // Configure settings + services.Configure(configuration.GetSection(ApiSettings.SectionName)); + services.Configure(configuration.GetSection(AppSettings.SectionName)); + + // Configure HttpClient for API calls + services.AddScoped(sp => + { + var apiSettings = configuration.GetSection(ApiSettings.SectionName).Get(); + var client = new HttpClient(); + + // Try HTTPS first, fallback to HTTP if configured + if (!string.IsNullOrEmpty(apiSettings?.BaseUrl)) + { + client.BaseAddress = new Uri(apiSettings.BaseUrl); + } + else if (!string.IsNullOrEmpty(apiSettings?.HttpBaseUrl)) + { + client.BaseAddress = new Uri(apiSettings.HttpBaseUrl); + } + else + { + // Default fallback + client.BaseAddress = new Uri("https://localhost:7160"); + } + + client.Timeout = TimeSpan.FromSeconds(apiSettings?.TimeoutSeconds ?? 30); + + return client; + }); + + // Register API service + services.AddScoped(); + + return services; + } +} \ No newline at end of file diff --git a/TuneRipper/Program.cs b/TuneRipper/Program.cs index aa9d86e..5697230 100644 --- a/TuneRipper/Program.cs +++ b/TuneRipper/Program.cs @@ -1,11 +1,32 @@ using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using TuneRipper; +using TuneRipper.Extensions; +using TuneRipper.Shared.Configuration; var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); +// Add configuration +builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); +builder.Configuration.AddJsonFile($"appsettings.{builder.HostEnvironment.Environment}.json", optional: true, reloadOnChange: true); -await builder.Build().RunAsync(); +// Add logging +builder.Logging.SetMinimumLevel(LogLevel.Information); + +// Add TuneRipper services (API service, HttpClient, settings) +builder.Services.AddTuneRipperServices(builder.Configuration); + +var app = builder.Build(); + +// Log startup information +var logger = app.Services.GetRequiredService>(); +logger.LogInformation("?? TuneRipper Blazor WebAssembly app starting..."); + +var apiSettings = app.Configuration.GetApiSettings(); +logger.LogInformation("?? API Base URL: {BaseUrl}", apiSettings.BaseUrl); +logger.LogInformation("?? API Settings - Timeout: {Timeout}s, Logging: {Logging}", + apiSettings.TimeoutSeconds, apiSettings.EnableLogging); + +await app.RunAsync(); diff --git a/TuneRipper/Services/TuneRipperApiService.cs b/TuneRipper/Services/TuneRipperApiService.cs new file mode 100644 index 0000000..e4606be --- /dev/null +++ b/TuneRipper/Services/TuneRipperApiService.cs @@ -0,0 +1,367 @@ +using System.Net.Http.Json; +using System.Text.Json; +using TuneRipper.Shared.Models; +using TuneRipper.Shared.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging; + +namespace TuneRipper.Services; + +public interface ITuneRipperApiService +{ + Task GetVideoDetailsAsync(string url); + Task StartProcessingAsync(ProcessVideoRequest request); + Task GetProcessingStatusAsync(string jobId); + Task GetProcessingResultAsync(string jobId); + Task?> GetAllJobsAsync(); + Task DownloadFileAsync(string jobId); + Task IsApiHealthyAsync(); +} + +public class TuneRipperApiService : ITuneRipperApiService +{ + private readonly HttpClient _httpClient; + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + + public TuneRipperApiService( + HttpClient httpClient, + IOptions apiSettings, + ILogger logger) + { + _httpClient = httpClient; + _apiSettings = apiSettings.Value; + _logger = logger; + + // Configure JSON serialization options + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + // Configure HttpClient + _httpClient.BaseAddress = new Uri(_apiSettings.BaseUrl ?? "https://localhost:7160"); + _httpClient.Timeout = TimeSpan.FromSeconds(_apiSettings.TimeoutSeconds); + + if (_apiSettings.EnableLogging) + { + _logger.LogInformation("TuneRipperApiService initialized with base URL: {BaseUrl}", _httpClient.BaseAddress); + } + } + + public async Task GetVideoDetailsAsync(string url) + { + try + { + if (string.IsNullOrWhiteSpace(url)) + { + _logger.LogWarning("GetVideoDetailsAsync called with empty URL"); + return null; + } + + _logger.LogInformation("Fetching video details for URL: {Url}", url); + + var encodedUrl = Uri.EscapeDataString(url); + var response = await _httpClient.GetAsync($"api/video/details?url={encodedUrl}"); + + if (response.IsSuccessStatusCode) + { + var videoDetails = await response.Content.ReadFromJsonAsync(_jsonOptions); + _logger.LogInformation("Successfully retrieved video details for URL: {Url}", url); + return videoDetails; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to get video details. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return null; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while fetching video details for URL: {Url}", url); + return null; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while fetching video details for URL: {Url}", url); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while fetching video details for URL: {Url}", url); + return null; + } + } + + public async Task StartProcessingAsync(ProcessVideoRequest request) + { + try + { + if (request == null || string.IsNullOrWhiteSpace(request.Url)) + { + _logger.LogWarning("StartProcessingAsync called with invalid request"); + return null; + } + + _logger.LogInformation("Starting video processing for URL: {Url}", request.Url); + + var response = await _httpClient.PostAsJsonAsync("api/video/process", request, _jsonOptions); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(_jsonOptions); + _logger.LogInformation("Successfully started processing for URL: {Url}, JobId: {JobId}", + request.Url, result?.FileName); + return result; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to start processing. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return new ProcessVideoResponse + { + Success = false, + Message = $"Failed to start processing: {errorContent}" + }; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while starting video processing"); + return new ProcessVideoResponse { Success = false, Message = "Network error occurred" }; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while starting video processing"); + return new ProcessVideoResponse { Success = false, Message = "Request timeout" }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while starting video processing"); + return new ProcessVideoResponse { Success = false, Message = "An unexpected error occurred" }; + } + } + + public async Task GetProcessingStatusAsync(string jobId) + { + try + { + if (string.IsNullOrWhiteSpace(jobId)) + { + _logger.LogWarning("GetProcessingStatusAsync called with empty jobId"); + return null; + } + + var response = await _httpClient.GetAsync($"api/video/status/{jobId}"); + + if (response.IsSuccessStatusCode) + { + var status = await response.Content.ReadFromJsonAsync(_jsonOptions); + return status; + } + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _logger.LogWarning("Job not found: {JobId}", jobId); + return new DownloadProgress + { + Status = "NotFound", + Message = "Job not found", + PercentageComplete = 0 + }; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to get processing status. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return null; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while getting processing status for job: {JobId}", jobId); + return null; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while getting processing status for job: {JobId}", jobId); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while getting processing status for job: {JobId}", jobId); + return null; + } + } + + public async Task GetProcessingResultAsync(string jobId) + { + try + { + if (string.IsNullOrWhiteSpace(jobId)) + { + _logger.LogWarning("GetProcessingResultAsync called with empty jobId"); + return null; + } + + var response = await _httpClient.GetAsync($"api/video/result/{jobId}"); + + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(_jsonOptions); + return result; + } + + if (response.StatusCode == System.Net.HttpStatusCode.Accepted) + { + return new ProcessVideoResponse + { + Success = false, + Message = "Processing not completed yet" + }; + } + + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _logger.LogWarning("Job not found: {JobId}", jobId); + return new ProcessVideoResponse + { + Success = false, + Message = "Job not found" + }; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to get processing result. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return new ProcessVideoResponse + { + Success = false, + Message = $"Failed to get result: {errorContent}" + }; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while getting processing result for job: {JobId}", jobId); + return new ProcessVideoResponse { Success = false, Message = "Network error occurred" }; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while getting processing result for job: {JobId}", jobId); + return new ProcessVideoResponse { Success = false, Message = "Request timeout" }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while getting processing result for job: {JobId}", jobId); + return new ProcessVideoResponse { Success = false, Message = "An unexpected error occurred" }; + } + } + + public async Task?> GetAllJobsAsync() + { + try + { + _logger.LogInformation("Fetching all jobs"); + + var response = await _httpClient.GetAsync("api/video/jobs"); + + if (response.IsSuccessStatusCode) + { + var jobs = await response.Content.ReadFromJsonAsync>(_jsonOptions); + _logger.LogInformation("Successfully retrieved {Count} jobs", jobs?.Count() ?? 0); + return jobs; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to get jobs. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return null; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while getting all jobs"); + return null; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while getting all jobs"); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while getting all jobs"); + return null; + } + } + + public async Task DownloadFileAsync(string jobId) + { + try + { + if (string.IsNullOrWhiteSpace(jobId)) + { + _logger.LogWarning("DownloadFileAsync called with empty jobId"); + return null; + } + + _logger.LogInformation("Downloading file for job: {JobId}", jobId); + + var response = await _httpClient.GetAsync($"api/download/{jobId}"); + + if (response.IsSuccessStatusCode) + { + var fileBytes = await response.Content.ReadAsByteArrayAsync(); + _logger.LogInformation("Successfully downloaded file for job: {JobId}, Size: {Size} bytes", + jobId, fileBytes.Length); + return fileBytes; + } + + var errorContent = await response.Content.ReadAsStringAsync(); + _logger.LogWarning("Failed to download file. Status: {StatusCode}, Error: {Error}", + response.StatusCode, errorContent); + + return null; + } + catch (HttpRequestException ex) + { + _logger.LogError(ex, "HTTP error while downloading file for job: {JobId}", jobId); + return null; + } + catch (TaskCanceledException ex) + { + _logger.LogError(ex, "Timeout while downloading file for job: {JobId}", jobId); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Unexpected error while downloading file for job: {JobId}", jobId); + return null; + } + } + + public async Task IsApiHealthyAsync() + { + try + { + var response = await _httpClient.GetAsync("api/health"); + var isHealthy = response.IsSuccessStatusCode; + + if (_apiSettings.EnableLogging) + { + _logger.LogInformation("API health check result: {IsHealthy}", isHealthy); + } + + return isHealthy; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during API health check"); + return false; + } + } +} \ No newline at end of file diff --git a/TuneRipper/_Imports.razor b/TuneRipper/_Imports.razor index 9a3dbb2..cb30bbd 100644 --- a/TuneRipper/_Imports.razor +++ b/TuneRipper/_Imports.razor @@ -8,3 +8,7 @@ @using Microsoft.JSInterop @using TuneRipper @using TuneRipper.Layout +@using TuneRipper.Services +@using TuneRipper.Extensions +@using TuneRipper.Shared.Models +@using TuneRipper.Shared.Configuration diff --git a/TuneRipper/wwwroot/appsettings.Development.json b/TuneRipper/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..4015160 --- /dev/null +++ b/TuneRipper/wwwroot/appsettings.Development.json @@ -0,0 +1,15 @@ +{ + "ApiSettings": { + "BaseUrl": "https://localhost:7160", + "HttpBaseUrl": "http://localhost:5112", + "TimeoutSeconds": 60, + "EnableLogging": true, + "RetryAttempts": 3, + "RetryDelaySeconds": 1 + }, + "AppSettings": { + "Environment": "Development", + "EnableDetailedErrors": true, + "DebugMode": true + } +} \ No newline at end of file diff --git a/TuneRipper/wwwroot/appsettings.json b/TuneRipper/wwwroot/appsettings.json new file mode 100644 index 0000000..c55d058 --- /dev/null +++ b/TuneRipper/wwwroot/appsettings.json @@ -0,0 +1,16 @@ +{ + "ApiSettings": { + "BaseUrl": "https://localhost:7160", + "HttpBaseUrl": "http://localhost:5112", + "TimeoutSeconds": 30, + "EnableLogging": true, + "RetryAttempts": 3, + "RetryDelaySeconds": 2 + }, + "AppSettings": { + "AppName": "TuneRipper", + "Version": "1.0.0", + "Environment": "Development", + "EnableDetailedErrors": true + } +} \ No newline at end of file