Saturday, September 05, 2009

Arch :: Pyburn

#!/usr/bin/env python
# -*- coding: iso-8859-1 -*-

import sys, os, signal, re
import pygtk
pygtk.require('2.0')
import gtk, gobject
import ConfigParser, tempfile
from subprocess import *

#
# TODO:
# - needs error checking everywhere
#

# CONFIG
TEMP_DIR = "/var/tmp"
CDRW_DEV = "/dev/cdrw"
DUMMY_MODE = False

# IF you want to manually set the speed of your burner, use this
#os.environ["CDR_SPEED"]="6"

class Burn:
# burn modes
MODE_DATA = 1
MODE_AUDIO = 2
MODE_ISO = 3
MODE_ERASE = 4
# our working directory
tempdir = None
# sub processes
p1, p2 = None, None
int_watch, ext_watch = None, None
# function to use to report errors
errfunc = None

def __init__(self, mode, files, callback, errfunc):
self.mode = mode
self.files = files
self.callback = callback
self.errfunc = errfunc

def start(self):
self.tempdir = tempfile.mkdtemp(dir=TEMP_DIR, prefix="pyburn-")
if self.mode == Burn.MODE_DATA:
self.prepare_data()
if self.mode == Burn.MODE_AUDIO:
self.filenum = 1
self.prepare_audio()
if self.mode == Burn.MODE_ISO:
self.burn_iso()
if self.mode == Burn.MODE_ERASE:
self.erase_disc()

def cancel(self):
if self.ext_watch:
gobject.source_remove(self.ext_watch)
self.ext_watch = None
if self.int_watch:
gobject.source_remove(self.int_watch)
self.int_watch = None
if self.p1 and self.p1.returncode == None:
os.kill(self.p1.pid, signal.SIGTERM)
self.p1.wait()
if self.p2 and self.p2.returncode == None:
os.kill(self.p2.pid, signal.SIGTERM)
self.p2.wait()
self.cleanup()

