mirror of
https://github.com/theotherp/nzbhydra2.git
synced 2026-02-06 11:17:18 +00:00
Add release parser
This commit is contained in:
parent
0c5c3f5331
commit
280680a16c
4
.github/workflows/system-test.yml
vendored
4
.github/workflows/system-test.yml
vendored
@ -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: |
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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"/>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 $?) {
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
<modules>
|
||||
<module>mapping</module>
|
||||
<module>release-parser</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
||||
|
||||
174
shared/release-parser/README.md
Normal file
174
shared/release-parser/README.md
Normal 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.
|
||||
60
shared/release-parser/pom.xml
Normal file
60
shared/release-parser/pom.xml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user