#!/usr/bin/perl -w #Last Updated: 2004.09.17 (xris) ############################################################################### # # burncd, a script for burning ISO CD's of mp3 files # # This script also creates the necessary .nautilus-metafile files, # sets up "music" views and sets up any cover art as folder icons. # ############################################################################### #Autoflush buffers $|++; # Constant my $max_cd_size = 900000000; #Load in the commandline arguments use Getopt::Long; my %Args; GetOptions(\%Args, 'help', # Device options 'speed=i', 'device=s', 'list-devices', # Blank a rewritable disk? 'blank:s', # Normal burning options 'overburn', # Verify something that was already burned? 'verify-only', ); # Default arguments $Args{speed} ||= 1; $Args{device} ||= '/dev/cdrom'; # Load the libraries used for verification use utf8; use File::Find; use File::Compare; # Turn off warnings or the cdrecord call may generate UTF-8 errors no warnings; # Scan the SCSI bus for compatible drives my @cd_devices; my $count = 0; foreach my $device (split(/(?=\nHost)/, `cat /proc/scsi/scsi`)) { my ($host, $channel, $id, $lun, $vendor, $model, $type) = $device =~ /Host:\s*scsi(\d+)\b\s*Channel:\s*(\d+)\s*Id:\s*(\d+)\b\s*Lun:\s*(\d+)\b .+?Vendor:\s*(.+?)\b\s*Model:\s*(.+?)\b\s*Rev:.+?Type:\s*(.+?)\s/sx; # Not a cd drive, skip it. next unless ($type && $type =~ /cd-?rom/i); # Get some info about this device my $dev_id = join(',', $host+0, $id+0, $lun+0); my $output = `cdrecord -dev=$dev_id -prcap 2>/dev/null`; # Is this a burner? If not, skip the device my $is_cd_burner = $output =~ /does\s+write\s+CD/i ? 1 : 0; my $is_dvd_burner = $output =~ /does\s+write\s+DVD/i ? 1 : 0; next unless ($is_cd_burner || $is_dvd_burner); # Determine maximum write speeds my $cd_speed = 0; my $dvd_speed = 0; if ($output =~ /Maximum\s+write\s+speed\s+in\s+kB\/s:\s*(\d+)/i or $output =~ /Maximum\s+write\s+speed:\s+(\d+)\s+kB\/s/i) { my $speed = $1; $cd_speed = int($speed / 176) if ($is_cd_burner); $dvd_speed = int($speed / 1385) if ($is_dvd_burner); } else { die "Unrecognized drive -prcap response from cdrecord:\n\n$output\n\n"; } # Get some info about the normal device file my $device = '/dev/scd'.$count++; # # Really need to find some way to see if there is media in the drive or not # # Add it to the list push @cd_devices, { 'id' => $dev_id, 'vendor' => $vendor, 'model' => $model, 'device' => $device, 'cd_burner' => $is_cd_burner, 'cd_speed' => $cd_speed, 'dvd_burner' => $is_dvd_burner, 'dvd_speed' => $dvd_speed, 'symlinks' => [] }; } # Load in the devices listed in /etc/fstab my %fstab_devices; open(DATA, '/etc/fstab') or die "Can't read /etc/fstab: $!\n\n"; while (my $line = ) { my ($device, $mountpoint) = split(/\s+/, $line); if ($device =~ /^\/dev\//) { $fstab_devices{$device} = $mountpoint; } # Found a cd-like or dvd-like device? if ($device =~ /\/(?:cd|dvd)(?:writer|-?rom|rw)?$/i) { # Get some info about this device my $output = `cdrecord -dev=$device -prcap 2>/dev/null`; # Is this a burner? If not, skip the device my $is_cd_burner = $output =~ /does\s+write\s+CD/i ? 1 : 0; my $is_dvd_burner = $output =~ /does\s+write\s+DVD/i ? 1 : 0; next unless ($is_cd_burner || $is_dvd_burner); # Pull out the vendor/model info my ($vendor) = $output =~ /^Vendor_info\s*:\s*'\s*(.+?)\s*'/m; my ($model) = $output =~ /^Identifikation\s*:\s*'\s*(.+?)\s*'/m; # Determine maximum write speeds my $cd_speed = 0; my $dvd_speed = 0; if ($output =~ /Maximum\s+write\s+speed\s+in\s+kB\/s:\s*(\d+)/i or $output =~ /Maximum\s+write\s+speed:\s+(\d+)\s+kB\/s/i) { my $speed = $1; $cd_speed = int($speed / 176) if ($is_cd_burner); $dvd_speed = int($speed / 1385) if ($is_dvd_burner); } else { die "Unrecognized drive -prcap response from cdrecord:\n\n$output\n\n"; } # # Really need to find some way to see if there is media in the drive or not # # Add it to the list push @cd_devices, { 'id' => $device, 'vendor' => $vendor, 'model' => $model, 'device' => $device, 'cd_burner' => $is_cd_burner, 'cd_speed' => $cd_speed, 'dvd_burner' => $is_dvd_burner, 'dvd_speed' => $dvd_speed, 'symlinks' => [] }; } } close DATA; # Scan fstab and look for cd/dvd drives # Turn warnings back on use warnings; # No drives found? die "No SCSI CD or DVD burners found.\n\n" unless (@cd_devices > 0); # Parse the found cd_devices and cd-like entries in /dev for matches. opendir(DIR, '/dev/') or die "Can't open directory /dev: $!\n\n"; foreach my $file (grep /^(cd-?[rw]|dvd)/i, readdir(DIR)) { substr($file, 0, 0) = '/dev/'; # Make sure this device is listed in /etc/fstab #next unless ($fstab_devices{$file}); # Check to see if $file is an alias to a known cd device if (-l $file) { my $link = readlink $file; foreach my $device (@cd_devices) { next unless ($link eq $device->{device}); push @{$device->{symlinks}}, $file; last; } } # Not an alias? Maybe it's a node that points to a known cd device elsif (0) { } } closedir(DIR); # List devices? if ($Args{'list-devices'}) { print "\nDetected Burners:\n\n"; foreach my $dev (@cd_devices) { print " device: $dev->{device}\n", " vendor: $dev->{vendor}\n", " model: $dev->{model}\n", " burn CD: ", $dev->{cd_burner} ? 'Yes' : 'No', "\n", " burn DVD: ", $dev->{dvd_burner} ? 'Yes' : 'No', "\n", " symlink: ", join(', ', @{$dev->{symlinks}}), "\n\n"; } exit; } # Now we figure out which device we're supposed to be using my $cd_device = undef; foreach my $device (@cd_devices) { # First, we check the symlinks my $found = 0; foreach my $link (@{$device->{symlinks}}) { next unless ($link =~ /^$Args{device}$/); $found = 1; last; } # Not found, try matching one of the other strings if ($device->{device} =~ /^$Args{device}$/) { $found = 1; } elsif ($device->{vendor} =~ /$Args{device}/i) { $found = 1; } elsif ($device->{model} =~ /$Args{device}/i) { $found = 1; } # Found? next unless ($found); $cd_device = $device; last; } # Unknown device? die "\nUnknown burner: $Args{device}\n\n Try: burncd --list-devices\n\n" unless ($cd_device); # Figure out the mount point $cd_device->{mount_device} = $cd_device->{device}; $cd_device->{mountpoint} = $fstab_devices{$cd_device->{device}}; unless ($cd_device->{mountpoint}) { foreach my $link (@{$cd_device->{symlinks}}) { next unless ($fstab_devices{$link}); $cd_device->{mount_device} = $link; $cd_device->{mountpoint} = $fstab_devices{$cd_device->{mount_device}}; last; } } # Unknown mount point? die "\nCan't find mount point for $cd_device->{mount_device}; please check /etc/fstab\n\n" unless ($cd_device->{mountpoint}); # Blanking a disk? if (defined($Args{blank})) { $Args{blank} ||= 'fast'; $Args{blank} =~ tr/A-Za-z0-9//cd; #$burn_program = $cd_device->{dvd_burner} ? 'dvdrecord' : 'cdrecord'; $burn_program = 'cdrecord'; exec "$burn_program -vv dev=$cd_device->{id} --blank=$Args{blank}"; exit; # not really necessary, but here in case something goes wrong with the exec } # No arguments? if (!@ARGV) { die "Please specify a file/directory to burn.\n\n"; } # What are we trying to burn? Is it a CD or a DVD? my $action; my $burn_program = 'cdrecord'; my $burn_speed = $cd_device->{cd_speed}; my $the_file = shift @ARGV; $the_file =~ s/\/*$//s; my $orig_file = $the_file; # In case we need one, create a volume name for this disk (my $ISOName = $the_file) =~ s/^.*?((?:[^\/]|(?<=\\)\/)+?)$/$1/s; $ISOName =~ tr/a-zA-Z0-9/_/sc; # Too many arguments if (@ARGV) { die "Too many command line arguments. Please specify only one file/directory to burn.\n\n"; } # Burning a bin/cue pair? if ($the_file =~ /^(.+?)\.(?:cue|bin)$/i) { my $cue = $1.'.cue'; # Make sure the cue file exists die "Can't find $cue\n\n" unless (-e $cue); # Set the action, and save the filename $action = 'bincue'; $the_file = $cue; $the_file =~ s/["\$]/\\"/sg; } # Burning an iso image elsif ($the_file =~ /\.iso$/i) { # Get the size of this file, so we know if it'll fit on dvd or not my @stat = stat($the_file); my $size = $stat[7]; # Estimating that this isn't going to fit on a single CD if ($size > $max_cd_size) { die "This file is too big to fit onto a CD.\nTry using a dvd burner.\n\n" unless ($cd_device->{dvd_burner}); $burn_program = 'dvdrecord'; $burn_speed = $cd_device->{dvd_speed}; } # Set the action and make the filename commandline safe $action = 'iso'; $the_file =~ s/["\$]/\\"/sg; } # Unrecognized file type elsif (!-d $the_file) { die "Unrecognized file type: $the_file\n\n"; } # Burning files - this will take some more computing else { # Look at the files in this directory opendir(DIR, $the_file) or die "Can't open directory $the_file: $!\n\n"; my @files = sort grep(!/^\.\.?$/, readdir(DIR)); closedir(DIR); # Empty directory? die "$the_file is an empty directory!\n\n" unless (@files); # Is this a directory of files intended for a dvd, but of the wrong format? my $found_dvd = 1; foreach my $file (@files) { if ($file !~ /\.(bup|ifo|vob)$/i) { $found_dvd = 0; last; } } die "You are attempting to burn a dvd, but failed to create the VIDEO_TS and AUDIO_TS directories.\n\n" if ($found_dvd); # Is this directory of dvd files? my $is_dvd = 0; foreach my $file (@files) { next unless ($file =~ /^video_ts$/i); $is_dvd = 1; last; } if ($is_dvd) { #@files == 2 && $files[0] =~ /^audio_ts$/i && $files[1] =~ /^video_ts$/i #|| @files == 1 && $files[0] =~ /^video_ts$/i) { die "You can't burn a dvd without a dvd burner.\n\n" unless ($cd_device->{dvd_burner}); mkdir "$the_file/AUDIO_TS", 0755; $action = 'dvd'; $burn_program = 'dvdrecord'; $burn_speed = $cd_device->{dvd_speed}; $the_file =~ s/["\$]/\\"/sg; } # Or maybe a directory of music files elsif (scan_for_music(@files)) { $action = 'music'; die "Audio CD burning is not yet supported.\n\n"; } # Maybe it's just a bunch of files else { # Clean up the directory name so it's safe for command line use. $the_file =~ s/["\$]/\\"/sg; # Get the size of these files, so we know if it'll fit on dvd or not my $size = `du -sb "$the_file"`; $size =~ s/^(\d+)\b.*?$/$1/s; # Estimating that this isn't going to fit on a single CD if ($size > $max_cd_size) { die "This directory is too big to fit onto a CD.\nTry using a dvd burner.\n\n" unless ($cd_device->{dvd_burner}); $burn_program = 'dvdrecord'; $burn_speed = $cd_device->{dvd_speed}; } # Set the action $action = 'files'; } } # Set the global disk_verified var here my $disk_verified = 0; # Only verifying? if ($Args{'verify-only'}) { print "Action: verify $action\n"; verify_disk(); exit; } # Report the action print "Action: burn $action\n"; # Ask the user some questions about the burn... print "Detected drive speed: ${burn_speed}x\n"; #Determine the max speed of the current disk $output = `$burn_program dev=$cd_device->{id} -atip 2>&1`; no warnings; if ($output =~ /speed\s+high:\s*(\d+)/i) { if ($burn_speed > $1) { $burn_speed = $1; print "Detected disk speed: ${burn_speed}x\n"; } } use warnings; #Lastly, double check with the user about the disk speed my $answer; until ($answer and $answer > 0) { print "At what speed would you like to burn this disk [$burn_speed]? "; $answer = ; chomp $answer; $answer ||= $burn_speed; $answer =~ tr/0-9//cd; # Higher burn speed than detected? Double check if ($answer > $burn_speed) { print "This is faster than the detected maximum, are you sure [N]? "; my $answer2 = ; chomp $answer2; $answer = 0 unless ($answer2 =~ /^\s*y(?:es)?/i); } } $burn_speed = $answer; print "Setting write speed: ${burn_speed}x\n"; # Burn the disk if ($action eq 'bincue') { $overburn = $Args{overburn} ? ' --overburn' : ''; print "\ncdrdao write$overburn --device $cd_device->{id} --driver generic-mmc -n --eject --speed $burn_speed \"$the_file\"\n\n"; system("cdrdao write$overburn --device $cd_device->{id} --driver generic-mmc -n --eject --speed $burn_speed \"$the_file\""); } elsif ($action eq 'iso') { $overburn = $Args{overburn} ? ' -overburn' : ''; system("$burn_program$overburn -vv dev=$cd_device->{id} speed=$burn_speed driveropts=burnfree -dao -eject \"$the_file\""); # Make sure the cd tray is closed system("eject -t $cd_device->{mount_device}"); # Verify the disk verify_disk(); } elsif ($action eq 'audio') { die "audio cd burning is disabled\n\n"; } elsif ($action eq 'dvd') { # Make sure all of the files are uppercase opendir(DIR, $the_file) or die "Couldn't open directory $the_file\n\n"; foreach my $file (grep(/^(audio|video)_ts$/i, readdir(DIR))) { finddepth({ wanted => \&uppercase, no_chdir => 1 }, "$orig_file/$file"); } closedir(DIR); # Create a disk image system("nice -n 19 mkisofs -dvd-video -V \"$ISOName\" -o \"/tmp/$ISOName.iso\" \"$the_file\""); # Burn the disk system("$burn_program -vv dev=$cd_device->{id} speed=$burn_speed driveropts=burnfree -dao -eject \"/tmp/$ISOName.iso\""); #Make sure the cd tray is closed system("eject -t $cd_device->{mount_device}"); #Verify the disk verify_disk(); #Eject the cd system("eject $cd_device->{mount_device}"); #Delete the .iso? $answer = ''; while (!$answer) { print "Delete /tmp/$ISOName.iso ? [Y/n] "; $answer = ; chomp $answer; $answer = '' if ($answer =~ /^[^yn]/i); } if ($answer =~ /^y/i) { unlink("/tmp/$ISOName.iso"); } } elsif ($action eq 'files') { #Create a disk image system("nice -n 19 mkisofs -r -J -allow-leading-dots -allow-lowercase -allow-multidot -V \"$ISOName\" -o \"/tmp/$ISOName.iso\" \"$the_file\""); #Burn the directory to disk $overburn = $Args{overburn} ? ' -overburn' : ''; system("$burn_program$overburn -vv dev=$cd_device->{id} speed=$burn_speed driveropts=burnfree -dao -eject \"/tmp/$ISOName.iso\""); #Make sure the cd tray is closed system("eject -t $cd_device->{mount_device}"); #Verify the disk verify_disk(); #Eject the cd system("eject $cd_device->{mount_device}"); #Delete the .iso? $answer = ''; while (!$answer) { print "Delete /tmp/$ISOName.iso ? [Y/n] "; $answer = ; chomp $answer; $answer = '' if ($answer =~ /^[^yn]/i); } if ($answer =~ /^y/i) { unlink("/tmp/$ISOName.iso"); } } # Exit gracefully exit; # Scans a list of files and returns true if they're all music files sub scan_for_music { foreach my $file (@_) { return 0 unless ($file =~ /\.(?:wav|mp[23]|ogg)$/i); } return 1; } #### # # Verifies the burned disk against the original. # #### sub verify_disk { # Can't verify bin/cue images if ($action eq 'bincue') { print "burncd cannot verify bin/cue images - if you know how, please let me know\n"; return } # Can't verify audio disks elsif ($action eq 'audio') { print "Audio cd burning is disabled\n"; return; } # Initialize the disk_verified variable $disk_verified = 1; # Mount the burnt disk system("mount $cd_device->{mountpoint}"); # ISO images need to be mounted to a loopback device in order to be verified if ($action eq 'iso') { # Create a temporary directory for mounting the iso $tmp_dir = "/tmp/burncd.$$"; mkdir $tmp_dir, 0755; # Mount the iso print "Mounting $the_file to $tmp_dir\n"; system("mount \"$the_file\" $tmp_dir -o loop"); # Verify the disk find({ wanted => \&verify_file, no_chdir => 1 }, $tmp_dir); # Unmount the iso and remove the temp directory system("umount $tmp_dir"); rmdir $tmp_dir or print "Couldn't remove $tmp_dir: $!\n"; } # DVD or Files disks don't require anything special, just verify the tree elsif ($action eq 'dvd' || $action eq 'files') { find({ wanted => \&verify_file, no_chdir => 1 }, $the_file); } # Unmount the burnt disk system("umount $cd_device->{mountpoint}"); # Let the user know how the verification went print $disk_verified ? "Disk verification completed successfully.\n" : "Disk verification FAILED. I suggest that you reburn.\n"; } #### # # Verifies the burned disk's against the original - called via File::Find # #### sub verify_file { my $copy = my $orig = $File::Find::name; my $safe = quotemeta $the_file; $copy =~ s/^$safe\/?/$cd_device->{mountpoint}\//s; # Perform the quick check for existence unless (-e $copy) { print "ERROR - $copy does not exist!\n"; $disk_verified = 0; return; } # Not much to do for directories, but give the user some info about what's being checked if (-d $orig) { $copy = $orig; $copy =~ s/^$safe\/?//s; print "Verifying: $copy\n"; return; } # Print info out for every file on a dvd elsif ($burn_program eq 'dvdrecord') { print "Verifying: $orig\n"; } # Compare the files my $equality = compare($orig, $copy); return if ($equality == 0); # Try again, just in case print "Error comparing $copy (trying again)\n"; $equality = compare($orig, $copy); return if ($equality == 0); # Nope, it really failed if ($equality == -1) { die "ERROR - error comparing:\n\t$orig\n\t$copy\n\n"; } else { print "ERROR - $copy does not match the original!\n"; $disk_verified = 0; } } #### # # Makes all files and folders uppercase # #### sub uppercase { my $new = $File::Find::name; $new =~ s/^(.*?)((?:[^\/]|(?<=\\)\/)+?)\/*$/$1\U$2/s; rename $File::Find::name, $new or die "Couldn't rename $File::Find::name to $new: $!\n\n"; }