See the changelog. :)
[rox-ripper.git] / ripper.py
blobcd71111779cc54c4c6332eb72caad16ffbf0e25a
1 """
2 ripper.py
3 GUI front-end to cdda2wav and lame.
5 Copyright 2004-2006 Kenneth Hayber <ken@hayber.us>
6 All rights reserved.
8 This program is free software; you can redistribute it and/or modify
9 it under the terms of the GNU General Public License as published by
10 the Free Software Foundation; either version 2 of the License.
12 This program is distributed in the hope that it will be useful
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License
19 along with this program; if not, write to the Free Software
20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 """
23 import gtk, os, sys, signal, re, string, socket, time, popen2, Queue
24 from random import Random
26 import rox
27 from rox import i18n, app_options, Menu, filer, InfoWin, tasks, loading, fileutils
28 from rox.options import Option
30 import PyCDDB, cd_logic, CDROM, genres, support
32 try:
33 import xattr
34 HAVE_XATTR = True
35 except:
36 HAVE_XATTR = False
38 _ = rox.i18n.translation(os.path.join(rox.app_dir, 'Messages'))
40 #Who am I and how did I get here?
41 APP_NAME = 'Ripper' #I could call it Mr. Giles, but that would be gay.
42 APP_PATH = rox.app_dir
45 #Options.xml processing
46 from rox import choices
47 choices.migrate(APP_NAME, 'hayber.us')
48 rox.setup_app_options(APP_NAME, site='hayber.us')
49 Menu.set_save_name(APP_NAME, site='hayber.us')
51 #assume that everyone puts their music in ~/Music
52 LIBRARY = Option('library', '~/Music/$A/$L/$S')
54 #RIPPER options
55 DEVICE = Option('device', '/dev/cdrom')
56 RIPPER = Option('ripper', 'nice cdda2wav -g -x -H -D$D -A$D -t $T "$F"')
57 ENCODER = Option('encoder', 'nice lame --ta "$A" --tt "$S" --tl "$L" --tg "$G" --tn $T --ty $Y "$F.wav" "$F.mp3"')
59 EJECT_AFTER_RIP = Option('eject_after_rip', '0')
60 RIP_ONLY = Option('rip_only', '0')
61 KEEP_WAV = Option('keep_wav', '0')
63 CDDB_SERVER = Option('cddb_server', 'http://freedb.freedb.org/~cddb/cddb.cgi')
65 rox.app_options.notify()
67 COVER_SIZE = 96
69 #Column indicies
70 COL_ENABLE = 0
71 COL_TRACK = 1
72 COL_TIME = 2
73 COL_STATUS = 3
74 COL_PERCENT = 4
76 DEBUG = 0
77 def dbg(*args):
78 if DEBUG:
79 import sys
80 print >>sys.stderr, args
83 # My gentoo python doesn't have universal line ending support compiled in
84 # and these guys (cdda2wav and lame) use CR line endings to pretty-up their output.
85 def myreadline(file):
86 '''Return a line of input using \r or \n as terminators'''
87 line = ''
88 while '\n' not in line and '\r' not in line:
89 char = file.read(1)
90 if char == '': return line
91 line += char
92 return line
95 def which(filename):
96 '''Return the full path of an executable if found on the path'''
97 if (filename == None) or (filename == ''):
98 return None
100 env_path = os.getenv('PATH').split(':')
101 for p in env_path:
102 if os.access(p+'/'+filename, os.X_OK):
103 return p+'/'+filename
104 return None
107 def strip_illegal(instr):
108 '''remove illegal (filename) characters from string'''
109 str = instr
110 str = str.strip()
111 str = string.translate(str, string.maketrans(r'/+{}*.?', r'--()___'))
112 return str
115 class Ripper(rox.Window, rox.loading.XDSLoader):
116 '''Rip and Encode a CD'''
117 def __init__(self):
118 rox.Window.__init__(self)
120 # Support dropping Album Cover Art on our Window.
121 # Get types supported by gdk-pixbuf
122 mtypes = []
123 for fmt in gtk.gdk.pixbuf_get_formats():
124 mtypes += fmt['mime_types']
125 rox.loading.XDSLoader.__init__(self, mtypes)
127 self.set_title(APP_NAME)
128 self.set_default_size(450, 500)
129 self.set_position(gtk.WIN_POS_MOUSE)
131 # Capture wm delete event
132 self.connect("delete_event", self.delete_event)
134 # Update things when options change
135 rox.app_options.add_notify(self.get_options)
137 self.build_gui()
138 self.build_toolbar()
139 self.build_menu()
141 # Create layout, pack and show widgets
142 self.vbox = gtk.VBox()
143 self.add(self.vbox)
144 self.vbox.pack_start(self.toolbar, False, True, 0)
145 self.vbox.pack_start(self.table, False, True, 0)
146 self.vbox.pack_start(self.scroll_window, True, True, 0)
147 self.vbox.show_all()
149 # Defaults and Misc
150 self.is_ripping = False
151 self.is_encoding = False
152 self.is_cddbing = False
153 self.stop_request = False
154 self.closing = False
155 self.artwork_saved = False
157 cd_logic.set_dev(DEVICE.value)
158 self.cd_status = None
159 self.disc_id = None
160 self.cd_status_changed = False
162 tasks.Task(self.update_gui())
165 def build_menu(self):
166 self.add_events(gtk.gdk.BUTTON_PRESS_MASK)
167 self.connect('button-press-event', self.button_press)
168 self.view.add_events(gtk.gdk.BUTTON_PRESS_MASK)
169 self.view.connect('button-press-event', self.button_press)
171 self.menu = Menu.Menu('main', [
172 Menu.Action(_('Rip & Encode'), 'rip_n_encode', '', gtk.STOCK_EXECUTE),
173 Menu.Action(_('Reload CD'), 'do_get_tracks', '', gtk.STOCK_REFRESH),
174 Menu.Action(_('Stop'), 'stop', '', gtk.STOCK_STOP),
175 Menu.Separator(),
176 Menu.Action(_('Options'), 'show_options', '', gtk.STOCK_PREFERENCES),
177 Menu.Action(_('Help'), 'show_help', '', gtk.STOCK_HELP),
178 Menu.Action(_("Quit"), 'close', '', gtk.STOCK_CLOSE),
180 self.menu.attach(self,self)
183 def build_toolbar(self):
184 self.toolbar = gtk.Toolbar()
185 self.toolbar.set_style(gtk.TOOLBAR_ICONS)
186 self.toolbar.insert_stock(gtk.STOCK_HELP, _('Help'), None, self.show_help, None, 0)
187 self.toolbar.insert_stock(gtk.STOCK_PREFERENCES, _('Settings'), None, self.show_options, None, 0)
188 self.stop_btn = self.toolbar.insert_stock(gtk.STOCK_STOP, _('Stop'), None, self.stop, None, 0)
189 self.rip_btn = self.toolbar.insert_stock(gtk.STOCK_EXECUTE, _('Rip & Encode'), None, self.rip_n_encode, None, 0)
190 self.refresh_btn = self.toolbar.insert_stock(gtk.STOCK_REFRESH, _('Reload CD'), None, self.do_get_tracks, None, 0)
191 self.toolbar.insert_stock(gtk.STOCK_GO_UP, _('Show destination dir'), None, self.show_dir, None, 0)
192 self.toolbar.insert_stock(gtk.STOCK_CLOSE, _('Close'), None, self.close, None, 0)
195 def build_gui(self):
196 swin = gtk.ScrolledWindow()
197 self.scroll_window = swin
198 swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
199 swin.set_shadow_type(gtk.SHADOW_IN)
201 self.store = gtk.ListStore(int, str, str, str, int)
202 view = gtk.TreeView(self.store)
203 self.view = view
204 swin.add(view)
205 view.set_rules_hint(True)
207 cell = gtk.CellRendererToggle()
208 cell.connect('toggled', self.toggle_check)
209 column = gtk.TreeViewColumn('', cell, active=COL_ENABLE)
210 view.append_column(column)
211 column.set_resizable(False)
212 column.set_reorderable(False)
214 cell = gtk.CellRendererText()
215 column = gtk.TreeViewColumn(_('Track'), cell, text=COL_TRACK)
216 view.append_column(column)
217 column.set_resizable(True)
218 column.set_reorderable(False)
220 cell = gtk.CellRendererText()
221 column = gtk.TreeViewColumn(_('Time'), cell, text=COL_TIME)
222 view.append_column(column)
223 column.set_resizable(True)
224 column.set_reorderable(False)
226 cell = gtk.CellRendererProgress()
227 column = gtk.TreeViewColumn(_('Status'), cell, text=COL_STATUS, value=COL_PERCENT)
228 view.append_column(column)
229 column.set_resizable(True)
230 column.set_reorderable(False)
232 view.connect('row-activated', self.activate)
233 self.selection = view.get_selection()
234 self.handler = self.selection.connect('changed', self.set_selection)
236 self.table = gtk.Table(5, 3, False)
237 x_pad = 2
238 y_pad = 1
240 self.artwork = gtk.Image()
241 self.table.attach(self.artwork, 2, 3, 0, 5, 0, 0, x_pad, y_pad)
243 self.artist_entry = gtk.Entry(max=255)
244 self.artist_entry.connect('changed', self.stuff_changed)
245 self.table.attach(gtk.Label(str=_('Artist')), 0, 1, 2, 3, 0, 0, 4, y_pad)
246 self.table.attach(self.artist_entry, 1, 2, 2, 3, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
248 self.album_entry = gtk.Entry(max=255)
249 self.album_entry.connect('changed', self.stuff_changed)
250 self.table.attach(gtk.Label(str=_('Album')), 0, 1, 3, 4, 0, 0, 4, y_pad)
251 self.table.attach(self.album_entry, 1, 2, 3, 4, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
253 self.genre_entry = gtk.Entry(max=255)
254 self.genre_entry.connect('changed', self.stuff_changed)
255 self.table.attach(gtk.Label(str=_('Genre')), 0, 1, 4, 5, 0, 0, 4, y_pad)
256 self.table.attach(self.genre_entry, 1, 2, 4, 5, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
258 self.year_entry = gtk.Entry(max=4)
259 self.year_entry.connect('changed', self.stuff_changed)
260 self.table.attach(gtk.Label(str=_('Year')), 0, 1, 5, 6, 0, 0, 4, y_pad)
261 self.table.attach(self.year_entry, 1, 2, 5, 6, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad)
263 hbox1 = gtk.HBox()
265 ck = gtk.CheckButton(label=_('Eject after Rip'))
266 ck.set_active(bool(EJECT_AFTER_RIP.int_value))
267 ck.connect('toggled', self.toggled, EJECT_AFTER_RIP)
268 hbox1.pack_start(ck, False, False, 5)
270 ck = gtk.CheckButton(label=_('Keep WAV files'))
271 ck.set_active(bool(KEEP_WAV.int_value))
272 ck.connect('toggled', self.toggled, KEEP_WAV)
273 hbox1.pack_start(ck, False, False, 5)
275 ck = gtk.CheckButton(label=_('Rip Only'))
276 ck.set_active(bool(RIP_ONLY.int_value))
277 ck.connect('toggled', self.toggled, RIP_ONLY)
278 hbox1.pack_start(ck, False, False, 5)
280 self.table.attach(hbox1, 0, 3, 6, 7, True, True, x_pad, y_pad)
283 def toggled(self, button, param):
284 param.int_value = button.get_active()
287 def update_gui(self):
288 '''Update UI based on current state'''
289 while True:
290 cd_status = cd_logic.check_dev()
291 if self.cd_status != cd_status:
292 self.cd_status = cd_status
293 self.cd_status_changed = True
295 if self.is_ripping or self.is_encoding:
296 self.stop_btn.set_sensitive(True)
297 self.rip_btn.set_sensitive(False)
298 self.refresh_btn.set_sensitive(False)
300 if not self.is_ripping and not self.is_encoding and not self.is_cddbing:
301 self.stop_btn.set_sensitive(False)
302 self.rip_btn.set_sensitive(bool(self.disc_id is not None))
303 self.refresh_btn.set_sensitive(True)
305 #get tracks if cd changed and not doing other things
306 if self.cd_status_changed:
307 if self.cd_status in [CDROM.CDS_TRAY_OPEN, CDROM.CDS_NO_DISC]:
308 self.no_disc()
309 self.disc_id = None
310 else:
311 disc_id = cd_logic.get_disc_id()
312 if self.disc_id <> disc_id:
313 self.disc_id = disc_id
314 self.do_get_tracks()
315 self.cd_status_changed = False
317 if self.closing:
318 break
320 yield tasks.TimeoutBlocker(1)
323 def scale_cover(self, pixbuf):
324 w = pixbuf.get_width()
325 h = pixbuf.get_height()
326 if w < COVER_SIZE and h < COVER_SIZE:
327 return pixbuf
328 scale = 1.0
329 if w < COVER_SIZE*2 and h < COVER_SIZE*2:
330 scale = 0.5
331 else:
332 if w > h:
333 scale = float(COVER_SIZE)/float(w)
334 else:
335 scale = float(COVER_SIZE)/float(h)
337 return pixbuf.scale_simple(int(scale*w), int(scale*h), gtk.gdk.INTERP_BILINEAR)
340 def xds_load_from_stream(self, name, type, stream):
341 loader = gtk.gdk.PixbufLoader()
342 buf = stream.read()
343 loader.write(buf)
344 pbuf = loader.get_pixbuf()
345 self.artwork_saved = False
346 self.set_artwork(pbuf)
349 def set_artwork(self, pbuf):
350 if pbuf:
351 self.artwork.set_from_pixbuf(self.scale_cover(pbuf))
352 self.set_size_request(COVER_SIZE, COVER_SIZE)
353 self.save_artwork()
356 def save_artwork(self, force=False):
357 pbuf = self.artwork.get_pixbuf()
358 if not pbuf: return
360 # We're not sure about the target dir yet, so skip it.
361 if not force and not self.is_ripping and not self.is_encoding:
362 return
364 # OK, we're have the dir, check for existing file and ask to overwrite
365 filename = os.path.join(self.library, '.DirIcon')
366 if os.path.exists(filename):
367 # Don't keep asking
368 if self.artwork_saved:
369 return
370 if not rox.confirm(_("Overwrite current Artwork?"), gtk.STOCK_SAVE):
371 return
373 # Save the damn thing already, will ya?
374 pbuf.save(filename, 'png')
375 self.artwork_saved = True
378 def stuff_changed(self, button=None):
379 '''Get new text from edit boxes and save it'''
380 self.genre = self.genre_entry.get_text()
381 self.artist = self.artist_entry.get_text()
382 self.album = self.album_entry.get_text()
383 self.year = self.year_entry.get_text()
384 self.parse_library()
387 def stop(self, *it):
388 '''Stop current rip/encode process'''
389 self.stop_request = True
392 def parse_library(self):
393 library = os.path.expanduser(LIBRARY.value)
394 filespec = ""
395 if "$" in library:
396 (library, filespec) = os.path.split(library)
397 library = string.replace(library, '$A', strip_illegal(self.artist))
398 library = string.replace(library, '$L', strip_illegal(self.album))
399 self.library = library
400 self.filespec = filespec #still may contain $ parameters, we parse these later
403 def build_filename(self, track, tracknum):
404 fn = self.filespec
405 fn = string.replace(fn, '$A', strip_illegal(self.artist))
406 fn = string.replace(fn, '$L', strip_illegal(self.album))
407 fn = string.replace(fn, '$S', strip_illegal(track))
408 fn = string.replace(fn, '$T', "%02d" % (tracknum+1))
409 return fn
412 def show_dir(self, *dummy):
413 ''' Pops up a filer window. '''
414 filer.show_file(self.library)
417 def do_get_tracks(self, button=None):
418 '''Get the track info (cddb and cd) in a thread'''
419 if self.is_ripping:
420 return
421 tasks.Task(self.get_tracks())
424 def no_disc(self):
425 '''Clear all info and display <no disc>'''
426 #dbg("no disc in tray?")
427 self.store.clear()
428 self.artist_entry.set_text(_('<no disc>'))
429 self.album_entry.set_text('')
430 self.genre_entry.set_text('')
431 self.year_entry.set_text('')
432 self.view.columns_autosize()
433 self.stuff_changed()
436 def get_tracks(self):
437 '''Get the track info (cddb and cd)'''
438 self.is_cddbing = True
439 stuff = self.do_cddb()
440 (count, artist, album, genre, year, tracklist) = stuff
442 yield None
444 self.count = count
445 self.tracklist = tracklist
447 if artist: self.artist_entry.set_text(artist)
448 if album: self.album_entry.set_text(album)
449 if genre: self.genre_entry.set_text(genre)
450 if year: self.year_entry.set_text(year)
451 self.stuff_changed()
453 self.store.clear()
454 for track in tracklist:
455 #dbg(track)
456 iter = self.store.append(None)
457 self.store.set(iter, COL_TRACK, track[0])
458 self.store.set(iter, COL_TIME, track[1])
459 self.store.set(iter, COL_ENABLE, True)
460 self.store.set(iter, COL_STATUS, None)
461 self.store.set(iter, COL_STATUS, "")
462 yield None
464 self.view.columns_autosize()
465 self.is_cddbing = False
468 def do_cddb(self):
469 '''Query cddb for track and cd info'''
470 dlg = gtk.MessageDialog(buttons=gtk.BUTTONS_CANCEL, message_format="Getting Track Info.")
471 dlg.show()
472 dlg.set_transient_for(self)
473 dlg.set_modal(True)
474 while gtk.events_pending():
475 gtk.main_iteration()
477 count = artist = genre = album = year = ''
478 tracklist = []
479 tracktime = []
481 #Note: all the nested try|except|pass statements are to ensure that as much
482 #info is processed as possible. One exception should not stop
483 #the whole thing and return nothing.
485 try:
486 count = cd_logic.total_tracks()
487 cddb_id = cd_logic.get_cddb_id()
489 #PyCDDB wants a string delimited by spaces, go figure.
490 cddb_id_string = ''
491 for n in cddb_id:
492 cddb_id_string += str(n)+' '
493 #dbg(disc_id, cddb_id, cddb_id_string)
495 for i in range(count):
496 tracktime = cd_logic.get_track_time_total(i+1)
497 track_time = time.strftime('%M:%S', time.gmtime(tracktime))
498 tracklist.append((_('Track')+`i`,track_time))
500 try:
501 db = PyCDDB.PyCDDB(CDDB_SERVER.value)
502 query_info = db.query(cddb_id_string)
503 #dbg(query_info)
505 dlg.set_title(_('Got Disc Info'))
506 while gtk.events_pending():
507 gtk.main_iteration()
509 #make sure we didn't get an error, then query CDDB
510 if len(query_info) > 0:
511 rndm = Random()
512 index = rndm.randrange(0, len(query_info))
513 read_info = db.read(query_info[index])
514 #dbg(read_info)
515 dlg.set_title(_('Got Track Info'))
516 while gtk.events_pending():
517 gtk.main_iteration()
519 try:
520 (artist, album) = query_info[index]['title'].split('/')
521 artist = artist.strip().decode('latin-1', 'replace')
522 album = album.strip().decode('latin-1', 'replace')
523 genre = query_info[index]['category']
524 if genre in ['misc', 'data']:
525 genre = 'Other'
527 dbg(query_info['year'])
528 dbg(read_info['EXTD'])
529 dbg(read_info['YEARD'])
531 #x = re.match(r'.*YEAR: (.+).*',read_info['EXTD'])
532 #if x:
533 # print x.group(1)
534 # year = x.group(1)
535 except:
536 pass
538 if len(read_info['TTITLE']) > 0:
539 for i in range(count):
540 try:
541 track_name = read_info['TTITLE'][i].decode('latin-1', 'replace')
542 track_time = tracklist[i][1]
543 #dbg(i, track_name, track_time)
544 tracklist[i] = (track_name, track_time)
545 except:
546 pass
547 except:
548 pass
550 except:
551 pass
553 dlg.destroy()
554 return count, artist, album, genre, year, tracklist
557 def run_ripper(self, tracknum, track, filename):
558 '''Rip selected tracks from the CD'''
559 cmd = RIPPER.value
560 cmd = string.replace(cmd, '$D', DEVICE.value)
561 cmd = string.replace(cmd, '$T', str(tracknum+1))
562 cmd = string.replace(cmd, '$F', filename)
563 thing = popen2.Popen4(cmd)
564 return thing
567 def run_encoder(self, tracknum, track, filename, artist, genre, album, year):
568 '''Encode each WAV file'''
569 if year is None or not len(year):
570 year = "1900"
572 cmd = ENCODER.value
573 cmd = string.replace(cmd, '$A', artist)
574 cmd = string.replace(cmd, '$L', album)
575 cmd = string.replace(cmd, '$S', track)
576 cmd = string.replace(cmd, '$G', genre)
577 cmd = string.replace(cmd, '$T', str(tracknum+1))
578 cmd = string.replace(cmd, '$Y', year)
579 cmd = string.replace(cmd, '$F', filename)
580 thing = popen2.Popen4(cmd)
581 return thing
584 def rip_n_encode(self, button=None):
585 '''Process all selected tracks (rip and encode)'''
586 try:
587 rox.fileutils.makedirs(self.library)
588 os.chdir(self.library)
589 except:
590 rox.alert(_("Failed to find or create Library dir. Cannot save files."))
591 return
593 self.save_artwork(True) #force
595 self.stop_request = False
596 self.wavqueue = Queue.Queue(1000)
597 tasks.Task(self.ripit())
598 tasks.Task(self.encodeit())
601 def ripit(self):
602 '''Thread to rip all selected tracks'''
603 self.is_ripping = True
605 for tracknum in range(self.count):
606 if self.stop_request:
607 break;
609 if self.store[tracknum][COL_ENABLE]:
610 track = self.store[tracknum][COL_TRACK]
611 filename = self.build_filename(track, tracknum)
612 thing = self.run_ripper(tracknum, track, filename)
613 outfile = thing.fromchild
614 percent = 0
615 last_percent = -1
616 handler = support.get_handler(RIPPER.value)
617 while True:
618 blocker = tasks.InputBlocker(outfile)
619 yield blocker
621 if self.stop_request:
622 os.kill(thing.pid, signal.SIGKILL)
623 break
625 line = myreadline(outfile)
626 if line:
627 percent = handler.get_percent(line)
628 if percent <> last_percent:
629 self.status_update(tracknum, 'rip', percent)
630 last_percent = percent
631 else:
632 break
633 status = thing.wait()
634 self.status_update(tracknum, 'rip', 100)
636 if status <> 0:
637 #dbg('ripper died %d' % status)
638 self.status_update(tracknum, 'rip_error', 0)
639 else:
640 #push this track on the queue for the encoder
641 if self.wavqueue and not RIP_ONLY.int_value:
642 self.wavqueue.put((track, tracknum, filename))
644 #push None object to tell encoder we're done
645 if self.wavqueue:
646 self.wavqueue.put((None, None, None))
648 self.is_ripping = False
649 cd_logic.stop()
650 if EJECT_AFTER_RIP.int_value:
651 cd_logic.eject()
654 def encodeit(self):
655 '''Thread to encode all tracks from the wavqueue'''
656 self.is_encoding = True
658 while True:
659 if self.stop_request:
660 break
662 try:
663 (track, tracknum, filename) = self.wavqueue.get(False)
664 except Queue.Empty:
665 yield tasks.TimeoutBlocker(1)
666 continue
668 if track == None:
669 break
671 thing = self.run_encoder(tracknum, track, filename, self.artist, self.genre, self.album, self.year)
672 outfile = thing.fromchild
674 handler = support.get_handler(ENCODER.value)
675 while True:
676 blocker = tasks.InputBlocker(outfile)
677 yield blocker
679 if self.stop_request:
680 os.kill(thing.pid, signal.SIGKILL)
681 break
683 line = myreadline(outfile)
684 #dbg(line)
685 if line:
686 percent = handler.get_percent(line)
687 self.status_update(tracknum, 'enc', percent)
688 else:
689 break
691 status = thing.wait()
692 self.status_update(tracknum, 'enc', 100)
694 if status <> 0:
695 dbg('encoder died %d' % status)
696 self.status_update(tracknum, 'enc_error', 0)
698 if not KEEP_WAV.int_value:
699 try: os.unlink(filename+".wav")
700 except: pass
702 self.is_encoding = False
705 def status_update(self, row, state, percent):
706 '''Callback from rip/encode tasks to update display'''
707 if 'rip' in state:
708 msg1 = _('Ripping')
709 elif 'enc' in state:
710 msg1 = _('Encoding')
712 if 'error' in state:
713 msg2 = ': %s' % _('error')
714 # elif percent == -1:
715 # pulse #how to handle unknown processes?
716 elif percent < 100:
717 msg2 = ': %d%%' % percent
718 else:
719 msg2 = ': %s' % _('done')
721 iter = self.store.get_iter((row,))
722 if iter:
723 self.store.set_value(iter, COL_STATUS, msg1+msg2)
724 self.store.set_value(iter, COL_PERCENT, percent)
727 def activate(self, view, path, column):
728 '''Edit a track name'''
729 model, iter = self.view.get_selection().get_selected()
730 if iter:
731 track = model.get_value(iter, COL_TRACK)
732 dlg = gtk.Dialog(APP_NAME)
733 dlg.set_default_size(350, 100)
734 dlg.set_transient_for(self)
735 dlg.set_modal(True)
736 dlg.show()
738 entry = gtk.Entry()
739 entry.set_text(track)
740 entry.show()
741 entry.set_activates_default(True)
742 dlg.vbox.pack_start(entry)
744 dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK)
745 dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
746 dlg.set_default_response(gtk.RESPONSE_OK)
747 response = dlg.run()
749 if response == gtk.RESPONSE_OK:
750 track = entry.get_text()
751 #dbg(track)
752 model.set_value(iter, COL_TRACK, track)
753 self.view.columns_autosize()
755 dlg.destroy()
758 def toggle_check(self, cell, rownum):
759 '''Toggle state for each song'''
760 row = self.store[rownum]
761 row[COL_ENABLE] = not row[COL_ENABLE]
762 self.store.row_changed(rownum, row.iter)
765 def set_selection(self, thing):
766 '''Get current selection'''
767 # Not needed, just here for future reference.
768 #model, iter = self.view.get_selection().get_selected()
769 #if iter:
770 # track = model.get_value(iter, COL_TRACK)
772 def button_press(self, text, event):
773 '''Popup menu handler'''
774 if event.button != 3:
775 return 0
776 self.menu.popup(self, event)
777 return 1
779 def show_options(self, button=None):
780 '''Show Options dialog'''
781 rox.edit_options()
783 def get_options(self):
784 '''Get changed Options'''
785 #TODO Update toggle buttons
786 self.stuff_changed()
788 def show_help(self, button=None):
789 rox.filer.open_dir(os.path.join(rox.app_dir, 'Help'))
791 def delete_event(self, ev, e1):
792 '''Bye-bye'''
793 self.close()
795 def close(self, button = None):
796 '''We are outta here!'''
797 self.stop()
798 self.closing = True
799 if self.is_ripping or self.is_encoding:
800 dlg = gtk.MessageDialog(self, gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT)
801 dlg.set_title(_("Please Wait"))
802 dlg.set_markup(_("Stopping current operation..."))
803 dlg.show()
804 while self.is_ripping or self.is_encoding:
805 while gtk.events_pending():
806 gtk.main_iteration()
807 dlg.destroy()
808 self.destroy()