Add release parser

This commit is contained in:
TheOtherP 2026-01-16 12:55:52 +01:00
parent 0c5c3f5331
commit 280680a16c
17 changed files with 2318 additions and 4 deletions

View File

@ -84,7 +84,7 @@ jobs:
cache: 'maven'
- name: "Install maven"
run: mvn --batch-mode clean install -DskipTests -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping
run: mvn --batch-mode clean install -DskipTests -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:release-parser
- name: "Create docker network"
run: docker network create systemtest
@ -253,7 +253,7 @@ jobs:
cache: 'maven'
- name: "Install maven"
run: mvn --batch-mode clean install -DskipTests -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:mockserver
run: mvn --batch-mode clean install -DskipTests -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:release-parser,org.nzbhydra:mockserver
- name: "Copy mockserver"
run: |

View File

@ -44,6 +44,15 @@
</processorPath>
<module name="core" />
</profile>
<profile name="Annotation profile for release-parser" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
</processorPath>
<module name="release-parser" />
</profile>
<profile name="Annotation profile for nzbhydra2" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />

View File

@ -15,7 +15,7 @@
<list>
<option value="-q"/>
<option value="-pl"/>
<option value="org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:core"/>
<option value="org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:release-parser,org.nzbhydra:core"/>
<option value="clean"/>
<option value="install"/>
<option value="-B"/>

View File

@ -102,6 +102,11 @@
<artifactId>mapping</artifactId>
<version>8.2.4-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.nzbhydra</groupId>
<artifactId>release-parser</artifactId>
<version>8.2.4-SNAPSHOT</version>
</dependency>
<!-- spring (boot) -->
<dependency>

View File

