Initial commit
parent
10e068bc86
commit
16c8f7e2ed
@ -0,0 +1,48 @@
|
||||
# image-disc
|
||||
|
||||
A small utility for making archival copies of CDs and DVDs under Linux.
|
||||
|
||||
There's a lot of duct tape in here, but it generally works. You've been warned.
|
||||
|
||||
Pull requests welcome, but please keep in mind that the purpose of this script is *archival-quality* copies where possible.
|
||||
|
||||
## Supported disc types
|
||||
|
||||
* Data CD-ROM
|
||||
* Mixed-content CD-ROM (eg. data + audio)
|
||||
* Audio-DVD
|
||||
* Video-DVD
|
||||
* Data DVD-ROM
|
||||
|
||||
## Unsupported disc types
|
||||
|
||||
* Audio-CD (use a secure ripper such as [Rubyripper](http://wiki.hydrogenaudio.org/index.php?title=Rubyripper) for this!)
|
||||
* Bluray
|
||||
* HD-DVD
|
||||
* Other non-CD/DVD disc types
|
||||
|
||||
## Dependencies
|
||||
|
||||
You must have the following installed:
|
||||
|
||||
* cdrdao
|
||||
* `ddrescue` (*not* `dd_rescue`!)
|
||||
* UDisks (shipped by most distributions)
|
||||
* `eject` (shipped by most distributions)
|
||||
* udev (available in recent Linux kernels)
|
||||
|
||||
## Usage
|
||||
|
||||
`python image.py DEVICE TARGET [--ddrescue]`
|
||||
|
||||
* **DEVICE**: The source device to image from. This will be something like `/dev/sr0` or `/dev/cdrom`.
|
||||
* **TARGET**: The target directory to place images in. Each image will be named according to the name you enter for that disc.
|
||||
* **--ddrescue**: Optional flag to force the script to use ddrescue, even for (potential mixed-content) CD-ROMs. Useful for recovering damaged CD-ROMs. **This flag must always be *after* the device and target!**
|
||||
|
||||
## Remarks
|
||||
|
||||
This is duct tape. It ties together a bunch of existing utilities to automate your imaging. There is no guarantee that it'll work, or even that it'll produce valid images (although it will certainly try). **Check the integrity of your images, if you care about your data!**
|
||||
|
||||
The script is quite noisy; it doesn't try to understand output from utilities, and just passes it through wholesale. All messages originating from the script itself are prefixed with `##`.
|
||||
|
||||
Don't be alarmed if you get mounting/unmounting/eject failures; to avoid race conditions, the script is quite aggressive in making sure everything is unmounted/ejected when necessary. If you get errors, that usually just means that the disc was *already* unmounted or ejected.
|
@ -0,0 +1,245 @@
|
||||
import subprocess, sys, os, random, string, re, time
|
||||
|
||||
drive = sys.argv[1]
|
||||
target_path = sys.argv[2]
|
||||
|
||||
if sys.argv[3] == "--ddrescue":
|
||||
forced_ddrescue = True
|
||||
else:
|
||||
forced_ddrescue = False
|
||||
|
||||
def mount_drive(drive):
|
||||
output = subprocess.check_output(["udisksctl", "mount", "-b", drive])
|
||||
# urgh, regex to extract the mountpath...
|
||||
match = re.search("Mounted [^ ]+ at (.+)\.$", output, re.MULTILINE)
|
||||
if match is None:
|
||||
raise Exception("Drive failed to mount.")
|
||||
else:
|
||||
return match.group(1)
|
||||
|
||||
def unmount_drive(drive):
|
||||
retcode = subprocess.call(["udisksctl", "unmount", "-b", drive])
|
||||
if retcode != 0:
|
||||
raise Exception("Drive failed to unmount.")
|
||||
|
||||
def get_disc_info(drive):
|
||||
return [line.strip() for line in subprocess.check_output(["udevadm", "info", "-q", "env", "-n", drive]).splitlines()]
|
||||
|
||||
def format_bytes(inp):
|
||||
amount, unit = inp.rsplit(" ", 1)
|
||||
map_ = {
|
||||
"B": 1,
|
||||
"KB": 1024,
|
||||
"MB": 1024 * 1024,
|
||||
"GB": 1024 * 1024 * 1024
|
||||
}
|
||||
return int(amount) * map_[unit.upper()]
|
||||
|
||||
while True:
|
||||
name = raw_input("## What is the name of the next disc? ")
|
||||
|
||||
# Try to unmount the drive, just in case it was auto-mounted
|
||||
try:
|
||||
unmount_drive(drive)
|
||||
except:
|
||||
# Doesn't matter, wasn't mounted to begin with.
|
||||
pass
|
||||
|
||||
media_type = "unknown"
|
||||
|
||||
print "## Waiting for media to be recognized... Please close the tray if it is still opened."
|
||||
|
||||
while True:
|
||||
# This is a loop to wait until the disc is recognized...
|
||||
disc_info = get_disc_info(drive)
|
||||
|
||||
if "ID_CDROM_MEDIA=1" in disc_info:
|
||||
break # Disc was recognized, end loop
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Now determine the media type.
|
||||
if "ID_CDROM_MEDIA_CD=1" in disc_info or "ID_CDROM_MEDIA_CD_R=1" in disc_info:
|
||||
# Some kind of CD.
|
||||
data_tracks = 0
|
||||
audio_tracks = 0
|
||||
total_tracks = 0
|
||||
|
||||
for line in disc_info:
|
||||
key, value = line.split("=", 1)
|
||||
|
||||
if key == "ID_CDROM_MEDIA_TRACK_COUNT_AUDIO":
|
||||
audio_tracks = int(value)
|
||||
elif key == "ID_CDROM_MEDIA_TRACK_COUNT_DATA":
|
||||
data_tracks = int(value)
|
||||
elif key == "ID_CDROM_MEDIA_TRACK_COUNT":
|
||||
total_tracks = int(value)
|
||||
|
||||
if total_tracks != (data_tracks + audio_tracks):
|
||||
print "## ERROR: Unrecognized tracks found on CD! Please report this as a bug."
|
||||
continue # Abort imaging cycle
|
||||
|
||||
if data_tracks == 0 and audio_tracks > 0:
|
||||
media_type = "cd-audio"
|
||||
elif data_tracks > 0 and audio_tracks == 0:
|
||||
media_type = "cd-data"
|
||||
elif data_tracks > 0 and audio_tracks > 0:
|
||||
media_type = "cd-mixed"
|
||||
else:
|
||||
media_type = "cd-blank"
|
||||
elif "ID_CDROM_MEDIA_DVD=1" in disc_info:
|
||||
# We cannot distinguish between a Video-DVD and a DVD-ROM from the udev data alone.
|
||||
try:
|
||||
mount_path = mount_drive(drive)
|
||||
except subprocess.CalledProcessError, e:
|
||||
# Could not mount; try to unmount and mount again.
|
||||
print "## WARNING: Drive already mounted; re-mounting..."
|
||||
# Wait for a bit first, to let the auto-mount complete...
|
||||
time.sleep(1)
|
||||
unmount_drive(drive)
|
||||
mount_path = mount_drive(drive)
|
||||
|
||||
if os.path.exists(os.path.join(mount_path, "VIDEO_TS")):
|
||||
media_type = "dvd-video"
|
||||
elif os.path.exists(os.path.join(mount_path, "AUDIO_TS")):
|
||||
media_type = "dvd-audio"
|
||||
else:
|
||||
media_type = "dvd-data"
|
||||
# Apparently some discs will not work correctly, if we don't wait before unmounting...
|
||||
time.sleep(1)
|
||||
unmount_drive(drive)
|
||||
|
||||
# Now we'll do something, depending on the media type.
|
||||
if media_type == "unknown":
|
||||
print "## ERROR: Failed to determine media type for disc."
|
||||
continue # abort
|
||||
|
||||
if media_type == "cd-blank": # TODO: Figure out blank-ness of a DVD.
|
||||
print "## ERROR: Cannot image blank disc!"
|
||||
continue # abort
|
||||
|
||||
type_map = {
|
||||
"cd-audio": "Audio CD",
|
||||
"cd-data": "Data CD(-ROM)",
|
||||
"cd-mixed": "CD(-ROM) with non-data (eg. audio) tracks",
|
||||
"dvd-video": "Video DVD",
|
||||
"dvd-audio": "Audio DVD",
|
||||
"dvd-data": "Data DVD-ROM"
|
||||
}
|
||||
|
||||
print "## Disc detected as %s" % type_map[media_type]
|
||||
|
||||
if media_type in ("cd-data", "cd-mixed") and not forced_ddrescue:
|
||||
bin_path = os.path.join(target_path, "%s.bin" % name)
|
||||
cue_path = os.path.join(target_path, "%s.cue" % name)
|
||||
tmp_path = "/tmp/image-%s" % ''.join(random.choice(string.ascii_uppercase + string.digits) for x in range(8))
|
||||
|
||||
print "## Starting imaging process..."
|
||||
|
||||
tries = 0
|
||||
success = False
|
||||
|
||||
# TODO: Read output to scan for "Device or resource busy"
|
||||
while True:
|
||||
try:
|
||||
unmount_drive(drive)
|
||||
except:
|
||||
# Doesn't matter, wasn't mounted to begin with.
|
||||
pass
|
||||
|
||||
retcode = subprocess.call([
|
||||
"cdrdao", "read-cd",
|
||||
"--device", drive,
|
||||
"--read-raw",
|
||||
"--datafile", bin_path,
|
||||
"-v", "2",
|
||||
tmp_path
|
||||
])
|
||||
|
||||
if retcode != 0:
|
||||
if tries < 2:
|
||||
print "## WARNING: Imaging failed, retrying in a second..."
|
||||
time.sleep(1)
|
||||
tries += 1
|
||||
continue # Retry
|
||||
else:
|
||||
print "## ERROR: IMAGING FAILED."
|
||||
success = False
|
||||
break
|
||||
else:
|
||||
print "## Imaging finished successfully."
|
||||
success = True
|
||||
break
|
||||
|
||||
if success == False:
|
||||
continue # Abort
|
||||
|
||||
print "## Generating cuesheet..."
|
||||
|
||||
retcode = subprocess.call([
|
||||
"toc2cue",
|
||||
tmp_path,
|
||||
cue_path
|
||||
])
|
||||
|
||||
if retcode != 0:
|
||||
print "## ERROR: CUESHEET CREATION FAILED."
|
||||
continue
|
||||
else:
|
||||
print "## Cuesheet creation finished successfully."
|
||||
|
||||
if media_type == "cd-mixed":
|
||||
print "## WARNING: The disc contains audio tracks as well. You may want to consider using an Audio CD ripper for those."
|
||||
elif media_type == "cd-audio":
|
||||
print "## ERROR: Cannot currently image Audio CDs. Please use an Audio CD ripper instead."
|
||||
elif media_type in ("dvd-audio", "dvd-video", "dvd-data") or forced_ddrescue:
|
||||
# Create an ISO, that should be sufficient... use ddrescue by default.
|
||||
iso_path = os.path.join(target_path, "%s.iso" % name)
|
||||
log_path = os.path.join(target_path, "%s.ddrescuelog" % name)
|
||||
|
||||
if media_type == "dvd-video":
|
||||
# We hook the stdout/stderr here, to detect large amounts of read errors for video-DVDs,
|
||||
# which is a likely indicator of ARccOS protection.
|
||||
# FIXME: Currently -completely- broken! Needs fixing ASAP.
|
||||
arccos = False
|
||||
proc = subprocess.Popen(["ddrescue", "-A", "-M", "-r", "20", "-b", "2048", drive, iso_path, log_path], stderr=subprocess.STDOUT, stdout=subprocess.PIPE, bufsize=1)
|
||||
|
||||
while proc.poll() is None:
|
||||
line = proc.stdout.readline()
|
||||
sys.stdout.write(line)
|
||||
match = re.match("rescued:\s*([0-9]+ [A-Z]+),\s*errsize:\s*([0-9]+ [A-Z]+)", line)
|
||||
if match is not None and arccos == False:
|
||||
# Status line...
|
||||
rescued_bytes = format_bytes(match.group(1))
|
||||
error_bytes = format_bytes(match.group(2))
|
||||
if error_bytes > rescued_bytes and error_bytes > (1024 * 1024 * 10) and rescued_bytes > (1024 * 1024 * 10):
|
||||
# Only trigger if there's more read errors than read successes, and both
|
||||
# values exceed 10MB.
|
||||
print "## WARNING: Large amount of read errors detected, likely ARccOS-protected. Restarting with different parameters..."
|
||||
arccos = True
|
||||
# TODO: Figure out working parameters...
|
||||
#break
|
||||
|
||||
if arccos == False:
|
||||
proc.communicate()
|
||||
retcode = proc.returncode
|
||||
else:
|
||||
os.remove(iso_path)
|
||||
os.remove(log_path)
|
||||
# TODO: Run with proper params...
|
||||
else:
|
||||
retcode = subprocess.call(["ddrescue", "-A", "-M", "-r", "20", "-b", "2048", drive, iso_path, log_path])
|
||||
|
||||
if retcode in (1, 3):
|
||||
print "## ERROR: An error occurred in ddrescue."
|
||||
continue
|
||||
elif retcode == 2:
|
||||
print "## WARNING: ddrescue indicates corruption, image may have failed."
|
||||
# arccos bypass:
|
||||
# sdparm --set=RRC=0 /dev/sr0
|
||||
|
||||
# It might've been automounted by something again...
|
||||
subprocess.call(["udisksctl", "unmount", "-b", drive])
|
||||
|
||||
# Eject the drive
|
||||
subprocess.call(["eject", drive])
|
Loading…
Reference in New Issue