161 Commits
v2.0 ... 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
b86c7b98a5 Fix logging for portable 2025-05-01 22:55:11 +02:00
Michael
69c192c08b Update README.md 2025-02-22 20:37:04 +01:00
Michael
7dd01234b6 Merge pull request #57 from RatzzFatzz/dev
Fix execution on linux
2025-02-18 19:23:08 +01:00
RatzzFatzz
8f38abcf3a Fix execution on linux 2025-02-18 19:15:02 +01:00
Michael
fc4e80ead0 Merge pull request #55 from RatzzFatzz/dev
Release v4.0
2025-02-06 22:27:06 +01:00
RatzzFatzz
e81b06f6fa Improve logging 2025-02-06 00:18:49 +01:00
RatzzFatzz
dc770c9325 Fix file count if no info was found 2025-02-06 00:00:28 +01:00
RatzzFatzz
471255a09b Fix windows installer build 2025-02-05 23:32:01 +01:00
RatzzFatzz
9c8315aec7 Remove config-template 2025-02-05 23:13:54 +01:00
RatzzFatzz
c33777b038 Add debian build to github action 2025-02-05 23:02:40 +01:00
RatzzFatzz
6c08ce69ea Fix logging for debian build 2025-02-05 22:59:38 +01:00
RatzzFatzz
7f8c14e3a9 Use maven properties for wix installer vars 2025-02-05 16:00:01 +01:00
RatzzFatzz
553c672e4d Implement debian package build 2025-02-05 15:49:46 +01:00
RatzzFatzz
d98c4cd49e Handle empty input 2025-02-04 17:57:34 +01:00
RatzzFatzz
21f244ff3f Update logging config 2025-02-04 17:48:45 +01:00
RatzzFatzz
ffac36ac27 Update example gif 2025-02-04 14:45:57 +01:00
RatzzFatzz
0813744148 Update README.md 2025-02-04 13:56:29 +01:00
RatzzFatzz
44d2601d3e Update config parameters and descriptions 2025-02-04 13:49:19 +01:00
RatzzFatzz
36bd93bb50 Fix windows artifact upload 2025-02-04 12:33:19 +01:00
RatzzFatzz
ecc5c56c8c Update github action 2025-02-04 00:54:09 +01:00
RatzzFatzz
f6310c71ee Update github action 2025-02-04 00:52:09 +01:00
RatzzFatzz
bb4a686dfc Update github action 2025-02-04 00:39:13 +01:00
RatzzFatzz
c63fcd4f37 Update maven plugins 2025-02-04 00:34:40 +01:00
RatzzFatzz
9f15b542bd Update github action 2025-02-04 00:31:07 +01:00
RatzzFatzz
76321bb904 Update dependencies 2025-02-04 00:03:56 +01:00
RatzzFatzz
895597b91f Fix github action 2025-02-03 23:32:37 +01:00
RatzzFatzz
4fa5448e1c Fix logging on installed app 2025-02-03 01:13:15 +01:00
RatzzFatzz
f3accd77d6 Replace GraalVM with jpackage and wix 2025-02-01 17:00:01 +01:00
RatzzFatzz
2710ea2602 Update graalvm and lombok config 2025-01-28 23:45:06 +01:00
RatzzFatzz
547b5ad86c Update threads test 2024-11-27 21:19:08 +01:00
RatzzFatzz
1863432dc6 Make mkvtoolnix path default value of dependant 2024-11-27 00:32:23 +01:00
RatzzFatzz
7ea0ab17b0 Update mkvtoolnix path test 2024-11-26 23:42:16 +01:00
RatzzFatzz
47b4cdc896 Update path & pattern tests 2024-11-24 23:57:24 +01:00
RatzzFatzz
b638d93358 Improve command description 2024-11-22 00:44:02 +01:00
RatzzFatzz
939f6053dd Update collection config parameter config 2024-11-22 00:21:13 +01:00
RatzzFatzz
4714ef8db1 Update config tests 2024-11-18 23:59:17 +01:00
RatzzFatzz
321115b9ca Rework app to use picocli 2024-11-17 23:42:06 +01:00
Michael
a075dfb27c Remove forced audio tracks & minor refactoring 2024-05-27 22:31:34 +02:00
RatzzFatzz
ed8e592963 Fix file change detection 2024-05-27 22:24:47 +02:00
RatzzFatzz
0a7996f049 Remove forced audio tracks & minor refactoring 2024-05-24 01:09:11 +02:00
Michael
dd60ca93da Merge pull request #50 from RatzzFatzz/bugfix/file-with-no-default-subtitle-track
Fix setting of default audio track on files with no default subtitle track
2024-02-19 21:11:10 +01:00
RatzzFatzz
ba770abb6a Fix setting of default audio track on files with no default subtitle track 2024-02-19 20:54:44 +01:00
Michael
91f1e8f7bf Merge pull request #48 from RatzzFatzz/dev
Dev
2023-04-30 13:35:03 +02:00
0fda98426e Update use of enum 2023-04-30 13:34:34 +02:00
c74cdde442 Add ConfigLoader test 2023-04-29 20:16:58 +02:00
a8551fdbd5 Add tests 2023-04-22 20:30:35 +02:00
Michael
b2e9762366 Merge pull request #47 from RatzzFatzz/dev
Fix OFF for subtitles
2023-04-19 21:51:40 +02:00
cafb12f22a Fix OFF for subtitles 2023-04-19 21:44:49 +02:00
Michael
f6d65c2d53 Merge pull request #45 from RatzzFatzz/dev
Implement cache & fix minor bugs
2023-04-16 14:44:32 +02:00
1963d1cc5c Fix total file count 2023-04-16 13:45:46 +02:00
686a9a0da1 Implement caching & fix minor bugs 2023-04-16 13:07:58 +02:00
e19f780ff0 Update readme 2023-04-13 21:57:59 +02:00
f928cb035e Update config template 2023-04-10 21:44:43 +02:00
Michael
fd9a421edc Merge pull request #43 from RatzzFatzz/dev
Add date filter & incremental updating
2023-04-09 21:08:37 +02:00
285533bb28 Clean-up 2023-04-09 21:07:49 +02:00
9330deb75f Fix last-execution file saving 2023-04-08 21:07:44 +02:00
094b772257 Implement last-execution creation 2023-04-05 23:19:56 +02:00
Michael
873f6fca6d Merge pull request #42 from RatzzFatzz/dependabot/maven/com.fasterxml.jackson.core-jackson-databind-2.13.4.2
Bump jackson-databind from 2.13.4.1 to 2.13.4.2
2023-04-05 09:31:14 +02:00
4309109583 Expand date filter by reading from last-execution.yml 2023-04-04 23:14:37 +02:00
e3baae55d9 Expand date filter by last execution overwrite 2023-04-03 21:36:11 +02:00
7ee51421e0 Add basic date filter 2023-04-02 20:11:42 +02:00
dependabot[bot]
df6a82fd62 Bump jackson-databind from 2.13.4.1 to 2.13.4.2
Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.4.1 to 2.13.4.2.
- [Release notes](https://github.com/FasterXML/jackson/releases)
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-31 15:21:10 +00:00
Michael
c551e2e2a5 Merge pull request #41 from RatzzFatzz/dev
Improve subtitle recognition
2023-03-19 11:32:36 +01:00
943308dd59 Add preferred-subtitle parameter 2023-03-19 11:29:59 +01:00
ba4c1bc1fe Improve subtitle selection 2023-03-18 18:07:15 +01:00
Michael
62f75818d9 Merge pull request #40 from RatzzFatzz/dev
Coherent Feature
2023-03-12 12:46:02 +01:00
cf64833d3e Fix excluded file count 2023-03-11 19:21:55 +01:00
6372cc560c Finalize coherent feature 2023-03-07 22:55:03 +01:00
8317e97639 Add track detection for coherent feature 2023-03-05 17:48:07 +01:00
440251c7c9 Add dir loading for coherent feature 2023-03-01 19:13:07 +01:00
b07f6894aa Add CoherentConfigValidator 2023-02-28 20:04:51 +01:00
73be93a4b6 Prepare for coherent feature by restructuring AttributeUpdaterKernel 2023-02-26 18:55:17 +01:00
143206b08c Expand forced keywords 2023-02-23 22:48:14 +01:00
Michael
80348756f9 Merge pull request #37 from RatzzFatzz/dev
Update README.md
2023-02-21 16:23:47 +01:00
773018e3bc Update README.md 2023-02-21 16:23:36 +01:00
Michael
923b4d06c5 Merge pull request #36 from RatzzFatzz/dev
Update README.md
2023-02-21 14:58:23 +01:00
d7cd74bfaf Update README.md 2023-02-21 14:56:16 +01:00
Michael
9f40d97d8a Merge pull request #34 from RatzzFatzz/dev
Add attribute config via cli
2023-02-20 23:50:05 +01:00
156e327943 Add attribute config via cli 2023-02-20 23:48:49 +01:00
Michael
5f72f4545f Merge pull request #28 from RatzzFatzz/dev-config-validators
Update parameter validation
2023-02-19 16:55:11 +01:00
Michael
1e341a0112 Merge branch 'master' into dev-config-validators 2023-02-19 16:53:24 +01:00
fe30d186df Fix CR comments & remove OS Validator 2023-02-19 16:52:27 +01:00
ce9a2fc805 Add linux compatibility & Finalize Config Loader 2023-02-19 15:51:50 +01:00
f69fbedee0 Add ConfigProperty Validation 2023-02-18 23:53:26 +01:00
d0c4b07f52 Add AttributeConfigValidatorTest 2023-02-16 22:43:38 +01:00
313abd311a Add ConfigPathValidatorTest 2023-02-15 23:06:12 +01:00
47f6d65eb2 Add PathValidatorTest 2023-02-14 20:05:37 +01:00
33276b7aa2 Fix PatternValidator & Add PatternValidatorTest 2023-02-13 23:12:08 +01:00
51b4885e65 Add config validator tests & Improve mkvtoolnix error logging & ConfigPathValidator 2023-02-12 21:34:38 +01:00
Michael
847a3f1f68 Merge pull request #29 from RatzzFatzz/dependabot/maven/com.fasterxml.jackson.core-jackson-databind-2.13.4.1
Bump jackson-databind from 2.13.2.1 to 2.13.4.1
2023-02-12 12:11:22 +01:00
dependabot[bot]
937c644b32 Bump jackson-databind from 2.13.2.1 to 2.13.4.1
Bumps [jackson-databind](https://github.com/FasterXML/jackson) from 2.13.2.1 to 2.13.4.1.
- [Release notes](https://github.com/FasterXML/jackson/releases)
- [Commits](https://github.com/FasterXML/jackson/commits)

---
updated-dependencies:
- dependency-name: com.fasterxml.jackson.core:jackson-databind
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-19 05:39:05 +00:00
1d6098efc1 Add config validators & tests 2022-09-13 18:58:16 +02:00
b5030f9401 Add config validators 2022-09-03 21:14:07 +02:00
892bc59803 Fix build workflow 2022-06-16 13:22:42 +02:00
1bd136af6a Fix build workflow 2022-06-16 13:18:56 +02:00
d8f0dcdc87 Fix build workflow 2022-06-16 13:14:29 +02:00
10cfb07457 Fix build workflow 2022-06-16 13:11:15 +02:00
Michael
17158e3f15 Merge pull request #27 from RatzzFatzz/dev
Dev
2022-06-16 13:01:07 +02:00
5e27a72499 Fix build workflow 2022-06-16 12:56:40 +02:00
658849417a Add build workflow 2022-06-16 12:55:23 +02:00
8d63c02abd Add sponsor button & increase version number 2022-06-16 12:32:26 +02:00
bad2a39614 Ignore commentary tracks 2022-06-12 14:33:00 +02:00
Michael
2d861b0f1c Merge pull request #25 from RatzzFatzz/master
Update dev
2022-06-12 14:32:09 +02:00
80 changed files with 3242 additions and 947 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [RatzzFatzz]
custom: "https://paypal.me/ratzmichael"

144
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,144 @@
# This workflow will run every time a tag starting with v is created.
name: Build and release
on:
release:
types: [ created ]
jobs:
portable-build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Install mkvtoolnix
run: sudo apt-get install -y mkvtoolnix
- name: Checkout
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
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "Europe/Berlin"
- name: Set up JDK 17
uses: actions/setup-java@v4.7.0
with:
distribution: temurin
java-version: 17
- name: Setup workspace
run: mkdir artifacts
- name: Build with Maven
run: |
mvn clean package --file pom.xml -P portable -Drevision="${{ steps.get_version.outputs.VERSION }}"
cp target/M*.{zip,tar} artifacts/
- name: Upload artifacts
uses: skx/github-action-publish-binaries@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: 'artifacts/M*'
debian-build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Install mkvtoolnix
run: sudo apt-get install -y mkvtoolnix
- name: Checkout
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
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "Europe/Berlin"
- name: Set up JDK 17
uses: actions/setup-java@v4.7.0
with:
distribution: temurin
java-version: 17
- name: Setup workspace
run: mkdir artifacts
- name: Build with Maven
run: |
mvn clean package --file pom.xml -P debian -Drevision="${{ steps.get_version.outputs.VERSION }}"
cp target/M*.deb artifacts/
- name: Upload artifacts
uses: skx/github-action-publish-binaries@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: 'artifacts/M*'
windows-installer-build:
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Install mkvtoolnix
uses: crazy-max/ghaction-chocolatey@v3
with:
args: install mkvtoolnix -y
- name: Checkout
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
uses: szenius/set-timezone@v2.0
with:
timezoneWindows: "Berlin Standard Time"
- name: Set up JDK 21
uses: actions/setup-java@v4.7.0
with:
distribution: temurin
java-version: 21
- name: Setup workspace
run: mkdir artifacts
- name: Build with Maven
run: mvn clean package --file pom.xml -P windows -Drevision="${{ steps.get_version.outputs.VERSION }}"
- name: Upload artifacts
uses: AButler/upload-release-assets@v3.0
with:
files: 'target/installer/*'
repo-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

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

119
README.md
View File

@@ -1,59 +1,78 @@
### Table of content
- Introduction
- Requirements
- Running
- Configuration
- Additional parameters
## Introduction
### Introduction
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)
This program helps to change audio and subtitle lines of mkv files.
## Requirements
### Requirements
- Java 11 or higher
- Java 21 or newer
- mkvtoolnix installation
### Running
1. Extract downloaded archive
2. Copy `config-template.yaml` to `config.yaml`
3. Update `config.yaml` to fit your needs
4. Open terminal / cmd in the directory of the jar and the config file
5. Execute following commands:
1. (Optional) `java -jar mkvaudiosubtitleschanger.jar -l [path to mkv or dir with mkv] --safe-mode`
2. To permanently apply changes: `java -jar mkvaudiosubtitleschanger.jar -l [path to mkv or dir with mkv]`
### Configuration
Config file needs to be placed in the same directory as the jar or path to config has to be passed via command line
argument.
The list of language configurations can be expanded. Use `OFF` if you want to turn of the audio or subtitle lane.
Players probably will display forced subtitles nonetheless.
```yaml
config:
1:
audio: ger
subtitle: OFF
2:
audio: eng
subtitle: ger
## Execution
```shell
# Portable
java -jar mkvaudiosubtitlechanger.jar --attribute-config eng:ger -s ./videos
# Installed
mkvaudiosubtitlechanger.jar --attribute-config eng:ger -s ./videos
```
Subtitle lanes recognized as forced will be set as one. Already existing ones will not be overwritten or changed.
Remove `--safemode` or `-s` to actually apply the changes. Using safemode for the first execution is recommended.
### Update defaults
To update the default flag for tracks use `--attribute-config` or `-a`.
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.
### Additional arameters
These properties overwrite already existing values in the config file.
```properties
-c,--config <arg> Path to config file
-e,--exclude-directories <arg> Directories to be excluded, combines with config file
-h,--help "for help this is" - Yoda
-i,--include-pattern <arg> Include files matching pattern
-k,--forcedKeywords <arg> Additional keywords to identify forced tracks, combines with config file
-l,--library <arg> Path to library
-m,--mkvtoolnix <arg> Path to mkv tool nix installation
-s,--safe-mode Test run (no files will be changes)
-t,--threads <arg> thread count (default: 2)
-v,--version Display version
Using this parameter is not required, but it is the reason I originally started developing this tool.
### Available parameters
```
-a, --attribute-config=<attributeConfig>...
List of audio:subtitle pairs for matching defaults in order (e.g. jpn:eng jpn:ger)
-m, --mkvtoolnix=<mkvToolNix>
path to mkvtoolnix installation
-s, --safemode test run (no files will be changes)
-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.
```
If you need more information how each parameter works, check out [this wiki page](https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/wiki/Parameters-v4).
All parameters can also be defined in a [config file](https://picocli.info/#_argument_files_for_long_command_lines).
## Build requirements
- JDK 21 or newer
- Maven 3
- Git
## Build from source
```shell
git clone https://github.com/RatzzFatzz/MKVAudioSubtitleChanger.git
cd MKVAudioSubtitleChanger
mvn clean package -Pportable
```

View File

@@ -1,18 +0,0 @@
mkvtoolnix: C:\Program Files\MKVToolNix
config:
1:
audio: ger
subtitle: OFF
2:
audio: eng
subtitle: ger
# Recommendations for data stored on HDDs, increase when using SSDs
#threads: 2
#forcedKeywords: ["forced", "signs"]
#exclude-directories:
# - "D:/Path/To/File.mkv"
# - "D:/Path/To/Directory"
#include-pattern: "regex"

BIN
example.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -14,10 +14,6 @@
<destName>${project.artifactId}.jar</destName>
<outputDirectory>/</outputDirectory>
</file>
<file>
<source>${project.basedir}/config-template.yaml</source>
<outputDirectory>/</outputDirectory>
</file>
<file>
<source></source>
</file>

410
pom.xml
View File

@@ -4,23 +4,243 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>MKVAudioSubtileChanger</groupId>
<groupId>at.pcgamingfreaks</groupId>
<artifactId>MKVAudioSubtitleChanger</artifactId>
<version>2.0</version>
<version>${revision}</version>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.maintainer>RatzzFatzz</project.maintainer>
<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>
<java-version>17</java-version>
<lombok-version>1.18.42</lombok-version>
</properties>
<profiles>
<profile>
<id>portable</id>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>log4j2.yaml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>archive</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>maven/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>windows</id>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>log4j2-windows.yaml</include>
</includes>
</resource>
</resources>
<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>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>copy-jpackage-input</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/jpackage-input</outputDirectory>
<resources>
<resource>
<directory>${project.build.directory}</directory>
<includes>
<include>${project.artifactId}.jar</include>
</includes>
</resource>
</resources>
</configuration>
</execution>
<execution>
<id>filter-windows-installer-info</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/wix-resources</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/wix/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.7.1</version>
<configuration>
<name>${project.artifactId}</name>
<vendor>RatzzFatzz</vendor>
<appVersion>${project.version}</appVersion>
<destination>target/installer</destination>
<input>target/jpackage-input</input>
<mainClass>at.pcgamingfreaks.mkvaudiosubtitlechanger.Main</mainClass>
<mainJar>${project.artifactId}.jar</mainJar>
<resourceDir>${project.build.directory}/wix-resources/</resourceDir>
<type>EXE</type>
<winConsole>true</winConsole>
<winShortcut>false</winShortcut>
<winMenu>false</winMenu>
<javaOptions>
<javaOption>-Dlog4j2.configurationFile=log4j2-windows.yaml</javaOption>
</javaOptions>
</configuration>
<executions>
<execution>
<id>windows</id>
<phase>package</phase>
<goals>
<goal>jpackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>debian</id>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>log4j2-debian.yaml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>filter-linux-package-info</id>
<phase>package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/debian-package-info</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/deb</directory>
<filtering>true</filtering>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>jdeb</artifactId>
<groupId>org.vafer</groupId>
<version>1.13</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jdeb</goal>
</goals>
<configuration>
<dataSet>
<!-- JAR file -->
<data>
<src>${project.build.directory}/${project.build.finalName}.jar</src>
<type>file</type>
<mapper>
<type>perm</type>
<prefix>/usr/lib/${project.artifactId}</prefix>
</mapper>
</data>
<!-- Launcher script -->
<data>
<src>${project.build.directory}/debian-package-info/bin/mkvaudiosubtitlechanger</src>
<type>file</type>
<mapper>
<type>perm</type>
<prefix>/usr/bin</prefix>
<filemode>755</filemode>
</mapper>
</data>
</dataSet>
<controlDir>${project.build.directory}/debian-package-info/control</controlDir>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<defaultGoal>clean package</defaultGoal>
<sourceDirectory>src/main/java</sourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>./</directory>
<includes>
<include>language-codes</include>
<include>version.properties</include>
<include>project.properties</include>
<include>LICENSE</include>
</includes>
<filtering>true</filtering>
</resource>
@@ -35,11 +255,11 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.1</version>
<version>3.4.2</version>
<configuration>
<archive>
<manifestEntries>
<Main-Class>at/pcgamingfreaks/mkvaudiosubtitlechanger/Main</Main-Class>
<Main-Class>${mainClass}</Main-Class>
</manifestEntries>
</archive>
</configuration>
@@ -47,7 +267,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
@@ -62,6 +282,13 @@
<include>*:*</include>
</includes>
</artifactSet>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Multi-Release>true</Multi-Release>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
@@ -69,35 +296,33 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<version>3.5.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>11</source>
<target>11</target>
<source>${java-version}</source>
<target>${java-version}</target>
<compilerArgs>
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok-version}</version>
</path>
<path>
<groupId>info.picocli</groupId>
<artifactId>picocli-codegen</artifactId>
<version>4.7.7</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>maven/assembly.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -109,101 +334,148 @@
</repositories>
<dependencies>
<dependency>
<groupId>com.intellij</groupId>
<artifactId>forms_rt</artifactId>
<version>7.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<version>${lombok-version}</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/info.picocli/picocli -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.7</version>
</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 -->
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>9.1.0.Final</version>
</dependency>
<!-- Expression Language Implementation -->
<!-- https://mvnrepository.com/artifact/jakarta.el/jakarta.el-api -->
<dependency>
<groupId>jakarta.el</groupId>
<artifactId>jakarta.el-api</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>5.0.0-M1</version>
</dependency>
<!-- region logging -->
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.1</version>
<version>2.25.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.1</version>
<version>2.25.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-slf4j2-impl -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.1</version>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/tools.jackson.dataformat/jackson-dataformat-yaml -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.28</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.28</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.13.1</version>
<version>3.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/tools.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.2.1</version>
<version>3.0.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-cli/commons-cli -->
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.5.0</version>
<version>1.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<version>3.20.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/me.tongfei/progressbar -->
<dependency>
<groupId>me.tongfei</groupId>
<artifactId>progressbar</artifactId>
<version>0.9.3</version>
<version>0.10.1</version>
</dependency>
<!-- endregion -->
<!-- region unit-tests -->
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-all -->
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.9.5</version>
<artifactId>mockito-core</artifactId>
<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>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>6.0.1</version>
<scope>test</scope>
</dependency>
<!-- endregion -->
<dependency>
<groupId>at.pcgamingfreaks</groupId>
<artifactId>YAML-Parser</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.harawata/appdirs -->
<dependency>
<groupId>net.harawata</groupId>
<artifactId>appdirs</artifactId>
<version>1.5.0</version>
</dependency>
</dependencies>
</project>

2
project.properties Normal file
View File

@@ -0,0 +1,2 @@
version=${project.version}
project_name=${project.artifactId}

View File

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

8
src/deb/control/control Normal file
View File

@@ -0,0 +1,8 @@
Package: [[artifactId]]
Version: [[version]]
Section: misc
Priority: optional
Architecture: all
Depends: java-runtime (>=${java-version}), mkvtoolnix
Maintainer: ${project.maintainer} <${project.maintainer.mail}>
Description: ${project.description}

View File

@@ -1,92 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.FileProcessor;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileAttribute;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.FileInfoDto;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
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.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Log4j2
public class AttributeUpdaterKernel {
private final ExecutorService executor = Executors.newFixedThreadPool(Config.getInstance().getThreadCount());
private final FileCollector collector;
private final FileProcessor processor;
private final ResultStatistic statistic = new ResultStatistic();
public AttributeUpdaterKernel(FileCollector collector, FileProcessor processor) {
this.collector = collector;
this.processor = processor;
}
@SneakyThrows
public void execute() {
statistic.startTimer();
try (ProgressBar progressBar = pbBuilder().build()) {
List<File> excludedFiles = Config.getInstance().getExcludedDirectories().stream()
.map(collector::loadFiles)
.flatMap(Collection::stream)
.collect(Collectors.toList());
List<File> files = collector.loadFiles(Config.getInstance().getLibraryPath()).stream()
.filter(file -> !excludedFiles.contains(file))
.collect(Collectors.toList());
progressBar.maxHint(files.size());
files.forEach(file -> executor.submit(() -> process(file, progressBar)));
executor.shutdown();
executor.awaitTermination(1, TimeUnit.DAYS);
}
statistic.stopTimer();
System.out.println(statistic);
log.info(statistic);
}
private void process(File file, ProgressBar progressBar) {
List<FileAttribute> attributes = processor.loadAttributes(file);
FileInfoDto fileInfo = processor.filterAttributes(attributes);
statistic.total();
if (fileInfo.isChangeNecessary()) {
statistic.shouldChange();
if (!Config.getInstance().isSafeMode()) {
try {
processor.update(file, fileInfo);
statistic.success();
log.info("Updated {}", file.getAbsolutePath());
} catch (IOException e) {
statistic.failedChanging();
log.warn("File couldn't be updated: {}", file.getAbsoluteFile());
}
}
} else if (fileInfo.isUnableToApplyConfig()) {
statistic.noSuitableConfigFound();
} else if (fileInfo.isAlreadySuitable()){
statistic.alreadyFits();
} else {
statistic.failure();
}
progressBar.step();
}
private static ProgressBarBuilder pbBuilder() {
return new ProgressBarBuilder()
.setStyle(ProgressBarStyle.ASCII)
.setUpdateIntervalMillis(250)
.setMaxRenderedLength(75);
}
}

View File

@@ -1,15 +1,18 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.MkvFileCollector;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.MkvFileProcessor;
import lombok.extern.log4j.Log4j2;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.CommandRunner;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.impl.validation.ValidationExecutionStrategy;
import picocli.CommandLine;
@Log4j2
public class Main {
public static void main(String[] args) {
Config.getInstance().initConfig(args);
AttributeUpdaterKernel kernel = new AttributeUpdaterKernel(new MkvFileCollector(), new MkvFileProcessor());
kernel.execute();
if (args.length == 0) {
CommandLine.usage(CommandRunner.class, System.out);
return;
}
new CommandLine(CommandRunner.class)
.setExecutionStrategy(new ValidationExecutionStrategy())
.execute(args);
}
}

View File

@@ -1,223 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.AttributeConfig;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.VersionUtil;
import at.pcgamingfreaks.yaml.YAML;
import at.pcgamingfreaks.yaml.YamlInvalidContentException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.cli.*;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ConfigProperty.*;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.CommandLineOptionsUtil.optionOf;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.util.LanguageValidatorUtil.isLanguageValid;
@Log4j2
@Getter
public class Config {
@Getter(AccessLevel.NONE)
CommandLineParser parser = new DefaultParser();
@Getter(AccessLevel.NONE)
HelpFormatter formatter = new HelpFormatter();
@Getter(AccessLevel.NONE)
private static Config config = null;
private File configPath;
private String libraryPath;
private boolean isSafeMode;
private int threadCount;
private Pattern includePattern;
@Getter(AccessLevel.NONE)
private String mkvToolNixPath;
private boolean isWindows;
private final Set<String> forcedKeywords = new HashSet<>(Arrays.asList("forced", "signs"));
private final Set<String> excludedDirectories = new HashSet<>();
private List<AttributeConfig> attributeConfig;
public static Config getInstance() {
if (config == null) {
config = new Config();
}
return config;
}
public void initConfig(String[] args) throws InvalidConfigException {
ConfigErrors errors = new ConfigErrors();
CommandLine cmd = null;
Options options = initOptions();
try {
cmd = parser.parse(options, args);
if (cmd == null) throw new NullPointerException();
} catch (ParseException | NullPointerException e) {
formatter.printHelp(106, "java -jar MKVAudioSubtitlesChanger.jar -l <path_to_library>",
"\nParameters:", options,
"\nFeature requests and bug reports: https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/issues");
System.exit(1);
}
exitIfHelp(cmd, options);
exitIfVersion(cmd);
configPath = loadConfigPath(cmd, errors);
libraryPath = loadLibraryPath(cmd, errors);
isSafeMode = cmd.hasOption(SAFE_MODE.prop());
try (YAML config = new YAML(configPath)) {
threadCount = loadThreadCount(cmd, config);
includePattern = loadIncludePattern(cmd, config, errors);
mkvToolNixPath = loadMkvToolNixPath(cmd, config, errors);
isWindows = loadOperatingSystem();
loadForcedKeywords(cmd, config);
loadExcludedDirectories(cmd, config);
attributeConfig = loadAttributeConfig(config, errors);
} catch (IOException | YamlInvalidContentException ignored) {}
if (errors.hasErrors()) {
throw new InvalidConfigException(errors);
}
}
private static Options initOptions() {
Options options = new Options();
options.addOption(optionOf(HELP, "h", false));
options.addOption(optionOf(VERSION, "v", false));
options.addOption(optionOf(LIBRARY, "l", true));
options.addOption(optionOf(MKV_TOOL_NIX, "m", true));
options.addOption(optionOf(CONFIG_PATH, "c", true));
options.addOption(optionOf(THREADS, "t", true));
options.addOption(optionOf(SAFE_MODE, "s", false));
options.addOption(optionOf(FORCED_KEYWORDS, "k", Option.UNLIMITED_VALUES, false));
options.addOption(optionOf(EXCLUDE_DIRECTORY, "e", Option.UNLIMITED_VALUES, false));
options.addOption(optionOf(INCLUDE_PATTERN, "i", true));
return options;
}
private void exitIfHelp(CommandLine cmd, Options options) {
if (cmd.hasOption("help")) {
formatter.printHelp(106, "java -jar MKVAudioSubtitlesChanger.jar -l <path_to_library>",
"\nParameters:", options,
"\nFeature requests and bug reports: https://github.com/RatzzFatzz/MKVAudioSubtitleChanger/issues");
System.exit(0);
}
}
private void exitIfVersion(CommandLine cmd) {
if (cmd.hasOption(VERSION.prop())) {
System.out.printf("MKV Audio Subtitle Changer Version %s%n", VersionUtil.getVersion());
System.exit(0);
}
}
private File loadConfigPath(CommandLine cmd, ConfigErrors errors) {
File configPath = new File(cmd.getOptionValue(CONFIG_PATH.prop(), "config.yaml"));
if (configPath.isFile()) return configPath;
errors.add("invalid config path");
return null;
}
private String loadLibraryPath(CommandLine cmd, ConfigErrors errors) {
if (cmd.hasOption(LIBRARY.prop())) {
File libraryPath = new File(cmd.getOptionValue(LIBRARY.prop()));
if (libraryPath.isFile() || libraryPath.isDirectory()) {
return libraryPath.getAbsolutePath();
} else {
errors.add("invalid library path");
}
} else {
errors.add("missing library path");
}
return null;
}
private int loadThreadCount(CommandLine cmd, YAML config) {
return cmd.hasOption(THREADS.prop())
? Integer.parseInt(cmd.getOptionValue(THREADS.prop()))
: config.getInt(THREADS.prop(), 2);
}
private Pattern loadIncludePattern(CommandLine cmd, YAML config, ConfigErrors errors) {
try {
return Pattern.compile(cmd.hasOption(INCLUDE_PATTERN.prop())
? cmd.getOptionValue(INCLUDE_PATTERN.prop())
: config.getString(INCLUDE_PATTERN.prop(), ".*"));
} catch (PatternSyntaxException e) {
errors.add("invalid regex pattern");
}
return null;
}
@SneakyThrows
private String loadMkvToolNixPath(CommandLine cmd, YAML config, ConfigErrors errors){
if (cmd.hasOption(MKV_TOOL_NIX.prop())) return cmd.getOptionValue(MKV_TOOL_NIX.prop());
if (config.isSet(MKV_TOOL_NIX.prop())) return config.getString(MKV_TOOL_NIX.prop());
errors.add("path to mkv tool nix installation missing");
return null;
}
private boolean loadOperatingSystem() {
return System.getProperty("os.name").toLowerCase().contains("windows");
}
@SneakyThrows
private void loadForcedKeywords(CommandLine cmd, YAML config) {
if (cmd.hasOption(FORCED_KEYWORDS.prop())) forcedKeywords.addAll(List.of(cmd.getOptionValues(FORCED_KEYWORDS.prop())));
if (config.isSet(FORCED_KEYWORDS.prop())) forcedKeywords.addAll(config.getStringList(FORCED_KEYWORDS.prop()));
}
@SneakyThrows
private void loadExcludedDirectories(CommandLine cmd, YAML config) {
if (cmd.hasOption(EXCLUDE_DIRECTORY.prop())) excludedDirectories.addAll(List.of(cmd.getOptionValues(EXCLUDE_DIRECTORY.prop())));
if (config.isSet(EXCLUDE_DIRECTORY.prop())) excludedDirectories.addAll(config.getStringList(EXCLUDE_DIRECTORY.prop()));
}
private List<AttributeConfig> loadAttributeConfig(YAML config, ConfigErrors errors) {
Function<String, String> audio = key -> config.getString(key + ".audio", null);
Function<String, String> subtitle = key -> config.getString(key + ".subtitle", null);
List<AttributeConfig> attributeConfigs = config.getKeysFiltered(".*audio.*").stream()
.sorted()
.map(key -> key.replace(".audio", ""))
.map(key -> new AttributeConfig(audio.apply(key), subtitle.apply(key)))
.collect(Collectors.toList());
if (attributeConfigs.isEmpty()) {
errors.add("no language configuration");
} else {
for (AttributeConfig attributeConfig : attributeConfigs) {
isLanguageValid(attributeConfig.getAudioLanguage(), errors);
isLanguageValid(attributeConfig.getSubtitleLanguage(), errors);
}
}
return attributeConfigs;
}
public String getPathFor(MkvToolNix exe) {
return mkvToolNixPath.endsWith("/") ? mkvToolNixPath + exe : mkvToolNixPath + "/" + exe;
}
}

View File

@@ -1,22 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
public class ConfigErrors {
private final List<String> errors = new ArrayList<>();
public void add(String errorMessage) {
errors.add(errorMessage);
}
public boolean hasErrors() {
return !errors.isEmpty();
}
public String toString() {
return StringUtils.capitalize(String.join(", ", errors));
}
}

View File

@@ -1,7 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.config;
public class InvalidConfigException extends RuntimeException{
public InvalidConfigException(ConfigErrors errors) {
super("Errors in config: " + errors);
}
}

View File

@@ -0,0 +1,13 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.exceptions;
public class MkvToolNixException extends RuntimeException{
public MkvToolNixException(String message) {
super(message);
}
@Override
public String getMessage() {
return super.getMessage().replaceAll("\\r|\\n", " ");
}
}

View File

@@ -0,0 +1,19 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
public class Cache<Key, Value> {
private final Map<Key, Value> cache = new HashMap<>();
/**
* Retrieve {@link Value} from Cache or run creationFunction and return its value.
* @param key key of cache map
* @param creationFunction function to create missing values
* @return {@link Value} from Cache, or if missing result from creationFunction.
*/
public synchronized Value retrieve(Key key, Function<Key, Value> creationFunction) {
return cache.computeIfAbsent(key, creationFunction::apply);
}
}

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,13 +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);
}

View File

@@ -1,13 +1,89 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import org.apache.commons.lang3.StringUtils;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.ResultStatistic;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.util.DateUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@RequiredArgsConstructor
public class FileFilter {
static boolean accept(File pathName, String[] fileExtensions) {
return StringUtils.endsWithAny(pathName.getAbsolutePath().toLowerCase(), fileExtensions)
&& Config.getInstance().getIncludePattern().matcher(pathName.getName()).matches();
private final Set<String> excluded;
private final Pattern includePattern;
private final Date filterDate;
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;
}
if (!hasMatchingPattern(pathName)
|| !isNewer(pathName)
|| isExcluded(pathName, new HashSet<>(excluded))) {
log.debug("Excluded {}", pathName);
ResultStatistic.getInstance().excluded();
return false;
}
return true;
}
private boolean hasProperFileExtension(File pathName, Set<String> fileExtensions) {
Matcher matcher = extensionPattern.matcher(pathName.getName());
return matcher.find() && fileExtensions.contains(matcher.group(EXTENSION_GROUP));
}
private boolean hasMatchingPattern(File pathName) {
return includePattern.matcher(pathName.getName()).matches();
}
private boolean isNewer(File pathName) {
if (filterDate == null) return true;
try {
BasicFileAttributes attributes = Files.readAttributes(pathName.toPath(), BasicFileAttributes.class);
return isNewer(DateUtils.convert(attributes.creationTime().toMillis()));
} catch (IOException e) {
log.warn("File attributes could not be read", e);
}
return true;
}
private boolean isNewer(Date creationDate) {
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,21 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
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 {
/**
* @param file Takes the file from which the attributes will be returned
* @return list of all important attributes
*/
List<FileAttribute> loadAttributes(File file);
FileInfoDto filterAttributes(List<FileAttribute> attributes);
void update(File file, FileInfoDto fileInfo) throws IOException;
}

View File

@@ -1,31 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import lombok.extern.log4j.Log4j2;
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;
@Log4j2
public class MkvFileCollector implements FileCollector {
private static final String[] fileExtensions = new String[]{".mkv", ".mka", ".mks", ".mk3d"};
@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<>();
}
}
}

