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
15 changes: 3 additions & 12 deletions listenarr.application/Audiobooks/AuthorMonitoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Listenarr.Application.Interfaces;
using Listenarr.Application.Interfaces.Repositories;
using Listenarr.Application.Metadata;
using Listenarr.Domain.Common;
using Listenarr.Domain.Models;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -234,7 +235,7 @@ private async Task<MonitorAuthorSyncResult> SyncAuthorInternalAsync(
{
result.Succeeded = false;
result.ErrorMessage = "Author catalog could not be loaded.";
monitoredAuthor.LastError = TruncateError(result.ErrorMessage);
monitoredAuthor.LastError = StringUtils.Truncate(result.ErrorMessage, 2048);
monitoredAuthor.LastCheckedAt = DateTime.UtcNow;
monitoredAuthor.UpdatedAt = DateTime.UtcNow;
await _authors.UpsertAsync(monitoredAuthor, cancellationToken);
Expand Down Expand Up @@ -318,7 +319,7 @@ private async Task<MonitorAuthorSyncResult> SyncAuthorInternalAsync(
result.ErrorMessage = ex.Message;
result.FailedCount++;
monitoredAuthor.LastCheckedAt = DateTime.UtcNow;
monitoredAuthor.LastError = TruncateError(ex.Message);
monitoredAuthor.LastError = StringUtils.Truncate(ex.Message, 2048);
monitoredAuthor.UpdatedAt = DateTime.UtcNow;
await _authors.UpsertAsync(monitoredAuthor, cancellationToken);
return result;
Expand Down Expand Up @@ -514,15 +515,5 @@ private static string BuildTitleAuthorKey(string? title, IEnumerable<string>? au
var match = System.Text.RegularExpressions.Regex.Match(value, "\\d{4}");
return match.Success ? match.Value : null;
}

private static string? TruncateError(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}

return value.Length <= 2048 ? value : value[..2048];
}
}
}
15 changes: 3 additions & 12 deletions listenarr.application/Audiobooks/SeriesMonitoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Listenarr.Application.Interfaces;
using Listenarr.Application.Interfaces.Repositories;
using Listenarr.Application.Metadata;
using Listenarr.Domain.Common;
using Listenarr.Domain.Models;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -233,7 +234,7 @@ private async Task<MonitorSeriesSyncResult> SyncSeriesInternalAsync(
{
result.Succeeded = false;
result.ErrorMessage = "Series catalog could not be loaded.";
monitoredSeries.LastError = TruncateError(result.ErrorMessage);
monitoredSeries.LastError = StringUtils.Truncate(result.ErrorMessage, 2048);
monitoredSeries.LastCheckedAt = DateTime.UtcNow;
monitoredSeries.UpdatedAt = DateTime.UtcNow;
await _series.UpsertAsync(monitoredSeries, cancellationToken);
Expand Down Expand Up @@ -317,7 +318,7 @@ private async Task<MonitorSeriesSyncResult> SyncSeriesInternalAsync(
result.ErrorMessage = ex.Message;
result.FailedCount++;
monitoredSeries.LastCheckedAt = DateTime.UtcNow;
monitoredSeries.LastError = TruncateError(ex.Message);
monitoredSeries.LastError = StringUtils.Truncate(ex.Message, 2048);
monitoredSeries.UpdatedAt = DateTime.UtcNow;
await _series.UpsertAsync(monitoredSeries, cancellationToken);
return result;
Expand Down Expand Up @@ -513,15 +514,5 @@ private static string BuildTitleAuthorKey(string? title, IEnumerable<string>? au
var match = System.Text.RegularExpressions.Regex.Match(value, "\\d{4}");
return match.Success ? match.Value : null;
}

private static string? TruncateError(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return value;
}

return value.Length <= 2048 ? value : value[..2048];
}
}
}
4 changes: 2 additions & 2 deletions listenarr.application/Common/FileNamingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ private static HashSet<char> BuildPortableInvalidFileNameChars()
/// <summary>
/// Ensure the generated path does not exceed platform limits.
/// On Windows: total path ≤ 259 chars, each component ≤ 255 chars.
/// Truncates the longest non-root components first while preserving the file extension.
/// StringUtils.Truncates the longest non-root components first while preserving the file extension.
/// </summary>
public string EnsurePathWithinLimits(string fullPath)
{
Expand Down Expand Up @@ -553,7 +553,7 @@ public string EnsurePathWithinLimits(string fullPath)

if (result != originalPath)
{
_logger.LogWarning("Path truncated to fit Windows MAX_PATH limit ({Limit} chars). Original length: {OriginalLength}, New length: {NewLength}. Truncated path: {Path}",
_logger.LogWarning("Path truncated to fit Windows MAX_PATH limit ({Limit} chars). Original length: {OriginalLength}, New length: {NewLength}. StringUtils.Truncated path: {Path}",
WindowsMaxPath, originalPath.Length, result.Length, result);
}

Expand Down
2 changes: 0 additions & 2 deletions listenarr.application/Interfaces/IFileMover.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public interface IFileMover
{
Task<bool> MoveDirectoryAsync(string source, string destination);

Task<bool> CopyDirectoryAsync(string source, string destination);

/// <summary>
/// Perform the given action on the given file
/// </summary>
Expand Down
67 changes: 27 additions & 40 deletions listenarr.application/Notification/NotificationPayloadBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using Listenarr.Application.Common;
using Listenarr.Domain.Common;
using Microsoft.AspNetCore.Http;

namespace Listenarr.Application.Notification
Expand Down Expand Up @@ -95,16 +96,8 @@ public static JsonNode CreateDiscordPayload(string trigger, object data, string?
narrators = DecodeHtml(narrators);
description = DecodeHtml(description);

// Use centralized constants declared at class scope

static string Truncate(string? value, int max)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
return value.Length <= max ? value : value.Substring(0, max);
}

var embed = new JsonObject();
if (!string.IsNullOrWhiteSpace(title)) embed["title"] = Truncate(title, MAX_TITLE);
if (!string.IsNullOrWhiteSpace(title)) embed["title"] = StringUtils.Truncate(title, MAX_TITLE);

string? absoluteImageUrl = null;
string? thumbnailUrl = null;
Expand All @@ -131,7 +124,7 @@ static string Truncate(string? value, int max)
}
else if (!string.IsNullOrWhiteSpace(absoluteImageUrl))
{
embed["thumbnail"] = new JsonObject { ["url"] = Truncate(absoluteImageUrl, 2000) };
embed["thumbnail"] = new JsonObject { ["url"] = StringUtils.Truncate(absoluteImageUrl, 2000) };
}

var embeds = new JsonArray();
Expand All @@ -140,42 +133,42 @@ static string Truncate(string? value, int max)
if (!string.IsNullOrWhiteSpace(author))
{
var fa = new JsonObject();
fa["name"] = Truncate("Author", MAX_FIELD_NAME);
fa["value"] = Truncate(author, MAX_FIELD_VALUE);
fa["name"] = StringUtils.Truncate("Author", MAX_FIELD_NAME);
fa["value"] = StringUtils.Truncate(author, MAX_FIELD_VALUE);
fa["inline"] = false;
fields.Add(fa);
}

if (!string.IsNullOrWhiteSpace(publisher))
{
var f = new JsonObject();
f["name"] = Truncate("Publisher", MAX_FIELD_NAME);
f["value"] = Truncate(publisher, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Publisher", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(publisher, MAX_FIELD_VALUE);
f["inline"] = true;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(year))
{
var f = new JsonObject();
f["name"] = Truncate("Year", MAX_FIELD_NAME);
f["value"] = Truncate(year, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Year", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(year, MAX_FIELD_VALUE);
f["inline"] = true;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(narrators))
{
var f = new JsonObject();
f["name"] = Truncate("Narrated by", MAX_FIELD_NAME);
f["value"] = Truncate(narrators, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Narrated by", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(narrators, MAX_FIELD_VALUE);
f["inline"] = false;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(description))
{
var cleanedDescription = CleanHtml(description);
var truncatedDesc = Truncate(cleanedDescription, Math.Min(MAX_FIELD_VALUE, 500));
var truncatedDesc = StringUtils.Truncate(cleanedDescription, Math.Min(MAX_FIELD_VALUE, 500));
var f = new JsonObject();
f["name"] = Truncate("Description", MAX_FIELD_NAME);
f["name"] = StringUtils.Truncate("Description", MAX_FIELD_NAME);
f["value"] = truncatedDesc;
f["inline"] = false;
fields.Add(f);
Expand Down Expand Up @@ -226,7 +219,7 @@ static string Truncate(string? value, int max)
{
int reduce = Math.Min(excess, descriptionText.Length);
descriptionText = descriptionText.Substring(0, Math.Max(0, descriptionText.Length - reduce));
e["description"] = Truncate(descriptionText, MAX_DESCRIPTION);
e["description"] = StringUtils.Truncate(descriptionText, MAX_DESCRIPTION);
excess = excess - reduce;
}

Expand All @@ -241,7 +234,7 @@ static string Truncate(string? value, int max)
{
int reduce = Math.Min(excess, v.Length);
var newVal = v.Substring(0, Math.Max(0, v.Length - reduce));
fo["value"] = Truncate(newVal, MAX_FIELD_VALUE);
fo["value"] = StringUtils.Truncate(newVal, MAX_FIELD_VALUE);
excess -= reduce;
}
}
Expand Down Expand Up @@ -312,14 +305,8 @@ static string Truncate(string? value, int max)

// Use centralized constants declared at class scope

static string Truncate(string? value, int max)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
return value.Length <= max ? value : value.Substring(0, max);
}

var embed = new JsonObject();
if (!string.IsNullOrWhiteSpace(title)) embed["title"] = Truncate(title, MAX_TITLE);
if (!string.IsNullOrWhiteSpace(title)) embed["title"] = StringUtils.Truncate(title, MAX_TITLE);

string? absoluteImageUrl = null;
string? thumbnailUrl = null;
Expand Down Expand Up @@ -404,7 +391,7 @@ static string Truncate(string? value, int max)
}
else if (!string.IsNullOrWhiteSpace(absoluteImageUrl))
{
embed["thumbnail"] = new JsonObject { ["url"] = Truncate(absoluteImageUrl, 2000) };
embed["thumbnail"] = new JsonObject { ["url"] = StringUtils.Truncate(absoluteImageUrl, 2000) };
thumbnailSet = true;
}
else if (!string.IsNullOrWhiteSpace(thumbnailUrl))
Expand All @@ -424,42 +411,42 @@ static string Truncate(string? value, int max)
if (!string.IsNullOrWhiteSpace(author))
{
var fa = new JsonObject();
fa["name"] = Truncate("Author", MAX_FIELD_NAME);
fa["value"] = Truncate(author, MAX_FIELD_VALUE);
fa["name"] = StringUtils.Truncate("Author", MAX_FIELD_NAME);
fa["value"] = StringUtils.Truncate(author, MAX_FIELD_VALUE);
fa["inline"] = false;
fields.Add(fa);
}

if (!string.IsNullOrWhiteSpace(publisher))
{
var f = new JsonObject();
f["name"] = Truncate("Publisher", MAX_FIELD_NAME);
f["value"] = Truncate(publisher, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Publisher", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(publisher, MAX_FIELD_VALUE);
f["inline"] = true;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(year))
{
var f = new JsonObject();
f["name"] = Truncate("Year", MAX_FIELD_NAME);
f["value"] = Truncate(year, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Year", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(year, MAX_FIELD_VALUE);
f["inline"] = true;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(narrators))
{
var f = new JsonObject();
f["name"] = Truncate("Narrated by", MAX_FIELD_NAME);
f["value"] = Truncate(narrators, MAX_FIELD_VALUE);
f["name"] = StringUtils.Truncate("Narrated by", MAX_FIELD_NAME);
f["value"] = StringUtils.Truncate(narrators, MAX_FIELD_VALUE);
f["inline"] = false;
fields.Add(f);
}
if (!string.IsNullOrWhiteSpace(description))
{
var cleanedDescription = CleanHtml(description);
var truncatedDesc = Truncate(cleanedDescription, Math.Min(MAX_FIELD_VALUE, 500));
var truncatedDesc = StringUtils.Truncate(cleanedDescription, Math.Min(MAX_FIELD_VALUE, 500));
var f = new JsonObject();
f["name"] = Truncate("Description", MAX_FIELD_NAME);
f["name"] = StringUtils.Truncate("Description", MAX_FIELD_NAME);
f["value"] = truncatedDesc;
f["inline"] = false;
fields.Add(f);
Expand Down
7 changes: 7 additions & 0 deletions listenarr.domain/Common/StringUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,12 @@ public static int LevenshteinDistance(string s, string t)
}
return d[n, m];
}

public static string Truncate(string? s, int max)
{
if (string.IsNullOrEmpty(s)) return string.Empty;
if (s.Length <= max) return s;
return s.Substring(0, max - 3) + "...";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Listenarr.Infrastructure.Extensions
{
Expand Down Expand Up @@ -107,6 +108,8 @@ public static IServiceCollection AddListenarrAppServices(this IServiceCollection
services.AddScoped<IArchiveExtractor, ArchiveExtractor>();
// Bind FileMover options from configuration (optional)
services.Configure<FileMoverOptions>(config.GetSection("FileMover"));
services.AddSingleton(resolver =>
resolver.GetRequiredService<IOptions<FileMoverOptions>>().Value);
// Gateway that wraps adapters for higher-level orchestration
services.AddScoped<IDownloadClientGateway, DownloadClientGateway>();
// Process runner for external process execution (robocopy, ffprobe, playwright installer)
Expand Down
Loading
Loading