Showing posts with label script. Show all posts
Showing posts with label script. Show all posts

Sunday, November 29, 2020

Handy FFmpeg + bash scripts

I created these scripts to perform some common tasks with FFmpeg and figured I'd post them here in case anyone else needs to do the same jobs.

Convert to mp3

I keep my music collection in lossless FLAC format, but not everything supports this format, including the stereo in my car. It'll play music files from a USB thumb drive and is compatible with a variety of lossy formats, just not FLAC, so if I want to put a new album on my drive to get it into my regular rotation, I have to convert from my lossless archival copy to a compatible lossy format. I made this basic script to simplify the process:

#!/bin/bash

if [ "$1" == "" ] || [ "$1" == "--help" ]; then
echo "Convert audio files to mp3 using LAME with high-quality VB0 settings."
echo "Single files or wildcards are valid arguments."
exit
fi

for i in "$@"
do

[ ! -d "mp3s" ] && mkdir "mp3s"

name=`echo "$i" | sed 's/\.[^.]*$//' | sed 's/.*\///'`

ffmpeg -i "$i" -acodec libmp3lame -q:a 0 mp3s/"$name".mp3

done
To explain a bit what's going on: the first 'if' statement echos out a helper statement if the script is run either without an argument or with the standard '--help' switch. You'll notice I use the special built-in "$1", which means "first argument". More info on the built-in argument variables here. It also exits here instead of going on to the rest of the script, which wouldn't really make sense without the proper arguments anyway.

Next, we start the 'for' loop. Here, I've used another built-in alias, "$@", which is important because it will allow the loop to expand to accept wildcards, such as asterisk/*. When I was first working on this script, I used the same "$1" as before and it would only hit the first match from the wildcard and then stop. More info on that problem in this stackexchange post.

So, for each match to the pattern (be it a single file or a series), it will first check whether the directory named 'mp3s' exists and, if not, it will make it (I find it's easier to deal with the files if they're all in one place instead of mixed in with the FLACs that have exactly the same name other than file extension...).

Next, we declare a variable, "name", which uses some sed magic to remove the file extension. Unfortunately, I looked this up a long time ago and no longer have a link to where I found the answer, and sed syntax is pure black magic to me, so I have zero clue what it's actually doing, but it works. The reason we need to do this is because the resulting mp3s would all be named foo.flac.mp3 otherwise, which is ugly.

Alright, the last bit is where we use FFmpeg to actually do the work of reencoding. The only real gotcha here is knowing that the LAME encoder is called libmp3lame, and knowing about the qscale:a setting, which determines the variable quality/bitrate. Since my car supports it, I use VB0, which is variable bitrate that goes up to 320 kbps when necessary but doesn't waste bits encoding silence, for example. If your device doesn't support variable-bitrate mp3s, you can do -b:a 128k (or whatever bitrate you want) instead.

Trim From The End

Turns out it's really hard to get FFmpeg to trim an arbitrary number of seconds from the end of a file without reencoding, which is exactly what I needed to do to remove a very loud and very useless sequence at the end of every episode of my Duckman DVD rips (my wife likes to fall asleep to reruns, and this sequence would startle her awake every time).

#!/bin/bash

if [ "$1" == "--help" ] || [ "$1" == "" ]; then
echo "Enter a video's filename followed by the number of seconds to trim from it."
echo "For example (to trim 25 seconds from a video named 'foo.mp4'):"
echo "foo.mp4 25"
echo "The trimmed file will be placed in the newly created 'trunc' directory."
exit
fi

DURATION=`ffprobe -v 0 -show_entries format=duration -of compact=p=0:nk=1 "$1"`

echo "$DURATION"

if [ "$2" == "" ]; then
echo "Your video is '$DURATION' seconds long. You need to specify how many seconds to trim."
exit
fi

