using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using MediaBrowser.Common.Configuration; namespace Jellyfin.Plugin.MediaBarEnhanced.Api { /// /// Controller for handling custom overlay image uploads and retrieval. /// [ApiController] [Route("MediaBarEnhanced")] public class OverlayImageController : ControllerBase { private readonly IApplicationPaths _applicationPaths; private readonly string _imageDirectory; public OverlayImageController(IApplicationPaths applicationPaths) { _applicationPaths = applicationPaths; // We use the plugin's data folder to store the image _imageDirectory = MediaBarEnhancedPlugin.Instance?.DataFolderPath ?? Path.Combine(applicationPaths.DataPath, "plugins", "MediaBarEnhanced"); // We no longer define the exact path here, just the directory // The filename is determined per request } /// /// Uploads a new custom overlay image. /// [HttpPost("OverlayImage")] [Consumes("multipart/form-data")] public async Task UploadImage([FromForm] IFormFile file, [FromQuery] string? filename = null) { if (file == null || file.Length == 0) { return BadRequest("No file uploaded."); } string targetFileName = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image.dat" : $"custom_overlay_image_{filename}.dat"; string targetPath = Path.Combine(_imageDirectory, targetFileName); try { if (!Directory.Exists(_imageDirectory)) { Directory.CreateDirectory(_imageDirectory); } // Delete is not strictly necessary and can cause locking issues if someone is currently reading it. // FileMode.Create will truncate the file if it exists, effectively overwriting it. // We use FileShare.None to ensure we have exclusive write access, but handle potential IOExceptions gracefully. using (var stream = new FileStream(targetPath, FileMode.Create, FileAccess.Write, FileShare.None)) { await file.CopyToAsync(stream).ConfigureAwait(false); } // Return the GET URL that the frontend can use var qs = string.IsNullOrWhiteSpace(filename) ? "" : $"?filename={Uri.EscapeDataString(filename)}&"; var getUrl = $"/MediaBarEnhanced/OverlayImage{qs}{(qs == "" ? "?" : "")}t={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; return Ok(new { url = getUrl }); } catch (Exception ex) { return StatusCode(500, $"Internal server error: {ex.Message}"); } } /// /// Retrieves the custom overlay image. /// [HttpGet("OverlayImage")] public IActionResult GetImage([FromQuery] string? filename = null) { string targetFileName = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image.dat" : $"custom_overlay_image_{filename}.dat"; string targetPath = Path.Combine(_imageDirectory, targetFileName); if (!System.IO.File.Exists(targetPath)) { return NotFound(); } // Read the file and return as a generic octet stream. // We use FileShare.ReadWrite so that if someone is currently overwriting the file (uploading), we don't block them, // and we also don't get blocked by other readers. var stream = new FileStream(targetPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // "image/*" works reliably as browsers will sniff the exact image mime type (jpeg, png, webp). return File(stream, "image/*"); } /// /// Deletes a custom overlay image. /// [HttpDelete("OverlayImage")] public IActionResult DeleteImage([FromQuery] string? filename = null) { string targetFileName = string.IsNullOrWhiteSpace(filename) ? "custom_overlay_image.dat" : $"custom_overlay_image_{filename}.dat"; string targetPath = Path.Combine(_imageDirectory, targetFileName); if (System.IO.File.Exists(targetPath)) { try { System.IO.File.Delete(targetPath); return Ok(); } catch (Exception ex) { return StatusCode(500, $"Error deleting file: {ex.Message}"); } } return NotFound(); } /// /// Renames a custom overlay image (used when a seasonal section is renamed). /// [HttpPut("OverlayImage/Rename")] public IActionResult RenameImage([FromQuery] string oldName, [FromQuery] string newName) { if (string.IsNullOrWhiteSpace(oldName) || string.IsNullOrWhiteSpace(newName)) { return BadRequest("Both oldName and newName must be provided."); } string oldPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{oldName}.dat"); string newPath = Path.Combine(_imageDirectory, $"custom_overlay_image_{newName}.dat"); if (!System.IO.File.Exists(oldPath)) { // If it doesn't exist, there is nothing to rename, but we still consider it a success // since the end state (file with oldName is gone, file with newName doesn't exist yet) is acceptable. return Ok(); } try { // If a file with the new name already exists, delete it first to avoid conflicts if (System.IO.File.Exists(newPath)) { System.IO.File.Delete(newPath); } System.IO.File.Move(oldPath, newPath); var qs = $"?filename={Uri.EscapeDataString(newName)}&"; var getUrl = $"/MediaBarEnhanced/OverlayImage{qs}t={DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; return Ok(new { url = getUrl }); } catch (Exception ex) { return StatusCode(500, $"Error renaming file: {ex.Message}"); } } } }