Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
73215e5
fix(lychee): use valid TOML strings for regex patterns; prevent parse…
RicherTunes Sep 11, 2025
59a22d5
fix(lychee): convert all exclude patterns to literal strings to satis…
RicherTunes Sep 11, 2025
b135bd1
fix(lychee): convert 172.16/12 private CIDR pattern to literal string
RicherTunes Sep 11, 2025
2d4e14d
fix(styles): read ETag via header enumeration (HttpResponse.Headers h…
alexricher Sep 12, 2025
fc9516c
chore(format): apply dotnet format and deflake stress timing for CI
Sep 13, 2025
c13c93e
test: add UrlValidator and ModelIdMappingValidator coverage
Sep 13, 2025
b654b48
test: add Policy regex coverage (suspicious, fences, whitespace)
Sep 13, 2025
6fe9ff9
chore(lychee): normalize EOL and ensure trailing newline
RicherTunes Sep 11, 2025
85369d1
ci(lychee): scope link check to README + docs for stability; exclude …
RicherTunes Sep 11, 2025
e0e59e6
docs(release): add notes for 1.2.3
alexricher Sep 11, 2025
ecf6037
ci/docs: make lychee non-blocking; fix release-notes lint; align docs…
RicherTunes Sep 11, 2025
cb22019
docs: update comprehensive styles plan and nightly config
alexricher Sep 12, 2025
bec3277
docs: 1.2.3 docs pass — Perplexity + LM Studio tested; add Perplexity…
Sep 13, 2025
f061d3e
docs(tasks): add provider testing checklist; mark LM Studio, Gemini, …
Sep 13, 2025
033b4d0
fix(core/cache): report size via dictionary count; avoid drift under …
Sep 14, 2025
eed0194
test(cache): cover async capacity eviction and Remove(false) to stabi…
Sep 14, 2025
4cb96ce
test(cache): add overwrite and clear tests to raise coverage
Sep 14, 2025
b4770f7
test(cache): fix namespace/class braces for additional tests
Sep 14, 2025
3c3f959
test(cache): fix file structure; add missing [Fact] attributes; keep …
Sep 14, 2025
30490f5
test(correlation): cover LoggerExtensions and CorrelationScope to rai…
Sep 14, 2025
d54e550
test(correlation,url): add CorrelationContext + UrlSanitizer edge-cas…
Sep 14, 2025
2f93fc5
test(sanitizer): exercise injection/XSS/path removal and filtering to…
Sep 14, 2025
64e2034
ci: trigger pull_request checks (no-op)
Sep 14, 2025
3319537
test(hallucination): add detector edge-case tests to raise coverage
Sep 14, 2025
dfdb84e
test(sanitizer): fix FA method name (BeLessThanOrEqualTo)
Sep 14, 2025
bddc305
test(sanitizer): fix FA method name (BeLessThanOrEqualTo)
Sep 14, 2025
b4be14d
test(schema): add validator test covering nulls, missing artist, clam…
Sep 14, 2025
c7219fc
test(url): relax SanitizeApiUrl assertion to robust contains/startWit…
Sep 14, 2025
027eb39
ci: refresh PR checks (pull_request)
Sep 14, 2025
80be6d7
merge: resolve conflicts with main (sync UrlValidatorTests merged; ke…
Sep 15, 2025
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
18 changes: 17 additions & 1 deletion .github/workflows/nightly-perf-stress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,29 @@ jobs:

- name: Run Performance/Stress tests
run: |
set -euo pipefail
mkdir -p TestResults

# Run only Performance/Stress tests. If none are discovered, treat as success
# until dedicated perf/stress tests are landed.
set +e
dotnet test Brainarr.sln --no-build --configuration Release \
--verbosity normal \
--settings Brainarr.Tests/test.runsettings \
--filter "(Category=Performance|TestCategory=Performance|Category=Stress|TestCategory=Stress)" \
--logger "trx;LogFileName=test-results.trx" \
--results-directory TestResults/
--results-directory TestResults/ 2>&1 | tee TestResults/perf_stress.log
exit_code=${PIPESTATUS[0]}
set -e

# Allow the job to pass if no tests matched the filter.
if [ "$exit_code" -ne 0 ]; then
if grep -qi "No test is available" TestResults/perf_stress.log; then
echo "No Performance/Stress tests found; skipping with success."
exit 0
fi
exit "$exit_code"
fi

- name: Upload results
if: always()
Expand Down
20 changes: 20 additions & 0 deletions Brainarr.Plugin/BrainarrSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,26 @@ public bool EnableAutoDetection
HelpText = "Temporary per-model concurrency cap for cloud providers after 429. Default 2.")]
public int? AdaptiveThrottleCloudCap { get; set; }