echo "Total duration is: '$DURATION'"
DUR_INT=`awk 'BEGIN { print int('$DURATION'+0.5) }'`
TRIM=`expr "$DUR_INT" - "$2"`
echo "Duration after trimming is: $TRIM"

if [ ! -d trunc ]; then
  mkdir trunc;
fi

ffmpeg -i "$1" -c copy -t "$TRIM" trunc/"$1"
echo "Success"
exit

Again, I started out with some helpful explanation, just in case I forget how to use the script.

Since we're using two arguments, we use the built-in "$1" and "$2". This command will not run with wildcards.

Next up, we'll use ffprobe (a utility that comes with FFmpeg) to determine the duration of the file in seconds. This gives us a floating point value, which means it usually includes a bunch of fractional stuff after the decimal point. This becomes important later. We'll store this value in the DURATION variable.

So, we now know how long our video is, and we know how many seconds we want to trim, so we should be able to subtract the trim amount from the total value using bash's built-in expr arithmetic, but life is never that easy. No, expr won't subtract an integer from a float, so we need to convert the DURATION float value to an integer using some awk magic (I know as little about awk as sed, sadly). This will take our float value, add 0.5 to it, "floor" it (that is, round it down) to the nearest integer and then cast the value to an "int" (i.e., store it as an actual integer rather than a float; like "2" instead of "2.0"). We'll store that int value in DUR_INT.

Now we can subtract our seconds from the total, and we'll store the resulting total value as TRIM.

After that, it's just like before, where we check for (and if necessary, create) our storage directory (even more important this time because we don't want to accidentally overwrite our original files with the trimmed version), then loop through our pattern matches and use FFmpeg's "-t" switch to stop the new file at our newly calculated "trimmed" duration, which will cut off the end (and we're using the -c copy switch to avoid reencoding, which takes a long time and reduces the quality of the video each time).

Thursday, October 22, 2015

Bash Scripts for Non-Repeating Random Playback

I have an HTPC with a huge amount of files to choose from, which can lead to analysis paralysis / the paradox of choice: when there's so much to choose from, I end up consuming the same handful of media over and over instead of consuming a proper variety. Many media players have "shuffle" functions that will pick a random item from a list but that only works for traditional media and doesn't help with, for example, emulated video games. So, I decided to play around with some bash scripting to see if I could come up with anything more universal.

The first thing I wanted to do was to create a playlist based on a recursive scan of a directory. For example, I have my TV shows separated by show and then subdivided by season/series, and I want all of these files included in a playlist, which I accomplished with this script:
#!/bin/bash
if [ -e playlist]
then
rm playlist
fi
for f in **/*
do
echo $f >> playlist
done
This script will check for any existing playlist and delete it if it finds one already, then it search recursively and adds any files (but not directories, importantly) from any subdirectories to the playlist. If I add more files (e.g., I get new episodes of a show), I can just run the playlist-maker script again and it will delete the old playlist and make a new one with the new files added.

So that's great. However, these files are all in order and I want them to be randomly sorted instead (in case my desired launch program doesn't have a built-in shuffle function), which I can do like this:
#!/bin/bash
if [ -e random-playlist ]
then
rm random-playlist
fi
for f in **/*
do
echo $f >> nonrandom-playlist
shuf --output=random-playlist nonrandom-playlist
done
rm nonrandom-playlist
This script does all of the same steps as before but it also uses the shuf command, which is a common UNIX utility that will randomize the entries in the non-randomized playlist and then output a randomized playlist. The script then deletes the non-randomized playlist.