View File

@@ -1,152 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.Config;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.log4j.Log4j2;
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 static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.LaneType.AUDIO;
import static at.pcgamingfreaks.mkvaudiosubtitlechanger.model.LaneType.SUBTITLES;
import static java.lang.String.format;
@Log4j2
public class MkvFileProcessor implements FileProcessor {
private final ObjectMapper mapper = new ObjectMapper();
private final String[] forcedKeywords = new String[]{"forced", "signs"};
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 ENABLE_FORCED_TRACK = "--edit track:%s --set flag-forced=1 ";
@Override
public List<FileAttribute> loadAttributes(File file) {
Map<String, Object> jsonMap;
List<FileAttribute> fileAttributes = new ArrayList<>();
try {
String command = format("\"%s\"", Config.getInstance().getPathFor(MkvToolNix.MKV_MERGER));
String[] arguments = new String[]{
command,
"--identify",
"--identification-format",
"json",
file.getAbsoluteFile().toString()
};
InputStream inputStream = Runtime.getRuntime().exec(arguments).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);
} catch (IOException e) {
e.printStackTrace();
log.error("File could not be found or loaded!");
}
return fileAttributes;
}
@Override
public FileInfoDto filterAttributes(List<FileAttribute> attributes) {
FileInfoDto info = new FileInfoDto();
List<FileAttribute> nonForcedTracks = attributes.stream()
.filter(elem -> !StringUtils.containsAnyIgnoreCase(elem.getTrackName(), forcedKeywords))
.collect(Collectors.toList());
detectDefaultTracks(attributes, info, nonForcedTracks);
detectDesiredTracks(info, nonForcedTracks);
log.debug(info);
return info;
}
private void detectDefaultTracks(List<FileAttribute> attributes, FileInfoDto info, List<FileAttribute> nonForcedTracks) {
Set<FileAttribute> detectedForcedSubtitleLanes = new HashSet<>();
for (FileAttribute attribute : attributes) {
if (attribute.isDefaultTrack() && AUDIO.equals(attribute.getType()))
info.getDefaultAudioLanes().add(attribute);
if (attribute.isDefaultTrack() && SUBTITLES.equals(attribute.getType()))
info.getDefaultSubtitleLanes().add(attribute);
if (attribute.isForcedTrack() && SUBTITLES.equals(attribute.getType()))
detectedForcedSubtitleLanes.add(attribute);
}
info.setDesiredForcedSubtitleLanes(attributes.stream()
.filter(e -> !nonForcedTracks.contains(e))
.filter(e -> !detectedForcedSubtitleLanes.contains(e))
.collect(Collectors.toSet())
);
}
private void detectDesiredTracks(FileInfoDto info, List<FileAttribute> nonForcedTracks) {
for (AttributeConfig config : Config.getInstance().getAttributeConfig()) {
FileAttribute desiredAudio = null;
FileAttribute desiredSubtitle = null;
for (FileAttribute attribute : nonForcedTracks) {
if (attribute.getLanguage().equals(config.getAudioLanguage())
&& AUDIO.equals(attribute.getType())) desiredAudio = attribute;
if (attribute.getLanguage().equals(config.getSubtitleLanguage())
&& SUBTITLES.equals(attribute.getType())) desiredSubtitle = attribute;
}
if (desiredAudio != null && desiredSubtitle != null) {
info.setDesiredAudioLane(desiredAudio);
info.setDesiredSubtitleLane(desiredSubtitle);
break;
}
}
}
@Override
public void update(File file, FileInfoDto fileInfo) throws IOException {
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.getDefaultAudioLanes() != null && !fileInfo.getDefaultSubtitleLanes().isEmpty()) {
for (FileAttribute track: fileInfo.getDefaultAudioLanes()) {
sb.append(format(DISABLE_DEFAULT_TRACK, track.getId()));
}
}
sb.append(format(ENABLE_DEFAULT_TRACK, fileInfo.getDesiredAudioLane().getId()));
}
if (fileInfo.isSubtitleDifferent()) {
if (fileInfo.getDefaultSubtitleLanes() != null && !fileInfo.getDefaultSubtitleLanes().isEmpty()) {
for (FileAttribute track: fileInfo.getDefaultSubtitleLanes()) {
sb.append(format(DISABLE_DEFAULT_TRACK, track.getId()));
}
}
sb.append(format(ENABLE_DEFAULT_TRACK, fileInfo.getDesiredSubtitleLane().getId()));
}
if (fileInfo.areForcedTracksDifferent()) {
for (FileAttribute attribute : fileInfo.getDesiredForcedSubtitleLanes()) {
sb.append(format(ENABLE_FORCED_TRACK, attribute.getId()));
}
}
InputStream inputstream = Runtime.getRuntime().exec(sb.toString()).getInputStream();
log.debug(IOUtils.toString(new InputStreamReader(inputstream)));
}
}