// ===== Music Styles (dynamic TagSelect) =====
[FieldDefinition(34, Label = "Music Styles", Type = FieldType.TagSelect,
HelpText = "Select one or more styles (aliases supported). Leave empty to use your library profile.",
HelpLink = "https://github.com/RicherTunes/Brainarr/wiki/Styles",
SelectOptionsProviderAction = "styles/getoptions")]
public IEnumerable<string> StyleFilters { get; set; } = Array.Empty<string>();

// Hidden/advanced knobs related to styles & token budgets
[FieldDefinition(35, Label = "Max Selected Styles", Type = FieldType.Number, Advanced = true, Hidden = HiddenType.Hidden,
HelpText = "Soft cap for number of selected styles applied in prompts (default 10). Exceeding selections are trimmed with a log warning.")]
public int MaxSelectedStyles { get; set; } = 10;

[FieldDefinition(36, Label = "Comprehensive Token Budget Override", Type = FieldType.Number, Advanced = true, Hidden = HiddenType.Hidden,
HelpText = "Optional override for Comprehensive prompt token budget. Leave blank to auto-detect from model.")]
public int? ComprehensiveTokenBudgetOverride { get; set; }

[FieldDefinition(37, Label = "Relax Style Matching", Type = FieldType.Checkbox, Advanced = true, Hidden = HiddenType.Hidden,
HelpText = "When enabled, allow parent/adjacent styles as fallback. Default OFF for strict matching.")]
public bool RelaxStyleMatching { get; set; } = false;

[FieldDefinition(27, Label = "Adaptive Throttle Cap (Local)", Type = FieldType.Number, Advanced = true, Hidden = HiddenType.Hidden,
HelpText = "Temporary per-model concurrency cap for local providers after 429. Default 8.")]
public int? AdaptiveThrottleLocalCap { get; set; }
Expand Down
6 changes: 6 additions & 0 deletions Brainarr.Plugin/Configuration/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public static class BrainarrConstants
"Hip Hop", "R&B", "Country", "Folk", "Metal"
};

// Styles Catalog (dynamic JSON)
// NOTE: Replace with canonical GitHub raw URL for the maintained catalog.
public const string StylesCatalogUrl = "https://raw.githubusercontent.com/RicherTunes/Brainarr/main/resources/music_styles.json";
public const int StylesCatalogRefreshHours = 24; // periodic auto-refresh
public const int StylesCatalogTimeoutMs = 5000; // ms network timeout for catalog fetch

// Provider API endpoints
public const string OpenAIChatCompletionsUrl = "https://api.openai.com/v1/chat/completions";
public const string OpenRouterChatCompletionsUrl = "https://openrouter.ai/api/v1/chat/completions";
Expand Down
26 changes: 25 additions & 1 deletion Brainarr.Plugin/Services/Core/BrainarrOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
using System.Diagnostics;
using NzbDrone.Core.ImportLists.Brainarr.Services.Core;
using NzbDrone.Core.ImportLists.Brainarr.Services.Resilience;
using NzbDrone.Core.ImportLists.Brainarr.Services.Styles;

