I want to convert a list of flac files to mp3 using ffmpeg.
I have written the list of files to convert in a file.
Here is my script
#!/bin/bash
while read -r line
do
ffmpeg -i "$line" -ab 320k "${line%.flac}.mp3"
done < flac_list
It works, however when a filename contains a single quote, it does not work.
And here begins my escaping nightmare.
I have found dozens of combinations without find how to make it work.
Could someone help ?
Thanks to #chepner, the -nostdin flag appended to ffmpeg solves the issue.
I have found too that ffmpeg has surprising problems sometimes if it encounters certain characters in the filename. I stumbled over this problem when converting m4a to mp3 using a script. I didn't know that an innocent single-quote is one of them.
What you can do - aside of reporting a bug to ffmpeg - is to test, whether your filename contains an unwanted character, and either rename the file or create a symbolic link to the file, using a "good" name, and undo these changes when your conversion is done.
Related
I'm looking for some help.
What I'm trying to do is to have AppleScript open a folder of videos, then get QuickTime to randomly trim them (as in the start frame from where it trims) and then have a variable length of the trim itself (say random between 1sec to 2sec as boundaries for the new 'clip'). After trimming and creating the new 'edit' (it would add this new random trimming to all the videos in the folder then add to timeline). QT then needs to export the edit to a new folder.
In summary, trying to make a quick auto-editing app that can just pick random selects from a folder full of videos then save an edit to a new video.
ANY help with this would be hugely appreciated!
I've been trying but to nail avail (I'm fairly new to coding to going around in circles).
Thanks!
Dylan
Like #CJK, I would recommend you use bash and ffmpeg as they are both available on macOS and both better known and more widely applicable than Applescript and QuickTime.
IMHO, as Apple does not ship a package manager, you would be well advised to use homebrew to install, update and remove packages. It is available on the homebrew website.
Once you have that, you can find any package you want with:
brew search packageXYZ
So, you can now install ffmpeg with:
brew install ffmpeg
Now you would want a bash script that:
loops through all your ".mov" files in a directory
gets their lengths
calculates a random duration
calculates a random start time
extracts that piece of video as a clip
and, finally at the end, assembles all the clips together
That will look something like this, which I would suggest you save in a bash script called $HOME/RandomClips
#!/bin/bash
# Set up globbing
shopt -s nullglob nocaseglob
# Clear list of files we are going to concatenate
> list.txt
N=1
# Loop through all ".MOV" files
for f in *.MOV ; do
# Tell user which one we are processing
echo Processing file $f
# Get length of this video in seconds
duration=$(mdls -raw -name kMDItemDurationSeconds "$f")
echo ... Duration: $duration
# Generate a random clip length less than 5 seconds
((seconds=RANDOM%5))
echo ... Clip length: $seconds
# Generate start time
((start=RANDOM%(duration-seconds)))
echo ... Start time: $start
# Extract clip into file called "Clip-1.mov", "Clip-2.mov" etc
clipname="Clip-$N.mov"
echo ... Extracting $clipname
ffmpeg -hide_banner -ss $start -i "$f" -t $seconds -pix_fmt yuv420p "$clipname"
# Add name of this clip to the list of files to concatenate at the end
echo "file $clipname" >> list.txt
# Increment clip counter
((N=N+1))
done
# Now join together all the extracted clips into a single file
ffmpeg -hide_banner -f concat -i list.txt -c copy -pix_fmt yuv420p mergedVideo.mov
Now you need to make that executable, just necessary once, with:
chmod +x $HOME/RandomClips
Then use cd to navigate to a directory of movies:
cd some/place/with/movies
and run the script with:
$HOME/RandomClips
The script is not the most robust or well-tested in the entire world but it should be 90+% good. You may have to resize videos to a fixed size so they all match formats. Likewise with codecs. Probably ask another question if that becomes an issue.
No-one said answers have to be 100% perfect, and as no-one else has suggested anything, this will hopefully get you well on your way.
If you want to debug the script, you can:
read the debug output line by line
check all the extracted clips look correct, i.e, "Clip-1.mov", "Clip-2.mov"
read the file called "list.txt" to see if it contains all the clips
So, I made a script for Cygwin that uses Windows's ImageMagick and FFmpeg, but I am not sure if the results here will also apply for bash on Linux. So, what the script does is I have some cartoon video files and I'm using Waifu2x to enhance and upscale the images to 4K, and then using ImageMagick to pipe it to FFmpeg, which is also used to resize it to 3840x2160 in case the resolution is slightly different. Here's a small script I wrote for this example to simplify how it outputs to FFmpeg, as the real script is extremely lengthy and complex.
#!/bin/bash
fun(){
convert out.png JPG:-|tee "$outfile"
}
fun|ffmpeg -f image2pipe -r 60 -i - -c:v libx265 -movflags +faststart "$outputfile"
Now, what I noticed is that if FFmpeg fails to encode, the function continues but fails to output to $outfile. What I want to do is have it able to output to that file in case the encoding fails since I also write all the images to a cache folder for FFmpeg to run through in case the encoding fails, but I also want to write to both the pipe for FFmpeg and the file at the same time. What seems to be happening is that the command tee appears to be refusing to write to the file if it can't write to the pipe. I'm not sure if this behavior is intended, and/or if it also does this on Linux bash. How can I get around this and have it write to the file even if it can't write to the pipe, but write to both at the same time rather than writing to the file and attempting to read it back to the pipe?
Have you tried tee with the -p option? It makes tee continue writing even if tee can't write to its standard output, which in your case means it should cope if ffmpeg fails.
fun() {
convert out.png JPG:- | tee -p "$outfile"
}
I am trying to use youtube-dl to get the urls of some videos and then pipe the resulting urls into the input of my script. So in my terminal I do
youtube-dl --ignore-config -iga ~/Desktop/youtube/videolist.txt | myscript.sh
In my script I define things as
command='ffmpeg'
inputArgs='-i'
outputArgs='-c:v libx264 -preset ultrafast -qp 0'
directory="${HOME}/Desktop/Videos/"
output="video${count}"
extension='mp4'
I test it with echo to make sure everything appears in the correct order.
echo "${command}" "${inputArgs}" "${input}" "${outputArgs}" \
"${directory}""${output}${count}"."${extension}"
And the output from that looks correct. But when I try to run the same thing without the preceding echo command, i.e.,
"${command}" "${inputArgs}" "${input}" "${outputArgs}" \
"${directory}""${output}${count}"."${extension}"
I get an error message that says
At least one output file must be specified.
So it seems pretty obvious to me that I'm doing something wrong when attempting to execute it.
I have tried:
quoting the entire line as a whole
quoting different sections together
using the exec command in front of everything
No matter what I do, an error occurs at some point in the process. I know it's something simple I'm doing wrong. Would someone please enlighten me as to what that might be?
I feel very strongly that the . shouldn't just be in the middle of everything like that, but I really don't know.
Again, everything looks as it should when I run echo before the string of shell parameters.
If more of the script I'm using is needed to understand what I'm talking about, that is not a problem.
The problem is that because you put it in quotes "${outputArgs}" is expanded as a single argument. It doesn't get split up into separate arguments so ffmpeg only see it as a single -c option with a really long stream specifier. The next argument, the output file is interpreted as the codec instead.
To fix the problem simply remove the quotes:
"$command" $inputArgs "$input" $outputArgs "$directory$output$count.$extension"
I removed the curly braces ({}) just to save space. There's nothing wrong with using them if you prefer.
You tried to rely on a string to convey multiple arguments. You probably would want to use an array in all cases like this. An array is easy to use and more versatile (works with any arbitrary strings) and you don't have to walk on that many eggshells in order to avoid quirks and security holes, unlike when leaving quotes off.
command='ffmpeg'
inputArgs=(-i)
outputArgs=(-c:v libx264 -preset ultrafast -qp 0
-metadata 'title=another * string'
-metadata 'artist=; echo "Just a string'
-metadata "comment=Processed by my ${command} script.")
directory="${HOME}/Desktop/Videos/"
output="video${count}"
extension='mp4'
outputArgs+=(-metadata "track=${count}")
When expanding an array, the reference must have the {} around it. When used in the form: "${name[#]}", it behaves as if you had typed the contents on the line directly.
printf '%q\n' is a more useful way of examining a command compared to echo, as it can clearly tell you what belongs to which separate argument.You can also expand an array into another array:
whole_thing=("${command}" "${inputArgs[#]}" "${input}"
"${outputArgs[#]}"
"${directory}/${output}"."${extension}")
#This will output lines you can paste
# into bash to recreate the array:
printf 'whole_thing=()\n'
printf 'whole_thing+=(%q)\n' "${whole_thing[#]}"
#This will run the command:
"${whole_thing[#]}"
Short story: I'm trying to write a script that will use FFmpeg to convert the many files stored in one directory to a "standard" mp4 format and save the converted files in another directory. It's been a learning experience (a fun one!) since I haven't done any real coding since using Pascal and FORTRAN on an IBM 370 mainframe was in vogue.
Essentially the script takes the filename, strips the path and extension off it, reassembles the filename with the path and an mp4 extension and calls FFmpeg with some set parameters to do the conversion. If the directory contains only video files with without spaces in the names, then everything works fine. If the filenames contain spaces, then FFmpeg is not able to process the file and moves on to the next one. The error indicates that FFMpeg is only seeing the filename up to the first space. I've included both the script and output below.
Thanks for any help and suggestions you may have. If you think I should be doing this in another way, please by all means, give me your suggestions. As I said, it's been a long time since I did anything like this. I'm enjoying it though.
I've include the code first followed by example output.
for file in ./TBC/*.mp4
do
echo "Start of iteration"
echo "Full text of file name:" $file
#Remove everything up to "C/" (filename without path)
fn_orig=${file#*C/}
echo "Original file name:" $fn_orig
#Length of file name
fn_len=${#fn_orig}
echo "Filename Length:" $fn_len
#file name without path or extension
fn_base=${fn_orig:0:$fn_len-4}
echo "Base file name:" $fn_base
#new filename suffix
newsuffix=".conv.mp4"
fn_out=./CONV/$fn_base$newsuffix
echo "Converted file name:" $fn_out
ffmpeg -i $file -metadata title="$fn_orig" -c:v libx264 -c:a libfdk_aac -b:a 128k $fn_out
echo "End of iteration"
echo
done
echo "Script completed"
With the ffmpeg line commented out, and two files in the ./TBC directory, this is the output that I get
Start of iteration
Full text of file name: ./TBC/Test file with spaces.mp4
Original filename: Test file with spaces.mp4
Filename Length: 25
Base filename: Test file with spaces
Converted file name: ./CONV/Test file with spaces.conv.mp4
End of iteration
Start of iteration
Full text of file name: ./TBC/Test_file_with_NO_spaces.mp4
Original file name: Test_file_with_NO_spaces.mp4
Filename Length: 28
Base file name: Test_file_with_NO_spaces
Converted file name: ./CONV/Test_file_with_NO_spaces.conv.mp4
End of iteration
Script completed
I won't bother to post the results when ffmpeg is uncommented, other than to state that it fails with the error:
./TBC/Test: No such file or directory
The script then continues to the next file which completes successfully because it has no spaces in its name. The actual filename is "Test file with spaces.mp4" so you can see that ffmpeg stops after the word "Test" when it encounters a space.
I hope this has been clear and concise and hopefully someone will be able to point me in the right direction. There is a lot more that I want to do with this script such as parsing subdirectories and ignoring non-video files, etc.
I look forward to any insight you can give!
try quoting you output file:
ffmpeg -i "$file" ... "$fn_out"
bash separates arguments based on spaces, so you have to tell him that $fn_out is one single argument; whence the "" to show that this is one argument.
There is another edge-case where spaces break bash for loops.
"BASH for loop works nicely under UNIX / Linux / Windows and OS X while working on set of files. However, if you try to process a for loop on file name with spaces in them you are going to have some problem. For loop uses $IFS variable to determine what the field separators are. By default $IFS is set to the space character..."
https://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html
Before:
for file in $(find . -name '*.txt'); do echo "$file"; done
Outputs:
./files/my
documents/item1.txt
./files/my
documents/item2.txt
./files/my
documents/item3.txt
Therefore you should set IFS to ignore spaces.
After:
IFS=$'\n'
for file in $(find . -name '*.txt'); do echo "$file"; done
Outputs:
./files/my documents/item1.txt
./files/my documents/item2.txt
./files/my documents/item3.txt
I know there are many of these questions out there already, but my situation is unique enough that none of them have helped me thus far.
I'm writing a script that will reformat my (poorly tagged) music library. It's currently in the form of Music/Genre/Artist/Album/##-Title.wav.
From the Music directory, I run my script1, which calls find -name "*.wav" -exec script2 {} ${other_args}. Script2 parses the path to variables, creates the appropriate output directory if it doesn't exist, and then calls ffmpeg to do the compression.
ex: ($sourcefilepath contains {} from find)
ffmpeg -i "$sourcefilepath" "$path/$to/$outfile.$extension"
If my file name or path contains no spaces, it works like a charm.
If it contains spaces, ffmpeg fails, with the following error:
Blues/Country Blues/Rhythm, Country And Blues/08-Natalie Cole & Reba
McEntire Since I Fell For You.wav: No such file or directory
Note: that's the source file it can't find. The output file appears to be working correctly, even with spaces. That's not the full command I'm running (it has some -metadata flags too, but that also appears to be working correctly. I'm debugging with the command I wrote above).
The interesting thing here is that ffmpeg is NOT interpreting $sourcefilepath as two args, it just can't find the file I specified. Running the following from the Music directory works.
ffmpeg -i Blues/Country\ Blues/Rhythm\,\ Country\ And\ Blues/08-Natalie\ Cole\ \&\ Reba\ McEntire\ \ \ Since\ I\ Fell\ For\ You.wav output.flac
I'm kind of stuck here. The other information on the web involves getting a command to see a variable with a space as one arg instead of multiple. I appear to have done that, but ffmpeg can't find my file now.
Normally I'd just use find -name "*.wav" -exec ffmpeg -i {} ${args} \; but I need to process the file path to extract my metadata.
I'm fairly new to shell scripting; does anybody know what's going on here?
My psychic powers suggest that at some point in your code, you're doing something along the lines of:
something=$(echo $filename | something)
This should instead have been:
something=$(echo "$filename" | something)
Here's a script demonstrating why this is a problem:
#!/bin/sh
filename="Reba McEntire Since I fell"
echo "$filename" # Correct, quoted
echo $filename # Incorrect, unquoted
It outputs:
Reba McEntire Since I fell
Reba McEntire Since I fell
You'll notice that
the first line has three spaces after the artist, as in your correct command.
the second line has one space after the artist, as in your error message.
This is due to unquoted variable expansion undergoing wordsplitting. echo then concatenates the arguments with single spaces. The net result is multiple whitespace coalescing into one space.