How to loop through files in subdirectories? - bash

I'm trying to convert a bunch of .aac files to .mp3 files in separate subdirectories in a directory. This is the code I've written:
for dir in */
do
OUTPUT_DIR=./mp3
mkdir "$OUTPUT_DIR"
for i in *.aac;
do name=`echo "$i" | cut -d'.' -f1`
echo "$name"
ffmpeg -i "$i" -vn -ar 44100 -ac 2 -b:a 192k "$OUTPUT_DIR/${name}.mp3"
done
done
However this just try to run the commands in the outside directory and gives an error. I'm new at bash so it might be a very simple mistake I'm not seeing.

How to loop through files in subdirectories?
Read ex How to loop through a directory recursively to delete files with certain extensions
This is the code I've written:
Check your scripts with http://shellcheck.net
Do not use `. Use $(...) instead.
To get file name and dir see basename and dirname commands. Your name=echo "$i" | cut -d'.' -f1 will fail when directory has a dot.
Use printf "%s\n" "$i" or in bash <<<"$i" here string instead echo "$i" |. See ex. https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo .
Prefer lower case variables in your scripts.
so it might be a very simple mistake I'm not seeing.
You are not changing the direcotry at all. You need to cd somewhere to that dir and then cd - back (or pushd + popd). But just reference directories relative to your current dir anyway. Maybe something along:
for dir in ./*/; do
# create the directory in dir
output_dir="./$dir/mp3"
mkdir "$output_dir"
# find files aac inside $dir/
for i in "$dir"/*.aac; do
name=$(basename "$i" .acc)
echo "$name"
ffmpeg -i "$i" -vn -ar 44100 -ac 2 -b:a 192k "$output_dir/${name}.mp3"
done
done

You may want to use find to recursively find all files in all subdirectories of a directory.
Here's an example (it assumes you also want to recreate the subdirectories in the output directory).
OUTPUT_DIR=./mp3
for item in $(find . -type f -name *.aac); do
name="$(basename $item .aac)"
subdir="$(dirname $item)"
mkdir -p "$OUTPUT_DIR/$subdir"
ffmpeg -i "$name" -vn -ar 44100 -ac 2 -b:a 192k "$OUTPUT_DIR/$subdir/$name.mp3"
done

Related

Bash: loop ffmpeg command through sets of subfolders and direct it to files in the folders for processing

I am playing around with embedding captions into mp4 video files, and want to find a way to do this across large sets of directories with the .mp4 and .srt files in them. Each pair of .mp4 and .srt files will be subfoldered together in their own directory, and the basename should be the same between the two. Example:
Video1
Video1.mp4
Video1.srt
Video2
Video2.mp4
Video2.srt
I’ve tried several things but I’m a novice at this and only write very simple bash scripts for much more straightforward processes. For this I need to figure out how to write the bash script to run an ffmpeg command in every subfolder that will grab the mp4 and srt file and output a new mp4 of the merged data. The basic ffmpeg command to do this is:
ffmpeg -i filename.mp4 -i filename.srt -c copy -c:s mov_text output.mp4
I’ve tried to add:
for dir in ./*/; do ffmpeg -i *.mp4 -i *.srt -c copy -c:s move_text “$file”.mp4
…and several variations of this, but ffmpeg always stops with a “*.mp4: No such file or directory” error. Then I tried to add "for file in..." after the "for dir in" statement but didn't have any positive results. The following is closest to what I need - it at least goes to each folder and processes the files - but it does them independently and doesn't combine the mp4 and srt source files as the ffmpeg command should. It outputs a video.mp4.mp4 and video.srt.mp4, and fails to combine them in either case.
for dir in ./**/*;
do ffmpeg -i "$dir" -i "$dir" -c copy -c:s mov_text "$dir".mp4
I tried "$dir".mp4 and "$dir".srt but that just results in an error. I tried to pull just directory names:
for dir in ./**/*;
do ffmpeg -i "$(basename $dir)" -i "$(basename $dir)" -c copy -c:s mov_text "$dir".mp4
and my attempts using "$(basename $dir).extension" have resulted in errors - it looks for video.mp4.mp4 or video.srt.mp4. Any tips as to what to add to get this process to work or another approach entirely would be greatly appreciated! I figure it's a simple bash thing I'm just ignorant of, but certainly need to learn how to do! Thanks!
Run this within the dir containing Video1/, Video2/...
#!/bin/bash -e
shopt -s globstar
for v in ./**/*.mp4; do
s=${v%.*}.srt
if [ -f "$s" ]; then
ffmpeg -i "$v" -i "$s" -c copy -c:s mov_text "${v##*/}"
fi
done
./**/*.mp4 expands to ./Video1/Video1.mp4 ./Video1/Video2.mp4 ...,
${v%.*} removes the extension (./Video1/Video1.mp4 > ./Video1/Video1),
[ -f "$s" ] checks if $s (i.e. ./Video1/Video1.srt) exists,
${v##*/} extracts the basename of $v (./Video1/Video1.mp4 > Video1.mp4).
So the final structure of . will be like:
Video1.mp4 # subbed
Video1
Video1.mp4
Video1.srt
Video2.mp4 # subbed
Video2
Video2.mp4
Video2.srt
As a tweak to the excellent answer by ogizismail, the below is an approach that works with versions of bash too old to support globstar:
while IFS= read -r -d '' v; do
s=${v%.mp4}.srt
[[ -e $s ]] && ffmpeg -i "$v" -i "$s" -c copy -c:s mov_text "${v##*/}"
done < <(find . -mindepth 2 -name '*.mp4' -printf '%P\0')
The general technique is discussed in Using Find. Using -mindepth 2 stops it from finding your already-subbed output files.

Using bash to cycle through files in directory then create sub-directory based on file name

What I'm trying to do is use bash to
1) cycle through files in a directory
2) then create a sub-directory based on the file names
3) then split mp3 files using ffmpeg in 3 second increments.
I can get the sub-directories created and the ffmpeg code splits the files up using the correct names. The issue I seem to be having is the loop. It creates the split-chirp directory and all it's files but it doesn't process the pink.mp3 file. See image below of the starting directory
See image of ending directory
It doesn't create the split-pink directory and all the files (I know it's a looping problem just can't figure out why)
I run the code using the command bash mp3spl.sh *.mp3
Code below
#!/bin/bash
currentdir=$(pwd) #get current directory
for f in $currentdir/*.mp3
do
fn=`echo "$1" | cut -d'.' -f1` #get just the filename no extension
splitdirname="$currentdir/split-$fn" #sub directory with correct names
mkdir -p "$splitdirname" #make split directory
echo "Processing $f"
ffmpeg -i "$1" 2> tmp.txt
ffmpeg -i "$1" -f segment -segment_time 3 -ar 22050 -ac 1 "$splitdirname/$fn-%03d.mp3"
#rm tmp.txt
done
$1 is unnecessary, because you want to loop over all mp3 files.
fn=$(basename "$f" | cut -d'.' -f1) #get just the filename no extension
I left the rest untouched.
This will work with spaces in directories and filenames
#!/bin/bash
#run using bash mp3spl.sh
currentdir="$(pwd)" #get current directory
for f in *.mp3 #only look for mp3 files
do
fn=$(basename "$f" | cut -d'.' -f1) #get just the filename no extension
splitdirname="$currentdir/split-$fn" #sub directory with correct names
mkdir -p "$splitdirname" #make split directory
#echo "Processing $f"
ffmpeg -i "$1" 2> tmp.txt
ffmpeg -i "$f" -f segment -segment_time 1200 -ar 22050 -ac 1 "$splitdirname/%03d-$fn.mp3" #split every 20mins
#rm tmp.txt
done

Strange behavior about ”ls | while read" with ffmpeg in shell script [duplicate]

This question already has answers here:
Execute "ffmpeg" command in a loop [duplicate]
(3 answers)
Closed 4 years ago.
I created a shell script to convert all wave files to mp3 files. I use Ubuntu 18.04, FFmpeg was installed using apt, and I run my script in bash.
My script:
#!/bin/bash
ls *.wav | while read file
do
echo $file
ffmpeg -i "$file" -codec:a libmp3lame -b:a 192k "${file%.*}.mp3"
done
The target files are followings: (include space characters. The real filenames are longer, but I simplified the filenames. Also in this case, the problem occurs)
% ls *.wav
'01 A.wav' '02 A.wav' '03 A.wav'
The problem is that sometimes $file in the loop is blank or a part of filename strangely ('echo $file' shows that), and ffmpeg says '[broken filename]: No such file or directory'. I confirmed the following things.
When I comment out the ffmpeg line, 'echo' shows what I expected. ($file not broken)
When I replace ffmpeg to a similar command like 'lame', it works. ($file not broken)
When I replace 'ls *.wav | while read file' to 'for file in *.wav', it works. ($file not broken)
So, only when I use combination of 'ls' and 'ffmpeg', $file is broken. What's going on? Or do I misunderstand something?
You should not use ls to pipe a list of files, because this approach will not work well for corner cases like filenames with blanks.
The most readable way is to use globbing:
for file in *.wav
do
echo "$file"
ffmpeg -i "$file" -codec:a libmp3lame -b:a 192k "${file%.*}.mp3"
done
find also does a great job here, but you will end up with a little less maintainable code:
find -name *.wav -exec ffmpeg -i {} -codec:a libmp3lame -b:a 192k {}.mp3 \;
The find example will give you filenames ending in .wav.mp3 instead of .mp3, if you want to avoid that, you have to call a shell from find, allowing you to either use basename or do something like ${file.*} (you would have to assign find's {} to a variable like file in that shell first:
find -name "*.wav" -exec sh -c "file=\"{}\" ; ffmpeg -i \"\$file\" -codec:a libmp3lame -b:a 192k \${file%.*}.mp3" \;

Bash script that lists files in a directory doesn't work

I made a bash script because I need to convert a lot of files in a directory from .MOV to .mp4 format.
I created this script for the purpose:
#!/bin/bash
touch .lista
ls -1 "$1" | grep -i .MOV > .lista
list= `pwd`/.lista
cd "$1"
while read -r line;
do filename=${line%????}
ffmpeg -i "$line" -vcodec copy -acodec copy "$filename.mp4"; done < $list
rm .lista
This script is supposed to convert me each .MOV file into the directory indicated by $1, but it doesn't work, it converts me only one file, then it terminates. I can't understand why. What's wrong with that?
It's better to simply loop using globs:
for file in "$1"/*.MOV; do
ffmpeg -i "$file" ... "${file%.*}.mp4"
done
Why you shouldn't parse the output of ls.
Do them all fast and succinctly in parallel with GNU Parallel like this:
parallel --dry-run ffmpeg -i {} -vcodec copy -acodec copy {.}.mp4 ::: movies/*MOV
Sample Output
ffmpeg -i movies/a.MOV -vcodec copy -acodec copy movies/a.mp4
ffmpeg -i movies/b.MOV -vcodec copy -acodec copy movies/b.mp4
If that looks good, do it again but without --dry-run.
Note how easily GNU Parallel takes care of all the loops, all the quoting and changing the extension for you.
Your code is working for me. I cannot see any error. But I can suggest you a better approach. Don't use ls to get the filenames, it is not a good idea. Also, you can avoid changing dir.
#!/bin/bash
for line in $(find "$1" -maxdepth 1 -type f -iname "*.mov")
do
ffmpeg -i "$line" -vcodec copy -acodec copy "${line%????}.mp4"
done
You don't need to start by touching the file. In any case, you don't need a file at all, you can use a for loop to iterate over the files returned by find directly. With find, I'm already selecting all the files in the specified folder that have the expected extension.
Here I add a one-liner that should avoid problems with spaces:
find "$1" -maxdepth 1 -type f -iname "*.mov" -print0 | xargs -0 -n 1 -I{} bash -c "F={}; ffmpeg -i \"\$F\" -vcodec copy -acodec copy \"\${F%.*}\".mp4"

Bash Script to convert all flv file in a directory to mp3

This is my code so far.
#!/bin/bash
#James Kenaley
#Flv to Mp3 directory converter
find /home/downloads -iname "*.flv" | \
while read I;
do
`ffmpeg -i ${I} -acodec copy ${I/%.flv/.mp3}`
echo "$I has been converted"
done
but its picking up white spaces in the names of the flv files and throws a error saying its not in the directory. how do make it use the whole file name and not the just the first word before the space?
ffmpeg runs in forked threads, so simple batching can give weird behaviours. If you are running ffmpeg in the suggested batch loop, you should control your command and command-error output, so that it doesn't interfere.
If you run this and are getting every other item converted properly, but errors on the rest, try using this ffmpeg call in the loop:
ffmpeg -y -i "${I}" -acodec mp3 -ar 22050 -f wav "${I/%.3gp/.mp3}" > /dev/null & 2> /dev/null
Notice the > dev/null & 2> /dev/null on the end. This pipes the command output, and command error output into oblivion. Then the script works.
One should note too that the program output will look strangely disorganized, with multiple files compressing at the same time. The results will be correct.
[EDIT: NOTE THE -y THAT I HAVE, THIS MAKES FFMPEG OVERWRITE EXISTING MP3 FILES]
Try this:
`ffmpeg -i "${I}" -acodec copy "${I/%.flv/.mp3}"`
Use quotes. And don't use backquotes.
ffmpeg -i "${I}" -acodec copy "${I%.flv}".mp3
Either call a short script, to do conversion and renaming in one pass:
adhoc.sh:
$file="$1"
ffmpeg -i "$file" -acodec copy "${file/%.flv/.mp3}"
call it:
find /home/downloads -iname "*.flv" -exec ./adhoc.sh {} ";" -ls
or convert:
find /home/downloads -iname "*.flv" -exec ffmpeg -i {} -acodec copy {}.mp3 ";" -ls
and rename later:
rename 's/.flv.mp3/.mp3/' /home/downloads/*.flv.mp3
Rename is part of a perl package which might need installation.

Resources