Alright. Looking good. If my playback program has a built-in shuffle function, I can just pass this playlist to it and it will shuffle among those files. However, as anyone who uses shuffle frequently will tell you, it often feels like a small minority of songs/videos gets played more often than others (whether this is actually true or not is up for some debate but it can be annoying all the same), so I want to remove entries from the playlist once I've consumed them so there can't be any repeats until every file has been played at least once:
#!/bin/bash
line=$(head -1 random-playlist)
echo "Next up: $line"
mpv --fs "$line"
echo "I have a lot of great memories from that episode..."
sed -i -e "1d" random-playlist
echo "And now they're gone!"
This script uses the UNIX utility head, which lists any number of lines from a file, starting with the first line. In this case, I just ask for the first line, echo it (for diagnostic purposes, and it will be useful later) and then pass it to my player (in this case, mpv). After playback is finished, it echoes again and then uses the UNIX utility sed, which is a super-powerful text/stream editor, to delete the first line ("1d") from the random playlist. It echoes one more time to complete the Bender quote and let me know that the entry was successfully deleted.

Ok, this is great but I don't want to have to always run my script to consume the next file. That is, I want to just get things rolling and have a marathon of random selections without continued interaction from me. To do this, we just wrap the whole thing in a 'for' loop, like this:
#!/bin/bash
for i in {1..100}; do
line=$(head -1 random-playlist)
echo "Next up: $line"
mpv --fs "$line"
echo "I have a lot of great memories from that episode..."
sed -i -e "1d" random-playlist
echo "And now they're gone!"
done
This will go through playing and deleting 100 times (you could make this 1,000 or 62 or whatever), which is great. However, I don't usually want to watch 100 episodes of a show, and if I launched this script from a desktop environment (i.e., by double-clicking), I have no way of stopping it! That is, each time I close my playback program, either because the episode is over or I clicked on the 'close' button, the next episode is queued up and starts automatically. If this were running in a terminal window, I could just close that window and it would stop the script in its tracks but this leads to a dilemma: it's not convenient to have to launch the script from a command line each time instead of double-clicking, and if I tell Nautilus (my file manager) to launch scripts in a terminal when I double-click them, it tries to run the scripts from my home directory instead of their actual location >.<

Thankfully, there's a solution. I can write a helper script that doesn't care if it runs from the home directory, and that script can launch my marathon script:
#!/bin/bash
DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
x-terminal-emulator -e ./marathon-script
The first line of the script is a handy one-liner that navigates to the directory where the script lives. The second line tells it to launch whichever terminal application is registered to your alternatives system as the 'terminal emulator,' in my case it's gnome-terminal, and the -e switch tells it to run the marathon script inside that terminal, where we can easily view our diagnostic echo messages and close it when we're done consuming media.

As I mentioned, these scripts are easily modified to suit any kind of file, so I have some set up to launch a random NES game from my collection via RetroArch. I plan to use this setup to play through the entire NES library over a period of a few weeks/months.

Friday, May 1, 2009

Monitoring a Directory to Automatically Invoke HandBrake

I've seen this question pop up a lot on HandBrake's forums and IRC channel, so I thought I'd make an entry about it here (Mac users skip down to the bottom for your directions):

Many folks have expressed interest in being able to specify a directory for HandBrake to 'watch' for new files that it would then automatically attempt to convert with predefined settings. I think most people are wanting this for use with devices, such as iPods, PS3s and AppleTVs, which require specific settings for videos to work. While HB doesn't support this functionality on its own (and the devs don't sound too interested in adding it), you can accomplish much the same thing in Ubuntu Linux using HandBrakeCLI and a little shell scripting.
WARNING: I'm a novice at scripting and there is definitely a more effective and elegant way of doing this. If you have a suggestion, please leave a comment! Similar steps will also work on other platforms/distros, so feel free to leave a comment about your successes or failures.

First, we'll need to install a utility to enable monitoring of directories:
sudo aptitude install inotify-tools
Next, we'll make some new directories in our home folder to hold our scripts and conversions. In a terminal, type:
cd $HOME ; mkdir HandBrake ; mkdir HandBrake/convert
Navigate to the newly created HandBrake directory:
cd HandBrake
and type:
gedit monitor.sh
This is where we'll write our script to monitor the 'convert' directory and invoke another script to do the actual conversion:
#!/bin/bash

