MKVsortbyAudio bash shell script to sort MKVs by audio codec

Discussion of advanced MakeMKV functionality, expert mode, conversion profiles
Post Reply
Message
Author
mgutt
Posts: 141
Joined: Sun May 05, 2019 6:38 pm

MKVsortbyAudio bash shell script to sort MKVs by audio codec

#1 Post by mgutt » Fri Nov 22, 2019 4:49 pm

One of the steps I need after MakeMKV generated the MKV is to add a missing AC3 audio track to avoid transcoding through Plex as most of my clients to not support DTS.

I do not like the internal conversion of MakeMKV as it takes ages to rip and convert multiple discs. Instead I create the discs and afterwards/parallel I use PopCorn MKV AudioConverter to add missing AC3 tracks. But as some movies still contain AC3 and DTS tracks and some only have AC3 tracks for comments I need to sort all the MKVs first by "with AC3" and "with DTS".

And to save this work I created the MKVsortbyAudio script that recursively scans all mkvs in the folder <movies_path> and moves them according their audio codec to <movies_path>_AC3, <movies_path>_DTS and so on. It uses the same folder structure and leaves all other files in <movies_path> untouched so you can drag & drop them back. MKVs without the <required_audio_lang> are moved to <movies_path>_Unknown_Audio so you can check those manually. If an MKV contains multiple audio codecs <prefer_audio_codec> is used for sorting. MKVsortbyAudio sorts <iterations> mkvs per script execution.

Requirements
- Linux
- mkvtoolnix or docker
Last edited by mgutt on Fri Nov 22, 2019 5:37 pm, edited 2 times in total.

mgutt
Posts: 141
Joined: Sun May 05, 2019 6:38 pm

Re: MKVsortbyAudio bash shell script to sort MKVs by audio codec

#2 Post by mgutt » Fri Nov 22, 2019 4:50 pm

Script

Code: Select all