namespace NzbDrone.Core.ImportLists.Brainarr.Services.Core
{
Expand Down Expand Up @@ -58,6 +59,7 @@ public class BrainarrOrchestrator : IBrainarrOrchestrator
private readonly NzbDrone.Core.ImportLists.Brainarr.Services.Core.ITopUpPlanner _topUpPlanner;
private readonly NzbDrone.Core.ImportLists.Brainarr.Services.Core.IRecommendationPipeline _pipeline;
private readonly NzbDrone.Core.ImportLists.Brainarr.Services.Core.IRecommendationCoordinator _coordinator;
private readonly IStyleCatalogService _styleCatalog;

private IAIProvider _currentProvider;
private AIProvider? _currentProviderType;
Expand Down Expand Up @@ -93,7 +95,8 @@ public BrainarrOrchestrator(
NzbDrone.Core.ImportLists.Brainarr.Services.Core.ITopUpPlanner topUpPlanner = null,
NzbDrone.Core.ImportLists.Brainarr.Services.Core.IRecommendationPipeline pipeline = null,
NzbDrone.Core.ImportLists.Brainarr.Services.Core.IRecommendationCoordinator coordinator = null,
NzbDrone.Core.ImportLists.Brainarr.Services.ILibraryAwarePromptBuilder promptBuilder = null)
NzbDrone.Core.ImportLists.Brainarr.Services.ILibraryAwarePromptBuilder promptBuilder = null,
IStyleCatalogService styleCatalog = null)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
Expand All @@ -118,6 +121,7 @@ public BrainarrOrchestrator(
_topUpPlanner = topUpPlanner ?? new TopUpPlanner(logger);
_pipeline = pipeline ?? new RecommendationPipeline(logger, _libraryAnalyzer, _validator, _safetyGates, _topUpPlanner, _mbidResolver, _artistResolver, _duplicationPrevention, _metrics, _history);
_coordinator = coordinator ?? new RecommendationCoordinator(logger, _cache, _pipeline, _sanitizer, _schemaValidator, _history, _libraryAnalyzer);
_styleCatalog = styleCatalog ?? new StyleCatalogService(logger, httpClient);
}

// ====== CORE RECOMMENDATION WORKFLOW ======
Expand Down Expand Up @@ -578,6 +582,8 @@ public object HandleAction(string action, IDictionary<string, string> query, Bra
"observability/get" => settings.EnableObservabilityPreview ? GetObservabilitySummary(query, settings) : new { disabled = true },
"observability/getoptions" => settings.EnableObservabilityPreview ? GetObservabilityOptions() : new { options = Array.Empty<object>() },
"observability/html" => settings.EnableObservabilityPreview ? GetObservabilityHtml(query) : "<html><body><p>Observability preview is disabled.</p></body></html>",
// Styles TagSelect options
"styles/getoptions" => GetStylesOptions(query),
// Options for Approve Suggestions Select field
"review/getoptions" => GetReviewOptions(),
// Read-only Review Summary options
Expand All @@ -592,6 +598,24 @@ public object HandleAction(string action, IDictionary<string, string> query, Bra
}
}

private object GetStylesOptions(IDictionary<string, string> query)
{
try
{
string Get(IDictionary<string, string> q, string k) => q != null && q.TryGetValue(k, out var v) ? v : null;
var q = Get(query, "query") ?? string.Empty;
var items = _styleCatalog.Search(q, 50)
.Select(s => new { value = s.Slug, name = s.Name })
.ToList();
return new { options = items };
}
catch (Exception ex)
{
_logger.Error(ex, "styles/getoptions failed");
return new { options = Array.Empty<object>() };
}
}

private object HandleReviewUpdate(IDictionary<string, string> query, Services.Support.ReviewQueueService.ReviewStatus status)
{
var artist = query.TryGetValue("artist", out var a) ? a : null;
Expand Down
11 changes: 6 additions & 5 deletions Brainarr.Plugin/Services/Core/ConcurrentCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public void Clear()

private async Task EnsureCapacityAsync()
{
var currentSize = Interlocked.Read(ref _currentSize);
var currentSize = _cache.Count;
if (currentSize <= _maxSize)
return;

Expand All @@ -184,7 +184,7 @@ await Task.Run(() =>
_sizeLock.EnterWriteLock();
try
{
currentSize = Interlocked.Read(ref _currentSize);
currentSize = _cache.Count;
if (currentSize <= _maxSize)
return;

Expand Down Expand Up @@ -216,13 +216,13 @@ await Task.Run(() =>

private void EnsureCapacitySync()
{
var currentSize = Interlocked.Read(ref _currentSize);
var currentSize = _cache.Count;
if (currentSize <= _maxSize) return;

_sizeLock.EnterWriteLock();
try
{
currentSize = Interlocked.Read(ref _currentSize);
currentSize = _cache.Count;
if (currentSize <= _maxSize) return;

var toRemoveCount = (int)(currentSize - _maxSize);
Expand Down Expand Up @@ -286,7 +286,7 @@ public CacheStatistics GetStatistics()
var totalRequests = _hits + _misses;
return new CacheStatistics
{
Size = Interlocked.Read(ref _currentSize),
Size = _cache.Count,
MaxSize = _maxSize,
Hits = _hits,
Misses = _misses,
Expand Down Expand Up @@ -377,3 +377,4 @@ public override string ToString()
}
}
}

Loading
Loading