inotifywait --monitor -e moved_to -e create ~/HandBrake/convert | while read dir;
do
(~/HandBrake/convert.sh)

done
Save, exit and--again--type:
gedit convert.sh
Here we will create our conversion script (be sure to put your desired file extension and preset in place instead of the bracketed reminders):
#!/bin/bash
for file in ~/HandBrake/convert/*
do HandBrakeCLI -v -i "$file" -o "$file".converted.[FILE-EXTENSION-GOES-HERE] --preset [PRESET-NAME-GOES-HERE] ;

#uncomment next line to delete original
#rm $file

done
Save and exit, then type:
chmod +x *.sh
to make both scripts executable.

Now, you can start monitoring by typing:
sh ~/HandBrake/monitor.sh
or you can set the script to run as a startup item where it will run continuously in the background starting the next time you log on.

Henceforth, any file you move or copy into the 'convert' directory will automatically convert to the desired format. This script will only work on one file at a time (i.e., you have to wait for the encoding to finish before dropping in the next file to convert). Also of note: HB will choke if the file is weird in any way--e.g. no audio track--and you'll have no way of knowing it if the script is running in the background, since it won't print any output.

Good luck and let me know if you run into any problems.

Bulk Encoding on Macs


Update (06/01/09): There's been a lot of clamoring on the Mac board of the HandBrake forums asking for bulk input of files to be converted using a preset. The devs have no interest in adding such a feature at this time because of the tremendous support headache it could cause, but you Mac users can do scripting to accomplish the same thing.

Just like the Linux users, open a Terminal (Applications > Utilities > Terminal) and type:

cd Desktop ; nano convert.sh
then paste in this (shift+ctrl+v; be sure to put your desired file extension and preset in place instead of the bracketed reminders):
#!/bin/bash
for file in ./*
do ./HandBrakeCLI -v -i "$file" -o "$file".converted.[FILE-EXTENSION-GOES-HERE] --preset [PRESET-NAME-GOES-HERE] ;

#uncomment next line to delete original
#rm $file

done
Save and exit (ctrl+x), then type:
chmod +x *.sh
to make the script executable. Now, just put the script into a folder with your HandBrakeCLI binary and you should be able to invoke the script (navigate to its directory in the Terminal by typing cd [space after cd] and then dragging your conversion folder onto the Terminal window and hit 'Enter,' then type ./convert.sh) and automatically convert any files within the directory using the chosen preset. I would recommend just keeping a folder around that you use for conversions and keep the script and HandBrakeCLI binary in there at all times, then you can just drop in the files you want to convert, start the script and go along your merry way.

UPDATE (12/20/2013): An anonymous reader shared his script, which sounds much more robust than mine:
inotifywait -r --monitor --quiet -e moved_to -e close_write --format '%w%f' /mnt/public/convert/video/android-hq/ | while read -r FILE; do echo "File copy detected" echo "Starting HandBrake..." (sleep 15 && /usr/bin/HandBrakeCLI -v -i "$FILE" -o /mnt/public/convert/output/video/"$(echo "$FILE" | cut -c38- | rev | cut -c4-| rev)android-hq.mkv" -e x264 -x weightp=0:cabac=0 -b 650 --audio 1 --aencoder faac --ab 96 --mixdown stereo --gain 3 --width 720 --loose-crop --decomb --markers --turbo --two-pass --vfr --subtitle 1 --native-language eng && sleep 15 && rm -rf "$FILE")& done
UPDATE (9/27/2014): The same user (name is teeedubb, apparently) is back with an update. This was posted in the comments but got mangled, so I tried to piece it back together:
I'm back - I have revisited the above script because it had some glaring faults - it would start a handbrakecli process for each file, which when encoding a seasons worth of tv shows it would bring my pc to its knees, it didnt handle files in sub directories, didnt handle multiple profiles and probably would have given undesirable results if a file extension had more than 3 character, plus it was kinda messy. New version below solves these issues: It queues encodes using task-spooler (tsp package in ubuntu), works with sub-directories in the watch folder (and deletes any empty subdirectories within watch folders) and supports multiple profiles. Options are 'watch directory', 'output directory' and 'task-spooler slots' (concurrent jobs) which are set through the variables. You can set multiple handbrake profiles which the script uses based on which directory the files are copied into, if files are copied into the root watch directory the first profile is used. Options for profiles are: 'name', profile name which will be appended to the transcoded file (this needs to match the profile directory and be fairly unique - script searches the input file location for profile name, so a profile called 'android' could confuse the script when transcoding the movie 'android cop'), 'handbrake settings', settings for handbrake to use (I'm pretty sure you could use --preset XXXX here as well), 'file extension', file extension for handbrake to use on output file and 'delete source file', whether or not to delete the source video file. You can create extra profiles by adding variables beginning with PRESET2 etc and adding elif entries to the if/then statement in the script. By default the script attempts to delete any empty subdirectories *and its ancestors* when completing a transcode, so you need to ensure you profile directories are not empty - I have done this by creating a hidden file in each profile dir and making the file un-deletable (eg: profile1 dir is /mnt/public/convert/video/android-hq, run the command touch /mnt/public/convert/video/android-hq/.android-hq && sudo chattr +i /mnt/public/convert/video/android-hq/.android-hq).
and here's the script:
#!/bin/bash
#script to watch a directory for incoming files and pass them onto HandBrakeCLI

WATCH_DIR="/mnt/public/convert/video/"
OUTPUT_DIR="/mnt/public/convert/output/video/"
TASK_SPOOLER_SLOTS=2

##HANDBRAKE PROFILE 1
PRESET1_NAME="android-hq"
PRESET1_HANDBRAKE_SETTINGS=" -e x264 -x weightp=0:cabac=0 -b 650 --audio 1 --aencoder faac --ab 96 --mixdown stereo --gain 3 --width 720 --loose-crop --decomb --markers --turbo --two-pass --vfr --subtitle 1 --native-language eng"
PRESET1_FILE_TYPE="mkv"
PRESET1_DELETE_SOURCE="yes"

###########################

ts -S $TASK_SPOOLER_SLOTS
inotifywait --recursive --monitor --quiet -e moved_to -e close_write --format '%w%f' "$WATCH_DIR" | while read -r INPUT_FILE; do

PRESET_NAME="$PRESET1_NAME"
HANDBRAKE_SETTINGS="$PRESET1_HANDBRAKE_SETTINGS"
FILE_TYPE="$PRESET1_FILE_TYPE"
DELETE_SOURCE="$PRESET1_DELETE_SOURCE"

if [[ $(echo "$WATCH_DIR" | grep -i "$PRESET1_NAME") ]] ; then
PRESET_NAME="$PRESET1_NAME"
HANDBRAKE_SETTINGS="$PRESET1_HANDBRAKE_SETTINGS"
FILE_TYPE="$PRESET1_FILE_TYPE"
DELETE_SOURCE="$PRESET1_DELETE_SOURCE"
fi
FULL_FILE_NAME=$(echo ${INPUT_FILE##*/})
OUTPUT_FILE=$(echo ${FULL_FILE_NAME%.*})
tsp bash -c 'nice -n 19 /usr/bin/HandBrakeCLI -v -i "$0" -o "$1""$2"-"$3"."$4" "$5" && if [[ "$6" = yes ]] ; then sleep 15 ; rm -f "$0" ; fi ; rmdir -p "$(dirname "$0")"' "$INPUT_FILE" "$OUTPUT_DIR" "$OUTPUT_FILE" "$PRESET_NAME" "$FILE_TYPE" "$HANDBRAKE_SETTINGS" "$DELETE_SOURCE"
done

Analytics Tracking Footer