def cleanup(self):
if self.tempdir:
# remove all our temp files
for root, dirs, files in os.walk(self.tempdir, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
# might be a symlink -- if so, treat it like a file, not a dir
if os.path.islink(os.path.join(root, name)):
os.remove(os.path.join(root, name))
else:
os.rmdir(os.path.join(root, name))
os.rmdir(self.tempdir)

def get_output(self):
if self.p2:
return self.p2.stdout.read(1)
elif self.p1:
return self.p1.stdout.read(1)
return None

def prepare_data(self):
for src in self.files:
dst = self.tempdir + "/" + src.split("/")[-1]
os.symlink(src, dst)
self.burn_data()

def burn_data(self):
# Pipe mkisofs into cdrecord
cmd1 = ["mkisofs", "-J", "-r", "-follow-links", self.tempdir]
dev = "dev=%s" % (CDRW_DEV)
if DUMMY_MODE:
cmd2 = ["cdrecord", "-v", "-eject", "-dummy", "driveropts=burnfree", dev, "-data", "-"]
else:
cmd2 = ["cdrecord", "-v", "-eject", "driveropts=burnfree", dev, "-data", "-"]
print "cmd1: %s" % (cmd1)
print "cmd2: %s" % (cmd2)
self.p1 = Popen(cmd1, stdout=PIPE)
self.p2 = Popen(cmd2, stdin=self.p1.stdout, stdout=PIPE, stderr=STDOUT)
self.ext_watch = gobject.io_add_watch(self.p2.stdout, gobject.IO_IN | gobject.IO_HUP, self.callback)

def burn_iso(self):
# we should only have one file in the list
isofn = self.files[0]
dev = "dev=%s" % (CDRW_DEV)
if DUMMY_MODE:
cmd = ["cdrecord", "-v", "-eject", "-dummy", "driveropts=burnfree", dev, "-data", isofn]
else:
cmd = ["cdrecord", "-v", "-eject", "driveropts=burnfree", dev, "-data", isofn]
print "cmd: %s" % (cmd)
self.p1 = Popen(cmd, stdout=PIPE, stderr=STDOUT)
self.ext_watch = gobject.io_add_watch(self.p1.stdout, gobject.IO_IN | gobject.IO_HUP, self.callback)

def erase_disc(self):
dev = "dev=%s" % (CDRW_DEV)
if DUMMY_MODE:
cmd = ["cdrecord", "-v", "-eject", "-dummy", "driveropts=burnfree", dev, "blank=fast"]
else:
cmd = ["cdrecord", "-v", "-eject", "driveropts=burnfree", dev, "blank=fast"]
print "cmd: %s" % (cmd)
self.p1 = Popen(cmd, stdout=PIPE, stderr=STDOUT)
self.ext_watch = gobject.io_add_watch(self.p1.stdout, gobject.IO_IN | gobject.IO_HUP, self.callback)

def prepare_audio(self):
fn = self.files[self.filenum-1]
wavfn = "%s/%d.wav" % (self.tempdir, self.filenum)
if fn.endswith(".wav") or fn.endswith(".WAV"):
# a symlink should suffice
cmd = ["ln", "-s", fn, wavfn]
elif fn.endswith(".mp3") or fn.endswith(".MP3"):
cmd = ["mpg123", "-w", wavfn, fn]
elif fn.endswith(".ogg") or fn.endswith(".OGG"):
cmd = ["oggdec", "-o", wavfn, fn]
else:
self.errfunc("Unrecognized file extension: %s" % (fn))
return
print "cmd: %s" % (cmd)
self.p1 = Popen(cmd, stdout=PIPE, stderr=STDOUT)
self.ext_watch = gobject.io_add_watch(self.p1.stdout, gobject.IO_IN, self.callback)
# additional callback, so we know when the decode process is finished
self.int_watch = gobject.io_add_watch(self.p1.stdout, gobject.IO_HUP, self.pcallback)

def burn_audio(self):
dev = "dev=%s" % (CDRW_DEV)
if DUMMY_MODE:
cmd = ["cdrecord", "-v", "-eject", "-dummy", "driveropts=burnfree", dev, "-pad", "-audio"]
else:
cmd = ["cdrecord", "-v", "-eject", "driveropts=burnfree", dev, "-pad", "-audio"]
for i in range(1,len(self.files)+1):
fn = "%s/%d.wav" % (self.tempdir, i)
cmd.append(fn)
print "cmd: %s" % (cmd)
self.p1 = Popen(cmd, stdout=PIPE, stderr=STDOUT)
gobject.io_add_watch(self.p1.stdout, gobject.IO_IN | gobject.IO_HUP, self.callback)

def pcallback(self, source, condition):
# remove the other io watch callback
gobject.source_remove(self.ext_watch)
self.ext_watch = None
# if there's a zombie kicking around, wait 'em out
if self.p1:
self.p1.wait()
if self.filenum == len(self.files):
# finished decoding, start the cdrecord process
self.burn_audio()
else:
# decode the next file
self.filenum = self.filenum + 1
self.prepare_audio()
return False


class PyBurn:
def __init__(self):
# Initialization
self.burndlg = None
# Main window
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("destroy", gtk.main_quit, "WM destroy")
self.window.set_title("PyBurn 0.1")

#
# Menu bar
#
menubar = gtk.MenuBar()
# File Menu
file_menu = gtk.Menu()
file = gtk.MenuItem("_File")
file.set_submenu(file_menu)
menubar.append(file)
# Help Menu
help_menu = gtk.Menu()
help = gtk.MenuItem("_Help")
help.set_submenu(help_menu)
menubar.append(help)

menuit = gtk.MenuItem("_About...")
menuit.connect_object("activate", self.menu_callback, "about")
help_menu.append(menuit)
menuit = gtk.MenuItem("_Load Playlist...")
menuit.connect_object("activate", self.menu_callback, "loadplaylist")
file_menu.append(menuit)
menuit = gtk.MenuItem("_Blank CD-RW...")
menuit.connect_object("activate", self.menu_callback, "erasedisc")
file_menu.append(menuit)
menuit = gtk.MenuItem("Burn _ISO Image...")
menuit.connect_object("activate", self.menu_callback, "burniso")
file_menu.append(menuit)
menuit = gtk.MenuItem("_Quit")
menuit.connect_object("activate", gtk.main_quit, "WM destroy")
file_menu.append(menuit)

#
# Radio Buttons
#
label1 = gtk.Label("Burn Mode:")
self.mode_audio = gtk.RadioButton(None, "Audio", False)
self.mode_data = gtk.RadioButton(self.mode_audio, "Data", False)

#
# File list
#
self.flist = gtk.ListStore(gobject.TYPE_STRING)
self.ftree = gtk.TreeView(self.flist)
self.ftree.set_reorderable(True)
fcol = gtk.TreeViewColumn('Files to Burn:')
self.ftree.append_column(fcol)
cell = gtk.CellRendererText()
fcol.pack_start(cell, True)
fcol.add_attribute(cell, 'text', 0)

#
# Buttons
#
# Helper function to add buttons
def new_button(icon, label, callbackstr):
btn = gtk.Button(label=None)
hbox = gtk.HBox(False, 0)
btn.add(hbox)
s = gtk.Style()
icon = s.lookup_icon_set(icon).render_icon(
s, gtk.TEXT_DIR_LTR, gtk.STATE_NORMAL, gtk.ICON_SIZE_BUTTON,
hbox, None)
img = gtk.Image()
img.set_from_pixbuf(icon)
hbox.add(img)
label = gtk.Label(label)
label.set_use_underline(True)
hbox.add(label)
btn.connect("clicked", self.button_callback, callbackstr)
return btn
# The Add/Add/Remove buttons
btn_add = new_button(gtk.STOCK_FILE, "Add _Files", "addfiles")
btn_adddir = new_button(gtk.STOCK_DIRECTORY, "Add _Directory", "adddirs")
btn_del = new_button(gtk.STOCK_REMOVE, "_Remove", "removefiles")
# The "Start Burn" button
btn_burn = new_button(gtk.STOCK_MEDIA_RECORD, "_Start Burn", "burncd")

# Assemble the main window and all its components
vbox = gtk.VBox(False, 0)
vbox.set_border_width(1)
self.window.add(vbox)

hbox = gtk.HBox(False, 0)
hbox.set_border_width(1)

scrollwin = gtk.ScrolledWindow()
scrollwin.add(self.ftree)

btnbox = gtk.HBox(False, 0)
btnbox_l = gtk.HButtonBox()
btnbox_l.set_spacing(5)
btnbox_l.pack_start(btn_add, False, True, 0)
btnbox_l.pack_start(btn_adddir, False, True, 0)
btnbox_l.pack_start(btn_del, False, True, 0)
btnbox_r = gtk.HButtonBox()
btnbox_r.set_spacing(5)
btnbox_r.pack_start(btn_burn, False, True, 0)
btnbox_r.set_layout(gtk.BUTTONBOX_END)

# Main VBox
vbox.pack_start(menubar, False, True, 0)
vbox.pack_start(hbox, False, True, 0)
vbox.pack_start(scrollwin, True, True, 0)
vbox.pack_start(btnbox, False, True, 0)
# Radio Button HBox
hbox.pack_end(self.mode_data, False, True, 0)
hbox.pack_end(self.mode_audio, False, True, 0)
hbox.pack_end(label1, False, True, 0)
# Button HBox
btnbox.pack_start(btnbox_l, False, True, 0)
btnbox.pack_start(btnbox_r, True, True, 0)

# Drag n' Drop
self.ftree.enable_model_drag_dest([
('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0),
('text/plain', 0, 1), ('TEXT', 0, 2), ('STRING', 0, 3)],
gtk.gdk.ACTION_DEFAULT)
self.ftree.enable_model_drag_source(gtk.gdk.BUTTON1_MASK,
[('MY_TREE_MODEL_ROW', gtk.TARGET_SAME_WIDGET, 0)], gtk.gdk.ACTION_MOVE)
self.ftree.connect("drag_data_get", self.dnd_get_data)
self.ftree.connect("drag_data_received", self.dnd_received_data)

# and.... show it!
self.window.set_default_size(640, 350)
self.window.show_all()

def show_burn_dialog(self):
self.burndlg = gtk.Dialog("Burn Progress", self.window, gtk.DIALOG_MODAL)
self.burnbtn1 = self.burndlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)
self.burnbtn1.connect("clicked", self.button_callback, "cancelburn")
self.burnbtn2 = self.burndlg.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE)
self.burnbtn2.connect("clicked", self.button_callback, "closeburn")