View File

@@ -0,0 +1,51 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import org.apache.logging.log4j.util.Strings;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
public class SubtitleTrackComparator implements Comparator<TrackAttributes> {
private final Set<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}
*/
@Override
public int compare(TrackAttributes track1, TrackAttributes track2) {
int result = 0;
String track1Name = Strings.isNotBlank(track1.trackName()) ? track1.trackName().toLowerCase() : "";
String track2Name = Strings.isNotBlank(track2.trackName()) ? track2.trackName().toLowerCase() : "";
if (preferredSubtitles.contains(track1Name)) 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 (track1.defaultt()) result++;
if (track2.defaultt()) 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

@@ -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

@@ -2,15 +2,38 @@ package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;
@Log4j2
import java.util.Objects;
@Slf4j
@Getter
@AllArgsConstructor
public class AttributeConfig {
private final String audioLanguage;
private final String subtitleLanguage;
public static AttributeConfig of(String audioLanguage, String subtitleLanguage) {
return new AttributeConfig(audioLanguage, subtitleLanguage);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AttributeConfig that = (AttributeConfig) o;
return Objects.equals(audioLanguage, that.audioLanguage) && Objects.equals(subtitleLanguage, that.subtitleLanguage);
}
@Override
public int hashCode() {
return Objects.hash(audioLanguage, subtitleLanguage);
}
public String toStringShort() {
return audioLanguage + ":" + subtitleLanguage;
}
@Override
public String toString() {
return "AttributeConfig{"

View File

@@ -1,28 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum ConfigProperty {
CONFIG_PATH("config", "Path to config file"),
LIBRARY("library", "Path to library"),
SAFE_MODE("safe-mode", "Test run (no files will be changes)"),
THREADS("threads", "thread count (default: 2)"),
INCLUDE_PATTERN("include-pattern", "Include files matching pattern"),
MKV_TOOL_NIX("mkvtoolnix", "Path to mkv tool nix installation"),
FORCED_KEYWORDS("forcedKeywords", "Additional keywords to identify forced tracks, combines with config file"),
EXCLUDE_DIRECTORY("exclude-directories", "Directories to be excluded, combines with config file"),
HELP("help", "\"for help this is\" - Yoda"),
VERSION("version", "Display version");
private final String property;
private final String description;
public String desc() {
return description;
}
public String prop() {
return property;
}
}

View File

@@ -1,28 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.log4j.Log4j2;
@Log4j2
@Getter
@AllArgsConstructor
public class FileAttribute {
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
public String toString() {
return "[" + "id=" + id +
", language='" + language + '\'' +
", trackName='" + trackName + '\'' +
", defaultTrack=" + defaultTrack +
", forcedTrack=" + forcedTrack +
", type=" + type +
']';
}
}

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,53 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.Getter;
import lombok.Setter;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
public class FileInfoDto {
private Set<FileAttribute> defaultAudioLanes = new HashSet<>();
private Set<FileAttribute> defaultSubtitleLanes = new HashSet<>();
private Set<FileAttribute> desiredForcedSubtitleLanes;
private FileAttribute desiredAudioLane;
private FileAttribute desiredSubtitleLane;
public boolean isUnableToApplyConfig() {
return desiredAudioLane == null && desiredSubtitleLane == null;
}
public boolean isAlreadySuitable() {
return defaultAudioLanes.contains(desiredAudioLane) && defaultSubtitleLanes.contains(desiredSubtitleLane);
}
public boolean isChangeNecessary() {
return isAudioDifferent() || isSubtitleDifferent() || areForcedTracksDifferent();
}
public boolean isAudioDifferent() {
return desiredAudioLane != null &&
(defaultAudioLanes == null || !defaultAudioLanes.contains(desiredAudioLane));
}
public boolean isSubtitleDifferent() {
return desiredSubtitleLane != null &&
(defaultSubtitleLanes == null || !defaultSubtitleLanes.contains(desiredSubtitleLane));
}
public boolean areForcedTracksDifferent() {
return desiredForcedSubtitleLanes.size() > 0;
}
@Override
public String toString() {
return "[" + "defaultAudioLanes=" + defaultAudioLanes +
", defaultSubtitleLanes=" + defaultSubtitleLanes +
", desiredForcedSubtitleLanes=" + desiredForcedSubtitleLanes +
", desiredAudioLane=" + desiredAudioLane +
", desiredSubtitleLane=" + desiredSubtitleLane +
']';
}
}

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

@@ -4,8 +4,8 @@ import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum MkvToolNix {
MKV_MERGER("mkvmerge.exe"),
MKV_PROP_EDIT("mkvpropedit.exe");
MKV_MERGE("mkvmerge"),
MKV_PROP_EDIT("mkvpropedit");
private final String file;

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,59 +1,65 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Getter
@Slf4j
public class ResultStatistic {
private static final String result = "Total files: %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: %ss";
private static final String PRINT_TEMPLATE = "Total: %s, Changing: %s (Successful: %s, Failed %s), Unchanged: %s, Excluded: %s, Unknown/Failed: %s\nRuntime: %s";
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 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 runtime = 0;
public synchronized void total() {
filesTotal++;
public static ResultStatistic getInstance() {
return getInstance(false);
}
public synchronized void shouldChange() {
shouldChange++;
public static ResultStatistic getInstance(boolean reset) {
if (instance == null || reset) {
instance = new ResultStatistic();
}
return instance;
}
public synchronized void success() {
successfullyChanged++;
public int total() {
return changePlanned + unchanged + excluded + unknownFailed;
}
public synchronized void failedChanging() {
failedChanging++;
public synchronized void changePlanned() {
changePlanned++;
}
public synchronized void noSuitableConfigFound() {
noSuitableConfigFound++;
public synchronized void changeSuccessful() {
changeSuccessful++;
}
public synchronized void alreadyFits() {
alreadyFits++;
public synchronized void changeFailed() {
changeFailed++;
}
public synchronized void failure() {
failed++;
public synchronized void unchanged() {
unchanged++;
}
public synchronized void increaseUnchangedBy(int amount) {
unchanged += amount;
}
public synchronized void excluded() {
excluded++;
}
public synchronized void unknownFailed() {
unknownFailed++;
}
public void startTimer() {
@@ -64,9 +70,31 @@ public class ResultStatistic {
runtime = System.currentTimeMillis() - startTime;
}
private String formatTimer() {
int seconds = (int) (runtime / 1000);
int minutes = seconds / 60;
int hours = minutes / 60;
int days = hours / 24;
if (days >= 1) {
return String.format("%sd %sh %sm %ss", days, hours % 24, minutes % 60, seconds % 60);
} else if (hours >= 1) {
return String.format("%sh %sm %ss", hours, minutes % 60, seconds % 60);
} else if (minutes >= 1) {
return String.format("%sm %ss", minutes, seconds % 60);
} else {
return String.format("%ss", seconds % 60);
}
}
public void print() {
String result = this.toString();
System.out.println(result);
log.info(result);
}
@Override
public String toString() {
return String.format(result, filesTotal, shouldChange, failedChanging, successfullyChanged,
noSuitableConfigFound, alreadyFits, failed, runtime / 1000);
return String.format(PRINT_TEMPLATE, total(), changePlanned, changeSuccessful, changeFailed, unchanged, excluded, unknownFailed, formatTimer());
}
}

View File

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

View File

@@ -1,6 +1,6 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.model;
public enum LaneType {
public enum TrackType {
AUDIO,
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, boolean hasArg) {
return optionOf(property, opt, hasArg ? 1 : 0, 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,29 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy-HH:mm:ss");
public static Date convert(long millis) {
return new Date(millis);
}
/**
* Convert String to date.
* @return parsed date, defaultDate if exception occurs
*/
public static Date convert(String date, Date defaultDate) {
try {
return dateFormat.parse(date);
} catch (ParseException e) {
return defaultDate;
}
}
public static String convert(Date date) {
return dateFormat.format(date);
}
}

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

@@ -1,7 +1,5 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.config.ConfigErrors;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@@ -28,10 +26,4 @@ public class LanguageValidatorUtil {
public static boolean isLanguageValid(String language) {
return ISO3_LANGUAGES.contains(language);
}
public static void isLanguageValid(String language, ConfigErrors errors) {
if (!isLanguageValid(language)) {
errors.add(String.format("%s is not a valid language", language));
}
}
}

View File

@@ -0,0 +1,52 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.MkvToolNix;
import org.apache.logging.log4j.util.Strings;
import picocli.CommandLine;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
public class ProjectUtil implements CommandLine.IVersionProvider {
private static final Properties PROJECT_PROPERTIES = new Properties();
static {
try (InputStream propertiesStream = ProjectUtil.class.getClassLoader().getResourceAsStream("project.properties")) {
PROJECT_PROPERTIES.load(propertiesStream);
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
public static String getProjectName() {
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

@@ -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

@@ -1,18 +0,0 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class VersionUtil {
public static String getVersion() {
try (InputStream propertiesStream = VersionUtil.class.getClassLoader().getResourceAsStream("version.properties")) {
Properties properties = new Properties();
properties.load(propertiesStream);
return properties.getProperty("version");
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
}

View File

@@ -0,0 +1,34 @@
Configuration:
name: DefaultLogger
Appenders:
RollingFile:
name: FileAppender
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
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter:
level: debug
Policies:
OnStartupTriggeringPolicy:
minSize: 0
DefaultRolloverStrategy:
max: 30
Delete:
basePath: archive
maxDepth: 1
IfLastModified:
age: 30d
IfAccumulatedFileSize:
exceeds: 1GB
Loggers:
Root:
level: info
AppenderRef:
- ref: FileAppender
Logger:
name: "com.zaxxer.hikari.HikariConfig"
level: info
AppenderRef:
- ref: FileAppender

View File

@@ -0,0 +1,42 @@
Configuration:
name: DefaultLogger
Appenders:
Console:
name: Console_Out
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %C{1} | %msg %n %throwable"
ThresholdFilter:
level: debug
RollingFile:
name: FileAppender
fileName: logs/application.log
filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %C{1} | %msg %n %throwable"
ThresholdFilter:
level: debug
Policies:
OnStartupTriggeringPolicy:
minSize: 0
DefaultRolloverStrategy:
max: 30
Delete:
basePath: logs/archive
maxDepth: 1
IfLastModified:
age: 30d
IfAccumulatedFileSize:
exceeds: 1GB
Loggers:
Root:
level: debug
AppenderRef:
- ref: Console_Out
- ref: FileAppender
Logger:
name: "com.zaxxer.hikari.HikariConfig"
level: info
AppenderRef:
- ref: Console_Out
- ref: FileAppender

View File

@@ -1,28 +0,0 @@
Configuration:
name: DefaultLogger
Appenders:
Console:
name: Console_Out
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable"
ThresholdFilter:
level: debug
File:
name: FileAppender
fileName: default.log
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable"
ThresholdFilter:
level: debug
Loggers:
Root:
level: debug
AppenderRef:
- ref: Console_Out
- ref: FileAppender
Logger:
name: "com.zaxxer.hikari.HikariConfig"
level: info
AppenderRef:
- ref: Console_Out
- ref: FileAppender

View File

@@ -0,0 +1,34 @@
Configuration:
name: DefaultLogger
Appenders:
RollingFile:
name: FileAppender
fileName: ${sys:user.home}/AppData/Local/MKVAudioSubtitleChanger/logs/application.log
filePattern: ${sys:user.home}/AppData/Local/MKVAudioSubtitleChanger/logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter:
level: debug
Policies:
OnStartupTriggeringPolicy:
minSize: 0
DefaultRolloverStrategy:
max: 30
Delete:
basePath: logs/archive
maxDepth: 1
IfLastModified:
age: 30d
IfAccumulatedFileSize:
exceeds: 1GB
Loggers:
Root:
level: info
AppenderRef:
- ref: FileAppender
Logger:
name: "com.zaxxer.hikari.HikariConfig"
level: info
AppenderRef:
- ref: FileAppender

View File

@@ -1,13 +1,27 @@
Configuration:
name: DefaultLogger
Appenders:
File:
RollingFile:
name: FileAppender
fileName: default.log
fileName: logs/application.log
filePattern: logs/archive/application-%d{yyyy-MM-dd}-%i.log.gz
PatternLayout:
Pattern: "%d{DEFAULT} | %-5level | %thread | %msg %n %throwable"
Pattern: "%d{DEFAULT} | %-5level | %msg %n %throwable"
ThresholdFilter:
level: info
level: debug
Policies:
OnStartupTriggeringPolicy:
minSize: 0
DefaultRolloverStrategy:
max: 30
Delete:
basePath: logs/archive
maxDepth: 1
IfLastModified:
age: 30d
IfAccumulatedFileSize:
exceeds: 1GB
Loggers:
Root:
level: info

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

@@ -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

@@ -0,0 +1,59 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.impl;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackAttributes;
import at.pcgamingfreaks.mkvaudiosubtitlechanger.model.TrackType;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
class SubtitleTrackComparatorTest {
private static Stream<Arguments> compareArguments() {
return Stream.of(
Arguments.of(attr(""), attr(""), 0),
Arguments.of(attr("pref"), attr(""), 1),
Arguments.of(attr(""), attr("pref"), -1),
Arguments.of(attr("pref"), attr("pref"), 0),
Arguments.of(attr("", true), attr("", true), 0),
Arguments.of(attr("", true), attr(""), -1),
Arguments.of(attr("CC", true), attr(""), -1),
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(attr("pref", true), attr("pref"), -1),
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
@MethodSource("compareArguments")
void compare(TrackAttributes track1, TrackAttributes track2, int expected) {
SubtitleTrackComparator comparator = new SubtitleTrackComparator(List.of("pref"), List.of("CC", "SDH"));
int actual = comparator.compare(track1, track2);
assertEquals(expected, actual);
}
private static TrackAttributes attr(String trackname) {
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

@@ -0,0 +1,23 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.*;
@Disabled
class DateUtilsTest {
@Test
void convert() {
Date expectedDate = new Date(0);
String expectedString = "01.01.1970-01:00:00";
assertEquals(expectedDate, DateUtils.convert(0));
assertEquals(expectedDate, DateUtils.convert(expectedString, expectedDate));
assertEquals(expectedDate, DateUtils.convert("1234;15", expectedDate));
assertEquals(expectedString, DateUtils.convert(expectedDate));
}
}

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

@@ -0,0 +1,10 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
import org.apache.commons.lang3.SystemUtils;
public class PathUtils {
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_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

@@ -0,0 +1,12 @@
package at.pcgamingfreaks.mkvaudiosubtitlechanger.util;
public class TestUtil {
public static String[] args(String... args) {
String[] staticArray = new String[]{"-a", "jpn:ger", "/"};
String[] result = new String[staticArray.length + args.length];
System.arraycopy(args, 0, result, 0, args.length);
System.arraycopy(staticArray, 0, result, args.length, staticArray.length);
return result;
}
}

144
src/wix/resources/main.wxs Normal file
View File

@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<?ifdef JpIsSystemWide ?>
<?define JpInstallScope="perMachine"?>
<?else?>
<?define JpInstallScope="perUser"?>
<?endif?>
<?define JpProductLanguage=1033 ?>
<?define JpInstallerVersion=200 ?>
<?define JpCompressedMsi=yes ?>
<?ifdef JpAllowUpgrades ?>
<?define JpUpgradeVersionOnlyDetectUpgrade="no"?>
<?else?>
<?define JpUpgradeVersionOnlyDetectUpgrade="yes"?>
<?endif?>
<?ifdef JpAllowDowngrades ?>
<?define JpUpgradeVersionOnlyDetectDowngrade="no"?>
<?else?>
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
<?endif?>
<?define JpProductCode="*"?>
<?define JpAppName="${project.artifactId}"?>
<?define JpAppVersion="${project.version}"?>
<?define JpAppVendor="${project.maintainer}"?>
<?define JpProductUpgradeCode="a9527300-d364-4cc3-a392-94035065d8c9"?>
<?define JpAppDescription="${project.description}"?>
<?define JpHelpURL="github.com/${project.maintainer}/${project.artifactId}"?>
<Product
Id="$(var.JpProductCode)"
Name="$(var.JpAppName)"
Language="$(var.JpProductLanguage)"
Version="$(var.JpAppVersion)"
Manufacturer="$(var.JpAppVendor)"
UpgradeCode="$(var.JpProductUpgradeCode)">
<Package
Description="$(var.JpAppDescription)"
Manufacturer="$(var.JpAppVendor)"
InstallerVersion="$(var.JpInstallerVersion)"
Compressed="$(var.JpCompressedMsi)"
InstallScope="$(var.JpInstallScope)" Platform="x64"
/>
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
<Upgrade Id="$(var.JpProductUpgradeCode)">
<UpgradeVersion
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectUpgrade)"
Property="JP_UPGRADABLE_FOUND"
Maximum="$(var.JpAppVersion)"
MigrateFeatures="yes"
IncludeMaximum="$(var.JpUpgradeVersionOnlyDetectUpgrade)" />
<UpgradeVersion
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectDowngrade)"
Property="JP_DOWNGRADABLE_FOUND"
Minimum="$(var.JpAppVersion)"
MigrateFeatures="yes"
IncludeMinimum="$(var.JpUpgradeVersionOnlyDetectDowngrade)" />
</Upgrade>
<?ifndef JpAllowUpgrades ?>
<CustomAction Id="JpDisallowUpgrade" Error="!(loc.DisallowUpgradeErrorMessage)" />
<?endif?>
<?ifndef JpAllowDowngrades ?>
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
<?endif?>
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
<!-- Standard required root -->
<Directory Id="TARGETDIR" Name="SourceDir"/>
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
<ComponentGroupRef Id="Shortcuts"/>
<ComponentGroupRef Id="Files"/>
<ComponentGroupRef Id="FileAssociations"/>
<Component Id="pathEnvironmentVariable" Guid="$(var.JpProductUpgradeCode)" KeyPath="yes" Directory="TARGETDIR">
<Environment Id="MyPathVariable" Name="Path" Value="[INSTALLDIR]" Action="set" System="no" Permanent="no" Part="last" Separator=";" />
</Component>
</Feature>
<CustomAction Id="JpSetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
<CustomAction Id="JpSetARPCOMMENTS" Property="ARPCOMMENTS" Value="$(var.JpAppDescription)" />
<CustomAction Id="JpSetARPCONTACT" Property="ARPCONTACT" Value="$(var.JpAppVendor)" />
<!-- <CustomAction Id="JpSetARPSIZE" Property="ARPSIZE" Value="$(var.JpAppSizeKb)" /> -->
<?ifdef JpHelpURL ?>
<CustomAction Id="JpSetARPHELPLINK" Property="ARPHELPLINK" Value="$(var.JpHelpURL)" />
<?endif?>
<?ifdef JpAboutURL ?>
<CustomAction Id="JpSetARPURLINFOABOUT" Property="ARPURLINFOABOUT" Value="$(var.JpAboutURL)" />
<?endif?>
<?ifdef JpUpdateURL ?>
<CustomAction Id="JpSetARPURLUPDATEINFO" Property="ARPURLUPDATEINFO" Value="$(var.JpUpdateURL)" />
<?endif?>
<?ifdef JpIcon ?>
<Property Id="ARPPRODUCTICON" Value="JpARPPRODUCTICON"/>
<Icon Id="JpARPPRODUCTICON" SourceFile="$(var.JpIcon)"/>
<?endif?>
<UIRef Id="JpUI"/>
<InstallExecuteSequence>
<Custom Action="JpSetARPINSTALLLOCATION" After="CostFinalize">Not Installed</Custom>
<Custom Action="JpSetARPCOMMENTS" After="CostFinalize">Not Installed</Custom>
<Custom Action="JpSetARPCONTACT" After="CostFinalize">Not Installed</Custom>
<!-- <Custom Action="JpSetARPSIZE" After="CostFinalize">Not Installed</Custom> -->
<?ifdef JpHelpURL ?>
<Custom Action="JpSetARPHELPLINK" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifdef JpAboutURL ?>
<Custom Action="JpSetARPURLINFOABOUT" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifdef JpUpdateURL ?>
<Custom Action="JpSetARPURLUPDATEINFO" After="CostFinalize">Not Installed</Custom>
<?endif?>
<?ifndef JpAllowUpgrades ?>
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
<?endif?>
<?ifndef JpAllowDowngrades ?>
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
<?endif?>
<RemoveExistingProducts Before="CostInitialize"/>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallExecuteSequence>
<InstallUISequence>
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
</InstallUISequence>
</Product>
</Wix>

View File

@@ -1 +0,0 @@
version=${project.version}