I have a lot of videos cut into three parts, and I want to concat them.
I now have the following bash to output videos in the folder to a file named mergedVideo.mp4.
for f in *.mp4 ; do echo file \'$f\' >> fileList.txt;done
ffmpeg -f concat -safe 0 -i fileList.txt -c copy mergedVideo.mp4
All my input files are like Something part1.mp4, Something part2.mp4, Something part3.mp4
What I want is to output a file named Something.mp4
It is possible?
How can I modify my bash to achieve that~?
Would you please try the following:
#!/bin/bash
pat="^([^[:space:]]+)[[:space:]]*part[[:digit:]]+\.mp4$"
for f in *.mp4; do
echo file \'$f\' >> fileList.txt
[[ $f =~ $pat ]] && mergedvideo="${BASH_REMATCH[1]}.mp4"
done
ffmpeg -f concat -safe 0 -i fileList.txt -c copy "$mergedvideo"
Explanation of the regex $pat:
^([^[:space:]]+) matches a non-space substring from the beginning of the
string $f and is assigned to the shell variable BASH_REMATCH[1].
As for the provided example filenames, BASH_REMATCH[1] will be assigned to Something.
[[:space:]]* matches a zero or more whitespace(s).
part[[:digit:]]+ matches a string "part" followed by digit(s).
\.mp4 matches the suffix.
[Alternative]
If you are familiar with sed as well, the following may be more readable and maintainable:
mergedvideo=$(sed -E 's/[[:space:]]*part[[:digit:]]+//' <<< "$f")
I extend tshiono's answer to be able to manipulate all the files in the same folders (just like Marinos An mentioned, the idea is to merge files with the same prefix)
Cases like a folder with the following files:
Monday Blue part1.mp4
Monday Blue part2.mp4
Tuesday.mp4
Wednesday part1.mp4
Wednesday part2.mp4
Wednesday part3.mp4
Expected Output:
Monday Blue.mp4
Tuesday.mp4
Wednesday.mp4
#!/bin/bash
pat="^(.+)[[:space:]]+part[[:digit:]]+\.mp4$"
for f in *.mp4; do
if [[ $f =~ $pat ]]; then
echo file \'$f\' >> "${BASH_REMATCH[1]}.txt"
fi
done
for f in *.txt; do
ffmpeg -f concat -safe 0 -i "${f%%.*}.txt" -c copy "${f%%.*}.mp4"
done
Related
I have some audio recorded in Audacity 3.2.3 that I have exported as an mp3 and a flac. Then I have this file split_by_silence.sh
Which has hardcoded input path values that take an input file, split it by detecting silence, and then finally run an ffmpeg command to split the files. If you save the below code into a file split.sh, you can call it with the command $ ./split_by_silence.sh "value1" "value2"
# ./split_by_silence.sh "full_lowq.flac" %03d_output.flac
#IN=$1
#OUT=$2
OUT="%03d_output.flac"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dog on fire.flac"
OUTPUT_LOCATION="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/"
true ${SD_PARAMS:="-18dB"};
true ${MIN_FRAGMENT_DURATION:="20"};
export MIN_FRAGMENT_DURATION
if [ -z "$OUT" ]; then
echo "Usage: split_by_silence.sh full.mp3 output_template_%03d.mp3"
echo "Depends on FFmpeg, Bash, Awk, Perl 5. Not tested on Mac or Windows."
echo ""
echo "Environment variables (with their current values):"
echo " SD_PARAMS=$SD_PARAMS Parameters for FFmpeg's silencedetect filter: noise tolerance and minimal silence duration"
echo " MIN_FRAGMENT_DURATION=$MIN_FRAGMENT_DURATION Minimal fragment duration"
exit 1
fi
#
# get comma separated list of split points (use ffmpeg to determine points where audio is at SD_PARAMS [-18db] )
#
echo "_______________________"
echo "Determining split points..." >& 2
SPLITS=$(
ffmpeg -v warning -i "$IN" -af silencedetect="$SD_PARAMS",ametadata=mode=print:file=-:key=lavfi.silence_start -vn -sn -f s16le -y /dev/null \
| grep lavfi.silence_start= \
| cut -f 2-2 -d= \
| perl -ne '
our $prev;
INIT { $prev = 0.0; }
chomp;
if (($_ - $prev) >= $ENV{MIN_FRAGMENT_DURATION}) {
print "$_,";
$prev = $_;
}
' \
| sed 's!,$!!'
)
echo "SPLITS= $SPLITS"
#
# Add 5 seconds to each of the comma separated numbers
#
# Convert the comma-separated string into an array
arr=($(echo $SPLITS | tr ',' '\n'))
# Initialize a new array to store the results
new_arr=()
# Iterate through each element and add 5 seconds of padding
for i in "${arr[#]}"; do
result=$(echo "$i + 5" | bc -l)
new_arr+=("$result")
done
# Convert the array back into a comma-separated string
NEW_SPLITS=$(IFS=,; echo "${new_arr[*]}")
# Print the result
echo "NEW_SPLITS= $NEW_SPLITS"
SPLITS=$NEW_SPLITS
#
# Print how many tracks should be exported
#
res="${SPLITS//[^,]}"
CHARCOUNT="${#res}"
num=$((CHARCOUNT + 2))
echo "Exporting $num tracks"
echo "_______________________"
#
# Split audio into individual tracks
#
current_directory=$(pwd)
cd "$OUTPUT_LOCATION"
echo "Running ffmpeg command: "
ffmpeg -i "$IN" -c copy -map 0 -f segment -segment_times "$SPLITS" "$OUT"
#ffmpeg -i "full_lowq.flac" -c copy -map 0 -f segment -segment_times "302.825,552.017" "%03d_output.flac"
echo "Done."
cd $current_directory
echo "running flac command"
# check flac file intrgrity
If I call this code for my flac file:
OUT="%03d_output.flac"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dog on fire.flac"
The outputted files have an incorrect metadata for the length. They all report as having the same length, but if i import any of them into audacity, the file has a correct length.
but if i run this for my mp3 file, we can see the correct length metadata:
OUT="%03d_output.mp3"
IN="/mnt/e/martinradio/rips/vinyl/WIP/Dogs On Fire (1983, Vinyl)/dogs on fire.mp3"
So there is something with my ffmpeg command that causes it to export flac files with wrong 'length' metadata
ffmpeg -i "$IN" -c copy -map 0 -f segment -segment_times "$SPLITS" "$OUT"
I've tried with the flac example to change -c copy to -c:a flac, but that just gives every output flac file a length of 00:00:00
is it a problem with my ffmpeg command? Or my files? https://file.io/tIFsa1l70076
it works for mp3 files just fine, why does it have this issue with flac?
I have many files not having consistent filenames.
For example
IMG_20200823_1.jpg
IMG_20200823_10.jpg
IMG_20200823_12.jpg
IMG_20200823_9.jpg
I would like to rename all of them and ensure they all follow same naming convention
IMG_20200823_0001.jpg
IMG_20200823_0010.jpg
IMG_20200823_0012.jpg
IMG_20200823_0009.jpg
Found out it's possible to change for file having only a number using below
printf "%04d\n"
However am not able to do with my files considering they mix string + "_" + different numbers.
Could anyone help me ?
Thanks !
With Perl's standalone rename or prename command:
rename -n 's/(\d+)(\.jpg$)/sprintf("%04d%s",$1,$2)/e' *.jpg
Output:
rename(IMG_20200823_10.jpg, IMG_20200823_0010.jpg)
rename(IMG_20200823_12.jpg, IMG_20200823_0012.jpg)
rename(IMG_20200823_1.jpg, IMG_20200823_0001.jpg)
rename(IMG_20200823_9.jpg, IMG_20200823_0009.jpg)
if everything looks fine, remove -n.
With Bash regular expressions:
re='(IMG_[[:digit:]]+)_([[:digit:]]+)'
for f in *.jpg; do
[[ $f =~ $re ]]
mv "$f" "$(printf '%s_%04d.jpg' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}")"
done
where BASH_REMATCH is an array containing the capture groups of the regular expression. At index 0 is the whole match; index 1 contains IMG_ and the first group of digits; index 2 contains the second group of digits. The printf command is used to format the second group with zero padding, four digits wide.
Use a regex to extract the relevant sub-strings from the input and then pad it...
For each file.
Extract the prefix, number and suffix from the filename.
Pad the number with zeros.
Create the new filename.
Move files
The following code for bash:
echo 'IMG_20200823_1.jpg
IMG_20200823_10.jpg
IMG_20200823_12.jpg
IMG_20200823_9.jpg' |
while IFS= read -r file; do # foreach file
# Use GNU sed to extract parts on separate lines
tmp=$(<<<"$file" sed 's/\(.*_\)\([0-9]*\)\(\..*\)/\1\n\2\n\3\n/')
# Read the separate parts separated by newlines
{
IFS= read -r prefix
IFS= read -r number
IFS= read -r suffix
} <<<"$tmp"
# create new filename
newfilename="$prefix$(printf "%04d" "$number")$suffix"
# move the files
echo mv "$file" "$newfilename"
done
outputs:
mv IMG_20200823_1.jpg IMG_20200823_0001.jpg
mv IMG_20200823_10.jpg IMG_20200823_0010.jpg
mv IMG_20200823_12.jpg IMG_20200823_0012.jpg
mv IMG_20200823_9.jpg IMG_20200823_0009.jpg
Being puzzled by your hint at printf...
Current folder content:
$ ls -1 IMG_*
IMG_20200823_1.jpg
IMG_20200823_21.jpg
Surely is not a good solution but with printf and sed we can do that:
$ printf "mv %3s_%8s_%d.%3s %3s_%8s_%04d.%3s\n" $(ls -1 IMG_* IMG_* | sed 's/_/ /g; s/\./ /')
mv IMG_20200823_1.jpg IMG_20200823_0001.jpg
mv IMG_20200823_21.jpg IMG_20200823_0021.jpg
my script is not giving me any output
for file in ./*
do
if [ "$file" = *.mov ];
then
ffmpeg -i $file -an -f framemd5
fi
done
whenever I run it it just give me back my prompt immediately.
Because your test:
if [ "$file" = *.mov ];
is false.
If, for exampe, the current directory has these files:
file1.mov
file2
file3.txt
file4.mov
Then the $file variable will be set as follows through each iteration:
./file1.mov
./file2
./file3.txt
./file4.mov
But the right-hand side of the test will remain to be "file1.mov file4.mov", so each test is:
if [ "./file1.mov" = file1.mov file4.mov];
if [ "./file2" = file1.mov file4.mov];
if [ "./file3.txt" = file1.mov file4.mov];
if [ "./file4.mov" = file1.mov file4.mov];
...neither of which is ever true.
If you want to loop through all the .mov files in the current directory, use this instead:
for file in ./*.mov; do
ffmpeg -i $file -an -f framemd5
done
By the way, you should always be ready for files with spaces and other annoying characters in the name, so this would be a bit more robust:
for file in ./*.mov; do
ffmpeg -i "$file" -an -f framemd5
done
As it will put quotes around the file name.
Assuming you are using bash, you need to use the [[ command for pattern matching.
for file in ./*
do
if [[ "$file" = *.mov ]];
then
ffmpeg -i "$file" -an -f framemd5
fi
done
However, it's simpler to just match the .mov files in the first pattern as shown by dovetalk.
I am trying to build a script to do as the title says, but I am somewhat unfamiliar with Bash and other online resources have only been so helpful.
#! /bin/bash
function inout #Create Function inout
{
output[0]=" " #Initialize variables
input[0]=" "
count=1
while [ "$count" -lt 10 ]; #Start loop to get all filenames
do
echo "Grabbing filename" #User feedback
input=$(ls | grep 0$count | grep MID | sed 's/ /\\ /g') #Grab filename
#Replace ' ' character with '\ '
output=$(echo $input | tr 'MID' 'mp3')
#set output filename
echo $count #Output variables for testing
echo $input
echo $output
let count+=1 #Increment counter
echo "converting $input to $output." #User feedback
foo="timidity $input -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 320k $output"
echo $foo
#The last two lines are for the purpose of testing the full output
#I can get the program to run if I copy and paste the output from above
#but if I run it directly with the script it fails
done
}
inout
I am trying to figure out why I can't just run it from inside the script, and why I must copy/paste the output of $foo
Any ideas?
It's impossible to tell from your code how the input files are named; I'll assume something like song_02.MID:
inout () {
for input in song_*.MID; do
output=${input%.MID}.mp3
timidity "$input" -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 320k "$output"
done
}
They key is to define an appropriate pattern to match your input files, then iterate over the matching files with a for loop.
Also, your use of tr is incorrect: that call would replace any occurrence of M, I, or D with m, p, and 3, respectively; it does not replace occurrences of the 3-character string MID with mp3.
here's a script.. just wanna practise some bash skills and make a quick util for my chinese mp4 player =)
#!/bin/bash
#####################################
# RockChip 4gb Player mencoder preset
#####################################
TOOL='mencoder'
OUTS='./out/'
OPT1='-noodml'
OPT2="-of avi -ofps 22 -vf-add scale=320:-2,expand=320:240 -srate 44100 -ovc xvid -xvidencopts bitrate=400:max_bframes=0:quant_type=s16le -oac lavc -lavcopts acodec=mp2:abitrate=128"
bold=`tput bold`
normal=`tput sgr0`
# check does argument exists
if test -z "$1"; then
echo "There's no file given =)"
fi
# Check is it dir or file
if [ -d $1 ]; then
echo "Directory is given: $1"
# Test if output argument is given
if [ -z $2 ]; then
echo "No output argument given using default: ${bold}${red}$OUTS${normal}"
mkdir out
else
# test is given path a directory
if [ -d $2 ]; then
OUT="$2"
else
echo "Output argument is not a directory"
fi
fi
OLD_IFS=IFS; IFS=$'\n'
for file in `find . -name "*.*" -type f | sed 's!.*/!!'` ; do
file=`printf "%q" "$file"`
echo ${TOOL} ${OPT1} ${file} -o ${OUTS}${file} ${OPT2}
done
IFS=OLD_IFS
fi
Problem is this line:
echo ${TOOL} ${OPT1} ${file} -o ${OUTS}${file} ${OPT2}
When you remove echo , to execute command, command fails, but if you'll copy this echoed script, and execute it manually everything works.
When executing command from shell script output is :
MEncoder 1.0rc4-4.2.1 (C) 2000-2010 MPlayer Team
158 audio & 340 video codecs
-of avi -ofps 22 -vf-add scale=320:-2,expand=320:240 -srate 44100 -ovc xvid -xvidencopts bitrate=400:max_bframes=0:quant_type=s16le -oac lavc -lavcopts acodec=mp2:abitrate=128 is not an MEncoder option
Exiting... (error parsing command line)
as I mentioned before executing command manualy everything works for example:
mencoder -noodml 12\ I\ Love\ You\ 1\ \ I\ Love\ You\ 2\ \ I\ Love\ You\ 3.avi -o ./out/12\ I\ Love\ You\ 1\ \ I\ Love\ You\ 2\ \ I\ Love\ You\ 3.avi -of avi -ofps 22 -vf-add scale=320:-2,expand=320:240 -srate 44100 -ovc xvid -xvidencopts bitrate=400:max_bframes=0:quant_type=s16le -oac lavc -lavcopts acodec=mp2:abitrate=128
now all I can do is to copy paste generated commands.. where's the problem? I tried to google really hard.. with no result... (I know that mencoder have profiles.. it's not the case where I want them)
You have (line 37 i believe):
OUT="$2"
but I think you meant:
OUTS="$2"
I'm not fully sure but maybe it's better to quote the file name with double quotes (") instead of doing printf "%q" "$file".
So replace:
file=`printf "%q" "$file"`
${TOOL} ${OPT1} ${file} -o ${OUTS}${file} ${OPT2}
with
${TOOL} ${OPT1} "${file}" -o "${OUTS}${file}" ${OPT2}
First, use $() instead of back ticks.
bold=$(tput bold)
normal=$(tput sgr0)
OLD_IFS=IFS; IFS=$'\n' should be OLD_IFS=$IFS . you want to get the value of IFS, so put a dollar sign.
You don't need to call sed to get the base name of files
while read -r file
do
filename="${file##*/}"
filename=$(printf "%q" $filename)
echo mencoder "${OPT1}" "${file}" -o "${OUTS}${file}" "${OPT2}"
done < <(find . -name "*.*" -type f)
lastly,
IFS=OLD_IFS should be IFS=$OLD_IFS
You have this statement before your for loop:
IFS=$'\n'
This sets your internal field separator to newlines, instead of the default of matching any whitespace. That changes how substituted parameters are parsed. In the line constructing the command:
${TOOL} ${OPT1} ${file} -o ${OUTS}${file} ${OPT2}
what will happen is that each of those ${variable} expressions will be expanded, and then the shell will try splitting them on \n, not on whitespace as you would normally expect. It will give you a similar result as the following would (unless one of these variables contained a newline):
"${TOOL}" "${OPT1}" "${file}" -o "${OUTS}${file}" "${OPT2}"
Here you can see that you're passing your entire ${OPT2} string in as a single parameter, rather than allowing Bash to split it on spaces and pass each flag in individually. mencoder then gets confused by this one huge parameter that it doesn't know how to deal with. Of course, since the spaces are all still there, they will be printed out by the echo command, and will work fine in a shell in which $IFS has not been reset.
You can demonstrate this effect pretty easily, by defining a simple function that will print each of its arguments on a separate line:
$ print_args() { for arg; do echo $arg; done }
$ foo="1 2"
$ print_args ${foo} ${foo}
1
2
1
2
$ IFS=$'\n'
$ print_args ${foo} ${foo}
1 2
1 2
I would recommend not using the $IFS trick for your for loop. Instead, you can use while read file to iterate over each line in the input. I'd also recommend not using printf "%q" for escaping spaces, but instead just quote the argument to mencoder, which will pass the whole thing in as a single argument. Note that I'm quoting ${file} and ${OUTS}${file} to make sure that they are passed in each as a single argument, but not quoting ${OPT1} and ${OPT2} in order to allow them to be parsed as separate arguments by the shell.
find . -name "*.*" -type f | sed 's!.*/!!' | while read -r file
do
"${TOOL}" ${OPT1} "${file}" -o "${OUTS}${file}" ${OPT2}
done
By the way, I'd recommend that you use $() for command substitution rather than ``; there are many reasons why it's preferable, such as readability, more sane quoting and escaping rules within it, and the ability to nest multiple levels of command substitution. And the problems that Jonathan and Wes point out are good to note, though they aren't what are causing your immediate problem.
Using ... | xargs bash -c '...' you just need to escape embedded single quotes in the file name.
TOOL='mencoder'
OUTS='./out/'
OPT1='-noodml'
OPT2='-of avi -ofps 22 -vf-add scale=320:-2,expand=320:240 -srate 44100 -ovc xvid -xvidencopts bitrate=400:max_bframes=0:quant_type=s16le -oac lavc -lavcopts acodec=mp2:abitrate=128'
# test cases
file='12 I Love You 1 I Love You 2 I Love You 3.avi'
file='12 I Lo"ve You 1 I Love You 2 I Love You 3.avi' # one embedded double quote
file='12 I Lo"ve You 1 I Lo"ve You 2 I Love You 3.avi' # two embedded double quotes
file="12 I Lo've You 1 I Love You 2 I Love You 3.avi" # one embedded single quote
file="12 I Lo've You 1 I Lo've You 2 I Love You 3.avi" # two embedded single quotes
# escape embedded single quotes in $file
escsquote="'\''"
file="${file//\'/${escsquote}}"
# we're passing ${TOOL} as arg0 to bash -c (which then is available as "$0")
# put the variables in the printf line into single quotes to preserve spaces
# remove no-op ':' to execute the command
printf '%s\n' "${file}"
printf '%s' "${OPT1} '${file}' -o '${OUTS}${file}' ${OPT2}" |
xargs bash -c 'set -xv; printf "%s\n" "$0" "$#" | nl' "${TOOL}"
printf '%s' "${OPT1} '${file}' -o '${OUTS}${file}' ${OPT2}" |
xargs bash -c 'set -xv; : "$0" "$#"' "${TOOL}"