Add "Download All in ZIP" feature for session downloads

Introduce automatic ZIP file creation and management for session-based downloads.
- Server: Add endpoints to serve and provide info about session ZIPs; VideoService now updates ZIP after each MP3 download with user-friendly filenames.
- Client: Add "Download All in ZIP" button with loading state and ZIP info; trigger browser download via JS interop; responsive and styled UI.
- API: Extend service interfaces for ZIP download/info; add SessionZipInfo model.
- Docs: Update SESSION_MANAGEMENT.md with ZIP workflow, endpoints, and UX details.
- Robust error handling and logging throughout.
This commit is contained in:
Michal Vrba
2025-12-14 11:55:53 +01:00
parent 2d2e855202
commit bf305de75f
7 changed files with 588 additions and 23 deletions
+117 -22
View File
@@ -1,18 +1,20 @@
# Session Management Implementation
This document describes the session management feature implemented for TuneRipper to organize MP3 files by user sessions.
This document describes the session management feature implemented for TuneRipper to organize MP3 files by user sessions, including automatic ZIP file creation and management.
## Overview
The session management system automatically creates and manages user sessions to organize downloaded files in separate directories. Each session gets its own subdirectory under the downloads folder.
The session management system automatically creates and manages user sessions to organize downloaded files in separate directories. Each session gets its own subdirectory under the downloads folder, with automatic ZIP file creation and updates.
## Key Features
1. **Automatic Session Creation**: When the app starts, it creates or retrieves an existing session ID
2. **Session-Based File Organization**: All MP3 files are stored in `/downloads/{sessionId}/` directories
3. **Session Persistence**: Session IDs are stored in browser's localStorage
4. **Session Validation**: Server validates session IDs and handles expired sessions
5. **Backward Compatibility**: Legacy endpoints still work for existing files
3. **Automatic ZIP File Management**: Session ZIP files are created/updated automatically after each successful download
4. **Download All in ZIP**: Users can download all session files in a single ZIP archive
5. **Session Persistence**: Session IDs are stored in browser's localStorage
6. **Session Validation**: Server validates session IDs and handles expired sessions
7. **Backward Compatibility**: Legacy endpoints still work for existing files
## Architecture Changes
@@ -26,9 +28,16 @@ The session management system automatically creates and manages user sessions to
- Automatic cleanup of expired sessions (24-hour timeout)
- Background timer for periodic cleanup
#### Updated Services
- **`VideoService`**: Enhanced with ZIP file management
- Automatically creates/updates session ZIP files after successful MP3 downloads
- Uses System.IO.Compression for ZIP operations
- Creates user-friendly filenames within ZIP archives
- Handles ZIP creation errors gracefully without breaking main process
#### Updated Controllers
- **`VideoController`**: Now accepts and manages session IDs in processing requests
- **`DownloadController`**: Enhanced with session-aware endpoints
- **`DownloadController`**: Enhanced with session-aware endpoints and ZIP download support
- **`SessionController`**: New controller for session CRUD operations
#### New Endpoints
@@ -41,6 +50,9 @@ GET /api/download/{sessionId}/{jobId} - Session-aware file download
GET /api/download/info/{sessionId}/{jobId} - Session-aware file info
DELETE /api/download/{sessionId}/{jobId} - Session-aware file deletion
GET /api/download/zip/{sessionId} - Download session ZIP file
GET /api/download/zip/info/{sessionId} - Get session ZIP file information
GET /api/video/jobs/{sessionId} - Get jobs for specific session
```
@@ -49,13 +61,22 @@ GET /api/video/jobs/{sessionId} - Get jobs for specific session
downloads/
??? legacy_files.mp3 # Legacy files (root level)
??? {sessionId1}/ # Session 1 directory
? ??? {jobId}_video_title.mp3 # Session 1 files
? ??? {jobId}_video_title.mp3 # Session 1 MP3 files
? ??? {jobId}_video_title.mp3
? ??? session_{sessionId1}.zip # Automatic ZIP containing all MP3s
??? {sessionId2}/ # Session 2 directory
??? {jobId}_video_title.mp3 # Session 2 files
??? {jobId}_video_title.mp3 # Session 2 MP3 files
??? {jobId}_video_title.mp3
??? session_{sessionId2}.zip # Automatic ZIP containing all MP3s
```
#### ZIP File Management
- **Automatic Creation**: ZIP files are created/updated after each successful MP3 download
- **User-Friendly Names**: Files within ZIP use clean titles instead of job IDs
- **Atomic Updates**: Old ZIP is deleted before creating new one to ensure consistency
- **Error Handling**: ZIP creation failures don't interrupt main download process
- **Efficient Storage**: Only MP3 files are included in ZIP archives
### Client-Side (TuneRipper)
#### New Services
@@ -66,16 +87,20 @@ downloads/
- Automatic session creation when needed
#### Updated Components
- **`Home.razor`**: Enhanced to use session management
- **`Home.razor`**: Enhanced to use session management and ZIP downloads
- Displays current session ID (truncated)
- Includes session ID in processing requests
- Constructs session-aware download URLs
- **"Download All in ZIP" Button**: Prominently displayed when multiple files exist
- ZIP file information display (file count, size)
- Loading states for ZIP preparation
#### Updated API Service
- **`TuneRipperApiService`**: Extended with session-aware methods
- **`TuneRipperApiService`**: Extended with session-aware methods and ZIP support
- Session creation and validation
- Session-aware file downloads
- Session-specific job queries
- **ZIP download methods**: `DownloadSessionZipAsync()`, `GetSessionZipInfoAsync()`
## Data Flow
@@ -86,27 +111,55 @@ downloads/
2. **Video Processing**:
```
User submits URL ? Include sessionId in request ? Server processes to session directory
User submits URL ? Include sessionId in request ? Server processes to session directory ? Update ZIP file
```
3. **File Download**:
3. **File Download** (Individual):
```
User clicks download ? Use session-aware URL ? Server serves from session directory
```
4. **ZIP Download** (All Files):
```
User clicks "Download All in ZIP" ? Server serves session ZIP file ? Browser downloads ZIP
```
## ZIP File Features
### Automatic ZIP Management
1. **Creation Trigger**: ZIP file is created/updated after each successful MP3 download
2. **Content**: Contains all MP3 files in the session with user-friendly names
3. **Naming Convention**: `session_{sessionId}.zip`
4. **Download Name**: `TuneRipper_Session_{sessionId_prefix}.zip`
### User Interface
1. **Visibility**: "Download All in ZIP" button appears when 2+ files exist in session
2. **Information Display**: Shows file count and ZIP file size
3. **Visual Design**: Distinctive purple gradient button with package icon
4. **Loading States**: Shows "Preparing ZIP..." during download initiation
5. **Responsive**: Adapts to mobile screen sizes
### Technical Implementation
1. **Server-Side**: Uses `System.IO.Compression.ZipFile` for ZIP operations
2. **Client-Side**: Direct browser download via JavaScript `window.open()`
3. **Error Handling**: Graceful fallback if ZIP is not available
4. **Performance**: ZIP creation happens asynchronously without blocking UI
## Session Lifecycle
1. **Creation**: 16-character unique ID generated
2. **Storage**: Stored in browser localStorage and server memory
3. **Validation**: Checked on each request, 24-hour timeout
4. **Cleanup**: Automatic background cleanup of expired sessions
5. **Directory Management**: Session directories created/deleted as needed
4. **ZIP Management**: ZIP files created/updated automatically with each download
5. **Cleanup**: Automatic background cleanup of expired sessions and their ZIP files
6. **Directory Management**: Session directories and ZIP files created/deleted as needed
## Backward Compatibility
- Legacy download endpoints still function
- Old files remain accessible through fallback logic
- Gradual migration as users create new sessions
- No ZIP files created for legacy sessions
## Configuration
@@ -114,9 +167,13 @@ downloads/
- **Timeout**: 24 hours (configurable in `SessionService`)
- **Cleanup Interval**: 1 hour (configurable in `SessionService`)
- **Directory Structure**: `/downloads/{sessionId}/`
- **ZIP Naming**: `session_{sessionId}.zip`
### Environment Variables
No new environment variables required - uses existing web root path configuration.
### ZIP Settings
- **Compression**: Default ZIP compression
- **Content**: MP3 files only (excludes temporary files)
- **Entry Names**: User-friendly titles extracted from file names
- **Update Strategy**: Replace entire ZIP file on each update
## Security Considerations
@@ -124,21 +181,26 @@ No new environment variables required - uses existing web root path configuratio
- Server validates all session operations
- Automatic cleanup prevents disk space accumulation
- No sensitive data stored in sessions
- ZIP files are only accessible with valid session IDs
## Monitoring and Logging
- Session creation/validation logged
- File operations include session context
- ZIP creation/update operations logged
- Cleanup operations logged with statistics
- Error handling for session failures
- Error handling for session and ZIP failures
## Future Enhancements
1. **Database Storage**: Replace in-memory session storage with persistent storage
2. **Session Sharing**: Allow sharing sessions between devices
3. **Session Analytics**: Track usage patterns and statistics
4. **Extended Metadata**: Store additional session information
5. **Admin Interface**: Management interface for session administration
2. **ZIP Compression Options**: Allow users to choose compression levels
3. **Selective ZIP Downloads**: Allow users to choose which files to include in ZIP
4. **ZIP Password Protection**: Optional password protection for ZIP files
5. **Session Sharing**: Allow sharing sessions between devices
6. **Session Analytics**: Track usage patterns and statistics
7. **Extended Metadata**: Store additional session information
8. **Admin Interface**: Management interface for session administration
## API Examples
@@ -154,8 +216,41 @@ POST /api/video/process
{ "url": "https://youtube.com/watch?v=...", "sessionId": "abc123def456", "format": "mp3" }
```
### Download File by Session
### Download Individual File
```http
GET /api/download/abc123def456/job-id-123
Response: Binary audio file
```
### Download Session ZIP
```http
GET /api/download/zip/abc123def456
Response: Binary ZIP file containing all session MP3s
```
### Get ZIP Information
```http
GET /api/download/zip/info/abc123def456
Response: {
"sessionId": "abc123def456",
"zipFileName": "session_abc123def456.zip",
"fileSize": "15.2 MB",
"mp3FilesCount": 5,
"downloadUrl": "/api/download/zip/abc123def456"
}
```
## User Experience
### New User Flow
1. User visits site ? Session created automatically
2. User downloads videos ? Files organized in session, ZIP updated
3. User sees "Download All in ZIP" button ? Can download all files at once
4. User returns later ? Session restored from localStorage
### Benefits for Users
- **Organization**: All downloads from a session grouped together
- **Convenience**: Single-click download of all files via ZIP
- **Efficiency**: No need to download files individually
- **Clean Downloads**: Files in ZIP have user-friendly names
- **Session Persistence**: Downloads available across browser sessions
@@ -18,6 +18,108 @@ public class DownloadController : ControllerBase
_sessionService = sessionService;
}
// New endpoint to download session ZIP file
[HttpGet("zip/{sessionId}")]
public IActionResult DownloadSessionZip(string sessionId)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
return BadRequest("Session ID is required");
}
if (!_sessionService.IsValidSession(sessionId))
{
return NotFound("Invalid or expired session");
}
try
{
var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId);
if (!Directory.Exists(sessionDownloadsDir))
{
_logger.LogWarning("Session downloads directory not found: {Path}", sessionDownloadsDir);
return NotFound("Session downloads directory not found");
}
var zipFileName = $"session_{sessionId}.zip";
var zipFilePath = Path.Combine(sessionDownloadsDir, zipFileName);
if (!System.IO.File.Exists(zipFilePath))
{
_logger.LogWarning("Session ZIP file not found: {ZipFile}", zipFilePath);
return NotFound("No ZIP file available for this session");
}
_logger.LogInformation("Serving session ZIP file: {ZipFile} for session: {SessionId}", zipFileName, sessionId);
var friendlyZipName = $"TuneRipper_Session_{sessionId.Substring(0, 8)}.zip";
var fileStream = new FileStream(zipFilePath, FileMode.Open, FileAccess.Read);
return File(fileStream, "application/zip", friendlyZipName);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading session ZIP for session: {SessionId}", sessionId);
return StatusCode(500, $"Error downloading session ZIP: {ex.Message}");
}
}
// New endpoint to get session ZIP info
[HttpGet("zip/info/{sessionId}")]
public IActionResult GetSessionZipInfo(string sessionId)
{
if (string.IsNullOrWhiteSpace(sessionId))
{
return BadRequest("Session ID is required");
}
if (!_sessionService.IsValidSession(sessionId))
{
return NotFound("Invalid or expired session");
}
try
{
var sessionDownloadsDir = _sessionService.GetSessionDownloadsPath(sessionId);
if (!Directory.Exists(sessionDownloadsDir))
{
return NotFound("Session downloads directory not found");
}
var zipFileName = $"session_{sessionId}.zip";
var zipFilePath = Path.Combine(sessionDownloadsDir, zipFileName);
if (!System.IO.File.Exists(zipFilePath))
{
return NotFound("No ZIP file available for this session");
}
var fileInfo = new FileInfo(zipFilePath);
var mp3Count = Directory.GetFiles(sessionDownloadsDir, "*.mp3").Length;
var info = new
{
sessionId,
zipFileName,
fileSize = FormatFileSize(fileInfo.Length),
fileSizeBytes = fileInfo.Length,
mp3FilesCount = mp3Count,
createdAt = fileInfo.CreationTime,
lastModified = fileInfo.LastWriteTime,
downloadUrl = $"/api/download/zip/{sessionId}"
};
return Ok(info);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting session ZIP info for session: {SessionId}", sessionId);
return StatusCode(500, $"Error getting session ZIP info: {ex.Message}");
}
}
// New session-aware download endpoint
[HttpGet("{sessionId}/{jobId}")]
public IActionResult DownloadFileBySession(string sessionId, string jobId)
+12
View File
@@ -66,9 +66,20 @@ app.MapGet("/", (IWebHostEnvironment env) => new
get_result = "/api/video/result/{jobId}",
download = "/api/download/{jobId}",
download_by_session = "/api/download/{sessionId}/{jobId}",
download_session_zip = "/api/download/zip/{sessionId}",
session_zip_info = "/api/download/zip/info/{sessionId}",
session_create = "/api/session/create",
session_validate = "/api/session/validate/{sessionId}",
static_files = "/downloads/{fileName}",
openapi = "/openapi/v1.json"
},
features = new
{
session_management = true,
automatic_zip_creation = true,
session_based_organization = true,
backward_compatibility = true
},
testing = new
{
httpFile = "Use api-tests.http to test endpoints",
@@ -88,5 +99,6 @@ app.Logger.LogInformation("?? Health check: /api/health");
app.Logger.LogInformation("?? API info: /");
app.Logger.LogInformation("?? Static files served from: /downloads/*");
app.Logger.LogInformation("?? Session-based downloads supported");
app.Logger.LogInformation("?? Automatic ZIP file creation enabled");
app.Run();
+91
View File
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text;
using System.IO.Compression;
namespace TuneRipper.Api.Services;
@@ -206,6 +207,9 @@ public class VideoService : IVideoService
_logger.LogInformation("Successfully processed video. Job: {JobId}, Session: {SessionId}, File: {FileName}", jobId, sessionId, finalFileName);
// Update the session ZIP file after successful download
await UpdateSessionZipAsync(sessionId, downloadDir);
progressCallback(100, "Processing completed successfully");
return finalFileName;
@@ -218,6 +222,93 @@ public class VideoService : IVideoService
}
}
private async Task UpdateSessionZipAsync(string sessionId, string sessionDir)
{
try
{
var zipFileName = $"session_{sessionId}.zip";
var zipFilePath = Path.Combine(sessionDir, zipFileName);
// Get all MP3 files in the session directory
var mp3Files = Directory.GetFiles(sessionDir, "*.mp3")
.Where(f => !f.EndsWith(zipFileName)) // Exclude the ZIP file itself
.ToArray();
if (mp3Files.Length == 0)
{
_logger.LogWarning("No MP3 files found to add to ZIP for session: {SessionId}", sessionId);
return;
}
// Delete existing ZIP file if it exists
if (File.Exists(zipFilePath))
{
File.Delete(zipFilePath);
}
// Create new ZIP file with all MP3 files
using (var archive = ZipFile.Open(zipFilePath, ZipArchiveMode.Create))
{
foreach (var mp3File in mp3Files)
{
var fileName = Path.GetFileName(mp3File);
// Create user-friendly filename for ZIP entry
var friendlyFileName = CreateUserFriendlyFileName(fileName);
// Add file to ZIP
archive.CreateEntryFromFile(mp3File, friendlyFileName);
}
}
_logger.LogInformation("Updated session ZIP file: {ZipFile} with {Count} MP3 files for session: {SessionId}",
zipFileName, mp3Files.Length, sessionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating session ZIP file for session: {SessionId}", sessionId);
// Don't throw - ZIP creation failure shouldn't break the main process
}
}
private string CreateUserFriendlyFileName(string originalFileName)
{
try
{
// Parse our pattern: jobId_title.ext
var nameWithoutExt = Path.GetFileNameWithoutExtension(originalFileName);
var extension = Path.GetExtension(originalFileName);
// Split by underscore - first part is job ID, rest is title
var underscoreIndex = nameWithoutExt.IndexOf('_');
if (underscoreIndex > 0 && underscoreIndex < nameWithoutExt.Length - 1)
{
// Extract title part (everything after the first underscore)
var titlePart = nameWithoutExt.Substring(underscoreIndex + 1);
// Convert underscores back to spaces in title
var friendlyTitle = titlePart.Replace("_", " ");
// Create a clean filename: "Title.mp3"
if (friendlyTitle.Length > 0)
{
// Remove any duplicate spaces and clean up
friendlyTitle = System.Text.RegularExpressions.Regex.Replace(friendlyTitle, @"\s+", " ").Trim();
return $"{friendlyTitle}{extension}";
}
}
// Fallback to original filename if pattern doesn't match
return originalFileName;
}
catch (Exception)
{
// If anything goes wrong, return the original filename
return originalFileName;
}
}
private string SanitizeFileName(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
+86
View File
@@ -128,6 +128,31 @@
{
<div class="download-links-section">
<h3 class="downloads-title">📥 Your Downloads</h3>
@* Download All in ZIP Button *@
@if (!string.IsNullOrWhiteSpace(currentSessionId) && downloadLinks.Count > 1)
{
<div class="download-all-container">
<button class="download-all-btn @(isLoadingZip ? "loading" : "")"
@onclick="DownloadAllInZip"
disabled="@isLoadingZip">
<span class="download-all-icon">📦</span>
@if (isLoadingZip)
{
<span>Preparing ZIP...</span>
}
else
{
<span>Download All in ZIP</span>
}
@if (sessionZipInfo != null)
{
<span class="zip-info">(@sessionZipInfo.Mp3FilesCount files, @sessionZipInfo.FileSize)</span>
}
</button>
</div>
}
<div class="download-links">
@foreach (var download in downloadLinks.AsEnumerable().Reverse())
{
@@ -168,11 +193,13 @@
private string outputMessage = "";
private string apiErrorMessage = "";
private bool isProcessing = false;
private bool isLoadingZip = false;
private int progressPercentage = 0;
private string progressMessage = "";
private List<DownloadItem> downloadLinks = new();
private YouTubeVideoDetails? currentVideoDetails = null;
private string currentSessionId = "";
private SessionZipInfo? sessionZipInfo = null;
public class DownloadItem
{
@@ -189,6 +216,10 @@
// Initialize session on component load
currentSessionId = await SessionService.GetCurrentSessionIdAsync();
Logger.LogInformation("Initialized with session: {SessionId}", currentSessionId);
// Load session ZIP info if available
await LoadSessionZipInfoAsync();
StateHasChanged();
}
catch (Exception ex)
@@ -198,6 +229,58 @@
}
}
private async Task LoadSessionZipInfoAsync()
{
try
{
if (!string.IsNullOrWhiteSpace(currentSessionId))
{
sessionZipInfo = await ApiService.GetSessionZipInfoAsync(currentSessionId);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading session ZIP info");
// Don't block the UI if ZIP info loading fails
}
}
private async Task DownloadAllInZip()
{
if (string.IsNullOrWhiteSpace(currentSessionId))
{
apiErrorMessage = "Session not available for ZIP download";
return;
}
try
{
isLoadingZip = true;
StateHasChanged();
Logger.LogInformation("Downloading session ZIP for session: {SessionId}", currentSessionId);
// Create the download URL for the ZIP file
var zipDownloadUrl = $"https://localhost:7160/api/download/zip/{currentSessionId}";
// Use browser to download the ZIP file
await JSRuntime.InvokeVoidAsync("open", zipDownloadUrl, "_blank");
Logger.LogInformation("Successfully initiated ZIP download for session: {SessionId}", currentSessionId);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error downloading session ZIP for session: {SessionId}", currentSessionId);
apiErrorMessage = $"Error downloading ZIP file: {ex.Message}";
StateHasChanged();
}
finally
{
isLoadingZip = false;
StateHasChanged();
}
}
private async Task DownloadMp3()
{
if (!string.IsNullOrWhiteSpace(youtubeUrl))
@@ -377,6 +460,9 @@
progressPercentage = 100;
progressMessage = "Complete!";
// Reload session ZIP info after successful download
await LoadSessionZipInfoAsync();
// Small delay to show completion
await Task.Delay(500);
+93
View File
@@ -125,6 +125,89 @@
font-size: 1.2rem;
}
/* Download All in ZIP Button Styles */
.download-all-container {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.download-all-btn {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%);
color: white;
border: none;
padding: 16px 28px;
border-radius: 12px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
min-width: 220px;
position: relative;
overflow: hidden;
}
.download-all-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(99, 102, 241, 0.4);
}
.download-all-btn:disabled {
opacity: 0.7;
cursor: not-allowed;
transform: none;
}
.download-all-btn.loading::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.2),
transparent
);
animation: shimmer 1.5s infinite;
}
.download-all-icon {
font-size: 1.3rem;
animation: bounce 2s infinite;
}
.zip-info {
font-size: 0.85rem;
opacity: 0.9;
font-weight: 400;
margin-left: 4px;
}
@keyframes shimmer {
0% { left: -100%; }
100% { left: 100%; }
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-3px);
}
60% {
transform: translateY(-2px);
}
}
.output-message {
background: #f0f9ff;
border: 1px solid #bae6fd;
@@ -481,6 +564,16 @@
font-size: 0.8rem;
padding: 6px 12px;
}
.download-all-btn {
min-width: 180px;
padding: 14px 20px;
font-size: 0.9rem;
}
.zip-info {
font-size: 0.8rem;
}
}
/* Responsive Design */
@@ -16,12 +16,26 @@ public interface ITuneRipperApiService
Task<IEnumerable<JobStatusResponse>?> GetJobsBySessionAsync(string sessionId);
Task<byte[]?> DownloadFileAsync(string jobId);
Task<byte[]?> DownloadFileBySessionAsync(string sessionId, string jobId);
Task<byte[]?> DownloadSessionZipAsync(string sessionId);
Task<SessionZipInfo?> GetSessionZipInfoAsync(string sessionId);
Task<bool> IsApiHealthyAsync();
Task<string?> CreateSessionAsync();
Task<bool> ValidateSessionAsync(string sessionId);
Task<bool> RefreshSessionAsync(string sessionId);
}
public class SessionZipInfo
{
public string? SessionId { get; set; }
public string? ZipFileName { get; set; }
public string? FileSize { get; set; }
public long FileSizeBytes { get; set; }
public int Mp3FilesCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime LastModified { get; set; }
public string? DownloadUrl { get; set; }
}
public class TuneRipperApiService : ITuneRipperApiService
{
private readonly HttpClient _httpClient;
@@ -55,6 +69,78 @@ public class TuneRipperApiService : ITuneRipperApiService
}
}
public async Task<byte[]?> DownloadSessionZipAsync(string sessionId)
{
try
{
if (string.IsNullOrWhiteSpace(sessionId))
{
_logger.LogWarning("DownloadSessionZipAsync called with empty sessionId");
return null;
}
_logger.LogInformation("Downloading session ZIP for session: {SessionId}", sessionId);
var response = await _httpClient.GetAsync($"api/download/zip/{sessionId}");
if (response.IsSuccessStatusCode)
{
var fileBytes = await response.Content.ReadAsByteArrayAsync();
_logger.LogInformation("Successfully downloaded session ZIP for session: {SessionId}, Size: {Size} bytes",
sessionId, fileBytes.Length);
return fileBytes;
}
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogWarning("Failed to download session ZIP. Status: {StatusCode}, Error: {Error}",
response.StatusCode, errorContent);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while downloading session ZIP for session: {SessionId}", sessionId);
return null;
}
}
public async Task<SessionZipInfo?> GetSessionZipInfoAsync(string sessionId)
{
try
{
if (string.IsNullOrWhiteSpace(sessionId))
{
_logger.LogWarning("GetSessionZipInfoAsync called with empty sessionId");
return null;
}
var response = await _httpClient.GetAsync($"api/download/zip/info/{sessionId}");
if (response.IsSuccessStatusCode)
{
var zipInfo = await response.Content.ReadFromJsonAsync<SessionZipInfo>(_jsonOptions);
return zipInfo;
}
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
_logger.LogInformation("No ZIP file available for session: {SessionId}", sessionId);
return null;
}
var errorContent = await response.Content.ReadAsStringAsync();
_logger.LogWarning("Failed to get session ZIP info. Status: {StatusCode}, Error: {Error}",
response.StatusCode, errorContent);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while getting session ZIP info for session: {SessionId}", sessionId);
return null;
}
}
public async Task<string?> CreateSessionAsync()
{
try