Created
August 4, 2010 03:17
-
-
Save dmoo1790/507582 to your computer and use it in GitHub Desktop.
Script to cut MythTV recordings
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# MYTHCUTKEY Version 0.1 | |
# This script cuts myth recordings at key frames using the MythTV seek table. | |
# Why? No external programs required, lossless and fast. | |
# Updates the myth database with sql calls including rebuilding the seek table. | |
# Output files may NOT be easily editable again and may have glitches at cut points. | |
# | |
# Undo option allows restoration of original recording and associated database | |
# records including seek table and markup table. | |
# NOTE: The undo function requires a directory for which mysql has write permissions. | |
# The default is /tmp/ however you will lose your undo information when you reboot. | |
# Undo file names are the recording file base name with seekN.sql, markN.sql or recN.sql | |
# appended, where N is a number starting from zero and the highest number indicating the | |
# latest undo files. The undo files are not huge but may eat up disk space over time | |
# after many edits. | |
# | |
# The script does not delete the original recording so you need disk free space equal | |
# to something less than the original recording file size depending on how much you cut | |
# from the file. | |
OVERWRITE=0 | |
OUTPUT_DIR="" | |
CHANID=-9999 | |
STARTTIME="" | |
rec_dir="" | |
basename="" | |
USER="root" | |
PASS="" # Change this to "-p your_password" if necessary | |
UNDO=0 | |
UNDO_DIR="/tmp/" | |
STARTSECS=`date +%s` | |
USAGE="$0 -c channel_id -s starttime [-d dir] [-f filename] [-o directory] [-h] [-Z] | |
[-u undo_dir] [-U] | |
-h Help/usage | |
-c Channel ID (required, %CHANID% from Mythtv) | |
-s Start time (required, %STARTTIME% from Mythtv) | |
-d Directory where the recording is store (optional, %DIR% from Mythtv) | |
-f Recording file name (optional, %FILE% from Mythtv) | |
-o Output directory (optional, recording directory if not specified) | |
-Z Overwrite original recording (optional, default is false, ignored if -o specified) | |
-u Directory where undo info is saved (optional, mysql must have permission to write | |
to this directory, default is /tmp/) | |
-U Undo the last edit | |
Example usage overwriting original recording (original is renamed, not deleted) | |
mythcutkey -c %CHANID% -s %STARTTIME% -Z | |
Example usage to save the file to a different directory | |
mythcutnz -c %CHANID% -s %STARTTIME% -o /your/directory/ | |
Example usage specifying an undo directory | |
mythcutnz -c %CHANID% -s %STARTTIME% -Z -u /myundodir | |
Example usage to undo the last edit | |
mythcutnz -c %CHANID% -s %STARTTIME% -U -u /myundodir | |
Warning: The end files may not be editable and/or playable outside MythTV. Keep the original | |
recording if you think you may want to do further edits or conversions in future. This script does | |
NOT delete the original recording. It is renamed with the extension \".old\"." | |
while getopts "c:s:o:d:f:t:u:hZU" opt | |
do | |
case $opt in | |
c ) | |
CHANID="$OPTARG" | |
;; | |
s ) | |
STARTTIME="$OPTARG" | |
;; | |
d ) | |
rec_dir="$OPTARG" | |
;; | |
f ) | |
basename="$OPTARG" | |
;; | |
h ) | |
echo "Usage: $USAGE" >&2 | |
exit 0 | |
;; | |
o ) | |
OUTPUT_DIR="$OPTARG" | |
;; | |
u ) | |
UNDO_DIR="$OPTARG" | |
;; | |
Z ) OVERWRITE=1 # Will be reset to false if -o directory is specified | |
;; | |
U ) UNDO=1 | |
;; | |
? ) | |
echo "Invalid option: -$OPTARG" >&2 | |
echo "Usage: $USAGE" >&2 | |
exit -1 | |
;; | |
esac | |
done | |
if [ "$OUTPUT_DIR" != "" ]; then | |
OVERWRITE=0 | |
fi | |
if [ $CHANID == -9999 -o "$STARTTIME" == "" ]; then | |
echo "Channel ID and/or Start Time missing" >&2 | |
echo "Usage: $USAGE" >&2 | |
exit -1 | |
fi | |
############################################################################# | |
# Find where the recording is stored if not specified with -d and -f options | |
############################################################################# | |
if [ "$rec_dir" == "" -o "$basename" == "" ]; then | |
storage_dirs=`echo "select dirname from storagegroup;" | mysql -N -u $USER $PASS mythconverg` | |
basename=`echo "select basename from recorded where chanid=$CHANID and starttime=\"$STARTTIME\";" \ | |
| mysql -N -u $USER $PASS mythconverg` | |
#echo $basename | |
#echo $storage_dirs | |
found=0 | |
for f in $storage_dirs; do | |
if [ -e "$f$basename" ]; then | |
rec_dir=$f | |
found=1 | |
fi | |
done | |
echo $rec_dir$basename | |
if [ $found == 0 ]; then | |
echo "Can't find recording, aborting" >&2 | |
exit -1 | |
fi | |
fi | |
# Set output directory to recording directory if not specified with -o option | |
if [ "$OUTPUT_DIR" == "" ]; then | |
OUTPUT_DIR=$rec_dir | |
fi | |
if [ ! -d $UNDO_DIR ]; then | |
echo "Can't find undo directory "$UNDO_DIR", aborting" >&2 | |
exit -1 | |
fi | |
############################################################################# | |
# Undo the last edit if -U option specified | |
############################################################################# | |
if [ $UNDO == 1 ]; then | |
if [ ! -e $rec_dir$basename".old" ]; then | |
echo "Can't find original recording "$rec_dir$basename".old, aborting" >&2 | |
exit -1 | |
fi | |
# Don't delete. Just rename. | |
mv $rec_dir$basename $rec_dir$basename".new" | |
mv $rec_dir$basename".old" $rec_dir$basename | |
# Undo various database changes | |
num=0 | |
while [ -e $UNDO_DIR$basename.seek$num.sql ]; do | |
let num++ | |
done | |
let num-- | |
echo "delete from recordedseek where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
echo "load data infile \"$UNDO_DIR$basename.seek$num.sql\" into table recordedseek;" | mysql -u $USER $PASS mythconverg | |
echo "delete from recordedmarkup where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
echo "load data infile \"$UNDO_DIR$basename.mark$num.sql\" into table recordedmarkup;" | mysql -u $USER $PASS mythconverg | |
echo "delete from recorded where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
echo "load data infile \"$UNDO_DIR$basename.rec$num.sql\" into table recorded;" | mysql -u $USER $PASS mythconverg | |
exit 0 | |
fi | |
############################################################################# | |
# Edit the recording based on cuts at keyframes | |
############################################################################# | |
thefile=$rec_dir$basename | |
outfile=$OUTPUT_DIR$basename".new" | |
MARKTYPES="0,1" | |
# Get the cuts from recordedmarkup table and store in an array | |
cuts=( `echo "select type,mark from recordedmarkup where chanid=$CHANID and starttime=\"$STARTTIME\" \ | |
and type in ($MARKTYPES) order by mark" | mysql -N -u $USER $PASS mythconverg` ) | |
i=0 | |
pieces=${#cuts[@]} | |
# Insert a record for the start of the recording if it does not exist | |
if [ ${cuts[1]} != 0 ]; then | |
let k=pieces-1 | |
while [ $k -gt 0 ]; do | |
cuts[k+2]=${cuts[k]} | |
cuts[k+1]=${cuts[k-1]} | |
let k-=2 | |
done | |
cuts[0]=0 | |
cuts[1]=0 | |
let pieces=pieces+2 | |
fi | |
# Check if last cut is not end of recording, i.e., we want to keep the end of the recording | |
if [ ${cuts[pieces-2]} == 0 ]; then | |
lastcut=${cuts[pieces-1]} | |
lastseek="" | |
lastseek=`echo "select mark from recordedseek where chanid=$CHANID and starttime=\"$STARTTIME\" and mark>$lastcut;" | \ | |
mysql -N -u $USER $PASS mythconverg | tail -n 1` | |
if [ "$lastseek" != "" ]; then | |
if [ $lastseek -gt $lastcut ]; then | |
cuts[pieces]=1 | |
let cuts[pieces+1]=$lastseek+100 # Assume end of recording is within 100 frames of last seek table entry | |
let pieces=pieces+2 | |
fi | |
fi | |
fi | |
let pieces=pieces-3 | |
k=0 | |
nextmark=0 | |
totaloffset=0 | |
part=1 | |
while [ $i -le $pieces ]; do | |
if [ ${cuts[i]} == 0 -o ${cuts[i]} == 5 ]; then # Look for a cut end, i.e., the start of a segment we want to keep | |
# Cuts at keyframes are actually keyframe+1 so fix | |
cutstart=$((${cuts[i+1]}-1)) | |
key1="" | |
while [ "$key1" == "" ]; do | |
key1=`echo "select offset from recordedseek where mark=$cutstart and chanid=$CHANID and starttime=\"$STARTTIME\";" | \ | |
mysql -N -u $USER $PASS mythconverg` | |
let cutstart-- | |
if [ $cutstart -le 0 ]; then | |
key1=0 | |
cutstart=-1 | |
fi | |
done | |
cutend=$((${cuts[i+3]}-1)) | |
key2="" | |
while [ "$key2" == "" ]; do | |
key2=`echo "select offset from recordedseek where mark=$cutend and chanid=$CHANID and starttime=\"$STARTTIME\";" | \ | |
mysql -N -u $USER $PASS mythconverg` | |
# Check if cutend is past the last record in the seek table | |
lastseek=`echo "select mark from recordedseek where chanid=$CHANID and starttime=\"$STARTTIME\" order by mark desc limit 1;" | \ | |
mysql -N -u $USER $PASS mythconverg` | |
if [ $cutend -gt $lastseek ]; then | |
key2=`echo "select filesize from recorded where chanid=$CHANID and starttime=\"$STARTTIME\";" | \ | |
mysql -N -u $USER $PASS mythconverg` | |
let key2-- | |
else | |
let cutend++ | |
fi | |
done | |
startmark[k]=$(($cutstart+1)) | |
endmark[k]=$(($cutend-1)) | |
markshift[k]=$(($cutstart+1-$nextmark)) | |
nextmark=$(($cutend-1-${markshift[k]})) # +1 | |
offsetshift[k]=$(($key1-$totaloffset)) | |
bytes=$(($key2-$key1)) # +188 | |
totaloffset=$(($totaloffset+$bytes)) | |
let k++ | |
echo "Segment start="$key1" bytes (frame="$(($cutstart+1))", cutpoint frame was "$((${cuts[i+1]}-1))")" | |
echo "Segment end="$key2" bytes (frame="$(($cutend-1))", cutpoint frame was "$((${cuts[i+3]}-1))")" | |
kblocks=100 | |
blocksize=$((1024*$kblocks)) | |
blockstart=$((($key1+1)/$blocksize+1)) | |
pre=$(($blockstart*$blocksize-$key1)) | |
blocks=$((($bytes-$pre)/$blocksize)) | |
post=$(($bytes-$pre-$blocks*$blocksize)) | |
poststart=$((($blockstart+$blocks)*$blocksize)) | |
echo "Total bytes in segment="$bytes" ="$pre" bytes+"$blocks" blocks+"$post" bytes" | |
# Cut and paste append the segment with dd | |
echo "Cutting and pasting segment "$part"a" | |
if [ $part == 1 ]; then | |
ionice -c3 dd ibs=1c obs=1K skip=$key1 count=$pre if=$thefile of=$outfile | |
else | |
ionice -c3 dd ibs=1c obs=1K skip=$key1 count=$pre if=$thefile of=$outfile oflag=append conv=notrunc | |
fi | |
echo "Cutting and pasting segment "$part"b" | |
ionice -c3 dd bs=$kblocks"K" skip=$blockstart count=$blocks if=$thefile of=$outfile oflag=append conv=notrunc | |
if [ $post -gt 0 ]; then | |
echo "Cutting and pasting segment "$part"c" | |
ionice -c3 dd ibs=1c obs=1c skip=$poststart count=$post if=$thefile of=$outfile oflag=append conv=notrunc | |
fi | |
let part++ | |
# Increment loop counter by 4 since array has cut start/end pairs plus cut types | |
let i+=4 | |
else | |
# Skips the case where the first cut point is a cut start, i.e., when start of recording is deleted | |
let i+=2 | |
fi | |
done | |
segs=$k | |
############################################################################# | |
# Reset the cut list, seek table, etc. for final file | |
############################################################################# | |
finalfile=$OUTPUT_DIR$basename | |
if [ $OVERWRITE == 1 ]; then | |
# Don't delete original. Just rename. | |
mv $rec_dir$basename $rec_dir$basename".old" | |
mv $outfile $finalfile | |
# Delete tmp file if it exists | |
if [ -e $OUTPUT_DIR$basename".tmp" ]; then | |
ionice -c3 rm $OUTPUT_DIR$basename".tmp" | |
fi | |
# Backup various data so we can undo the changes | |
num=0 | |
while [ -e $UNDO_DIR$basename.seek$num.sql ]; do | |
let num++ | |
done | |
echo "select * from recordedseek where chanid=$CHANID and starttime=\"$STARTTIME\" into outfile \"$UNDO_DIR$basename.seek$num.sql\";" | \ | |
mysql -u $USER $PASS mythconverg | |
echo "select * from recordedmarkup where chanid=$CHANID and starttime=\"$STARTTIME\" into outfile \"$UNDO_DIR$basename.mark$num.sql\";" | \ | |
mysql -u $USER $PASS mythconverg | |
echo "select * from recorded where chanid=$CHANID and starttime=\"$STARTTIME\" into outfile \"$UNDO_DIR$basename.rec$num.sql\";" | \ | |
mysql -u $USER $PASS mythconverg | |
# Clear the markup table | |
echo "delete from recordedmarkup where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
# Clear bookmark, cutlist, commflagged flags | |
echo "update recorded set bookmark=0, cutlist=0, commflagged=0 where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
# Update file size | |
FILESIZE=`du -b $OUTPUT_DIR$basename | awk '{print $1}'` | |
echo "update recorded set filesize=$FILESIZE where chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
# Rebuild the seek table | |
i=0 | |
while [ $i -lt $segs ]; do | |
echo "Rebuilding seek table part "$i" of "$(($segs-1)) | |
if [ ${startmark[i]} -lt $((${endmark[i]}-${markshift[i]})) ]; then | |
delmark=${startmark[i]} | |
else | |
delmark=$((${endmark[i]}-${markshift[i]})) | |
fi | |
echo "delete from recordedseek where chanid=$CHANID and starttime=\"$STARTTIME\" and \ | |
mark>=$((${startmark[i]}-${markshift[i]})) and mark<$delmark;" | mysql -u $USER $PASS mythconverg | |
echo "update recordedseek set mark=mark-${markshift[i]}, offset=offset-${offsetshift[i]} \ | |
where chanid=$CHANID and starttime=\"$STARTTIME\" and mark>=${startmark[i]} and mark<${endmark[i]} order by mark;" | mysql -u $USER $PASS mythconverg | |
let i++ | |
done | |
echo "delete from recordedseek where mark>=$nextmark and chanid=$CHANID and starttime=\"$STARTTIME\";" | mysql -u $USER $PASS mythconverg | |
else | |
# Don't accidentally overwrite an existing file | |
if [ -e $finalfile ]; then | |
i=1 | |
while [ -e $finalfile"."$i".mpg" ]; do | |
let i++ | |
done | |
mv $outfile $finalfile"."$i".mpg" | |
else | |
mv $outfile $finalfile | |
fi | |
fi | |
ENDSECS=`date +%s` | |
SECS=$(($ENDSECS-$STARTSECS)) | |
MINS=$(($SECS/60)) | |
SECS=$(($SECS-$MINS*60)) | |
echo "Elapsed time: "$MINS" minutes "$SECS" seconds" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment