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