#!/bin/sh
# #####################################
# MKVsortbyAudio v0.3
# 
# Description:
# MKVsortbyAudio scans recursive all mkvs in the folder <movies_path> and moves them 
# according their audio codec to <movies_path>_AC3, <movies_path>_DTS and so on.
# It uses the same folder structure and leaves all other files in <movies_path> 
# untouched so you can drag & drop them back. MKVs without the <required_audio_lang>
# are moved to <movies_path>_Unknown_Audio so you can check those manually.
# If an MKV contains multiple audio codecs <prefer_audio_codec> is used for sorting.
# MKVsortbyAudio requires mkvtoolnix or docker.
# 
# Changelog:
# 0.3
# - the source folder will be deleted if its empty
# 0.2
# - check mkv modification filetime to ensure its not currently written through an other app
# 0.1
# - first release
# 
# Todo:
# 
# #####################################
# 
# ######### Settings ##################
movies_path="/volume1/movies/"
iterations=10 # mkv files to process per script execution
required_audio_lang="ger"
prefer_audio_codec="A_AC3" # A_AC3, A_DTS, A_AAC, A_PCM, A_MPEG, etc
docker_config_path="/volume1/docker" # do not mind if you do not use docker
# #####################################
# 
# ######### Script ####################
# check user settings
movies_path=$([[ "${movies_path: -1}" == "/" ]] && echo "${movies_path%?}" || echo "$movies_path")
docker_config_path=$([[ "${docker_config_path: -1}" != "/" ]] && echo "${docker_config_path}/" || echo "$docker_config_path")
# globals
file_basename=""
file_extension=""
mkv_path=""
mkv_dirbasename=""
mkv_filename=""
function exitus() {
    exit_status=$1
    if [[ -x "$(command -v docker)" ]] && [[ "$(docker ps -q -f name=mkvtoolnix)" ]]; then
        echo "Stop mkvtoolnix container"
        docker stop mkvtoolnix
        docker rm mkvtoolnix
    fi
    exit $exit_status
}
function mkv_next() {
    path=$1
    echo "Parsing $path ..."
    for file in "$path"/*; do
        # mkv has been already found
        if [[ -n $mkv_path ]]; then
            return
        # regular file
        elif [ -f "$file" ]; then
            file_time=$(stat -c %Y "$file") # file modification time
            file_time=$(($file_time+60)) # the last modification of the file should be a few time ago
            current_time=$(date +%s) # actual timestamp
            if [[ $file_time -gt $current_time ]]; then
                continue
            fi
            file_basename=$(basename -- "$file")
            file_extension="${file_basename##*.}"
            if [[ $file_extension != "mkv" ]]; then
                continue
            fi
            mkv_path=$file
            mkv_dirname=$(dirname "$file")
            mkv_dirname="${mkv_dirname/$movies_path/}"
            mkv_dirname="${mkv_dirname:1}" # remove first slash
            mkv_dirname="${mkv_dirname/$movies_path/}" # remove <movies_path> from path
            docker_mkv_path="/storage/${mkv_dirname}/${file_basename}"
            echo "Found $mkv_path"
            break
        # dir
        else
            mkv_next "$file"
        fi
    done
}
function mkv_move() {
    path=$1
    subdir="${path}/${mkv_dirname}"
    mkdir -p "$subdir/"
    if [[ $? != 0 ]]; then
        echo "Error while creating the directory '$subdir/' in line $LINENO"
        exitus 1
    fi
    echo "Created the directory '$subdir/'"
    mkv_path_new="${subdir}/${file_basename}"
    mv "$mkv_path" "$mkv_path_new"
    if [[ $? != 0 ]]; then
        echo "Error moving the file '$mkv_path_new' in line $LINENO"
        exitus 1
    fi
    echo "Moved MKV to '$mkv_path_new'"
    mkv_dirname=$(dirname "$mkv_path")
    if [[ -z "$(ls -A "$mkv_dirname")" ]]; then
        echo "Delete empty folder '$mkv_dirname'"
        rmdir "$mkv_dirname"
    fi
}
function mkv_getinfo() {
    # check if mkvtoolnix exists
    if [[ -x "$(command -v mkvmerge)" ]]; then
        echo "mkvtoolnix will be used to fetch tracks information"
        mkv_info="$(mkvmerge -J "$mkv_path")"
        return "$mkv_info"
    # check if docker exists
    elif [[ -x "$(command -v docker)" ]]; then
        echo "Docker will be used to fetch tracks information"
        # check if mkvtoolnix container exists
        if [[ ! "$(docker ps -q -f name=mkvtoolnix)" ]]; then # https://stackoverflow.com/a/38576401/318765
            # check for blocking container
            if [[ ! "$(docker ps -aq -f status=exited -f name=mkvtoolnix)" ]]; then
                docker rm mkvtoolnix
            fi
            echo "mkvtoolnixcontainer needs to be started"
            # start mkvtoolnix container
            docker_options=(
                run -d
                --name=mkvtoolnix
                -e TZ=Europe/Berlin
                -v "${docker_config_path}mkvtoolnix:/config:rw"
                -v "${movies_path}:/storage:rw"
                jlesage/mkvtoolnix
            )
            echo "docker ${docker_options[@]}"
            docker "${docker_options[@]}"
        fi
        mkv_info="$(docker exec mkvtoolnix /usr/bin/mkvmerge -J "$docker_mkv_path")"
        return
    fi
    echo "mkvtoolnix and docker do not exist!"
    exitus 1
}
function delete_empty_folder() {
    path=$1
    
}
for i in $(seq 1 $iterations); do
    # obtain next mkv file
    shopt -s nullglob # avoid empty directory errors (https://unix.stackexchange.com/questions/56051/avoiding-errors-due-to-unexpanded-asterisk)
    mkv_next "$movies_path" # fills $mkv_path
    shopt -u nullglob # its important to reset this setting (https://unix.stackexchange.com/questions/534858/why-does-shopt-s-nullglob-remove-a-string-with-question-mark-in-an-array-elemen)
    # no mkv file found
    if [[ -z $mkv_path ]]; then
        echo "No mkv file found!"
        exitus 0
    fi
    # get tracks info
    mkv_getinfo # fills $mkv_info
    if [[ -z $mkv_info ]]; then
        echo "Error while fetching tracks information with mkvmerge"
        exitus 1
    fi
    echo "Informations of all tracks have been obtained."
    # parse info
    best_channels=0
    best_codec=false
    sub_track_ids=(); track_langs=(); track_names=();
    while read -r line ; do
        # Note: we did not use "jq -r" to parse JSON as it needs installation
        track_codec_name=$(echo $line | grep -oP '^.*?(?=\")')
        track_id=$(echo $line | grep -oP '(?<="id": )[0-9]+')
        track_bits=$(echo $line | grep -oP '(?<="audio_bits_per_sample": )[0-9]+')
        track_channels=$(echo $line | grep -oP '(?<="audio_channels": )[0-9]+')
        track_codec_id=$(echo $line | grep -oP '(?<="codec_id": ").*?[^\\](?=\",)')
        track_lang=$(echo $line | grep -oP '(?<="language": ")[a-z]+')
        track_name=$(echo $line | grep -oP '(?<="track_name": ").*?[^\\](?=\",)') # most flexible way of getting a JSON value (https://stackoverflow.com/a/6852427/318765)
        track_default=$(echo $line | grep -oP '(?<="default_track": )(true|false)')
        track_forced=$(echo $line | grep -oP '(?<="forced_track": )(true|false)')
        track_type=$(echo $line | grep -oP '(?<=")[a-z]+$')
        echo "track_id:$track_id track_type:$track_type track_channels:$track_channels track_codec_id:$track_codec_id track_lang:$track_lang best_codec:$best_codec"
        # collect subtitles in prefered languages
        if [[ $track_type == "audio" ]] && [[ $track_lang == $required_audio_lang ]]; then
            if [[ $best_channels -gt $track_channels ]]; then
                continue
            fi
            best_codec="$track_codec_id"
            best_channels="$track_channels"
            # we already found the audio codec with the hightest priority
            if [[ $best_codec == $prefer_audio_codec ]] && [[ $best_channels -ge 6 ]]; then
                break
            fi
        fi
    done < <(echo "$mkv_info" | 
            tr -d '\n' | # we need to remove line breaks with "tr" to force grep to return one-liners
            grep -oP '(?<=codec": ").*?"type": "[a-z]+') # Regex is faster than looping through all lines
    # those mkvs need manual checking
    if [[ $best_codec == false ]]; then
        best_codec="Unknown_Audio"
    fi
    # sort mkv by audio codec
    best_codec="${best_codec/A_/}" # remove "A_" from "A_AC3"
    mkv_subdir="${movies_path}_${best_codec}"
    mkv_move "$mkv_subdir"
    # unset mkv path to be able to obtain the next mkv file
    mkv_path=""
    mkv_info=""
done
exitus 0
Last edited by mgutt on Mon Nov 25, 2019 9:16 am, edited 2 times in total.

mgutt
Posts: 141
Joined: Sun May 05, 2019 6:38 pm

Re: MKVsortbyAudio bash shell script to sort MKVs by audio codec

#3 Post by mgutt » Fri Nov 22, 2019 4:55 pm

This is how it looks after several MKVs have been sorted:
2019-11-22 17_32_28.jpg
2019-11-22 17_32_28.jpg (153.66 KiB) Viewed 13393 times

The source folder and the cover.jpg is left untouched:
2019-11-22 17_46_19.jpg
2019-11-22 17_46_19.jpg (5.36 KiB) Viewed 13393 times

And the MKV has been moved to the the target "DTS" folder:
2019-11-22 17_46_30.jpg
2019-11-22 17_46_30.jpg (7.27 KiB) Viewed 13393 times
Last edited by mgutt on Fri Nov 22, 2019 5:01 pm, edited 1 time in total.

mgutt
Posts: 141
Joined: Sun May 05, 2019 6:38 pm

Re: MKVsortbyAudio bash shell script to sort MKVs by audio codec

#4 Post by mgutt » Fri Nov 22, 2019 5:00 pm

As you can see this MKV is moved to the "AC3" folder as the best German audio track is "AC3 Stereo":
2019-11-22 17_57_23.jpg
2019-11-22 17_57_23.jpg (53.22 KiB) Viewed 13380 times

Instead, this MKV has been moved to "DTS" as the German "AC3 Stereo" track has less channels than the 5.1 DTS track (and finally is an AC3 stereo track that follows a 5.1 track in 99% of all cases a "Director's Comment"):
2019-11-22 18_18_40.jpg
2019-11-22 18_18_40.jpg (46.18 KiB) Viewed 13387 times

And finally this MKV has been moved to "AC3" even tough it contains an DTS track with more channels (6-channels are all I need - feel free to change the script to fit your needs):
2019-11-22 18_37_40.jpg
2019-11-22 18_37_40.jpg (49.59 KiB) Viewed 13383 times

mgutt
Posts: 141
Joined: Sun May 05, 2019 6:38 pm

Re: MKVsortbyAudio bash shell script to sort MKVs by audio codec

#5 Post by mgutt » Mon Nov 25, 2019 9:17 am

Version 0.3 has been released. Since 0.1 I did small changes:

Code: Select all

# 0.3
# - the source folder will be deleted if its empty
# 0.2
# - check mkv modification filetime to ensure its not currently written through an other app

Post Reply