@ -142,7 +142,7 @@ if (-not $?) {
Write-Host "Building core jar"
exec { mvn -q -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:core clean install -B -T 1C `-DskipTests=true}
exec { mvn -q -pl org.nzbhydra:nzbhydra2,org.nzbhydra:shared,org.nzbhydra:mapping,org.nzbhydra:release-parser,org.nzbhydra:core clean install -B -T 1C `-DskipTests=true}
erase .\releases\generic-release\include\*.jar
copy .\core\target\*-exec.jar .\releases\generic-release\include\
if (-not $?) {

View File

@ -13,6 +13,7 @@
<modules>
<module>mapping</module>
<module>release-parser</module>
</modules>
</project>

View File

@ -0,0 +1,174 @@
# Release Parser
A Java library for parsing movie release names and extracting quality information such as source, resolution, codec, and more.
## Attribution
This library is inspired by and based on the parsing logic from [Radarr](https://github.com/Radarr/Radarr), an open-source movie collection manager. The regex patterns and parsing approach were adapted from Radarr's C# implementation to Java.
Radarr is licensed under the GNU General Public License v3.0.
## Purpose
When downloading movies from various sources, release names follow a common naming convention that encodes quality information:
```
Movie.Title.2023.1080p.BluRay.x264-GROUP
```
This library parses these release names to extract:
- **Movie title and year**
- **Video source** (BluRay, WEB-DL, HDTV, CAM, etc.)
- **Resolution** (480p, 720p, 1080p, 2160p/4K)
- **Video codec** (x264, x265/HEVC, XviD, AV1)
- **Release group**
- **Edition** (Director's Cut, Extended, IMAX, etc.)
- **HDR information** (HDR10, Dolby Vision)
- **Hardcoded subtitles detection**
- **Languages**
### Quality Analysis
The library also provides quality analysis features:
- **Quality ratings** (1-10 scale) to quickly assess release quality
- **Warnings** about poor quality sources (CAM, Telesync) or issues (hardcoded subs)
- **Release comparison** to determine which of two releases is better quality
This is useful for:
- Automatically categorizing movie downloads
- Warning users about poor quality releases before downloading
- Comparing multiple releases to choose the best one
- Building media management applications
## Requirements
- Java 17 or higher
- Maven 3.6+
## Installation
Clone the repository and build with Maven:
```bash
mvn clean install
```
## Usage
### Basic Parsing
```java
import com.releaseparser.parser.ReleaseParser;
import com.releaseparser.model.ReleaseInfo;
ReleaseParser parser = new ReleaseParser();
ReleaseInfo info = parser.parse("The.Matrix.1999.1080p.BluRay.x264-SPARKS");
System.out.println(info.getMovieTitle()); // "The Matrix"
System.out.println(info.getYear()); // 1999
System.out.println(info.getSource()); // Source.BLURAY
System.out.println(info.getResolution()); // Resolution.R1080P
System.out.println(info.getCodec()); // Codec.X264
System.out.println(info.getReleaseGroup()); // "SPARKS"
```
### Quality Analysis
```java
import com.releaseparser.analyzer.QualityAnalyzer;
QualityAnalyzer analyzer = new QualityAnalyzer();
// Get quality rating (1-10)
int rating = analyzer.getQualityRating(info);
String description = analyzer.getQualityDescription(rating);
// rating: 7, description: "Good - High quality release"
// Get warnings
List<QualityWarning> warnings = analyzer.analyze(info);
// [INFO] Blu-ray source - high quality
```
### Detecting Poor Quality Releases
```java
ReleaseInfo camRelease = parser.parse("New.Movie.2024.HDCAM.x264-NOGRP");
List<QualityWarning> warnings = analyzer.analyze(camRelease);
// Outputs:
// [CRITICAL] CAM source - extremely poor quality (camera recording from cinema)
```
### Detecting Hardcoded Subtitles
```java
ReleaseInfo hcRelease = parser.parse("Movie.2024.1080p.BluRay.HC.x264-GROUP");
if (hcRelease.isHardcodedSubs()) {
System.out.println("Warning: This release has hardcoded subtitles!");
}
// Analyzer also warns:
// [WARNING] Contains HARDCODED subtitles - these cannot be disabled
```
### Comparing Releases
```java
ReleaseInfo bluray = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP1");
ReleaseInfo webdl = parser.parse("Movie.2024.1080p.WEB-DL.x264-GROUP2");
ComparisonResult result = analyzer.compare(bluray, webdl);
if (result.hasClearWinner()) {
System.out.println("Better release: " + result.better().getOriginalTitle());
System.out.println("Reasons: " + result.reasons());
}
// Better release: Movie.2024.1080p.BluRay.x264-GROUP1
// Reasons: [Blu-ray source is better than WEB-DL]
```
## Quality Rankings
### Sources (worst to best)
| Rank | Source | Description |
|------|--------|-------------|
| 1 | CAM | Camera recording from cinema |
| 2 | Telesync | Direct audio, CAM video |
| 3 | Telecine | Copied from film reel |
| 4 | Workprint | Unfinished version |
| 5-6 | Screener | Pre-release review copy |
| 7-10 | SDTV/DVD | Standard definition |
| 11-13 | HDTV | HD TV recording |
| 14-15 | WEB-DL/WEBRip | Streaming service |
| 16-17 | BDRip/BRRip | Blu-ray encode |
| 18 | Blu-ray | Direct Blu-ray encode |
| 19 | Remux | Untouched Blu-ray |
### Resolutions
| Resolution | Quality |
|------------|---------|
| 360p-576p | SD (Standard Definition) |
| 720p | HD (High Definition) |
| 1080p | Full HD |
| 2160p/4K | Ultra HD |
## Running the Demo
```bash
mvn compile exec:java -Dexec.mainClass="com.releaseparser.Demo"
```
## Running Tests
```bash
mvn test
```
## License
This project is provided for educational purposes. The parsing logic is derived from Radarr which is licensed under GPL-3.0.

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.nzbhydra</groupId>
<artifactId>shared</artifactId>
<version>8.2.4-SNAPSHOT</version>
</parent>
<artifactId>release-parser</artifactId>
<packaging>jar</packaging>
<name>Release Parser</name>
<description>A library for parsing movie release names and extracting quality data</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.10.2</junit.version>
<assertj.version>3.25.3</assertj.version>
</properties>
<dependencies>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.12.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,87 @@
package com.releaseparser;
import com.releaseparser.analyzer.QualityAnalyzer;
import com.releaseparser.model.ReleaseInfo;
import com.releaseparser.parser.ReleaseParser;
/**
* Demo class showing how to use the release parser library.
*/
public class Demo {
public static void main(String[] args) {
ReleaseParser parser = new ReleaseParser();
QualityAnalyzer analyzer = new QualityAnalyzer();
// Example release titles
String[] titles = {
"Bomb.Guy.2023.IMAX.2160p.UHD.BluRay.Remux.DV.HDR.HEVC.TrueHD.Atmos.7.1-FGT",
"The.Thingy.1999.1080p.BluRay.x264-SPARKS",
"New.Movie.2024.HDCAM.HC.XviD-NOGRP",
"Movie.Title.2023.1080p.WEB-DL.DDP5.1.H.264-GROUP",
"Movie.2024.TS.x264-LOWQ"
};
System.out.println("=".repeat(80));
System.out.println("RELEASE PARSER DEMO");
System.out.println("=".repeat(80));
for (String title : titles) {
System.out.println("\n" + "-".repeat(80));
System.out.println("Parsing: " + title);
System.out.println("-".repeat(80));
// Parse the release
ReleaseInfo info = parser.parse(title);
// Show parsed info
System.out.println("\nParsed Info:");
System.out.println(" Movie: " + info.getMovieTitle() + (info.getYear() != null ? " (" + info.getYear() + ")" : ""));
System.out.println(" Source: " + info.getSource().getDisplayName() + " - " + info.getSource().getDescription());
System.out.println(" Resolution: " + info.getResolution().getDisplayName());
System.out.println(" Codec: " + info.getCodec().getDisplayName());
if (info.getReleaseGroup() != null) {
System.out.println(" Release Group: " + info.getReleaseGroup());
}
if (info.getEdition() != null) {
System.out.println(" Edition: " + info.getEdition());
}
if (info.isHdr()) {
System.out.println(" HDR: Yes" + (info.isDolbyVision() ? " (Dolby Vision)" : ""));
}
if (info.isRemux()) {
System.out.println(" Remux: Yes");
}
// Get quality rating
int rating = analyzer.getQualityRating(info);
String description = analyzer.getQualityDescription(rating);
System.out.println("\nQuality Rating: " + rating + "/10 - " + description);
// Show warnings
var warnings = analyzer.analyze(info);
if (!warnings.isEmpty()) {
System.out.println("\nWarnings:");
for (var warning : warnings) {
System.out.println(" " + warning);
}
}
}
// Compare two releases
System.out.println("\n" + "=".repeat(80));
System.out.println("QUALITY COMPARISON");
System.out.println("=".repeat(80));
ReleaseInfo bluray = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP1");
ReleaseInfo cam = parser.parse("Movie.2024.CAM.x264-GROUP2");
System.out.println("\nComparing:");
System.out.println(" 1. " + bluray.getOriginalTitle());
System.out.println(" 2. " + cam.getOriginalTitle());
var comparison = analyzer.compare(bluray, cam);
System.out.println("\nResult:");
System.out.println(comparison);
}
}

View File

@ -0,0 +1,340 @@
package com.releaseparser.analyzer;
import com.releaseparser.model.*;
import java.util.ArrayList;
import java.util.List;
/**
* Analyzes release quality and provides warnings and comparisons.
*/
public class QualityAnalyzer {
/**
* Severity level for quality warnings.
*/
public enum Severity {
INFO,
WARNING,
CRITICAL
}
/**
* A quality warning or information message.
*/
public record QualityWarning(Severity severity, String message) {
@Override
public String toString() {
return "[" + severity + "] " + message;
}
}
/**
* Result of comparing two releases.
*/
public record ComparisonResult(
ReleaseInfo better,
ReleaseInfo worse,
List<String> reasons
) {
public boolean hasClearWinner() {
return better != null && worse != null;
}
@Override
public String toString() {
if (!hasClearWinner()) {
return "No clear winner - releases are similar quality";
}
StringBuilder sb = new StringBuilder();
sb.append("Better: ").append(better.getMovieTitle()).append("\n");
sb.append("Reasons:\n");
for (String reason : reasons) {
sb.append(" - ").append(reason).append("\n");
}
return sb.toString();
}
}
/**
* Analyze a release and return any quality warnings.
*
* @param info the release info to analyze
* @return list of quality warnings
*/
public List<QualityWarning> analyze(ReleaseInfo info) {
List<QualityWarning> warnings = new ArrayList<>();
// Check source quality
analyzeSource(info, warnings);
// Check resolution
analyzeResolution(info, warnings);
// Check codec
analyzeCodec(info, warnings);
// Check hardcoded subs
analyzeHardcodedSubs(info, warnings);
// Check for positive indicators
analyzePositiveIndicators(info, warnings);
return warnings;
}
private void analyzeSource(ReleaseInfo info, List<QualityWarning> warnings) {
Source source = info.getSource();
switch (source) {
case CAM -> warnings.add(new QualityWarning(Severity.CRITICAL,
"CAM source - extremely poor quality (camera recording from cinema)"));
case TELESYNC -> warnings.add(new QualityWarning(Severity.CRITICAL,
"Telesync source - very poor video quality"));
case TELECINE -> warnings.add(new QualityWarning(Severity.CRITICAL,
"Telecine source - poor quality (copied from film reel)"));
case WORKPRINT -> warnings.add(new QualityWarning(Severity.CRITICAL,
"Workprint - unfinished version, may have missing scenes or effects"));
case SCREENER -> warnings.add(new QualityWarning(Severity.WARNING,
"Screener source - may have watermarks or quality issues"));
case DVDSCR -> warnings.add(new QualityWarning(Severity.WARNING,
"DVD Screener - pre-release copy, may have watermarks"));
case SDTV, PDTV, DSR, TVRIP -> warnings.add(new QualityWarning(Severity.INFO,
source.getDisplayName() + " source - standard definition TV quality"));
case DVD -> warnings.add(new QualityWarning(Severity.INFO,
"DVD source - standard definition (480p typical)"));
case UNKNOWN -> warnings.add(new QualityWarning(Severity.WARNING,
"Unknown source - quality cannot be determined"));
case REMUX -> warnings.add(new QualityWarning(Severity.INFO,
"Remux - highest possible quality (untouched video/audio from disc)"));
case BLURAY -> warnings.add(new QualityWarning(Severity.INFO,
"Blu-ray source - high quality"));
default -> {
// Good sources don't need warnings
}
}
}
private void analyzeResolution(ReleaseInfo info, List<QualityWarning> warnings) {
Resolution resolution = info.getResolution();
if (resolution == Resolution.UNKNOWN) {
warnings.add(new QualityWarning(Severity.INFO,
"Resolution not detected in title"));
return;
}
if (resolution.isSD()) {
warnings.add(new QualityWarning(Severity.WARNING,
"Standard definition resolution (" + resolution.getDisplayName() + ") - consider HD alternatives"));
} else if (resolution.isUHD()) {
warnings.add(new QualityWarning(Severity.INFO,
"4K UHD resolution - excellent quality (ensure your display supports it)"));
}
}
private void analyzeCodec(ReleaseInfo info, List<QualityWarning> warnings) {
Codec codec = info.getCodec();
if (codec.isLegacy()) {
warnings.add(new QualityWarning(Severity.WARNING,
codec.getDisplayName() + " is a legacy codec - newer codecs (x264/x265) offer better quality"));
}
}
private void analyzeHardcodedSubs(ReleaseInfo info, List<QualityWarning> warnings) {
if (info.isHardcodedSubs()) {
String message = "Contains HARDCODED subtitles - these cannot be disabled";
if (info.getHardcodedSubsLanguage() != null) {
message += " (Language: " + info.getHardcodedSubsLanguage() + ")";
}
warnings.add(new QualityWarning(Severity.WARNING, message));
}
}
private void analyzePositiveIndicators(ReleaseInfo info, List<QualityWarning> warnings) {
if (info.isProper()) {
warnings.add(new QualityWarning(Severity.INFO,
"PROPER release - fixes issues from previous release"));
}
if (info.isRepack()) {
warnings.add(new QualityWarning(Severity.INFO,
"REPACK release - fixes issues from initial release by same group"));
}
if (info.getVersion() > 1) {
warnings.add(new QualityWarning(Severity.INFO,
"Version " + info.getVersion() + " - improved release"));
}
if (info.isHdr()) {
warnings.add(new QualityWarning(Severity.INFO,
"HDR content - enhanced color/brightness (requires HDR display)"));
}
if (info.isDolbyVision()) {
warnings.add(new QualityWarning(Severity.INFO,
"Dolby Vision - premium HDR format (requires compatible display)"));
}
if (info.getEdition() != null) {
warnings.add(new QualityWarning(Severity.INFO,
"Special edition: " + info.getEdition()));
}
}
/**
* Compare two releases and determine which is better quality.
*
* @param release1 first release
* @param release2 second release
* @return comparison result
*/
public ComparisonResult compare(ReleaseInfo release1, ReleaseInfo release2) {
List<String> reasons = new ArrayList<>();
int score1 = 0;
int score2 = 0;
// Compare source (most important)
int sourceCompare = compareSource(release1, release2, reasons);
score1 += sourceCompare > 0 ? 3 : 0;
score2 += sourceCompare < 0 ? 3 : 0;
// Compare resolution
int resolutionCompare = compareResolution(release1, release2, reasons);
score1 += resolutionCompare > 0 ? 2 : 0;
score2 += resolutionCompare < 0 ? 2 : 0;
// Compare codec
int codecCompare = compareCodec(release1, release2, reasons);
score1 += codecCompare > 0 ? 1 : 0;
score2 += codecCompare < 0 ? 1 : 0;
// Penalize hardcoded subs
if (release1.isHardcodedSubs() && !release2.isHardcodedSubs()) {
score2 += 1;
reasons.add("Release 2 has no hardcoded subtitles");
} else if (!release1.isHardcodedSubs() && release2.isHardcodedSubs()) {
score1 += 1;
reasons.add("Release 1 has no hardcoded subtitles");
}
// Prefer HDR
if (release1.isHdr() && !release2.isHdr()) {
score1 += 1;
reasons.add("Release 1 has HDR");
} else if (!release1.isHdr() && release2.isHdr()) {
score2 += 1;
reasons.add("Release 2 has HDR");
}
// Prefer PROPER/REPACK
if ((release1.isProper() || release1.isRepack()) && !(release2.isProper() || release2.isRepack())) {
score1 += 1;
reasons.add("Release 1 is PROPER/REPACK");
} else if (!(release1.isProper() || release1.isRepack()) && (release2.isProper() || release2.isRepack())) {
score2 += 1;
reasons.add("Release 2 is PROPER/REPACK");
}
if (score1 > score2) {
return new ComparisonResult(release1, release2, reasons);
} else if (score2 > score1) {
return new ComparisonResult(release2, release1, reasons);
} else {
return new ComparisonResult(null, null, List.of("Releases are similar quality"));
}
}
private int compareSource(ReleaseInfo r1, ReleaseInfo r2, List<String> reasons) {
Source s1 = r1.getSource();
Source s2 = r2.getSource();
if (s1.isBetterThan(s2)) {
reasons.add(s1.getDisplayName() + " source is better than " + s2.getDisplayName());
return 1;
} else if (s2.isBetterThan(s1)) {
reasons.add(s2.getDisplayName() + " source is better than " + s1.getDisplayName());
return -1;
}
return 0;
}
private int compareResolution(ReleaseInfo r1, ReleaseInfo r2, List<String> reasons) {
Resolution res1 = r1.getResolution();
Resolution res2 = r2.getResolution();
if (res1.isBetterThan(res2)) {
reasons.add(res1.getDisplayName() + " resolution is better than " + res2.getDisplayName());
return 1;
} else if (res2.isBetterThan(res1)) {
reasons.add(res2.getDisplayName() + " resolution is better than " + res1.getDisplayName());
return -1;
}
return 0;
}
private int compareCodec(ReleaseInfo r1, ReleaseInfo r2, List<String> reasons) {
Codec c1 = r1.getCodec();
Codec c2 = r2.getCodec();
if (c1.getEfficiencyRank() > c2.getEfficiencyRank()) {
reasons.add(c1.getDisplayName() + " codec is more efficient than " + c2.getDisplayName());
return 1;
} else if (c2.getEfficiencyRank() > c1.getEfficiencyRank()) {
reasons.add(c2.getDisplayName() + " codec is more efficient than " + c1.getDisplayName());
return -1;
}
return 0;
}
/**
* Get a simple quality rating for a release (1-10 scale).
*
* @param info the release info
* @return quality rating from 1 (worst) to 10 (best)
*/
public int getQualityRating(ReleaseInfo info) {
int rating = 5; // Base rating
// Source contributes most (up to 4 points)
rating += (info.getSource().getQualityRank() - 10) / 5;
// Resolution (up to 2 points)
if (info.getResolution().isUHD()) rating += 2;
else if (info.getResolution().isFullHD()) rating += 1;
else if (info.getResolution().isSD()) rating -= 1;
// Codec (up to 1 point)
if (info.getCodec().isHEVC() || info.getCodec() == Codec.AV1) rating += 1;
else if (info.getCodec().isLegacy()) rating -= 1;
// Penalties
if (info.isHardcodedSubs()) rating -= 1;
if (info.getSource().isPoorQuality()) rating -= 2;
// Bonuses
if (info.isHdr()) rating += 1;
if (info.isRemux()) rating += 1;
return Math.max(1, Math.min(10, rating));
}
/**
* Get a text description of the quality level.
*
* @param rating the quality rating (1-10)
* @return text description
*/
public String getQualityDescription(int rating) {
return switch (rating) {
case 1, 2 -> "Very Poor - Avoid if possible";
case 3, 4 -> "Poor - Low quality release";
case 5, 6 -> "Average - Acceptable quality";
case 7, 8 -> "Good - High quality release";
case 9, 10 -> "Excellent - Premium quality release";
default -> "Unknown";
};
}
}

View File

@ -0,0 +1,50 @@
package com.releaseparser.model;
/**
* Video codec types with efficiency rankings.
* Higher efficiency means better quality at same bitrate, or smaller file at same quality.
*/
public enum Codec {
UNKNOWN(0, "Unknown", "Unknown codec"),
DIVX(1, "DivX", "Legacy MPEG-4 Part 2 codec"),
XVID(2, "XviD", "Open source MPEG-4 Part 2 codec"),
H264(3, "H.264", "AVC/H.264 - widely compatible"),
X264(4, "x264", "High quality H.264 encoder"),
H265(5, "H.265", "HEVC - better compression than H.264"),
X265(6, "x265", "High quality HEVC encoder"),
AV1(7, "AV1", "Next-gen open codec - best compression");
private final int efficiencyRank;
private final String displayName;
private final String description;
Codec(int efficiencyRank, String displayName, String description) {
this.efficiencyRank = efficiencyRank;
this.displayName = displayName;
this.description = description;
}
public int getEfficiencyRank() {
return efficiencyRank;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public boolean isModern() {
return this.efficiencyRank >= H264.efficiencyRank;
}
public boolean isLegacy() {
return this == DIVX || this == XVID;
}
public boolean isHEVC() {
return this == H265 || this == X265;
}
}

View File

@ -0,0 +1,230 @@
package com.releaseparser.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Contains all parsed information from a movie release title.
*/
public class ReleaseInfo {
private String originalTitle;
private String movieTitle;
private Integer year;
private Source source;
private Resolution resolution;
private Codec codec;
private String releaseGroup;
private boolean hardcodedSubs;
private String hardcodedSubsLanguage;
private boolean proper;
private boolean repack;
private int version;
private String edition;
private boolean remux;
private boolean hdr;
private boolean dolbyVision;
private boolean threeDimensional;
private List<String> languages;
public ReleaseInfo() {
this.source = Source.UNKNOWN;
this.resolution = Resolution.UNKNOWN;
this.codec = Codec.UNKNOWN;
this.version = 1;
this.languages = new ArrayList<>();
}
// Getters and Setters
public String getOriginalTitle() {
return originalTitle;
}
public void setOriginalTitle(String originalTitle) {
this.originalTitle = originalTitle;
}
public String getMovieTitle() {
return movieTitle;
}
public void setMovieTitle(String movieTitle) {
this.movieTitle = movieTitle;
}
public Integer getYear() {
return year;
}
public void setYear(Integer year) {
this.year = year;
}
public Source getSource() {
return source;
}
public void setSource(Source source) {
this.source = source;
}
public Resolution getResolution() {
return resolution;
}
public void setResolution(Resolution resolution) {
this.resolution = resolution;
}
public Codec getCodec() {
return codec;
}
public void setCodec(Codec codec) {
this.codec = codec;
}
public String getReleaseGroup() {
return releaseGroup;
}
public void setReleaseGroup(String releaseGroup) {
this.releaseGroup = releaseGroup;
}
public boolean isHardcodedSubs() {
return hardcodedSubs;
}
public void setHardcodedSubs(boolean hardcodedSubs) {
this.hardcodedSubs = hardcodedSubs;
}
public String getHardcodedSubsLanguage() {
return hardcodedSubsLanguage;
}
public void setHardcodedSubsLanguage(String hardcodedSubsLanguage) {
this.hardcodedSubsLanguage = hardcodedSubsLanguage;
}
public boolean isProper() {
return proper;
}
public void setProper(boolean proper) {
this.proper = proper;
}
public boolean isRepack() {
return repack;
}
public void setRepack(boolean repack) {
this.repack = repack;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getEdition() {
return edition;
}
public void setEdition(String edition) {
this.edition = edition;
}
public boolean isRemux() {
return remux;
}
public void setRemux(boolean remux) {
this.remux = remux;
}
public boolean isHdr() {
return hdr;
}
public void setHdr(boolean hdr) {
this.hdr = hdr;
}
public boolean isDolbyVision() {
return dolbyVision;
}
public void setDolbyVision(boolean dolbyVision) {
this.dolbyVision = dolbyVision;
}
public boolean isThreeDimensional() {
return threeDimensional;
}
public void setThreeDimensional(boolean threeDimensional) {
this.threeDimensional = threeDimensional;
}
public List<String> getLanguages() {
return languages;
}
public void setLanguages(List<String> languages) {
this.languages = languages;
}
public void addLanguage(String language) {
if (!this.languages.contains(language)) {
this.languages.add(language);
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("ReleaseInfo{\n");
sb.append(" originalTitle='").append(originalTitle).append("'\n");
sb.append(" movieTitle='").append(movieTitle).append("'\n");
if (year != null) sb.append(" year=").append(year).append("\n");
sb.append(" source=").append(source.getDisplayName()).append("\n");
sb.append(" resolution=").append(resolution.getDisplayName()).append("\n");
sb.append(" codec=").append(codec.getDisplayName()).append("\n");
if (releaseGroup != null) sb.append(" releaseGroup='").append(releaseGroup).append("'\n");
if (hardcodedSubs) {
sb.append(" hardcodedSubs=true");
if (hardcodedSubsLanguage != null) sb.append(" (").append(hardcodedSubsLanguage).append(")");
sb.append("\n");
}
if (proper) sb.append(" proper=true\n");
if (repack) sb.append(" repack=true\n");
if (version > 1) sb.append(" version=").append(version).append("\n");
if (edition != null) sb.append(" edition='").append(edition).append("'\n");
if (remux) sb.append(" remux=true\n");
if (hdr) sb.append(" hdr=true\n");
if (dolbyVision) sb.append(" dolbyVision=true\n");
if (threeDimensional) sb.append(" 3D=true\n");
if (!languages.isEmpty()) sb.append(" languages=").append(languages).append("\n");
sb.append("}");
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ReleaseInfo that = (ReleaseInfo) o;
return Objects.equals(originalTitle, that.originalTitle);
}
@Override
public int hashCode() {
return Objects.hash(originalTitle);
}
}

View File

@ -0,0 +1,67 @@
package com.releaseparser.model;
/**
* Video resolution types ordered by quality (lowest to highest).
*/
public enum Resolution {
UNKNOWN(0, "Unknown", 0, 0),
R360P(1, "360p", 640, 360),
R480P(2, "480p", 854, 480),
R540P(3, "540p", 960, 540),
R576P(4, "576p", 1024, 576),
R720P(5, "720p", 1280, 720),
R1080P(6, "1080p", 1920, 1080),
R2160P(7, "2160p", 3840, 2160);
private final int qualityRank;
private final String displayName;
private final int width;
private final int height;
Resolution(int qualityRank, String displayName, int width, int height) {
this.qualityRank = qualityRank;
this.displayName = displayName;
this.width = width;
this.height = height;
}
public int getQualityRank() {
return qualityRank;
}
public String getDisplayName() {
return displayName;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public boolean isBetterThan(Resolution other) {
return this.qualityRank > other.qualityRank;
}
public boolean isWorseThan(Resolution other) {
return this.qualityRank < other.qualityRank;
}
public boolean isHD() {
return this.qualityRank >= R720P.qualityRank;
}
public boolean isFullHD() {
return this.qualityRank >= R1080P.qualityRank;
}
public boolean isUHD() {
return this == R2160P;
}
public boolean isSD() {
return this.qualityRank > 0 && this.qualityRank < R720P.qualityRank;
}
}

View File

@ -0,0 +1,70 @@
package com.releaseparser.model;
/**
* Video source types ordered by quality (lowest to highest).
* Higher ordinal values indicate better quality.
*/
public enum Source {
UNKNOWN(0, "Unknown", "Unknown source quality"),
CAM(1, "CAM", "Camera recording from cinema - very poor quality"),
TELESYNC(2, "Telesync", "Audio from direct source, video from CAM - poor quality"),
TELECINE(3, "Telecine", "Copied from film reel - poor quality"),
WORKPRINT(4, "Workprint", "Unfinished version leaked - poor quality"),
SCREENER(5, "Screener", "Pre-release copy for reviewers - moderate quality"),
DVDSCR(6, "DVD Screener", "DVD sent to reviewers - moderate quality"),
SDTV(7, "SDTV", "Standard definition TV recording"),
PDTV(8, "PDTV", "Pure Digital TV recording"),
DSR(9, "DSR", "Digital satellite rip"),
TVRIP(10, "TVRip", "Captured from TV broadcast"),
DVD(11, "DVD", "Standard DVD rip"),
DVDR(12, "DVD-R", "Full DVD backup"),
HDTV(13, "HDTV", "High definition TV recording"),
WEBDL(14, "WEB-DL", "Downloaded from streaming service - no re-encoding"),
WEBRIP(15, "WEBRip", "Screen captured from streaming service"),
BDRIP(16, "BDRip", "Encoded from Blu-ray source"),
BRRIP(17, "BRRip", "Re-encoded from BDRip"),
BLURAY(18, "Blu-ray", "Direct Blu-ray encode - high quality"),
REMUX(19, "Remux", "Untouched video/audio from Blu-ray - highest quality");
private final int qualityRank;
private final String displayName;
private final String description;
Source(int qualityRank, String displayName, String description) {
this.qualityRank = qualityRank;
this.displayName = displayName;
this.description = description;
}
public int getQualityRank() {
return qualityRank;
}
public String getDisplayName() {
return displayName;
}
public String getDescription() {
return description;
}
public boolean isBetterThan(Source other) {
return this.qualityRank > other.qualityRank;
}
public boolean isWorseThan(Source other) {
return this.qualityRank < other.qualityRank;
}
public boolean isPoorQuality() {
return this.qualityRank <= SCREENER.qualityRank;
}
public boolean isHighQuality() {
return this.qualityRank >= BLURAY.qualityRank;
}
public boolean isStreamingSource() {
return this == WEBDL || this == WEBRIP;
}
}

View File

@ -0,0 +1,442 @@
package com.releaseparser.parser;
import com.releaseparser.model.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Parses movie release titles and extracts quality information.
* Based on parsing patterns from Radarr.
*/
public class ReleaseParser {
// Source patterns (ordered for priority matching)
private static final Pattern REMUX_PATTERN = Pattern.compile(
"\\b(?<remux>(?:BD|UHD)?[-.]?Remux)\\b",
Pattern.CASE_INSENSITIVE
);
private static final Pattern SOURCE_PATTERN = Pattern.compile(
"(?<bluray>M?Blu[-_. ]?Ray|HD[-_. ]?DVD|BD(?!$)|UHD2?BD|BDISO|BDMux|BD25|BD50|BR[-_. ]?DISK)|" +
"(?<webdl>WEB[-_. ]?DL(?:mux)?|AmazonHD|AmazonSD|iTunesHD|MaxdomeHD|NetflixU?HD|WebHD|HBOMaxHD|DisneyHD|" +
"[. ]WEB[. ](?:[xh][ .]?26[45]|AVC|HEVC|DDP?5[. ]1)|[. ](?-i:WEB)$|(?:\\d{3,4}0p)[-. ](?:Hybrid[-_. ]?)?WEB[-. ]|" +
"[-. ]WEB[-. ]\\d{3,4}0p|\\b\\s/\\sWEB\\s/\\s\\b|(?:AMZN|NF|DP)[. -]WEB[. -](?!Rip))|" +
"(?<webrip>WebRip|Web-Rip|WEBMux)|" +
"(?<hdtv>HDTV)|" +
"(?<bdrip>BDRip|BDLight|HD[-_. ]?DVDRip|UHDBDRip)|" +
"(?<brrip>BRRip)|" +
"(?<scr>DVDSCR|DVDSCREENER|SCR|SCREENER)|" +
"(?<dvd>DVDRip|xvidvd)|" +
"(?<dvdr>\\d?x?M?DVD-?[R59]|DVD(?!SCR|SCREEN|Rip))|" +
"(?<dsr>WS[-_. ]DSR|DSR)|" +
"(?<ts>TS[-_. ]|TELESYNCH?|HD-TS|HDTS|PDVD|TSRip|HDTSRip)|" +
"(?<tc>TC|TELECINE|HD-TC|HDTC)|" +
"(?<cam>CAMRIP|(?:NEW)?CAM|HD-?CAM(?:Rip)?|HQCAM)|" +
"(?<wp>WORKPRINT|WP)|" +
"(?<pdtv>PDTV)|" +
"(?<sdtv>SDTV)|" +
"(?<tvrip>TVRip)",
Pattern.CASE_INSENSITIVE
);
// Resolution pattern
private static final Pattern RESOLUTION_PATTERN = Pattern.compile(
"\\b(?:" +
"(?<R360p>360p)|" +
"(?<R480p>480p|480i|640x480|848x480)|" +
"(?<R540p>540p)|" +
"(?<R576p>576p)|" +
"(?<R720p>720p|1280x720|960p)|" +
"(?<R1080p>1080p|1920x1080|1440p|FHD|1080i|4kto1080p)|" +
"(?<R2160p>2160p|3840x2160|4k[-_. ]?(?:UHD|HEVC|BD|H\\.?265)?|(?:UHD|HEVC|BD|H\\.?265)[-_. ]4k|UHD)" +
")\\b",
Pattern.CASE_INSENSITIVE
);
// Codec patterns
private static final Pattern CODEC_PATTERN = Pattern.compile(
"\\b(?:" +
"(?<x265>[xh][-_. ]?265|HEVC)|" +
"(?<x264>[xh][-_. ]?264|AVC)|" +
"(?<xvidhd>XvidHD)|" +
"(?<xvid>X-?vid)|" +
"(?<divx>divx)|" +
"(?<av1>AV1)" +
")\\b",
Pattern.CASE_INSENSITIVE
);
// Hardcoded subs pattern
private static final Pattern HARDCODED_SUBS_PATTERN = Pattern.compile(
"\\b(?:" +
"(?<hcsub>(?<lang>\\w+)?(?<!SOFT|MULTI|HORRIBLE)SUBS?)|" +
"(?<hc>HC|SUBBED)" +
")\\b",
Pattern.CASE_INSENSITIVE
);
// Release group pattern
private static final Pattern RELEASE_GROUP_PATTERN = Pattern.compile(
"-(?<releasegroup>[a-z0-9]+(?:-[a-z0-9]+)?)(?:\\b|[-._ ]|$)",
Pattern.CASE_INSENSITIVE
);
// Title patterns
private static final Pattern TITLE_YEAR_PATTERN = Pattern.compile(
"^(?<title>.+?)[._\\s-]+(?<year>(?:19|20)\\d{2})(?:[._\\s-]|$)",
Pattern.CASE_INSENSITIVE
);
private static final Pattern TITLE_ONLY_PATTERN = Pattern.compile(
"^(?<title>.+?)(?:[._\\s-]+(?:480p|540p|576p|720p|1080p|2160p|4k|HDTV|WEB|BluRay|BDRip|DVDRip))",
Pattern.CASE_INSENSITIVE
);
// Version patterns
private static final Pattern VERSION_PATTERN = Pattern.compile(
"\\bv(?<version>\\d)\\b",
Pattern.CASE_INSENSITIVE
);
private static final Pattern PROPER_PATTERN = Pattern.compile(
"\\b(?<proper>PROPER)\\b",
Pattern.CASE_INSENSITIVE
);
private static final Pattern REPACK_PATTERN = Pattern.compile(
"\\b(?<repack>REPACK|RERIP)\\b",
Pattern.CASE_INSENSITIVE
);
// Edition patterns
private static final Pattern EDITION_PATTERN = Pattern.compile(
"\\b(?<edition>" +
"(?:Director'?s?|Collector'?s?|Theatrical|Ultimate|Extended|Rogue|International|" +
"Diamond|Anniversary|Criterion|Unrated|Uncut|Final|Remastered|Special|Limited|" +
"IMAX|Cinema|Restored)[._\\s-]*(Cut|Edition|Version)?|" +
"(?:Special|Extended|Unrated|Uncut)[._\\s-]*Edition|" +
"(?:2|3|4|5)in1" +
")\\b",
Pattern.CASE_INSENSITIVE
);
// HDR patterns
private static final Pattern HDR_PATTERN = Pattern.compile(
"\\b(?<hdr>HDR10(?:Plus|\\+)?|HDR|DV|DoVi|Dolby[-_. ]?Vision)\\b",
Pattern.CASE_INSENSITIVE
);
// 3D pattern
private static final Pattern THREE_D_PATTERN = Pattern.compile(
"\\b(?<threeD>3D|SBS|H[-_.]?SBS|H[-_.]?OU)\\b",
Pattern.CASE_INSENSITIVE
);
// Language patterns
private static final Pattern LANGUAGE_PATTERN = Pattern.compile(
"\\b(?:" +
"(?<multi>MULTI)|" +
"(?<english>ENGLISH|ENG)|" +
"(?<french>FRENCH|TRUEFRENCH|VFF|VFQ|VFI|VF2|FRA?)|" +
"(?<spanish>SPANISH|ESPANOL|ESP)|" +
"(?<german>GERMAN|GER)|" +
"(?<italian>ITALIAN|ITA)|" +
"(?<dutch>DUTCH|FLEMISH|NL)|" +
"(?<danish>DANISH|DAN)|" +
"(?<finnish>FINNISH|FIN)|" +
"(?<norwegian>NORWEGIAN|NOR)|" +
"(?<swedish>SWEDISH|SWE)|" +
"(?<russian>RUSSIAN|RUS)|" +
"(?<polish>POLISH|POL|PL)|" +
"(?<portuguese>PORTUGUESE|POR)|" +
"(?<chinese>CHINESE|CHI|MANDARIN|CANTONESE)|" +
"(?<japanese>JAPANESE|JPN?)|" +
"(?<korean>KOREAN|KOR)|" +
"(?<hindi>HINDI|HIN)|" +
"(?<arabic>ARABIC|ARA)|" +
"(?<hebrew>HEBREW|HEB)|" +
"(?<greek>GREEK|GRE)|" +
"(?<turkish>TURKISH|TUR)|" +
"(?<thai>THAI|THA)" +
")\\b",
Pattern.CASE_INSENSITIVE
);
// Website prefix/postfix cleanup
private static final Pattern WEBSITE_PREFIX_PATTERN = Pattern.compile(
"^\\[?(?:www\\.)?[-a-z0-9]+\\.(com|net|org|info|tv|cc|co|uk|ws)\\]?[-_. ]",
Pattern.CASE_INSENSITIVE
);
private static final Pattern TORRENT_SUFFIX_PATTERN = Pattern.compile(
"\\[(?:ettv|rartv|rarbg|cttv|publichd|eztv|yify|yts)\\]$",
Pattern.CASE_INSENSITIVE
);
/**
* Parse a movie release title and extract all quality information.
*
* @param releaseTitle the release title to parse
* @return ReleaseInfo containing parsed data
*/
public ReleaseInfo parse(String releaseTitle) {
if (releaseTitle == null || releaseTitle.isBlank()) {
return new ReleaseInfo();
}
ReleaseInfo info = new ReleaseInfo();
info.setOriginalTitle(releaseTitle);
// Clean the title
String cleanedTitle = cleanTitle(releaseTitle);
// Parse all components
parseSource(cleanedTitle, info);
parseResolution(cleanedTitle, info);
parseCodec(cleanedTitle, info);
parseHardcodedSubs(cleanedTitle, info);
parseReleaseGroup(cleanedTitle, info);
parseVersionInfo(cleanedTitle, info);
parseEdition(cleanedTitle, info);
parseHDR(cleanedTitle, info);
parse3D(cleanedTitle, info);
parseLanguages(cleanedTitle, info);
parseMovieTitle(cleanedTitle, info);
return info;
}
private String cleanTitle(String title) {
String cleaned = title;
// Remove website prefixes
cleaned = WEBSITE_PREFIX_PATTERN.matcher(cleaned).replaceFirst("");
// Remove torrent suffixes
cleaned = TORRENT_SUFFIX_PATTERN.matcher(cleaned).replaceFirst("");
return cleaned.trim();
}
private void parseSource(String title, ReleaseInfo info) {
// Check for remux first (highest quality indicator)
Matcher remuxMatcher = REMUX_PATTERN.matcher(title);
if (remuxMatcher.find()) {
info.setRemux(true);
info.setSource(Source.REMUX);
return;
}
Matcher matcher = SOURCE_PATTERN.matcher(title);
if (matcher.find()) {
if (matcher.group("bluray") != null) {
info.setSource(Source.BLURAY);
} else if (matcher.group("webdl") != null) {
info.setSource(Source.WEBDL);
} else if (matcher.group("webrip") != null) {
info.setSource(Source.WEBRIP);
} else if (matcher.group("hdtv") != null) {
info.setSource(Source.HDTV);
} else if (matcher.group("bdrip") != null) {
info.setSource(Source.BDRIP);
} else if (matcher.group("brrip") != null) {
info.setSource(Source.BRRIP);
} else if (matcher.group("dvdr") != null) {
info.setSource(Source.DVDR);
} else if (matcher.group("dvd") != null) {
info.setSource(Source.DVD);
} else if (matcher.group("dsr") != null) {
info.setSource(Source.DSR);
} else if (matcher.group("scr") != null) {
info.setSource(Source.SCREENER);
} else if (matcher.group("ts") != null) {
info.setSource(Source.TELESYNC);
} else if (matcher.group("tc") != null) {
info.setSource(Source.TELECINE);
} else if (matcher.group("cam") != null) {
info.setSource(Source.CAM);
} else if (matcher.group("wp") != null) {
info.setSource(Source.WORKPRINT);
} else if (matcher.group("pdtv") != null) {
info.setSource(Source.PDTV);
} else if (matcher.group("sdtv") != null) {
info.setSource(Source.SDTV);
} else if (matcher.group("tvrip") != null) {
info.setSource(Source.TVRIP);
}
}
}
private void parseResolution(String title, ReleaseInfo info) {
Matcher matcher = RESOLUTION_PATTERN.matcher(title);
if (matcher.find()) {
if (matcher.group("R360p") != null) {
info.setResolution(Resolution.R360P);
} else if (matcher.group("R480p") != null) {
info.setResolution(Resolution.R480P);
} else if (matcher.group("R540p") != null) {
info.setResolution(Resolution.R540P);
} else if (matcher.group("R576p") != null) {
info.setResolution(Resolution.R576P);
} else if (matcher.group("R720p") != null) {
info.setResolution(Resolution.R720P);
} else if (matcher.group("R1080p") != null) {
info.setResolution(Resolution.R1080P);
} else if (matcher.group("R2160p") != null) {
info.setResolution(Resolution.R2160P);
}
}
}
private void parseCodec(String title, ReleaseInfo info) {
Matcher matcher = CODEC_PATTERN.matcher(title);
if (matcher.find()) {
if (matcher.group("x265") != null) {
info.setCodec(Codec.X265);
} else if (matcher.group("x264") != null) {
info.setCodec(Codec.X264);
} else if (matcher.group("xvidhd") != null || matcher.group("xvid") != null) {
info.setCodec(Codec.XVID);
} else if (matcher.group("divx") != null) {
info.setCodec(Codec.DIVX);
} else if (matcher.group("av1") != null) {
info.setCodec(Codec.AV1);
}
}
}
private void parseHardcodedSubs(String title, ReleaseInfo info) {
Matcher matcher = HARDCODED_SUBS_PATTERN.matcher(title);
if (matcher.find()) {
info.setHardcodedSubs(true);
if (matcher.group("lang") != null) {
info.setHardcodedSubsLanguage(matcher.group("lang"));
}
}
}
private void parseReleaseGroup(String title, ReleaseInfo info) {
// Get the last match to avoid false positives from the title
Matcher matcher = RELEASE_GROUP_PATTERN.matcher(title);
String lastGroup = null;
while (matcher.find()) {
String group = matcher.group("releasegroup");
// Filter out false positives (quality indicators, resolutions, etc.)
if (!isQualityIndicator(group)) {
lastGroup = group;
}
}
if (lastGroup != null) {
info.setReleaseGroup(lastGroup);
}
}
private boolean isQualityIndicator(String group) {
String upper = group.toUpperCase();
return upper.matches("(?:480P|720P|1080P|2160P|HDTV|WEB|DL|RIP|DTS|HD|MA|X264|X265|HEVC|AVC|AAC|AC3|DD5|ATMOS|TRUEHD|FLAC)");
}
private void parseVersionInfo(String title, ReleaseInfo info) {
Matcher versionMatcher = VERSION_PATTERN.matcher(title);
if (versionMatcher.find()) {
info.setVersion(Integer.parseInt(versionMatcher.group("version")));
}
if (PROPER_PATTERN.matcher(title).find()) {
info.setProper(true);
}
if (REPACK_PATTERN.matcher(title).find()) {
info.setRepack(true);
}
}
private void parseEdition(String title, ReleaseInfo info) {
Matcher matcher = EDITION_PATTERN.matcher(title);
if (matcher.find()) {
info.setEdition(matcher.group("edition").trim());
}
}
private void parseHDR(String title, ReleaseInfo info) {
Matcher matcher = HDR_PATTERN.matcher(title);
if (matcher.find()) {
String hdrType = matcher.group("hdr").toUpperCase();
if (hdrType.contains("DV") || hdrType.contains("DOVI") || hdrType.contains("DOLBY")) {
info.setDolbyVision(true);
}
info.setHdr(true);
}
}
private void parse3D(String title, ReleaseInfo info) {
if (THREE_D_PATTERN.matcher(title).find()) {
info.setThreeDimensional(true);
}
}
private void parseLanguages(String title, ReleaseInfo info) {
Matcher matcher = LANGUAGE_PATTERN.matcher(title);
while (matcher.find()) {
if (matcher.group("multi") != null) info.addLanguage("Multi");
if (matcher.group("english") != null) info.addLanguage("English");
if (matcher.group("french") != null) info.addLanguage("French");
if (matcher.group("spanish") != null) info.addLanguage("Spanish");
if (matcher.group("german") != null) info.addLanguage("German");
if (matcher.group("italian") != null) info.addLanguage("Italian");
if (matcher.group("dutch") != null) info.addLanguage("Dutch");
if (matcher.group("danish") != null) info.addLanguage("Danish");
if (matcher.group("finnish") != null) info.addLanguage("Finnish");
if (matcher.group("norwegian") != null) info.addLanguage("Norwegian");
if (matcher.group("swedish") != null) info.addLanguage("Swedish");
if (matcher.group("russian") != null) info.addLanguage("Russian");
if (matcher.group("polish") != null) info.addLanguage("Polish");
if (matcher.group("portuguese") != null) info.addLanguage("Portuguese");
if (matcher.group("chinese") != null) info.addLanguage("Chinese");
if (matcher.group("japanese") != null) info.addLanguage("Japanese");
if (matcher.group("korean") != null) info.addLanguage("Korean");
if (matcher.group("hindi") != null) info.addLanguage("Hindi");
if (matcher.group("arabic") != null) info.addLanguage("Arabic");
if (matcher.group("hebrew") != null) info.addLanguage("Hebrew");
if (matcher.group("greek") != null) info.addLanguage("Greek");
if (matcher.group("turkish") != null) info.addLanguage("Turkish");
if (matcher.group("thai") != null) info.addLanguage("Thai");
}
}
private void parseMovieTitle(String title, ReleaseInfo info) {
// Try title with year first
Matcher titleYearMatcher = TITLE_YEAR_PATTERN.matcher(title);
if (titleYearMatcher.find()) {
String movieTitle = titleYearMatcher.group("title");
movieTitle = cleanMovieTitle(movieTitle);
info.setMovieTitle(movieTitle);
info.setYear(Integer.parseInt(titleYearMatcher.group("year")));
return;
}
// Try title only (matched by quality indicators)
Matcher titleOnlyMatcher = TITLE_ONLY_PATTERN.matcher(title);
if (titleOnlyMatcher.find()) {
String movieTitle = titleOnlyMatcher.group("title");
movieTitle = cleanMovieTitle(movieTitle);
info.setMovieTitle(movieTitle);
return;
}
// Fallback: use everything before first quality indicator
String[] parts = title.split("[._\\s-]+(?=(?:480p|540p|576p|720p|1080p|2160p|4k|HDTV|WEB|BluRay|BDRip|DVDRip|CAM|TS|TC|SCREENER))", 2);
if (parts.length > 0) {
info.setMovieTitle(cleanMovieTitle(parts[0]));
}
}
private String cleanMovieTitle(String title) {
// Replace separators with spaces
String cleaned = title.replaceAll("[._]", " ");
// Remove double spaces
cleaned = cleaned.replaceAll("\\s+", " ");
return cleaned.trim();
}
}

View File

@ -0,0 +1,330 @@
package com.releaseparser;
import com.releaseparser.analyzer.QualityAnalyzer;
import com.releaseparser.analyzer.QualityAnalyzer.QualityWarning;
import com.releaseparser.analyzer.QualityAnalyzer.Severity;
import com.releaseparser.model.*;
import com.releaseparser.parser.ReleaseParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class QualityAnalyzerTest {
private QualityAnalyzer analyzer;
private ReleaseParser parser;
@BeforeEach
void setUp() {
analyzer = new QualityAnalyzer();
parser = new ReleaseParser();
}
@Nested
class WarningGeneration {
@Test
void shouldWarnAboutCAMSource() {
ReleaseInfo info = parser.parse("Movie.2024.CAM.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.CRITICAL && w.message().contains("CAM"));
}
@Test
void shouldWarnAboutTelesync() {
ReleaseInfo info = parser.parse("Movie.2024.TS.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.CRITICAL && w.message().toLowerCase().contains("telesync"));
}
@Test
void shouldWarnAboutScreener() {
ReleaseInfo info = parser.parse("Movie.2024.DVDSCR.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.WARNING && w.message().toLowerCase().contains("screener"));
}
@Test
void shouldWarnAboutHardcodedSubs() {
ReleaseInfo info = parser.parse("Movie.2024.1080p.BluRay.HC.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.WARNING && w.message().contains("HARDCODED"));
}
@Test
void shouldWarnAboutSDResolution() {
ReleaseInfo info = parser.parse("Movie.2024.480p.BluRay.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.WARNING && w.message().contains("480p"));
}
@Test
void shouldWarnAboutLegacyCodec() {
ReleaseInfo info = parser.parse("Movie.2024.DVDRip.XviD-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.WARNING && w.message().contains("legacy"));
}
@Test
void shouldInfoAboutRemux() {
ReleaseInfo info = parser.parse("Movie.2024.2160p.UHD.BluRay.Remux.HEVC-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().toLowerCase().contains("remux"));
}
@Test
void shouldInfoAboutHDR() {
ReleaseInfo info = parser.parse("Movie.2024.2160p.UHD.BluRay.HDR.x265-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().contains("HDR"));
}
@Test
void shouldInfoAboutProper() {
ReleaseInfo info = parser.parse("Movie.2024.1080p.BluRay.PROPER.x264-GROUP");
List<QualityWarning> warnings = analyzer.analyze(info);
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().contains("PROPER"));
}
}
@Nested
class QualityComparison {
@Test
void shouldPreferBlurayOverWebDL() {
ReleaseInfo bluray = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
ReleaseInfo webdl = parser.parse("Movie.2024.1080p.WEB-DL.x264-GROUP");
var result = analyzer.compare(bluray, webdl);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(bluray);
}
@Test
void shouldPreferWebDLOverCAM() {
ReleaseInfo webdl = parser.parse("Movie.2024.1080p.WEB-DL.x264-GROUP");
ReleaseInfo cam = parser.parse("Movie.2024.CAM.x264-GROUP");
var result = analyzer.compare(webdl, cam);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(webdl);
}
@Test
void shouldPreferRemuxOverBluray() {
ReleaseInfo remux = parser.parse("Movie.2024.1080p.BluRay.Remux.AVC-GROUP");
ReleaseInfo bluray = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
var result = analyzer.compare(remux, bluray);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(remux);
}
@Test
void shouldPrefer2160pOver1080p() {
ReleaseInfo uhd = parser.parse("Movie.2024.2160p.BluRay.x265-GROUP");
ReleaseInfo fhd = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
var result = analyzer.compare(uhd, fhd);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(uhd);
}
@Test
void shouldPreferReleaseWithoutHardcodedSubs() {
ReleaseInfo withSubs = parser.parse("Movie.2024.1080p.BluRay.HC.x264-GROUP");
ReleaseInfo withoutSubs = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
var result = analyzer.compare(withSubs, withoutSubs);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(withoutSubs);
}
@Test
void shouldPreferHDRRelease() {
ReleaseInfo hdr = parser.parse("Movie.2024.2160p.BluRay.HDR.x265-GROUP");
ReleaseInfo sdr = parser.parse("Movie.2024.2160p.BluRay.x265-GROUP");
var result = analyzer.compare(hdr, sdr);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(hdr);
}
@Test
void shouldPreferProperRelease() {
ReleaseInfo proper = parser.parse("Movie.2024.1080p.BluRay.PROPER.x264-GROUP");
ReleaseInfo normal = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
var result = analyzer.compare(proper, normal);
assertThat(result.hasClearWinner()).isTrue();
assertThat(result.better()).isEqualTo(proper);
}
}
@Nested
class QualityRating {
@Test
void shouldRateRemux4KHighest() {
ReleaseInfo info = parser.parse("Movie.2024.2160p.UHD.BluRay.Remux.HDR.HEVC-GROUP");
int rating = analyzer.getQualityRating(info);
assertThat(rating).isGreaterThanOrEqualTo(9);
}
@Test
void shouldRateBluray1080pWell() {
ReleaseInfo info = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
int rating = analyzer.getQualityRating(info);
assertThat(rating).isBetween(6, 8);
}
@Test
void shouldRateCAMPoorly() {
ReleaseInfo info = parser.parse("Movie.2024.CAM.x264-GROUP");
int rating = analyzer.getQualityRating(info);
assertThat(rating).isLessThanOrEqualTo(3);
}
@Test
void shouldRateTelesyncPoorly() {
ReleaseInfo info = parser.parse("Movie.2024.TS.x264-GROUP");
int rating = analyzer.getQualityRating(info);
assertThat(rating).isLessThanOrEqualTo(3);
}
@Test
void shouldPenalizeHardcodedSubs() {
ReleaseInfo withSubs = parser.parse("Movie.2024.1080p.BluRay.HC.x264-GROUP");
ReleaseInfo withoutSubs = parser.parse("Movie.2024.1080p.BluRay.x264-GROUP");
int ratingWithSubs = analyzer.getQualityRating(withSubs);
int ratingWithoutSubs = analyzer.getQualityRating(withoutSubs);
assertThat(ratingWithSubs).isLessThan(ratingWithoutSubs);
}
}
@Nested
class QualityDescription {
@Test
void shouldDescribeExcellentQuality() {
assertThat(analyzer.getQualityDescription(10)).containsIgnoringCase("excellent");
assertThat(analyzer.getQualityDescription(9)).containsIgnoringCase("excellent");
}
@Test
void shouldDescribeGoodQuality() {
assertThat(analyzer.getQualityDescription(8)).containsIgnoringCase("good");
assertThat(analyzer.getQualityDescription(7)).containsIgnoringCase("good");
}
@Test
void shouldDescribeAverageQuality() {
assertThat(analyzer.getQualityDescription(6)).containsIgnoringCase("average");
assertThat(analyzer.getQualityDescription(5)).containsIgnoringCase("average");
}
@Test
void shouldDescribePoorQuality() {
assertThat(analyzer.getQualityDescription(4)).containsIgnoringCase("poor");
assertThat(analyzer.getQualityDescription(3)).containsIgnoringCase("poor");
}
@Test
void shouldDescribeVeryPoorQuality() {
assertThat(analyzer.getQualityDescription(2)).containsIgnoringCase("very poor");
assertThat(analyzer.getQualityDescription(1)).containsIgnoringCase("very poor");
}
}
@Nested
class RealWorldScenarios {
@Test
void shouldCorrectlyAnalyzePremiumRelease() {
ReleaseInfo info = parser.parse("Oppenheimer.2023.IMAX.2160p.UHD.BluRay.Remux.DV.HDR.HEVC-FGT");
List<QualityWarning> warnings = analyzer.analyze(info);
int rating = analyzer.getQualityRating(info);
// Should have positive info messages
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().toLowerCase().contains("remux"));
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().contains("HDR"));
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.INFO && w.message().toLowerCase().contains("dolby"));
// Should have high rating
assertThat(rating).isGreaterThanOrEqualTo(9);
}
@Test
void shouldCorrectlyWarnAboutPoorRelease() {
ReleaseInfo info = parser.parse("New.Movie.2024.HDCAM.HC.XviD-NOGRP");
List<QualityWarning> warnings = analyzer.analyze(info);
int rating = analyzer.getQualityRating(info);
// Should have critical/warning messages
assertThat(warnings)
.anyMatch(w -> w.severity() == Severity.CRITICAL);
assertThat(warnings)
.anyMatch(w -> w.message().contains("HARDCODED"));
// Should have low rating
assertThat(rating).isLessThanOrEqualTo(3);
}
@Test
void shouldCompareTypicalReleases() {
ReleaseInfo premium = parser.parse("Movie.2024.2160p.UHD.BluRay.Remux.HDR.HEVC-GROUP1");
ReleaseInfo standard = parser.parse("Movie.2024.1080p.WEB-DL.x264-GROUP2");
ReleaseInfo poor = parser.parse("Movie.2024.CAM.x264-GROUP3");
// Premium should beat standard
var result1 = analyzer.compare(premium, standard);
assertThat(result1.better()).isEqualTo(premium);
// Standard should beat poor
var result2 = analyzer.compare(standard, poor);
assertThat(result2.better()).isEqualTo(standard);
// Premium should beat poor
var result3 = analyzer.compare(premium, poor);
assertThat(result3.better()).isEqualTo(premium);
}
}
}

View File

@ -0,0 +1,449 @@
package com.releaseparser;
import com.releaseparser.analyzer.QualityAnalyzer;
import com.releaseparser.model.*;
import com.releaseparser.parser.ReleaseParser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ReleaseParserTest {
private ReleaseParser parser;
@BeforeEach
void setUp() {
parser = new ReleaseParser();
}
@Nested
class SourceParsing {
@Test
void shouldParseBluraySource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.BLURAY);
}
@Test
void shouldParseBDSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BD.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.BLURAY);
}
@Test
void shouldParseWebDLSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.WEB-DL.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.WEBDL);
}
@Test
void shouldParseWebRipSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.WEBRip.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.WEBRIP);
}
@Test
void shouldParseHDTVSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.720p.HDTV.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.HDTV);
}
@Test
void shouldParseDVDRipSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.DVDRip.XviD-GROUP");
assertThat(info.getSource()).isEqualTo(Source.DVD);
}
@Test
void shouldParseCAMSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.CAM.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.CAM);
}
@Test
void shouldParseTSSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.TS.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.TELESYNC);
}
@Test
void shouldParseTelecineSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.TC.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.TELECINE);
}
@Test
void shouldParseScreenerSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.DVDSCR.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.SCREENER);
}
@Test
void shouldParseRemuxSource() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.UHD.BluRay.Remux.HEVC-GROUP");
assertThat(info.getSource()).isEqualTo(Source.REMUX);
assertThat(info.isRemux()).isTrue();
}
@Test
void shouldParseAmazonWebDL() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.AMZN.WEB-DL.DDP5.1.H.264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.WEBDL);
}
@Test
void shouldParseNetflixWebDL() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.NF.WEB-DL.DDP5.1.x264-GROUP");
assertThat(info.getSource()).isEqualTo(Source.WEBDL);
}
}
@Nested
class ResolutionParsing {
@Test
void shouldParse480pResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.480p.BluRay.x264-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R480P);
}
@Test
void shouldParse720pResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.720p.BluRay.x264-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R720P);
}
@Test
void shouldParse1080pResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R1080P);
}
@Test
void shouldParse2160pResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.BluRay.x265-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R2160P);
}
@Test
void shouldParse4kResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.4K.UHD.BluRay.x265-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R2160P);
}
@Test
void shouldParseFHDResolution() {
ReleaseInfo info = parser.parse("Movie.Title.2023.FHD.BluRay.x264-GROUP");
assertThat(info.getResolution()).isEqualTo(Resolution.R1080P);
}
}
@Nested
class CodecParsing {
@Test
void shouldParseX264Codec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.X264);
}
@Test
void shouldParseX265Codec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.BluRay.x265-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.X265);
}
@Test
void shouldParseHEVCCodec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.BluRay.HEVC-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.X265);
}
@Test
void shouldParseH264Codec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.WEB-DL.H.264-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.X264);
}
@Test
void shouldParseXviDCodec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.DVDRip.XviD-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.XVID);
}
@Test
void shouldParseAV1Codec() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.WEB-DL.AV1-GROUP");
assertThat(info.getCodec()).isEqualTo(Codec.AV1);
}
}
@Nested
class HardcodedSubsParsing {
@Test
void shouldDetectHCSubbed() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.HC.x264-GROUP");
assertThat(info.isHardcodedSubs()).isTrue();
}
@Test
void shouldDetectSubbed() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.SUBBED.BluRay.x264-GROUP");
assertThat(info.isHardcodedSubs()).isTrue();
}
@Test
void shouldDetectKoreanSubs() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.KoreanSUBS.x264-GROUP");
assertThat(info.isHardcodedSubs()).isTrue();
assertThat(info.getHardcodedSubsLanguage()).isEqualTo("Korean");
}
@Test
void shouldNotDetectSoftSubs() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.SOFTSUBS.x264-GROUP");
assertThat(info.isHardcodedSubs()).isFalse();
}
@Test
void shouldNotDetectMultiSubs() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.MULTISUBS.x264-GROUP");
assertThat(info.isHardcodedSubs()).isFalse();
}
}
@Nested
class TitleParsing {
@Test
void shouldParseMovieTitleWithYear() {
ReleaseInfo info = parser.parse("The.Matrix.1999.1080p.BluRay.x264-GROUP");
assertThat(info.getMovieTitle()).isEqualTo("The Matrix");
assertThat(info.getYear()).isEqualTo(1999);
}
@Test
void shouldParseMovieTitleWithSpaces() {
ReleaseInfo info = parser.parse("Guardians of the Galaxy Vol 3 2023 1080p BluRay x264-GROUP");
assertThat(info.getMovieTitle()).isEqualTo("Guardians of the Galaxy Vol 3");
assertThat(info.getYear()).isEqualTo(2023);
}
@Test
void shouldParseMovieTitleWithDotsAndYear() {
ReleaseInfo info = parser.parse("Avengers.Endgame.2019.2160p.UHD.BluRay.x265-GROUP");
assertThat(info.getMovieTitle()).isEqualTo("Avengers Endgame");
assertThat(info.getYear()).isEqualTo(2019);
}
}
@Nested
class ReleaseGroupParsing {
@Test
void shouldParseReleaseGroup() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-SPARKS");
assertThat(info.getReleaseGroup()).isEqualTo("SPARKS");
}
@Test
void shouldParseReleaseGroupWithNumbers() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-D3G");
assertThat(info.getReleaseGroup()).isEqualTo("D3G");
}
@Test
void shouldParseYIFYGroup() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-YIFY");
assertThat(info.getReleaseGroup()).isEqualTo("YIFY");
}
}
@Nested
class VersionAndProperParsing {
@Test
void shouldDetectProper() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.PROPER.x264-GROUP");
assertThat(info.isProper()).isTrue();
}
@Test
void shouldDetectRepack() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.REPACK.x264-GROUP");
assertThat(info.isRepack()).isTrue();
}
@Test
void shouldDetectVersion2() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264.v2-GROUP");
assertThat(info.getVersion()).isEqualTo(2);
}
}
@Nested
class EditionParsing {
@Test
void shouldParseDirectorsCut() {
ReleaseInfo info = parser.parse("Movie.Title.2023.Directors.Cut.1080p.BluRay.x264-GROUP");
assertThat(info.getEdition()).containsIgnoringCase("director");
}
@Test
void shouldParseExtendedEdition() {
ReleaseInfo info = parser.parse("Movie.Title.2023.Extended.Edition.1080p.BluRay.x264-GROUP");
assertThat(info.getEdition()).containsIgnoringCase("extended");
}
@Test
void shouldParseUnratedEdition() {
ReleaseInfo info = parser.parse("Movie.Title.2023.UNRATED.1080p.BluRay.x264-GROUP");
assertThat(info.getEdition()).containsIgnoringCase("unrated");
}
@Test
void shouldParseIMAX() {
ReleaseInfo info = parser.parse("Movie.Title.2023.IMAX.1080p.BluRay.x264-GROUP");
assertThat(info.getEdition()).containsIgnoringCase("IMAX");
}
}
@Nested
class HDRParsing {
@Test
void shouldDetectHDR() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.UHD.BluRay.HDR.x265-GROUP");
assertThat(info.isHdr()).isTrue();
}
@Test
void shouldDetectHDR10() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.UHD.BluRay.HDR10.x265-GROUP");
assertThat(info.isHdr()).isTrue();
}
@Test
void shouldDetectDolbyVision() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.UHD.BluRay.DoVi.x265-GROUP");
assertThat(info.isDolbyVision()).isTrue();
assertThat(info.isHdr()).isTrue();
}
@Test
void shouldDetectDV() {
ReleaseInfo info = parser.parse("Movie.Title.2023.2160p.UHD.BluRay.DV.HDR.x265-GROUP");
assertThat(info.isDolbyVision()).isTrue();
}
}
@Nested
class ThreeDParsing {
@Test
void shouldDetect3D() {
ReleaseInfo info = parser.parse("Movie.Title.2023.3D.1080p.BluRay.x264-GROUP");
assertThat(info.isThreeDimensional()).isTrue();
}
@Test
void shouldDetectSBS() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.SBS.x264-GROUP");
assertThat(info.isThreeDimensional()).isTrue();
}
}
@Nested
class LanguageParsing {
@Test
void shouldDetectGermanLanguage() {
ReleaseInfo info = parser.parse("Movie.Title.2023.GERMAN.1080p.BluRay.x264-GROUP");
assertThat(info.getLanguages()).contains("German");
}
@Test
void shouldDetectFrenchLanguage() {
ReleaseInfo info = parser.parse("Movie.Title.2023.FRENCH.1080p.BluRay.x264-GROUP");
assertThat(info.getLanguages()).contains("French");
}
@Test
void shouldDetectMultipleLanguages() {
ReleaseInfo info = parser.parse("Movie.Title.2023.MULTI.FRENCH.GERMAN.1080p.BluRay.x264-GROUP");
assertThat(info.getLanguages()).containsExactlyInAnyOrder("Multi", "French", "German");
}
}
@Nested
class WebsiteCleanup {
@Test
void shouldRemoveWebsitePrefix() {
ReleaseInfo info = parser.parse("[www.example.com] Movie.Title.2023.1080p.BluRay.x264-GROUP");
assertThat(info.getMovieTitle()).isEqualTo("Movie Title");
}
@Test
void shouldRemoveTorrentSuffix() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.BluRay.x264-GROUP[rarbg]");
assertThat(info.getReleaseGroup()).isEqualTo("GROUP");
}
}
@Nested
class RealWorldExamples {
@Test
void shouldParseComplexTitle1() {
ReleaseInfo info = parser.parse("Oppenheimer.2023.IMAX.2160p.UHD.BluRay.Remux.DV.HDR.HEVC.TrueHD.Atmos.7.1-FGT");
assertThat(info.getMovieTitle()).isEqualTo("Oppenheimer");
assertThat(info.getYear()).isEqualTo(2023);
assertThat(info.getResolution()).isEqualTo(Resolution.R2160P);
assertThat(info.getSource()).isEqualTo(Source.REMUX);
assertThat(info.getCodec()).isEqualTo(Codec.X265);
assertThat(info.isHdr()).isTrue();
assertThat(info.isDolbyVision()).isTrue();
assertThat(info.isRemux()).isTrue();
assertThat(info.getReleaseGroup()).isEqualTo("FGT");
}
@Test
void shouldParseComplexTitle2() {
ReleaseInfo info = parser.parse("The.Godfather.1972.REMASTERED.1080p.BluRay.x264.DTS-HD.MA.5.1-FGT");
assertThat(info.getMovieTitle()).isEqualTo("The Godfather");
assertThat(info.getYear()).isEqualTo(1972);
assertThat(info.getResolution()).isEqualTo(Resolution.R1080P);
assertThat(info.getSource()).isEqualTo(Source.BLURAY);
assertThat(info.getCodec()).isEqualTo(Codec.X264);
assertThat(info.getEdition()).containsIgnoringCase("remastered");
}
@Test
void shouldParseLowQualityTitle() {
ReleaseInfo info = parser.parse("New.Movie.2024.HDCAM.x264-NOGRP");
assertThat(info.getSource()).isEqualTo(Source.CAM);
assertThat(info.getCodec()).isEqualTo(Codec.X264);
}
@Test
void shouldParseStreamingServiceTitle() {
ReleaseInfo info = parser.parse("Movie.Title.2023.1080p.NF.WEB-DL.DDP5.1.Atmos.H.264-FLUX");
assertThat(info.getSource()).isEqualTo(Source.WEBDL);
assertThat(info.getResolution()).isEqualTo(Resolution.R1080P);
assertThat(info.getCodec()).isEqualTo(Codec.X264);
assertThat(info.getReleaseGroup()).isEqualTo("FLUX");
}
}
}