From bea4ec9a992ebd5f8e4f71c7569aabd273343609 Mon Sep 17 00:00:00 2001 From: TheCaptain989 Date: Sun, 9 Feb 2025 10:43:16 -0600 Subject: [PATCH 1/4] Release 2.12.0 --- .assets/radarr-enable-hardlinks.png | Bin 0 -> 38575 bytes README.md | 212 ++++-- SECURITY.md | 4 +- root/usr/local/bin/striptracks-dut.sh | 2 +- root/usr/local/bin/striptracks-eng-debug.sh | 2 +- root/usr/local/bin/striptracks-eng-fre.sh | 2 +- root/usr/local/bin/striptracks-eng-jpn.sh | 2 +- root/usr/local/bin/striptracks-eng.sh | 2 +- root/usr/local/bin/striptracks-fre-debug.sh | 3 - root/usr/local/bin/striptracks-fre.sh | 2 +- root/usr/local/bin/striptracks-ger.sh | 2 +- root/usr/local/bin/striptracks-org-eng.sh | 2 +- root/usr/local/bin/striptracks-spa.sh | 2 +- root/usr/local/bin/striptracks.sh | 764 +++++++++++--------- 14 files changed, 586 insertions(+), 415 deletions(-) create mode 100644 .assets/radarr-enable-hardlinks.png delete mode 100755 root/usr/local/bin/striptracks-fre-debug.sh diff --git a/.assets/radarr-enable-hardlinks.png b/.assets/radarr-enable-hardlinks.png new file mode 100644 index 0000000000000000000000000000000000000000..8c6526cf2e6c505b4976632ce7dc4d3184c8ce79 GIT binary patch literal 38575 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[notes] This mod supports Linux OSes only. +> [!NOTE] +> This mod supports Linux OSes only. + Production Container info: [![Docker Image Size](https://img.shields.io/docker/image-size/linuxserver/mods/radarr-striptracks)](https://hub.docker.com/r/linuxserver/mods/tags?name=radarr-striptracks "Docker image size") [![linuxserver/docker-mods/mods/radarr-striptracks](https://img.shields.io/badge/dynamic/json?logo=github&url=https%3A%2F%2Fthecaptain989.github.io%2Fghcr-pulls%2Fradarr-striptracks.json&query=%24.pulls&label=ghcr%20pulls&color=1572A4)](https://github.com/linuxserver/docker-mods/pkgs/container/mods "GitHub package pulls") @@ -64,7 +66,7 @@ Development Container info:
Synology Screenshot - *Example Synology Configuration* + *Example Synology Configuration* ![striptracks](.assets/striptracks-synology.png "Synology container settings")
@@ -77,7 +79,7 @@ Development Container info:
Screenshot - *Example Custom Script* + *Example Custom Script* ![striptracks custom script](.assets/striptracks-v3-custom-script.png "Radarr/Sonarr custom script settings")
@@ -85,7 +87,8 @@ Development Container info: The script will detect the language(s) defined in Radarr/Sonarr for the movie or TV show and only keep the audio and subtitles selected. Alternatively, a wrapper script or an environment variable may be used to more granularly define which tracks to keep. See [Wrapper Scripts](./README.md#wrapper-scripts) or [Environment Variable](./README.md#environment-variable) for more details. - >![notes] You **must** configure language(s) in Radarr/Sonarr *or* pass command-line arguments for the script to do anything! See the next section for an example. +> [!IMPORTANT] +> You **must** configure language(s) in Radarr/Sonarr *or* pass command-line arguments for the script to do anything! See the next section for an example. ## Radarr Configuration Example The following is a simplified example and steps to configure Radarr so the script will keep Original and English languages of an imported movie. @@ -95,7 +98,7 @@ The following is a simplified example and steps to configure Radarr so the scrip
Screenshot - *New Custom Format Example* + *New Custom Format Example* ![add custom format](.assets/add-custom-format.png "New Custom Format")
@@ -118,7 +121,7 @@ The following is a simplified example and steps to configure Radarr so the scrip
Screenshot - *Radarr Quality Profile Example* + *Radarr Quality Profile Example* ![quality profile](.assets/radarr-quality-profile.png "Radarr Quality Profile Language scoring")
@@ -132,34 +135,43 @@ Chapters, if they exist, are preserved. The Title attribute in the MKV is set to (ex: `The Sting (1973)`) or the series title plus episode information (ex: `Happy! 01x01 - What Smiles Are For`). The language of the video file will be updated in the Radarr or Sonarr database to reflect the actual languages preserved in the remuxed video, and the video will be renamed according to the Radarr/Sonarr rules if needed (for example, if a removed track would trigger a name change.) -If you've configured the Radarr/Sonarr **Recycle Bin** path correctly, the original video will be moved there. ->![warning] **WARNING:** If you have *not* configured the Recycle Bin, the original video file will be deleted/overwritten and permanently lost. - If the resulting video file would contain the same tracks as the original, and it's already an MKV, the remux step is skipped. +> [!TIP] +> If you've configured the Radarr/Sonarr **Recycle Bin** path correctly, the original video will be moved there. + +> [!CAUTION] +> If you have ***not*** configured the Recycle Bin, the original video file will be deleted/overwritten and permanently lost. + ## Automatic Language Detection -Beginning with version 2.0 of this mod, the script may be called with no arguments. It will detect the language(s) configured within Radarr/Sonarr on the particular movie or TV show. -Language selection(s) may be configured in ***Custom Formats*** (in Radarr v3 and higher and Sonarr v4 and higher), ***Quality Profiles*** (only in Radarr), or ***Language Profiles*** (Sonarr v3). Example screenshots are below. +When the script is called with no arguments, it will attempt to detect the language(s) configured within Radarr/Sonarr on the particular movie or TV show. +Language selection(s) may be configured in: +- ***Custom Formats*** (in Radarr v3 and higher and Sonarr v4 and higher), +- ***Quality Profiles*** (only in Radarr), or +- ***Language Profiles*** (Sonarr v3) Both audio **and** subtitle tracks that match the configured language(s) are kept. +> [!TIP] +> It is **highly recommended** to review the [TraSH Guides](https://trash-guides.info/Radarr/Tips/How-to-setup-language-custom-formats/) setup instructions for Language Custom Formats. + +### Special Language Selections The language selection **'Original'** will use the language Radarr pulled from [The Movie Database](https://www.themoviedb.org/ "TMDB") or that Sonarr pulled from [The TVDB](https://www.thetvdb.com/ "TVDB") during its last refresh. Selecting this language is functionally equivalent to calling the script with `--audio :org --subs :org` command-line arguments. See [Original language code](./README.md#original-language-code) below for more details. The language selection **'Unknown'** will match tracks with **no configured language** in the video file. Selecting this language is functionally equivalent to calling the script with `--audio :und --subs :und` command-line arguments. See [Unknown language code](./README.md#unknown-language-code) below for more details. -The language selection **'Any'** has two purposes (Radarr only): - 1) It will trigger a search of languages in ***Custom Formats*** - 2) If none are found, it will preserve **all languages** in the video file. This is functionally equivalent to calling the script with `--audio :any --subs :any` command-line arguments. +The language selection **'Any'** has two purposes: + 1) In Radarr only, when set on a Quality Profile, it will trigger a search of languages in ***Custom Formats*** + 2) If languages are not configured in a Custom Format, or if you're using Sonarr, it will preserve **all languages** in the video file. This is functionally equivalent to calling the script with `--audio :any --subs :any` command-line arguments. See [Any language code](./README.md#any-language-code) below for more details. ->![notes] When using *Custom Format* language conditions and scoring you may not get the results you expect. ->This can be non-intuitive configuration, especially when using negative scoring, the 'Negate' option, and the 'Except Language' option. ->The script does not care what custom format is *detected* by Radarr/Sonarr on the video file, only what the *scores* are in the *Quality Profile*. ->If you choose to use Custom Formats, it is **highly recommended** to first run the script with the debug option `-d`, perform some test downloads and script runs, and then examine your results and the script logs closely to be sure things are working the way you want them to. - -It is **highly recommended** to review the [TraSH Guides](https://trash-guides.info/Radarr/Tips/How-to-setup-language-custom-formats/) setup instructions for Language Custom Formats. +> [!IMPORTANT] +> When using *Custom Formats* language conditions and scoring you may not get the results you expect. +> This can be non-intuitive configuration, especially when using negative scoring, the 'Negate' option, and the 'Except Language' option. +> The script does not care what custom format is *applied* by Radarr/Sonarr on the video file, only what the custom format conditions are and the *scores* are in the corresponding *Quality Profile*. +> If you choose to use Custom Formats, it is **highly recommended** to first run the script with the debug option `-d`, perform some test downloads and script runs, and then examine your results and the script logs closely to be sure things are working the way you want them to. ### Language Detection Precedence The following chart represents the order of precedence that the script uses to decide which language(s) to select when there are multiple settings configured. Moving left to right, it will stop when it finds a configured language. @@ -167,25 +179,30 @@ The following chart represents the order of precedence that the script uses to d ```mermaid graph LR A[Command-Line] - B["Quality + B["Environment + Variable"] + C["Quality Profile"] - C["Custom + D["Custom Formats"] - D["Language Profile + E["Language Profile (Sonarr only)"] A-->B - B-- 'Any' -->C - C-->D + B-->C + C-- 'Any' -->D + D-->E ``` Descriptively, these steps are: -1. Command-line arguments (or environment variable) override all automatic language selection. -2. If there are no command-line arguments, the video's *Quality Profile* is examined for a language configuration (only supported in Radarr). -3. If there is no *Quality Profile* language **or** it is set to 'Any', then examine the *Custom Formats* and scores associated with the quality profile. +1. Command-line arguments override all automatic language selection. +2. Environment variable is checked for arguments. +3. If there are no command-line or environment variable arguments, the video's *Quality Profile* is examined for a language configuration (only supported in Radarr). +4. If there is no *Quality Profile* language **or** it is set to 'Any', then examine the *Custom Formats* and scores associated with the quality profile. All language conditions with positive scores *and* Negated conditions with negative scores *and* non-Negated Except Language conditions with negative scores are selected. -4. If the *Custom Format* scores are zero (0) or there are none with configured language conditions, use the *Language Profile* (only supported in Sonarr v3) +5. If the *Custom Format* scores are zero (0) or there are none with configured language conditions, use the *Language Profile* (only supported in Sonarr v3) ->![notes] For step 3 above, using *Custom Formats* when 'Any' is in the *Quality Profile* is consistent with the behavior described in [TRaSH Guides](https://trash-guides.info/Radarr/Tips/How-to-setup-language-custom-formats/ "TraSH Guides: How to setup Language Custom Formats"). +> [!NOTE] +> For step 4 above, using *Custom Formats* when 'Any' is in the *Quality Profile* is consistent with the behavior described in [TRaSH Guides](https://trash-guides.info/Radarr/Tips/How-to-setup-language-custom-formats/ "TraSH Guides: How to setup Language Custom Formats"). ## Command-Line Syntax @@ -193,21 +210,21 @@ All language conditions with positive scores *and* Negated conditions with negat The script also supports command-line arguments that will override the automatic language detection. More granular control can therefore be exerted or extended using tagging and defining multiple *Connect* scripts (this is native Radarr/Sonarr functionality outside the scope of this documentation). The syntax for the command-line is: -`striptracks.sh [{-a|--audio} [{-s|--subs} ] [{-f|--file} ]] [{-l,--log} ] [{-c,--config} ] [{-d|--debug} []]` +`striptracks.sh [{-a|--audio} [{-s|--subs} ] [{-f|--file} ]] [{-l|--log} ] [{-c|--config} ] [{-d|--debug} []]`
Table of Command-Line Arguments Option|Argument|Description ---|---|--- --a, --audio||Audio languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`) --s, --subs||Subtitle languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`) --f, --file||If included, the script enters **[Batch Mode](./README.md#batch-mode)** and converts the specified video file.
Requires the `-a` option.
![notes] **Do not** use this argument when called from Radarr or Sonarr! --l, --log|\|The log filename
Default is /config/log/striptracks.txt --c, --config|\|Radarr/Sonarr XML configuration file
Default is /config/config.xml --d, --debug|\[\\]|Enables debug logging. Level is optional.
Default is 1 (low)
2 includes JSON output
3 contains even more JSON output ---help| |Display help and exit. ---version| |Display version and exit. +`-a`, `--audio`|``|Audio languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`)
Each code may optionally be followed by a plus (`+`) and one or more [modifiers](./README.md#language-code-modifiers). +`-s`, `--subs`|``|Subtitle languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`)
Each code may optionally be followed by a plus (`+`) and one or more modifiers. +`-f`, `--file`|``|If included, the script enters **[Batch Mode](./README.md#batch-mode)** and converts the specified video file.
Requires the `-a` option.
![notes] **Do not** use this argument when called from Radarr or Sonarr! +`-l`, `--log`|``|The log filename
Default is `/config/log/striptracks.txt` +`-c`, `--config`|``|Radarr/Sonarr XML configuration file
Default is `/config/config.xml` +`-d`, `--debug`|`[]`|Enables debug logging. Level is optional.
Default is `1` (low)
`2` includes JSON output
`3` contains even more JSON output +`--help`| |Display help and exit. +`--version`| |Display version and exit.
@@ -222,7 +239,28 @@ For example: Multiple codes may be concatenated, such as `:eng:spa` for both English and Spanish. Order is unimportant. ->![warning] **WARNING:** If no subtitle language is detected via Radarr/Sonarr configuration or specified on the command-line, all subtitles are removed. +> [!WARNING] +> If no subtitle language is detected via Radarr/Sonarr configuration or specified on the command-line, all subtitles are removed. + +### Language Code Modifiers +Each language code can optionally be followed by a plus (`+`) and one or more modifier characters. Supported modifiers are: + +Modifier|Function +---|--- +`f`|Selects only tracks with the forced flag set +`d`|Selects only tracks with the default flag set +`[0-9]`|Specifies the maximum number of tracks to select.
Based on the order of the tracks in the original source video. + +These modifiers must be applied to each language code you want to modify. They may be used with either audio or subtitles codes. +For example, the following options, `--audio :org:any+d --subs :eng+1:any+f` would keep: +- All original language audio tracks, and all Default audio tracks regardless of language +- One English language subtitles track, and all Forced subtitles tracks regardless of language + +Modifiers may be combined, such as `:any+fd` to keep all forced and all default tracks, or `:eng+1d` to keep one default English track. + +> [!NOTE] +> Note the exact phrasing of the previous sentence. There is nuance here that is not obvious. +> `:any+fd` is equivalent to `:any+f:any+d`, but `:eng+1d` is **not** the same as `:eng+1:eng+d`. ### Any language code The `:any` language code is a special code. When used, the script will preserve all language tracks, regardless of how they are tagged in the source video. @@ -232,12 +270,14 @@ The `:org` language code is a special code. When used, instead of retaining a sp As an example, when importing "*Amores Perros (2000)*" with options `--audio :org:eng`, the Spanish and English audio tracks are preserved. Several [Included Wrapper Scripts](./README.md#included-wrapper-scripts) use this special code. ->![notes] This feature relies on the 'originalLanguage' field in the Radarr/Sonarr database. The `:org` code is therefore invalid when used in Batch Mode. +> [!NOTE] +> This feature relies on the 'originalLanguage' field in the Radarr/Sonarr database. The `:org` code is therefore invalid when used in Batch Mode. > The script will log a warning if it detects the use of `:org` in an invalid way, though it will continue to execute. ### Unknown language code The `:und` language code is a special code. When used, the script will match on any track that has a null or blank language attribute. If not included, tracks with no language attribute will be removed. ->![warning] **WARNING:** It is common for M2TS and AVI files to have tracks with unknown languages! It is strongly recommended to include `:und` in most instances unless you know exactly what you're doing. +> [!TIP] +> It is common for M2TS and AVI files to have tracks with unknown languages! It is recommended to include `:und` in most instances unless you know exactly what you're doing. ## Special Handling of Audio The script is smart enough to not remove the last audio track. There is in fact no way to force the script to remove all audio. This way you don't have to specify every possible language if you are importing a foreign film, for example. @@ -255,15 +295,20 @@ There is no way to force the script to remove audio tracks with these codes. -d 2 # Enable debugging level 2, audio and subtitles # languages detected from Radarr/Sonarr -a :eng:und -s :eng # Keep English and Unknown audio, and English subtitles --a :org:eng -s :eng # Keep English and Original audio, and English subtitles -:eng "" # Keep English audio and remove all subtitles --d -a :eng:kor:jpn -s :eng:spa # Enable debugging level 1, keeping English, Korean, and Japanese audio, and - # English and Spanish subtitles +-a :org:eng -s :any+f:eng # Keep English and Original audio, and all forced or English subtitles +-a :eng -s "" # Keep English audio and remove all subtitles +-a :any -s "" # Keep all audio and remove all subtitles +-d -a :eng:kor:jpn -s :eng:spa # Enable debugging level 1, keeping English, Korean, and Japanese audio, + # and English and Spanish subtitles -f "/movies/Finding Nemo (2003).mkv" -a :eng:und -s :eng # Batch Mode # Keep English and Unknown audio and English subtitles, converting # video specified --a :any -s "" # Keep all audio and remove all subtitles +--audio :org:any+d1 --subs :eng+1:any+f2 + # Keep Original audio and one default audio track regardless of language + # (first audio track flagged as Default as it appears in the source file), + # one English subtitles track and two forced subtitles regardless of + # language (as they appear in the source file) ``` @@ -271,6 +316,10 @@ There is no way to force the script to remove audio tracks with these codes. ## Wrapper Scripts To supply arguments to the script, you must either use one of the included wrapper scripts, create a custom wrapper script, or set the `STRIPTRACKS_ARGS` [environment variable](./README.md#environment-variable). +> [!TIP] +> If you followed the Linuxserver.io recommendations when configuring your container, the `/config` directory will be mapped to an external storage location. +> It is therefore recommended to place custom scripts in the `/config` directory so they will survive container updates, but they may be placed anywhere that is accessible by Radarr or Sonarr. + ### Included Wrapper Scripts For your convenience, several wrapper scripts are included in the `/usr/local/bin/` directory. You may use any of these in place of `striptracks.sh` mentioned in the [Installation](./README.md#installation) section above. @@ -288,10 +337,9 @@ striptracks-eng-debug.sh # Keep English and Unknown audio, and English subtitl striptracks-eng-fre.sh # Keep English, French, and Unknown audio, and English and French subtitles striptracks-eng-jpn.sh # Keep English, Japanese, and Unknown audio and English subtitles striptracks-fre.sh # Keep French and Unknown audio, and French subtitles -striptracks-fre-debug.sh # Keep French and Unknown audio, French subtitles, and enable debug logging striptracks-ger.sh # Keep German and Unknown audio, and German subtitles striptracks-spa.sh # Keep Spanish and Unknown audio, and Spanish subtitles -striptracks-org-eng.sh # Keep Original, English, and Unknown audio, and Original and English subtitles +striptracks-org-eng.sh # Keep Original, English, Unknown, and forced audio, and Original, English, and forced subtitles striptracks-org-ger.sh # Keep Original, German, and Unknown audio, and Original and German subtitles striptracks-org-spa.sh # Keep Original, Spanish, and Unknown audio, and Original and Spanish subtitles ``` @@ -299,6 +347,9 @@ striptracks-org-spa.sh # Keep Original, Spanish, and Unknown audio, and Orig ### Example Wrapper Script +
+Example Script + To configure an entry from the [Examples](./README.md#examples) section above, create and save a file called `striptracks-custom.sh` to `/config` containing the following text: ```shell @@ -315,10 +366,16 @@ chmod +x /config/striptracks-custom.sh Then put `/config/striptracks-custom.sh` in the **Path** field in place of `/usr/local/bin/striptracks.sh` mentioned in the [Installation](./README.md#installation) section above. ->![notes] If you followed the Linuxserver.io recommendations when configuring your container, the `/config` directory will be mapped to an external storage location. It is therefore recommended to place custom scripts in the `/config` directory so they will survive container updates, but they may be placed anywhere that is accessible by Radarr or Sonarr. +
## Environment Variable -The `striptracks.sh` script can read command-line arguments from the `STRIPTRACKS_ARGS` environment variable. This allows advanced use cases without having to provide a custom script. +The script can also read arguments from the `STRIPTRACKS_ARGS` environment variable. This allows advanced use cases without having to provide a custom wrapper script. + +> [!NOTE] +> The environment variable is *only* used when **no** command-line arguments are present. **Any** command-line argument will disable the use of the environment variable. + +
+Example Docker Compose For example, the following lines in your `compose.yml` file would keep English, Japanese, and Unknown audio and English subtitles: @@ -327,12 +384,18 @@ environment: - STRIPTRACKS_ARGS=--audio :eng:jpn:und --subs :eng ``` +
+
+Example Docker Run Command + In a `docker run` command, it would be: ```shell -e STRIPTRACKS_ARGS='--audio :eng:jpn:und --subs :eng' ``` +
+
Synology Screenshot @@ -341,8 +404,6 @@ In a `docker run` command, it would be:
->![notes] The environment variable is *only* read when **no** command-line arguments are present. **Any** command-line argument will disable the use of the environment variable. - ## Triggers The only events/notification triggers that are supported are **On Import** and **On Upgrade**. The script will log an error if executed by any other trigger. @@ -361,18 +422,23 @@ Because the script is not called from within Radarr or Sonarr, their database is * *Original video files are deleted.*
The Recycle Bin function is not available. ### Batch Example +
+Batch Mode Example + To keep English and Unknown audio and English subtitles on all video files ending in .MKV, .AVI, or .MP4 in the `/movies` directory, enter the following at the Linux command-line: ```shell find /movies/ -type f \( -name "*.mkv" -o -name "*.avi" -o -name "*.mp4" \) | while read file; do /usr/local/bin/striptracks.sh -f "$file" -a :eng:und -s :eng; done ``` -Here's another example to keep English, Danish, and Unknown languages on all video files in your `./videos` directory (requires the `file` program; testable with `file -v`): +Here's another example to keep English, Danish, Unknown languages, and all forced subtitles on all video files in your `./videos` directory (requires the `file` program; testable with `file -v`): ```shell -find ./videos/ -type f | while read filename; do if file -i "$filename" | grep -q video; then /usr/local/bin/striptracks.sh -f "$filename" --audio :eng:dan:und --subs :eng:dan:und; fi; done +find ./videos/ -type f | while read filename; do if file -i "$filename" | grep -q video; then /usr/local/bin/striptracks.sh -f "$filename" --audio :eng:dan:und --subs :eng:dan:und:any+f; fi; done ``` +
+ ## Logs By default, a log file is created for the script activity called: @@ -383,7 +449,35 @@ This log can be inspected or downloaded from Radarr/Sonarr under *System* > *Log Script errors will show up in both the script log and the native Radarr/Sonarr log. Log rotation is performed with 5 log files of 512KB each being kept. ->![warning] **WARNING:** If debug logging is enabled with a level above 1, the log file can grow very large very quickly. *Do not leave high-level debug logging enabled permanently.* +> [!CAUTION] +> If debug logging is enabled with a level above 1, the log file can grow very large very quickly. *Do not leave high-level debug logging enabled permanently.* + +# Limitations +It should be noted that this script's core functionality nulifies some of the benefits of [hardlinks](https://trash-guides.info/hardlinks/). +However, configuring hardlinks is still recommended. + +
+Hardlink Limitations + +*Radarr Hardlinks Configuration Screenshot* +![radarr-enable-hardlinks](./.assets/radarr-enable-hardlinks.png "Radarr hardlinks screenshot") + +Hardlinks are essentially multiple references to the *same file*. +The purpose of a hardlink is to: +- Allow instant file moves from the download client to Radarr or Sonarr +- Reduce duplicate storage space +- Allow torrent seeding after download + +Because the script creates a brand-new video file that includes only the selected streams and deletes the original, a hardlink cannot be preserved. +Instant file moves from your download client will continue to work, but the new file will consume additional space, and the original file will be deleted (or unlinked) by +the script which could prevent torrent seeding. + +The script will log a warning if it detects the input video is a hardlink. + +Note that the script does not *always* create a new file. If there are no streams removed, the original video file is not deleted and any hardlinks are preserved. +It is therefore still recommended to enable and use hardlinks in Radarr and Sonarr. + +
# Uninstall To completely remove the mod: @@ -403,11 +497,11 @@ This would not be possible without the following: [LinuxServer.io Sonarr](https://hub.docker.com/r/linuxserver/sonarr "Sonarr Docker container") container [LinuxServer.io Docker Mods](https://hub.docker.com/r/linuxserver/mods "Docker Mods containers") project [MKVToolNix](https://mkvtoolnix.download/ "MKVToolNix homepage") by Moritz Bunkus -The AWK script parsing mkvmerge output is adapted from Endoro's post on [VideoHelp](https://forum.videohelp.com/threads/343271-BULK-remove-non-English-tracks-from-MKV-container#post2292889). +Inspired by Endoro's post on [VideoHelp](https://forum.videohelp.com/threads/343271-BULK-remove-non-English-tracks-from-MKV-container#post2292889). Icons made by [Freepik](https://www.freepik.com) from [Flaticon](https://www.flaticon.com/) ## Legacy Change Notes -Beginning with version 2.0 of this mod, it only supports v3 or later of Radarr/Sonarr. For legacy Radarr/Sonarr v2 please use mod release 1.3 or earlier. +Beginning with version 2.0 of this mod, it only supports v3 or later of Radarr/Sonarr. For legacy Radarr/Sonarr v2 please use mod release 1.3 or earlier. +Version 2.0 of this mod introduced automatic language detection. -[warning]: .assets/warning.png "Warning" [notes]: .assets/notes.png "Note" diff --git a/SECURITY.md b/SECURITY.md index d8bfb32..bed2e28 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,8 @@ Only the latest major and minor version are supported. | Version | Supported | | ------- | ------------------ | -| 2.9.x | :heavy_check_mark: | -| < 2.9 | :x: | +| 2.12.x | :heavy_check_mark: | +| < 2.12 | :x: | ## Reporting a Vulnerability diff --git a/root/usr/local/bin/striptracks-dut.sh b/root/usr/local/bin/striptracks-dut.sh index 57571f6..1d635ee 100755 --- a/root/usr/local/bin/striptracks-dut.sh +++ b/root/usr/local/bin/striptracks-dut.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :nld:dut:und :nld:dut +. /usr/local/bin/striptracks.sh --audio :nld:dut:und --subs :nld:dut diff --git a/root/usr/local/bin/striptracks-eng-debug.sh b/root/usr/local/bin/striptracks-eng-debug.sh index 87ed2bb..3759e3b 100755 --- a/root/usr/local/bin/striptracks-eng-debug.sh +++ b/root/usr/local/bin/striptracks-eng-debug.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh -d :eng:und :eng +. /usr/local/bin/striptracks.sh -d --audio :eng:und:any+d --subs :eng:any+f diff --git a/root/usr/local/bin/striptracks-eng-fre.sh b/root/usr/local/bin/striptracks-eng-fre.sh index 47030e3..7acca83 100755 --- a/root/usr/local/bin/striptracks-eng-fre.sh +++ b/root/usr/local/bin/striptracks-eng-fre.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :eng:fre:und :eng:fre +. /usr/local/bin/striptracks.sh --audio :eng:fre:und --subs :eng:fre diff --git a/root/usr/local/bin/striptracks-eng-jpn.sh b/root/usr/local/bin/striptracks-eng-jpn.sh index ea37f5d..9e9268c 100755 --- a/root/usr/local/bin/striptracks-eng-jpn.sh +++ b/root/usr/local/bin/striptracks-eng-jpn.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :eng:jpn:und :eng +. /usr/local/bin/striptracks.sh --audio :eng:jpn:und --subs :eng diff --git a/root/usr/local/bin/striptracks-eng.sh b/root/usr/local/bin/striptracks-eng.sh index a0eb344..c499f2a 100755 --- a/root/usr/local/bin/striptracks-eng.sh +++ b/root/usr/local/bin/striptracks-eng.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :eng:und :eng +. /usr/local/bin/striptracks.sh --audio :eng:und --subs :eng diff --git a/root/usr/local/bin/striptracks-fre-debug.sh b/root/usr/local/bin/striptracks-fre-debug.sh deleted file mode 100755 index 8c3b765..0000000 --- a/root/usr/local/bin/striptracks-fre-debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -. /usr/local/bin/striptracks.sh -d :fre:fra:und :fre:fra diff --git a/root/usr/local/bin/striptracks-fre.sh b/root/usr/local/bin/striptracks-fre.sh index 832339b..717a3b7 100755 --- a/root/usr/local/bin/striptracks-fre.sh +++ b/root/usr/local/bin/striptracks-fre.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :fre:fra:und :fre:fra +. /usr/local/bin/striptracks.sh --audio :fre:fra:und --subs :fre:fra diff --git a/root/usr/local/bin/striptracks-ger.sh b/root/usr/local/bin/striptracks-ger.sh index 00b99fe..2244030 100755 --- a/root/usr/local/bin/striptracks-ger.sh +++ b/root/usr/local/bin/striptracks-ger.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :ger:deu:und :ger:deu +. /usr/local/bin/striptracks.sh --audio :ger:deu:und --subs :ger:deu diff --git a/root/usr/local/bin/striptracks-org-eng.sh b/root/usr/local/bin/striptracks-org-eng.sh index 364dbd3..6718f68 100755 --- a/root/usr/local/bin/striptracks-org-eng.sh +++ b/root/usr/local/bin/striptracks-org-eng.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh --audio :org:eng:und --subs :org:eng +. /usr/local/bin/striptracks.sh --audio :org:eng:und:any+d --subs :org:eng:any+f diff --git a/root/usr/local/bin/striptracks-spa.sh b/root/usr/local/bin/striptracks-spa.sh index 2464f25..ae4e244 100755 --- a/root/usr/local/bin/striptracks-spa.sh +++ b/root/usr/local/bin/striptracks-spa.sh @@ -1,3 +1,3 @@ #!/bin/bash -. /usr/local/bin/striptracks.sh :spa:und :spa +. /usr/local/bin/striptracks.sh --audio :spa:und --subs :spa diff --git a/root/usr/local/bin/striptracks.sh b/root/usr/local/bin/striptracks.sh index 82d1c75..d79bf83 100755 --- a/root/usr/local/bin/striptracks.sh +++ b/root/usr/local/bin/striptracks.sh @@ -34,16 +34,16 @@ # 1 - no video file specified on command line # 2 - no audio language specified on command line # 3 - no subtitles language specified on command line -# 4 - mkvmerge or mkvpropedit not found +# 4 - mkvmerge, mkvpropedit, or jq not found # 5 - input video file not found # 6 - unable to rename temp video to MKV # 7 - unknown eventtype environment variable # 8 - unsupported Radarr/Sonarr version (v2) -# 9 - mkvmerge get media info failed +# 9 - mkvmerge get media info produced an error or warning # 10 - remuxing completed, but no output file found -# 11 - source video had no audio or subtitle tracks +# 11 - source video had no audio tracks # 12 - log file is not writable -# 13 - awk script exited abnormally +# 13 - mkvmerge or mkvpropedit exited with an error # 15 - could not set permissions and/or owner on new file # 16 - could not delete the original file # 17 - Radarr/Sonarr API error @@ -82,9 +82,13 @@ Options and Arguments: -a, --audio Audio languages to keep ISO639-2 code(s) prefixed with a colon \`:\` multiple codes may be concatenated. + Each code may optionally be followed by a + plus \`+\` and one or more modifiers. -s, --subs Subtitles languages to keep ISO639-2 code(s) prefixed with a colon \`:\` multiple codes may be concatenated. + Each code may optionally be followed by a + plus \`+\` and one or more modifiers. -f, --file If included, the script enters batch mode and converts the specified video file. WARNING: Do not use this argument when called @@ -105,6 +109,9 @@ audio or subtitle languages configured in the Radarr or Sonarr profile. When used on the command line, they override the detected codes. They are also accepted as positional parameters for backwards compatibility. +Language modifiers may be \`f\` or \`d\` which select Forced or Default tracks +respectively, or a number which specifies the maximum tracks to keep. + Batch Mode: In batch mode the script acts as if it were not called from within Radarr or Sonarr. It converts the file specified on the command line and ignores @@ -117,9 +124,9 @@ Examples: # Radarr/Sonarr $striptracks_script -a :eng:und -s :eng # keep English and Unknown audio and # English subtitles - $striptracks_script -a :eng:org -s :eng # keep English and Original audio and - # English subtitles - $striptracks_script :eng \"\" # keep English audio and no subtitles + $striptracks_script -a :eng:org -s :any+f:eng # keep English and Original audio, + # and forced or English subtitles + $striptracks_script -a :eng -s \"\" # keep English audio and no subtitles $striptracks_script -d :eng:kor:jpn :eng:spa # Enable debugging level 1, keeping # English, Korean, and Japanese # audio, and English and Spanish @@ -151,7 +158,7 @@ if [ -n "$STRIPTRACKS_ARGS" ]; then fi # Process arguments -# Taken from Drew Strokes post 3/24/2015: +# Taken from Drew Stokes post 3/24/2015: # https://medium.com/@Drew_Stokes/bash-argument-parsing-54f3b81a6a8f unset striptracks_pos_params while (( "$#" )); do @@ -175,11 +182,11 @@ while (( "$#" )); do exit 1 fi ;; - --help ) # Display usage + -h|--help ) # Display usage usage exit 0 ;; - --version ) # Display version + -v|--version ) # Display version echo "$striptracks_script $striptracks_ver" exit 0 ;; @@ -196,24 +203,30 @@ while (( "$#" )); do fi ;; -a|--audio ) # Audio languages to keep - if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then - export striptracks_audiokeep="$2" - shift 2 - else + if [ -z "$2" ] || [ ${2:0:1} = "-" ]; then echo "Error|Invalid option: $1 requires an argument." >&2 usage exit 2 + elif [[ "$2" != :* ]]; then + echo "Error|Invalid option: $1 argument requires a colon." >&2 + usage + exit 2 fi + export striptracks_audiokeep="$2" + shift 2 ;; - -s|--subs ) # Subtitles languages to keep - if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then - export striptracks_subskeep="$2" - shift 2 - else + -s|--subs|--subtitles ) # Subtitles languages to keep + if [ -z "$2" ] || [ ${2:0:1} = "-" ]; then echo "Error|Invalid option: $1 requires an argument." >&2 usage exit 3 + elif [[ "$2" != :* ]]; then + echo "Error|Invalid option: $1 argument requires a colon." >&2 + usage + exit 3 fi + export striptracks_subskeep="$2" + shift 2 ;; -c|--config ) # *arr XML configuration file if [ -n "$2" ] && [ ${2:0:1} != "-" ]; then @@ -314,7 +327,7 @@ export striptracks_eventtype="${striptracks_type,,}_eventtype" export striptracks_newvideo="${striptracks_video%.*}.mkv" # If this were defined directly in Radarr or Sonarr this would not be needed here # shellcheck disable=SC2089 -striptracks_isocodemap='{"languages":[{"language":{"name":"Any","iso639-2":["any"]}},{"language":{"name":"Arabic","iso639-2":["ara"]}},{"language":{"name":"Bengali","iso639-2":["ben"]}},{"language":{"name":"Bosnian","iso639-2":["bos"]}},{"language":{"name":"Bulgarian","iso639-2":["bul"]}},{"language":{"name":"Catalan","iso639-2":["cat"]}},{"language":{"name":"Chinese","iso639-2":["zho","chi"]}},{"language":{"name":"Croatian","iso639-2":["hrv"]}},{"language":{"name":"Czech","iso639-2":["ces","cze"]}},{"language":{"name":"Danish","iso639-2":["dan"]}},{"language":{"name":"Dutch","iso639-2":["nld","dut"]}},{"language":{"name":"English","iso639-2":["eng"]}},{"language":{"name":"Estonian","iso639-2":["est"]}},{"language":{"name":"Finnish","iso639-2":["fin"]}},{"language":{"name":"Flemish","iso639-2":["nld","dut"]}},{"language":{"name":"French","iso639-2":["fra","fre"]}},{"language":{"name":"German","iso639-2":["deu","ger"]}},{"language":{"name":"Greek","iso639-2":["ell","gre"]}},{"language":{"name":"Hebrew","iso639-2":["heb"]}},{"language":{"name":"Hindi","iso639-2":["hin"]}},{"language":{"name":"Hungarian","iso639-2":["hun"]}},{"language":{"name":"Icelandic","iso639-2":["isl","ice"]}},{"language":{"name":"Indonesian","iso639-2":["ind"]}},{"language":{"name":"Italian","iso639-2":["ita"]}},{"language":{"name":"Japanese","iso639-2":["jpn"]}},{"language":{"name":"Kannada","iso639-2":["kan"]}},{"language":{"name":"Korean","iso639-2":["kor"]}},{"language":{"name":"Latvian","iso639-2":["lav"]}},{"language":{"name":"Lithuanian","iso639-2":["lit"]}},{"language":{"name":"Macedonian","iso639-2":["mac","mkd"]}},{"language":{"name":"Malayalam","iso639-2":["mal"]}},{"language":{"name":"Norwegian","iso639-2":["nno","nob","nor"]}},{"language":{"name":"Persian","iso639-2":["fas","per"]}},{"language":{"name":"Polish","iso639-2":["pol"]}},{"language":{"name":"Portuguese","iso639-2":["por"]}},{"language":{"name":"Portuguese (Brazil)","iso639-2":["por"]}},{"language":{"name":"Romanian","iso639-2":["rum","ron"]}},{"language":{"name":"Russian","iso639-2":["rus"]}},{"language":{"name":"Serbian","iso639-2":["srp"]}},{"language":{"name":"Slovak","iso639-2":["slk","slo"]}},{"language":{"name":"Slovenian","iso639-2":["slv"]}},{"language":{"name":"Spanish","iso639-2":["spa"]}},{"language":{"name":"Spanish (Latino)","iso639-2":["spa"]}},{"language":{"name":"Swedish","iso639-2":["swe"]}},{"language":{"name":"Tamil","iso639-2":["tam"]}},{"language":{"name":"Telugu","iso639-2":["tel"]}},{"language":{"name":"Thai","iso639-2":["tha"]}},{"language":{"name":"Turkish","iso639-2":["tur"]}},{"language":{"name":"Ukrainian","iso639-2":["ukr"]}},{"language":{"name":"Vietnamese","iso639-2":["vie"]}},{"language":{"name":"Unknown","iso639-2":["und"]}}]}' +striptracks_isocodemap='{"languages":[{"language":{"name":"Any","iso639-2":["any"]}},{"language":{"name":"Afrikaans","iso639-2":["afr"]}},{"language":{"name":"Albanian","iso639-2":["sqi","alb"]}},{"language":{"name":"Arabic","iso639-2":["ara"]}},{"language":{"name":"Bengali","iso639-2":["ben"]}},{"language":{"name":"Bosnian","iso639-2":["bos"]}},{"language":{"name":"Bulgarian","iso639-2":["bul"]}},{"language":{"name":"Catalan","iso639-2":["cat"]}},{"language":{"name":"Chinese","iso639-2":["zho","chi"]}},{"language":{"name":"Croatian","iso639-2":["hrv"]}},{"language":{"name":"Czech","iso639-2":["ces","cze"]}},{"language":{"name":"Danish","iso639-2":["dan"]}},{"language":{"name":"Dutch","iso639-2":["nld","dut"]}},{"language":{"name":"English","iso639-2":["eng"]}},{"language":{"name":"Estonian","iso639-2":["est"]}},{"language":{"name":"Finnish","iso639-2":["fin"]}},{"language":{"name":"Flemish","iso639-2":["nld","dut"]}},{"language":{"name":"French","iso639-2":["fra","fre"]}},{"language":{"name":"German","iso639-2":["deu","ger"]}},{"language":{"name":"Greek","iso639-2":["ell","gre"]}},{"language":{"name":"Hebrew","iso639-2":["heb"]}},{"language":{"name":"Hindi","iso639-2":["hin"]}},{"language":{"name":"Hungarian","iso639-2":["hun"]}},{"language":{"name":"Icelandic","iso639-2":["isl","ice"]}},{"language":{"name":"Indonesian","iso639-2":["ind"]}},{"language":{"name":"Italian","iso639-2":["ita"]}},{"language":{"name":"Japanese","iso639-2":["jpn"]}},{"language":{"name":"Kannada","iso639-2":["kan"]}},{"language":{"name":"Korean","iso639-2":["kor"]}},{"language":{"name":"Latvian","iso639-2":["lav"]}},{"language":{"name":"Lithuanian","iso639-2":["lit"]}},{"language":{"name":"Macedonian","iso639-2":["mac","mkd"]}},{"language":{"name":"Malayalam","iso639-2":["mal"]}},{"language":{"name":"Marathi","iso639-2":["mar"]}},{"language":{"name":"Norwegian","iso639-2":["nno","nob","nor"]}},{"language":{"name":"Persian","iso639-2":["fas","per"]}},{"language":{"name":"Polish","iso639-2":["pol"]}},{"language":{"name":"Portuguese","iso639-2":["por"]}},{"language":{"name":"Portuguese (Brazil)","iso639-2":["por"]}},{"language":{"name":"Romanian","iso639-2":["rum","ron"]}},{"language":{"name":"Russian","iso639-2":["rus"]}},{"language":{"name":"Serbian","iso639-2":["srp"]}},{"language":{"name":"Slovak","iso639-2":["slk","slo"]}},{"language":{"name":"Slovenian","iso639-2":["slv"]}},{"language":{"name":"Spanish","iso639-2":["spa"]}},{"language":{"name":"Spanish (Latino)","iso639-2":["spa"]}},{"language":{"name":"Swedish","iso639-2":["swe"]}},{"language":{"name":"Tagalog","iso639-2":["tgl"]}},{"language":{"name":"Tamil","iso639-2":["tam"]}},{"language":{"name":"Telugu","iso639-2":["tel"]}},{"language":{"name":"Thai","iso639-2":["tha"]}},{"language":{"name":"Turkish","iso639-2":["tur"]}},{"language":{"name":"Ukrainian","iso639-2":["ukr"]}},{"language":{"name":"Vietnamese","iso639-2":["vie"]}},{"language":{"name":"Unknown","iso639-2":["und"]}}]}' ### Functions @@ -351,7 +364,7 @@ function get_version { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -373,7 +386,7 @@ function get_video_info { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -393,9 +406,9 @@ function get_videofile_info { striptracks_result=$(curl -s --fail-with-body -H "X-Api-Key: $striptracks_apikey" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ - --get "$url" ) + --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -422,13 +435,13 @@ function rescan { -d "$data" \ "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } [ $striptracks_debug -ge 2 ] && echo "API returned: $striptracks_result" | awk '{print "Debug|"$0}' | log # Exit loop if database is not locked, else wait 1 minute - if [[ ! "$(echo $striptracks_result | jq -jcrM .message?)" =~ database\ is\ locked ]]; then + if [[ ! "$(echo $striptracks_result | jq -jcM .message?)" =~ database\ is\ locked ]]; then break else echo "Warn|Database is locked; system is likely overloaded. Sleeping 1 minute." | log @@ -461,7 +474,7 @@ function check_job { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 local striptracks_return=10 @@ -500,7 +513,7 @@ function get_profiles { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -527,7 +540,7 @@ function get_language_codes { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -551,7 +564,7 @@ function get_custom_formats { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -577,13 +590,13 @@ function delete_video { -H "Accept: application/json" \ -X DELETE "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } [ $striptracks_debug -ge 2 ] && echo "API returned: $striptracks_result" | awk '{print "Debug|"$0}' | log # Exit loop if database is not locked, else wait 1 minute - if [[ ! "$(echo $striptracks_result | jq -jcrM .message?)" =~ database\ is\ locked ]]; then + if [[ ! "$(echo $striptracks_result | jq -jcM .message?)" =~ database\ is\ locked ]]; then break else echo "Warn|Database is locked; system is likely overloaded. Sleeping 1 minute." | log @@ -614,7 +627,7 @@ function delete_video { # -d "filterExistingFiles=false" \ # --get "$url") # local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - # local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url?${temp_id:+$temp_id&}folder=$striptracks_video_folder&filterExistingFiles=false\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + # local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url?${temp_id:+$temp_id&}folder=$striptracks_video_folder&filterExistingFiles=false\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') # echo "$striptracks_message" | log # echo "$striptracks_message" >&2 # } @@ -641,14 +654,14 @@ function set_metadata { -d "$data" \ -X PUT "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } [ $striptracks_debug -ge 2 ] && echo "Debug|API returned ${#striptracks_result} bytes." | log [ $striptracks_debug -ge 3 ] && echo "API returned: $striptracks_result" | awk '{print "Debug|"$0}' | log # Exit loop if database is not locked, else wait 1 minute - if [[ ! "$(echo $striptracks_result | jq -jcrM .message?)" =~ database\ is\ locked ]]; then + if [[ ! "$(echo $striptracks_result | jq -jcM .message?)" =~ database\ is\ locked ]]; then break else echo "Warn|Database is locked; system is likely overloaded. Sleeping 1 minute." | log @@ -662,22 +675,32 @@ function set_metadata { fi return $striptracks_return } -# Read in the output of mkvmerge info extraction +# Read in the output of mkvmerge info extraction (see issue #87) function get_mediainfo { [ $striptracks_debug -ge 1 ] && echo "Debug|Executing: /usr/bin/mkvmerge -J \"$1\"" | log unset striptracks_json - striptracks_json=$(/usr/bin/mkvmerge -J "$1" 2>&1) - local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message="Error|[$striptracks_curlret] Error executing mkvmerge. It returned: $striptracks_json" - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - } + striptracks_json=$(/usr/bin/mkvmerge -J "$1") + local striptracks_return=$? [ $striptracks_debug -ge 2 ] && echo "mkvmerge returned: $striptracks_json" | awk '{print "Debug|"$0}' | log - if [ "$(echo $striptracks_json | jq -crM '.container.supported')" = "true" ]; then - local striptracks_return=0 - else - local striptracks_return=1 - fi + case $striptracks_return in + 0) + # Check for unsupported container. + if [ "$(echo "$striptracks_json" | jq -crM '.container.supported')" = "false" ]; then + striptracks_message="Error|Video format for '$1' is unsupported. Unable to continue. mkvmerge returned container info: $(echo $striptracks_json | jq -crM .container)" + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + end_script 9 + fi + ;; + 1) striptracks_message=$(echo -e "[$striptracks_return] Warning when inspecting video.\nmkvmerge returned: $(echo "$striptracks_json" | jq -crM '.warnings[]')" | awk '{print "Warn|"$0}') + echo "$striptracks_message" | log + ;; + 2) striptracks_message=$(echo -e "[$striptracks_return] Error when inspecting video.\nmkvmerge returned: $(echo "$striptracks_json" | jq -crM '.errors[]')" | awk '{print "Error|"$0}') + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + end_script 9 + ;; + esac return $striptracks_return } # # Import new video into Radarr/Sonarr @@ -693,7 +716,7 @@ function get_mediainfo { # -d "$data" \ # "$url") # local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - # local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + # local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') # echo "$striptracks_message" | log # echo "$striptracks_message" >&2 # } @@ -717,7 +740,7 @@ function get_rename { -d "$data" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url&$data\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url&$data\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -742,7 +765,7 @@ function rename_video { -d "$data" \ "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -766,7 +789,7 @@ function set_radarr_language { -d "$data" \ -X PUT "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -790,7 +813,7 @@ function set_sonarr_language { -d "$data" \ -X PUT "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -811,23 +834,30 @@ function check_compat { [ ${striptracks_arr_version/.*/} -ge 3 ] && local striptracks_return=0 ;; languageprofile) + # Langauge Profiles [ "${striptracks_type,,}" = "sonarr" ] && [ ${striptracks_arr_version/.*/} -eq 3 ] && local striptracks_return=0 ;; customformat) + # Language option in Custom Formats [ "${striptracks_type,,}" = "radarr" ] && [ ${striptracks_arr_version/.*/} -ge 3 ] && local striptracks_return=0 [ "${striptracks_type,,}" = "sonarr" ] && [ ${striptracks_arr_version/.*/} -ge 4 ] && local striptracks_return=0 ;; originallanguage) + # Original language selection [ "${striptracks_type,,}" = "radarr" ] && [ ${striptracks_arr_version/.*/} -ge 3 ] && local striptracks_return=0 [ "${striptracks_type,,}" = "sonarr" ] && [ ${striptracks_arr_version/.*/} -ge 4 ] && local striptracks_return=0 ;; + qualitylanguage) + # Language option in Quality Profile + [ "${striptracks_type,,}" = "radarr" ] && [ ${striptracks_arr_version/.*/} -ge 3 ] && local striptracks_return=0 + ;; *) # Unknown feature local striptracks_message="Error|Unknown feature $1 in ${striptracks_type^}" echo "$striptracks_message" | log echo "$striptracks_message" >&2 ;; esac - [ $striptracks_debug -ge 1 ] && echo "Debug|Feature $1 is $([ $striptracks_return -eq 1 ] && echo "not ")compatible with ${striptracks_type^} v${striptracks_arr_version}." | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Feature $1 is $([ $striptracks_return -eq 1 ] && echo "not ")compatible with ${striptracks_type^} v${striptracks_arr_version}" | log return $striptracks_return } # Get media management configuration @@ -840,7 +870,7 @@ function get_media_config { -H "Accept: application/json" \ --get "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\"\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } @@ -855,10 +885,10 @@ function get_media_config { # Update file metadata in Radarr/Sonarr function set_video_info { local url="$striptracks_api_url/$striptracks_video_api/$striptracks_video_id" - local data="$(echo $striptracks_videoinfo | jq -crM .monitored='true')" + local data="$(echo $striptracks_videoinfo | jq -crM .monitored="$striptracks_videomonitored")" local i=0 for ((i=1; i <= 5; i++)); do - [ $striptracks_debug -ge 1 ] && echo "Debug|Updating monitored to 'true'. Calling ${striptracks_type^} API using PUT and URL '$url' with data $data" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Updating monitored to '$striptracks_videomonitored'. Calling ${striptracks_type^} API using PUT and URL '$url' with data $data" | log unset striptracks_result striptracks_result=$(curl -s --fail-with-body -H "X-Api-Key: $striptracks_apikey" \ -H "Content-Type: application/json" \ @@ -866,14 +896,14 @@ function set_video_info { -d "$data" \ -X PUT "$url") local striptracks_curlret=$?; [ $striptracks_curlret -ne 0 ] && { - local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcrM .message?)" | awk '{print "Error|"$0}') + local striptracks_message=$(echo -e "[$striptracks_curlret] curl error when calling: \"$url\" with data $data\nWeb server returned: $(echo $striptracks_result | jq -jcM .message?)" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 } [ $striptracks_debug -ge 2 ] && echo "Debug|API returned ${#striptracks_result} bytes." | log [ $striptracks_debug -ge 3 ] && echo "API returned: $striptracks_result" | awk '{print "Debug|"$0}' | log # Exit loop if database is not locked, else wait 1 minute - if [[ ! "$(echo $striptracks_result | jq -jcrM .message?)" =~ database\ is\ locked ]]; then + if [[ ! "$(echo $striptracks_result | jq -jcM .message?)" =~ database\ is\ locked ]]; then break else echo "Warn|Database is locked; system is likely overloaded. Sleeping 1 minute." | log @@ -887,6 +917,30 @@ function set_video_info { fi return $striptracks_return } +# Handle :org language code +function process_org_code { + local striptracks_track_type="$1" # 'audio' or 'subtitles' + local striptracks_keep_var="$2" # Variable name, e.g., striptracks_audiokeep or striptracks_subskeep + + if [[ "${!striptracks_keep_var}" =~ :org ]]; then + # Check compatibility + if [ "${striptracks_type,,}" = "batch" ]; then + local striptracks_message="Warn|${striptracks_track_type^} argument contains ':org' code, but this is undefined for Batch mode! Unexpected behavior may result." + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + elif ! check_compat originallanguage; then + local striptracks_message="Warn|${striptracks_track_type^} argument contains ':org' code, but this is undefined and not compatible with this mode/version! Unexpected behavior may result." + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + fi + + # Log debug message if applicable + [ "$striptracks_debug" -ge 1 ] && echo "Debug|${striptracks_track_type^} argument ':org' specified. Changing '${!striptracks_keep_var}' to '${!striptracks_keep_var//:org/${striptracks_originalLangCode}}'" | log + + # Replace :org with the original language code + declare -g "$striptracks_keep_var=${!striptracks_keep_var//:org/${striptracks_originalLangCode}}" + fi +} # Exit program function end_script { # Cool bash feature @@ -907,7 +961,7 @@ fi # Check that the log file exists if [ ! -f "$striptracks_log" ]; then echo "Info|Creating a new log file: $striptracks_log" - touch "$striptracks_log" 2>&1 + touch "$striptracks_log" fi # Check that the log file is writable @@ -977,7 +1031,7 @@ if [[ "${!striptracks_eventtype}" = "Test" ]]; then end_script 0 fi -# First normal log entry (when there are no errors) +# First normal log entry (when there are no errors) (see issue #61) # shellcheck disable=SC2046 striptracks_filesize=$(stat -c %s "${striptracks_video}" | numfmt --to iec --format "%.3f") striptracks_message="Info|${striptracks_type^} event: ${!striptracks_eventtype}, Video: $striptracks_video, Size: $striptracks_filesize" @@ -1019,10 +1073,10 @@ elif [ -f "$striptracks_arr_config" ]; then # Check for localhost [[ $striptracks_bindaddress = "*" ]] && striptracks_bindaddress=localhost - # Strip leading and trailing forward slashes from URL base + # Strip leading and trailing forward slashes from URL base (see issue #66) striptracks_urlbase="$(echo "$striptracks_urlbase" | sed -re 's/^\/+//; s/\/+$//')" - # Build URL to Radarr/Sonarr API + # Build URL to Radarr/Sonarr API (see issue #57) striptracks_api_url="http://$striptracks_bindaddress:$striptracks_port${striptracks_urlbase:+/$striptracks_urlbase}/api/v3" # Check Radarr/Sonarr version @@ -1054,7 +1108,7 @@ fi # Check if video file variable is blank if [ -z "$striptracks_video" ]; then - striptracks_message="Error|No video file detected! radarr_moviefile_path or sonarr_episodefile_path environment variable missing and -f option not specified on command line." + striptracks_message="Error|No video file found! radarr_moviefile_path or sonarr_episodefile_path environment variable missing and -f option not specified on command line." echo "$striptracks_message" | log echo "$striptracks_message" >&2 usage @@ -1063,7 +1117,7 @@ fi # Check if source video exists if [ ! -f "$striptracks_video" ]; then - striptracks_message="Error|Input file not found: \"$striptracks_video\"" + striptracks_message="Error|Input video file not found: \"$striptracks_video\"" echo "$striptracks_message" | log echo "$striptracks_message" >&2 end_script 5 @@ -1088,6 +1142,7 @@ elif [ -n "$striptracks_api_url" ]; then # Get video profile if get_video_info; then striptracks_videoinfo="$striptracks_result" + striptracks_videomonitored="$(echo "$striptracks_videoinfo" | jq -crM ".monitored")" # This is not strictly necessary as this is normally set in the environment. However, this is needed for testing scripts and it doesn't hurt to use the data returned by the API call. striptracks_videofile_id="$(echo $striptracks_videoinfo | jq -crM .${striptracks_json_quality_root}.id)" @@ -1101,14 +1156,14 @@ elif [ -n "$striptracks_api_url" ]; then # Save original metadata striptracks_original_metadata="$(echo $striptracks_videofile_info | jq -crM '{quality, releaseGroup}')" - [ $striptracks_debug -ge 1 ] && echo "Debug|Detected video file quality '$(echo $striptracks_original_metadata | jq -crM .quality.quality.name)' and release group '$(echo $striptracks_original_metadata | jq -crM '.releaseGroup | select(. != null)')'" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Found video file quality '$(echo $striptracks_original_metadata | jq -crM .quality.quality.name)' and release group '$(echo $striptracks_original_metadata | jq -crM '.releaseGroup | select(. != null)')'" | log # Get language name(s) from quality profile used by video striptracks_profileId="$(echo $striptracks_videoinfo | jq -crM ${striptracks_video_rootNode}.qualityProfileId)" striptracks_profileName="$(echo $striptracks_qualityProfiles | jq -crM ".[] | select(.id == $striptracks_profileId).name")" striptracks_profileLanguages="$(echo $striptracks_qualityProfiles | jq -cM "[.[] | select(.id == $striptracks_profileId) | .language]")" striptracks_languageSource="quality profile" - [ $striptracks_debug -ge 1 ] && echo "Debug|Detected quality profile '(${striptracks_profileId}) ${striptracks_profileName}' with language '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "(\(.id | tostring)) \(.name)"] | join(",")')'" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Found quality profile '${striptracks_profileName} (${striptracks_profileId})'$(check_compat qualitylanguage && echo " with language '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "\(.name) (\(.id | tostring))"] | join(",")')'")" | log # Query custom formats if returned language from quality profile is null or -1 (Any) if [ -z "$striptracks_profileLanguages" -o "$striptracks_profileLanguages" = "[null]" -o "$(echo $striptracks_profileLanguages | jq -crM '.[].id')" = "-1" ] && check_compat customformat; then @@ -1121,35 +1176,36 @@ elif [ -n "$striptracks_api_url" ]; then # Pick our languages by combining data from quality profile and custom format configuration. # I'm open to suggestions if there's a better way to get this list or selected languages. # Did I mention that JQ is crazy hard? - striptracks_qcf_langcodes=$(echo "$striptracks_qualityProfiles $striptracks_customFormats" | jq -s -crM " + striptracks_qcf_langcodes=$(echo "$striptracks_qualityProfiles $striptracks_customFormats" | jq -s -crM --argjson ProfileId $striptracks_profileId ' [ # This combines the custom formats [1] with the quality profiles [0], iterating over custom formats that # specify languages and evaluating the scoring from the selected quality profile. ( .[1] | .[] | - {id, specs: [.specifications[] | select(.implementation == \"LanguageSpecification\") | {langCode: .fields[] | select(.name == \"value\").value, negate, except: ((.fields[] | select(.name == \"exceptLanguage\").value) // false)}]} - ) as \$cf | + {id, specs: [.specifications[] | select(.implementation == "LanguageSpecification") | {langCode: .fields[] | select(.name == "value").value, negate, except: ((.fields[] | select(.name == "exceptLanguage").value) // false)}]} + ) as $CustomFormat | .[0] | .[] | - select(.id == $striptracks_profileId) | .formatItems[] | select(.format == \$cf.id) | - {format, name, score, specs: \$cf.specs} + select(.id == $ProfileId) | .formatItems[] | select(.format == $CustomFormat.id) | + {format, name, score, specs: $CustomFormat.specs} ] | [ # Only count languages with positive scores plus languages with negative scores that are negated, and # languages with negative scores that use Except .[] | - (select(.score > 0) | .specs[] | select(.negate == false and .except == false)), (select(.score < 0) | .specs[] | select(.negate == true and .except == false)), (select(.score < 0) | .specs[] | select(.negate == false and .except == true)) | + (select(.score > 0) | .specs[] | select(.negate == false and .except == false)), + (select(.score < 0) | .specs[] | select(.negate == true and .except == false)), + (select(.score < 0) | .specs[] | select(.negate == false and .except == true)) | .langCode ] | - unique | - join(\",\") - ") + unique | join(",") + ') [ $striptracks_debug -ge 2 ] && echo "Debug|Custom format language code(s) '$striptracks_qcf_langcodes' were selected based on quality profile scores." | log if [ -n "$striptracks_qcf_langcodes" ]; then # Convert the language codes into language code/name pairs striptracks_profileLanguages="$(echo $striptracks_lang_codes | jq -crM "map(select(.id | inside($striptracks_qcf_langcodes)) | {id, name})")" striptracks_languageSource="custom format" - [ $striptracks_debug -ge 1 ] && echo "Debug|Detected custom format language(s) '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "(\(.id | tostring)) \(.name)"] | join(",")')'" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Found custom format language(s) '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "\(.name) (\(.id | tostring))"] | join(",")')'" | log else [ $striptracks_debug -ge 1 ] && echo "Debug|None of the applied custom formats have language conditions with usable scores." | log fi @@ -1166,7 +1222,7 @@ elif [ -n "$striptracks_api_url" ]; then striptracks_profileName="$(echo $striptracks_languageProfiles | jq -crM ".[] | select(.id == $striptracks_profileId).name")" striptracks_profileLanguages="$(echo $striptracks_languageProfiles | jq -cM "[.[] | select(.id == $striptracks_profileId) | .languages[] | select(.allowed).language]")" striptracks_languageSource="language profile" - [ $striptracks_debug -ge 1 ] && echo "Debug|Detected language profile '(${striptracks_profileId}) ${striptracks_profileName}' with language(s) '$(echo $striptracks_profileLanguages | jq -crM '[.[].name] | join(",")')'" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Found language profile '(${striptracks_profileId}) ${striptracks_profileName}' with language(s) '$(echo $striptracks_profileLanguages | jq -crM '[.[].name] | join(",")')'" | log else # languageProfile API failed striptracks_message="Warn|The 'languageprofile' API returned an error." @@ -1185,7 +1241,7 @@ elif [ -n "$striptracks_api_url" ]; then else # Final determination of configured languages in profiles or custom formats striptracks_profileLangNames="$(echo $striptracks_profileLanguages | jq -crM '[.[].name]')" - [ $striptracks_debug -ge 1 ] && echo "Debug|Determined ${striptracks_type^} configured language(s) of '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "(\(.id | tostring)) \(.name)"] | join(",")')' from $striptracks_languageSource" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Determined ${striptracks_type^} configured language(s) of '$(echo $striptracks_profileLanguages | jq -crM '[.[] | "\(.name) (\(.id | tostring))"] | join(",")')' from $striptracks_languageSource" | log fi # Get originalLanguage of video @@ -1193,8 +1249,8 @@ elif [ -n "$striptracks_api_url" ]; then striptracks_originalLangName="$(echo $striptracks_videoinfo | jq -crM ${striptracks_video_rootNode}.originalLanguage.name)" # shellcheck disable=SC2090 - striptracks_originalLangCode="$(echo $striptracks_isocodemap | jq -jcrM ".languages[] | select(.language.name == \"$striptracks_originalLangName\") | .language | \":\(.\"iso639-2\"[])\"")" - [ $striptracks_debug -ge 1 ] && echo "Debug|Detected original video language of '$striptracks_originalLangName ($striptracks_originalLangCode)' from $striptracks_video_type '$striptracks_rescan_id'" | log + striptracks_originalLangCode="$(echo $striptracks_isocodemap | jq -jcM ".languages[] | select(.language.name == \"$striptracks_originalLangName\") | .language | \":\(.\"iso639-2\"[])\"")" + [ $striptracks_debug -ge 1 ] && echo "Debug|Found original video language of '$striptracks_originalLangName ($striptracks_originalLangCode)' from $striptracks_video_type '$striptracks_rescan_id'" | log fi # Map language names to ISO code(s) used by mkvmerge @@ -1205,7 +1261,7 @@ elif [ -n "$striptracks_api_url" ]; then striptracks_templang="$striptracks_originalLangName" fi # shellcheck disable=SC2090 - striptracks_profileLangCodes+="$(echo $striptracks_isocodemap | jq -jcrM ".languages[] | select(.language.name == \"$striptracks_templang\") | .language | \":\(.\"iso639-2\"[])\"")" + striptracks_profileLangCodes+="$(echo $striptracks_isocodemap | jq -jcM ".languages[] | select(.language.name == \"$striptracks_templang\") | .language | \":\(.\"iso639-2\"[])\"")" done [ $striptracks_debug -ge 1 ] && echo "Debug|Mapped $striptracks_languageSource language(s) '$(echo $striptracks_profileLangNames | jq -crM "join(\",\")")' to ISO639-2 code list '$striptracks_profileLangCodes'" | log else @@ -1246,7 +1302,6 @@ elif [ -n "$striptracks_api_url" ]; then striptracks_exitstatus=17 } if [ "$(echo "$striptracks_result" | jq -crM ".autoUnmonitorPreviouslyDownloaded${striptracks_video_api^}s")" = "true" ]; then - striptracks_conf_unmonitor=1 striptracks_message="Warn|Will compensate for ${striptracks_type^} configuration to unmonitor deleted ${striptracks_video_api}s." echo "$striptracks_message" | log fi @@ -1259,24 +1314,8 @@ else fi # Special handling for ':org' code from command line. -if [[ "$striptracks_audiokeep" =~ :org ]]; then - [ $striptracks_debug -ge 1 ] && echo "Debug|Command line ':org' code specified for audio. Changing '${striptracks_audiokeep}' to '${striptracks_audiokeep//:org/${striptracks_originalLangCode}}'" | log - striptracks_audiokeep="${striptracks_audiokeep//:org/${striptracks_originalLangCode}}" - if ! check_compat originallanguage; then - striptracks_message="Warn|:org code specified for audio, but this is undefined and not compatible with this mode/version! Unexpected behavior may result." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - fi -fi -if [[ "$striptracks_subskeep" =~ :org ]]; then - [ $striptracks_debug -ge 1 ] && echo "Debug|Command line ':org' specified for subtitles. Changing '${striptracks_subskeep}' to '${striptracks_subskeep//:org/${striptracks_originalLangCode}}'" | log - striptracks_subskeep="${striptracks_subskeep//:org/${striptracks_originalLangCode}}" - if [ "${striptracks_type,,}" = "batch" ]; then - striptracks_message="Warn|:org code specified for subtitles, but this is undefined for Batch mode! Unexpected behavior may result." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - fi -fi +process_org_code "audio" "striptracks_audiokeep" +process_org_code "subtitles" "striptracks_subskeep" # Final assignment of audio and subtitles selection ## Guard clause @@ -1295,7 +1334,7 @@ else [ $striptracks_debug -ge 1 ] && echo "Debug|Using command line audio languages '$striptracks_audiokeep'" | log fi -## Guard clause +## Log configuration that removes all subtitles if [ -z "$striptracks_subskeep" -a -z "$striptracks_profileLangCodes" ]; then striptracks_message="Info|No subtitles languages specified or detected. Removing all subtitles found." echo "$striptracks_message" | log @@ -1315,184 +1354,229 @@ echo "$striptracks_message" | log #### BEGIN MAIN # Read in the output of mkvmerge info extraction -if get_mediainfo "$striptracks_video"; then - # This and the modified AWK script are a hack, and I know it. JQ is crazy hard to learn, BTW. - # Mimic the mkvmerge --identify-verbose option that has been deprecated - striptracks_json_processed=$(echo $striptracks_json | jq -jcrM ' - ( if (.chapters[] | .num_entries) then - "Chapters: \(.chapters[] | .num_entries) entries\n" - else - empty - end - ), - ( .tracks[] | - ( "Track ID \(.id): \(.type) (\(.codec)) [", - ( [.properties | to_entries[] | "\(.key):\(.value | tostring | gsub(" "; "\\s"))"] | join(" ")), - "]\n" - ) - ) - ') - [ $striptracks_debug -ge 1 ] && echo "$striptracks_json_processed" | awk '{print "Debug|"$0}' | log -else - # Get media info failed - if [ "$(echo $striptracks_json | jq -crM '.container.supported')" = "false" ]; then - striptracks_message="Error|Container format '$(echo $striptracks_json | jq -crM .container.type)' is unsupported by mkvmerge. Unable to continue." - else - striptracks_message="Error|mkvmerge error. Unable to continue." - fi +# Populates the striptracks_json variable +get_mediainfo "$striptracks_video" + +# Process JSON data from MKVmerge; track selection logic +striptracks_json_processed=$(echo "$striptracks_json" | jq -jcM --arg AudioKeep "$striptracks_audiokeep" \ +--arg SubsKeep "$striptracks_subskeep" ' +# Parse input string into JSON language rules +def parse_language_codes(codes): + # Supports f, d, and number modifiers (see issues #82 and #86) + # -1 default value in language key means to keep unlimited tracks + # NOTE: Logic can result in duplicate keys, but jq just uses the last defined key + codes | split(":")[1:] | map(split("+") | {lang: .[0], mods: .[1]}) | + {languages: map( + # Select tracks with no modifiers or only numeric modifiers + (select(.mods == null) | {(.lang): -1}), + (select(.mods | test("^[0-9]+$")?) | {(.lang): .mods | tonumber}) + ) | add, + forced_languages: map( + # Select tracks with f modifier + select(.mods | contains("f")?) | {(.lang): ((.mods | scan("[0-9]+") | tonumber) // -1)} + ) | add, + default_languages: map( + # Select tracks with d modifier + select(.mods | contains("d")?) | {(.lang): ((.mods | scan("[0-9]+") | tonumber) // -1)} + ) | add + }; + +# Language rules for audio and subtitles, adding required audio tracks (see issue #54) +(parse_language_codes($AudioKeep) | .languages += {"mis":-1,"zxx":-1}) as $AudioRules | +parse_language_codes($SubsKeep) as $SubsRules | + +# Log chapter information +if (.chapters[0].num_entries) then + .striptracks_log = "Info|Chapters: \(.chapters[].num_entries)" +else . end | + +# Process tracks +reduce .tracks[] as $track ( + # Create object to hold tracks and counters for each reduce iteration + # This is what will be output at the end of the reduce loop + {"tracks": [], "counters": {"audio": {"normal": {}, "forced": {}, "default": {}}, "subtitles": {"normal": {}, "forced": {}, "default": {}}}}; + + # Set track language to "und" if null or empty + (if ($track.properties.language == "" or $track.properties.language == null) then "und" else $track.properties.language end) as $track_lang | + + # Initialize counters for each track type and language + .counters[$track.type].normal[$track_lang] = (.counters[$track.type].normal[$track_lang] // 0) | + if $track.properties.forced_track then .counters[$track.type].forced[$track_lang] = (.counters[$track.type].forced[$track_lang] // 0) else . end | + if $track.properties.default_track then .counters[$track.type].default[$track_lang] = (.counters[$track.type].default[$track_lang] // 0) else . end | + .counters[$track.type] as $track_counters | + + # Add tracks one at a time to output object above + .tracks += [ + $track | + .striptracks_debug_log = "Debug|Parsing track ID:\(.id) Type:\(.type) Name:\(.properties.track_name) Lang:\($track_lang) Codec:\(.codec) Default:\(.properties.default_track) Forced:\(.properties.forced_track)" | + + # Determine keep logic based on type and rules + if .type == "video" then + .striptracks_keep = true + elif .type == "audio" or .type == "subtitles" then + .striptracks_log = "\(.id): \($track_lang) (\(.codec))\(if .properties.track_name then " \"" + .properties.track_name + "\"" else "" end)" | + # Same logic for both audio and subtitles + (if .type == "audio" then $AudioRules else $SubsRules end) as $currentRules | + if ($currentRules.languages["any"] == -1 or ($track_counters.normal | add) < $currentRules.languages["any"] or + $currentRules.languages[$track_lang] == -1 or $track_counters.normal[$track_lang] < $currentRules.languages[$track_lang]) then + .striptracks_keep = true + elif (.properties.forced_track and + ($currentRules.forced_languages["any"] == -1 or ($track_counters.forced | add) < $currentRules.forced_languages["any"] or + $currentRules.forced_languages[$track_lang] == -1 or $track_counters.forced[$track_lang] < $currentRules.forced_languages[$track_lang])) then + .striptracks_keep = true | + .striptracks_rule = "forced" + elif (.properties.default_track and + ($currentRules.default_languages["any"] == -1 or ($track_counters.default | add) < $currentRules.default_languages["any"] or + $currentRules.default_languages[$track_lang] == -1 or $track_counters.default[$track_lang] < $currentRules.default_languages[$track_lang])) then + .striptracks_keep = true | + .striptracks_rule = "default" + else . end | + if .striptracks_keep then + .striptracks_log = "Info|Keeping \(if .striptracks_rule then .striptracks_rule + " " else "" end)\(.type) track " + .striptracks_log + else + .striptracks_keep = false + end + else . end + ] | + + # Increment counters for each track type and language + .counters[$track.type].normal[$track_lang] += + if .tracks[-1].striptracks_keep then + 1 + else 0 end | + .counters[$track.type].forced[$track_lang] += + if ($track.properties.forced_track and .tracks[-1].striptracks_keep) then + 1 + else 0 end | + .counters[$track.type].default[$track_lang] += + if ($track.properties.default_track and .tracks[-1].striptracks_keep) then + 1 + else 0 end +) | + +# Ensure at least one audio track is kept +if ((.tracks | map(select(.type == "audio")) | length == 1) and (.tracks | map(select(.type == "audio" and .striptracks_keep)) | length == 0)) then + # If there is only one audio track and none are kept, keep the only audio track + .tracks |= map(if .type == "audio" then + .striptracks_log = "Warn|No audio tracks matched! Keeping only audio track " + .striptracks_log | + .striptracks_keep = true + else . end) +elif (.tracks | map(select(.type == "audio" and .striptracks_keep)) | length == 0) then + # If no audio tracks are kept, first try to keep the default audio track + .tracks |= map(if .type == "audio" and .properties.default_track then + .striptracks_log = "Warn|No audio tracks matched! Keeping default audio track " + .striptracks_log | + .striptracks_keep = true + else . end) | + # If still no audio tracks are kept, keep the first audio track + if (.tracks | map(select(.type == "audio" and .striptracks_keep)) | length == 0) then + (first(.tracks[] | select(.type == "audio"))) |= . + + {striptracks_log: ("Warn|No audio tracks matched! Keeping first audio track " + .striptracks_log), + striptracks_keep: true} + else . end +else . end | + +# Output simplified dataset +{ striptracks_log, tracks: [ .tracks[] | { id, type, forced: .properties.forced_track, default: .properties.default_track, striptracks_debug_log, striptracks_log, striptracks_keep } ] } +') +[ $striptracks_debug -ge 1 ] && echo "Debug|Track processing returned ${#striptracks_json_processed} bytes." | log +[ $striptracks_debug -ge 2 ] && echo "Track processing returned: $(echo "$striptracks_json_processed" | jq)" | awk '{print "Debug|"$0}' | log + +# Write messages to log +echo "$striptracks_json_processed" | jq -crM --argjson Debug $striptracks_debug ' +# Join log messages into one line +def log_removed_tracks($type): + if (.tracks | map(select(.type == $type and .striptracks_keep == false)) | length > 0) then + "Info|Removing \($type) tracks: " + + (.tracks | map(select(.type == $type and .striptracks_keep == false) | .striptracks_log) | join(", ")) + else empty end; + +# Log the chapters, if any +.striptracks_log // empty, + +# Log debug messages +( .tracks[] | (if $Debug >= 1 then .striptracks_debug_log else empty end), + + # Log messages for kept tracks + (select(.striptracks_keep) | .striptracks_log // empty) +), + +# Log removed tracks +log_removed_tracks("audio"), +log_removed_tracks("subtitles"), + +# Summary of kept tracks +"Info|Kept tracks: \(.tracks | map(select(.striptracks_keep)) | length) " + +"(audio: \(.tracks | map(select(.type == "audio" and .striptracks_keep)) | length), " + +"subtitles: \(.tracks | map(select(.type == "subtitles" and .striptracks_keep)) | length))" +' | log + +# Check for no audio or subtitle tracks +if [ "$(echo "$striptracks_json_processed" | jq -crM '.tracks|map(select(.type=="audio" and .striptracks_keep))')" = "" ]; then + striptracks_message="Warn|Script encountered an error when determining audio tracks to keep and must close." echo "$striptracks_message" | log echo "$striptracks_message" >&2 - end_script 9 + end_script 11 fi -# Process video file -echo "$striptracks_json_processed" | awk -v Debug=$striptracks_debug \ --v Video="$striptracks_video" \ --v TempVideo="$striptracks_tempvideo" \ --v Title="$striptracks_title" \ --v AudioKeep="$striptracks_audiokeep" \ --v SubsKeep="$striptracks_subskeep" ' -# Exit codes: 0 success; 1 No tracks in source file; 2 No tracks removed; 3 How did we get here? -# Array join function, based on GNU docs -function join(array, sep, i, ret) { - for (i in array) - if (ret == "") - ret = array[i] - else - ret = ret sep array[i] - return ret -} -BEGIN { - MKVMerge = "/usr/bin/mkvmerge" - FS = "[\t\n: ]" - IGNORECASE = 1 - split("", AudioCommand) - split("", SubsCommand) - split("", AudRmvLog) - split("", SubsRmvLog) -} -/^Track ID/ { - FieldCount = split($0, Fields) - if (Fields[1] == "Track") { - NoTr++ - Track[NoTr, "id"] = Fields[3] - Track[NoTr, "typ"] = Fields[5] - # This is inelegant and I know it - # Finds the codec in parenthesis - if (Fields[6] ~ /^\(/) { - for (i = 6; i <= FieldCount; i++) { - Track[NoTr, "codec"] = Track[NoTr, "codec"]" "Fields[i] - if (match(Fields[i], /\)$/)) - break - } - sub(/^ /, "", Track[NoTr, "codec"]) +# All tracks matched/no tracks removed (see issues #49 and #89) +if [ "$(echo "$striptracks_json" | jq -crM '.tracks|map(select(.type=="audio" or .type=="subtitles"))|length')" = "$(echo "$striptracks_json_processed" | jq -crM '.tracks|map(select((.type=="audio" or .type=="subtitles") and .striptracks_keep))|length')" ]; then + [ $striptracks_debug -ge 1 ] && echo "Debug|No tracks will be removed from video \"$striptracks_video\"" | log + # Check if already MKV + if [[ $striptracks_video == *.mkv ]]; then + # Remuxing not performed + striptracks_message="Info|No tracks would be removed from video. Setting Title only and exiting." + echo "$striptracks_message" | log + striptracks_mkvcommand="/usr/bin/mkvpropedit -q --edit info --set \"title=$striptracks_title\" \"$striptracks_video\"" + [ $striptracks_debug -ge 1 ] && echo "Debug|Executing: $striptracks_mkvcommand" | log + striptracks_result=$(eval $striptracks_mkvcommand) + striptracks_return=$?; [ $striptracks_return -ne 0 ] && { + striptracks_message=$(echo -e "[$striptracks_return] Error when setting video title: \"$striptracks_tempvideo\"\nmkvpropedit returned: $striptracks_result" | awk '{print "Error|"$0}') + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + striptracks_exitstatus=13 } - if (Track[NoTr, "typ"] == "video") VidCnt++ - if (Track[NoTr, "typ"] == "audio") AudCnt++ - if (Track[NoTr, "typ"] == "subtitles") SubsCnt++ - for (i = 6; i <= FieldCount; i++) { - if (Fields[i] == "language") - Track[NoTr, "lang"] = Fields[++i] - } - if (Track[NoTr, "lang"] == "") - Track[NoTr, "lang"] = "und" - } -} -/^Chapters/ { - Chapters = $3 -} -END { - # Source video had no tracks - if (!NoTr) { - exit 1 - } - if (!AudCnt) AudCnt=0; if (!SubsCnt) SubsCnt=0 - print "Info|Original tracks: "NoTr" (audio: "AudCnt", subtitles: "SubsCnt")" - if (Chapters) print "Info|Chapters: "Chapters - for (i = 1; i <= NoTr; i++) { - if (Debug >= 2) print "Debug|Parsed: Track ID:"Track[i,"id"],"Type:"Track[i,"typ"],"Lang:"Track[i, "lang"],"Codec:"Track[i, "codec"] - if (Track[i, "typ"] == "audio") { - # Keep track if it matches command line selection, or if it is matches pseudo code ":any" - if (AudioKeep ~ Track[i, "lang"] || AudioKeep ~ ":any") { - print "Info|Keeping audio track "Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - AudioCommand[i] = Track[i, "id"] - # Special case if there is only one audio track, even if it was not selected - } else if (AudCnt == 1) { - print "Warn|No audio tracks matched! Keeping only audio track "Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - AudioCommand[i] = Track[i, "id"] - # Special case if there were multiple tracks, none were selected, and this is the last one. - } else if (length(AudioCommand) == 0 && Track[i, "id"] == AudCnt) { - print "Warn|No audio tracks matched! Keeping last audio track "Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - AudioCommand[i] = Track[i, "id"] - # Special case for mis and zxx - } else if (":mis:zxx" ~ Track[i, "lang"]) { - print "Info|Keeping special audio track "Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - AudioCommand[i] = Track[i, "id"] - } else - AudRmvLog[i] = Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - } else { - if (Track[i, "typ"] == "subtitles") { - if (SubsKeep ~ Track[i, "lang"] || SubsKeep ~ ":any") { - print "Info|Keeping subtitles track "Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - SubsCommand[i] = Track[i, "id"] - } else - SubsRmvLog[i] = Track[i, "id"]": "Track[i, "lang"]" "Track[i, "codec"] - } - } - } - if (length(AudRmvLog) != 0) print "Info|Removed audio tracks: " join(AudRmvLog, ",") - if (length(SubsRmvLog) != 0) print "Info|Removed subtitles tracks: " join(SubsRmvLog, ",") - print "Info|Kept tracks: "length(AudioCommand)+length(SubsCommand)+VidCnt" (audio: "length(AudioCommand)", subtitles: "length(SubsCommand)")" - # All tracks matched/no tracks removed. - if (length(AudioCommand)+length(SubsCommand)+VidCnt == NoTr) { - if (Debug >= 1) print "Debug|No tracks will be removed from video \""Video"\"" - # Only skip remux if already MKV. - if (match(Video, /\.mkv$/)) { - exit 2 - } - if (Debug >= 1) print "Debug|Source video is not MKV. Remuxing anyway." - } - # This should never happen, but belt and suspenders - if (length(AudioCommand) == 0) { - print "Warn|Script encountered an error when determining audio tracks to keep and must close." - exit 3 - } - CommandLine = "-a " join(AudioCommand, ",") - if (length(SubsCommand) == 0) - CommandLine = CommandLine" -S" + end_script else - CommandLine = CommandLine" -s " join(SubsCommand, ",") - if (Debug >= 1) print "Debug|Executing: nice "MKVMerge" --title \""Title"\" -q -o \""TempVideo"\" "CommandLine" \""Video"\"" - Result = system("nice "MKVMerge" --title \""Title"\" -q -o \""TempVideo"\" "CommandLine" \""Video"\"") - if (Result > 1) print "Error|["Result"] remuxing \""Video"\"" > "/dev/stderr" -}' | log -#### END MAIN + [ $striptracks_debug -ge 1 ] && echo "Debug|Source video is not MKV. Remuxing anyway." | log + fi +fi -# Check awk exit code -striptracks_return="${PIPESTATUS[1]}" -[ $striptracks_debug -ge 2 ] && echo "Debug|awk exited with code: $striptracks_return" | log -[ $striptracks_return -ne 0 ] && { - case "$striptracks_return" in - 1) # Source video had no tracks - striptracks_message="Error|The original video \"$striptracks_video\" had no audio or subtitle tracks!" - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - end_script 11 - ;; - 2) # All tracks matched/no tracks removed and already MKV. Remuxing not performed. - striptracks_message="Info|No tracks would be removed from video. Setting Title only and exiting." - echo "$striptracks_message" | log - [ $striptracks_debug -ge 1 ] && echo "Debug|Executing: /usr/bin/mkvpropedit -q --edit info --set \"title=$striptracks_title\" \"$striptracks_video\"" | log - /usr/bin/mkvpropedit -q --edit info --set "title=$striptracks_title" "$striptracks_video" 2>&1 | log - end_script 0 - ;; - *) striptracks_message="Error|[$striptracks_return] Script exited abnormally." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - end_script 13 - ;; - esac -} +# Test for hardlinked file (see issue #85) +striptracks_refcount=$(stat -c %h "$striptracks_video") +[ $striptracks_debug -ge 1 ] && echo "Debug|Input file has a hard link count of $striptracks_refcount" | log +if [ "$striptracks_refcount" != "1" ]; then + striptracks_message="Warn|Input video file is a hardlink and this will be broken by remuxing." + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 +fi + +# Build argument with kept audio tracks for MKVmerge +striptracks_audioarg=$(echo "$striptracks_json_processed" | jq -crM '.tracks | map(select(.type == "audio" and .striptracks_keep) | .id) | join(",")') +striptracks_audioarg="-a $striptracks_audioarg" + +# Build argument with kept subtitles tracks for MKVmerge, or remove all subtitles +striptracks_subsarg=$(echo "$striptracks_json_processed" | jq -crM '.tracks | map(select(.type == "subtitles" and .striptracks_keep) | .id) | join(",")') +if [ ${#striptracks_subsarg} -ne 0 ]; then + striptracks_subsarg="-s $striptracks_subsarg" +else + striptracks_subsarg="-S" +fi + +# Execute MKVmerge (remux then rename, see issue #46) +striptracks_mkvcommand="nice /usr/bin/mkvmerge --title \"$striptracks_title\" -q -o \"$striptracks_tempvideo\" $striptracks_audioarg $striptracks_subsarg \"$striptracks_video\"" +[ $striptracks_debug -ge 1 ] && echo "Debug|Executing: $striptracks_mkvcommand" | log +striptracks_result=$(eval $striptracks_mkvcommand) +striptracks_return=$? +case $striptracks_return in + 1) striptracks_message=$(echo -e "[$striptracks_return] Warning when remuxing video: \"$striptracks_video\"\nmkvmerge returned: $striptracks_result" | awk '{print "Warn|"$0}') + echo "$striptracks_message" | log + ;; + 2) striptracks_message=$(echo -e "[$striptracks_return] Error when remuxing video: \"$striptracks_video\"\nmkvmerge returned: $striptracks_result" | awk '{print "Error|"$0}') + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + end_script 13 + ;; +esac # Check for non-empty file if [ ! -s "$striptracks_tempvideo" ]; then @@ -1506,9 +1590,9 @@ fi if [ "$(id -u)" -eq 0 ]; then # Set owner [ $striptracks_debug -ge 1 ] && echo "Debug|Changing owner of file \"$striptracks_tempvideo\"" | log - chown --reference="$striptracks_video" "$striptracks_tempvideo" >&2 + striptracks_result=$(chown --reference="$striptracks_video" "$striptracks_tempvideo") striptracks_return=$?; [ $striptracks_return -ne 0 ] && { - striptracks_message="Error|[$striptracks_return] Error when changing owner of file: \"$striptracks_tempvideo\"" + striptracks_message=$(echo -e "[$striptracks_return] Error when changing owner of file: \"$striptracks_tempvideo\"\nchown returned: $striptracks_result" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 striptracks_exitstatus=15 @@ -1518,9 +1602,9 @@ else [ $striptracks_debug -ge 1 ] && echo "Debug|Unable to change owner of file when running as user '$(id -un)'" | log fi # Set permissions -chmod --reference="$striptracks_video" "$striptracks_tempvideo" >&2 +striptracks_result=$(chmod --reference="$striptracks_video" "$striptracks_tempvideo") striptracks_return=$?; [ $striptracks_return -ne 0 ] && { - striptracks_message="Error|[$striptracks_return] Error when changing permissions of file: \"$striptracks_tempvideo\"" + striptracks_message=$(echo -e "[$striptracks_return] Error when changing permissions of file: \"$striptracks_tempvideo\"\nchmod returned: $striptracks_result" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 striptracks_exitstatus=15 @@ -1529,9 +1613,9 @@ striptracks_return=$?; [ $striptracks_return -ne 0 ] && { # Just delete the original video if running in batch mode if [ "$striptracks_type" = "batch" ]; then [ $striptracks_debug -ge 1 ] && echo "Debug|Deleting: \"$striptracks_video\"" | log - rm "$striptracks_video" 2>&1 | log + striptracks_result=$(rm "$striptracks_video") striptracks_return=$?; [ $striptracks_return -ne 0 ] && { - striptracks_message="Error|[$striptracks_return] Error when deleting video: \"$striptracks_video\"" + striptracks_message=$(echo -e "[$striptracks_return] Error when deleting video: \"$striptracks_video\"\nrm returned: $striptracks_result" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 striptracks_exitstatus=16 @@ -1547,7 +1631,7 @@ else } fi -# Another check for the temporary file, to make sure it wasn't deleted +# Another check for the temporary file, to make sure it wasn't deleted (see issue #65) if [ ! -f "$striptracks_tempvideo" ]; then striptracks_message="Error|${striptracks_type^} deleted the temporary remuxed file: \"$striptracks_tempvideo\". Halting." echo "$striptracks_message" | log @@ -1557,18 +1641,20 @@ fi # Rename the temporary video file to MKV [ $striptracks_debug -ge 1 ] && echo "Debug|Renaming \"$striptracks_tempvideo\" to \"$striptracks_newvideo\"" | log -mv -f "$striptracks_tempvideo" "$striptracks_newvideo" 2>&1 | log +striptracks_result=$(mv -f "$striptracks_tempvideo" "$striptracks_newvideo") striptracks_return=$?; [ $striptracks_return -ne 0 ] && { - striptracks_message="Error|[$striptracks_return] Unable to rename temp video: \"$striptracks_tempvideo\" to: \"$striptracks_newvideo\". Halting." + striptracks_message=$(echo -e "[$striptracks_return] Unable to rename temp video: \"$striptracks_tempvideo\" to: \"$striptracks_newvideo\". Halting.\nmv returned: $striptracks_result" | awk '{print "Error|"$0}') echo "$striptracks_message" | log echo "$striptracks_message" >&2 end_script 6 } +# Get file size (see issue #61) # shellcheck disable=SC2046 striptracks_filesize=$(stat -c %s "${striptracks_newvideo}" | numfmt --to iec --format "%.3f") striptracks_message="Info|New size: $striptracks_filesize" echo "$striptracks_message" | log +#### END MAIN #### Call Radarr/Sonarr API to RescanMovie/RescanSeries # Check for URL @@ -1583,7 +1669,7 @@ elif [ -n "$striptracks_api_url" ]; then # if get_import_info; then # # Build JSON data # [ $striptracks_debug -ge 1 ] && echo "Debug|Building JSON data to import" | log - # striptracks_json=$(echo $striptracks_result | jq -jcrM " + # striptracks_json=$(echo $striptracks_result | jq -jcM " # map( # select(.path == \"$striptracks_newvideo\") | # {path, folderName, \"${striptracks_video_type}Id\":.${striptracks_video_type}.id,${striptracks_sonarr_json} quality, $striptracks_language_node} @@ -1632,11 +1718,11 @@ elif [ -n "$striptracks_api_url" ]; then striptracks_videofile_id="$(echo $striptracks_videoinfo | jq -crM .${striptracks_json_quality_root}.id)" [ $striptracks_debug -ge 1 ] && echo "Debug|Using new video file id '$striptracks_videofile_id'" | log - # Check if video is unmonitored after the delete/import - if [ ${striptracks_conf_unmonitor:-0} -eq 1 -a "$(echo "$striptracks_videoinfo" | jq -crM ".monitored")" = "false" ]; then - striptracks_message="Warn|'$striptracks_title' is unmonitored after deleting the original video. Compensating for ${striptracks_type^} configuration." + # Check if video monitored status changed after the delete/import (see issues #87 and #90) + if [ "$(echo "$striptracks_videoinfo" | jq -crM ".monitored")" != "$striptracks_videomonitored" ]; then + striptracks_message="Warn|Video monitor status changed after deleting the original. Setting it back to '$striptracks_videomonitored'" echo "$striptracks_message" | log - # Set video to monitored again + # Set video monitor state set_video_info fi @@ -1666,76 +1752,70 @@ elif [ -n "$striptracks_api_url" ]; then # If we stripped out other languages, remove them # Only works in Radarr and Sonarr v4 (no per-episode edit function in Sonarr v3) [ $striptracks_debug -ge 1 ] && echo "Debug|Getting languages in new video file \"$striptracks_newvideo\"" | log - if get_mediainfo "$striptracks_newvideo"; then - # Build array of full name languages - striptracks_newvideo_langcodes="$(echo $striptracks_json | jq -crM '.tracks[] | select(.type == "audio") | .properties.language')" - unset striptracks_newvideo_languages - for i in $striptracks_newvideo_langcodes; do - # shellcheck disable=SC2090 - # Exclude Any, Original, and Unknown - striptracks_newvideo_languages+="$(echo $striptracks_isocodemap | jq -crM ".languages[] | .language | select((.\"iso639-2\"[]) == \"$i\") | select(.name != \"Any\" and .name != \"Original\" and .name != \"Unknown\").name")" - done - if [ -n "$striptracks_newvideo_languages" ]; then - # Covert to standard JSON - striptracks_json_languages="$(echo $striptracks_lang_codes | jq -crM "map(select(.name | inside(\"$striptracks_newvideo_languages\")) | {id, name})")" - - # Check languages for Radarr and Sonarr v4 - # Sooooo glad I did it this way - if [ "$(echo $striptracks_videofile_info | jq -crM .languages)" != "null" ]; then - if [ "$(echo $striptracks_videofile_info | jq -crM .languages)" != "$striptracks_json_languages" ]; then - if set_radarr_language; then - striptracks_exitstatus=0 - else - striptracks_message="Error|${striptracks_type^} error when updating video language(s)." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - striptracks_exitstatus=17 - fi + get_mediainfo "$striptracks_newvideo" + + # Build array of full name languages + striptracks_newvideo_langcodes="$(echo $striptracks_json | jq -crM '.tracks[] | select(.type == "audio") | .properties.language')" + unset striptracks_newvideo_languages + for i in $striptracks_newvideo_langcodes; do + # shellcheck disable=SC2090 + # Exclude Any, Original, and Unknown + striptracks_newvideo_languages+="$(echo $striptracks_isocodemap | jq -crM ".languages[] | .language | select((.\"iso639-2\"[]) == \"$i\") | select(.name != \"Any\" and .name != \"Original\" and .name != \"Unknown\").name")" + done + if [ -n "$striptracks_newvideo_languages" ]; then + # Covert to standard JSON + striptracks_json_languages="$(echo $striptracks_lang_codes | jq -crM "map(select(.name | inside(\"$striptracks_newvideo_languages\")) | {id, name})")" + + # Check languages for Radarr and Sonarr v4 + # Sooooo glad I did it this way + if [ "$(echo $striptracks_videofile_info | jq -crM .languages)" != "null" ]; then + if [ "$(echo $striptracks_videofile_info | jq -crM .languages)" != "$striptracks_json_languages" ]; then + if set_radarr_language; then + striptracks_exitstatus=0 else - # The languages are already correct - [ $striptracks_debug -ge 1 ] && echo "Debug|Language(s) '$(echo $striptracks_json_languages | jq -crM "[.[].name] | join(\",\")")' remained unchanged." | log - fi - # Check languages for Sonarr v3 and earlier - elif [ "$(echo $striptracks_videofile_info | jq -crM .language)" != "null" ]; then - if [ "$(echo $striptracks_videofile_info | jq -crM .language)" != "$(echo $striptracks_json_languages | jq -crM '.[0]')" ]; then - if set_sonarr_language; then - striptracks_exitstatus=0 - else - striptracks_message="Error|${striptracks_type^} error when updating video language(s)." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - striptracks_exitstatus=17 - fi - else - # The languages are already correct - [ $striptracks_debug -ge 1 ] && echo "Debug|Language '$(echo $striptracks_json_languages | jq -crM ".[0].name")' remained unchanged." | log + striptracks_message="Error|${striptracks_type^} error when updating video language(s)." + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + striptracks_exitstatus=17 fi else - # Some unknown JSON formatting - striptracks_message="Warn|The '$striptracks_videofile_api' API returned unknown JSON language node." - echo "$striptracks_message" | log - echo "$striptracks_message" >&2 - striptracks_exitstatus=20 + # The languages are already correct + [ $striptracks_debug -ge 1 ] && echo "Debug|Language(s) '$(echo $striptracks_json_languages | jq -crM "[.[].name] | join(\",\")")' remained unchanged." | log + fi + # Check languages for Sonarr v3 and earlier + elif [ "$(echo $striptracks_videofile_info | jq -crM .language)" != "null" ]; then + if [ "$(echo $striptracks_videofile_info | jq -crM .language)" != "$(echo $striptracks_json_languages | jq -crM '.[0]')" ]; then + if set_sonarr_language; then + striptracks_exitstatus=0 + else + striptracks_message="Error|${striptracks_type^} error when updating video language(s)." + echo "$striptracks_message" | log + echo "$striptracks_message" >&2 + striptracks_exitstatus=17 + fi + else + # The languages are already correct + [ $striptracks_debug -ge 1 ] && echo "Debug|Language '$(echo $striptracks_json_languages | jq -crM ".[0].name")' remained unchanged." | log fi - elif [ "$striptracks_newvideo_langcodes" = "und" ]; then - # Only language detected is Unknown - echo "Warn|The only language in the video file was Unknown (und). Not updating ${striptracks_type^} database." | log else - # Video language not in striptracks_isocodemap - striptracks_message="Warn|Video language code(s) '${striptracks_newvideo_langcodes//$'\n'/,}' not found in the ISO Codemap. Cannot evaluate." + # Some unknown JSON formatting + striptracks_message="Warn|The '$striptracks_videofile_api' API returned unknown JSON language node." echo "$striptracks_message" | log echo "$striptracks_message" >&2 striptracks_exitstatus=20 fi + elif [ "$striptracks_newvideo_langcodes" = "und" ]; then + # Only language detected is Unknown + echo "Warn|The only audio language in the video file was 'Unknown (und)'. Not updating ${striptracks_type^} database." | log else - # Get media info failed - striptracks_message="Error|Could not get media info from new video file. Can't check resulting languages." + # Video language not in striptracks_isocodemap + striptracks_message="Warn|Video language code(s) '${striptracks_newvideo_langcodes//$'\n'/,}' not found in the ISO Codemap. Cannot evaluate." echo "$striptracks_message" | log echo "$striptracks_message" >&2 - striptracks_exitstatus=9 + striptracks_exitstatus=20 fi - # Get list of videos that could be renamed + # Get list of videos that could be renamed (see issue #50) get_rename striptracks_return=$?; [ $striptracks_return -ne 0 ] && { striptracks_message="Warn|[$striptracks_return] ${striptracks_type^} error when getting list of videos to rename." From 29c8d679484721b3286fa5b96a701815fa0a3880 Mon Sep 17 00:00:00 2001 From: TheCaptain989 Date: Sun, 9 Feb 2025 10:47:29 -0600 Subject: [PATCH 2/4] Updated Build version --- .github/workflows/BuildImage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/BuildImage.yml b/.github/workflows/BuildImage.yml index 7ff8728..ba978a3 100644 --- a/.github/workflows/BuildImage.yml +++ b/.github/workflows/BuildImage.yml @@ -22,7 +22,7 @@ jobs: echo "MODNAME=${{ env.MODNAME }}" >> $GITHUB_OUTPUT echo "MULTI_ARCH=${{ env.MULTI_ARCH }}" >> $GITHUB_OUTPUT # **** If the mod needs to be versioned, set the versioning logic below. Otherwise leave as is. **** - MOD_VERSION="2.9.0" + MOD_VERSION="2.12.0" echo "MOD_VERSION=${MOD_VERSION}" >> $GITHUB_OUTPUT outputs: GITHUB_REPO: ${{ steps.outputs.outputs.GITHUB_REPO }} From 2395da4c364680cfee6835f06be7e9b72d6802a8 Mon Sep 17 00:00:00 2001 From: TheCaptain989 Date: Sat, 8 Mar 2025 10:46:29 -0600 Subject: [PATCH 3/4] Release 2.13..0 --- README.md | 39 ++++++------ SECURITY.md | 4 +- root/usr/local/bin/striptracks.sh | 99 +++++++++++++++++++++++++------ 3 files changed, 103 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index e0751be..4003d32 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # About -A [Docker Mod](https://github.com/linuxserver/docker-mods) for the LinuxServer.io Radarr/Sonarr v3 or higher Docker containers that adds a script to automatically strip out unwanted audio and subtitle tracks, keeping only the desired languages. +A [Docker Mod](https://github.com/linuxserver/docker-mods) for the LinuxServer.io Radarr/Sonarr v3 or higher Docker containers that adds a script to automatically strip out unwanted audio and subtitle tracks, keeping only the desired languages. A [Batch Mode](#batch-mode) is also supported that allows usage outside of Radarr/Sonarr. **This unified script works in both Radarr and Sonarr. Use this mod in either container!** @@ -85,7 +85,7 @@ Development Container info: The script will detect the language(s) defined in Radarr/Sonarr for the movie or TV show and only keep the audio and subtitles selected. - Alternatively, a wrapper script or an environment variable may be used to more granularly define which tracks to keep. See [Wrapper Scripts](./README.md#wrapper-scripts) or [Environment Variable](./README.md#environment-variable) for more details. + Alternatively, a wrapper script or an environment variable may be used to more granularly define which tracks to keep. See [Wrapper Scripts](#wrapper-scripts) or [Environment Variable](#environment-variable) for more details. > [!IMPORTANT] > You **must** configure language(s) in Radarr/Sonarr *or* pass command-line arguments for the script to do anything! See the next section for an example. @@ -127,7 +127,7 @@ The following is a simplified example and steps to configure Radarr so the scrip Now, when Radarr imports a movie with the 'Any' Quality Profile, the script will keep only Original and English languages. This is equivalent to calling the script with `--audio :org:eng --subs :org:eng` command-line arguments. -See [Automatic Language Detection](./README.md#automatic-language-detection) for more details. +See [Automatic Language Detection](#automatic-language-detection) for more details. # Usage Details The source video can be any mkvtoolnix supported video format. The output is an MKV file with the same name and the same permissions. Owner is preserved if the script is executed as root. @@ -157,15 +157,15 @@ Both audio **and** subtitle tracks that match the configured language(s) are kep ### Special Language Selections The language selection **'Original'** will use the language Radarr pulled from [The Movie Database](https://www.themoviedb.org/ "TMDB") or that Sonarr pulled from [The TVDB](https://www.thetvdb.com/ "TVDB") during its last refresh. -Selecting this language is functionally equivalent to calling the script with `--audio :org --subs :org` command-line arguments. See [Original language code](./README.md#original-language-code) below for more details. +Selecting this language is functionally equivalent to calling the script with `--audio :org --subs :org` command-line arguments. See [Original language code](#original-language-code) below for more details. The language selection **'Unknown'** will match tracks with **no configured language** in the video file. Selecting this language is functionally equivalent to calling the script with `--audio :und --subs :und` command-line arguments. -See [Unknown language code](./README.md#unknown-language-code) below for more details. +See [Unknown language code](#unknown-language-code) below for more details. The language selection **'Any'** has two purposes: 1) In Radarr only, when set on a Quality Profile, it will trigger a search of languages in ***Custom Formats*** 2) If languages are not configured in a Custom Format, or if you're using Sonarr, it will preserve **all languages** in the video file. This is functionally equivalent to calling the script with `--audio :any --subs :any` command-line arguments. - See [Any language code](./README.md#any-language-code) below for more details. + See [Any language code](#any-language-code) below for more details. > [!IMPORTANT] > When using *Custom Formats* language conditions and scoring you may not get the results you expect. @@ -210,16 +210,17 @@ All language conditions with positive scores *and* Negated conditions with negat The script also supports command-line arguments that will override the automatic language detection. More granular control can therefore be exerted or extended using tagging and defining multiple *Connect* scripts (this is native Radarr/Sonarr functionality outside the scope of this documentation). The syntax for the command-line is: -`striptracks.sh [{-a|--audio} [{-s|--subs} ] [{-f|--file} ]] [{-l|--log} ] [{-c|--config} ] [{-d|--debug} []]` +`striptracks.sh [{-a|--audio} [{-s|--subs} ] [--reorder] [{-f|--file} ]] [{-l|--log} ] [{-c|--config} ] [{-d|--debug} []]`
Table of Command-Line Arguments Option|Argument|Description ---|---|--- -`-a`, `--audio`|``|Audio languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`)
Each code may optionally be followed by a plus (`+`) and one or more [modifiers](./README.md#language-code-modifiers). +`-a`, `--audio`|``|Audio languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`)
Each code may optionally be followed by a plus (`+`) and one or more [modifiers](#language-code-modifiers). `-s`, `--subs`|``|Subtitle languages to keep
ISO 639-2 code(s) prefixed with a colon (`:`)
Each code may optionally be followed by a plus (`+`) and one or more modifiers. -`-f`, `--file`|``|If included, the script enters **[Batch Mode](./README.md#batch-mode)** and converts the specified video file.
Requires the `-a` option.
![notes] **Do not** use this argument when called from Radarr or Sonarr! +`--reorder`| |Reorder audio and subtitles tracks to match the language code order specified in the `` and `` arguments.
This is skipped if no tracks are removed. +`-f`, `--file`|``|If included, the script enters **[Batch Mode](#batch-mode)** and converts the specified video file.
Requires the `--audio` option.
![notes] **Do not** use this argument when called from Radarr or Sonarr! `-l`, `--log`|``|The log filename
Default is `/config/log/striptracks.txt` `-c`, `--config`|``|Radarr/Sonarr XML configuration file
Default is `/config/config.xml` `-d`, `--debug`|`[]`|Enables debug logging. Level is optional.
Default is `1` (low)
`2` includes JSON output
`3` contains even more JSON output @@ -228,7 +229,7 @@ Option|Argument|Description
-The `` and `` are optional arguments that are colon (`:`) prepended language codes in [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes "List of ISO 639-2 codes") format. +The `` and `` arguments are colon (`:`) prepended language codes in [ISO 639-2](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes "List of ISO 639-2 codes") format. For example: * `:eng` @@ -237,7 +238,7 @@ For example: ...etc. -Multiple codes may be concatenated, such as `:eng:spa` for both English and Spanish. Order is unimportant. +Multiple codes may be concatenated, such as `:eng:spa` for both English and Spanish. Order is unimportant, unless the `--reorder` option is also specified. > [!WARNING] > If no subtitle language is detected via Radarr/Sonarr configuration or specified on the command-line, all subtitles are removed. @@ -268,7 +269,7 @@ The `:any` language code is a special code. When used, the script will preserve ### Original language code The `:org` language code is a special code. When used, instead of retaining a specific language, the script substitutes the original movie or TV show language as specified in its [The Movie Database](https://www.themoviedb.org/ "TMDB") or [The TVDB](https://www.thetvdb.com/ "TVDB") entry. As an example, when importing "*Amores Perros (2000)*" with options `--audio :org:eng`, the Spanish and English audio tracks are preserved. -Several [Included Wrapper Scripts](./README.md#included-wrapper-scripts) use this special code. +Several [Included Wrapper Scripts](#included-wrapper-scripts) use this special code. > [!NOTE] > This feature relies on the 'originalLanguage' field in the Radarr/Sonarr database. The `:org` code is therefore invalid when used in Batch Mode. @@ -314,7 +315,7 @@ There is no way to force the script to remove audio tracks with these codes. ## Wrapper Scripts -To supply arguments to the script, you must either use one of the included wrapper scripts, create a custom wrapper script, or set the `STRIPTRACKS_ARGS` [environment variable](./README.md#environment-variable). +To supply arguments to the script, you must either use one of the included wrapper scripts, create a custom wrapper script, or set the `STRIPTRACKS_ARGS` [environment variable](#environment-variable). > [!TIP] > If you followed the Linuxserver.io recommendations when configuring your container, the `/config` directory will be mapped to an external storage location. @@ -322,7 +323,7 @@ To supply arguments to the script, you must either use one of the included wrapp ### Included Wrapper Scripts For your convenience, several wrapper scripts are included in the `/usr/local/bin/` directory. -You may use any of these in place of `striptracks.sh` mentioned in the [Installation](./README.md#installation) section above. +You may use any of these in place of `striptracks.sh` mentioned in the [Installation](#installation) section above.
List of scripts @@ -350,7 +351,7 @@ striptracks-org-spa.sh # Keep Original, Spanish, and Unknown audio, and Orig
Example Script -To configure an entry from the [Examples](./README.md#examples) section above, create and save a file called `striptracks-custom.sh` to `/config` containing the following text: +To configure an entry from the [Examples](#examples) section above, create and save a file called `striptracks-custom.sh` to `/config` containing the following text: ```shell #!/bin/bash @@ -364,7 +365,7 @@ Make it executable: chmod +x /config/striptracks-custom.sh ``` -Then put `/config/striptracks-custom.sh` in the **Path** field in place of `/usr/local/bin/striptracks.sh` mentioned in the [Installation](./README.md#installation) section above. +Then put `/config/striptracks-custom.sh` in the **Path** field in place of `/usr/local/bin/striptracks.sh` mentioned in the [Installation](#installation) section above.
@@ -410,7 +411,7 @@ The only events/notification triggers that are supported are **On Import** and * ## Batch Mode Batch mode allows the script to be executed independently of Radarr or Sonarr. It converts the file specified on the command-line and ignores any environment variables that are normally expected to be set by the video management program. -Using this function, you can easily process all of your video files in any subdirectory at once. See the [Batch Example](./README.md#batch-example) below. +Using this function, you can easily process all of your video files in any subdirectory at once. See the [Batch Example](#batch-example) below. ### Script Execution Differences in Batch Mode Because the script is not called from within Radarr or Sonarr, their database is unavailable to the script. Therefore, expect the following behavior while in Batch Mode: @@ -460,7 +461,7 @@ However, configuring hardlinks is still recommended. Hardlink Limitations *Radarr Hardlinks Configuration Screenshot* -![radarr-enable-hardlinks](./.assets/radarr-enable-hardlinks.png "Radarr hardlinks screenshot") +![radarr-enable-hardlinks](.assets/radarr-enable-hardlinks.png "Radarr hardlinks screenshot") Hardlinks are essentially multiple references to the *same file*. The purpose of a hardlink is to: @@ -481,7 +482,7 @@ It is therefore still recommended to enable and use hardlinks in Radarr and Sona # Uninstall To completely remove the mod: -1. Delete the custom script from Radarr's or Sonarr's *Settings* > *Connect* screen that you created in the [Installation](./README.md#installation) section above. +1. Delete the custom script from Radarr's or Sonarr's *Settings* > *Connect* screen that you created in the [Installation](#installation) section above. 2. Stop and delete the Radarr/Sonarr container. 3. Remove the **DOCKER_MODS** environment variable from your `compose.yaml` file or exclude it from the `docker run` command when re-creating the Radarr/Sonarr container. diff --git a/SECURITY.md b/SECURITY.md index bed2e28..21ce748 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,8 @@ Only the latest major and minor version are supported. | Version | Supported | | ------- | ------------------ | -| 2.12.x | :heavy_check_mark: | -| < 2.12 | :x: | +| 2.13.x | :heavy_check_mark: | +| < 2.13 | :x: | ## Reporting a Vulnerability diff --git a/root/usr/local/bin/striptracks.sh b/root/usr/local/bin/striptracks.sh index d79bf83..2090317 100755 --- a/root/usr/local/bin/striptracks.sh +++ b/root/usr/local/bin/striptracks.sh @@ -62,10 +62,13 @@ export striptracks_debug=0 # Presence of '*_eventtype' variable sets script mode export striptracks_type=$(printenv | sed -n 's/_eventtype *=.*$//p') -# Usage function +# Usage functions function usage { - usage=" -$striptracks_script Version: $striptracks_ver + usage="Try '$striptracks_script --help' for more information." + echo "$usage" >&2 +} +function long_usage { + usage="$striptracks_script Version: $striptracks_ver Video remuxing script that only keeps tracks with the specified languages. Designed for use with Radarr and Sonarr, but may be used standalone in batch mode. @@ -73,7 +76,7 @@ mode. Source: https://github.com/TheCaptain989/radarr-striptracks Usage: - $0 [{-a|--audio} [{-s|--subs} ] [{-f|--file} ]] [{-l|--log} ] [{-d|--debug} []] + $0 [{-a|--audio} [{-s|--subs} ] [--reorder] [{-f|--file} ]] [{-l|--log} ] [{-c|--config} ] [{-d|--debug} []] Options can also be set via the STRIPTRACKS_ARGS environment variable. Command-line arguments override the environment variable. @@ -89,6 +92,12 @@ Options and Arguments: multiple codes may be concatenated. Each code may optionally be followed by a plus \`+\` and one or more modifiers. + --reorder Reorder audio and subtitles tracks to match + the language code order specified in the + and + arguments. + Track reorder is skipped if no tracks are + removed. -f, --file If included, the script enters batch mode and converts the specified video file. WARNING: Do not use this argument when called @@ -122,11 +131,11 @@ Examples: $striptracks_script -d 2 # Enable debugging level 2, audio and # subtitles languages detected from # Radarr/Sonarr - $striptracks_script -a :eng:und -s :eng # keep English and Unknown audio and + $striptracks_script -a :eng:und -s :eng # Keep English and Unknown audio and # English subtitles - $striptracks_script -a :eng:org -s :any+f:eng # keep English and Original audio, + $striptracks_script -a :eng:org -s :any+f:eng # Keep English and Original audio, # and forced or English subtitles - $striptracks_script -a :eng -s \"\" # keep English audio and no subtitles + $striptracks_script -a :eng -s \"\" # Keep English audio and no subtitles $striptracks_script -d :eng:kor:jpn :eng:spa # Enable debugging level 1, keeping # English, Korean, and Japanese # audio, and English and Spanish @@ -137,8 +146,13 @@ Examples: # English subtitles, converting video # specified $striptracks_script -a :any -s \"\" # Keep all audio and no subtitles + $striptracks_script -a :org:any+d1 -s :eng+1:any+f2 + # Keep all Original and one default + # audio in any language, and one + # English and two forced subtitles + # in any language " - echo "$usage" >&2 + echo "$usage" } # Log command-line arguments @@ -182,11 +196,11 @@ while (( "$#" )); do exit 1 fi ;; - -h|--help ) # Display usage - usage + --help ) # Display full usage + long_usage exit 0 ;; - -v|--version ) # Display version + --version ) # Display version echo "$striptracks_script $striptracks_ver" exit 0 ;; @@ -239,6 +253,10 @@ while (( "$#" )); do exit 1 fi ;; + --reorder ) # Reorder audio and subtitles tracks + export striptracks_reorder="true" + shift + ;; -*) # Unknown option echo "Error|Unknown option: $1" >&2 usage @@ -319,7 +337,8 @@ elif [[ "${striptracks_type,,}" = "sonarr" ]]; then # export striptracks_sonarr_json=" \"episodeIds\":[.episodes[].id]," else # Called in an unexpected way - echo -e "Error|Unknown or missing '*_eventtype' environment variable: ${striptracks_type}\nNot called from Radarr/Sonarr.\nTry using Batch Mode option: -f " >&2 + echo -e "Error|Unknown or missing '*_eventtype' environment variable: ${striptracks_type}\nNot calling from Radarr/Sonarr? Try using Batch Mode option: -f " >&2 + usage exit 7 fi export striptracks_rescan_api="Rescan${striptracks_video_type^}" @@ -1397,19 +1416,22 @@ reduce .tracks[] as $track ( {"tracks": [], "counters": {"audio": {"normal": {}, "forced": {}, "default": {}}, "subtitles": {"normal": {}, "forced": {}, "default": {}}}}; # Set track language to "und" if null or empty + # NOTE: The // operator cannot be used here because it checks for null or empty values, not blank strings (if ($track.properties.language == "" or $track.properties.language == null) then "und" else $track.properties.language end) as $track_lang | # Initialize counters for each track type and language - .counters[$track.type].normal[$track_lang] = (.counters[$track.type].normal[$track_lang] // 0) | - if $track.properties.forced_track then .counters[$track.type].forced[$track_lang] = (.counters[$track.type].forced[$track_lang] // 0) else . end | - if $track.properties.default_track then .counters[$track.type].default[$track_lang] = (.counters[$track.type].default[$track_lang] // 0) else . end | + (.counters[$track.type].normal[$track_lang] //= 0) | + if $track.properties.forced_track then (.counters[$track.type].forced[$track_lang] //= 0) else . end | + if $track.properties.default_track then (.counters[$track.type].default[$track_lang] //= 0) else . end | .counters[$track.type] as $track_counters | # Add tracks one at a time to output object above .tracks += [ $track | .striptracks_debug_log = "Debug|Parsing track ID:\(.id) Type:\(.type) Name:\(.properties.track_name) Lang:\($track_lang) Codec:\(.codec) Default:\(.properties.default_track) Forced:\(.properties.forced_track)" | - + # Use track language evaluation above + .properties.language = $track_lang | + # Determine keep logic based on type and rules if .type == "video" then .striptracks_keep = true @@ -1476,7 +1498,7 @@ elif (.tracks | map(select(.type == "audio" and .striptracks_keep)) | length == else . end | # Output simplified dataset -{ striptracks_log, tracks: [ .tracks[] | { id, type, forced: .properties.forced_track, default: .properties.default_track, striptracks_debug_log, striptracks_log, striptracks_keep } ] } +{ striptracks_log, tracks: .tracks | map({ id, type, language: .properties.language, forced: .properties.forced_track, default: .properties.default_track, striptracks_debug_log, striptracks_log, striptracks_keep }) } ') [ $striptracks_debug -ge 1 ] && echo "Debug|Track processing returned ${#striptracks_json_processed} bytes." | log [ $striptracks_debug -ge 2 ] && echo "Track processing returned: $(echo "$striptracks_json_processed" | jq)" | awk '{print "Debug|"$0}' | log @@ -1541,6 +1563,47 @@ if [ "$(echo "$striptracks_json" | jq -crM '.tracks|map(select(.type=="audio" or fi fi +# Prepare to reorder tracks if option is enabled +if [ "$striptracks_reorder" = "true" ]; then + striptracks_neworder=$(echo "$striptracks_json_processed" | jq -jcM --arg AudioKeep "$striptracks_audiokeep" \ +--arg SubsKeep "$striptracks_subskeep" ' +# Reorder tracks +def order_tracks(tracks; rules; tracktype): + rules | split(":")[1:] | map(split("+") | {lang: .[0], mods: .[1]}) | + reduce .[] as $rule ( + []; + . as $orderedTracks | + . += [tracks | + map(. as $track | + select(.type == tracktype and .striptracks_keep and + ($rule.lang | in({"any":0,($track.language):0})) and + ($rule.mods == null or + ($rule.mods | test("[fd]") | not) or + ($rule.mods | contains("f") and $track.forced) or + ($rule.mods | contains("d") and $track.default) + ) + ) | + .id as $id | + # Remove track id from orderedTracks if it already exists + if ([$id] | flatten | inside($orderedTracks | flatten)) then empty else $id end + )] + ) | flatten; + +# Reorder audio and subtitles according to language rules +.tracks as $tracks | +order_tracks($tracks; $AudioKeep; "audio") as $audioOrder | +order_tracks($tracks; $SubsKeep; "subtitles") as $subsOrder | + +# Output ordered track string compatible with the mkvmerge --track-order option +# Video tracks are always first, followed by audio tracks, then subtitles +$tracks | map(select(.type == "video") | .id) + $audioOrder + $subsOrder | map("0:" + tostring) | join(",") +') + striptracks_neworder="--track-order $striptracks_neworder" + striptracks_message="Info|Reordering tracks using language rules." + echo "$striptracks_message" | log + [ $striptracks_debug -ge 1 ] && echo "Debug|Using track reorder string: $striptracks_neworder" | log +fi + # Test for hardlinked file (see issue #85) striptracks_refcount=$(stat -c %h "$striptracks_video") [ $striptracks_debug -ge 1 ] && echo "Debug|Input file has a hard link count of $striptracks_refcount" | log @@ -1563,7 +1626,7 @@ else fi # Execute MKVmerge (remux then rename, see issue #46) -striptracks_mkvcommand="nice /usr/bin/mkvmerge --title \"$striptracks_title\" -q -o \"$striptracks_tempvideo\" $striptracks_audioarg $striptracks_subsarg \"$striptracks_video\"" +striptracks_mkvcommand="nice /usr/bin/mkvmerge --title \"$striptracks_title\" -q -o \"$striptracks_tempvideo\" $striptracks_audioarg $striptracks_subsarg $striptracks_neworder \"$striptracks_video\"" [ $striptracks_debug -ge 1 ] && echo "Debug|Executing: $striptracks_mkvcommand" | log striptracks_result=$(eval $striptracks_mkvcommand) striptracks_return=$? From 04a32f98af8cfc26a15a6be751786e31a93bff8c Mon Sep 17 00:00:00 2001 From: TheCaptain989 Date: Sat, 8 Mar 2025 11:10:22 -0600 Subject: [PATCH 4/4] Updated MOD_VERSION in BuildImage --- .github/workflows/BuildImage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/BuildImage.yml b/.github/workflows/BuildImage.yml index ba978a3..79a5cf1 100644 --- a/.github/workflows/BuildImage.yml +++ b/.github/workflows/BuildImage.yml @@ -22,7 +22,7 @@ jobs: echo "MODNAME=${{ env.MODNAME }}" >> $GITHUB_OUTPUT echo "MULTI_ARCH=${{ env.MULTI_ARCH }}" >> $GITHUB_OUTPUT # **** If the mod needs to be versioned, set the versioning logic below. Otherwise leave as is. **** - MOD_VERSION="2.12.0" + MOD_VERSION="2.13.0" echo "MOD_VERSION=${MOD_VERSION}" >> $GITHUB_OUTPUT outputs: GITHUB_REPO: ${{ steps.outputs.outputs.GITHUB_REPO }}