mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
refactor(parser): restructure service with proper logging and sepofcon
This commit is contained in:
7
src/services/parser/Directory.Build.props
Normal file
7
src/services/parser/Directory.Build.props
Normal file
@@ -0,0 +1,7 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!-- Output to root dist/parser/ -->
|
||||
<BaseOutputPath>$(MSBuildThisFileDirectory)..\..\..\dist\parser\</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)..\..\..\dist\parser\obj\</BaseIntermediateOutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
15
src/services/parser/Endpoints/HealthEndpoints.cs
Normal file
15
src/services/parser/Endpoints/HealthEndpoints.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Parser.Logging;
|
||||
|
||||
namespace Parser.Endpoints;
|
||||
|
||||
public static class HealthEndpoints
|
||||
{
|
||||
public static void Map(WebApplication app, string version)
|
||||
{
|
||||
app.MapGet("/health", () =>
|
||||
{
|
||||
Log.Debug("Health check requested", "Health");
|
||||
return Results.Ok(new { status = "healthy", version });
|
||||
});
|
||||
}
|
||||
}
|
||||
123
src/services/parser/Endpoints/MatchEndpoints.cs
Normal file
123
src/services/parser/Endpoints/MatchEndpoints.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Logging;
|
||||
using Parser.Models;
|
||||
|
||||
namespace Parser.Endpoints;
|
||||
|
||||
public static class MatchEndpoints
|
||||
{
|
||||
public static void Map(WebApplication app)
|
||||
{
|
||||
app.MapPost("/match", HandleMatch);
|
||||
app.MapPost("/match/batch", HandleBatchMatch);
|
||||
}
|
||||
|
||||
private static IResult HandleMatch(MatchRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
{
|
||||
Log.Debug("Match request rejected: missing text", "Match");
|
||||
return Results.BadRequest(new { error = "Text is required" });
|
||||
}
|
||||
|
||||
if (request.Patterns == null || request.Patterns.Count == 0)
|
||||
{
|
||||
Log.Debug("Match request rejected: no patterns", "Match");
|
||||
return Results.BadRequest(new { error = "At least one pattern is required" });
|
||||
}
|
||||
|
||||
Log.Info($"Matching {request.Patterns.Count} patterns against text", "Match");
|
||||
|
||||
var results = new Dictionary<string, bool>();
|
||||
|
||||
foreach (var pattern in request.Patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
var regex = new Regex(
|
||||
pattern,
|
||||
RegexOptions.IgnoreCase,
|
||||
TimeSpan.FromMilliseconds(100) // Timeout to prevent ReDoS
|
||||
);
|
||||
results[pattern] = regex.IsMatch(request.Text);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
Log.Warn($"Pattern timed out: {pattern}", "Match");
|
||||
results[pattern] = false;
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Log.Debug($"Invalid regex pattern: {pattern} - {ex.Message}", "Match");
|
||||
results[pattern] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(new MatchResponse { Results = results });
|
||||
}
|
||||
|
||||
private static IResult HandleBatchMatch(BatchMatchRequest request)
|
||||
{
|
||||
if (request.Texts == null || request.Texts.Count == 0)
|
||||
{
|
||||
Log.Debug("Batch match request rejected: no texts", "Match");
|
||||
return Results.BadRequest(new { error = "At least one text is required" });
|
||||
}
|
||||
|
||||
if (request.Patterns == null || request.Patterns.Count == 0)
|
||||
{
|
||||
Log.Debug("Batch match request rejected: no patterns", "Match");
|
||||
return Results.BadRequest(new { error = "At least one pattern is required" });
|
||||
}
|
||||
|
||||
Log.Info($"Batch matching {request.Patterns.Count} patterns against {request.Texts.Count} texts", "Match");
|
||||
|
||||
// Pre-compile all regexes once
|
||||
var compiledPatterns = new Dictionary<string, Regex?>();
|
||||
foreach (var pattern in request.Patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
compiledPatterns[pattern] = new Regex(
|
||||
pattern,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled,
|
||||
TimeSpan.FromMilliseconds(100)
|
||||
);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Log.Debug($"Invalid regex pattern: {pattern} - {ex.Message}", "Match");
|
||||
compiledPatterns[pattern] = null; // Invalid pattern
|
||||
}
|
||||
}
|
||||
|
||||
// Process texts in parallel for better performance
|
||||
var results = new ConcurrentDictionary<string, Dictionary<string, bool>>();
|
||||
|
||||
Parallel.ForEach(request.Texts, text =>
|
||||
{
|
||||
var textResults = new Dictionary<string, bool>();
|
||||
foreach (var (pattern, regex) in compiledPatterns)
|
||||
{
|
||||
if (regex == null)
|
||||
{
|
||||
textResults[pattern] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
textResults[pattern] = regex.IsMatch(text);
|
||||
}
|
||||
catch (RegexMatchTimeoutException)
|
||||
{
|
||||
textResults[pattern] = false;
|
||||
}
|
||||
}
|
||||
results[text] = textResults;
|
||||
});
|
||||
|
||||
return Results.Ok(new BatchMatchResponse { Results = results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) });
|
||||
}
|
||||
}
|
||||
136
src/services/parser/Endpoints/ParseEndpoints.cs
Normal file
136
src/services/parser/Endpoints/ParseEndpoints.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using Parser.Logging;
|
||||
using Parser.Models;
|
||||
using Parser.Parsers;
|
||||
|
||||
namespace Parser.Endpoints;
|
||||
|
||||
public static class ParseEndpoints
|
||||
{
|
||||
public static void Map(WebApplication app)
|
||||
{
|
||||
app.MapPost("/parse", Handle);
|
||||
}
|
||||
|
||||
private static IResult Handle(ParseRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
Log.Debug("Parse request rejected: missing title", "Parse");
|
||||
return Results.BadRequest(new { error = "Title is required" });
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Type) ||
|
||||
(request.Type != "movie" && request.Type != "series"))
|
||||
{
|
||||
Log.Debug($"Parse request rejected: invalid type '{request.Type}'", "Parse");
|
||||
return Results.BadRequest(new { error = "Type is required and must be 'movie' or 'series'" });
|
||||
}
|
||||
|
||||
var qualityResult = QualityParser.ParseQuality(request.Title);
|
||||
var languages = LanguageParser.ParseLanguages(request.Title);
|
||||
var releaseGroup = ReleaseGroupParser.ParseReleaseGroup(request.Title);
|
||||
|
||||
if (request.Type == "movie")
|
||||
{
|
||||
var titleInfo = TitleParser.ParseMovieTitle(request.Title);
|
||||
var response = new ParseResponse
|
||||
{
|
||||
Title = request.Title,
|
||||
Type = "movie",
|
||||
Source = qualityResult.Source.ToString(),
|
||||
Resolution = (int)qualityResult.Resolution,
|
||||
Modifier = qualityResult.Modifier.ToString(),
|
||||
Revision = new RevisionResponse
|
||||
{
|
||||
Version = qualityResult.Revision.Version,
|
||||
Real = qualityResult.Revision.Real,
|
||||
IsRepack = qualityResult.Revision.IsRepack
|
||||
},
|
||||
Languages = languages.Select(l => l.ToString()).ToList(),
|
||||
ReleaseGroup = releaseGroup,
|
||||
MovieTitles = titleInfo?.MovieTitles ?? new List<string>(),
|
||||
Year = titleInfo?.Year ?? 0,
|
||||
Edition = titleInfo?.Edition,
|
||||
ImdbId = titleInfo?.ImdbId,
|
||||
TmdbId = titleInfo?.TmdbId ?? 0,
|
||||
HardcodedSubs = titleInfo?.HardcodedSubs,
|
||||
ReleaseHash = titleInfo?.ReleaseHash,
|
||||
Episode = null
|
||||
};
|
||||
|
||||
Log.Info($"Parsed movie: {request.Title}", new LogOptions
|
||||
{
|
||||
Source = "Parse",
|
||||
Meta = new
|
||||
{
|
||||
source = response.Source,
|
||||
resolution = response.Resolution,
|
||||
languages = response.Languages,
|
||||
releaseGroup = response.ReleaseGroup,
|
||||
year = response.Year,
|
||||
title = titleInfo?.PrimaryMovieTitle
|
||||
}
|
||||
});
|
||||
|
||||
return Results.Ok(response);
|
||||
}
|
||||
else // series
|
||||
{
|
||||
var episodeInfo = EpisodeParser.ParseTitle(request.Title);
|
||||
var response = new ParseResponse
|
||||
{
|
||||
Title = request.Title,
|
||||
Type = "series",
|
||||
Source = qualityResult.Source.ToString(),
|
||||
Resolution = (int)qualityResult.Resolution,
|
||||
Modifier = qualityResult.Modifier.ToString(),
|
||||
Revision = new RevisionResponse
|
||||
{
|
||||
Version = qualityResult.Revision.Version,
|
||||
Real = qualityResult.Revision.Real,
|
||||
IsRepack = qualityResult.Revision.IsRepack
|
||||
},
|
||||
Languages = languages.Select(l => l.ToString()).ToList(),
|
||||
ReleaseGroup = releaseGroup,
|
||||
MovieTitles = new List<string>(),
|
||||
Year = 0,
|
||||
Edition = null,
|
||||
ImdbId = null,
|
||||
TmdbId = 0,
|
||||
HardcodedSubs = null,
|
||||
ReleaseHash = null,
|
||||
Episode = episodeInfo != null ? new EpisodeResponse
|
||||
{
|
||||
SeriesTitle = episodeInfo.SeriesTitle,
|
||||
SeasonNumber = episodeInfo.SeasonNumber,
|
||||
EpisodeNumbers = episodeInfo.EpisodeNumbers.ToList(),
|
||||
AbsoluteEpisodeNumbers = episodeInfo.AbsoluteEpisodeNumbers.ToList(),
|
||||
AirDate = episodeInfo.AirDate,
|
||||
FullSeason = episodeInfo.FullSeason,
|
||||
IsPartialSeason = episodeInfo.IsPartialSeason,
|
||||
IsMultiSeason = episodeInfo.IsMultiSeason,
|
||||
IsMiniSeries = episodeInfo.IsMiniSeries,
|
||||
Special = episodeInfo.Special,
|
||||
ReleaseType = episodeInfo.ReleaseType.ToString()
|
||||
} : null
|
||||
};
|
||||
|
||||
Log.Info($"Parsed series: {request.Title}", new LogOptions
|
||||
{
|
||||
Source = "Parse",
|
||||
Meta = new
|
||||
{
|
||||
source = response.Source,
|
||||
resolution = response.Resolution,
|
||||
languages = response.Languages,
|
||||
releaseGroup = response.ReleaseGroup,
|
||||
series = episodeInfo?.SeriesTitle,
|
||||
season = episodeInfo?.SeasonNumber,
|
||||
episodes = episodeInfo?.EpisodeNumbers
|
||||
}
|
||||
});
|
||||
|
||||
return Results.Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/services/parser/Logging/Colors.cs
Normal file
14
src/services/parser/Logging/Colors.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Parser.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// ANSI color codes for terminal output
|
||||
/// </summary>
|
||||
public static class Colors
|
||||
{
|
||||
public const string Reset = "\x1b[0m";
|
||||
public const string Grey = "\x1b[90m";
|
||||
public const string Cyan = "\x1b[36m";
|
||||
public const string Green = "\x1b[32m";
|
||||
public const string Yellow = "\x1b[33m";
|
||||
public const string Red = "\x1b[31m";
|
||||
}
|
||||
139
src/services/parser/Logging/LogSettings.cs
Normal file
139
src/services/parser/Logging/LogSettings.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Parser.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Log settings manager
|
||||
/// Loads configuration from appsettings.json and environment variables
|
||||
/// </summary>
|
||||
public class LogSettingsManager
|
||||
{
|
||||
private LoggerConfig _config;
|
||||
private readonly IConfiguration? _configuration;
|
||||
|
||||
public LogSettingsManager(IConfiguration? configuration = null)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_config = LoadConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load settings from configuration
|
||||
/// </summary>
|
||||
private LoggerConfig LoadConfig()
|
||||
{
|
||||
var config = new LoggerConfig();
|
||||
|
||||
if (_configuration != null)
|
||||
{
|
||||
var section = _configuration.GetSection("ParserLogging");
|
||||
|
||||
config.LogsDir = section["LogsDir"] ?? GetEnvOrDefault("PARSER_LOGS_DIR", "/tmp/parser-logs");
|
||||
config.Enabled = ParseBool(section["Enabled"], GetEnvBool("PARSER_LOG_ENABLED", true));
|
||||
config.FileLogging = ParseBool(section["FileLogging"], GetEnvBool("PARSER_LOG_FILE", true));
|
||||
config.ConsoleLogging = ParseBool(section["ConsoleLogging"], GetEnvBool("PARSER_LOG_CONSOLE", true));
|
||||
config.MinLevel = ParseLogLevel(section["MinLevel"], GetEnvLogLevel("PARSER_LOG_LEVEL", LogLevel.INFO));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to environment variables only
|
||||
config.LogsDir = GetEnvOrDefault("PARSER_LOGS_DIR", "/tmp/parser-logs");
|
||||
config.Enabled = GetEnvBool("PARSER_LOG_ENABLED", true);
|
||||
config.FileLogging = GetEnvBool("PARSER_LOG_FILE", true);
|
||||
config.ConsoleLogging = GetEnvBool("PARSER_LOG_CONSOLE", true);
|
||||
config.MinLevel = GetEnvLogLevel("PARSER_LOG_LEVEL", LogLevel.INFO);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload settings from configuration
|
||||
/// </summary>
|
||||
public void Reload()
|
||||
{
|
||||
_config = LoadConfig();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current configuration
|
||||
/// </summary>
|
||||
public LoggerConfig Get() => _config;
|
||||
|
||||
/// <summary>
|
||||
/// Check if logging is enabled
|
||||
/// </summary>
|
||||
public bool IsEnabled() => _config.Enabled;
|
||||
|
||||
/// <summary>
|
||||
/// Check if file logging is enabled
|
||||
/// </summary>
|
||||
public bool IsFileLoggingEnabled() => _config.FileLogging;
|
||||
|
||||
/// <summary>
|
||||
/// Check if console logging is enabled
|
||||
/// </summary>
|
||||
public bool IsConsoleLoggingEnabled() => _config.ConsoleLogging;
|
||||
|
||||
/// <summary>
|
||||
/// Get minimum log level
|
||||
/// </summary>
|
||||
public LogLevel GetMinLevel() => _config.MinLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Check if a log level should be logged based on minimum level
|
||||
/// </summary>
|
||||
public bool ShouldLog(LogLevel level)
|
||||
{
|
||||
if (!IsEnabled()) return false;
|
||||
return level >= _config.MinLevel;
|
||||
}
|
||||
|
||||
// Helper methods for parsing config values
|
||||
private static string GetEnvOrDefault(string key, string defaultValue)
|
||||
=> Environment.GetEnvironmentVariable(key) ?? defaultValue;
|
||||
|
||||
private static bool GetEnvBool(string key, bool defaultValue)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(key);
|
||||
if (string.IsNullOrEmpty(value)) return defaultValue;
|
||||
return value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static LogLevel GetEnvLogLevel(string key, LogLevel defaultValue)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(key);
|
||||
if (string.IsNullOrEmpty(value)) return defaultValue;
|
||||
return Enum.TryParse<LogLevel>(value, true, out var level) ? level : defaultValue;
|
||||
}
|
||||
|
||||
private static bool ParseBool(string? value, bool defaultValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return defaultValue;
|
||||
return value.Equals("true", StringComparison.OrdinalIgnoreCase) ||
|
||||
value.Equals("1", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static LogLevel ParseLogLevel(string? value, LogLevel defaultValue)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value)) return defaultValue;
|
||||
return Enum.TryParse<LogLevel>(value, true, out var level) ? level : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Singleton instance for global access
|
||||
/// </summary>
|
||||
public static class LogSettings
|
||||
{
|
||||
private static LogSettingsManager? _instance;
|
||||
|
||||
public static void Initialize(IConfiguration? configuration = null)
|
||||
{
|
||||
_instance = new LogSettingsManager(configuration);
|
||||
}
|
||||
|
||||
public static LogSettingsManager Instance =>
|
||||
_instance ?? throw new InvalidOperationException("LogSettings not initialized. Call Initialize() first.");
|
||||
}
|
||||
241
src/services/parser/Logging/Logger.cs
Normal file
241
src/services/parser/Logging/Logger.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Parser.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Logger with console and file output
|
||||
/// Supports configurable settings and daily rotation
|
||||
/// </summary>
|
||||
public class Logger
|
||||
{
|
||||
private readonly LoggerConfig _config;
|
||||
private readonly LogSettingsManager? _settings;
|
||||
private static readonly object _fileLock = new();
|
||||
|
||||
public Logger(LoggerConfig? config = null, LogSettingsManager? settings = null)
|
||||
{
|
||||
_settings = settings;
|
||||
_config = config ?? _settings?.Get() ?? new LoggerConfig();
|
||||
}
|
||||
|
||||
private string FormatTimestamp()
|
||||
{
|
||||
var timestamp = DateTime.UtcNow.ToString("o");
|
||||
return $"{Colors.Grey}{timestamp}{Colors.Reset}";
|
||||
}
|
||||
|
||||
private string FormatLevel(LogLevel level)
|
||||
{
|
||||
var color = level switch
|
||||
{
|
||||
LogLevel.DEBUG => Colors.Cyan,
|
||||
LogLevel.INFO => Colors.Green,
|
||||
LogLevel.WARN => Colors.Yellow,
|
||||
LogLevel.ERROR => Colors.Red,
|
||||
_ => Colors.Reset
|
||||
};
|
||||
return $"{color}{level.ToString().PadRight(5)}{Colors.Reset}";
|
||||
}
|
||||
|
||||
private string FormatSource(string? source)
|
||||
{
|
||||
if (string.IsNullOrEmpty(source)) return "";
|
||||
return $"{Colors.Grey}[{source}]{Colors.Reset}";
|
||||
}
|
||||
|
||||
private string FormatMeta(object? meta)
|
||||
{
|
||||
if (meta == null) return "";
|
||||
return $"{Colors.Grey}{JsonSerializer.Serialize(meta)}{Colors.Reset}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get log file path with daily rotation (YYYY-MM-DD.log)
|
||||
/// </summary>
|
||||
private string GetLogFilePath()
|
||||
{
|
||||
var date = DateTime.UtcNow.ToString("yyyy-MM-dd");
|
||||
return Path.Combine(_config.LogsDir, $"{date}.log");
|
||||
}
|
||||
|
||||
private bool IsEnabled()
|
||||
=> _settings?.IsEnabled() ?? _config.Enabled;
|
||||
|
||||
private bool IsFileLoggingEnabled()
|
||||
=> _settings?.IsFileLoggingEnabled() ?? _config.FileLogging;
|
||||
|
||||
private bool IsConsoleLoggingEnabled()
|
||||
=> _settings?.IsConsoleLoggingEnabled() ?? _config.ConsoleLogging;
|
||||
|
||||
private bool ShouldLog(LogLevel level)
|
||||
{
|
||||
if (!IsEnabled()) return false;
|
||||
var minLevel = _settings?.GetMinLevel() ?? _config.MinLevel;
|
||||
return level >= minLevel;
|
||||
}
|
||||
|
||||
private void Log(LogLevel level, string message, LogOptions? options = null)
|
||||
{
|
||||
if (!ShouldLog(level)) return;
|
||||
|
||||
var timestamp = DateTime.UtcNow.ToString("o");
|
||||
|
||||
// Console output (colored)
|
||||
if (IsConsoleLoggingEnabled())
|
||||
{
|
||||
var parts = new List<string>
|
||||
{
|
||||
FormatTimestamp(),
|
||||
FormatLevel(level),
|
||||
message
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(options?.Source))
|
||||
parts.Add(FormatSource(options.Source));
|
||||
|
||||
if (options?.Meta != null)
|
||||
parts.Add(FormatMeta(options.Meta));
|
||||
|
||||
Console.WriteLine(string.Join(" | ", parts));
|
||||
}
|
||||
|
||||
// File output (JSON)
|
||||
if (IsFileLoggingEnabled())
|
||||
{
|
||||
var logEntry = new LogEntry
|
||||
{
|
||||
Timestamp = timestamp,
|
||||
Level = level.ToString(),
|
||||
Message = message,
|
||||
Source = options?.Source,
|
||||
Meta = options?.Meta
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Ensure logs directory exists
|
||||
Directory.CreateDirectory(_config.LogsDir);
|
||||
|
||||
var filePath = GetLogFilePath();
|
||||
var json = JsonSerializer.Serialize(logEntry) + Environment.NewLine;
|
||||
|
||||
// Thread-safe file write
|
||||
lock (_fileLock)
|
||||
{
|
||||
File.AppendAllText(filePath, json);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If file write fails, at least we have console output
|
||||
Console.Error.WriteLine($"Failed to write to log file: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Debug(string message, LogOptions? options = null)
|
||||
=> Log(LogLevel.DEBUG, message, options);
|
||||
|
||||
public void Debug(string message, string source)
|
||||
=> Log(LogLevel.DEBUG, message, new LogOptions { Source = source });
|
||||
|
||||
public void Info(string message, LogOptions? options = null)
|
||||
=> Log(LogLevel.INFO, message, options);
|
||||
|
||||
public void Info(string message, string source)
|
||||
=> Log(LogLevel.INFO, message, new LogOptions { Source = source });
|
||||
|
||||
public void Warn(string message, LogOptions? options = null)
|
||||
=> Log(LogLevel.WARN, message, options);
|
||||
|
||||
public void Warn(string message, string source)
|
||||
=> Log(LogLevel.WARN, message, new LogOptions { Source = source });
|
||||
|
||||
public void Error(string message, LogOptions? options = null)
|
||||
=> Log(LogLevel.ERROR, message, options);
|
||||
|
||||
public void Error(string message, string source)
|
||||
=> Log(LogLevel.ERROR, message, new LogOptions { Source = source });
|
||||
|
||||
public void Error(string message, Exception ex, LogOptions? options = null)
|
||||
{
|
||||
Log(LogLevel.ERROR, message, options);
|
||||
|
||||
// Print stack trace to console
|
||||
if (ex.StackTrace != null && IsConsoleLoggingEnabled())
|
||||
{
|
||||
Console.WriteLine($"{Colors.Grey}{ex.StackTrace}{Colors.Reset}");
|
||||
}
|
||||
|
||||
// Write stack trace to file
|
||||
if (ex.StackTrace != null && IsFileLoggingEnabled())
|
||||
{
|
||||
var traceEntry = new LogEntry
|
||||
{
|
||||
Timestamp = DateTime.UtcNow.ToString("o"),
|
||||
Level = "ERROR",
|
||||
Message = "Stack trace",
|
||||
Meta = new { stack = ex.StackTrace }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = GetLogFilePath();
|
||||
var json = JsonSerializer.Serialize(traceEntry) + Environment.NewLine;
|
||||
|
||||
lock (_fileLock)
|
||||
{
|
||||
File.AppendAllText(filePath, json);
|
||||
}
|
||||
}
|
||||
catch (Exception writeEx)
|
||||
{
|
||||
Console.Error.WriteLine($"Failed to write stack trace to log file: {writeEx.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global logger singleton for production use
|
||||
/// </summary>
|
||||
public static class Log
|
||||
{
|
||||
private static Logger? _instance;
|
||||
|
||||
public static void Initialize(LoggerConfig? config = null, LogSettingsManager? settings = null)
|
||||
{
|
||||
_instance = new Logger(config, settings);
|
||||
}
|
||||
|
||||
public static Logger Instance =>
|
||||
_instance ?? throw new InvalidOperationException("Logger not initialized. Call Initialize() first.");
|
||||
|
||||
// Convenience methods that delegate to the singleton
|
||||
public static void Debug(string message, LogOptions? options = null)
|
||||
=> Instance.Debug(message, options);
|
||||
|
||||
public static void Debug(string message, string source)
|
||||
=> Instance.Debug(message, source);
|
||||
|
||||
public static void Info(string message, LogOptions? options = null)
|
||||
=> Instance.Info(message, options);
|
||||
|
||||
public static void Info(string message, string source)
|
||||
=> Instance.Info(message, source);
|
||||
|
||||
public static void Warn(string message, LogOptions? options = null)
|
||||
=> Instance.Warn(message, options);
|
||||
|
||||
public static void Warn(string message, string source)
|
||||
=> Instance.Warn(message, source);
|
||||
|
||||
public static void Error(string message, LogOptions? options = null)
|
||||
=> Instance.Error(message, options);
|
||||
|
||||
public static void Error(string message, string source)
|
||||
=> Instance.Error(message, source);
|
||||
|
||||
public static void Error(string message, Exception ex, LogOptions? options = null)
|
||||
=> Instance.Error(message, ex, options);
|
||||
}
|
||||
92
src/services/parser/Logging/Startup.cs
Normal file
92
src/services/parser/Logging/Startup.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
namespace Parser.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Startup logging utilities
|
||||
/// </summary>
|
||||
public static class Startup
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if running inside a Docker container
|
||||
/// </summary>
|
||||
public static bool IsDocker()
|
||||
{
|
||||
// Check for .dockerenv file (most reliable)
|
||||
if (File.Exists("/.dockerenv"))
|
||||
return true;
|
||||
|
||||
// Check for docker in cgroup (fallback)
|
||||
try
|
||||
{
|
||||
var cgroup = File.ReadAllText("/proc/1/cgroup");
|
||||
return cgroup.Contains("docker");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log container configuration (only when running in Docker)
|
||||
/// </summary>
|
||||
public static void LogContainerConfig()
|
||||
{
|
||||
if (!IsDocker()) return;
|
||||
|
||||
Log.Info("Container initialized", new LogOptions
|
||||
{
|
||||
Source = "Docker",
|
||||
Meta = new
|
||||
{
|
||||
puid = Environment.GetEnvironmentVariable("PUID") ?? "1000",
|
||||
pgid = Environment.GetEnvironmentVariable("PGID") ?? "1000",
|
||||
umask = Environment.GetEnvironmentVariable("UMASK") ?? "022",
|
||||
tz = Environment.GetEnvironmentVariable("TZ") ?? "UTC"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server information record
|
||||
/// </summary>
|
||||
public record ServerInfo(
|
||||
string Version,
|
||||
string Environment,
|
||||
string Timezone,
|
||||
string Hostname
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Get server information
|
||||
/// </summary>
|
||||
public static ServerInfo GetServerInfo(string version)
|
||||
{
|
||||
return new ServerInfo(
|
||||
Version: version,
|
||||
Environment: System.Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production",
|
||||
Timezone: TimeZoneInfo.Local.Id,
|
||||
Hostname: System.Environment.MachineName
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log server startup information
|
||||
/// </summary>
|
||||
public static void LogServerInfo(string version, string url)
|
||||
{
|
||||
var info = GetServerInfo(version);
|
||||
|
||||
Log.Info($"Parser service started", new LogOptions
|
||||
{
|
||||
Source = "Startup",
|
||||
Meta = new
|
||||
{
|
||||
version = info.Version,
|
||||
url,
|
||||
environment = info.Environment,
|
||||
timezone = info.Timezone,
|
||||
hostname = info.Hostname
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/services/parser/Logging/Types.cs
Normal file
71
src/services/parser/Logging/Types.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
namespace Parser.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Log severity levels (DEBUG -> INFO -> WARN -> ERROR)
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Options for a single log call
|
||||
/// </summary>
|
||||
public class LogOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional source/context tag (e.g., "Parser", "Quality", "Language")
|
||||
/// </summary>
|
||||
public string? Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional metadata to include with the log
|
||||
/// </summary>
|
||||
public object? Meta { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single log entry (used for JSON file output)
|
||||
/// </summary>
|
||||
public class LogEntry
|
||||
{
|
||||
public required string Timestamp { get; set; }
|
||||
public required string Level { get; set; }
|
||||
public required string Message { get; set; }
|
||||
public string? Source { get; set; }
|
||||
public object? Meta { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger configuration
|
||||
/// </summary>
|
||||
public class LoggerConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Directory where log files will be written
|
||||
/// </summary>
|
||||
public string LogsDir { get; set; } = "/tmp/logs";
|
||||
|
||||
/// <summary>
|
||||
/// Master toggle for all logging
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable file logging
|
||||
/// </summary>
|
||||
public bool FileLogging { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable console logging
|
||||
/// </summary>
|
||||
public bool ConsoleLogging { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum log level to output
|
||||
/// </summary>
|
||||
public LogLevel MinLevel { get; set; } = LogLevel.INFO;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Models;
|
||||
|
||||
public enum Language
|
||||
{
|
||||
7
src/services/parser/Models/Requests.cs
Normal file
7
src/services/parser/Models/Requests.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Parser.Models;
|
||||
|
||||
public record ParseRequest(string Title, string? Type);
|
||||
|
||||
public record MatchRequest(string Text, List<string> Patterns);
|
||||
|
||||
public record BatchMatchRequest(List<string> Texts, List<string> Patterns);
|
||||
53
src/services/parser/Models/Responses.cs
Normal file
53
src/services/parser/Models/Responses.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace Parser.Models;
|
||||
|
||||
public record ParseResponse
|
||||
{
|
||||
public string Title { get; init; } = "";
|
||||
public string Type { get; init; } = "";
|
||||
public string Source { get; init; } = "";
|
||||
public int Resolution { get; init; }
|
||||
public string Modifier { get; init; } = "";
|
||||
public RevisionResponse Revision { get; init; } = new();
|
||||
public List<string> Languages { get; init; } = new();
|
||||
public string? ReleaseGroup { get; init; }
|
||||
public List<string> MovieTitles { get; init; } = new();
|
||||
public int Year { get; init; }
|
||||
public string? Edition { get; init; }
|
||||
public string? ImdbId { get; init; }
|
||||
public int TmdbId { get; init; }
|
||||
public string? HardcodedSubs { get; init; }
|
||||
public string? ReleaseHash { get; init; }
|
||||
public EpisodeResponse? Episode { get; init; }
|
||||
}
|
||||
|
||||
public record RevisionResponse
|
||||
{
|
||||
public int Version { get; init; } = 1;
|
||||
public int Real { get; init; }
|
||||
public bool IsRepack { get; init; }
|
||||
}
|
||||
|
||||
public record EpisodeResponse
|
||||
{
|
||||
public string? SeriesTitle { get; init; }
|
||||
public int SeasonNumber { get; init; }
|
||||
public List<int> EpisodeNumbers { get; init; } = new();
|
||||
public List<int> AbsoluteEpisodeNumbers { get; init; } = new();
|
||||
public string? AirDate { get; init; }
|
||||
public bool FullSeason { get; init; }
|
||||
public bool IsPartialSeason { get; init; }
|
||||
public bool IsMultiSeason { get; init; }
|
||||
public bool IsMiniSeries { get; init; }
|
||||
public bool Special { get; init; }
|
||||
public string ReleaseType { get; init; } = "Unknown";
|
||||
}
|
||||
|
||||
public record MatchResponse
|
||||
{
|
||||
public Dictionary<string, bool> Results { get; init; } = new();
|
||||
}
|
||||
|
||||
public record BatchMatchResponse
|
||||
{
|
||||
public Dictionary<string, Dictionary<string, bool>> Results { get; init; } = new();
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Models;
|
||||
|
||||
public enum QualitySource
|
||||
{
|
||||
@@ -4,6 +4,13 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>Parser</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers.Common;
|
||||
|
||||
internal static class ParserCommon
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers.Common;
|
||||
|
||||
public class RegexReplace
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Parsers.Common;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers;
|
||||
|
||||
public enum ReleaseType
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Models;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers;
|
||||
|
||||
public static class LanguageParser
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Models;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers;
|
||||
|
||||
public static class QualityParser
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Parsers.Common;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers;
|
||||
|
||||
public static class ReleaseGroupParser
|
||||
{
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Parser.Parsers.Common;
|
||||
|
||||
namespace Parser.Core;
|
||||
namespace Parser.Parsers;
|
||||
|
||||
public class ParsedMovieInfo
|
||||
{
|
||||
@@ -1,259 +1,32 @@
|
||||
using Parser.Core;
|
||||
|
||||
// Bump this version when parser logic changes (regex patterns, parsing behavior, etc.)
|
||||
// This invalidates the parse result cache in Profilarr
|
||||
const string ParserVersion = "1.0.0";
|
||||
using Parser.Endpoints;
|
||||
using Parser.Logging;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Load configuration
|
||||
builder.Configuration.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
|
||||
|
||||
// Get parser version from config
|
||||
var parserVersion = builder.Configuration["Parser:Version"] ?? "1.0.0";
|
||||
|
||||
// Initialize logging
|
||||
LogSettings.Initialize(builder.Configuration);
|
||||
Log.Initialize(settings: LogSettings.Instance);
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapPost("/parse", (ParseRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Title is required" });
|
||||
}
|
||||
// Get the URL the server will listen on
|
||||
var urls = app.Urls.Any() ? app.Urls.First() : "http://localhost:5000";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Type) ||
|
||||
(request.Type != "movie" && request.Type != "series"))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Type is required and must be 'movie' or 'series'" });
|
||||
}
|
||||
// Log startup info
|
||||
Startup.LogContainerConfig();
|
||||
Startup.LogServerInfo(parserVersion, urls);
|
||||
|
||||
var qualityResult = QualityParser.ParseQuality(request.Title);
|
||||
var languages = LanguageParser.ParseLanguages(request.Title);
|
||||
var releaseGroup = ReleaseGroupParser.ParseReleaseGroup(request.Title);
|
||||
|
||||
if (request.Type == "movie")
|
||||
{
|
||||
var titleInfo = TitleParser.ParseMovieTitle(request.Title);
|
||||
return Results.Ok(new ParseResponse
|
||||
{
|
||||
Title = request.Title,
|
||||
Type = "movie",
|
||||
Source = qualityResult.Source.ToString(),
|
||||
Resolution = (int)qualityResult.Resolution,
|
||||
Modifier = qualityResult.Modifier.ToString(),
|
||||
Revision = new RevisionResponse
|
||||
{
|
||||
Version = qualityResult.Revision.Version,
|
||||
Real = qualityResult.Revision.Real,
|
||||
IsRepack = qualityResult.Revision.IsRepack
|
||||
},
|
||||
Languages = languages.Select(l => l.ToString()).ToList(),
|
||||
ReleaseGroup = releaseGroup,
|
||||
MovieTitles = titleInfo?.MovieTitles ?? new List<string>(),
|
||||
Year = titleInfo?.Year ?? 0,
|
||||
Edition = titleInfo?.Edition,
|
||||
ImdbId = titleInfo?.ImdbId,
|
||||
TmdbId = titleInfo?.TmdbId ?? 0,
|
||||
HardcodedSubs = titleInfo?.HardcodedSubs,
|
||||
ReleaseHash = titleInfo?.ReleaseHash,
|
||||
Episode = null
|
||||
});
|
||||
}
|
||||
else // series
|
||||
{
|
||||
var episodeInfo = EpisodeParser.ParseTitle(request.Title);
|
||||
return Results.Ok(new ParseResponse
|
||||
{
|
||||
Title = request.Title,
|
||||
Type = "series",
|
||||
Source = qualityResult.Source.ToString(),
|
||||
Resolution = (int)qualityResult.Resolution,
|
||||
Modifier = qualityResult.Modifier.ToString(),
|
||||
Revision = new RevisionResponse
|
||||
{
|
||||
Version = qualityResult.Revision.Version,
|
||||
Real = qualityResult.Revision.Real,
|
||||
IsRepack = qualityResult.Revision.IsRepack
|
||||
},
|
||||
Languages = languages.Select(l => l.ToString()).ToList(),
|
||||
ReleaseGroup = releaseGroup,
|
||||
MovieTitles = new List<string>(),
|
||||
Year = 0,
|
||||
Edition = null,
|
||||
ImdbId = null,
|
||||
TmdbId = 0,
|
||||
HardcodedSubs = null,
|
||||
ReleaseHash = null,
|
||||
Episode = episodeInfo != null ? new EpisodeResponse
|
||||
{
|
||||
SeriesTitle = episodeInfo.SeriesTitle,
|
||||
SeasonNumber = episodeInfo.SeasonNumber,
|
||||
EpisodeNumbers = episodeInfo.EpisodeNumbers.ToList(),
|
||||
AbsoluteEpisodeNumbers = episodeInfo.AbsoluteEpisodeNumbers.ToList(),
|
||||
AirDate = episodeInfo.AirDate,
|
||||
FullSeason = episodeInfo.FullSeason,
|
||||
IsPartialSeason = episodeInfo.IsPartialSeason,
|
||||
IsMultiSeason = episodeInfo.IsMultiSeason,
|
||||
IsMiniSeries = episodeInfo.IsMiniSeries,
|
||||
Special = episodeInfo.Special,
|
||||
ReleaseType = episodeInfo.ReleaseType.ToString()
|
||||
} : null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy", version = ParserVersion }));
|
||||
|
||||
app.MapPost("/match", (MatchRequest request) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.Text))
|
||||
{
|
||||
return Results.BadRequest(new { error = "Text is required" });
|
||||
}
|
||||
|
||||
if (request.Patterns == null || request.Patterns.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new { error = "At least one pattern is required" });
|
||||
}
|
||||
|
||||
var results = new Dictionary<string, bool>();
|
||||
|
||||
foreach (var pattern in request.Patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
var regex = new System.Text.RegularExpressions.Regex(
|
||||
pattern,
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase,
|
||||
TimeSpan.FromMilliseconds(100) // Timeout to prevent ReDoS
|
||||
);
|
||||
results[pattern] = regex.IsMatch(request.Text);
|
||||
}
|
||||
catch (System.Text.RegularExpressions.RegexMatchTimeoutException)
|
||||
{
|
||||
// Pattern took too long, treat as no match
|
||||
results[pattern] = false;
|
||||
}
|
||||
catch (System.ArgumentException)
|
||||
{
|
||||
// Invalid regex pattern
|
||||
results[pattern] = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Results.Ok(new MatchResponse { Results = results });
|
||||
});
|
||||
|
||||
app.MapPost("/match/batch", (BatchMatchRequest request) =>
|
||||
{
|
||||
if (request.Texts == null || request.Texts.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new { error = "At least one text is required" });
|
||||
}
|
||||
|
||||
if (request.Patterns == null || request.Patterns.Count == 0)
|
||||
{
|
||||
return Results.BadRequest(new { error = "At least one pattern is required" });
|
||||
}
|
||||
|
||||
// Pre-compile all regexes once
|
||||
var compiledPatterns = new Dictionary<string, System.Text.RegularExpressions.Regex?>();
|
||||
foreach (var pattern in request.Patterns)
|
||||
{
|
||||
try
|
||||
{
|
||||
compiledPatterns[pattern] = new System.Text.RegularExpressions.Regex(
|
||||
pattern,
|
||||
System.Text.RegularExpressions.RegexOptions.IgnoreCase | System.Text.RegularExpressions.RegexOptions.Compiled,
|
||||
TimeSpan.FromMilliseconds(100)
|
||||
);
|
||||
}
|
||||
catch (System.ArgumentException)
|
||||
{
|
||||
compiledPatterns[pattern] = null; // Invalid pattern
|
||||
}
|
||||
}
|
||||
|
||||
// Process texts in parallel for better performance
|
||||
var results = new System.Collections.Concurrent.ConcurrentDictionary<string, Dictionary<string, bool>>();
|
||||
|
||||
System.Threading.Tasks.Parallel.ForEach(request.Texts, text =>
|
||||
{
|
||||
var textResults = new Dictionary<string, bool>();
|
||||
foreach (var (pattern, regex) in compiledPatterns)
|
||||
{
|
||||
if (regex == null)
|
||||
{
|
||||
textResults[pattern] = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
textResults[pattern] = regex.IsMatch(text);
|
||||
}
|
||||
catch (System.Text.RegularExpressions.RegexMatchTimeoutException)
|
||||
{
|
||||
textResults[pattern] = false;
|
||||
}
|
||||
}
|
||||
results[text] = textResults;
|
||||
});
|
||||
|
||||
return Results.Ok(new BatchMatchResponse { Results = results.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) });
|
||||
});
|
||||
// Map endpoints
|
||||
ParseEndpoints.Map(app);
|
||||
MatchEndpoints.Map(app);
|
||||
HealthEndpoints.Map(app, parserVersion);
|
||||
|
||||
app.Run();
|
||||
|
||||
public record ParseRequest(string Title, string? Type);
|
||||
|
||||
public record ParseResponse
|
||||
{
|
||||
public string Title { get; init; } = "";
|
||||
public string Type { get; init; } = "";
|
||||
public string Source { get; init; } = "";
|
||||
public int Resolution { get; init; }
|
||||
public string Modifier { get; init; } = "";
|
||||
public RevisionResponse Revision { get; init; } = new();
|
||||
public List<string> Languages { get; init; } = new();
|
||||
public string? ReleaseGroup { get; init; }
|
||||
public List<string> MovieTitles { get; init; } = new();
|
||||
public int Year { get; init; }
|
||||
public string? Edition { get; init; }
|
||||
public string? ImdbId { get; init; }
|
||||
public int TmdbId { get; init; }
|
||||
public string? HardcodedSubs { get; init; }
|
||||
public string? ReleaseHash { get; init; }
|
||||
public EpisodeResponse? Episode { get; init; }
|
||||
}
|
||||
|
||||
public record RevisionResponse
|
||||
{
|
||||
public int Version { get; init; } = 1;
|
||||
public int Real { get; init; }
|
||||
public bool IsRepack { get; init; }
|
||||
}
|
||||
|
||||
public record EpisodeResponse
|
||||
{
|
||||
public string? SeriesTitle { get; init; }
|
||||
public int SeasonNumber { get; init; }
|
||||
public List<int> EpisodeNumbers { get; init; } = new();
|
||||
public List<int> AbsoluteEpisodeNumbers { get; init; } = new();
|
||||
public string? AirDate { get; init; }
|
||||
public bool FullSeason { get; init; }
|
||||
public bool IsPartialSeason { get; init; }
|
||||
public bool IsMultiSeason { get; init; }
|
||||
public bool IsMiniSeries { get; init; }
|
||||
public bool Special { get; init; }
|
||||
public string ReleaseType { get; init; } = "Unknown";
|
||||
}
|
||||
|
||||
public record MatchRequest(string Text, List<string> Patterns);
|
||||
|
||||
public record MatchResponse
|
||||
{
|
||||
public Dictionary<string, bool> Results { get; init; } = new();
|
||||
}
|
||||
|
||||
public record BatchMatchRequest(List<string> Texts, List<string> Patterns);
|
||||
|
||||
public record BatchMatchResponse
|
||||
{
|
||||
public Dictionary<string, Dictionary<string, bool>> Results { get; init; } = new();
|
||||
}
|
||||
|
||||
17
src/services/parser/appsettings.json
Normal file
17
src/services/parser/appsettings.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "None",
|
||||
"Microsoft": "None"
|
||||
}
|
||||
},
|
||||
"ParserLogging": {
|
||||
"Enabled": true,
|
||||
"ConsoleLogging": true,
|
||||
"FileLogging": false,
|
||||
"MinLevel": "INFO"
|
||||
},
|
||||
"Parser": {
|
||||
"Version": "1.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user