246 lines
7.4 KiB
Python
246 lines
7.4 KiB
Python
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])
|