using System.Diagnostics; using System.Threading.Tasks; using Db.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using TagVid.Helpers; using TagVid.Models; namespace TagVid.Controllers; public class HomeController : Controller { private readonly ILogger _logger; private readonly AdapterContext _adapterContext; public HomeController(ILogger logger, AdapterContext adapterContext) { _logger = logger; _adapterContext = adapterContext; } public async Task Index() { // check if the video path was set var pathSettings = await _adapterContext.GetSettingAsync(SettingName.VideoPath); if (string.IsNullOrEmpty(pathSettings)) { _logger.LogWarning("Video path is not set. Please configure the video path in settings."); return RedirectToAction("Settings", "Home"); } return View(); } [HttpGet] [Route("Home/Index/{id}")] public async Task Index(string id) { throw new NotImplementedException(); } public async Task Settings() { var vm = new SettingsViewModel { VideoPath = await _adapterContext.GetSettingAsync(SettingName.VideoPath) }; return View(vm); } [HttpPost] public async Task Settings(SettingsViewModel model) { if (!ModelState.IsValid) { return View(model); } // Validate the video path if (string.IsNullOrWhiteSpace(model.VideoPath)) { ModelState.AddModelError(nameof(model.VideoPath), "Video path cannot be empty."); return View(model); } // Check if the path exists if (!Directory.Exists(model.VideoPath)) { ModelState.AddModelError(nameof(model.VideoPath), "The specified video path does not exist."); return View(model); } // Check if the path is a directory if (!System.IO.File.GetAttributes(model.VideoPath).HasFlag(FileAttributes.Directory)) { ModelState.AddModelError(nameof(model.VideoPath), "The specified path must be a directory."); return View(model); } // Save settings to the database await _adapterContext.SetSettingAsync(SettingName.VideoPath, model.VideoPath); return View(model); } [HttpPost] [ValidateAntiForgeryToken] public async Task Upload(IFormFile videoFile) { _logger.LogInformation("Upload called with file: {FileName}", videoFile?.Name); if (videoFile == null || videoFile.Length == 0) { return RedirectToAction("Index"); } // Validate the video file // no size limit for now, but could be added later if (!videoFile.ContentType.StartsWith("video/")) { return RedirectToAction("Index"); } // Save the video file to the configured path var videoPath = await _adapterContext.GetSettingAsync(SettingName.VideoPath); var ext = Path.GetExtension(videoFile.FileName); if (string.IsNullOrEmpty(videoPath) || !Directory.Exists(videoPath)) { return RedirectToAction("Settings"); } // asp.net docu suggests not using .FileName due to security reasons var id = Guid.NewGuid().ToString(); var filePath = Path.Combine(videoPath, $"{id}{ext}"); using (var stream = new FileStream(filePath, FileMode.Create)) { await videoFile.CopyToAsync(stream); } var data = await new FFProbeHelper(_logger).GetVideoMetadataAsync(filePath); var video = new Video { Id = id, Duration = data?.Duration ?? TimeSpan.Zero, Width = data?.Width ?? 0, Height = data?.Height ?? 0, Extension = ext, VideoTags = [] }; _adapterContext.Videos.Add(video); await _adapterContext.SaveChangesAsync(); // redirect to the video details page return RedirectToAction("Details", "Home", new { id = video.Id }); } public async Task Details(string id) { if (string.IsNullOrEmpty(id)) { return NotFound(); } var video = await _adapterContext.Videos .Include(v => v.VideoTags) .ThenInclude(vt => vt.Tag) .FirstOrDefaultAsync(v => v.Id == id); if (video == null) { return NotFound(); } var vm = new DetailsViewModel { VideoId = id, Title = video?.Title ?? string.Empty, Tags = video?.VideoTags.Select(vt => vt.Tag.Name).ToList() ?? [] }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task Details(DetailsViewModel videoModel) { if (string.IsNullOrEmpty(videoModel.VideoId)) { return NotFound(); } var existingVideo = await _adapterContext.Videos .Include(v => v.VideoTags) .ThenInclude(vt => vt.Tag) .FirstOrDefaultAsync(v => v.Id == videoModel.VideoId); if (existingVideo == null) { return NotFound(); } // Update video title existingVideo.Title = videoModel.Title; // Update tags existingVideo.VideoTags.Clear(); foreach (var tagName in videoModel.Tags) { var tag = await _adapterContext.Tags.FirstOrDefaultAsync(t => t.Name == tagName); if (tag == null) { tag = new Tag { Name = tagName }; _adapterContext.Tags.Add(tag); } existingVideo.VideoTags.Add(new VideoTag { Video = existingVideo, Tag = tag }); } _adapterContext.Videos.Update(existingVideo); await _adapterContext.SaveChangesAsync(); return RedirectToAction("Details", new { id = videoModel.VideoId }); } [HttpPost] [ValidateAntiForgeryToken] public async Task Delete(string id) { if (string.IsNullOrEmpty(id)) { return NotFound(); } var video = await _adapterContext.Videos.FindAsync(id); if (video == null) { return NotFound(); } // Delete the video file from the file system var videoPath = await _adapterContext.GetSettingAsync(SettingName.VideoPath); if (!string.IsNullOrEmpty(videoPath)) { var filePath = Path.Combine(videoPath, $"{video.Id}{video.Extension}"); if (System.IO.File.Exists(filePath)) { System.IO.File.Delete(filePath); } } // Remove the video from the database _adapterContext.Videos.Remove(video); await _adapterContext.SaveChangesAsync(); return RedirectToAction("Index"); } [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } }