58 Commits
v4.0.2 ... v5.0

Author SHA1 Message Date
RatzzFatzz
13283e3765 Update libs 2025-12-23 15:55:58 +01:00
Michael
949261fb17 Update log4j2
Bump org.apache.logging.log4j:log4j-core from 2.24.3 to 2.25.3
2025-12-23 01:05:03 +01:00
RatzzFatzz
6cf42e5915 Update example gif 2025-12-23 00:58:28 +01:00
RatzzFatzz
842b97dcb6 Fix NPE while sorting 2025-12-23 00:46:33 +01:00
RatzzFatzz
c2af135a57 Add information when safemode is active 2025-12-22 23:39:23 +01:00
RatzzFatzz
280771e545 Improve default value display on help 2025-12-22 23:37:45 +01:00
RatzzFatzz
7620771aed Update instructions in readme 2025-12-22 23:34:06 +01:00
RatzzFatzz
c7670e36c1 Add hearing impaired tracks as low prio default match 2025-12-21 00:44:49 +01:00
RatzzFatzz
1e31326ea2 Add logging about planned changes in safe mode 2025-12-20 20:05:45 +01:00
dependabot[bot]
7230134de6 Bump org.apache.logging.log4j:log4j-core from 2.24.3 to 2.25.3
Bumps org.apache.logging.log4j:log4j-core from 2.24.3 to 2.25.3.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 23:52:26 +00:00
RatzzFatzz
fa84c483d9 Add comment to explain id collection 2025-12-19 22:59:57 +01:00
RatzzFatzz
62b637c241 Add changes list to info logging 2025-12-19 22:35:07 +01:00
RatzzFatzz
1bdd3874e7 Remove unused code 2025-12-18 21:27:12 +01:00
RatzzFatzz
3e74e23512 Remove attribute config as required option 2025-12-18 21:17:51 +01:00
RatzzFatzz
5f2248653b Improve status detection 2025-12-18 21:14:15 +01:00
RatzzFatzz
15128583df Improve result print 2025-12-17 14:27:27 +01:00
RatzzFatzz
76d25fca1b Fix finding hearing impaired flag changes 2025-12-17 13:32:23 +01:00
RatzzFatzz
957295127a Remove version from jar in Windows installer 2025-12-17 11:42:44 +01:00
RatzzFatzz
15177268a0 Remove pre-release information before passing version to maven in workflow 2025-12-16 23:44:13 +01:00
RatzzFatzz
fa572030da Fix passing version to maven in workflow 2025-12-16 23:11:35 +01:00
RatzzFatzz
cc23d8c5bc Fix windows build log4j2 config inclusion 2025-12-16 22:31:09 +01:00
RatzzFatzz
94a3b419e0 Get release version from tag 2025-12-16 16:57:38 +01:00
RatzzFatzz
f88fcd0bd5 Fix tests for windows build 2025-12-16 16:35:02 +01:00
RatzzFatzz
69a70eb66f Renamed parameter excluded to exclude 2025-12-16 16:22:59 +01:00
RatzzFatzz
e5e5f56aed Add release build on tagged commit 2025-12-16 16:18:50 +01:00
RatzzFatzz
b6b15faf7d Add test for command construction 2025-12-16 15:24:24 +01:00
RatzzFatzz
ca29c22f00 Add tests for reading attributes 2025-12-16 02:13:18 +01:00
RatzzFatzz
1ae5b1bef1 Improve file loading abstraction for attribute updater 2025-12-16 00:58:57 +01:00
RatzzFatzz
cf04e14de2 Improve file exclusion 2025-12-16 00:50:57 +01:00
RatzzFatzz
2ecea906b1 Add tests for attributeupdater 2025-12-16 00:22:10 +01:00
RatzzFatzz
d3248e646b Improve coherent attribute updater 2025-12-15 21:24:34 +01:00
RatzzFatzz
a51922968e Reimplement coherent updating 2025-12-15 15:40:46 +01:00
RatzzFatzz
80c46508b8 Add tests for validation and extract command from Main 2025-12-14 22:47:41 +01:00
RatzzFatzz
a5b24e907d Improve logging when ignoring and excluding files 2025-12-14 19:11:49 +01:00
RatzzFatzz
7427e3aa27 Migrate CachedFileProcessor to proxy pattern 2025-12-13 17:29:43 +01:00
RatzzFatzz
d7ae865d55 Fix attribute config test 2025-12-13 17:29:05 +01:00
RatzzFatzz
363492be43 Fix faulty track attribute collection 2025-12-11 17:56:56 +01:00
RatzzFatzz
37cedecea7 Make InputConfig no longer be a singleton 2025-12-11 01:47:55 +01:00
RatzzFatzz
04722d9279 Improve attribute processor 2025-12-11 01:25:08 +01:00
RatzzFatzz
0b61deccbf Major system rework 2025-12-10 21:31:29 +01:00
RatzzFatzz
d5e452557c Add mkvmerge and mkvpropedit version 2025-12-09 04:54:49 +01:00
RatzzFatzz
e7a13c9f1d Add jvm and os info to version 2025-12-09 04:46:55 +01:00
RatzzFatzz
63bcd92db9 Use recommended way to terminate if no args are given 2025-12-09 04:35:25 +01:00
RatzzFatzz
0b8dfa7464 Improve usage help display 2025-12-09 04:34:32 +01:00
RatzzFatzz
f08a6ef1da Improve example command 2025-12-09 04:33:50 +01:00
RatzzFatzz
ae541e6fdf Improve attribute config converter 2025-12-06 20:16:37 +01:00
RatzzFatzz
aa5fd26b32 Add attribute conversion tests 2025-12-04 23:28:54 +01:00
RatzzFatzz
181c718e7a Make picocli fully handle config validation 2025-12-04 23:20:49 +01:00
RatzzFatzz
5eca28ecb9 Exclude log dir from git 2025-12-04 22:10:03 +01:00
RatzzFatzz
9ab417f71d Improve input validation 2025-12-04 22:09:24 +01:00
RatzzFatzz
0f6bc271b1 Add tests for detecting tracks 2025-10-18 03:28:16 +02:00
RatzzFatzz
99f929aabb Add test for FileFilter 2025-10-18 01:40:16 +02:00
RatzzFatzz
a156db16fe Remove deprecated validator 2025-10-18 01:39:48 +02:00
RatzzFatzz
b0f927dfa8 Implement OFF for audio tracks 2025-10-16 17:10:33 +02:00
RatzzFatzz
37c65df60c Update dependencies && Fix status reporting if no subs are available for file 2025-10-16 00:30:54 +02:00
0e9d008c7e Fix formatting in README.md 2025-05-12 16:00:19 +02:00
3205969d3b Update readme 2025-05-02 03:33:14 +02:00
d24aedb0af Fix mkvpropedit call & improve logging 2025-05-02 03:27:24 +02:00
89 changed files with 2283 additions and 2629 deletions

View File

@@ -1,4 +1,4 @@
# This workflow will run every time a new release is created. # This workflow will run every time a tag starting with v is created.
name: Build and release name: Build and release
on: on:
@@ -8,12 +8,23 @@ on:
jobs: jobs:
portable-build: portable-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- name: Install mkvtoolnix - name: Install mkvtoolnix
run: sudo apt-get install -y mkvtoolnix run: sudo apt-get install -y mkvtoolnix
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Extract version from tag
id: get_version
run: echo "VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d'-' -f1)" >> $GITHUB_OUTPUT
shell: bash
- name: Set timezone - name: Set timezone
uses: szenius/set-timezone@v2.0 uses: szenius/set-timezone@v2.0
@@ -31,7 +42,7 @@ jobs:
- name: Build with Maven - name: Build with Maven
run: | run: |
mvn clean package --file pom.xml -P portable mvn clean package --file pom.xml -P portable -Drevision="${{ steps.get_version.outputs.VERSION }}"
cp target/M*.{zip,tar} artifacts/ cp target/M*.{zip,tar} artifacts/
- name: Upload artifacts - name: Upload artifacts
@@ -43,12 +54,23 @@ jobs:
debian-build: debian-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- name: Install mkvtoolnix - name: Install mkvtoolnix
run: sudo apt-get install -y mkvtoolnix run: sudo apt-get install -y mkvtoolnix
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Extract version from tag
id: get_version
run: echo "VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d'-' -f1)" >> $GITHUB_OUTPUT
shell: bash
- name: Set timezone - name: Set timezone
uses: szenius/set-timezone@v2.0 uses: szenius/set-timezone@v2.0
@@ -66,7 +88,7 @@ jobs:
- name: Build with Maven - name: Build with Maven
run: | run: |
mvn clean package --file pom.xml -P linux mvn clean package --file pom.xml -P debian -Drevision="${{ steps.get_version.outputs.VERSION }}"
cp target/M*.deb artifacts/ cp target/M*.deb artifacts/
- name: Upload artifacts - name: Upload artifacts
@@ -78,6 +100,10 @@ jobs:
windows-installer-build: windows-installer-build:
runs-on: windows-latest runs-on: windows-latest
permissions:
contents: write
steps: steps:
- name: Install mkvtoolnix - name: Install mkvtoolnix
uses: crazy-max/ghaction-chocolatey@v3 uses: crazy-max/ghaction-chocolatey@v3
@@ -86,6 +112,13 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ github.event.release.tag_name }}
- name: Extract version from tag
id: get_version
run: echo "VERSION=$(echo ${GITHUB_REF_NAME#v} | cut -d'-' -f1)" >> $GITHUB_OUTPUT
shell: bash
- name: Set timezone - name: Set timezone
uses: szenius/set-timezone@v2.0 uses: szenius/set-timezone@v2.0
@@ -102,7 +135,7 @@ jobs:
run: mkdir artifacts run: mkdir artifacts
- name: Build with Maven - name: Build with Maven
run: mvn clean package --file pom.xml -P windows run: mvn clean package --file pom.xml -P windows -Drevision="${{ steps.get_version.outputs.VERSION }}"
- name: Upload artifacts - name: Upload artifacts
uses: AButler/upload-release-assets@v3.0 uses: AButler/upload-release-assets@v3.0

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
# Log file # Log file
*.log *.log
logs/
# BlueJ files # BlueJ files
*.ctxt *.ctxt

View File

@@ -1,8 +1,7 @@
## Introduction ## Introduction
A streamlined solution for managing MKV files, this program leverages MKVToolNix to modify audio and subtitle track properties without the need for time-consuming file reencoding. Users can easily set their track preferences, and the application intelligently applies the best matching configuration. The tool focuses on metadata modification rather than full file rewriting, ensuring quick operations while maintaining the original file integrity. This makes it an ideal choice for managing multilingual media collections or batch processing multiple MKV files. This CLI tool uses MKVToolNix to quickly modify track properties in MKV files without reencoding. Use profiles to set default audio/subtitle tracks and add commentary, hearing impaired, and forced flags in bulk.
![](example.gif)
![](https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/blob/master/example.gif)
## Requirements ## Requirements
@@ -10,43 +9,56 @@ A streamlined solution for managing MKV files, this program leverages MKVToolNix
- mkvtoolnix installation - mkvtoolnix installation
## Execution ## Execution
### Minimal usage ```shell
Portable: `java -jar mkvaudiosubtitlechanger-<version>.jar --library "X:/Files" --attribute-config eng:ger eng:OFF` # Portable
Windows & Linux (installed): `mkvaudiosubtitlechanger --library "X:/Files" --attribute-config eng:ger eng:OFF` java -jar mkvaudiosubtitlechanger.jar --attribute-config eng:ger -s ./videos
# Installed
mkvaudiosubtitlechanger.jar --attribute-config eng:ger -s ./videos
```
Remove `--safemode` or `-s` to actually apply the changes. Using safemode for the first execution is recommended.
### Safe usage (best for testing before applying to whole library) ### Update defaults
Portable: `java -jar mkvaudiosubtitlechanger-<version>.jar --library "X:/Files" --attribute-config eng:ger eng:OFF --safe-mode` To update the default flag for tracks use `--attribute-config` or `-a`.
Windows & Linux (installed): `mkvaudiosubtitlechanger --library "X:/Files" --attribute-config eng:ger eng:OFF --safe-mode` This parameter takes in a list of pairs `audio:subtitle` (E.g. `eng:ger`).
The order of these configs matters, because they are processed in order.
The matching stops when the first match was found or when no match was found.
For example `-a ger:OFF eng:ger` first tries to find a match for german audio, if that is not possible it tries the same for english with german subs.
This can be extended indefinitely.
**Attribute-config must be entered in pairs: `audio:subtitle`; Example: `jpn:eng`. More about this topic Using this parameter is not required, but it is the reason I originally started developing this tool.
[here](https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/wiki/Attribute-Config).**
## Available parameters ### Available parameters
``` ```
-a, --attribute-config=<attributeConfig>... -a, --attribute-config=<attributeConfig>...
List of audio:subtitle pairs used to match in order and update files accordingly (e.g. jpn:eng jpn:ger) List of audio:subtitle pairs for matching defaults in order (e.g. jpn:eng jpn:ger)
-c, --coherent=<coherent> try to match all files in dir of depth with the same attribute config
-cf, --force-coherent changes are only applied if it's a coherent match
--commentary-keywords=<commentaryKeywords>[, <commentaryKeywords>...]...
Keywords to identify commentary tracks (Defaults will be overwritten; Default: commentary, director)
-d, --filter-date=<filterDate>
only consider files created newer than entered date (format: "dd.MM.yyyy-HH:mm:ss")
-e, --excluded-directory=<excludedDirectories>...
Directories to be excluded, combines with config file
--forced-keywords=<forcedKeywords>[, <forcedKeywords>...]...
Keywords to identify forced tracks (Defaults will be overwritten; Default: forced, signs, songs)
-h, --help Show this help message and exit.
-i, --include-pattern=<includePattern>
include files matching pattern (default: ".*")
-l, --library=<libraryPath>
path to library
-m, --mkvtoolnix=<mkvToolNix> -m, --mkvtoolnix=<mkvToolNix>
path to mkvtoolnix installation path to mkvtoolnix installation
-n, --only-new-file sets filter-date to last successful execution (overwrites input of filter-date)
--preferred-subtitles=<preferredSubtitles>[, <preferredSubtitles>...]...
Keywords to prefer specific subtitle tracks (Defaults will be overwritten; Default: unstyled)
-s, --safemode test run (no files will be changes) -s, --safemode test run (no files will be changes)
-t, --threads=<threads> thread count (default: 2) -t, --threads=<threads> thread count
Default: 2
-c, --coherent=<coherent> try to match all files in dir of depth with the same attribute config. Attempting increasing deeper levels until match is found (worst case applying config on single file basis)
-cf, --force-coherent only applies changes if a coherent match was found for the specifically entered depth
-d, --filter-date=<filterDate>
only consider files created newer than entered date (format: "dd.MM.yyyy-HH:mm:ss")
-i, --include-pattern=<includePattern>
include files matching pattern
-e, --exclude=<excluded>...
relative directories and files to be excluded (no wildcard)
-o, -overwrite-forced remove all forced flags
--forced-keywords=<forcedKeywords>[, <forcedKeywords>...]...
Keywords to identify forced tracks (Defaults will be overwritten)
Default: forced, signs, songs
--commentary-keywords=<commentaryKeywords>[, <commentaryKeywords>...]...
Keywords to identify commentary tracks (Defaults will be overwritten)
Default: comment, commentary, director
--hearing-impaired=<hearingImpaired>[, <hearingImpaired>...]...
Keywords to identify hearing impaired tracks (Defaults will be overwritten
Default: SDH, CC
--preferred-subtitles=<preferredSubtitles>[, <preferredSubtitles>...]...
Keywords to prefer specific subtitle tracks (Defaults will be overwritten)
Default: unstyled
--debug Enable debug logging
-h, --help Show this help message and exit.
-V, --version Print version information and exit. -V, --version Print version information and exit.
``` ```
If you need more information how each parameter works, check out [this wiki page](https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/wiki/Parameters-v4). If you need more information how each parameter works, check out [this wiki page](https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/wiki/Parameters-v4).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 145 KiB

93
pom.xml
View File

@@ -6,17 +6,19 @@
<groupId>at.pcgamingfreaks</groupId> <groupId>at.pcgamingfreaks</groupId>
<artifactId>MKVAudioSubtitleChanger</artifactId> <artifactId>MKVAudioSubtitleChanger</artifactId>
<version>4.0.2</version> <version>${revision}</version>
<properties> <properties>
<revision>1.0.0-SNAPSHOT</revision>
<mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass> <mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.maintainer>RatzzFatzz</project.maintainer> <project.maintainer>RatzzFatzz</project.maintainer>
<project.maintainer.mail>github.contact@ratzloeffel.de</project.maintainer.mail> <project.maintainer.mail>github.contact@ratzloeffel.de</project.maintainer.mail>
<project.description>Command-line utility for batch-managing default audio and subtitle tracks in MKV files.</project.description> <project.description>Command-line utility for batch-managing default audio and subtitle tracks in MKV files.</project.description>
<java-version>17</java-version> <java-version>17</java-version>
<lombok-version>1.18.36</lombok-version> <lombok-version>1.18.42</lombok-version>
</properties> </properties>
<profiles> <profiles>
@@ -62,11 +64,19 @@
<resource> <resource>
<directory>src/main/resources</directory> <directory>src/main/resources</directory>
<includes> <includes>
<include>log4j2-installed.yaml</include> <include>log4j2-windows.yaml</include>
</includes> </includes>
</resource> </resource>
</resources> </resources>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<finalName>${project.artifactId}</finalName>
</configuration>
</plugin>
<plugin> <plugin>
<artifactId>maven-resources-plugin</artifactId> <artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version> <version>3.3.1</version>
@@ -83,7 +93,7 @@
<resource> <resource>
<directory>${project.build.directory}</directory> <directory>${project.build.directory}</directory>
<includes> <includes>
<include>${project.artifactId}-${project.version}.jar</include> <include>${project.artifactId}.jar</include>
</includes> </includes>
</resource> </resource>
</resources> </resources>
@@ -110,7 +120,7 @@
<plugin> <plugin>
<groupId>org.panteleyev</groupId> <groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId> <artifactId>jpackage-maven-plugin</artifactId>
<version>1.6.5</version> <version>1.7.1</version>
<configuration> <configuration>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
<vendor>RatzzFatzz</vendor> <vendor>RatzzFatzz</vendor>
@@ -120,7 +130,7 @@
<input>target/jpackage-input</input> <input>target/jpackage-input</input>
<mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass> <mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass>
<mainJar>${project.artifactId}-${project.version}.jar</mainJar> <mainJar>${project.artifactId}.jar</mainJar>
<resourceDir>${project.build.directory}/wix-resources/</resourceDir> <resourceDir>${project.build.directory}/wix-resources/</resourceDir>
<type>EXE</type> <type>EXE</type>
@@ -128,7 +138,7 @@
<winShortcut>false</winShortcut> <winShortcut>false</winShortcut>
<winMenu>false</winMenu> <winMenu>false</winMenu>
<javaOptions> <javaOptions>
<javaOption>-Dlog4j.configurationFile=log4j2-windows.yaml</javaOption> <javaOption>-Dlog4j2.configurationFile=log4j2-windows.yaml</javaOption>
</javaOptions> </javaOptions>
</configuration> </configuration>
<executions> <executions>
@@ -145,7 +155,7 @@
</build> </build>
</profile> </profile>
<profile> <profile>
<id>linux</id> <id>debian</id>
<build> <build>
<resources> <resources>
<resource> <resource>
@@ -307,7 +317,7 @@
<path> <path>
<groupId>info.picocli</groupId> <groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId> <artifactId>picocli-codegen</artifactId>
<version>4.7.6</version> <version>4.7.7</version>
</path> </path>
</annotationProcessorPaths> </annotationProcessorPaths>
</configuration> </configuration>
@@ -324,12 +334,6 @@
</repositories> </repositories>
<dependencies> <dependencies>
<!-- https://mvnrepository.com/artifact/com.intellij/forms_rt -->
<dependency>
<groupId>com.intellij</groupId>
<artifactId>forms_rt</artifactId>
<version>7.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
@@ -341,15 +345,22 @@
<dependency> <dependency>
<groupId>info.picocli</groupId> <groupId>info.picocli</groupId>
<artifactId>picocli</artifactId> <artifactId>picocli</artifactId>
<version>4.7.6</version> <version>4.7.7</version>
</dependency> </dependency>
<!-- Jakarta Bean Validation -->
<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>4.0.0-M1</version>
</dependency>
<!-- Hibernate Validator --> <!-- Hibernate Validator -->
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator --> <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency> <dependency>
<groupId>org.hibernate.validator</groupId> <groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId> <artifactId>hibernate-validator</artifactId>
<version>8.0.2.Final</version> <version>9.1.0.Final</version>
</dependency> </dependency>
<!-- Expression Language Implementation --> <!-- Expression Language Implementation -->
@@ -359,66 +370,56 @@
<artifactId>jakarta.el-api</artifactId> <artifactId>jakarta.el-api</artifactId>
<version>6.0.1</version> <version>6.0.1</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.glassfish</groupId> <groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId> <artifactId>jakarta.el</artifactId>
<version>5.0.0-M1</version> <version>5.0.0-M1</version>
</dependency> </dependency>
<!-- Jakarta Bean Validation -->
<!-- https://mvnrepository.com/artifact/jakarta.validation/jakarta.validation-api -->
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
</dependency>
<!-- region logging --> <!-- region logging -->
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api --> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId> <artifactId>log4j-api</artifactId>
<version>2.24.3</version> <version>2.25.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId> <artifactId>log4j-core</artifactId>
<version>2.24.3</version> <version>2.25.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j18-impl -->
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl --> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
<dependency> <dependency>
<groupId>org.apache.logging.log4j</groupId> <groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId> <artifactId>log4j-slf4j2-impl</artifactId>
<version>2.24.3</version> <version>2.25.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml --> <!-- https://mvnrepository.com/artifact/tools.jackson.dataformat/jackson-dataformat-yaml -->
<dependency> <dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId> <groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId> <artifactId>jackson-dataformat-yaml</artifactId>
<version>2.18.2</version> <version>3.0.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind --> <!-- https://mvnrepository.com/artifact/tools.jackson.core/jackson-databind -->
<dependency> <dependency>
<groupId>com.fasterxml.jackson.core</groupId> <groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId> <artifactId>jackson-databind</artifactId>
<version>2.18.2</version> <version>3.0.3</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/commons-cli/commons-cli --> <!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
<dependency> <dependency>
<groupId>commons-cli</groupId> <groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId> <artifactId>commons-cli</artifactId>
<version>1.9.0</version> <version>1.11.0</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency> <dependency>
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.17.0</version> <version>3.20.0</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/me.tongfei/progressbar --> <!-- https://mvnrepository.com/artifact/me.tongfei/progressbar -->
<dependency> <dependency>
@@ -432,28 +433,34 @@
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId> <artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version> <version>6.0.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId> <artifactId>junit-jupiter-engine</artifactId>
<version>5.11.4</version> <version>6.0.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core --> <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency> <dependency>
<groupId>org.mockito</groupId> <groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>
<version>5.15.2</version> <version>5.21.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.21.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params --> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId> <artifactId>junit-jupiter-params</artifactId>
<version>5.11.4</version> <version>6.0.1</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
@@ -467,7 +474,7 @@
<dependency> <dependency>
<groupId>net.harawata</groupId> <groupId>net.harawata</groupId>
<artifactId>appdirs</artifactId> <artifactId>appdirs</artifactId>
<version>1.3.0</version> <version>1.5.0</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -1 +1 @@
java -Dlog4j.configurationFile=log4j2-debian.yaml -jar /usr/lib/${project.artifactId}/${project.artifactId}-${project.version}.jar "$@" java -Dlog4j2.configurationFile=log4j2-debian.yaml -jar /usr/lib/${project.artifactId}/${project.artifactId}-${project.version}.jar "$@"

View File

@@ -1,5 +1,5 @@
Package: ${project.artifactId} Package: [[artifactId]]
Version: ${project.version} Version: [[version]]
Section: misc Section: misc
Priority: optional Priority: optional
Architecture: all Architecture: all

View File

@@ -1,69 +1,18 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger; package at.pcgamingfreaks.mkvaudiosubtitlechanger;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CommandRunner;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CachedMkvFileProcessor; import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation.ValidationExecutionStrategy;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.AttributeUpdaterKernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.CoherentAttributeUpdaterKernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel.DefaultAttributeUpdaterKernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.MkvFileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.ProjectUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import picocli.CommandLine; import picocli.CommandLine;
import java.util.Set; public class Main {
@Slf4j
@CommandLine.Command(
name = "mkvaudiosubtitlechanger",
usageHelpWidth = 120,
customSynopsis = {
"mkvaudiosubtitlechanger -a <attributeConfig>... -l <libraryPath> [-s]",
"Example: mkvaudiosubtitlechanger -a eng:eng eng:ger -l /mnt/media/ -s",
""
},
mixinStandardHelpOptions = true,
versionProvider = ProjectUtil.class
)
public class Main implements Runnable {
@Getter
@CommandLine.ArgGroup(exclusive = false)
private Config config;
@CommandLine.Spec
CommandLine.Model.CommandSpec spec;
@Override
public void run() {
validate();
Config.setInstance(config);
AttributeUpdaterKernel kernel = Config.getInstance().getCoherent() != null
? new CoherentAttributeUpdaterKernel(new MkvFileCollector(), new CachedMkvFileProcessor())
: new DefaultAttributeUpdaterKernel(new MkvFileCollector(), new CachedMkvFileProcessor());
kernel.execute();
}
private void validate() {
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<Config>> violations = validator.validate(config);
if (!violations.isEmpty()) {
StringBuilder errorMsg = new StringBuilder();
for (ConstraintViolation<Config> violation : violations) {
errorMsg.append("ERROR: ").append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("\n");
}
throw new CommandLine.ParameterException(spec.commandLine(), errorMsg.toString());
}
}
public static void main(String[] args) { public static void main(String[] args) {
if (args.length == 0) { if (args.length == 0) {
args = new String[] { "--help" }; CommandLine.usage(CommandRunner.class, System.out);
return;
} }
new CommandLine(Main.class).execute(args);
new CommandLine(CommandRunner.class)
.setExecutionStrategy(new ValidationExecutionStrategy())
.execute(args);
} }
} }

View File

@@ -1,58 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import picocli.CommandLine;
import java.util.regex.Pattern;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isAudioLanguageValid;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isLanguageValid;
public class AttributeConfigConverter implements CommandLine.ITypeConverter<AttributeConfig> {
private static final String SEPARATOR = ":";
private static final Pattern PATTERN = Pattern.compile("^.{3}:.{3}$");
/**
* Converts the input string into an AttributeConfig object.
*
* @param s The input string containing audio and subtitle language configuration in format "audioLang:subtitleLang"
* @return An AttributeConfig object representing the parsed configuration
* @throws CommandLine.TypeConversionException if the input string is invalid or contains invalid language codes
*/
@Override
public AttributeConfig convert(String s) throws Exception {
validateInput(s);
String[] split = s.split(SEPARATOR);
AttributeConfig attr = new AttributeConfig(split[0], split[1]);
validateResult(attr);
return attr;
}
/**
* Validates that the input string matches the expected pattern.
*
* @param s String to validate
* @throws CommandLine.TypeConversionException if the value doesn't match the expected pattern
*/
private static void validateInput(String s) {
if (!PATTERN.matcher(s).matches()) {
throw new CommandLine.TypeConversionException("Invalid Attribute config: " + s);
}
}
/**
* Validates that both language codes in the AttributeConfig object are valid.
*
* @param attr AttributeConfig object to validate
* @throws CommandLine.TypeConversionException if either language code is invalid
*/
private static void validateResult(AttributeConfig attr) {
if (!isAudioLanguageValid(attr.getAudioLanguage()))
throw new CommandLine.TypeConversionException("Audio language invalid: " + attr.getAudioLanguage());
if (!isLanguageValid(attr.getSubtitleLanguage()))
throw new CommandLine.TypeConversionException("Subtitle language invalid: " + attr.getSubtitleLanguage());
}
}

View File

@@ -1,152 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import jakarta.validation.constraints.Min;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemUtils;
import picocli.CommandLine;
import java.io.File;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
@Slf4j
@Getter
@Setter
@NoArgsConstructor
public class Config {
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private static Config config = null;
private File configPath;
@CommandLine.Spec
CommandLine.Model.CommandSpec spec;
@CommandLine.Option(names = {"-a", "--attribute-config"}, required = true, arity = "1..*", converter = AttributeConfigConverter.class,
description = "List of audio:subtitle pairs used to match in order and update files accordingly (e.g. jpn:eng jpn:ger)")
private List<AttributeConfig> attributeConfig;
@Setter(AccessLevel.NONE)
private File libraryPath;
@CommandLine.Option(names = {"-s", "--safemode"}, description = "test run (no files will be changes)")
private boolean safeMode;
@Setter(AccessLevel.NONE)
private File mkvToolNix;
@Min(value = 1)
@CommandLine.Option(names = {"-t", "--threads"}, defaultValue = "2", description = "thread count (default: ${DEFAULT-VALUE})")
private int threads;
@CommandLine.Option(names = {"-c", "--coherent"}, description = "try to match all files in dir of depth with the same attribute config")
private Integer coherent;
@CommandLine.Option(names = {"-cf", "--force-coherent"}, description = "changes are only applied if it's a coherent match")
private boolean forceCoherent;
@CommandLine.Option(names = {"-n", "--only-new-file"}, description = "sets filter-date to last successful execution (overwrites input of filter-date)")
private boolean onlyNewFiles;
@CommandLine.Option(names = {"-d", "--filter-date"}, defaultValue = CommandLine.Option.NULL_VALUE, description = "only consider files created newer than entered date (format: \"dd.MM.yyyy-HH:mm:ss\")")
private Date filterDate;
@CommandLine.Option(names = {"-i", "--include-pattern"}, defaultValue = ".*", description = "include files matching pattern (default: \".*\")")
private Pattern includePattern;
@CommandLine.Option(names = {"-e", "--excluded-directory"}, arity = "1..*",
description = "Directories to be excluded, combines with config file")
private Set<String> excludedDirectories = new HashSet<>();
@CommandLine.Option(names = {"--forced-keywords"}, arity = "1..*", defaultValue = "forced, signs, songs", split = ", ",
description = "Keywords to identify forced tracks (Defaults will be overwritten; Default: ${DEFAULT-VALUE})")
private Set<String> forcedKeywords;
@CommandLine.Option(names = {"--commentary-keywords"}, arity = "1..*", defaultValue = "commentary, director", split = ", ",
description = "Keywords to identify commentary tracks (Defaults will be overwritten; Default: ${DEFAULT-VALUE})")
private Set<String> commentaryKeywords;
@CommandLine.Option(names = {"--preferred-subtitles"}, arity = "1..*", defaultValue = "unstyled", split = ", ",
description = "Keywords to prefer specific subtitle tracks (Defaults will be overwritten; Default: ${DEFAULT-VALUE})")
private Set<String> preferredSubtitles;
@CommandLine.Option(names = {"-l", "--library"}, required = true, description = "path to library")
public void setLibraryPath(File libraryPath) {
if (!libraryPath.exists()) throw new CommandLine.ParameterException(spec.commandLine(), "Path does not exist: " + libraryPath.getAbsolutePath());
this.libraryPath = libraryPath;
}
static {
// Set default value into system properties to picocli can read the conditional value
System.setProperty("DEFAULT_MKV_TOOL_NIX", SystemUtils.IS_OS_WINDOWS ? "C:\\Program Files\\MKVToolNix" : "/usr/bin/");
}
@CommandLine.Option(names = {"-m", "--mkvtoolnix"}, defaultValue = "${DEFAULT_MKV_TOOL_NIX}", description = "path to mkvtoolnix installation")
public void setMkvToolNix(File mkvToolNix) {
this.mkvToolNix = mkvToolNix;
if (!mkvToolNix.exists()
|| !Path.of(getPathFor(MkvToolNix.MKV_MERGE, SystemUtils.IS_OS_WINDOWS)).toFile().exists()
|| !Path.of(getPathFor(MkvToolNix.MKV_PROP_EDIT, SystemUtils.IS_OS_WINDOWS)).toFile().exists()) {
throw new CommandLine.ParameterException(spec.commandLine(),
"Invalid path to mkvtoolnix installation: " + mkvToolNix.getAbsolutePath());
}
}
public static Config getInstance() {
return getInstance(false);
}
public static Config getInstance(boolean reset) {
if (config == null || reset) {
config = new Config();
}
return config;
}
public static void setInstance(Config c) {
config = c;
}
/**
* Get path to specific mkvtoolnix application.
*
* @return absolute path to desired application.
*/
public String getPathFor(MkvToolNix application) {
return mkvToolNix.getAbsolutePath().endsWith("/")
? mkvToolNix.getAbsolutePath() + application
: mkvToolNix.getAbsolutePath() + "/" + application;
}
public String getPathFor(MkvToolNix application, boolean isWindows) {
return getPathFor(application) + (isWindows ? ".exe" : "");
}
public String getNormalizedLibraryPath() {
return this.getLibraryPath().getAbsolutePath().replace("\\", "/");
}
@Override
public String toString() {
return new StringJoiner(", ", Config.class.getSimpleName() + "[", "]")
.add("configPath=" + configPath)
.add("libraryPath=" + libraryPath)
.add("mkvToolNix=" + mkvToolNix)
.add("threads=" + threads)
.add("includePattern=" + includePattern)
.add("safeMode=" + safeMode)
.add("coherent=" + coherent)
.add("forceCoherent=" + forceCoherent)
.add("onlyNewFiles=" + onlyNewFiles)
.add("filterDate=" + filterDate)
.add("forcedKeywords=" + forcedKeywords)
.add("commentaryKeywords=" + commentaryKeywords)
.add("excludedDirectories=" + excludedDirectories)
.add("preferredSubtitles=" + preferredSubtitles)
.add("attributeConfig=" + attributeConfig)
.toString();
}
}

View File

@@ -1,10 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
@Deprecated
public enum ValidationResult {
VALID,
DEFAULT,
NOT_PRESENT,
MISSING,
INVALID;
}

View File

@@ -1,89 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import org.apache.commons.cli.CommandLine;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isAudioLanguageValid;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isLanguageValid;
@Deprecated
public class AttributeConfigValidator extends ConfigValidator<List<AttributeConfig>> {
private static final String SEPARATOR = ":";
public AttributeConfigValidator() {
super(ConfigProperty.ATTRIBUTE_CONFIG, true, null);
}
/**
* {@inheritDoc}
*/
public ValidationResult validate(YAML yaml, CommandLine cmd) {
System.out.printf("%s: ", property.prop());
List<AttributeConfig> result;
if (cmd.hasOption(property.prop())) {
List<String> values = List.of(cmd.getOptionValues(property.prop()));
result = values.stream().anyMatch(pair -> !pair.contains(SEPARATOR))
? List.of()
: values.stream().map(pair -> pair.split(SEPARATOR))
.map(split -> new AttributeConfig(split[0], split[1]))
.collect(Collectors.toList());
} else if(yaml.getKeysFiltered(property.prop() + ".*").size() > 0) {
Function<String, String> audio = key -> yaml.getString(key + ".audio", null);
Function<String, String> subtitle = key -> yaml.getString(key + ".subtitle", null);
result = yaml.getKeysFiltered(".*audio.*").stream()
.sorted()
.map(key -> key.replace(".audio", ""))
.map(key -> new AttributeConfig(audio.apply(key), subtitle.apply(key)))
.collect(Collectors.toList());
} else if (required) {
System.out.println("missing");
return ValidationResult.MISSING;
} else {
System.out.println("ok");
return ValidationResult.NOT_PRESENT;
}
if (!isValid(result) || !setValue(result)) {
System.out.println("invalid");
return ValidationResult.INVALID;
}
System.out.println("ok");
return ValidationResult.VALID;
}
/**
* {@inheritDoc}
*/
@Override
List<AttributeConfig> parse(String value) {
return null;
}
/**
* {@inheritDoc}
*/
@Override
boolean isValid(List<AttributeConfig> result) {
if (result.isEmpty()) {
return false;
}
boolean isValid;
for (AttributeConfig attributeConfig : result) {
isValid = isAudioLanguageValid(attributeConfig.getAudioLanguage())
&& isLanguageValid(attributeConfig.getSubtitleLanguage());
if (!isValid) return false;
}
return true;
}
}

View File

@@ -1,64 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlKeyNotFoundException;
import org.apache.commons.cli.CommandLine;
import java.util.List;
import java.util.Optional;
import java.util.function.BiFunction;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty.ARGUMENTS;
@Deprecated
public class BooleanValidator extends ConfigValidator<Boolean> {
public BooleanValidator(ConfigProperty property, boolean required) {
super(property, required, null);
}
/**
* {@inheritDoc}
*/
protected BiFunction<YAML, ConfigProperty, Optional<Boolean>> provideDataYaml() {
return (yaml, property) -> {
if (yaml.isSet(ARGUMENTS.prop())
&& yaml.getStringList(ARGUMENTS.prop(), List.of()).contains(property.prop())) {
return Optional.of(true);
}
return Optional.empty();
};
}
/**
* {@inheritDoc}
*/
protected BiFunction<CommandLine, ConfigProperty, Optional<Boolean>> provideDataCmd() {
return (cmd, property) -> {
if (cmd.hasOption(property.prop())) {
return Optional.of(true);
}
return Optional.empty();
};
}
/**
* {@inheritDoc}
* This should not be used.
*/
@Override
Boolean parse(String value) {
throw new RuntimeException("This should not be called");
}
/**
* {@inheritDoc}
* Validation is skipped.
*/
@Override
boolean isValid(Boolean result) {
return true; // skip
}
}

View File

@@ -1,23 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import org.apache.commons.lang3.math.NumberUtils;
@Deprecated
public class CoherentConfigValidator extends ConfigValidator<Integer> {
private static final Integer DISABLED = -1;
public CoherentConfigValidator(ConfigProperty property, boolean required) {
super(property, required, null);
}
@Override
Integer parse(String value) {
return NumberUtils.isParsable(value) ? Integer.parseInt(value) : DISABLED;
}
@Override
boolean isValid(Integer result) {
return result >= 0;
}
}

View File

@@ -1,36 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import java.io.File;
import java.util.Optional;
import java.util.function.BiFunction;
@Deprecated
public class ConfigPathValidator extends PathValidator {
public ConfigPathValidator(ConfigProperty property, boolean required) {
super(property, required, null);
}
/**
* {@inheritDoc}
*/
@Override
protected BiFunction<YAML, ConfigProperty, Optional<File>> provideDataYaml() {
return (yaml, property) -> Optional.empty();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isValid(File result) {
return super.isValid(result) && (result.getAbsolutePath().endsWith(".yml") || result.getAbsolutePath().endsWith(".yaml"));
}
@Override
public int getWeight() {
return 100;
}
}

View File

@@ -1,177 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlKeyNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.StringJoiner;
import java.util.function.BiFunction;
import java.util.function.Predicate;
@Slf4j
@RequiredArgsConstructor
@Deprecated
public abstract class ConfigValidator<FieldType> {
protected final ConfigProperty property;
protected final boolean required;
protected final FieldType defaultValue;
/**
* Validate the user input. Parameters of cmd are prioritised.
*
* @param yaml config file
* @param cmd command line parameters
* @return {@link ValidationResult} containing validity of input.
*/
public ValidationResult validate(YAML yaml, CommandLine cmd) {
System.out.printf("%s: ", property.prop());
FieldType result;
Optional<FieldType> cmdResult = provideDataCmd().apply(cmd, property);
Optional<FieldType> yamlResult = provideDataYaml().apply(yaml, property);
if (isOverwritingNecessary()) {
result = overwriteValue();
} else if (cmdResult.isPresent()) {
result = cmdResult.get();
} else if (yamlResult.isPresent()) {
result = yamlResult.get();
} else {
if (defaultValue != null) {
if (setValue(defaultValue)) {
System.out.println("default");
return ValidationResult.DEFAULT;
} else {
System.out.println("invalid");
return ValidationResult.INVALID;
}
}
if (required) {
System.out.println("missing");
return ValidationResult.MISSING;
} else {
System.out.println("ok");
return ValidationResult.NOT_PRESENT;
}
}
if (!isValid(result) || !setValue(result)) {
System.out.println("invalid");
return ValidationResult.INVALID;
}
System.out.println("ok");
return ValidationResult.VALID;
}
/**
* @return parsed input of yaml config for property
*/
protected BiFunction<YAML, ConfigProperty, Optional<FieldType>> provideDataYaml() {
return (yaml, property) -> {
if (yaml.isSet(property.prop())) {
try {
return Optional.of(parse(yaml.getString(property.prop())));
} catch (YamlKeyNotFoundException e) {
throw new RuntimeException(e);
}
}
return Optional.empty();
};
}
/**
* @return parsed input of command line parameters config for property
*/
protected BiFunction<CommandLine, ConfigProperty, Optional<FieldType>> provideDataCmd() {
return (cmd, property) -> {
if (cmd.hasOption(property.prop())) {
return Optional.of(parse(cmd.getOptionValue(property.prop())));
}
return Optional.empty();
};
}
/**
* @return true if overwriting this property is necessary.
*/
protected boolean isOverwritingNecessary() {
return false;
}
/**
* @return {@link FieldType} to overwrite result with.
*/
protected FieldType overwriteValue() {
return null;
}
/**
* Parse input parameter to desired format.
*
* @param value input parameter
* @return parsed property
*/
abstract FieldType parse(String value);
/**
* Validate if the data has the desired and allowed format.
*
* @param result parsed property
* @return true if data is in desired format.
*/
abstract boolean isValid(FieldType result);
/**
* Sets valid properties to {@link Config} via reflections.
*
* @param result parsed property
* @return false if method invocation failed
*/
protected boolean setValue(FieldType result) {
for (Method method : Config.getInstance().getClass().getDeclaredMethods()) {
if (containsSetterOf(property).test(method)) {
try {
method.invoke(Config.getInstance(), result);
return true;
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
return false;
}
protected Predicate<Method> containsSetterOf(ConfigProperty property) {
return method -> StringUtils.startsWith(method.getName(), "set")
&& StringUtils.equalsIgnoreCase(method.getName().replace("set", ""), property.prop().replace("-", ""));
}
protected Predicate<Method> containsGetterOf(ConfigProperty property) {
return method -> StringUtils.startsWith(method.getName(), "get")
&& StringUtils.equalsIgnoreCase(method.getName().replace("get", ""), property.prop().replace("-", ""));
}
public int getWeight() {
return 50;
}
@Override
public String toString() {
return new StringJoiner(", ", ConfigValidator.class.getSimpleName() + "[", "]")
.add("property=" + property)
.add("required=" + required)
.add("defaultValue=" + defaultValue)
.toString();
}
}

View File

@@ -1,62 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.ProjectUtil;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlInvalidContentException;
import lombok.extern.slf4j.Slf4j;
import net.harawata.appdirs.AppDirsFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Date;
@Slf4j
@Deprecated
public class DateValidator extends ConfigValidator<Date> {
private static final Date INVALID_DATE = new Date(0);
private static final Date DEFAULT_DATE = new Date(1000);
public DateValidator(ConfigProperty property, boolean required) {
super(property, required, null);
}
@Override
protected boolean isOverwritingNecessary() {
return Config.getInstance().isOnlyNewFiles();
}
@Override
protected Date overwriteValue() {
try {
String filePath = AppDirsFactory.getInstance().getUserConfigDir(ProjectUtil.getProjectName(), null, null);
File lastExecutionFile = Path.of(filePath + "/last-execution.yml").toFile();
if (!lastExecutionFile.exists()) {
return DEFAULT_DATE;
}
YAML yaml = new YAML(lastExecutionFile);
return parse(yaml.getString(Config.getInstance().getNormalizedLibraryPath(), DateUtils.convert(DEFAULT_DATE)));
} catch (YamlInvalidContentException | IOException e) {
log.error("Couldn't open last-execution.properties");
return INVALID_DATE;
}
}
@Override
Date parse(String value) {
return DateUtils.convert(value, INVALID_DATE);
}
@Override
boolean isValid(Date result) {
return !result.equals(INVALID_DATE);
}
@Override
public int getWeight() {
return 40;
}
}

View File

@@ -1,27 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import java.io.File;
import java.nio.file.Path;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix.MKV_MERGE;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix.MKV_PROP_EDIT;
@Deprecated
public class MkvToolNixPathValidator extends PathValidator {
private static final String EXE = ".exe";
public MkvToolNixPathValidator(ConfigProperty property, boolean required, File defaultValue) {
super(property, required, defaultValue);
}
@Override
protected boolean isValid(File result) {
return result.isDirectory()
&& (Path.of(result.getAbsolutePath() + "/" + MKV_MERGE + EXE).toFile().isFile()
&& Path.of(result.getAbsolutePath() + "/" + MKV_PROP_EDIT + EXE).toFile().isFile())
|| (Path.of(result.getAbsolutePath() + "/" + MKV_MERGE).toFile().isFile()
&& Path.of(result.getAbsolutePath() + "/" + MKV_PROP_EDIT).toFile().isFile());
}
}

View File

@@ -1,31 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import java.io.File;
import java.nio.file.Path;
@Deprecated
public class PathValidator extends ConfigValidator<File> {
public PathValidator(ConfigProperty property, boolean required, File defaultValue) {
super(property, required, defaultValue);
}
/**
* {@inheritDoc}
*/
@Override
protected File parse(String value) {
return Path.of(value).toFile();
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isValid(File result) {
return result.isDirectory() || result.isFile();
}
}

View File

@@ -1,35 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
@Deprecated
public class PatternValidator extends ConfigValidator<Pattern> {
private static final Pattern EMPTY_PATTERN = Pattern.compile("");
public PatternValidator(ConfigProperty property, boolean required, Pattern defaultValue) {
super(property, required, defaultValue);
}
/**
* {@inheritDoc}
*/
@Override
Pattern parse(String value) {
try {
return Pattern.compile(value);
} catch (PatternSyntaxException e) {
return EMPTY_PATTERN;
}
}
/**
* {@inheritDoc}
*/
@Override
boolean isValid(Pattern result) {
return !result.equals(EMPTY_PATTERN);
}
}

View File

@@ -1,112 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlKeyNotFoundException;
import org.apache.commons.cli.CommandLine;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
@Deprecated
public class SetValidator extends ConfigValidator<Set<String>> {
private final boolean append;
public SetValidator(ConfigProperty property, boolean required, boolean append) {
super(property, required, null);
this.append = append;
}
public ValidationResult validate(YAML yaml, CommandLine cmd) {
System.out.printf("%s: ", property.prop());
List<String> resultList = null;
if (cmd.hasOption(property.prop())) {
resultList = List.of(cmd.getOptionValues(property.prop()));
} else if (yaml.isSet(property.prop())) {
try {
resultList = yaml.getStringList(property.prop());
} catch (YamlKeyNotFoundException ignored) {}
} else if (required) {
System.out.println("missing");
return ValidationResult.MISSING;
} else {
System.out.println("ok");
return ValidationResult.NOT_PRESENT;
}
Set<String> result = parse(resultList);
if (!isValid(result) || !setValue(result)) {
System.out.println("invalid");
return ValidationResult.INVALID;
}
System.out.println("ok");
return ValidationResult.VALID;
}
protected BiFunction<YAML, ConfigProperty, Optional<Set<String>>> provideDataYaml() {
return (yaml, property) -> {
if (yaml.isSet(property.prop())) {
try {
return Optional.of(parse(yaml.getStringList(property.prop())));
} catch (YamlKeyNotFoundException ignored) {
}
}
return Optional.empty();
};
}
protected BiFunction<CommandLine, ConfigProperty, Optional<Set<String>>> provideDataCmd() {
return (cmd, property) -> {
if (cmd.hasOption(property.prop())) {
return Optional.of(parse(List.of(cmd.getOptionValues(property.prop()))));
}
return Optional.empty();
};
}
@Override
Set<String> parse(String value) {
throw new RuntimeException("This should not be called");
}
protected Set<String> parse(List<String> value) {
return new HashSet<>(value);
}
@Override
boolean isValid(Set<String> result) {
return true;
}
@SuppressWarnings("unchecked")
protected boolean setValue(Set<String> result) {
List<Method> methods = append
? Arrays.stream(Config.getInstance().getClass().getDeclaredMethods())
.filter(containsGetterOf(property))
.collect(Collectors.toList())
: Arrays.stream(Config.getInstance().getClass().getDeclaredMethods())
.filter(containsSetterOf(property))
.collect(Collectors.toList());
if (methods.size() != 1) {
return false;
}
try {
if (append) {
((Set<String>) methods.get(0).invoke(Config.getInstance())).addAll(result);
} else {
methods.get(0).invoke(Config.getInstance(), result);
}
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
return true;
}
}

View File

@@ -1,27 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import org.apache.commons.lang3.math.NumberUtils;
@Deprecated
public class ThreadValidator extends ConfigValidator<Integer>{
public ThreadValidator(ConfigProperty property, boolean required, Integer defaultValue) {
super(property, required, defaultValue);
}
/**
* {@inheritDoc}
*/
@Override
Integer parse(String value) {
return NumberUtils.isParsable(value) ? Integer.parseInt(value) : defaultValue;
}
/**
* {@inheritDoc}
*/
@Override
boolean isValid(Integer result) {
return result > 0;
}
}

View File

@@ -1,15 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import java.io.File;
import java.util.List;
public class CachedMkvFileProcessor extends MkvFileProcessor {
Cache<File, List<FileAttribute>> cache = new Cache<>();
@Override
public List<FileAttribute> loadAttributes(File file) {
return cache.retrieve(file, super::loadAttributes);
}
}

View File

@@ -0,0 +1,51 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors.*;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.ProjectUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.core.config.Configurator;
import picocli.CommandLine;
@Slf4j
@CommandLine.Command(
name = "mkvaudiosubtitlechanger",
usageHelpAutoWidth = true,
customSynopsis = {
"mkvaudiosubtitlechanger [-a <attributeConfig> [...<attributeConfig>]] [-s] <libraryPath>",
"Example: mkvaudiosubtitlechanger -a eng:eng eng:ger -s /mnt/media/",
""
},
requiredOptionMarker = '*',
sortOptions = false,
mixinStandardHelpOptions = true,
versionProvider = ProjectUtil.class
)
public class CommandRunner implements Runnable {
@Getter
@CommandLine.ArgGroup(exclusive = false)
private InputConfig config;
@Override
public void run() {
if (config.isDebug()) {
Configurator.setRootLevel(Level.DEBUG);
}
if (config.isSafeMode()) {
log.info("Safemode active. No files will be changed!");
System.out.println("Safemode active. No files will be changed!");
}
FileFilter fileFilter = new FileFilter(config.getExcluded(), config.getIncludePattern(), config.getFilterDate());
FileProcessor fileProcessor = new CachedFileProcessor(new MkvFileProcessor(config.getMkvToolNix(), fileFilter));
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(config.getPreferredSubtitles().toArray(new String[0]), config.getForcedKeywords(), config.getCommentaryKeywords(), config.getHearingImpaired());
AttributeUpdater kernel = config.getCoherent() != null
? new CoherentAttributeUpdater(config, fileProcessor, attributeChangeProcessor)
: new SingleFileAttributeUpdater(config, fileProcessor, attributeChangeProcessor);
kernel.execute();
}
}

View File

@@ -1,22 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import java.io.File;
import java.util.List;
public interface FileCollector {
/**
* @param path leads to one file directly or a directory which will be loaded recursively
* @return list of all files within the directory
*/
List<File> loadFiles(String path);
/**
* Load all directories from path, but only until depth is reached.
*
* @param path leads to a directory which will be loaded recursively until depth
* @param depth limit directory crawling
* @return list of directory until depth
*/
List<File> loadDirectories(String path, int depth);
}

View File

@@ -1,52 +1,89 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl; package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils; import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j @Slf4j
@RequiredArgsConstructor
public class FileFilter { public class FileFilter {
static boolean accept(File pathName, String[] fileExtensions) { private final Set<String> excluded;
if (hasProperFileExtension(pathName, fileExtensions) private final Pattern includePattern;
&& hasMatchingPattern(pathName) private final Date filterDate;
&& isNewer(pathName)) {
return true; private final String EXTENSION_GROUP = "extension";
private final Pattern extensionPattern = Pattern.compile(String.format(".*(?<%s>\\..*)", EXTENSION_GROUP));
public boolean accept(File pathName, Set<String> fileExtensions) {
// Ignore files irrelevant for statistics
if (!hasProperFileExtension(pathName, new HashSet<>(fileExtensions))) {
log.debug("Ignored {}", pathName);
return false;
} }
ResultStatistic.getInstance().total(); if (!hasMatchingPattern(pathName)
ResultStatistic.getInstance().excluded(); || !isNewer(pathName)
return false; || isExcluded(pathName, new HashSet<>(excluded))) {
log.debug("Excluded {}", pathName);
ResultStatistic.getInstance().excluded();
return false;
}
return true;
} }
private static boolean hasProperFileExtension(File pathName, String[] fileExtensions) { private boolean hasProperFileExtension(File pathName, Set<String> fileExtensions) {
return StringUtils.endsWithAny(pathName.getAbsolutePath().toLowerCase(), fileExtensions); Matcher matcher = extensionPattern.matcher(pathName.getName());
return matcher.find() && fileExtensions.contains(matcher.group(EXTENSION_GROUP));
} }
private static boolean hasMatchingPattern(File pathName) { private boolean hasMatchingPattern(File pathName) {
return Config.getInstance().getIncludePattern().matcher(pathName.getName()).matches(); return includePattern.matcher(pathName.getName()).matches();
} }
private static boolean isNewer(File pathName) { private boolean isNewer(File pathName) {
Config config = Config.getInstance(); if (filterDate == null) return true;
if (config.getFilterDate() == null) return true;
try { try {
BasicFileAttributes attributes = Files.readAttributes(pathName.toPath(), BasicFileAttributes.class); BasicFileAttributes attributes = Files.readAttributes(pathName.toPath(), BasicFileAttributes.class);
return isNewer(DateUtils.convert(attributes.creationTime().toMillis())); return isNewer(DateUtils.convert(attributes.creationTime().toMillis()));
} catch (IOException e) { } catch (IOException e) {
log.warn("File attributes could not be read.", e); log.warn("File attributes could not be read", e);
} }
return true; return true;
} }
private static boolean isNewer(Date creationDate) { private boolean isNewer(Date creationDate) {
return creationDate.toInstant().isAfter(Config.getInstance().getFilterDate().toInstant()); return creationDate.toInstant().isAfter(filterDate.toInstant());
}
private boolean isExcluded(File pathName, Set<String> excludedDirs) {
if (excludedDirs.contains(pathName.getPath())) return true;
String[] pathSplit = pathName.getPath().split("/");
for (String excludedDir : excludedDirs) {
String[] excludeSplit = excludedDir.split("/");
if (excludeSplit.length > pathSplit.length) continue;
boolean matchingPaths = true;
for (int i = 0; i < excludeSplit.length; i++) {
if (!excludeSplit[i].equals(pathSplit[i])) {
matchingPaths = false;
break;
}
}
if (matchingPaths) return true;
}
return false;
} }
} }

View File

@@ -1,51 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfoDto;
import java.io.File;
import java.io.IOException;
import java.util.List;
public interface FileProcessor {
/**
* Load track information from file.
*
* @param file Takes the file from which the attributes will be returned
* @return list of all important attributes
*/
List<FileAttribute> loadAttributes(File file);
/**
* Populate FileInfoDto with the currently set default tracks.
* @param info to be populated
* @param attributes Track information of FileInfoDto
* @param nonForcedTracks List of all not forced tracks
*/
void detectDefaultTracks(FileInfoDto info, List<FileAttribute> attributes, List<FileAttribute> nonForcedTracks);
/**
* Populate FileInfoDto with the desired tracks, based on AttributeConfig.
* @param info to be populated
* @param nonForcedTracks List of all non-forced tracks
* @param nonCommentaryTracks List of all non-commentary tracks
*/
void detectDesiredTracks(FileInfoDto info, List<FileAttribute> nonForcedTracks, List<FileAttribute> nonCommentaryTracks,
AttributeConfig... configs);
List<FileAttribute> retrieveNonForcedTracks(List<FileAttribute> attributes);
List<FileAttribute> retrieveNonCommentaryTracks(List<FileAttribute> attributes);
/**
* Update the file.
* @param file to be updated
* @param fileInfo information to update file
* @throws IOException when error occurs accessing file retrieving information
* @throws MkvToolNixException when error occurs while sending query to mkvpropedit
*/
void update(File file, FileInfoDto fileInfo) throws IOException, MkvToolNixException;
}

View File

@@ -1,49 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
public class MkvFileCollector implements FileCollector {
private static final String[] fileExtensions = new String[]{".mkv", ".mka", ".mks", ".mk3d"};
/**
* {@inheritDoc}
*/
@Override
public List<File> loadFiles(String path) {
try (Stream<Path> paths = Files.walk(Paths.get(path))) {
return paths.filter(Files::isRegularFile)
.map(Path::toFile)
.filter(file -> FileFilter.accept(file, fileExtensions))
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Couldn't find file or directory!", e);
return new ArrayList<>();
}
}
/**
* {@inheritDoc}
*/
@Override
public List<File> loadDirectories(String path, int depth) {
try (Stream<Path> paths = Files.walk(Paths.get(path), depth)) {
return paths.map(Path::toFile)
.filter(File::isDirectory)
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Couldn't find file or directory!", e);
return new ArrayList<>();
}
}
}

View File

@@ -1,194 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.*;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.SetUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.core.util.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.LaneType.AUDIO;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.LaneType.SUBTITLES;
import static java.lang.String.format;
@Slf4j
public class MkvFileProcessor implements FileProcessor {
private final ObjectMapper mapper = new ObjectMapper();
private static final SubtitleTrackComparator subtitleTrackComparator =
new SubtitleTrackComparator(Config.getInstance().getPreferredSubtitles().toArray(new String[0]));
private static final String DISABLE_DEFAULT_TRACK = "--edit track:%s --set flag-default=0 ";
private static final String ENABLE_DEFAULT_TRACK = "--edit track:%s --set flag-default=1 ";
private static final String DISABLE_FORCED_TRACK = "--edit track:%s --set flag-forced=0 ";
private static final String ENABLE_FORCED_TRACK = "--edit track:%s --set flag-forced=1 ";
@SuppressWarnings("unchecked")
@Override
public List<FileAttribute> loadAttributes(File file) {
Map<String, Object> jsonMap;
List<FileAttribute> fileAttributes = new ArrayList<>();
try {
String[] command = new String[]{
Config.getInstance().getPathFor(MkvToolNix.MKV_MERGE),
"--identify",
"--identification-format",
"json",
file.getAbsolutePath()
};
log.debug("{}", String.join(" ", command));
InputStream inputStream = Runtime.getRuntime().exec(command)
.getInputStream();
jsonMap = mapper.readValue(inputStream, Map.class);
List<Map<String, Object>> tracks = (List<Map<String, Object>>) jsonMap.get("tracks");
if (tracks == null) {
log.warn("Couldn't retrieve information of {}", file.getAbsolutePath());
return new ArrayList<>();
}
for (Map<String, Object> attribute : tracks) {
if (!"video".equals(attribute.get("type"))) {
Map<String, Object> properties = (Map<String, Object>) attribute.get("properties");
fileAttributes.add(new FileAttribute(
(int) properties.get("number"),
(String) properties.get("language"),
(String) properties.get("track_name"),
(Boolean) properties.getOrDefault("default_track", false),
(Boolean) properties.getOrDefault("forced_track", false),
LaneType.valueOf(((String) attribute.get("type")).toUpperCase(Locale.ENGLISH))));
}
}
log.debug(fileAttributes.toString());
} catch (IOException e) {
log.error("File could not be found or loaded: ", e);
System.out.println("File could not be found or loaded: " + file.getAbsolutePath());
}
return fileAttributes;
}
/**
* {@inheritDoc}
*/
@Override
public void detectDefaultTracks(FileInfoDto info, List<FileAttribute> attributes, List<FileAttribute> nonForcedTracks) {
for (FileAttribute attribute : attributes) {
if (AUDIO.equals(attribute.getType())) {
if (attribute.isDefaultTrack()) info.getExistingDefaultAudioLanes().add(attribute);
if (attribute.isForcedTrack()) info.getExistingForcedAudioLanes().add(attribute);
} else if (SUBTITLES.equals(attribute.getType())) {
if (attribute.isDefaultTrack()) info.getExistingDefaultSubtitleLanes().add(attribute);
if (attribute.isForcedTrack()) info.getExistingForcedSubtitleLanes().add(attribute);
else if (!nonForcedTracks.contains(attribute)) info.getDesiredForcedSubtitleLanes().add(attribute);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void detectDesiredTracks(FileInfoDto info, List<FileAttribute> nonForcedTracks, List<FileAttribute> nonCommentaryTracks,
AttributeConfig... configs) {
Set<FileAttribute> tracks = SetUtils.retainOf(nonForcedTracks, nonCommentaryTracks);
Set<FileAttribute> audioTracks = tracks.stream().filter(a -> AUDIO.equals(a.getType())).collect(Collectors.toSet());
Set<FileAttribute> subtitleTracks = tracks.stream().filter(a -> SUBTITLES.equals(a.getType())).collect(Collectors.toSet());
for (AttributeConfig config : configs) {
Optional<FileAttribute> desiredAudio = detectDesiredTrack(config.getAudioLanguage(), audioTracks).findFirst();
Optional<FileAttribute> desiredSubtitle = detectDesiredSubtitleTrack(config.getSubtitleLanguage(), subtitleTracks).findFirst();
if (desiredAudio.isPresent() && ("OFF".equals(config.getSubtitleLanguage()) || desiredSubtitle.isPresent())) {
info.setMatchedConfig(config);
info.setDesiredDefaultAudioLane(desiredAudio.get());
info.setDesiredDefaultSubtitleLane(desiredSubtitle.orElse(null));
break;
}
}
}
private Stream<FileAttribute> detectDesiredTrack(String language, Set<FileAttribute> tracks) {
return tracks.stream().filter(track -> language.equals(track.getLanguage()));
}
private Stream<FileAttribute> detectDesiredSubtitleTrack(String language, Set<FileAttribute> tracks) {
return detectDesiredTrack(language, tracks)
.sorted(subtitleTrackComparator.reversed());
}
@Override
public List<FileAttribute> retrieveNonForcedTracks(List<FileAttribute> attributes) {
return attributes.stream()
.filter(elem -> !StringUtils.containsAnyIgnoreCase(elem.getTrackName(),
Config.getInstance().getForcedKeywords().toArray(new CharSequence[0])))
.filter(elem -> !elem.isForcedTrack())
.collect(Collectors.toList());
}
@Override
public List<FileAttribute> retrieveNonCommentaryTracks(List<FileAttribute> attributes) {
return attributes.stream()
.filter(elem -> !StringUtils.containsAnyIgnoreCase(elem.getTrackName(),
Config.getInstance().getCommentaryKeywords().toArray(new CharSequence[0])))
.collect(Collectors.toList());
}
/**
* {@inheritDoc}
*/
@Override
public void update(File file, FileInfoDto fileInfo) throws IOException, MkvToolNixException {
StringBuilder sb = new StringBuilder();
sb.append(format("\"%s\" ", Config.getInstance().getPathFor(MkvToolNix.MKV_PROP_EDIT)));
sb.append(format("\"%s\" ", file.getAbsolutePath()));
if (fileInfo.isAudioDifferent()) {
if (fileInfo.getExistingDefaultAudioLanes() != null && !fileInfo.getExistingDefaultAudioLanes().isEmpty()) {
for (FileAttribute track : fileInfo.getExistingDefaultAudioLanes()) {
sb.append(format(DISABLE_DEFAULT_TRACK, track.getId()));
}
}
sb.append(format(ENABLE_DEFAULT_TRACK, fileInfo.getDesiredDefaultAudioLane().getId()));
}
if (!fileInfo.getExistingForcedAudioLanes().isEmpty()) {
for (FileAttribute track : fileInfo.getExistingForcedAudioLanes()) {
sb.append(format(DISABLE_FORCED_TRACK, track.getId()));
}
}
if (fileInfo.isSubtitleDifferent()) {
if (fileInfo.getExistingDefaultSubtitleLanes() != null && !fileInfo.getExistingDefaultSubtitleLanes().isEmpty()) {
for (FileAttribute track : fileInfo.getExistingDefaultSubtitleLanes()) {
sb.append(format(DISABLE_DEFAULT_TRACK, track.getId()));
}
}
if (fileInfo.getDesiredDefaultSubtitleLane() != null) {
sb.append(format(ENABLE_DEFAULT_TRACK, fileInfo.getDesiredDefaultSubtitleLane().getId()));
}
}
if (fileInfo.areForcedTracksDifferent()) {
for (FileAttribute attribute : fileInfo.getDesiredForcedSubtitleLanes()) {
sb.append(format(ENABLE_FORCED_TRACK, attribute.getId()));
}
}
log.debug(sb.toString());
InputStream inputstream = Runtime.getRuntime().exec(sb.toString()).getInputStream();
String output = IOUtils.toString(new InputStreamReader(inputstream));
log.debug(output);
if (output.contains("Error")) throw new MkvToolNixException(output);
}
}

View File

@@ -1,32 +1,49 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl; package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import lombok.RequiredArgsConstructor; import org.apache.logging.log4j.util.Strings;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
@RequiredArgsConstructor public class SubtitleTrackComparator implements Comparator<TrackAttributes> {
public class SubtitleTrackComparator implements Comparator<FileAttribute> { private final Set<String> preferredSubtitles;
private final String[] preferredSubtitles; private final Set<String> hearingImpairedKeywords;
public SubtitleTrackComparator(Collection<String> preferredSubtitles, Collection<String> hearingImpairedKeywords) {
this.preferredSubtitles = new HashSet<>(preferredSubtitles.stream().map(String::toLowerCase).toList());
this.hearingImpairedKeywords = new HashSet<>(hearingImpairedKeywords.stream().map(String::toLowerCase).toList());
}
/** /**
* {@inheritDoc} * {@inheritDoc}
*/ */
@Override @Override
public int compare(FileAttribute track1, FileAttribute track2) { public int compare(TrackAttributes track1, TrackAttributes track2) {
int result = 0; int result = 0;
if (StringUtils.containsAnyIgnoreCase(track1.getTrackName(), preferredSubtitles)) { String track1Name = Strings.isNotBlank(track1.trackName()) ? track1.trackName().toLowerCase() : "";
result++; String track2Name = Strings.isNotBlank(track2.trackName()) ? track2.trackName().toLowerCase() : "";
}
if (StringUtils.containsAnyIgnoreCase(track2.getTrackName(), preferredSubtitles)) { if (preferredSubtitles.contains(track1Name)) result++;
result--; else for (String keyword: preferredSubtitles) if (track1Name.contains(keyword)) result++;
}
if (preferredSubtitles.contains(track2Name)) result--;
else for (String keyword: preferredSubtitles) if (track2Name.contains(keyword)) result--;
if (track1.hearingImpaired()) result--;
else if (hearingImpairedKeywords.contains(track1Name)) result--;
else for (String keyword: hearingImpairedKeywords) if (track1Name.contains(keyword)) result--;
if (track2.hearingImpaired()) result++;
else if (hearingImpairedKeywords.contains(track2Name)) result++;
else for (String keyword: hearingImpairedKeywords) if (track2Name.contains(keyword)) result++;
if (result == 0) { if (result == 0) {
if (track1.isDefaultTrack()) result++; if (track1.defaultt()) result++;
if (track2.isDefaultTrack()) result--; if (track2.defaultt()) result--;
} }
return result; return result;

View File

@@ -0,0 +1,47 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.converter;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import picocli.CommandLine;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isLanguageValid;
public class AttributeConfigConverter implements CommandLine.ITypeConverter<AttributeConfig> {
private static final String AUDIO_GROUP = "audio";
private static final String SUB_GROUP = "sub";
private static final Pattern PATTERN = Pattern.compile(String.format("^(?<%s>.{3}):(?<%s>.{3})$", AUDIO_GROUP, SUB_GROUP));
/**
* Converts the input string into an AttributeConfig object.
*
* @param s The input string containing audio and subtitle language configuration in format "audioLang:subtitleLang"
* @return An AttributeConfig object representing the parsed configuration
* @throws CommandLine.TypeConversionException if the input string is invalid or contains invalid language codes
*/
@Override
public AttributeConfig convert(String s) {
Matcher matcher = PATTERN.matcher(s);
if (!matcher.find()) throw new CommandLine.TypeConversionException("Invalid Attribute config: " + s);
return validateResult(new AttributeConfig(matcher.group(AUDIO_GROUP), matcher.group(SUB_GROUP)));
}
/**
* Validates that both language codes in the {@link AttributeConfig} object are valid.
*
* @param attr {@link AttributeConfig} object to validate
* @throws CommandLine.TypeConversionException if either language code is invalid
* @return valid {@link AttributeConfig}
*/
private static AttributeConfig validateResult(AttributeConfig attr) {
if (!isLanguageValid(attr.getAudioLanguage()))
throw new CommandLine.TypeConversionException("Audio language invalid: " + attr.getAudioLanguage());
if (!isLanguageValid(attr.getSubtitleLanguage()))
throw new CommandLine.TypeConversionException("Subtitle language invalid: " + attr.getSubtitleLanguage());
return attr;
}
}

View File

@@ -1,179 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfoDto;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.ProjectUtil;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlInvalidContentException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBar;
import me.tongfei.progressbar.ProgressBarBuilder;
import me.tongfei.progressbar.ProgressBarStyle;
import net.harawata.appdirs.AppDirsFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@RequiredArgsConstructor
public abstract class AttributeUpdaterKernel {
protected final FileCollector collector;
protected final FileProcessor processor;
protected final ResultStatistic statistic = ResultStatistic.getInstance();
private final ExecutorService executor = Executors.newFixedThreadPool(Config.getInstance().getThreads());
protected ProgressBarBuilder pbBuilder() {
return new ProgressBarBuilder()
.setStyle(ProgressBarStyle.ASCII)
.setUpdateIntervalMillis(250)
.setMaxRenderedLength(75);
}
@SneakyThrows
public void execute() {
statistic.startTimer();
try (ProgressBar progressBar = pbBuilder().build()) {
List<File> files = loadFiles(Config.getInstance().getLibraryPath().getAbsolutePath());
progressBar.maxHint(files.size());
files.forEach(file -> executor.submit(() -> {
process(file);
progressBar.step();
}));
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
}
endProcess();
statistic.stopTimer();
statistic.printResult();
}
protected List<File> loadExcludedFiles() {
List<File> excludedFiles = Config.getInstance().getExcludedDirectories().stream()
.map(collector::loadFiles)
.flatMap(Collection::stream)
.collect(Collectors.toList());
statistic.increaseTotalBy(excludedFiles.size());
statistic.increaseExcludedBy(excludedFiles.size());
return excludedFiles;
}
/**
* Load files or directories to update.
* Remove excluded directories.
*
* @param path Path to library
* @return List of files to update.
*/
abstract List<File> loadFiles(String path);
/**
* Start of the file updating process.
* This method is called by the executor and its contents are executed in parallel.
*
* @param file file or directory to update
*/
void process(File file) {
FileInfoDto fileInfo = new FileInfoDto(file);
List<FileAttribute> attributes = processor.loadAttributes(file);
if (attributes == null || attributes.isEmpty()) {
statistic.total();
statistic.failure();
return;
}
List<FileAttribute> nonForcedTracks = processor.retrieveNonForcedTracks(attributes);
List<FileAttribute> nonCommentaryTracks = processor.retrieveNonCommentaryTracks(attributes);
processor.detectDefaultTracks(fileInfo, attributes, nonForcedTracks);
processor.detectDesiredTracks(fileInfo, nonForcedTracks, nonCommentaryTracks,
Config.getInstance().getAttributeConfig().toArray(new AttributeConfig[]{}));
updateFile(fileInfo);
}
/**
* Persist file changes.
*
* @param fileInfoDto contains information about file and desired configuration.
*/
protected void updateFile(FileInfoDto fileInfoDto) {
statistic.total();
switch (fileInfoDto.getStatus()) {
case CHANGE_NECESSARY:
statistic.shouldChange();
commitChange(fileInfoDto);
break;
case NO_SUITABLE_CONFIG:
statistic.noSuitableConfigFound();
break;
case ALREADY_SUITED:
statistic.alreadyFits();
break;
case UNKNOWN:
default:
statistic.failure();
break;
}
}
private void commitChange(FileInfoDto fileInfo) {
if (Config.getInstance().isSafeMode()) {
return;
}
try {
processor.update(fileInfo.getFile(), fileInfo);
statistic.success();
log.info("Updated {}", fileInfo.getFile().getAbsolutePath());
} catch (IOException | MkvToolNixException e) {
statistic.failedChanging();
log.warn("File couldn't be updated: '{}', Error: {}", fileInfo.getFile().getAbsoluteFile(), e.getMessage());
}
}
protected void endProcess() {
if (Config.getInstance().isSafeMode()) {
return;
}
try {
String filePath = AppDirsFactory.getInstance().getUserConfigDir(ProjectUtil.getProjectName(), null, null);
File configDir = Path.of(filePath).toFile();
if (!configDir.exists()) configDir.mkdirs();
File lastExecutionFile = Path.of(filePath + "/last-execution.yml").toFile();
if (!lastExecutionFile.exists()) lastExecutionFile.createNewFile();
YAML yaml = new YAML(lastExecutionFile);
yaml.set(Config.getInstance().getNormalizedLibraryPath(), DateUtils.convert(new Date()));
yaml.save(lastExecutionFile);
} catch (IOException | YamlInvalidContentException e) {
log.error("last-execution.yml could not be created or read.", e);
}
}
}

View File

@@ -1,109 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfoDto;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class CoherentAttributeUpdaterKernel extends AttributeUpdaterKernel {
public CoherentAttributeUpdaterKernel(FileCollector collector, FileProcessor processor) {
super(collector, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" directories", 1);
}
/**
* {@inheritDoc}
*/
@Override
List<File> loadFiles(String path) {
return loadFiles(path, Config.getInstance().getCoherent());
}
List<File> loadFiles(String path, int depth) {
List<File> excludedFiles = loadExcludedFiles();
List<File> directories = collector.loadDirectories(path, depth)
.stream().filter(file -> !excludedFiles.contains(file))
.collect(Collectors.toList());
return directories.stream()
.filter(dir -> isParentDirectory(dir, directories))
.collect(Collectors.toList());
}
private boolean isParentDirectory(File directory, List<File> directories) {
String path = directory.getAbsolutePath();
return directories.stream()
.noneMatch(dir -> dir.getAbsolutePath().contains(path) && !StringUtils.equals(path, dir.getAbsolutePath()));
}
/**
* Update files in directory, if possible, with the same {@link AttributeConfig}.
* If {@link Config#isForceCoherent()} then there will be no changes to the file if they don't match the same config.
* Otherwise, the default behaviour is executed.
* This method is called by the executor and is run in parallel.
*
* @param file directory containing files
*/
@Override
void process(File file) {
process(file, Config.getInstance().getCoherent());
}
void process(File file, int depth) {
// TODO: Implement level crawl if coherence is not possible on user entered depth
// IMPL idea: recursive method call, cache needs to be implemented
List<FileInfoDto> fileInfos = collector.loadFiles(file.getAbsolutePath()).stream()
.map(FileInfoDto::new)
.collect(Collectors.toList());
for (AttributeConfig config : Config.getInstance().getAttributeConfig()) {
for (FileInfoDto fileInfo : fileInfos) {
List<FileAttribute> attributes = processor.loadAttributes(fileInfo.getFile());
List<FileAttribute> nonForcedTracks = processor.retrieveNonForcedTracks(attributes);
List<FileAttribute> nonCommentaryTracks = processor.retrieveNonCommentaryTracks(attributes);
processor.detectDefaultTracks(fileInfo, attributes, nonForcedTracks);
processor.detectDesiredTracks(fileInfo, nonForcedTracks, nonCommentaryTracks, config);
}
if (fileInfos.stream().allMatch(elem -> ("OFF".equals(config.getSubtitleLanguage()) || elem.getDesiredDefaultSubtitleLane() != null)
&& elem.getDesiredDefaultAudioLane() != null)) {
log.info("Found {}/{} match for {}", config.getAudioLanguage(), config.getSubtitleLanguage(), file.getAbsolutePath());
fileInfos.forEach(this::updateFile);
return; // match found, end process here
}
fileInfos.forEach(f -> {
f.setDesiredDefaultAudioLane(null);
f.setDesiredDefaultSubtitleLane(null);
});
}
log.info("No coherent match found for {}", file.getAbsoluteFile());
for (FileInfoDto fileInfo : fileInfos) {
if (!Config.getInstance().isForceCoherent()) {
super.process(fileInfo.getFile());
} else {
statistic.excluded();
}
}
}
}

View File

@@ -1,36 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.kernel;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileProcessor;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
public class DefaultAttributeUpdaterKernel extends AttributeUpdaterKernel {
public DefaultAttributeUpdaterKernel(FileCollector collector, FileProcessor processor) {
super(collector, processor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" files", 1);
}
/**
* {@inheritDoc}
*/
@Override
List<File> loadFiles(String path) {
List<File> excludedFiles = loadExcludedFiles();
return collector.loadFiles(Config.getInstance().getLibraryPath().getAbsolutePath()).stream()
.filter(file -> !excludedFiles.contains(file))
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,119 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.SubtitleTrackComparator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.*;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class AttributeChangeProcessor {
private final SubtitleTrackComparator subtitleTrackComparator;
private final Set<String> commentaryKeywords;
private final Set<String> hearingImpairedKeywords;
private final Set<String> forcedKeywords;
public AttributeChangeProcessor(String[] preferredSubtitles, Set<String> forcedKeywords, Set<String> commentaryKeywords, Set<String> hearingImpairedKeywords) {
this.subtitleTrackComparator = new SubtitleTrackComparator(Arrays.stream(preferredSubtitles).toList(), hearingImpairedKeywords);
this.commentaryKeywords = commentaryKeywords;
this.hearingImpairedKeywords = hearingImpairedKeywords;
this.forcedKeywords = forcedKeywords;
}
private List<TrackAttributes> filterForPossibleDefaults(List<TrackAttributes> tracks) {
Stream<TrackAttributes> attributes = tracks.stream();
return attributes
.filter(attr -> !attr.commentary())
.filter(attr -> {
if (attr.trackName() == null) return true;
return commentaryKeywords.stream().noneMatch(keyword -> keyword.compareToIgnoreCase(attr.trackName()) == 0);
})
.filter(attr -> !attr.forced())
.filter(attr -> {
if (attr.trackName() == null) return true;
return forcedKeywords.stream().noneMatch(keyword -> keyword.compareToIgnoreCase(attr.trackName()) == 0);
})
.toList();
}
public void findDefaultMatchAndApplyChanges(FileInfo fileInfo, AttributeConfig... configs) {
Map<String, List<TrackAttributes>> audiosByLanguage = new HashMap<>(fileInfo.getTracks().size());
Map<String, List<TrackAttributes>> subsByLanguage = new HashMap<>(fileInfo.getTracks().size());
filterForPossibleDefaults(fileInfo.getTracks()).forEach(track -> {
if (TrackType.AUDIO.equals(track.type()))
audiosByLanguage.computeIfAbsent(track.language(), (k) -> new ArrayList<>()).add(track);
else if (TrackType.SUBTITLES.equals(track.type()))
subsByLanguage.computeIfAbsent(track.language(), (k) -> new ArrayList<>()).add(track);
});
for (AttributeConfig config : configs) {
if (("OFF".equals(config.getAudioLanguage()) || audiosByLanguage.containsKey(config.getAudioLanguage()))
&& ("OFF".equals(config.getSubtitleLanguage()) || subsByLanguage.containsKey(config.getSubtitleLanguage()))) {
fileInfo.setMatchedConfig(config);
break;
}
// TODO: forced if OFF
}
if (fileInfo.getMatchedConfig() == null) return;
applyDefaultChanges(fileInfo, FileInfo::getAudioTracks, fileInfo.getMatchedConfig().getAudioLanguage(),
() -> audiosByLanguage.get(fileInfo.getMatchedConfig().getAudioLanguage()).get(0));
applyDefaultChanges(fileInfo, FileInfo::getSubtitleTracks, fileInfo.getMatchedConfig().getSubtitleLanguage(),
() -> subsByLanguage.get(fileInfo.getMatchedConfig().getSubtitleLanguage()).stream().max(subtitleTrackComparator).get());
}
private void applyDefaultChanges(FileInfo fileInfo, Function<FileInfo, List<TrackAttributes>> tracks, String language, Supplier<TrackAttributes> targetDefaultSupplier) {
tracks.apply(fileInfo).stream()
.filter(TrackAttributes::defaultt)
.forEach(attr -> fileInfo.getChanges().getDefaultTrack().put(attr, false));
if (!"OFF".equals(language)) {
TrackAttributes targetDefault = targetDefaultSupplier.get();
if (fileInfo.getChanges().getDefaultTrack().containsKey(targetDefault)) {
fileInfo.getChanges().getDefaultTrack().remove(targetDefault);
} else {
fileInfo.getChanges().getDefaultTrack().put(targetDefault, true);
}
}
}
public void findForcedTracksAndApplyChanges(FileInfo fileInfo, boolean overwrite) {
Stream<TrackAttributes> forcedTracks = fileInfo.getTracks().stream()
.filter(track -> track.trackName() != null)
.filter(track -> forcedKeywords.stream().anyMatch(keyword -> track.trackName().toLowerCase().contains(keyword.toLowerCase(Locale.ROOT))));
if (overwrite) {
fileInfo.getTracks().stream().filter(TrackAttributes::forced).forEach(attr -> {
fileInfo.getChanges().getForcedTrack().put(attr, false);
});
} else {
forcedTracks = forcedTracks.filter(attr -> !attr.forced());
}
forcedTracks.forEach(attr -> {
fileInfo.getChanges().getForcedTrack().put(attr, true);
});
}
public void findCommentaryTracksAndApplyChanges(FileInfo fileInfo) {
fileInfo.getTracks().stream()
.filter(track -> !track.commentary())
.filter(track -> track.trackName() != null)
.filter(track -> commentaryKeywords.stream().anyMatch(keyword -> track.trackName().toLowerCase().contains(keyword.toLowerCase(Locale.ROOT))))
.forEach(attr -> {
fileInfo.getChanges().getCommentaryTrack().put(attr, true);
});
}
public void findHearingImpairedTracksAndApplyChanges(FileInfo fileInfo) {
fileInfo.getTracks().stream()
.filter(track -> !track.hearingImpaired())
.filter(track -> track.trackName() != null)
.filter(track -> hearingImpairedKeywords.stream().anyMatch(keyword -> track.trackName().toLowerCase().contains(keyword.toLowerCase(Locale.ROOT))))
.forEach(attr -> {
fileInfo.getChanges().getHearingImpairedTrack().put(attr, true);
});
}
}

View File

@@ -0,0 +1,141 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBar;
import me.tongfei.progressbar.ProgressBarBuilder;
import me.tongfei.progressbar.ProgressBarStyle;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
public abstract class AttributeUpdater {
protected final InputConfig config;
protected final FileProcessor fileProcessor;
protected final AttributeChangeProcessor attributeChangeProcessor;
protected final ResultStatistic statistic = ResultStatistic.getInstance();
private final ExecutorService executor;
public AttributeUpdater(InputConfig config, FileProcessor fileProcessor, AttributeChangeProcessor attributeChangeProcessor) {
this.config = config;
this.fileProcessor = fileProcessor;
this.attributeChangeProcessor = attributeChangeProcessor;
this.executor = Executors.newFixedThreadPool(config.getThreads());
}
protected ProgressBarBuilder pbBuilder() {
return new ProgressBarBuilder()
.setStyle(ProgressBarStyle.ASCII)
.setUpdateIntervalMillis(250)
.setMaxRenderedLength(75);
}
@SneakyThrows
public void execute() {
statistic.startTimer();
try (ProgressBar progressBar = pbBuilder().build()) {
List<File> files = getFiles();
progressBar.maxHint(files.size());
progressBar.refresh();
files.forEach(file -> executor.submit(() -> {
process(file);
progressBar.step();
}));
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
}
// writeLastExecutionDate();
statistic.stopTimer();
statistic.print();
}
protected abstract List<File> getFiles();
/**
* Start of the file updating process.
* This method is called by the executor and its contents are executed in parallel.
*
* @param file file or directory to update
*/
protected abstract void process(File file);
/**
* Persist file changes.
*
* @param fileInfo contains information about file and desired configuration.
*/
protected void checkStatusAndUpdate(FileInfo fileInfo) {
if (!fileInfo.getChanges().isEmpty()) {
statistic.changePlanned();
if (config.isSafeMode()) {
log.info("Planned changes [{}] for {}", changeLog(fileInfo), fileInfo.getFile().getPath());
return;
}
try {
log.info("Committing changes [{}] to {}", changeLog(fileInfo), fileInfo.getFile().getPath());
fileProcessor.update(fileInfo);
statistic.changeSuccessful();
} catch (IOException | MkvToolNixException e) {
statistic.changeFailed();
log.warn("Couldn't commit changes [{}] to {}", changeLog(fileInfo), fileInfo.getFile().getPath(), e);
}
} else if (fileInfo.getChanges().isEmpty()) {
statistic.unchanged();
} else {
statistic.unknownFailed();
}
}
private String changeLog(FileInfo fileInfo) {
List<String> changes = new ArrayList<>();
if (fileInfo.getMatchedConfig() != null) changes.add("defaults " + fileInfo.getMatchedConfig().toStringShort());
if (!fileInfo.getChanges().getForcedTrack().isEmpty()) changes.add("forced tags");
if (!fileInfo.getChanges().getCommentaryTrack().isEmpty()) changes.add("commentary tags");
if (!fileInfo.getChanges().getHearingImpairedTrack().isEmpty()) changes.add("hearing impaired tags");
return String.join(", ", changes);
}
// should this be here?
// protected void writeLastExecutionDate() {
// if (config.isSafeMode()) {
// return;
// }
//
// try {
// String filePath = AppDirsFactory.getInstance().getUserConfigDir(ProjectUtil.getProjectName(), null, null);
//
// File configDir = Path.of(filePath).toFile();
// if (!configDir.exists()) configDir.mkdirs();
//
// File lastExecutionFile = Path.of(filePath + "/last-execution.yml").toFile();
// if (!lastExecutionFile.exists()) lastExecutionFile.createNewFile();
//
// YAML yaml = new YAML(lastExecutionFile);
// yaml.set(config.getNormalizedLibraryPath(), DateUtils.convert(new Date()));
// yaml.save(lastExecutionFile);
// } catch (IOException | YamlInvalidContentException e) {
// log.error("last-execution.yml could not be created or read.", e);
// }
// }
}

View File

@@ -0,0 +1,42 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.Cache;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import org.apache.commons.lang3.tuple.Pair;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class CachedFileProcessor implements FileProcessor {
private final FileProcessor processor;
Cache<String, List<File>> fileCache = new Cache<>();
Cache<Pair<String, Integer>, List<File>> directoryCache = new Cache<>();
Cache<File, FileInfo> attributeCache = new Cache<>();
public CachedFileProcessor(FileProcessor processor) {
this.processor = processor;
}
@Override
public List<File> loadFiles(String path) {
return fileCache.retrieve(path, processor::loadFiles);
}
@Override
public List<File> loadDirectory(String path, int depth) {
return directoryCache.retrieve(Pair.of(path, depth), key -> processor.loadDirectory(key.getLeft(), key.getRight()));
}
@Override
public FileInfo readAttributes(File file) {
return attributeCache.retrieve(file, processor::readAttributes);
}
@Override
public void update(FileInfo fileInfo) throws IOException, MkvToolNixException {
processor.update(fileInfo);
}
}

View File

@@ -0,0 +1,104 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import java.io.File;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
public class CoherentAttributeUpdater extends SingleFileAttributeUpdater {
public CoherentAttributeUpdater(InputConfig config, FileProcessor processor, AttributeChangeProcessor attributeChangeProcessor) {
super(config, processor, attributeChangeProcessor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" directories", 1);
}
protected List<File> getFiles() {
return fileProcessor.loadDirectory(config.getLibraryPath().getPath(), config.getCoherent());
}
@Override
public void process(File rootDir) {
if (rootDir.isFile()) {
super.process(rootDir);
return;
}
List<File> files = fileProcessor.loadFiles(rootDir.getPath());
Set<FileInfo> matchedFiles = new HashSet<>(files.size() * 2);
for (AttributeConfig config: config.getAttributeConfig()) {
AttributeConfig matchedConfig = findMatch(config, matchedFiles, files);
if (matchedConfig == null) continue;
if (matchedFiles.size() != files.size()) {
log.warn("Skip applying changes: Found coherent match, but matched count is different than file count (matched: {}, files: {}, dir: {})",
matchedFiles.size(), files.size(), rootDir.getPath());
}
log.info("Found coherent match {} for {}", matchedConfig.toStringShort(), rootDir.getPath());
matchedFiles.forEach(fileInfo -> {
attributeChangeProcessor.findForcedTracksAndApplyChanges(fileInfo, this.config.isOverwriteForced());
attributeChangeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
attributeChangeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
checkStatusAndUpdate(fileInfo);
});
return; // match was found and process must be stopped
}
// Couldn't match any config at current level. Resetting changes and trying to one level deeper
matchedFiles.forEach(fileInfo -> {
fileInfo.resetChanges();
fileInfo.setMatchedConfig(null);
});
if (config.isForceCoherent()) {
log.info("No coherent match found, skipping {}", rootDir.getPath());
statistic.increaseUnchangedBy(files.size());
return;
}
log.info("No coherent match found, attempting to find coherent match in child directories of {}", rootDir.getPath());
for (File dir: fileProcessor.loadDirectory(rootDir.getPath(), 1)) this.process(dir);
}
private AttributeConfig findMatch(AttributeConfig config, Set<FileInfo> matchedFiles, List<File> files) {
AttributeConfig matchedConfig = null;
matchedFiles.clear();
for (File file: files) {
FileInfo fileInfo = fileProcessor.readAttributes(file);
fileInfo.resetChanges();
fileInfo.setMatchedConfig(null);
if (fileInfo.getTracks().isEmpty()) {
log.warn("No attributes found for {}", file);
statistic.unknownFailed();
break;
}
attributeChangeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config);
if (matchedConfig == null) matchedConfig = fileInfo.getMatchedConfig();
if (matchedConfig == null || matchedConfig != fileInfo.getMatchedConfig()) {
matchedConfig = null;
break;
}
matchedFiles.add(fileInfo);
}
return matchedConfig;
}
}

View File

@@ -0,0 +1,51 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public interface FileProcessor {
/**
* @param path leads to one file directly or a directory which will be loaded recursively
* @return list of all files within the directory
*/
List<File> loadFiles(String path);
/**
* Load only directories and files at depth, ignoring everything between root dir and dir at depth.
* E.g. with file structure /base/depth1/depth2/depth3.file
* - with depth 1: return /base/depth1/
* - with depth 2: returns /base/depth1/depth2/
*
* @param path directory which will be loaded recursively until depth
* @param depth limit directory crawling
* @return list of directory at depth
*/
List<File> loadDirectory(String path, int depth);
/**
* Load track information from file.
*
* @param file Takes the file from which the attributes will be returned
* @return list of all important attributes
*/
FileInfo readAttributes(File file);
/**
* Update the file.
*
* @param fileInfo information used to update file
* @throws IOException when error occurs accessing file retrieving information
* @throws MkvToolNixException when error occurs while sending query to mkvpropedit
*/
void update(FileInfo fileInfo) throws IOException, MkvToolNixException;
default InputStream run(String[] command) throws IOException {
return Runtime.getRuntime().exec(command).getInputStream();
}
}

View File

@@ -0,0 +1,176 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions.MkvToolNixException;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileFilter;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.core.util.IOUtils;
import tools.jackson.databind.ObjectMapper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.FileUtils.getPathFor;
@Slf4j
@RequiredArgsConstructor
public class MkvFileProcessor implements FileProcessor {
protected final File mkvToolNixInstallation;
protected final FileFilter fileFilter;
private final ObjectMapper mapper = new ObjectMapper();
private static final Set<String> fileExtensions = new HashSet<>(Set.of(".mkv", ".mka", ".mks", ".mk3d"));
private static final String DEFAULT_TRACK = "--edit track:%s --set flag-default=%s";
private static final String FORCED_TRACK = "--edit track:%s --set flag-forced=%s";
private static final String COMMENTARY_TRACK = "--edit track:%s --set flag-commentary=%s";
private static final String HEARING_IMPAIRED_TRACK = "--edit track:%s --set flag-hearing-impaired=%s";
/**
* {@inheritDoc}
*/
@Override
public List<File> loadFiles(String path) {
try (Stream<Path> paths = Files.walk(Paths.get(path))) {
return paths
.filter(Files::isRegularFile)
.map(Path::toFile)
.filter(file -> fileFilter.accept(file, fileExtensions))
.collect(Collectors.toList());
} catch (IOException e) {
log.error("Couldn't find file or directory!", e);
return new ArrayList<>();
}
}
/**
* {@inheritDoc}
*/
@Override
// does this load /arst/arst & /arst ?
public List<File> loadDirectory(String path, int depth) {
File rootDir = Path.of(path).toFile();
if (!rootDir.exists()) {
log.error("Couldn't find file or directory!");
return new ArrayList<>();
}
List<File> result = new ArrayList<>();
exploreDirectory(rootDir, 0, depth, result);
return result;
}
/**
* Recursively explores directories to find items at the target depth.
*
* @param currentDir The current directory being explored
* @param currentDepth The current depth level
* @param targetDepth The target depth to collect files
* @param result The collection to store found files
*/
private static void exploreDirectory(File currentDir, int currentDepth, int targetDepth, List<File> result) {
if (currentDepth == targetDepth) {
result.add(currentDir);
return;
}
// Get all files and directories in the current directory
File[] files = currentDir.listFiles();
if (files == null) return;
// Recursively explore subdirectories
for (File file : files) {
if (file.isDirectory()) {
exploreDirectory(file, currentDepth + 1, targetDepth, result);
} else if (currentDepth + 1 == targetDepth) {
// If files at the next level would be at the target depth, include them
result.add(file);
}
}
}
@SuppressWarnings("unchecked")
@Override
public FileInfo readAttributes(File file) {
FileInfo fileInfo = new FileInfo(file);
try {
String[] command = new String[]{
getPathFor(mkvToolNixInstallation, MkvToolNix.MKV_MERGE).getAbsolutePath(),
"--identify",
"--identification-format",
"json",
file.getAbsolutePath()
};
log.debug("Executing: {}", String.join(" ", command));
InputStream inputStream = run(command);
Map<String, Object> jsonMap = mapper.readValue(inputStream, Map.class);
List<Map<String, Object>> tracks = (List<Map<String, Object>>) jsonMap.get("tracks");
if (tracks != null) {
for (Map<String, Object> attribute : tracks) {
if (!"video".equals(attribute.get("type"))) {
Map<String, Object> properties = (Map<String, Object>) attribute.get("properties");
// mkvpropedit takes in the n-th track, based on the order of mkvmerge --idenfity
fileInfo.addTrack(new TrackAttributes(
(int) properties.get("number"),
(String) properties.get("language"),
(String) properties.get("track_name"),
(Boolean) properties.getOrDefault("default_track", false),
(Boolean) properties.getOrDefault("forced_track", false),
(Boolean) properties.getOrDefault("commentary_track", false),
(Boolean) properties.getOrDefault("flag_hearing_impaired", false),
TrackType.valueOf(((String) attribute.get("type")).toUpperCase(Locale.ENGLISH))));
}
}
} else {
log.warn("Couldn't retrieve information of {}", file.getAbsolutePath());
}
log.debug("File attributes of '{}': {}", file.getAbsolutePath(), fileInfo.getTracks());
} catch (IOException e) {
log.error("File could not be found or loaded: ", e);
System.out.println("File could not be found or loaded: " + file.getAbsolutePath());
}
return fileInfo;
}
/**
* {@inheritDoc}
*/
@Override
public void update(FileInfo fileInfo) throws IOException, MkvToolNixException {
String[] command = getUpdateCommand(fileInfo);
log.debug("Executing '{}'", String.join(" ", command));
InputStream inputstream = run(command);
String output = IOUtils.toString(new InputStreamReader(inputstream));
log.debug("Result: {}", output.replaceAll("\\n", " '"));
if (output.contains("Error")) throw new MkvToolNixException(output);
}
private String[] getUpdateCommand(FileInfo fileInfo) {
List<String> command = new ArrayList<>();
command.add(getPathFor(mkvToolNixInstallation, MkvToolNix.MKV_PROP_EDIT).getAbsolutePath());
command.add(String.format(fileInfo.getFile().getAbsolutePath()));
PlannedChange changes = fileInfo.getChanges();
changes.getDefaultTrack().forEach((key, value) -> command.addAll(format(DEFAULT_TRACK, key.id(), value ? 1 : 0)));
changes.getForcedTrack().forEach((key, value) -> command.addAll(format(FORCED_TRACK, key.id(), value ? 1 : 0)));
changes.getCommentaryTrack().forEach((key, value) -> command.addAll(format(COMMENTARY_TRACK, key.id(), value ? 1 : 0)));
changes.getHearingImpairedTrack().forEach((key, value) -> command.addAll(format(HEARING_IMPAIRED_TRACK, key.id(), value ? 1 : 0)));
return command.toArray(new String[0]);
}
private List<String> format(String format, Object... args) {
return Arrays.asList(String.format(format, args).split(" "));
}
}

View File

@@ -0,0 +1,46 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import lombok.extern.slf4j.Slf4j;
import me.tongfei.progressbar.ProgressBarBuilder;
import java.io.File;
import java.util.List;
@Slf4j
public class SingleFileAttributeUpdater extends AttributeUpdater {
public SingleFileAttributeUpdater(InputConfig config, FileProcessor processor, AttributeChangeProcessor attributeChangeProcessor) {
super(config, processor, attributeChangeProcessor);
}
@Override
protected ProgressBarBuilder pbBuilder() {
return super.pbBuilder()
.setUnit(" files", 1);
}
@Override
protected List<File> getFiles() {
return fileProcessor.loadFiles(config.getLibraryPath().getPath());
}
@Override
public void process(File file) {
FileInfo fileInfo = fileProcessor.readAttributes(file);
if (fileInfo.getTracks().isEmpty()) {
log.warn("No attributes found for {}", file);
statistic.unknownFailed();
return;
}
attributeChangeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config.getAttributeConfig());
attributeChangeProcessor.findForcedTracksAndApplyChanges(fileInfo, config.isOverwriteForced());
attributeChangeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
attributeChangeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
checkStatusAndUpdate(fileInfo);
}
}

View File

@@ -0,0 +1,16 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = ValidFileValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidFile {
String message() default "File does not exist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,17 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.io.File;
public class ValidFileValidator implements ConstraintValidator<ValidFile, File> {
@Override
public void initialize(ValidFile constraintAnnotation) {
}
@Override
public boolean isValid(File file, ConstraintValidatorContext context) {
return file != null && file.exists();
}
}

View File

@@ -0,0 +1,16 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = ValidMkvToolNixValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidMkvToolNix {
String message() default "MkvToolNix does not exist";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,24 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.io.File;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.FileUtils.getPathFor;
public class ValidMkvToolNixValidator implements ConstraintValidator<ValidMkvToolNix, File> {
@Override
public void initialize(ValidMkvToolNix constraintAnnotation) {
}
@Override
public boolean isValid(File file, ConstraintValidatorContext context) {
return file != null && file.exists()
&& getPathFor(file, MkvToolNix.MKV_MERGE).exists()
&& getPathFor(file, MkvToolNix.MKV_PROP_EDIT).exists();
}
}

View File

@@ -0,0 +1,31 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CommandRunner;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.ValidationUtil;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import picocli.CommandLine;
import java.util.Set;
public class ValidationExecutionStrategy implements CommandLine.IExecutionStrategy {
public int execute(CommandLine.ParseResult parseResult) {
if (!parseResult.isVersionHelpRequested() && !parseResult.isUsageHelpRequested()) validate(parseResult.commandSpec());
return new CommandLine.RunLast().execute(parseResult);
}
private static void validate(CommandLine.Model.CommandSpec spec) {
Validator validator = ValidationUtil.getValidator();
Set<ConstraintViolation<InputConfig>> violations = validator.validate(((CommandRunner)spec.userObject()).getConfig());
if (!violations.isEmpty()) {
StringBuilder errors = new StringBuilder();
for (ConstraintViolation<InputConfig> violation : violations) {
errors.append(violation.getPropertyPath()).append(" ").append(violation.getMessage()).append("\n");
}
throw new CommandLine.ParameterException(spec.commandLine(), errors.toString());
}
}
}

View File

@@ -13,6 +13,10 @@ public class AttributeConfig {
private final String audioLanguage; private final String audioLanguage;
private final String subtitleLanguage; private final String subtitleLanguage;
public static AttributeConfig of(String audioLanguage, String subtitleLanguage) {
return new AttributeConfig(audioLanguage, subtitleLanguage);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -26,6 +30,10 @@ public class AttributeConfig {
return Objects.hash(audioLanguage, subtitleLanguage); return Objects.hash(audioLanguage, subtitleLanguage);
} }
public String toStringShort() {
return audioLanguage + ":" + subtitleLanguage;
}
@Override @Override
public String toString() { public String toString() {
return "AttributeConfig{" return "AttributeConfig{"

View File

@@ -1,68 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AllArgsConstructor;
import org.apache.commons.cli.Option;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@AllArgsConstructor
public enum ConfigProperty {
LIBRARY("library-path", "Path to library", "l", 1),
ATTRIBUTE_CONFIG("attribute-config", "Attribute config to decide which tracks to choose when", "a", Option.UNLIMITED_VALUES),
CONFIG_PATH("config-path", "Path to config file", "p", 1),
MKV_TOOL_NIX("mkvtoolnix", "Path to mkv tool nix installation", "m", 1),
SAFE_MODE("safe-mode", "Test run (no files will be changes)", "s", 0),
COHERENT("coherent", "Try to match all files in dir of depth with the same config", "c", 1),
FORCE_COHERENT("force-coherent", "Force coherent and don't update anything if config fits not whole config (default: false)", "cf", 0),
WINDOWS("windows", "Is operating system windows", null, 0),
ONLY_NEW_FILES("only-new-files", "Sets filter-date to last successful execution (Overwrites input of filter-date)", "n", 0),
FILTER_DATE("filter-date", "Only consider files created newer than entered date (format: \"dd.MM.yyyy-HH:mm:ss\")", "d", 1),
THREADS("threads", "Thread count (default: 2)", "t", 1),
INCLUDE_PATTERN("include-pattern", "Include files matching pattern (default: \".*\")", "i", 1),
EXCLUDED_DIRECTORY("excluded-directories", "Directories to be excluded, combines with config file", "e", Option.UNLIMITED_VALUES),
FORCED_KEYWORDS("forced-keywords", "Additional keywords to identify forced tracks", "fk", Option.UNLIMITED_VALUES),
COMMENTARY_KEYWORDS("commentary-keywords", "Additional keywords to identify commentary tracks", "ck", Option.UNLIMITED_VALUES),
PREFERRED_SUBTITLES("preferred-subtitles", "Additional keywords to prefer specific subtitle tracks", "ps", Option.UNLIMITED_VALUES),
ARGUMENTS("arguments", "List of arguments", null, 0),
VERSION("version", "Display version", "v", 0),
HELP("help", "\"For help this is\" - Yoda", "h", 0);
/*
* Verify at startup that there are no duplicated shortParameters.
*/
static {
Set<String> shortParameters = new HashSet<>();
for (String param : Arrays.stream(ConfigProperty.values()).map(ConfigProperty::abrv).collect(Collectors.toList())) {
if (shortParameters.contains(param)) {
throw new IllegalStateException("It is not allowed to have multiple properties with the same abbreviation!");
}
if (param != null) {
shortParameters.add(param);
}
}
}
private final String property;
private final String description;
private final String shortParameter;
private final int args;
public String prop() {
return property;
}
public String desc() {
return description;
}
public String abrv() {
return shortParameter;
}
public int args() {
return args;
}
}

View File

@@ -0,0 +1,56 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Getter
@RequiredArgsConstructor
public class FileInfo {
private final File file;
@Getter(AccessLevel.NONE)
private final List<TrackAttributes> tracks = new ArrayList<>();
@Getter(AccessLevel.NONE)
private final List<TrackAttributes> audioTracks = new ArrayList<>();
@Getter(AccessLevel.NONE)
private final List<TrackAttributes> subtitleTracks = new ArrayList<>();
private PlannedChange changes = new PlannedChange();
@Setter
private AttributeConfig matchedConfig;
public void addTrack(TrackAttributes track) {
tracks.add(track);
if (TrackType.AUDIO.equals(track.type())) audioTracks.add(track);
else if (TrackType.SUBTITLES.equals(track.type())) subtitleTracks.add(track);
}
public void addTracks(Collection<TrackAttributes> tracks) {
for (TrackAttributes track : tracks) addTrack(track);
}
public List<TrackAttributes> getTracks() {
return Collections.unmodifiableList(tracks);
}
public List<TrackAttributes> getAudioTracks() {
return Collections.unmodifiableList(audioTracks);
}
public List<TrackAttributes> getSubtitleTracks() {
return Collections.unmodifiableList(subtitleTracks);
}
public void resetChanges() {
changes = new PlannedChange();
}
}

View File

@@ -1,80 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import java.io.File;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@RequiredArgsConstructor
public class FileInfoDto {
private final File file;
private Set<FileAttribute> existingDefaultAudioLanes = new HashSet<>();
private Set<FileAttribute> existingForcedAudioLanes = new HashSet<>();
private Set<FileAttribute> existingDefaultSubtitleLanes = new HashSet<>();
private Set<FileAttribute> existingForcedSubtitleLanes = new HashSet<>();
private Set<FileAttribute> desiredForcedSubtitleLanes = new HashSet<>();
private FileAttribute desiredDefaultAudioLane;
private FileAttribute desiredDefaultSubtitleLane;
private AttributeConfig matchedConfig;
public boolean isAudioDifferent() {
return desiredDefaultAudioLane != null &&
(existingDefaultAudioLanes == null || !existingDefaultAudioLanes.contains(desiredDefaultAudioLane) || existingDefaultAudioLanes.size() > 1);
}
public boolean isSubtitleDifferent() {
return isSubtitleMatchDifferent() || isSubtitleOFF();
}
private boolean isSubtitleMatchDifferent() {
return desiredDefaultSubtitleLane != null
&& (existingDefaultSubtitleLanes == null || !existingDefaultSubtitleLanes.contains(desiredDefaultSubtitleLane) || existingDefaultSubtitleLanes.size() > 1);
}
private boolean isSubtitleOFF() {
return desiredDefaultSubtitleLane == null
&& (matchedConfig != null && "OFF".equals(matchedConfig.getSubtitleLanguage()))
&& (existingDefaultSubtitleLanes != null && !existingDefaultSubtitleLanes.isEmpty());
}
public boolean areForcedTracksDifferent() {
return !desiredForcedSubtitleLanes.isEmpty();
}
public FileStatus getStatus() {
if (isChangeNecessary()) return FileStatus.CHANGE_NECESSARY;
if (isUnableToApplyConfig()) return FileStatus.NO_SUITABLE_CONFIG;
if (isAlreadySuitable()) return FileStatus.ALREADY_SUITED;
return FileStatus.UNKNOWN;
}
private boolean isUnableToApplyConfig() {
return desiredDefaultAudioLane == null && desiredDefaultSubtitleLane == null;
}
private boolean isAlreadySuitable() {
return existingDefaultAudioLanes.contains(desiredDefaultAudioLane) && existingDefaultSubtitleLanes.contains(desiredDefaultSubtitleLane);
}
private boolean isChangeNecessary() {
return isAudioDifferent() || isSubtitleDifferent() || areForcedTracksDifferent() || !existingForcedAudioLanes.isEmpty();
}
@Override
public String toString() {
return "[" + "defaultAudioLanes=" + existingDefaultAudioLanes +
", defaultSubtitleLanes=" + existingDefaultSubtitleLanes +
", desiredForcedSubtitleLanes=" + desiredForcedSubtitleLanes +
", desiredAudioLane=" + desiredDefaultAudioLane +
", desiredSubtitleLane=" + desiredDefaultSubtitleLane +
']';
}
}

View File

@@ -1,8 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
public enum FileStatus {
CHANGE_NECESSARY,
NO_SUITABLE_CONFIG,
ALREADY_SUITED,
UNKNOWN;
}

View File

@@ -0,0 +1,118 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.converter.AttributeConfigConverter;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation.ValidFile;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation.ValidMkvToolNix;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SystemUtils;
import picocli.CommandLine;
import java.io.File;
import java.util.*;
import java.util.regex.Pattern;
import picocli.CommandLine.Option;
@Slf4j
@Getter
@Setter
@NoArgsConstructor
@CommandLine.Command
public class InputConfig implements CommandLine.IVersionProvider {
private File configPath;
@CommandLine.Spec
CommandLine.Model.CommandSpec spec;
@ValidFile(message = "does not exist")
@CommandLine.Parameters(description = "path to library")
private File libraryPath;
@Option(names = {"-a", "--attribute-config"}, arity = "1..*", converter = AttributeConfigConverter.class,
description = "List of audio:subtitle pairs for matching defaults in order (e.g. jpn:eng jpn:ger)")
private AttributeConfig[] attributeConfig = new AttributeConfig[0];
@ValidMkvToolNix(message = "does not exist")
@Option(names = {"-m", "--mkvtoolnix"}, defaultValue = "${DEFAULT_MKV_TOOL_NIX}", description = "path to mkvtoolnix installation")
private File mkvToolNix;
@Option(names = {"-s", "--safemode"}, description = "test run (no files will be changes)")
private boolean safeMode;
@Min(1)
@Option(names = {"-t", "--threads"}, defaultValue = "2", showDefaultValue = CommandLine.Help.Visibility.ALWAYS, description = "thread count")
private int threads;
@Min(0)
@Option(names = {"-c", "--coherent"}, description = "try to match all files in dir of depth with the same attribute config. Attempting increasing deeper levels until match is found (worst case applying config on single file basis)")
private Integer coherent;
@Option(names = {"-cf", "--force-coherent"}, description = "only applies changes if a coherent match was found for the specifically entered depth")
private boolean forceCoherent;
// TODO: implement usage
// @Option(names = {"-n", "--only-new-file"}, description = "sets filter-date to last successful execution (overwrites input of filter-date)")
// private boolean onlyNewFiles;
@Option(names = {"-d", "--filter-date"}, defaultValue = Option.NULL_VALUE, description = "only consider files created newer than entered date (format: \"dd.MM.yyyy-HH:mm:ss\")")
private Date filterDate;
@Option(names = {"-i", "--include-pattern"}, defaultValue = ".*", description = "include files matching pattern")
private Pattern includePattern;
@Option(names = {"-e", "--exclude"}, arity = "1..*",
description = "relative directories and files to be excluded (no wildcard)")
private Set<String> excluded = new HashSet<>();
@Option(names = {"-o", "-overwrite-forced"}, description = "remove all forced flags")
private boolean overwriteForced;
@Option(names = {"--forced-keywords"}, arity = "1..*", defaultValue = "forced, signs, songs", showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
split = ", ", description = "Keywords to identify forced tracks (Defaults will be overwritten)")
private Set<String> forcedKeywords;
@Option(names = {"--commentary-keywords"}, arity = "1..*", defaultValue = "comment, commentary, director", showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
split = ", ", description = "Keywords to identify commentary tracks (Defaults will be overwritten)")
private Set<String> commentaryKeywords;
@Option(names = {"--hearing-impaired"}, arity = "1..*", defaultValue = "SDH, CC", showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
split = ", ", description = "Keywords to identify hearing impaired tracks (Defaults will be overwritten")
private Set<String> hearingImpaired;
@Option(names = {"--preferred-subtitles"}, arity = "1..*", defaultValue = "unstyled", showDefaultValue = CommandLine.Help.Visibility.ALWAYS,
split = ", ", description = "Keywords to prefer specific subtitle tracks (Defaults will be overwritten)")
private Set<String> preferredSubtitles;
@Option(names = {"--debug"}, description = "Enable debug logging")
private boolean debug;
static {
// Set default value into system properties to picocli can read the conditional value
System.setProperty("DEFAULT_MKV_TOOL_NIX", SystemUtils.IS_OS_WINDOWS ? "C:\\Program Files\\MKVToolNix" : "/usr/bin/");
}
@Override
public String toString() {
return new StringJoiner(", ", InputConfig.class.getSimpleName() + "[", "]")
.add("configPath=" + configPath)
.add("spec=" + spec)
.add("libraryPath=" + libraryPath)
.add("attributeConfig=" + Arrays.toString(attributeConfig))
.add("mkvToolNix=" + mkvToolNix)
.add("safeMode=" + safeMode)
.add("threads=" + threads)
.add("coherent=" + coherent)
.add("forceCoherent=" + forceCoherent)
.add("filterDate=" + filterDate)
.add("includePattern=" + includePattern)
.add("excluded=" + excluded)
.add("overwriteForced=" + overwriteForced)
.add("forcedKeywords=" + forcedKeywords)
.add("commentaryKeywords=" + commentaryKeywords)
.add("hearingImpaired=" + hearingImpaired)
.add("preferredSubtitles=" + preferredSubtitles)
.add("debug=" + debug)
.toString();
}
@Override
public String[] getVersion() throws Exception {
return new String[0];
}
}

View File

@@ -0,0 +1,18 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter
public class PlannedChange {
private final Map<TrackAttributes, Boolean> defaultTrack = new HashMap<>();
private final Map<TrackAttributes, Boolean> forcedTrack = new HashMap<>();
private final Map<TrackAttributes, Boolean> commentaryTrack = new HashMap<>();
private final Map<TrackAttributes, Boolean> hearingImpairedTrack = new HashMap<>();
public boolean isEmpty() {
return defaultTrack.isEmpty() && forcedTrack.isEmpty() && commentaryTrack.isEmpty() && hearingImpairedTrack.isEmpty();
}
}

View File

@@ -1,82 +1,65 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model; package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@Getter @Getter
@Slf4j @Slf4j
public class ResultStatistic { public class ResultStatistic {
private static final String result = "Total files: %s%n" + private static final String PRINT_TEMPLATE = "Total: %s, Changing: %s (Successful: %s, Failed %s), Unchanged: %s, Excluded: %s, Unknown/Failed: %s\nRuntime: %s";
"├─ Excluded: %s%n" +
"├─ Should change: %s%n" +
"│ ├─ Failed changing: %s%n" +
"│ └─ Successfully changed: %s%n" +
"├─ No suitable config found: %s%n" +
"├─ Already fit config: %s%n" +
"└─ Failed: %s%n" +
"Runtime: %s";
private static ResultStatistic instance; private static ResultStatistic instance;
private int filesTotal = 0;
private int changePlanned = 0;
private int changeFailed = 0;
private int changeSuccessful = 0;
private int unchanged = 0;
private int excluded = 0; private int excluded = 0;
private int unknownFailed = 0;
private int shouldChange = 0;
private int failedChanging = 0;
private int successfullyChanged = 0;
private int noSuitableConfigFound = 0;
private int alreadyFits = 0;
private int failed = 0;
@Getter(AccessLevel.NONE)
private long startTime = 0; private long startTime = 0;
private long runtime = 0; private long runtime = 0;
public static ResultStatistic getInstance() { public static ResultStatistic getInstance() {
if (instance == null) { return getInstance(false);
}
public static ResultStatistic getInstance(boolean reset) {
if (instance == null || reset) {
instance = new ResultStatistic(); instance = new ResultStatistic();
} }
return instance; return instance;
} }
public void increaseTotalBy(int amount) { public int total() {
filesTotal += amount; return changePlanned + unchanged + excluded + unknownFailed;
} }
public synchronized void total() { public synchronized void changePlanned() {
filesTotal++; changePlanned++;
} }
public void increaseExcludedBy(int amount) { public synchronized void changeSuccessful() {
excluded += amount; changeSuccessful++;
}
public synchronized void changeFailed() {
changeFailed++;
}
public synchronized void unchanged() {
unchanged++;
}
public synchronized void increaseUnchangedBy(int amount) {
unchanged += amount;
} }
public synchronized void excluded() { public synchronized void excluded() {
excluded++; excluded++;
} }
public synchronized void shouldChange() { public synchronized void unknownFailed() {
shouldChange++; unknownFailed++;
}
public synchronized void success() {
successfullyChanged++;
}
public synchronized void failedChanging() {
failedChanging++;
}
public synchronized void noSuitableConfigFound() {
noSuitableConfigFound++;
}
public synchronized void alreadyFits() {
alreadyFits++;
}
public synchronized void failure() {
failed++;
} }
public void startTimer() { public void startTimer() {
@@ -87,11 +70,6 @@ public class ResultStatistic {
runtime = System.currentTimeMillis() - startTime; runtime = System.currentTimeMillis() - startTime;
} }
public void printResult() {
System.out.println(prettyPrint());
log.info(this.toString());
}
private String formatTimer() { private String formatTimer() {
int seconds = (int) (runtime / 1000); int seconds = (int) (runtime / 1000);
int minutes = seconds / 60; int minutes = seconds / 60;
@@ -109,21 +87,14 @@ public class ResultStatistic {
} }
} }
public String prettyPrint() { public void print() {
return String.format(result, filesTotal, excluded, shouldChange, failedChanging, successfullyChanged, String result = this.toString();
noSuitableConfigFound, alreadyFits, failed, formatTimer()); System.out.println(result);
log.info(result);
} }
@Override @Override
public String toString() { public String toString() {
return "ResultStatistic: " + "filesTotal=" + filesTotal + return String.format(PRINT_TEMPLATE, total(), changePlanned, changeSuccessful, changeFailed, unchanged, excluded, unknownFailed, formatTimer());
", excluded=" + excluded +
", shouldChange=" + shouldChange +
" (failedChanging=" + failedChanging +
", successfullyChanged=" + successfullyChanged +
"), noSuitableConfigFound=" + noSuitableConfigFound +
", alreadyFits=" + alreadyFits +
", failed=" + failed +
", runtime=" + formatTimer();
} }
} }

View File

@@ -1,47 +1,37 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model; package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.Objects; import java.util.Objects;
@Slf4j @Slf4j
@Getter public record TrackAttributes(int id, String language, String trackName,
@AllArgsConstructor boolean defaultt, boolean forced, boolean commentary, boolean hearingImpaired,
public class FileAttribute { TrackType type) {
private final int id;
private final String language;
private final String trackName;
private final boolean defaultTrack;
private final boolean forcedTrack;
private final LaneType type;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
FileAttribute attribute = (FileAttribute) o; TrackAttributes attribute = (TrackAttributes) o;
return id == attribute.id return id == attribute.id
&& defaultTrack == attribute.defaultTrack && defaultt == attribute.defaultt
&& forcedTrack == attribute.forcedTrack && forced == attribute.forced
&& commentary == attribute.commentary
&& hearingImpaired == attribute.hearingImpaired
&& Objects.equals(language, attribute.language) && Objects.equals(language, attribute.language)
&& Objects.equals(trackName, attribute.trackName) && Objects.equals(trackName, attribute.trackName)
&& type == attribute.type; && type == attribute.type;
} }
@Override
public int hashCode() {
return Objects.hash(id, language, trackName, defaultTrack, forcedTrack, type);
}
@Override @Override
public String toString() { public String toString() {
return "[" + "id=" + id + return "[" + "id=" + id +
", language='" + language + '\'' + ", language='" + language + '\'' +
", trackName='" + trackName + '\'' + ", trackName='" + trackName + '\'' +
", defaultTrack=" + defaultTrack + ", defaultt=" + defaultt +
", forcedTrack=" + forcedTrack + ", forced=" + forced +
", commentary=" + commentary +
", hearingImpaired=" + hearingImpaired +
", type=" + type + ", type=" + type +
']'; ']';
} }

View File

@@ -1,6 +1,6 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model; package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
public enum LaneType { public enum TrackType {
AUDIO, AUDIO,
SUBTITLES; SUBTITLES;
} }

View File

@@ -1,23 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import org.apache.commons.cli.Option;
public class CommandLineOptionsUtil {
public static Option optionOf(ConfigProperty property, String opt, int args) {
return optionOf(property, opt, args, false);
}
public static Option optionOf(ConfigProperty property, String opt, boolean hasArg, boolean required) {
return optionOf(property, opt, hasArg ? 1 : 0, required);
}
public static Option optionOf(ConfigProperty property, String opt, int args, boolean required) {
Option option = new Option(opt, property.desc());
option.setArgs(args);
option.setLongOpt(property.prop());
option.setRequired(required);
return option;
}
}

View File

@@ -0,0 +1,21 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import org.apache.commons.lang3.SystemUtils;
import java.io.File;
import java.nio.file.Path;
public class FileUtils {
private static final boolean isWindows = SystemUtils.IS_OS_WINDOWS;
private static String expandPath(File dir, MkvToolNix application) {
return dir.getAbsolutePath().endsWith("/")
? dir.getAbsolutePath() + application
: dir.getAbsolutePath() + "/" + application;
}
public static File getPathFor(File dir, MkvToolNix application) {
return Path.of(expandPath(dir, application) + (isWindows ? ".exe" : "")).toFile();
}
}

View File

@@ -23,10 +23,6 @@ public class LanguageValidatorUtil {
} }
} }
public static boolean isAudioLanguageValid(String language) {
return !language.equals("OFF") && isLanguageValid(language);
}
public static boolean isLanguageValid(String language) { public static boolean isLanguageValid(String language) {
return ISO3_LANGUAGES.contains(language); return ISO3_LANGUAGES.contains(language);
} }

View File

@@ -1,9 +1,13 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util; package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import org.apache.logging.log4j.util.Strings;
import picocli.CommandLine; import picocli.CommandLine;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties; import java.util.Properties;
public class ProjectUtil implements CommandLine.IVersionProvider { public class ProjectUtil implements CommandLine.IVersionProvider {
@@ -17,11 +21,32 @@ public class ProjectUtil implements CommandLine.IVersionProvider {
} }
} }
public String[] getVersion() {
return new String[] {getProjectName() + " " + PROJECT_PROPERTIES.getProperty("version")};
}
public static String getProjectName() { public static String getProjectName() {
return PROJECT_PROPERTIES.getProperty("project_name"); return PROJECT_PROPERTIES.getProperty("project_name");
} }
public String[] getVersion() throws IOException {
String mkvpropeeditVersion = getVersion(MkvToolNix.MKV_PROP_EDIT);
String mkvmergeVersion = getVersion(MkvToolNix.MKV_MERGE);
return new String[] {
getProjectName() + " " + PROJECT_PROPERTIES.getProperty("version"),
"Java ${java.version} (${java.vendor} ${java.vm.name} ${java.vm.version})",
"${os.name} ${os.version} ${os.arch}",
(!Strings.isBlank(mkvpropeeditVersion) ? mkvpropeeditVersion : "MkvPropEdit not found") + ", " + (!Strings.isBlank(mkvmergeVersion) ? mkvmergeVersion : "MkvMerge not found")
};
}
public static String getVersion(MkvToolNix app) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(app.toString(), "--version");
Process process = processBuilder.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String version = reader.readLine();
int exitCode = process.waitFor();
if (exitCode == 0) return version;
} catch (IOException | InterruptedException ignored) {}
return null;
}
} }

View File

@@ -1,13 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class SetUtils {
public static <T> Set<T> retainOf(List<T> list1, List<T> list2) {
Set<T> set = new HashSet<>(list1);
set.retainAll(list2);
return set;
}
}

View File

@@ -0,0 +1,12 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import lombok.Getter;
public class ValidationUtil {
private static final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
@Getter
private static final Validator validator = factory.getValidator();
}

View File

@@ -6,9 +6,9 @@ Configuration:
fileName: ${sys:user.home}/.local/mkvaudiosubtitlechanger/logs/application.log fileName: ${sys:user.home}/.local/mkvaudiosubtitlechanger/logs/application.log
filePattern: ${sys:user.home}/.local/mkvaudiosubtitlechanger/logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz filePattern: ${sys:user.home}/.local/mkvaudiosubtitlechanger/logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout: PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable" Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter: ThresholdFilter:
level: info level: debug
Policies: Policies:
OnStartupTriggeringPolicy: OnStartupTriggeringPolicy:
minSize: 0 minSize: 0

View File

@@ -4,7 +4,7 @@ Configuration:
Console: Console:
name: Console_Out name: Console_Out
PatternLayout: PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable" Pattern: "%d{DEFAULT} | %-5level | %thread | %C{1} | %msg %n %throwable"
ThresholdFilter: ThresholdFilter:
level: debug level: debug
RollingFile: RollingFile:
@@ -12,7 +12,7 @@ Configuration:
fileName: logs/application.log fileName: logs/application.log
filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout: PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable" Pattern: "%d{DEFAULT} | %-5level | %thread | %C{1} | %msg %n %throwable"
ThresholdFilter: ThresholdFilter:
level: debug level: debug
Policies: Policies:

View File

@@ -3,12 +3,12 @@ Configuration:
Appenders: Appenders:
RollingFile: RollingFile:
name: FileAppender name: FileAppender
fileName: ${sys:user.home}/AppData/Roaming/MKVAudioSubtitleChanger/logs/application.log fileName: ${sys:user.home}/AppData/Local/MKVAudioSubtitleChanger/logs/application.log
filePattern: ${sys:user.home}/AppData/Roaming/MKVAudioSubtitleChanger/logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz filePattern: ${sys:user.home}/AppData/Local/MKVAudioSubtitleChanger/logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout: PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable" Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter: ThresholdFilter:
level: info level: debug
Policies: Policies:
OnStartupTriggeringPolicy: OnStartupTriggeringPolicy:
minSize: 0 minSize: 0

View File

@@ -6,9 +6,9 @@ Configuration:
fileName: logs/application.log fileName: logs/application.log
filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout: PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable" Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter: ThresholdFilter:
level: info level: debug
Policies: Policies:
OnStartupTriggeringPolicy: OnStartupTriggeringPolicy:
minSize: 0 minSize: 0

View File

@@ -1,61 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class AttributeConfigTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args("-a", "jpn:ger"), attrConf("jpn", "ger")),
Arguments.of(args("-a", "jpn:ger", "jpn:eng"), attrConf("jpn", "ger", "jpn", "eng")),
Arguments.of(args("-a", "jpn:ger", "jpn:OFF"), attrConf("jpn", "ger", "jpn", "OFF"))
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, List<AttributeConfig> expectedConfig) {
Main underTest = new Main();
CommandLine.populateCommand(underTest, cmdArgs);
assertIterableEquals(expectedConfig, underTest.getConfig().getAttributeConfig());
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, new String[]{"-l", "/"}));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, new String[]{"-l", "/", "-a"}));
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, new String[]{"-l", "/", "-a", "ger:"}));
assertThrows(CommandLine.ParameterException.class,
() -> CommandLine.populateCommand(sut, new String[]{"-l", "/", "-a", "ger:qwf"})); // Invalid language code
}
private static String[] args(String... args) {
String[] staticArray = new String[]{"-l", "/"};
String[] result = new String[staticArray.length + args.length];
System.arraycopy(staticArray, 0, result, 0, staticArray.length);
System.arraycopy(args, 0, result, staticArray.length, args.length);
return result;
}
private static List<AttributeConfig> attrConf(String... languages) {
List<AttributeConfig> conf = new ArrayList<>();
for (int i = 0; i < languages.length; i += 2) {
conf.add(new AttributeConfig(languages[i], languages[i+1]));
}
return conf;
}
}

View File

@@ -1,37 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.util.function.Function;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.assertEquals;
class BooleanConfigParameterTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args("-s"), true, (Function<Config, Boolean>) Config::isSafeMode),
Arguments.of(args("--safemode"), true, (Function<Config, Boolean>) Config::isSafeMode),
Arguments.of(args(), false, (Function<Config, Boolean>) Config::isSafeMode),
Arguments.of(args("-cf"), true, (Function<Config, Boolean>) Config::isForceCoherent),
Arguments.of(args("--force-coherent"), true, (Function<Config, Boolean>) Config::isForceCoherent),
Arguments.of(args(), false, (Function<Config, Boolean>) Config::isForceCoherent),
Arguments.of(args("-n"), true, (Function<Config, Boolean>) Config::isOnlyNewFiles),
Arguments.of(args(), false, (Function<Config, Boolean>) Config::isOnlyNewFiles)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, boolean expected, Function<Config, Boolean> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(expected, fieldUnderTest.apply(sut.getConfig()));
}
}

View File

@@ -1,46 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import org.junit.jupiter.api.Test;
import picocli.CommandLine;
import java.util.List;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.TEST_FILE;
import static org.junit.jupiter.api.Assertions.*;
class ConfigTest {
@Test
void initConfig() {
String[] sut = new String[]{"-a", "ger:ger", "eng:eng", "-l", TEST_FILE,
"-s", "-cf", "-n",
"-c", "2",
"-t", "4",
"-i", ".*[abc].*",
"--forced-keywords", "testForced",
"--commentary-keywords", "testCommentary",
"--preferred-subtitles", "testPreferred"
};
CommandLine.populateCommand(Config.getInstance(), sut);
assertTrue(Config.getInstance().getLibraryPath().exists());
assertEquals(List.of(new AttributeConfig("ger", "ger"), new AttributeConfig("eng", "eng")),
Config.getInstance().getAttributeConfig());
assertTrue(Config.getInstance().isSafeMode());
assertTrue(Config.getInstance().isForceCoherent());
assertTrue(Config.getInstance().isOnlyNewFiles());
assertNull(Config.getInstance().getFilterDate());
assertEquals(2, Config.getInstance().getCoherent());
assertEquals(4, Config.getInstance().getThreads());
assertEquals(".*[abc].*", Config.getInstance().getIncludePattern().pattern());
assertTrue(Config.getInstance().getForcedKeywords().contains("testForced"));
assertTrue(Config.getInstance().getCommentaryKeywords().contains("testCommentary"));
assertTrue(Config.getInstance().getPreferredSubtitles().contains("testPreferred"));
assertNull(Config.getInstance().getConfigPath());
}
}

View File

@@ -1,50 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.function.Function;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.*;
class IntegerConfigParameterTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args(), 2, (Function<Config, Integer>) Config::getThreads),
Arguments.of(args("-t", "5"), 5, (Function<Config, Integer>) Config::getThreads),
Arguments.of(args("--threads", "5"), 5, (Function<Config, Integer>) Config::getThreads)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, int expected, Function<Config, Integer> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(expected, fieldUnderTest.apply(sut.getConfig()));
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("-t")));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("--threads")));
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
CommandLine underTest = new CommandLine(sut);
underTest = underTest.setErr(printWriter);
underTest.execute(args("-t", "0"));
printWriter.flush();
assertTrue(writer.toString().contains("ERROR: threads must be greater than or equal to 1"));
}
}

View File

@@ -1,54 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.io.File;
import java.nio.file.Path;
import java.util.function.Function;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.*;
class MkvToolNixPathConfigParameterTest {
private static final String TEST_INVALID_DIR = "src/test/resources/test-dir";
private static final String TEST_MKVTOOLNIX_DIR = "src/test/resources/mkvtoolnix";
private static final String TEST_MKVTOOLNIX_EXE_DIR = "src/test/resources/mkvtoolnix_exe";
private static Stream<Arguments> provideTestCases() {
if (SystemUtils.IS_OS_WINDOWS) {
return Stream.of(
Arguments.of(args("-m", TEST_MKVTOOLNIX_EXE_DIR), TEST_MKVTOOLNIX_EXE_DIR, (Function<Config, File>) Config::getMkvToolNix),
Arguments.of(args("--mkvtoolnix", TEST_MKVTOOLNIX_EXE_DIR), TEST_MKVTOOLNIX_EXE_DIR, (Function<Config, File>) Config::getMkvToolNix)
);
}
return Stream.of(
Arguments.of(args("-m", TEST_MKVTOOLNIX_DIR), TEST_MKVTOOLNIX_DIR, (Function<Config, File>) Config::getMkvToolNix),
Arguments.of(args("--mkvtoolnix", TEST_MKVTOOLNIX_DIR), TEST_MKVTOOLNIX_DIR, (Function<Config, File>) Config::getMkvToolNix)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, String expected, Function<Config, File> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(Path.of(expected).toFile().getAbsolutePath(), fieldUnderTest.apply(sut.getConfig()).getAbsolutePath());
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, args("-m", TEST_INVALID_DIR)));
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, args("-m")));
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, args("")));
}
}

View File

@@ -1,53 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.io.File;
import java.nio.file.Path;
import java.util.function.Function;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.TEST_DIR;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.TEST_FILE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PathConfigParameterTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args("-l", TEST_DIR), Path.of(TEST_DIR).toFile(), true, (Function<Config, File>) Config::getLibraryPath),
Arguments.of(args("-l", TEST_FILE), Path.of(TEST_FILE).toFile(), true, (Function<Config, File>) Config::getLibraryPath)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, File expected, boolean exists, Function<Config, File> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(expected.getAbsolutePath(), fieldUnderTest.apply(sut.getConfig()).getAbsolutePath());
assertEquals(exists, fieldUnderTest.apply(sut.getConfig()).exists());
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, args("-l", "arst")));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("-l")));
assertThrows(CommandLine.UnmatchedArgumentException.class, () -> CommandLine.populateCommand(sut, args("")));
}
private static String[] args(String... args) {
String[] staticArray = new String[]{"-a", "ger:ger"};
String[] result = new String[staticArray.length + args.length];
System.arraycopy(staticArray, 0, result, 0, staticArray.length);
System.arraycopy(args, 0, result, staticArray.length, args.length);
return result;
}
}

View File

@@ -1,42 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class PatternConfigParameterTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args("-i", "[abd]?.*"), Pattern.compile("[abd]?.*"), (Function<Config, Pattern>) Config::getIncludePattern),
Arguments.of(args("-i", ".*"), Pattern.compile(".*"), (Function<Config, Pattern>) Config::getIncludePattern),
Arguments.of(args(), Pattern.compile(".*"), (Function<Config, Pattern>) Config::getIncludePattern)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, Pattern expected, Function<Config, Pattern> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(expected.pattern(), fieldUnderTest.apply(sut.getConfig()).pattern());
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("-i")));
assertThrows(CommandLine.ParameterException.class, () -> CommandLine.populateCommand(sut, args("-i", "[")));
}
}

View File

@@ -1,50 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.Main;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class SetConfigParameterTest {
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(args("--commentary-keywords", "test"), 1, (Function<Config, Set<String>>) Config::getCommentaryKeywords),
Arguments.of(args("--commentary-keywords", "test", "test1", "test2", "test3", "test4"), 5, (Function<Config, Set<String>>) Config::getCommentaryKeywords),
Arguments.of(args(), 2, (Function<Config, Set<String>>) Config::getCommentaryKeywords),
Arguments.of(args("--forced-keywords", "test"), 1, (Function<Config, Set<String>>) Config::getForcedKeywords),
Arguments.of(args("--forced-keywords", "test", "test1", "test2", "test3", "test4"), 5, (Function<Config, Set<String>>) Config::getForcedKeywords),
Arguments.of(args(), 3, (Function<Config, Set<String>>) Config::getForcedKeywords),
Arguments.of(args("--preferred-subtitles", "test"), 1, (Function<Config, Set<String>>) Config::getPreferredSubtitles),
Arguments.of(args("--preferred-subtitles", "test", "test1", "test2", "test3", "test4"), 5, (Function<Config, Set<String>>) Config::getPreferredSubtitles),
Arguments.of(args(), 1, (Function<Config, Set<String>>) Config::getPreferredSubtitles)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(String[] cmdArgs, int expectedSize, Function<Config, Set<String>> fieldUnderTest) {
Main sut = new Main();
CommandLine.populateCommand(sut, cmdArgs);
assertEquals(expectedSize, fieldUnderTest.apply(sut.getConfig()).size());
}
@Test
void validate() {
Main sut = new Main();
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("--commentary-keywords")));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("--forced-keywords")));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("-e")));
assertThrows(CommandLine.MissingParameterException.class, () -> CommandLine.populateCommand(sut, args("--preferred-subtitles")));
}
}

View File

@@ -0,0 +1,50 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.converter;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.converter.AttributeConfigConverter;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class AttributeConfigConverterTest {
private static Stream<Arguments> validData() {
return Stream.of(
Arguments.of("jpn:ger", new AttributeConfig("jpn", "ger")),
Arguments.of("eng:eng", new AttributeConfig("eng", "eng")),
Arguments.of("OFF:OFF", new AttributeConfig("OFF", "OFF"))
);
}
@ParameterizedTest
@MethodSource("validData")
void convert(String input, AttributeConfig expected) {
AttributeConfigConverter underTest = new AttributeConfigConverter();
AttributeConfig actual = underTest.convert(input);
assertEquals(expected, actual);
}
private static Stream<Arguments> invalidData() {
return Stream.of(
Arguments.of("ars:eng"),
Arguments.of("ars:OFF"),
Arguments.of("OFF:ars"),
Arguments.of("ars:ars"),
Arguments.of("arss:ars"),
Arguments.of("ars:arsr")
);
}
@ParameterizedTest
@MethodSource("invalidData")
void convertInvalid(String input) {
AttributeConfigConverter underTest = new AttributeConfigConverter();
assertThrows(CommandLine.TypeConversionException.class, () -> underTest.convert(input));
}
}

View File

@@ -1,57 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config.validator;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlInvalidContentException;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult.*;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult.INVALID;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty.CONFIG_PATH;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.CommandLineOptionsUtil.optionOf;
import static org.junit.jupiter.api.Assertions.*;
class ConfigPathValidatorTest {
private static final String TEST_FILE = "src/test/resources/test-dir/test-file.mkv";
private static final String TEST_CONFIG = "src/test/resources/test-dir/test-config.yaml";
private static CommandLineParser parser;
private static Options options;
@BeforeAll
static void before() {
parser = new DefaultParser();
options = new Options();
options.addOption(optionOf(CONFIG_PATH, CONFIG_PATH.abrv(), CONFIG_PATH.args()));
}
private static Stream<Arguments> provideTestCases() {
return Stream.of(
Arguments.of(CONFIG_PATH, true, "", new String[]{"-p", TEST_CONFIG}, VALID),
Arguments.of(CONFIG_PATH, true, "config-path: " + TEST_CONFIG, new String[]{}, MISSING),
Arguments.of(CONFIG_PATH, false, "config-path: " + TEST_CONFIG, new String[]{}, NOT_PRESENT),
Arguments.of(CONFIG_PATH, true, "", new String[]{"-p", TEST_FILE}, INVALID)
);
}
@ParameterizedTest
@MethodSource("provideTestCases")
void validate(ConfigProperty property, boolean required, String yamlArgs, String[] cmdArgs,
ValidationResult expectedResult) throws ParseException, YamlInvalidContentException {
ConfigPathValidator underTest = new ConfigPathValidator(property, required);
ValidationResult result = underTest.validate(new YAML(yamlArgs), parser.parse(options, cmdArgs));
assertEquals(expectedResult, result);
}
}

View File

@@ -0,0 +1,85 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.TEST_FILE;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class FileFilterTest {
@Mock(strictness = Mock.Strictness.LENIENT)
File file;
@Mock(strictness = Mock.Strictness.LENIENT)
BasicFileAttributes attributes;
@BeforeEach
void beforeEach() {
ResultStatistic.getInstance(true);
}
private static Stream<Arguments> accept() {
return Stream.of(
Arguments.of("~/video.mkv", Set.of(".mkv"), Set.of(), -1, ".*", true, false),
Arguments.of("~/video.mp4", Set.of(".mkv"), Set.of(), -1, ".*", false, false),
Arguments.of("~/video.mkv", Set.of(".mkv"), Set.of(), -1, "v.*", true, false),
Arguments.of("~/video.mkv", Set.of(".mkv"), Set.of(), -1, "a.*", false, true),
Arguments.of("~/video.mkv", Set.of(".mkv"), Set.of(), -1000, ".*", true, false),
Arguments.of("~/video.mkv", Set.of(".mkv"), Set.of(), 1000, ".*", false, true),
Arguments.of("dir/video.mkv", Set.of(".mkv"), Set.of("dir"), -1, ".*", false, true),
Arguments.of("dir/dir2/video.mkv", Set.of(".mkv"), Set.of("dir/dir2"), -1, ".*", false, true),
Arguments.of("dir/video.mkv", Set.of(".mkv"), Set.of("dir/dir2"), -1, ".*", true, false),
Arguments.of("dirr/video.mkv", Set.of(".mkv"), Set.of("dir"), -1, ".*", true, false)
);
}
/**
* @param filterDateOffset move filter data into the future or past by positive and negative values
*/
@ParameterizedTest
@MethodSource
void accept(String path, Set<String> extensions, Set<String> excludedDirs, int filterDateOffset, String pattern, boolean acceptanceExpected, boolean excluded) {
when(file.getAbsolutePath()).thenReturn(path);
when(file.getPath()).thenReturn(path);
String[] split = path.split("/");
when(file.getName()).thenReturn(split[split.length - 1]);
when(file.toPath()).thenReturn(Path.of(TEST_FILE));
long currentTime = System.currentTimeMillis();
FileFilter fileFilter = new FileFilter(excludedDirs, Pattern.compile(pattern), new Date(currentTime + filterDateOffset));
try (MockedStatic<DateUtils> mockedFiles = Mockito.mockStatic(DateUtils.class)) {
mockedFiles
.when(() -> DateUtils.convert(anyLong()))
.thenReturn(new Date(currentTime));
assertEquals(acceptanceExpected, fileFilter.accept(file, new HashSet<>(extensions)), "File is accepted");
assertEquals(excluded, ResultStatistic.getInstance().getExcluded() > 0, "Is counted in excluded statistic");
}
}
}

View File

@@ -1,46 +1,59 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl; package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.LaneType; import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackType;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
class SubtitleTrackComparatorTest { class SubtitleTrackComparatorTest {
private static final SubtitleTrackComparator comparator = new SubtitleTrackComparator(new String[]{"unstyled"});
private static Stream<Arguments> compareArguments() { private static Stream<Arguments> compareArguments() {
return Stream.of( return Stream.of(
Arguments.of(List.of(attr("unstyled sub", false), attr("styled sub", false)), Arguments.of(attr(""), attr(""), 0),
List.of(attr("unstyled sub", false), attr("styled sub", false))), Arguments.of(attr("pref"), attr(""), 1),
Arguments.of(List.of(attr("styled sub", false), attr("unstyled sub", false)), Arguments.of(attr(""), attr("pref"), -1),
List.of(attr("unstyled sub", false), attr("styled sub", false))), Arguments.of(attr("pref"), attr("pref"), 0),
Arguments.of(List.of(attr("unstyled sub", true), attr("styled sub", false)), Arguments.of(attr("", true), attr("", true), 0),
List.of(attr("unstyled sub", true), attr("styled sub", false))), Arguments.of(attr("", true), attr(""), -1),
Arguments.of(List.of(attr("styled sub", true), attr("unstyled sub", false)), Arguments.of(attr("CC", true), attr(""), -1),
List.of(attr("unstyled sub", false), attr("styled sub", true))), Arguments.of(attr("CC"), attr(""), -1),
Arguments.of(attr(""), attr("", true), 1),
Arguments.of(attr(""), attr("CC", true), 1),
Arguments.of(attr(""), attr("CC"), 1),
Arguments.of(List.of(attr("unstyled sub", true), attr("unstyled sub", false)), Arguments.of(attr("pref", true), attr("pref"), -1),
List.of(attr("unstyled sub", true), attr("unstyled sub", false))) Arguments.of(attr("pref", true), attr("pref", true), 0),
Arguments.of(attr("pref"), attr("pref", true), 1),
Arguments.of(attr("", true), attr("pref"), -2),
Arguments.of(attr("pref"), attr("", true), 2),
Arguments.of(attr(null), attr(null), 0),
Arguments.of(attr(null), attr(""), 0),
Arguments.of(attr(null), attr("pref"), -1),
Arguments.of(attr(""), attr(null), 0),
Arguments.of(attr("pref"), attr(null), 1)
); );
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("compareArguments") @MethodSource("compareArguments")
void compare(List<FileAttribute> input, List<FileAttribute> expected) { void compare(TrackAttributes track1, TrackAttributes track2, int expected) {
List<FileAttribute> result = input.stream().sorted(comparator.reversed()).collect(Collectors.toList()); SubtitleTrackComparator comparator = new SubtitleTrackComparator(List.of("pref"), List.of("CC", "SDH"));
int actual = comparator.compare(track1, track2);
assertArrayEquals(expected.toArray(new FileAttribute[0]), result.toArray(new FileAttribute[0])); assertEquals(expected, actual);
} }
private static FileAttribute attr(String trackName, boolean defaultTrack) { private static TrackAttributes attr(String trackname) {
return new FileAttribute(0, "", trackName, defaultTrack, false, LaneType.SUBTITLES); return attr(trackname, false);
}
private static TrackAttributes attr(String trackName, boolean hearingImpaired) {
return new TrackAttributes(0, "", trackName, false, false, false, hearingImpaired, TrackType.SUBTITLES);
} }
} }

View File

@@ -0,0 +1,287 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.FileInfoTestUtil.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class AttributeChangeProcessorTest {
private static Stream<Arguments> attributeConfigMatching() {
return Stream.of(
Arguments.of(
List.of(withName(AUDIO_ENG, null), SUB_ENG),
arr(a("eng:eng")), "eng:eng",
Map.ofEntries(on(withName(AUDIO_ENG, null)), on(SUB_ENG))
),
Arguments.of(
List.of(AUDIO_ENG, SUB_ENG),
arr(a("eng:eng")), "eng:eng",
Map.ofEntries(on(AUDIO_ENG), on(SUB_ENG))
),
Arguments.of(
List.of(AUDIO_ENG, AUDIO_GER, SUB_ENG, SUB_GER),
arr(a("eng:eng")), "eng:eng",
Map.ofEntries(on(AUDIO_ENG), on(SUB_ENG))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER, SUB_ENG, SUB_GER),
arr(a("ger:eng")), "ger:eng",
Map.ofEntries(off(AUDIO_ENG_DEFAULT), on(AUDIO_GER), on(SUB_ENG))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER, SUB_ENG, SUB_GER),
arr(a("eng:ger")), "eng:ger",
Map.ofEntries(on(SUB_GER))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER, SUB_ENG_DEFAULT, SUB_GER),
arr(a("eng:OFF")), "eng:OFF",
Map.ofEntries(off(SUB_ENG_DEFAULT))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER, SUB_ENG_DEFAULT, SUB_GER),
arr(a("OFF:OFF")), "OFF:OFF",
Map.ofEntries(off(AUDIO_ENG_DEFAULT), off(SUB_ENG_DEFAULT))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER, SUB_GER),
arr(a("eng:eng"), a("eng:ger")), "eng:ger",
Map.ofEntries(on(SUB_GER))
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER_COMMENTARY, SUB_GER_FORCED),
arr(a("ger:ger")), null,
Map.ofEntries()
),
Arguments.of(
List.of(AUDIO_ENG_DEFAULT, AUDIO_GER_COMMENTARY, withName(SUB_GER, "forced")),
arr(a("ger:ger")), null,
Map.ofEntries()
),
Arguments.of(
List.of(AUDIO_GER, withName(SUB_GER, "SDH")),
arr(a("ger:ger")), "ger:ger",
Map.ofEntries(on(AUDIO_GER), on(withName(SUB_GER, "SDH")))
),
Arguments.of(
List.of(AUDIO_GER, withName(SUB_GER, "SDH"), SUB_GER),
arr(a("ger:ger")), "ger:ger",
Map.ofEntries(on(AUDIO_GER), on(SUB_GER))
),
Arguments.of(
List.of(AUDIO_GER, SUB_GER_HEARING),
arr(a("ger:ger")), "ger:ger",
Map.ofEntries(on(AUDIO_GER), on(SUB_GER_HEARING))
),
Arguments.of(
List.of(AUDIO_GER, SUB_GER_HEARING, SUB_GER),
arr(a("ger:ger")), "ger:ger",
Map.ofEntries(on(AUDIO_GER), on(SUB_GER))
),
Arguments.of(
List.of(AUDIO_GER, SUB_ENG_HEARING, SUB_GER),
arr(a("ger:eng")), "ger:eng",
Map.ofEntries(on(AUDIO_GER), on(SUB_ENG_HEARING))
)
);
}
@ParameterizedTest
@MethodSource("attributeConfigMatching")
void findDefaultMatchAndApplyChanges(List<TrackAttributes> tracks, AttributeConfig[] config, String expectedConfig, Map<TrackAttributes, Boolean> changes) {
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(new String[]{}, Set.of("forced"), Set.of("commentary"), Set.of("SDH"));
FileInfo fileInfo = new FileInfo(null);
fileInfo.addTracks(tracks);
attributeChangeProcessor.findDefaultMatchAndApplyChanges(fileInfo, config);
assertEquals(expectedConfig, fileInfo.getMatchedConfig() != null ? fileInfo.getMatchedConfig().toStringShort() : fileInfo.getMatchedConfig());
assertEquals(changes.size(), fileInfo.getChanges().getDefaultTrack().size());
changes.forEach((key, value) -> {
assertTrue(fileInfo.getChanges().getDefaultTrack().containsKey(key));
assertEquals(value, fileInfo.getChanges().getDefaultTrack().get(key));
});
}
private static AttributeConfig[] arr(AttributeConfig... configs) {
return configs;
}
private static AttributeConfig a(String config) {
String[] split = config.split(":");
return new AttributeConfig(split[0], split[1]);
}
private static Map.Entry<TrackAttributes, Boolean> on(TrackAttributes track) {
return Map.entry(track, true);
}
private static Map.Entry<TrackAttributes, Boolean> off(TrackAttributes track) {
return Map.entry(track, false);
}
private static Stream<Arguments> filterForPossibleDefaults() {
return Stream.of(
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER)),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "forced"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER)),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "Forced"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER)),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "commentary"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER)),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "Commentary"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER)),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "SDH"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER, withName(AUDIO_GER, "SDH"))),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, withName(AUDIO_GER, "sdh"), SUB_GER), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER, withName(AUDIO_GER, "sdh"))),
Arguments.of(List.of(AUDIO_GER, AUDIO_ENG, SUB_GER, SUB_GER_FORCED, AUDIO_GER_COMMENTARY, AUDIO_GER_HEARING), Set.of(AUDIO_GER, AUDIO_ENG, SUB_GER, AUDIO_GER_HEARING))
);
}
@ParameterizedTest
@MethodSource("filterForPossibleDefaults")
void filterForPossibleDefaults(List<TrackAttributes> tracks, Set<TrackAttributes> expected) throws InvocationTargetException, IllegalAccessException {
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(new String[]{}, Set.of("forced"), Set.of("commentary"), Set.of("SDH"));
Optional<Method> method = Arrays.stream(AttributeChangeProcessor.class.getDeclaredMethods())
.filter(m -> m.getName().equals("filterForPossibleDefaults"))
.findFirst();
assertTrue(method.isPresent());
Method underTest = method.get();
underTest.setAccessible(true);
List<TrackAttributes> result = (List<TrackAttributes>) underTest.invoke(attributeChangeProcessor, tracks);
assertEquals(expected.size(), result.size());
for (TrackAttributes track : result) {
assertTrue(expected.contains(track));
}
}
private static Stream<Arguments> findForcedTracksAndApplyChanges() {
return Stream.of(
Arguments.of(List.of(),
Set.of("song & signs"), false,
Map.ofEntries()
),
Arguments.of(List.of(withName(SUB_GER, "song & signs"), SUB_GER),
Set.of("song & signs"), false,
Map.ofEntries(on(withName(SUB_GER, "song & signs")))
),
Arguments.of(List.of(withName(SUB_GER, "song & signs"), withName(SUB_GER, "")),
Set.of("song & signs"), false,
Map.ofEntries(on(withName(SUB_GER, "song & signs")))
),
Arguments.of(List.of(withName(SUB_GER, "song & signs"), withName(SUB_GER, null)),
Set.of("song & signs"), false,
Map.ofEntries(on(withName(SUB_GER, "song & signs")))
),
Arguments.of(List.of(withName(SUB_GER, "song & signs")),
Set.of("song & signs"), false,
Map.ofEntries(on(withName(SUB_GER, "song & signs")))
),
Arguments.of(List.of(withName(SUB_GER_FORCED, "song & signs")),
Set.of("song & signs"), false,
Map.ofEntries()
),
Arguments.of(List.of(SUB_GER_FORCED, withName(SUB_GER, "song & signs")),
Set.of("song & signs"), true,
Map.ofEntries(off(SUB_GER_FORCED), on(withName(SUB_GER, "song & signs")))
)
);
}
@ParameterizedTest
@MethodSource("findForcedTracksAndApplyChanges")
void findForcedTracksAndApplyChanges(List<TrackAttributes> tracks, Set<String> keywords, boolean overwrite, Map<TrackAttributes, Boolean> changes) {
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(new String[]{}, keywords, Set.of(), Set.of());
FileInfo fileInfo = new FileInfo(null);
fileInfo.addTracks(tracks);
attributeChangeProcessor.findForcedTracksAndApplyChanges(fileInfo, overwrite);
assertEquals(changes.size(), fileInfo.getChanges().getForcedTrack().size());
changes.forEach((key, value) -> {
assertTrue(fileInfo.getChanges().getForcedTrack().containsKey(key));
assertEquals(value, fileInfo.getChanges().getForcedTrack().get(key));
});
}
private static Stream<Arguments> findCommentaryTracksAndApplyChanges() {
return Stream.of(
Arguments.of(List.of(withName(SUB_GER, "commentary"), withName(SUB_GER, null)),
Set.of("commentary"),
Map.ofEntries(on(withName(SUB_GER, "commentary")))
),
Arguments.of(List.of(withName(SUB_GER, "commentary")),
Set.of("commentary"),
Map.ofEntries(on(withName(SUB_GER, "commentary")))
),
Arguments.of(List.of(withName(AUDIO_GER_COMMENTARY, "commentary")),
Set.of("commentary"),
Map.ofEntries()
),
Arguments.of(List.of(AUDIO_GER_COMMENTARY, withName(SUB_GER, "commentary")),
Set.of("commentary"),
Map.ofEntries(on(withName(SUB_GER, "commentary")))
)
);
}
@ParameterizedTest
@MethodSource("findCommentaryTracksAndApplyChanges")
void findCommentaryTracksAndApplyChanges(List<TrackAttributes> tracks, Set<String> keywords, Map<TrackAttributes, Boolean> changes) {
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(new String[]{}, Set.of(), keywords, Set.of());
FileInfo fileInfo = new FileInfo(null);
fileInfo.addTracks(tracks);
attributeChangeProcessor.findCommentaryTracksAndApplyChanges(fileInfo);
assertEquals(changes.size(), fileInfo.getChanges().getCommentaryTrack().size());
changes.forEach((key, value) -> {
assertTrue(fileInfo.getChanges().getCommentaryTrack().containsKey(key));
assertEquals(value, fileInfo.getChanges().getCommentaryTrack().get(key));
});
}
private static Stream<Arguments> findHearingImpairedTracksAndApplyChanges() {
return Stream.of(
Arguments.of(List.of(withName(SUB_GER, "SDH"), withName(SUB_GER, null)),
Set.of("SDH"),
Map.ofEntries(on(withName(SUB_GER, "SDH")))
),
Arguments.of(List.of(withName(SUB_GER, "SDH")),
Set.of("SDH"),
Map.ofEntries(on(withName(SUB_GER, "SDH")))
),
Arguments.of(List.of(withName(AUDIO_GER_HEARING, "SDH")),
Set.of("SDH"),
Map.ofEntries()
),
Arguments.of(List.of(AUDIO_GER_HEARING, withName(SUB_GER, "SDH")),
Set.of("SDH"),
Map.ofEntries(on(withName(SUB_GER, "SDH")))
)
);
}
@ParameterizedTest
@MethodSource("findHearingImpairedTracksAndApplyChanges")
void findHearingImpairedTracksAndApplyChanges(List<TrackAttributes> tracks, Set<String> keywords, Map<TrackAttributes, Boolean> changes) {
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(new String[]{}, Set.of(), Set.of(), keywords);
FileInfo fileInfo = new FileInfo(null);
fileInfo.addTracks(tracks);
attributeChangeProcessor.findHearingImpairedTracksAndApplyChanges(fileInfo);
assertEquals(changes.size(), fileInfo.getChanges().getHearingImpairedTrack().size());
changes.forEach((key, value) -> {
assertTrue(fileInfo.getChanges().getHearingImpairedTrack().containsKey(key));
assertEquals(value, fileInfo.getChanges().getHearingImpairedTrack().get(key));
});
}
}

View File

@@ -0,0 +1,65 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.File;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.FileInfoTestUtil.AUDIO_GER;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.TEST_FILE;
import static org.junit.jupiter.api.Assertions.*;
class AttributeUpdaterTest {
@BeforeEach
void setup() {
ResultStatistic.getInstance(true);
}
private static Stream<Arguments> checkStatusAndUpdate() {
return Stream.of(
Arguments.of(info(new AttributeConfig("ger", "ger"), AUDIO_GER), supplier(() -> ResultStatistic.getInstance().getChangePlanned())),
Arguments.of(info(new AttributeConfig("ger", "ger"), null), supplier(() -> ResultStatistic.getInstance().getUnchanged())),
Arguments.of(info(null, null), supplier(() -> ResultStatistic.getInstance().getUnchanged()))
);
}
@ParameterizedTest
@MethodSource("checkStatusAndUpdate")
void checkStatusAndUpdate(FileInfo fileInfo, Supplier<Integer> getActual) {
InputConfig config = new InputConfig();
config.setThreads(1);
config.setSafeMode(true);
AttributeUpdater underTest = new AttributeUpdater(config, null, null) {
@Override
protected List<File> getFiles() {
return List.of();
}
@Override
protected void process(File file) {
}
};
underTest.checkStatusAndUpdate(fileInfo);
assertEquals(1, getActual.get());
}
private static Supplier<Integer> supplier(Supplier<Integer> supplier) {
return supplier;
}
private static FileInfo info(AttributeConfig config, TrackAttributes attr) {
FileInfo fileInfo = new FileInfo(new File(TEST_FILE));
fileInfo.setMatchedConfig(config);
if(attr != null) fileInfo.getChanges().getDefaultTrack().put(attr, true);
return fileInfo;
}
}

View File

@@ -0,0 +1,94 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CommandRunner;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.InputConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import picocli.CommandLine;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.FileInfoTestUtil.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CoherentAttributeUpdaterTest {
@Mock(lenient = true)
FileProcessor fileProcessor;
@Test
void process() {
}
private static Stream<Arguments> findMatch() {
return Stream.of(
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(), false, 0),
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(fileInfoMock("test.mkv", AUDIO_GER, SUB_GER)), true, 1),
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(fileInfoMock("test.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test2.mkv", AUDIO_GER, SUB_GER)), true, 2),
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(fileInfoMock("test.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test2.mkv", AUDIO_ENG, SUB_ENG)), false, 1),
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(fileInfoMock("test.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test2.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test3.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test4.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test5.mkv", AUDIO_ENG, SUB_ENG)), false, 4),
Arguments.of(AttributeConfig.of("ger", "ger"),
List.of(fileInfoMock("test.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test2.mkv", AUDIO_ENG, SUB_GER),
fileInfoMock("test3.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test4.mkv", AUDIO_GER, SUB_GER),
fileInfoMock("test5.mkv", AUDIO_GER, SUB_ENG)), false, 1)
);
}
@ParameterizedTest
@MethodSource("findMatch")
void findMatch(AttributeConfig attributeConfig, List<Pair<File, FileInfo>> fileInfoMock, boolean expectedMatch, int expectedMatchCount) throws InvocationTargetException, IllegalAccessException {
CommandRunner commandRunner = new CommandRunner();
new CommandLine(commandRunner).parseArgs("-a", "ger:ger", "/arst");
InputConfig config = commandRunner.getConfig();
AttributeChangeProcessor attributeChangeProcessor = new AttributeChangeProcessor(config.getPreferredSubtitles().toArray(new String[0]), config.getForcedKeywords(), config.getCommentaryKeywords(), config.getHearingImpaired());
CoherentAttributeUpdater updater = new CoherentAttributeUpdater(config, fileProcessor, attributeChangeProcessor);
Set<FileInfo> matchedFiles = new HashSet<>(fileInfoMock.size() * 2);
List<File> files = new ArrayList<>();
for (Pair<File, FileInfo> pair : fileInfoMock) {
when(fileProcessor.readAttributes(pair.getKey())).thenReturn(pair.getRight());
files.add(pair.getKey());
}
Method underTest = Arrays.stream(updater.getClass().getDeclaredMethods()).filter(m -> "findMatch".equals(m.getName())).findFirst().get();
underTest.setAccessible(true);
AttributeConfig actualMatch = (AttributeConfig) underTest.invoke(updater, attributeConfig, matchedFiles, files);
assertEquals(expectedMatch ? attributeConfig : null, actualMatch, "Matched AttributeConfig");
assertEquals(expectedMatchCount, matchedFiles.size(), "Matched files count");
}
private static Pair<File, FileInfo> fileInfoMock(String path, TrackAttributes... tracks) {
File file = new File(path);
FileInfo fileInfo = new FileInfo(file);
fileInfo.addTracks(List.of(tracks));
return Pair.of(file, fileInfo);
}
}

View File

@@ -0,0 +1,135 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.processors;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfo;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackType;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.spy;
@ExtendWith(MockitoExtension.class)
class MkvFileProcessorTest {
@Test
void readAttributes() throws IOException {
String mkvmergeResponse = """
{
"tracks": [
{
"id": 0,
"properties": {
"default_track": true,
"enabled_track": true,
"forced_track": false,
"language": "jpn",
"number": 1
},
"type": "video"
},
{
"id": 1,
"properties": {
"track_name": "testing",
"default_track": true,
"enabled_track": true,
"forced_track": false,
"language": "jpn",
"number": 2
},
"type": "audio"
},
{
"id": 2,
"properties": {
"default_track": true,
"enabled_track": true,
"forced_track": false,
"commentary_track": true,
"flag_hearing_impaired": true,
"language": "eng",
"number": 3
},
"type": "subtitles"
}
]
}
""";
MkvFileProcessor underTest = spy(new MkvFileProcessor(new File("mkvtoolnix"), null));
doReturn(new ByteArrayInputStream(mkvmergeResponse.getBytes(StandardCharsets.UTF_8)))
.when(underTest).run(any(String[].class));
FileInfo result = underTest.readAttributes(new File("arst"));
TrackAttributes audio = result.getAudioTracks().get(0);
assertEquals(2, audio.id());
assertEquals("testing", audio.trackName());
assertEquals("jpn", audio.language());
assertTrue(audio.defaultt());
assertFalse(audio.forced());
assertFalse(audio.hearingImpaired());
assertFalse(audio.commentary());
assertEquals(TrackType.AUDIO, audio.type());
TrackAttributes sub = result.getSubtitleTracks().get(0);
assertEquals(3, sub.id());
assertNull(sub.trackName());
assertEquals("eng", sub.language());
assertTrue(sub.defaultt());
assertFalse(sub.forced());
assertTrue(sub.hearingImpaired());
assertTrue(sub.commentary());
assertEquals(TrackType.SUBTITLES, sub.type());
}
@Test
void getUpdateCommand() throws InvocationTargetException, IllegalAccessException {
FileInfo fileInfo = new FileInfo(new File("./"));
fileInfo.getChanges().getDefaultTrack().put(t(1), true);
fileInfo.getChanges().getDefaultTrack().put(t(2), false);
fileInfo.getChanges().getForcedTrack().put(t(3), true);
fileInfo.getChanges().getForcedTrack().put(t(4), false);
fileInfo.getChanges().getCommentaryTrack().put(t(5), true);
fileInfo.getChanges().getCommentaryTrack().put(t(6), false);
fileInfo.getChanges().getHearingImpairedTrack().put(t(7), true);
fileInfo.getChanges().getHearingImpairedTrack().put(t(8), false);
String[] expectedCommand = """
--edit track:1 --set flag-default=1
--edit track:2 --set flag-default=0
--edit track:3 --set flag-forced=1
--edit track:4 --set flag-forced=0
--edit track:5 --set flag-commentary=1
--edit track:6 --set flag-commentary=0
--edit track:7 --set flag-hearing-impaired=1
--edit track:8 --set flag-hearing-impaired=0
""".split("\\n");
MkvFileProcessor mkvFileProcessor = new MkvFileProcessor(new File("mkvtoolnix"), null);
Method underTest = Arrays.stream(mkvFileProcessor.getClass().getDeclaredMethods()).filter(m -> "getUpdateCommand".equals(m.getName())).findFirst().get();
underTest.setAccessible(true);
String[] actualCommand = (String[]) underTest.invoke(mkvFileProcessor, fileInfo);
String[] trimmedActualCommand = Arrays.copyOfRange(actualCommand, 2, actualCommand.length);
String actualCommandString = String.join(" ", trimmedActualCommand);
assertTrue(expectedCommand.length * 4 == trimmedActualCommand.length, "Command length is equal");
for (String commandPart: expectedCommand) {
assertTrue(actualCommandString.contains(commandPart));
}
}
private static TrackAttributes t(int id) {
return new TrackAttributes(id, "", "", false, false, false, false, TrackType.AUDIO);
}
}

View File

@@ -0,0 +1,60 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CommandRunner;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.PathUtils.*;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.args;
import static org.junit.jupiter.api.Assertions.*;
class ValidationExecutionStrategyTest {
@Test
void validate() {
CommandRunner underTest = new CommandRunner();
new CommandLine(underTest)
.setExecutionStrategy(new ValidationExecutionStrategy())
.parseArgs("-a", "ger:ger", "-m", TEST_MKVTOOLNIX_DIR, TEST_FILE);
assertEquals(TEST_FILE, underTest.getConfig().getLibraryPath().getPath().replace("\\", "/"));
assertEquals(TEST_MKVTOOLNIX_DIR, underTest.getConfig().getMkvToolNix().getPath().replace("\\", "/"));
}
private static Stream<Arguments> validateFailure() {
return Stream.of(
Arguments.of(new String[]{"-a", "jpn:ger"}, "Error: Missing required argument(s): <libraryPath>"),
Arguments.of(new String[]{"/arstarstarst"}, "libraryPath does not exist"),
Arguments.of(new String[]{"/arstarstarst", "-a",}, "Missing required parameter for option '--attribute-config' at index 0 (<attributeConfig>)"),
Arguments.of(new String[]{"/arstarstarst", "-a", "jpn:ger"}, "libraryPath does not exist"),
Arguments.of(new String[]{"/arstarstarst", "-m"}, "Missing required parameter for option '--mkvtoolnix' (<mkvToolNix>)"),
Arguments.of(new String[]{"./", "-m", TEST_INVALID_DIR}, "mkvToolNix does not exist"),
Arguments.of(new String[]{"./", "-t"}, "Missing required parameter for option '--threads' (<threads>)"),
Arguments.of(new String[]{"./", "-t", "0"}, "threads must be greater than or equal to 1"),
Arguments.of(new String[]{"./", "-t", "-1"}, "threads must be greater than or equal to 1"),
Arguments.of(new String[]{"./", "-c", "-1"}, "coherent must be greater than or equal to 0")
);
}
@ParameterizedTest
@MethodSource("validateFailure")
void validateFailure(String[] args, String expectedMessage) {
StringWriter writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
new CommandLine(CommandRunner.class)
.setExecutionStrategy(new ValidationExecutionStrategy())
.setErr(printWriter)
.execute(args);
printWriter.flush();
assertEquals(expectedMessage, writer.toString().split("[\r\n]")[0]);
}
}

View File

@@ -1,59 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Set;
import java.util.stream.Stream;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.TestUtil.createFileInfo;
import static org.junit.jupiter.api.Assertions.*;
class FileInfoDtoTest {
private static final FileAttribute AUDIO_GER_DEFAULT = new FileAttribute(0, "ger", "", true, false, LaneType.AUDIO);
private static final FileAttribute AUDIO_GER = new FileAttribute(0, "ger", "", false, false, LaneType.AUDIO);
private static final FileAttribute AUDIO_ENG_DEFAULT = new FileAttribute(1, "eng", "", true, false, LaneType.AUDIO);
private static final FileAttribute AUDIO_ENG = new FileAttribute(1, "eng", "", false, false, LaneType.AUDIO);
private static final FileAttribute SUB_GER_DEFAULT = new FileAttribute(0, "ger", "", true, false, LaneType.SUBTITLES);
private static final FileAttribute SUB_GER = new FileAttribute(0, "ger", "", false, false, LaneType.SUBTITLES);
private static final FileAttribute SUB_ENG_DEFAULT = new FileAttribute(1, "eng", "", true, false, LaneType.SUBTITLES);
private static final FileAttribute SUB_ENG = new FileAttribute(1, "eng", "", false, false, LaneType.SUBTITLES);
private static Stream<Arguments> isAudioDifferent() {
return Stream.of(
Arguments.of(createFileInfo(Set.of(AUDIO_GER_DEFAULT), AUDIO_GER_DEFAULT), false),
Arguments.of(createFileInfo(Set.of(AUDIO_GER_DEFAULT), AUDIO_ENG), true),
Arguments.of(createFileInfo(Set.of(AUDIO_GER_DEFAULT, AUDIO_ENG_DEFAULT), AUDIO_GER_DEFAULT), true),
Arguments.of(createFileInfo(Set.of(), AUDIO_GER), true),
Arguments.of(createFileInfo(null, AUDIO_GER), true)
);
}
@ParameterizedTest
@MethodSource
void isAudioDifferent(FileInfoDto underTest, boolean expected) {
assertEquals(expected, underTest.isAudioDifferent());
}
private static Stream<Arguments> isSubtitleDifferent() {
return Stream.of(
Arguments.of(createFileInfo(Set.of(SUB_GER_DEFAULT), SUB_GER_DEFAULT, new AttributeConfig("", "ger")), false),
Arguments.of(createFileInfo(Set.of(SUB_GER_DEFAULT), SUB_ENG, new AttributeConfig("", "eng")), true),
Arguments.of(createFileInfo(Set.of(SUB_GER_DEFAULT, SUB_ENG_DEFAULT), SUB_ENG, new AttributeConfig("", "eng")), true),
Arguments.of(createFileInfo(Set.of(), SUB_ENG, new AttributeConfig("", "ger")), true),
Arguments.of(createFileInfo(null, SUB_GER, new AttributeConfig("", "ger")), true),
Arguments.of(createFileInfo(null, null, new AttributeConfig("", "OFF")), false),
Arguments.of(createFileInfo(Set.of(), null, new AttributeConfig("", "OFF")), false),
Arguments.of(createFileInfo(Set.of(SUB_GER_DEFAULT), null, new AttributeConfig("", "OFF")), true)
);
}
@ParameterizedTest
@MethodSource
void isSubtitleDifferent(FileInfoDto underTest, boolean expected) {
assertEquals(expected, underTest.isSubtitleDifferent());
}
}

View File

@@ -0,0 +1,30 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackType;
public class FileInfoTestUtil {
public static final TrackAttributes AUDIO_GER = new TrackAttributes(0, "ger", "", false, false, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_ENG = new TrackAttributes(1, "eng", "", false, false, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_GER_DEFAULT = new TrackAttributes(0, "ger", "", true, false, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_ENG_DEFAULT = new TrackAttributes(1, "eng", "", true, false, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_GER_FORCED = new TrackAttributes(0, "ger", "", false, true, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_ENG_FORCED = new TrackAttributes(1, "eng", "", false, true, false, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_GER_COMMENTARY = new TrackAttributes(0, "ger", "", false, false, true, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_ENG_COMMENTARY = new TrackAttributes(1, "eng", "", false, false, true, false, TrackType.AUDIO);
public static final TrackAttributes AUDIO_GER_HEARING = new TrackAttributes(0, "ger", "", false, false, false, true, TrackType.AUDIO);
public static final TrackAttributes AUDIO_ENG_HEARING = new TrackAttributes(1, "eng", "", false, false, false, true, TrackType.AUDIO);
public static final TrackAttributes SUB_GER = new TrackAttributes(0, "ger", "", false, false, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_ENG = new TrackAttributes(1, "eng", "", false, false, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_GER_DEFAULT = new TrackAttributes(0, "ger", "", true, false, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_ENG_DEFAULT = new TrackAttributes(1, "eng", "", true, false, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_GER_FORCED = new TrackAttributes(0, "ger", "", false, true, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_ENG_FORCED = new TrackAttributes(1, "eng", "", false, true, false, false, TrackType.SUBTITLES);
public static final TrackAttributes SUB_GER_HEARING = new TrackAttributes(0, "ger", "", false, false, false, true, TrackType.SUBTITLES);
public static final TrackAttributes SUB_ENG_HEARING = new TrackAttributes(1, "eng", "", false, false, false, true, TrackType.SUBTITLES);
public static TrackAttributes withName(TrackAttributes track, String trackName) {
return new TrackAttributes(track.id(), track.language(), trackName, track.defaultt(), track.forced(), track.commentary(), track.hearingImpaired(), track.type());
}
}

View File

@@ -1,6 +1,10 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util; package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import org.apache.commons.lang3.SystemUtils;
public class PathUtils { public class PathUtils {
public static final String TEST_DIR = "src/test/resources/test-dir"; public static final String TEST_DIR = "src/test/resources/test-dir";
public static final String TEST_FILE = "src/test/resources/test-dir/test-file.mkv"; public static final String TEST_FILE = "src/test/resources/test-dir/test-file.mkv";
public static final String TEST_INVALID_DIR = "src/test/resources/test-dir";
public static final String TEST_MKVTOOLNIX_DIR = SystemUtils.IS_OS_WINDOWS ? "src/test/resources/mkvtoolnix_exe" : "src/test/resources/mkvtoolnix";
} }

View File

@@ -1,20 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
class SetUtilsTest {
@Test
void retainOf() {
List<Integer> list1 = List.of(1, 2, 3, 4, 5);
List<Integer> list2 = List.of(2, 4, 6, 8, 10);
Set<Integer> expected = Set.of(2, 4);
assertEquals(expected, SetUtils.retainOf(list1, list2));
}
}

View File

@@ -1,54 +1,12 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util; package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ValidationResult;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfoDto;
import org.junit.jupiter.params.provider.Arguments;
import java.util.Arrays;
import java.util.Set;
import static java.util.stream.Collectors.joining;
public class TestUtil { public class TestUtil {
public static String yamlList(ConfigProperty main, ConfigProperty... child) {
return main.prop() + ":\n" + Arrays.stream(child)
.map(ConfigProperty::prop)
.collect(joining("\n", " - ", ""));
}
public static <T> Arguments argumentsOf(ConfigProperty property, boolean required, T defaultValue, String yaml, String[] cmd,
ValidationResult result) {
return Arguments.of(property, required, defaultValue, yaml, cmd, result);
}
public static Arguments argumentsOf(ConfigProperty property, boolean required, boolean append, String yaml, String[] cmd,
ValidationResult result, int expectedSize) {
return Arguments.of(property, required, append, yaml, cmd, result, expectedSize);
}
public static FileInfoDto createFileInfo(Set<FileAttribute> defaultAudio, FileAttribute desiredAudio) {
FileInfoDto fileInfoDto = new FileInfoDto(null);
fileInfoDto.setExistingDefaultAudioLanes(defaultAudio);
fileInfoDto.setDesiredDefaultAudioLane(desiredAudio);
return fileInfoDto;
}
public static FileInfoDto createFileInfo(Set<FileAttribute> defaultSubtitle, FileAttribute desiredSubtitle, AttributeConfig config) {
FileInfoDto fileInfoDto = new FileInfoDto(null);
fileInfoDto.setExistingDefaultSubtitleLanes(defaultSubtitle);
fileInfoDto.setDesiredDefaultSubtitleLane(desiredSubtitle);
fileInfoDto.setMatchedConfig(config);
return fileInfoDto;
}
public static String[] args(String... args) { public static String[] args(String... args) {
String[] staticArray = new String[]{"-l", "/", "-a", "jpn:ger"}; String[] staticArray = new String[]{"-a", "jpn:ger", "/"};
String[] result = new String[staticArray.length + args.length]; String[] result = new String[staticArray.length + args.length];
System.arraycopy(staticArray, 0, result, 0, staticArray.length); System.arraycopy(args, 0, result, 0, args.length);
System.arraycopy(args, 0, result, staticArray.length, args.length); System.arraycopy(staticArray, 0, result, args.length, staticArray.length);
return result; return result;
} }
} }