feat(parser): implement C# parser microservice with regex-based title parsing

- Added RegexReplace class for handling regex replacements.
- Created ReleaseGroupParser for extracting release groups from titles.
- Developed TitleParser for parsing movie titles, including editions and IDs.
- Introduced QualitySource, Resolution, QualityModifier enums and QualityResult class for quality metadata.
- Set up Dockerfile and docker-compose for containerized deployment.
- Implemented ASP.NET Core web API for parsing requests.
- Added TypeScript client for interacting with the parser service.
- Enhanced configuration to support dynamic parser service URL.
This commit is contained in:
Sam Chau
2025-12-30 10:33:52 +10:30
parent 8a3f266593
commit 5c26d6d7b2
19 changed files with 2505 additions and 2 deletions

View File

@@ -0,0 +1,136 @@
/**
* Parser Service Client
* Calls the C# parser microservice
*/
import { config } from '$config';
import {
QualitySource,
QualityModifier,
Language,
ReleaseType,
type QualityInfo,
type ParseResult,
type EpisodeInfo,
type Resolution,
type MediaType
} from './types.ts';
interface EpisodeResponse {
seriesTitle: string | null;
seasonNumber: number;
episodeNumbers: number[];
absoluteEpisodeNumbers: number[];
airDate: string | null;
fullSeason: boolean;
isPartialSeason: boolean;
isMultiSeason: boolean;
isMiniSeries: boolean;
special: boolean;
releaseType: string;
}
interface ParseResponse {
title: string;
type: MediaType;
source: string;
resolution: number;
modifier: string;
revision: {
version: number;
real: number;
isRepack: boolean;
};
languages: string[];
releaseGroup: string | null;
movieTitles: string[];
year: number;
edition: string | null;
imdbId: string | null;
tmdbId: number;
hardcodedSubs: string | null;
releaseHash: string | null;
episode: EpisodeResponse | null;
}
/**
* Parse a release title - returns quality, resolution, modifier, revision, and languages
* @param title - The release title to parse
* @param type - The media type: 'movie' or 'series'
*/
export async function parse(title: string, type: MediaType): Promise<ParseResult> {
const res = await fetch(`${config.parserUrl}/parse`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, type })
});
if (!res.ok) {
throw new Error(`Parser error: ${res.status}`);
}
const data: ParseResponse = await res.json();
return {
title: data.title,
type: data.type,
source: QualitySource[data.source as keyof typeof QualitySource] ?? QualitySource.Unknown,
resolution: data.resolution as Resolution,
modifier:
QualityModifier[data.modifier as keyof typeof QualityModifier] ?? QualityModifier.None,
revision: data.revision,
languages: data.languages.map(
(l) => Language[l as keyof typeof Language] ?? Language.Unknown
),
releaseGroup: data.releaseGroup,
movieTitles: data.movieTitles,
year: data.year,
edition: data.edition,
imdbId: data.imdbId,
tmdbId: data.tmdbId,
hardcodedSubs: data.hardcodedSubs,
releaseHash: data.releaseHash,
episode: data.episode
? {
seriesTitle: data.episode.seriesTitle,
seasonNumber: data.episode.seasonNumber,
episodeNumbers: data.episode.episodeNumbers,
absoluteEpisodeNumbers: data.episode.absoluteEpisodeNumbers,
airDate: data.episode.airDate,
fullSeason: data.episode.fullSeason,
isPartialSeason: data.episode.isPartialSeason,
isMultiSeason: data.episode.isMultiSeason,
isMiniSeries: data.episode.isMiniSeries,
special: data.episode.special,
releaseType:
ReleaseType[data.episode.releaseType as keyof typeof ReleaseType] ??
ReleaseType.Unknown
}
: null
};
}
/**
* Parse quality info from a release title (legacy - use parse() for full results)
*/
export async function parseQuality(title: string, type: MediaType): Promise<QualityInfo> {
const result = await parse(title, type);
return {
source: result.source,
resolution: result.resolution,
modifier: result.modifier,
revision: result.revision
};
}
/**
* Check parser service health
*/
export async function isParserHealthy(): Promise<boolean> {
try {
const res = await fetch(`${config.parserUrl}/health`);
return res.ok;
} catch {
return false;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Release Title Parser
* Client for the C# parser microservice
*/
export * from './types.ts';
export { parse, parseQuality, isParserHealthy } from './client.ts';

View File

@@ -0,0 +1,154 @@
/**
* Parser Types
* Matches the C# parser microservice types
*/
export enum QualitySource {
Unknown = 0,
Cam = 1,
Telesync = 2,
Telecine = 3,
Workprint = 4,
DVD = 5,
TV = 6,
WebDL = 7,
WebRip = 8,
Bluray = 9
}
export enum QualityModifier {
None = 0,
Regional = 1,
Screener = 2,
RawHD = 3,
BRDisk = 4,
Remux = 5
}
export enum Resolution {
Unknown = 0,
R360p = 360,
R480p = 480,
R540p = 540,
R576p = 576,
R720p = 720,
R1080p = 1080,
R2160p = 2160
}
export enum Language {
Unknown = 0,
English = 1,
French = 2,
Spanish = 3,
German = 4,
Italian = 5,
Danish = 6,
Dutch = 7,
Japanese = 8,
Icelandic = 9,
Chinese = 10,
Russian = 11,
Polish = 12,
Vietnamese = 13,
Swedish = 14,
Norwegian = 15,
Finnish = 16,
Turkish = 17,
Portuguese = 18,
Flemish = 19,
Greek = 20,
Korean = 21,
Hungarian = 22,
Hebrew = 23,
Lithuanian = 24,
Czech = 25,
Hindi = 26,
Romanian = 27,
Thai = 28,
Bulgarian = 29,
PortugueseBR = 30,
Arabic = 31,
Ukrainian = 32,
Persian = 33,
Bengali = 34,
Slovak = 35,
Latvian = 36,
SpanishLatino = 37,
Catalan = 38,
Croatian = 39,
Serbian = 40,
Bosnian = 41,
Estonian = 42,
Tamil = 43,
Indonesian = 44,
Telugu = 45,
Macedonian = 46,
Slovenian = 47,
Malayalam = 48,
Kannada = 49,
Albanian = 50,
Afrikaans = 51,
Marathi = 52,
Tagalog = 53,
Urdu = 54,
Romansh = 55,
Mongolian = 56,
Georgian = 57,
Original = 58
}
export enum ReleaseType {
Unknown = 0,
SingleEpisode = 1,
MultiEpisode = 2,
SeasonPack = 3
}
export interface Revision {
version: number;
real: number;
isRepack: boolean;
}
export interface QualityInfo {
source: QualitySource;
resolution: Resolution;
modifier: QualityModifier;
revision: Revision;
}
export interface EpisodeInfo {
seriesTitle: string | null;
seasonNumber: number;
episodeNumbers: number[];
absoluteEpisodeNumbers: number[];
airDate: string | null;
fullSeason: boolean;
isPartialSeason: boolean;
isMultiSeason: boolean;
isMiniSeries: boolean;
special: boolean;
releaseType: ReleaseType;
}
export type MediaType = 'movie' | 'series';
export interface ParseResult {
title: string;
type: MediaType;
source: QualitySource;
resolution: Resolution;
modifier: QualityModifier;
revision: Revision;
languages: Language[];
releaseGroup: string | null;
movieTitles: string[];
year: number;
edition: string | null;
imdbId: string | null;
tmdbId: number;
hardcodedSubs: string | null;
releaseHash: string | null;
episode: EpisodeInfo | null;
}

View File

@@ -5,6 +5,7 @@
class Config {
private basePath: string;
public readonly timezone: string;
public readonly parserUrl: string;
constructor() {
// Default base path logic:
@@ -24,6 +25,11 @@ class Config {
// 1. Check TZ environment variable
// 2. Fall back to system timezone
this.timezone = Deno.env.get('TZ') || Intl.DateTimeFormat().resolvedOptions().timeZone;
// Parser service configuration
const parserHost = Deno.env.get('PARSER_HOST') || 'localhost';
const parserPort = Deno.env.get('PARSER_PORT') || '5000';
this.parserUrl = `http://${parserHost}:${parserPort}`;
}
/**