self.buffer = gtk.TextBuffer()
scrollwin = gtk.ScrolledWindow()
self.outputw = gtk.TextView(self.buffer)
self.outputw.set_editable(False)
self.outputw.set_cursor_visible(False)
self.outputw.set_wrap_mode(gtk.WRAP_NONE)

# set a mark for auto-scrolling
self.buffer.create_mark("tail", self.buffer.get_end_iter(), False)

scrollwin.add(self.outputw)
self.burndlg.vbox.pack_start(scrollwin, True, True, 0)
scrollwin.show()
self.outputw.show()
self.burndlg.set_default_size(600, 300)
self.burndlg.vbox.show_all()
self.burnbtn1.show()
self.burnbtn2.hide()
self.burndlg.show()

def button_callback(self, widget, data):
if data == "adddirs":
chooser = gtk.FileChooserDialog(title="Select a Directory",action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
chooser.set_select_multiple(True)

response = chooser.run()
if response == gtk.RESPONSE_OK:
for fn in chooser.get_filenames():
self.flist.append((fn,))
chooser.destroy()

if data == "addfiles":
chooser = gtk.FileChooserDialog(title="Select a File",action=gtk.FILE_CHOOSER_ACTION_OPEN,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
chooser.set_select_multiple(True)

filter = gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
chooser.add_filter(filter)

filter = gtk.FileFilter()
filter.set_name("Music files (*.mp3,*.ogg,*.wav)")
filter.add_pattern("*.mp3")
filter.add_pattern("*.ogg")
filter.add_pattern("*.wav")
chooser.add_filter(filter)

response = chooser.run()
if response == gtk.RESPONSE_OK:
for fn in chooser.get_filenames():
self.flist.append((fn,))
chooser.destroy()

if data == "removefiles":
sel = self.ftree.get_selection()
model, iter = sel.get_selected()
if iter:
model.remove(iter)

if data == "burncd":
filelist = [i[0].strip() for i in self.ftree.get_model()]
if len(filelist) == 0:
self.error("Add some files first!")
return
if self.mode_audio.get_active():
mode = Burn.MODE_AUDIO
else:
mode = Burn.MODE_DATA
self.burn = Burn(mode, filelist, self.burn_callback, self.error)
# Start the burn process
self.show_burn_dialog()
self.burn.start()

if data == "cancelburn":
self.burn.cancel()
self.burndlg.destroy()

if data == "closeburn":
self.burndlg.destroy()

def burn_callback(self, source, condition):
if condition == gobject.IO_IN:
self.buffer.insert_at_cursor(self.burn.get_output())
# scroll down to the last line in the text window
self.buffer.move_mark_by_name("tail", self.buffer.get_end_iter())
self.outputw.scroll_mark_onscreen(self.buffer.get_mark("tail"))
return True
if condition == gobject.IO_HUP:
self.burn.cancel()
self.burnbtn1.hide()
self.burnbtn2.show()
self.burndlg.set_title("Burn Finished")
self.buffer.insert_at_cursor("\nBURN FINISHED.\n")
# scroll down to the last line in the text window
self.buffer.move_mark_by_name("tail", self.buffer.get_end_iter())
self.outputw.scroll_mark_onscreen(self.buffer.get_mark("tail"))
return False

def dnd_received_data(self, treeview, context, x, y, selection,
info, etime):
model = treeview.get_model()
data = selection.data.strip(chr(0)).strip("\r\n")
for fn in data.split("\r\n"):
if fn.startswith("file:"):
# trim off the "file:" prefix
fn = fn[5:]
# trim out consecutive slashes
fn = re.sub("//+", "/", fn)
drop_info = treeview.get_dest_row_at_pos(x, y)
if drop_info:
path, position = drop_info
iter = model.get_iter(path)
if (position == gtk.TREE_VIEW_DROP_BEFORE
or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
model.insert_before(iter, [fn])
else:
model.insert_after(iter, [fn])
else:
model.append([fn])
if context.action == gtk.gdk.ACTION_MOVE:
context.finish(True, True, etime)

def dnd_get_data(self, treeview, context, selection, target_id, etime):
treeselection = treeview.get_selection()
model, iter = treeselection.get_selected()
data = model.get_value(iter, 0)
selection.set(selection.target, 8, data)
model.remove(iter)

def menu_callback(self, data):
if data == "about":
dlg = gtk.AboutDialog()
dlg.set_name("PyBurn")
dlg.set_comments("A frontend application for burning CDs with the cdrecord utility")
dlg.set_version("0.1")
dlg.set_copyright(u"Copyright ©2005 Judd Vinet")
dlg.set_license("""Copyright (C)2005 Judd Vinet

This is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2, or (at your option)
any later version.

This software is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with program; see the file COPYING. If not, write to the
Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
MA 02111-1307, USA.""")
dlg.run()
dlg.destroy()

if data == "erasedisc":
msgdlg = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL,
gtk.MESSAGE_QUESTION, gtk.BUTTONS_YES_NO, "Are you sure?")
response = msgdlg.run()
msgdlg.destroy()
if response != gtk.RESPONSE_YES:
return

self.burn = Burn(Burn.MODE_ERASE, [], self.burn_callback, self.error)
self.show_burn_dialog()
self.burn.start()

if data == "burniso":
chooser = gtk.FileChooserDialog(title="Select an ISO Image",action=gtk.FILE_CHOOSER_ACTION_OPEN,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))

filter = gtk.FileFilter()
filter.set_name("ISO images (*.iso)")
filter.add_pattern("*.iso")
chooser.add_filter(filter)
filter = gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
chooser.add_filter(filter)

response = chooser.run()
if response != gtk.RESPONSE_OK:
chooser.destroy()
return
isofn = chooser.get_filename()
chooser.destroy()

self.burn = Burn(Burn.MODE_ISO, [isofn], self.burn_callback, self.error)
# Start the burn process
self.show_burn_dialog()
self.burn.start()

if data == "loadplaylist":
chooser = gtk.FileChooserDialog(title="Select a Playlist File",action=gtk.FILE_CHOOSER_ACTION_OPEN,
buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))

