Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Hubs/DiscChangerHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ namespace DiscChanger.Hubs
public class DiscChangerHub : Hub
{
public DiscChangerService discChangerService;
public CustomDiscService customDiscService;
private readonly Microsoft.Extensions.Logging.ILogger<DiscChangerHub> _logger;

public DiscChangerHub(DiscChangerService discChangerService, ILogger<DiscChangerHub> logger)
public DiscChangerHub(DiscChangerService discChangerService, CustomDiscService customDiscService, ILogger<DiscChangerHub> logger)
{
_logger = logger;
this.discChangerService = discChangerService;
this.customDiscService = customDiscService;
}

public void Control(string changerKey, string command)
Expand Down Expand Up @@ -98,7 +100,15 @@ public async Task DeleteDiscs(string changerKey, string discSet)
{
try
{
// Delete from regular changer
await discChangerService.Changer(changerKey).DeleteDiscs(discSet);

// Also delete from custom discs
var slots = Models.DiscChanger.ParseSet(discSet);
foreach (var slot in slots)
{
await customDiscService.DeleteCustomDisc(changerKey, slot.ToString());
}
}
catch (Exception e)
{
Expand Down
133 changes: 133 additions & 0 deletions Models/CustomDisc.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;

namespace DiscChanger.Models
{
/// <summary>
/// Represents a custom disc entry stored separately from scanned discs
/// </summary>
public class CustomDisc
{
[JsonPropertyName("ChangerKey")]
public string ChangerKey { get; set; }

[JsonPropertyName("Slot")]
public string Slot { get; set; }

[JsonPropertyName("DiscType")]
public string DiscType { get; set; }

[JsonPropertyName("Artist")]
public string Artist { get; set; }

[JsonPropertyName("Title")]
public string Title { get; set; }

[JsonPropertyName("CoverArtFileName")]
public string CoverArtFileName { get; set; }

[JsonPropertyName("Tracks")]
public List<TrackInfo> Tracks { get; set; }

[JsonPropertyName("DateTimeAdded")]
public DateTime? DateTimeAdded { get; set; }

public CustomDisc()
{
Tracks = new List<TrackInfo>();
}

public string ToHtml(string changerName)
{
string HtmlEncode(string s) => System.Web.HttpUtility.HtmlEncode(s);
string HtmlAttributeEncode(string s) => System.Web.HttpUtility.HtmlAttributeEncode(s);

var afp = !string.IsNullOrEmpty(CoverArtFileName) ? "/CustomCoverArt/" + System.Web.HttpUtility.UrlEncode(CoverArtFileName) : null;

var slotHtml = HtmlEncode(Slot ?? "--");
var a = HtmlEncode(Artist ?? String.Empty);
var t = HtmlEncode(Title ?? String.Empty);

StringBuilder sb = new StringBuilder(@"<div class=""disc"" data-bs-toggle=""popover"" data-bs-html=""true"" data-bs-title=""", 8192);
sb.Append(HtmlAttributeEncode(a + "/" + t + @"<button class=""btn-close"">"));
sb.Append(@""" data-bs-content=""");
StringBuilder content = new StringBuilder(8192);

if (Tracks != null && Tracks.Count > 0)
{
content.Append(@"<table class=""tracks"">");
foreach (var track in Tracks)
{
content.Append(@"<tr><td>");
content.Append(track.Position);
content.Append(@"</td><td>"); content.Append(HtmlEncode(track.Title ?? "---")); content.Append("</td><td>");
content.Append(track.Length ?? "--");
content.Append(@"</td></tr>");
}
content.Append(@"</table>");
}

// Add Go and Edit buttons to content (before encoding) - use single quotes to avoid encoding issues
content.Append(@"<div class='disc-actions'>");
content.Append(@"<button class='btn btn-primary btn-sm disc-go-btn' data-changer='");
content.Append(ChangerKey); content.Append(@"' data-slot='");
content.Append(slotHtml); content.Append(@"'>Go</button>");
content.Append(@"<a href='/AddCustomDisc?changerKey=");
content.Append(ChangerKey); content.Append(@"&slot=");
content.Append(slotHtml); content.Append(@"' class='btn btn-secondary btn-sm'>Edit</a>");
content.Append(@"</div>");

sb.Append(HtmlAttributeEncode(content.ToString()));
sb.Append(@""" data-changer=""");
sb.Append(ChangerKey); sb.Append(@""" data-slot="""); sb.Append(slotHtml);
sb.Append(@""" data-media-type=""");
sb.Append((DiscType != null && DiscType.ToUpper().Contains("CD")) ? "CD" : ((DiscType != null && (DiscType.ToUpper().Contains("DVD") || DiscType.ToUpper().Contains("BD") || DiscType.ToUpper().Contains("BLU"))) ? "MOVIE" : "OTHER"));
sb.Append(@""">");

if (afp != null)
{
sb.Append("<img src=\""); sb.Append(afp); sb.Append(@"""/>");
}
sb.Append(@"<div class=""artist"">");
sb.Append(a);
sb.Append(@"</div><div class=""title"">");
sb.Append(t);
sb.Append(@"</div><div class=""disc-header""><span class=""slot"">");
sb.Append(HtmlEncode(changerName)); sb.Append(':'); sb.Append(slotHtml);
sb.Append(@"</span><span class=""disc-type"">");
sb.Append(HtmlEncode(DiscType ?? "-"));
sb.Append("</span></div>");
sb.Append(@"</div>");
return sb.ToString();
}
}

public class TrackInfo
{
[JsonPropertyName("Position")]
public int Position { get; set; }

[JsonPropertyName("Title")]
public string Title { get; set; }

[JsonPropertyName("Length")]
public string Length { get; set; }
}

/// <summary>
/// Container for all custom discs
/// </summary>
public class CustomDiscCollection
{
[JsonPropertyName("CustomDiscs")]
public List<CustomDisc> CustomDiscs { get; set; }

public CustomDiscCollection()
{
CustomDiscs = new List<CustomDisc>();
}
}
}

128 changes: 128 additions & 0 deletions Models/CustomDiscService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Hosting;

namespace DiscChanger.Models
{
/// <summary>
/// Service for managing custom discs stored separately from scanned discs
/// </summary>
public class CustomDiscService
{
private readonly ILogger<CustomDiscService> _logger;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly string _customDiscsFilePath;
private CustomDiscCollection _customDiscs;
private readonly object _lockObject = new object();

public CustomDiscService(ILogger<CustomDiscService> logger, IWebHostEnvironment webHostEnvironment)
{
_logger = logger;
_webHostEnvironment = webHostEnvironment;
_customDiscsFilePath = Path.Combine(_webHostEnvironment.WebRootPath, "CustomDiscs.json");
LoadCustomDiscs();
}

private void LoadCustomDiscs()
{
try
{
if (File.Exists(_customDiscsFilePath))
{
string json = File.ReadAllText(_customDiscsFilePath);
_customDiscs = JsonSerializer.Deserialize<CustomDiscCollection>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
_logger.LogInformation($"Loaded {_customDiscs.CustomDiscs.Count} custom discs from {_customDiscsFilePath}");
}
else
{
_customDiscs = new CustomDiscCollection();
_logger.LogInformation("No custom discs file found, starting with empty collection");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error loading custom discs");
_customDiscs = new CustomDiscCollection();
}
}

public async Task SaveCustomDiscs()
{
try
{
lock (_lockObject)
{
string json = JsonSerializer.Serialize(_customDiscs, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(_customDiscsFilePath, json);
_logger.LogInformation($"Saved {_customDiscs.CustomDiscs.Count} custom discs to {_customDiscsFilePath}");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error saving custom discs");
}
}

public CustomDisc GetCustomDisc(string changerKey, string slot)
{
return _customDiscs.CustomDiscs.FirstOrDefault(d => d.ChangerKey == changerKey && d.Slot == slot);
}

public List<CustomDisc> GetCustomDiscsForChanger(string changerKey)
{
return _customDiscs.CustomDiscs.Where(d => d.ChangerKey == changerKey).ToList();
}

public async Task AddOrUpdateCustomDisc(CustomDisc customDisc)
{
lock (_lockObject)
{
// Remove existing custom disc for this slot if it exists
_customDiscs.CustomDiscs.RemoveAll(d => d.ChangerKey == customDisc.ChangerKey && d.Slot == customDisc.Slot);

// Add the new/updated custom disc
_customDiscs.CustomDiscs.Add(customDisc);
}

await SaveCustomDiscs();
}

public async Task RemoveCustomDisc(string changerKey, string slot)
{
lock (_lockObject)
{
_customDiscs.CustomDiscs.RemoveAll(d => d.ChangerKey == changerKey && d.Slot == slot);
}

await SaveCustomDiscs();
}

public async Task DeleteCustomDisc(string changerKey, string slot)
{
await RemoveCustomDisc(changerKey, slot);
}

public Dictionary<string, CustomDisc> GetAllCustomDiscs()
{
var result = new Dictionary<string, CustomDisc>();
foreach (var disc in _customDiscs.CustomDiscs)
{
string key = $"{disc.ChangerKey}_{disc.Slot}";
result[key] = disc;
}
return result;
}
}
}

51 changes: 33 additions & 18 deletions Models/Disc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,25 +162,36 @@ public virtual string ToHtml()
}
content.Append(@"</table>");
}
if (DataMusicBrainz != null)
{
content.Append(@"<div class=""urls"">");
var urls = DataMusicBrainz?.URLs;
if (urls != null)
foreach (var url in urls)
{
content.Append(@"<a href = """); content.Append(url); content.Append(@""" target = ""_blank""></a>");
}
content.Append(@"<a class=""diag"" href = """); content.Append(DataMusicBrainz.diagURL()); content.Append(@""" target = ""_blank""></a>");
content.Append(@"</div>");
}
// URLs section hidden per user request
// if (DataMusicBrainz != null)
// {
// content.Append(@"<div class=""urls"">");
// var urls = DataMusicBrainz?.URLs;
// if (urls != null)
// foreach (var url in urls)
// {
// content.Append(@"<a href = """); content.Append(url); content.Append(@""" target = ""_blank""></a>");
// }
// content.Append(@"<a class=""diag"" href = """); content.Append(DataMusicBrainz.diagURL()); content.Append(@""" target = ""_blank""></a>");
// content.Append(@"</div>");
// }

// Add Go and Edit buttons to content (before encoding) - use single quotes to avoid encoding issues
content.Append(@"<div class='disc-actions'>");
content.Append(@"<button class='btn btn-primary btn-sm disc-go-btn' data-changer='");
content.Append(DiscChanger.Key); content.Append(@"' data-slot='");
content.Append(slotHtml); content.Append(@"'>Go</button>");
content.Append(@"<a href='/AddCustomDisc?changerKey=");
content.Append(DiscChanger.Key); content.Append(@"&slot=");
content.Append(slotHtml); content.Append(@"' class='btn btn-secondary btn-sm'>Edit</a>");
content.Append(@"</div>");

sb.Append(HtmlAttributeEncode(content.ToString()));
sb.Append(@""" data-changer=""");
sb.Append(DiscChanger.Key); sb.Append(@""" data-slot= """); sb.Append(slotHtml); sb.Append(@"""><div class=""disc-header""><span class=""slot"">");
sb.Append(HtmlEncode(DiscChanger.Name)); sb.Append(':'); sb.Append(slotHtml);
sb.Append(@"</span><span class=""disc-type"">");
sb.Append(HtmlEncode(GetDiscType() ?? "-"));
sb.Append("</span></div>");
sb.Append(DiscChanger.Key); sb.Append(@""" data-slot="""); sb.Append(slotHtml);
sb.Append(@""" data-media-type=""");
sb.Append(IsCD() ? "CD" : (IsDVD() || IsBD()) ? "MOVIE" : "OTHER");
sb.Append(@""">");

if (afp != null)
{
Expand All @@ -190,7 +201,11 @@ public virtual string ToHtml()
sb.Append(a);
sb.Append(@"</div><div class=""title"">");
sb.Append(t);
sb.Append(@"</div>");
sb.Append(@"</div><div class=""disc-header""><span class=""slot"">");
sb.Append(HtmlEncode(DiscChanger.Name)); sb.Append(':'); sb.Append(slotHtml);
sb.Append(@"</span><span class=""disc-type"">");
sb.Append(HtmlEncode(GetDiscType() ?? "-"));
sb.Append("</span></div>");
sb.Append(@"</div>");
return sb.ToString();
}
Expand Down
Loading