Add DI, config, and API service for Blazor frontend

Introduce configuration classes and environment-specific settings files. Add DI extensions for registering API/app settings and HttpClient. Implement ITuneRipperApiService with robust error handling and logging. Update Program.cs for config loading and startup logging. Add usage documentation and update imports for new services and extensions. Improves maintainability and developer experience.
This commit is contained in:
Michal Vrba
2025-12-11 14:37:38 +01:00
parent e2d9ee3c59
commit 8bfcc3617b
9 changed files with 695 additions and 2 deletions
+192
View File
@@ -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
<h3>API Service Example</h3>
<button @onclick="TestApi">Test API</button>
@if (!string.IsNullOrEmpty(message))
{
<p>@message</p>
}
@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();
}
}
```
@@ -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<ApiSettings>() ?? new ApiSettings();
}
public static AppSettings GetAppSettings(this IConfiguration configuration)
{
return configuration.GetSection(AppSettings.SectionName).Get<AppSettings>() ?? new AppSettings();
}
}
@@ -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<ApiSettings>(configuration.GetSection(ApiSettings.SectionName));
services.Configure<AppSettings>(configuration.GetSection(AppSettings.SectionName));
// Configure HttpClient for API calls
services.AddScoped(sp =>
{
var apiSettings = configuration.GetSection(ApiSettings.SectionName).Get<ApiSettings>();
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<ITuneRipperApiService, TuneRipperApiService>();
return services;
}
}
+23 -2
View File
@@ -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>("#app");
builder.RootComponents.Add<HeadOutlet>("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<ILogger<Program>>();
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();
+367
View File
@@ -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<YouTubeVideoDetails?> GetVideoDetailsAsync(string url);
Task<ProcessVideoResponse?> StartProcessingAsync(ProcessVideoRequest request);
Task<DownloadProgress?> GetProcessingStatusAsync(string jobId);
Task<ProcessVideoResponse?> GetProcessingResultAsync(string jobId);
Task<IEnumerable<JobStatusResponse>?> GetAllJobsAsync();
Task<byte[]?> DownloadFileAsync(string jobId);
Task<bool> IsApiHealthyAsync();
}
public class TuneRipperApiService : ITuneRipperApiService
{
private readonly HttpClient _httpClient;
private readonly ApiSettings _apiSettings;
private readonly ILogger<TuneRipperApiService> _logger;
private readonly JsonSerializerOptions _jsonOptions;
public TuneRipperApiService(
HttpClient httpClient,
IOptions<ApiSettings> apiSettings,
ILogger<TuneRipperApiService> 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<YouTubeVideoDetails?> 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<YouTubeVideoDetails>(_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<ProcessVideoResponse?> 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<ProcessVideoResponse>(_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<DownloadProgress?> 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<DownloadProgress>(_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<ProcessVideoResponse?> 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<ProcessVideoResponse>(_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<IEnumerable<JobStatusResponse>?> GetAllJobsAsync()
{
try
{
_logger.LogInformation("Fetching all jobs");
var response = await _httpClient.GetAsync("api/video/jobs");
if (response.IsSuccessStatusCode)
{
var jobs = await response.Content.ReadFromJsonAsync<IEnumerable<JobStatusResponse>>(_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<byte[]?> 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<bool> 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;
}
}
}
+4
View File
@@ -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
@@ -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
}
}
+16
View File
@@ -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
}
}