Initial implementation of TuneRipper API backend
- Add Health, Video, and Download controllers with endpoints for health checks, video details, processing, status, results, and simulated downloads - Implement IVideoService for YouTube URL validation and mock video info - Add API models for responses and job status tracking - Configure DI, CORS (BlazorPolicy), OpenAPI, and root metadata endpoint in Program.cs - Add README with usage, troubleshooting, and development notes - Minor project file cleanup and encoding fix
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace TuneRipper.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class DownloadController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet("{jobId}")]
|
||||||
|
public IActionResult DownloadFile(string jobId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(jobId))
|
||||||
|
{
|
||||||
|
return BadRequest("Job ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// In production, you would:
|
||||||
|
// 1. Verify the job exists and is completed
|
||||||
|
// 2. Get the actual file path from storage
|
||||||
|
// 3. Return the actual file
|
||||||
|
|
||||||
|
// For demo purposes, return a sample response
|
||||||
|
var sampleContent = System.Text.Encoding.UTF8.GetBytes("Sample MP3 file content - this would be actual audio data in production");
|
||||||
|
var fileName = $"video_{jobId}.mp3";
|
||||||
|
|
||||||
|
return File(sampleContent, "audio/mpeg", fileName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, $"Error downloading file: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("info/{jobId}")]
|
||||||
|
public IActionResult GetDownloadInfo(string jobId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(jobId))
|
||||||
|
{
|
||||||
|
return BadRequest("Job ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return download metadata
|
||||||
|
var info = new
|
||||||
|
{
|
||||||
|
jobId,
|
||||||
|
fileName = $"video_{jobId}.mp3",
|
||||||
|
fileSize = "3.2 MB",
|
||||||
|
format = "MP3",
|
||||||
|
quality = "128 kbps",
|
||||||
|
createdAt = DateTime.UtcNow,
|
||||||
|
expiresAt = DateTime.UtcNow.AddHours(24)
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace TuneRipper.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
status = "healthy",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
version = "1.0.0",
|
||||||
|
service = "TuneRipper.Api",
|
||||||
|
environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("ping")]
|
||||||
|
public IActionResult Ping()
|
||||||
|
{
|
||||||
|
return Ok(new { message = "pong", timestamp = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TuneRipper.Shared.Models;
|
||||||
|
using TuneRipper.Api.Services;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace TuneRipper.Api.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class VideoController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IVideoService _videoService;
|
||||||
|
private readonly ILogger<VideoController> _logger;
|
||||||
|
|
||||||
|
// In-memory storage for processing status (in production, use database or Redis)
|
||||||
|
private static readonly ConcurrentDictionary<string, ProcessingJob> _processingJobs = new();
|
||||||
|
|
||||||
|
private class ProcessingJob
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = string.Empty;
|
||||||
|
public ProcessVideoRequest Request { get; set; } = new();
|
||||||
|
public DownloadProgress Progress { get; set; } = new();
|
||||||
|
public ProcessVideoResponse? Result { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoController(IVideoService videoService, ILogger<VideoController> logger)
|
||||||
|
{
|
||||||
|
_videoService = videoService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("details")]
|
||||||
|
public async Task<ActionResult<YouTubeVideoDetails>> GetVideoDetails([FromQuery] string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
return BadRequest("URL is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_videoService.IsValidYouTubeUrl(url))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid YouTube URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Fetching video details for URL: {Url}", url);
|
||||||
|
|
||||||
|
var videoDetails = await _videoService.GetVideoDetailsAsync(url);
|
||||||
|
|
||||||
|
if (videoDetails == null)
|
||||||
|
{
|
||||||
|
return NotFound("Video not found or unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(videoDetails);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching video details for URL: {Url}", url);
|
||||||
|
return StatusCode(500, $"Error fetching video details: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("process")]
|
||||||
|
public async Task<ActionResult<ProcessVideoResponse>> StartProcessing([FromBody] ProcessVideoRequest request)
|
||||||
|
{
|
||||||
|
if (request == null || string.IsNullOrWhiteSpace(request.Url))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid request");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_videoService.IsValidYouTubeUrl(request.Url))
|
||||||
|
{
|
||||||
|
return BadRequest("Invalid YouTube URL");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Starting video processing for URL: {Url}", request.Url);
|
||||||
|
|
||||||
|
// Generate a job ID
|
||||||
|
var jobId = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
// Create processing job
|
||||||
|
var job = new ProcessingJob
|
||||||
|
{
|
||||||
|
Id = jobId,
|
||||||
|
Request = request,
|
||||||
|
Progress = new DownloadProgress
|
||||||
|
{
|
||||||
|
PercentageComplete = 0,
|
||||||
|
Status = "Queued",
|
||||||
|
Message = "Processing request queued"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_processingJobs[jobId] = job;
|
||||||
|
|
||||||
|
// Start processing in background (fire and forget)
|
||||||
|
_ = Task.Run(() => ProcessVideoAsync(jobId));
|
||||||
|
|
||||||
|
var response = new ProcessVideoResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Processing started",
|
||||||
|
FileName = jobId, // Use job ID as temporary filename
|
||||||
|
DownloadUrl = null, // Will be available when processing completes
|
||||||
|
VideoDetails = null // Will be populated during processing
|
||||||
|
};
|
||||||
|
|
||||||
|
return Accepted(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error starting video processing for URL: {Url}", request.Url);
|
||||||
|
return StatusCode(500, $"Error starting video processing: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("status/{jobId}")]
|
||||||
|
public ActionResult<DownloadProgress> GetProcessingStatus(string jobId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(jobId))
|
||||||
|
{
|
||||||
|
return BadRequest("Job ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_processingJobs.TryGetValue(jobId, out var job))
|
||||||
|
{
|
||||||
|
return NotFound("Job not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(job.Progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("result/{jobId}")]
|
||||||
|
public ActionResult<ProcessVideoResponse> GetProcessingResult(string jobId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(jobId))
|
||||||
|
{
|
||||||
|
return BadRequest("Job ID is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_processingJobs.TryGetValue(jobId, out var job))
|
||||||
|
{
|
||||||
|
return NotFound("Job not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.Result == null)
|
||||||
|
{
|
||||||
|
return Accepted("Processing not completed yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(job.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("jobs")]
|
||||||
|
public ActionResult<IEnumerable<JobStatusResponse>> GetAllJobs()
|
||||||
|
{
|
||||||
|
var jobs = _processingJobs.Values.Select(job => new JobStatusResponse
|
||||||
|
{
|
||||||
|
JobId = job.Id,
|
||||||
|
Status = job.Progress.Status,
|
||||||
|
PercentageComplete = job.Progress.PercentageComplete,
|
||||||
|
Message = job.Progress.Message,
|
||||||
|
CreatedAt = job.CreatedAt,
|
||||||
|
CompletedAt = job.Result != null ? job.CreatedAt.AddMinutes(5) : null, // Simulated completion time
|
||||||
|
Result = job.Result
|
||||||
|
}).OrderByDescending(j => j.CreatedAt);
|
||||||
|
|
||||||
|
return Ok(jobs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessVideoAsync(string jobId)
|
||||||
|
{
|
||||||
|
if (!_processingJobs.TryGetValue(jobId, out var job))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Processing video for job {JobId}", jobId);
|
||||||
|
|
||||||
|
// Simulate video processing steps
|
||||||
|
var steps = new[]
|
||||||
|
{
|
||||||
|
("Fetching video information", 10),
|
||||||
|
("Analyzing audio stream", 25),
|
||||||
|
("Downloading audio data", 60),
|
||||||
|
("Converting to MP3", 85),
|
||||||
|
("Finalizing", 100)
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var (message, percentage) in steps)
|
||||||
|
{
|
||||||
|
job.Progress.Status = "Processing";
|
||||||
|
job.Progress.Message = message;
|
||||||
|
job.Progress.PercentageComplete = percentage;
|
||||||
|
|
||||||
|
_logger.LogInformation("Job {JobId}: {Message} ({Percentage}%)", jobId, message, percentage);
|
||||||
|
|
||||||
|
// Simulate processing time
|
||||||
|
await Task.Delay(Random.Shared.Next(1000, 3000));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get video details using the service
|
||||||
|
var videoDetails = await _videoService.GetVideoDetailsAsync(job.Request.Url!);
|
||||||
|
|
||||||
|
// Complete processing
|
||||||
|
job.Progress.Status = "Completed";
|
||||||
|
job.Progress.Message = "Processing completed successfully";
|
||||||
|
job.Progress.PercentageComplete = 100;
|
||||||
|
|
||||||
|
job.Result = new ProcessVideoResponse
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Video processed successfully",
|
||||||
|
FileName = $"{videoDetails?.Title?.Replace(" ", "_") ?? "video"}.{job.Request.Format ?? "mp3"}",
|
||||||
|
DownloadUrl = $"/api/download/{jobId}",
|
||||||
|
VideoDetails = videoDetails
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully completed processing for job {JobId}", jobId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error processing video for job {JobId}", jobId);
|
||||||
|
|
||||||
|
job.Progress.Status = "Failed";
|
||||||
|
job.Progress.Message = $"Processing failed: {ex.Message}";
|
||||||
|
job.Progress.PercentageComplete = 0;
|
||||||
|
|
||||||
|
job.Result = new ProcessVideoResponse
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = ex.Message,
|
||||||
|
FileName = null,
|
||||||
|
DownloadUrl = null,
|
||||||
|
VideoDetails = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
|
using TuneRipper.Api.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// Register services
|
||||||
|
builder.Services.AddScoped<IVideoService, VideoService>();
|
||||||
|
|
||||||
|
// Add CORS for Blazor WebAssembly client
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("BlazorPolicy", policy =>
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
|
|
||||||
@@ -16,8 +32,50 @@ if (app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
// Enable CORS
|
||||||
|
app.UseCors("BlazorPolicy");
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
// Add root endpoint with startup info
|
||||||
|
app.MapGet("/", (IWebHostEnvironment env) => new
|
||||||
|
{
|
||||||
|
service = "TuneRipper API",
|
||||||
|
version = "1.0.0",
|
||||||
|
status = "running",
|
||||||
|
timestamp = DateTime.UtcNow,
|
||||||
|
environment = env.EnvironmentName,
|
||||||
|
urls = new
|
||||||
|
{
|
||||||
|
https = "https://localhost:7160",
|
||||||
|
http = "http://localhost:5112"
|
||||||
|
},
|
||||||
|
endpoints = new
|
||||||
|
{
|
||||||
|
health = "/api/health",
|
||||||
|
video_details = "/api/video/details?url={youtubeUrl}",
|
||||||
|
process_video = "/api/video/process",
|
||||||
|
check_status = "/api/video/status/{jobId}",
|
||||||
|
get_result = "/api/video/result/{jobId}",
|
||||||
|
download = "/api/download/{jobId}",
|
||||||
|
openapi = "/openapi/v1.json"
|
||||||
|
},
|
||||||
|
testing = new
|
||||||
|
{
|
||||||
|
httpFile = "Use api-tests.http to test endpoints",
|
||||||
|
startWith = "GET /api/health to verify API is running"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Log startup information
|
||||||
|
app.Logger.LogInformation("?? TuneRipper API is starting...");
|
||||||
|
app.Logger.LogInformation("?? Environment: {Environment}", app.Environment.EnvironmentName);
|
||||||
|
app.Logger.LogInformation("?? API will be available at:");
|
||||||
|
app.Logger.LogInformation(" • HTTPS: https://localhost:7160");
|
||||||
|
app.Logger.LogInformation(" • HTTP: http://localhost:5112");
|
||||||
|
app.Logger.LogInformation("?? Health check: /api/health");
|
||||||
|
app.Logger.LogInformation("?? API info: /");
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# TuneRipper API
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Starting the API
|
||||||
|
|
||||||
|
**Option 1: Using the start script (Windows)**
|
||||||
|
```cmd
|
||||||
|
cd TuneRipper.Api
|
||||||
|
start-api.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Using the start script (Linux/Mac)**
|
||||||
|
```bash
|
||||||
|
cd TuneRipper.Api
|
||||||
|
chmod +x start-api.sh
|
||||||
|
./start-api.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 3: Manual start**
|
||||||
|
```cmd
|
||||||
|
cd TuneRipper.Api
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
Once running, the API will be available at:
|
||||||
|
- **HTTPS**: https://localhost:7160
|
||||||
|
- **HTTP**: http://localhost:5112
|
||||||
|
|
||||||
|
### Testing the API
|
||||||
|
|
||||||
|
1. **Start the API** using one of the methods above
|
||||||
|
2. **Open the HTTP test file**: `TuneRipper.Api/api-tests.http`
|
||||||
|
3. **Run the connectivity tests first** to verify the API is responding
|
||||||
|
4. **Use the main API tests** once connectivity is confirmed
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
#### "This localhost page can't be found" errors:
|
||||||
|
|
||||||
|
1. **Check if API is running**:
|
||||||
|
- Look for console output showing "Now listening on: https://localhost:7160"
|
||||||
|
- If not running, start it using the methods above
|
||||||
|
|
||||||
|
2. **Try HTTP instead of HTTPS**:
|
||||||
|
- Use `http://localhost:5112` instead of `https://localhost:7160`
|
||||||
|
- Some development environments have HTTPS certificate issues
|
||||||
|
|
||||||
|
3. **Trust development certificates** (for HTTPS):
|
||||||
|
```cmd
|
||||||
|
dotnet dev-certs https --trust
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Check port availability**:
|
||||||
|
- Make sure ports 7160 and 5112 aren't used by other applications
|
||||||
|
- You can change ports in `Properties/launchSettings.json`
|
||||||
|
|
||||||
|
#### HTTP test file variables not working:
|
||||||
|
|
||||||
|
1. **Copy job IDs manually**:
|
||||||
|
- Run a POST request to start processing
|
||||||
|
- Copy the `fileName` value from the response
|
||||||
|
- Use it in the manual test sections
|
||||||
|
|
||||||
|
2. **Use the troubleshooting section**:
|
||||||
|
- Start with connectivity tests
|
||||||
|
- Use manual GUID replacements if variables fail
|
||||||
|
|
||||||
|
### API Documentation
|
||||||
|
|
||||||
|
When the API is running, you can access:
|
||||||
|
- **Root endpoint**: https://localhost:7160/ (lists available endpoints)
|
||||||
|
- **OpenAPI spec**: https://localhost:7160/openapi/v1.json
|
||||||
|
- **Health check**: https://localhost:7160/api/health
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
The API includes:
|
||||||
|
- Automatic reload on code changes (when using `dotnet run`)
|
||||||
|
- Detailed logging in the console
|
||||||
|
- CORS enabled for Blazor WebAssembly client
|
||||||
|
- OpenAPI/Swagger support for API documentation
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
using TuneRipper.Shared.Models;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace TuneRipper.Api.Services;
|
||||||
|
|
||||||
|
public interface IVideoService
|
||||||
|
{
|
||||||
|
Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url);
|
||||||
|
bool IsValidYouTubeUrl(string url);
|
||||||
|
string ExtractVideoId(string url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoService : IVideoService
|
||||||
|
{
|
||||||
|
private readonly ILogger<VideoService> _logger;
|
||||||
|
|
||||||
|
public VideoService(ILogger<VideoService> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<YouTubeVideoDetails?> GetVideoDetailsAsync(string url)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!IsValidYouTubeUrl(url))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Invalid YouTube URL: {Url}", url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate API call delay
|
||||||
|
await Task.Delay(Random.Shared.Next(500, 2000));
|
||||||
|
|
||||||
|
var videoId = ExtractVideoId(url);
|
||||||
|
|
||||||
|
// In production, use YouTube API or yt-dlp to get actual video details
|
||||||
|
var videoDetails = new YouTubeVideoDetails
|
||||||
|
{
|
||||||
|
Id = videoId,
|
||||||
|
Title = GenerateSampleTitle(videoId),
|
||||||
|
Description = $"Video description for {videoId}. In production, this would be fetched from YouTube API.",
|
||||||
|
ThumbnailUrl = $"https://img.youtube.com/vi/{videoId}/maxresdefault.jpg",
|
||||||
|
Duration = TimeSpan.FromMinutes(Random.Shared.Next(2, 10)).Add(TimeSpan.FromSeconds(Random.Shared.Next(0, 59))),
|
||||||
|
Author = $"Channel_{Random.Shared.Next(1, 1000)}",
|
||||||
|
ViewCount = Random.Shared.Next(100, 5000000),
|
||||||
|
UploadDate = DateTime.UtcNow.AddDays(-Random.Shared.Next(1, 1000)),
|
||||||
|
Tags = GenerateSampleTags()
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.LogInformation("Successfully fetched details for video {VideoId}", videoId);
|
||||||
|
return videoDetails;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error fetching video details for URL: {Url}", url);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidYouTubeUrl(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var patterns = new[]
|
||||||
|
{
|
||||||
|
@"^https?://(www\.)?youtube\.com/watch\?v=[\w-]+",
|
||||||
|
@"^https?://(www\.)?youtu\.be/[\w-]+",
|
||||||
|
@"^https?://(www\.)?youtube\.com/embed/[\w-]+",
|
||||||
|
@"^https?://(www\.)?youtube\.com/v/[\w-]+"
|
||||||
|
};
|
||||||
|
|
||||||
|
return patterns.Any(pattern => Regex.IsMatch(url, pattern, RegexOptions.IgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ExtractVideoId(string url)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Different YouTube URL formats
|
||||||
|
var patterns = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ @"youtu\.be/([^?&]+)", "$1" },
|
||||||
|
{ @"youtube\.com/watch\?v=([^&]+)", "$1" },
|
||||||
|
{ @"youtube\.com/embed/([^?&]+)", "$1" },
|
||||||
|
{ @"youtube\.com/v/([^?&]+)", "$1" }
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var pattern in patterns)
|
||||||
|
{
|
||||||
|
var match = Regex.Match(url, pattern.Key, RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error extracting video ID from URL: {Url}", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GenerateSampleTitle(string videoId)
|
||||||
|
{
|
||||||
|
var sampleTitles = new[]
|
||||||
|
{
|
||||||
|
"Amazing Music Video",
|
||||||
|
"Best Song Ever",
|
||||||
|
"Epic Guitar Solo",
|
||||||
|
"Relaxing Piano Music",
|
||||||
|
"Dance Hit 2024",
|
||||||
|
"Acoustic Session",
|
||||||
|
"Live Performance",
|
||||||
|
"Official Music Video"
|
||||||
|
};
|
||||||
|
|
||||||
|
var randomTitle = sampleTitles[Random.Shared.Next(sampleTitles.Length)];
|
||||||
|
return $"{randomTitle} - {videoId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> GenerateSampleTags()
|
||||||
|
{
|
||||||
|
var allTags = new[] { "music", "video", "song", "artist", "album", "live", "official", "acoustic", "cover", "remix" };
|
||||||
|
var tagCount = Random.Shared.Next(3, 7);
|
||||||
|
return allTags.OrderBy(x => Random.Shared.Next()).Take(tagCount).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
@@ -14,8 +14,4 @@
|
|||||||
<ProjectReference Include="..\TuneRipper.Shared\TuneRipper.Shared.csproj" />
|
<ProjectReference Include="..\TuneRipper.Shared\TuneRipper.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Controllers\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
namespace TuneRipper.Shared.Models;
|
||||||
|
|
||||||
|
public class ApiResponse<T>
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public T? Data { get; set; }
|
||||||
|
public List<string> Errors { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JobStatusResponse
|
||||||
|
{
|
||||||
|
public string? JobId { get; set; }
|
||||||
|
public string? Status { get; set; }
|
||||||
|
public int PercentageComplete { get; set; }
|
||||||
|
public string? Message { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public DateTime? CompletedAt { get; set; }
|
||||||
|
public ProcessVideoResponse? Result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VideoInfo
|
||||||
|
{
|
||||||
|
public string? Url { get; set; }
|
||||||
|
public string? VideoId { get; set; }
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user