Jump to content

Auto sort audiobooks for Emby (script)


troy_

Recommended Posts

As an Emby newbie I'm struggling with some things, like lack of chapter support for M4B files.

If you fetch your audio books with something like OpenAudible then the script below (gist here) can sort them out into Emby's preferred directory structure, and break them up into chapters.

Copy the script to the folder with your M4B's, and run it.

You need to have ffmpeg and ffprobe installed and working - that's about it.

The original file is moved to an ignored directory, and cover art is stored per the Emby requirements.

#!/usr/bin/env zsh

# Check if ffmpeg is installed
if ! [ -x "$(command -v ffmpeg)" ]; then
	echo 'Error: ffmpeg is not installed.' >&2
	exit 1
fi

# Check if ffprobe is installed
if ! [ -x "$(command -v ffprobe)" ]; then
	echo 'Error: ffprobe is not installed.' >&2
	exit 1
fi

ffmpeg_loglevel="error"
max_title_length=100

# Loop over the folder
for file in *.m4b; do
	# Get the title and year of the file
	title=$(ffprobe -loglevel ${ffmpeg_loglevel} -show_entries format_tags=album -of default=noprint_wrappers=1:nokey=1 "$file")
	if [ "${#title}" -ge "$max_title_length" ]; then
		title="${file%.*}"
	fi
	author=$(ffprobe -loglevel ${ffmpeg_loglevel} -show_entries format_tags=artist -of default=noprint_wrappers=1:nokey=1 "$file")
	narrator=$(ffprobe -loglevel ${ffmpeg_loglevel} -show_entries format_tags=composer -of default=noprint_wrappers=1:nokey=1 "$file")
	year=$(ffprobe -loglevel ${ffmpeg_loglevel} -show_entries format_tags=date -of default=noprint_wrappers=1:nokey=1 "$file")
	audiobook_folder="${author}/${title} ($year)"
	audiobook_source="${audiobook_folder}/source"
	# Create a folder for the file if it doesn't exist
	if [ ! -d "$audiobook_source" ]; then
		mkdir -p "$audiobook_source"
	fi
	if [ ! -f "$audiobook_source/.ignore" ]; then
		touch "$audiobook_source/.ignore"
	fi
	if [ -f "$audiobook_source/$file" ]; then
		if cmp -s "${file}" "$audiobook_source/$file" ; then
			rm -f "${file}"
		else
			echo "⚠️ File aready exists"
			mv "$audiobook_source/$file" "$audiobook_source/$file.bak"
			mv "$file" "$audiobook_source/"
		fi
	else
		mv "$file" "$audiobook_source/"
	fi
	
	if [ ! -f "${audiobook_folder}/cover.jpg" ]; then
		ffmpeg -loglevel ${ffmpeg_loglevel} -i "${audiobook_source}/$file" -frames:v 1 -an -vcodec copy "${audiobook_folder}/cover.jpg"
	fi
	ffmpeg -loglevel ${ffmpeg_loglevel} -i "${audiobook_source}/$file" -map_chapters -1 -f ffmetadata "${audiobook_source}/metadata.txt"
	# Get the chapters from the file
	chapters_json=$(ffprobe -loglevel ${ffmpeg_loglevel} -show_chapters -print_format json "$audiobook_source/$file")
	echo "${chapters_json}" > "${audiobook_source}/chapters.json"
	# Loop over the chapters
	count=`jq '.chapters | length' <<< "${chapters_json}"`
	total_chapters=$((count+1))
	for ((i=0; i<$count; i++)); do
		chapter=`jq -r '.chapters['$i']' <<< "${chapters_json}"`
		chapter_id=$(jq -r '.id' <<< "$chapter")
		if [ -z "$chapter_id" ]; then echo "No chapter id. JSON: ${chapter}"; exit 1; fi
		printf -v chapter_number "%d" $((chapter_id+1))
		chapter_title=$(jq -r '.tags.title' <<< "$chapter")
		seek_position=$(jq -r '.start_time' <<< "$chapter")
		if [ -z "$seek_position" ]; then echo "No seek position for chapter id ${chapter_id}"; exit 1; fi
		end_position=$(jq -r '.end_time' <<< "$chapter")
		if [ -z "$end_position" ]; then echo "No end position for chapter id ${chapter_id}"; exit 1; fi
		length_seconds=$((end_position-seek_position))
		printf -v chapter_start_ms "%0d" $((seek_position*1000))
		printf -v chapter_end_ms "%0d" $((end_position*1000))
		printf -v chapter_length_ms "%0.6f" $((length_seconds*1000))
		printf -v chapter_start_seconds "%0.6f" "${seek_position}"
		printf -v chapter_end_seconds "%0.6f" "${end_position}"
		printf -v chapter_length_seconds "%0.6f" "${length_seconds}"
		file_name="${chapter_number}- ${chapter_title}.m4b"
		chapter_file_name="${chapter_number}- ${chapter_title}.txt"
		cp "${audiobook_source}/metadata.txt" "${audiobook_source}/${chapter_file_name}"
		printf "\n\n[CHAPTER]\nTIMEBASE=1/1000\nSTART=%d\nEND=%d\ntitle=%q\n\n" "0" "${chapter_length_ms}" "${chapter_title}" >> "${audiobook_source}/${chapter_file_name}"
		echo "${file_name} ==> ${seek_position} to ${end_position} (${chapter_end_seconds})"
		if [ ! -f "${audiobook_folder}/${file_name}" ]; then
			ffmpeg -loglevel ${ffmpeg_loglevel} -ss "${seek_position}s" -to "${end_position}s" -i "${audiobook_source}/$file" -i "${audiobook_source}/${chapter_file_name}" -map_metadata 0 -map_chapters 1 -metadata "track=${chapter_number}/${total_chapters}" -c copy "${audiobook_folder}/${file_name}"
		fi		
	done
done

 

Edited by troy_
more fixes :-D
Link to comment
Share on other sites

Of note - this doesn't seem to fix my issue with Emby... 
Now I have a heap of files - all with the title instead of the chapter.

At least Emby seems to recognise the Chapter inside the M4B's generated with the script above.

I'm not sure what to do at this point. The metadata for the OpenAudible M4B's is correct and works with other players. Splitting the M4B up clearly isn't the answer (as I now have a book for every chapter); putting the files in the Emby preferred folder structure isn't the answer; mp3's don't seem to work either.

Open to suggestions!

Screenshot 2023-01-12 at 9.46.19 pm.png

Screenshot 2023-01-12 at 9.46.04 pm.png

Screenshot 2023-01-12 at 9.48.44 pm.png

Link to comment
Share on other sites

HI, the title doesn't come from the file name, but rather the metadata embedded within the file. Have you checked that?

Link to comment
Share on other sites

Some of my M4B's are working now (with chapters) which may be because I've moved to `beta`

I don't know if it's just an import delay, I'm a couple of weeks in now and it's still scanning my library - so perhaps the issue will clear up.

You are correct @Lukethe title of each M4B is the title of the work, not the title of the chapter - so that's what's causing the issue above.

I've updated the script to work as a post processor now, and it can break up and put the chapter title into the track title.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...