filter = gtk.FileFilter()
filter.set_name("pls files (*.pls)")
filter.add_pattern("*.pls")
chooser.add_filter(filter)
filter = gtk.FileFilter()
filter.set_name("plain-text file list (*.lst)")
filter.add_pattern("*.lst")
chooser.add_filter(filter)
filter = gtk.FileFilter()
filter.set_name("All files")
filter.add_pattern("*")
chooser.add_filter(filter)

response = chooser.run()
if response != gtk.RESPONSE_OK:
chooser.destroy()
return
plsfn = chooser.get_filename()
chooser.destroy()

# try to figure out what type of list we have...
# default to a plain-text list of files, but use the INI parser if
# the file starts with "[playlist]"
pls = open(plsfn, "r")
if pls.readline().startswith("[playlist]"):
# INI format
pls.close()
pls = ConfigParser.ConfigParser()
try:
pls.read([plsfn])
except:
self.error("Error parsing %s" % (plsfn))
return
num = pls.get("playlist", "NumberOfEntries")
for i in range(0,int(num)):
fn = pls.get("playlist", "File%d" % (i+1))
self.flist.append((fn,))
else:
# TXT format
pls.seek(0)
for fn in pls:
self.flist.append((fn.strip(),))

def error(self, msg):
dlg = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
gtk.BUTTONS_OK, msg)
dlg.run()
dlg.destroy()
if self.burndlg:
self.burn.cancel()
self.burndlg.destroy()

def main(self):
gtk.main()


if __name__ == "__main__":
pyb = PyBurn()
pyb.main()

No comments: