From 7ff2690b4596f74db7ab420575a0bb223a50c439 Mon Sep 17 00:00:00 2001 From: khayber Date: Wed, 25 Oct 2006 15:48:52 +0000 Subject: [PATCH] See the changelog. :) --- AppInfo.xml | 8 +- Help/Changes | 9 ++ Help/README | 2 +- Help/Recipies | 31 +++++ Help/TODO | 1 - Options.xml | 101 ++++++--------- ripper.py | 402 ++++++++++++++++++++++++++++++++++------------------------ support.py | 76 +++++++++++ 8 files changed, 399 insertions(+), 231 deletions(-) create mode 100644 Help/Recipies rewrite Options.xml (79%) create mode 100644 support.py diff --git a/AppInfo.xml b/AppInfo.xml index 6977af8..725f77f 100644 --- a/AppInfo.xml +++ b/AppInfo.xml @@ -7,12 +7,12 @@ - MP3/OGG CD Ripper - MP3/OGG CD Ripper + CD Ripper + CD Ripper - MP3/OGG CD Ripper + CD Ripper Ken Hayber - 006 (13-Sep-2006) + 007 (18-Oct-2006) GNU General Public License http://www.hayber.us/rox/Ripper diff --git a/Help/Changes b/Help/Changes index bd5ca42..0e5bd14 100644 --- a/Help/Changes +++ b/Help/Changes @@ -1,3 +1,12 @@ +007 (24-Oct-2006) +- Rewrite options and processors to allow other rip/encoders to be used + (other than cdda2wav, lame and oggenc) +- Add options to keep WAV files and not Encode (partly suggested by Jonatan Liljedahl) +- Add Album Cover art support (based on idea and code from Stephen Watson) +- Change UI to use Progress bars in status area (requires gtk 2.6+) +- Configurable Directory and Filename patterns (e.g. Track Number, Artist, etc.) +- Bunches of general fixes and stuff :) + 006 (13-Sep-2006) - Remove Threads usage (using ROX-Lib's tasks module) - Modularize the UI code diff --git a/Help/README b/Help/README index 168a24a..0b08443 100644 --- a/Help/README +++ b/Help/README @@ -7,7 +7,7 @@ Brief GUI front-end to cdda2wav and lame to rip and encode CD music to mp3 or ogg The latest version of Ripper is at: - http://khayber.dyndns.org/rox/ripper + http://hayber.us/rox/Ripper Conditions diff --git a/Help/Recipies b/Help/Recipies new file mode 100644 index 0000000..66f312b --- /dev/null +++ b/Help/Recipies @@ -0,0 +1,31 @@ + +Rippers +~~~~~~~ +Commands and parameters for supported ripper commands. + +cdda2wav: + cdda2wav -g -x -H -D/dev/cdrom -A/dev/cdrom -t $T "$F" + + This is the fastest ripper on my system. However it takes over the CPU + so much that I always run it with 'nice'. + +cdparanoia: + cdparanoia -e $T "$F" + + The -e parameter is required to get progress reporting from cdparanoia + as this generates a lot of extra output, performance is a bit lower. + + +Encoders +~~~~~~~~ +These examples also add tags to the file. You could remove these parameters +if you use a separate tagging application. + +lame: + lame --ta "$A" --tt "$S" --tl "$L" --tg "$G" --tn $T --ty $Y "$F.wav" "$F.mp3" + +oggenc: + oggenc -a "$A" -t "$S" -l "$L" -G "$G" -N $T -d $Y "$F.wav" -o "$F.oga" + + Note: Recent mime-type database lists *.ogg as Video. Set to *.oga for Audio. + diff --git a/Help/TODO b/Help/TODO index 26a0faa..8262607 100644 --- a/Help/TODO +++ b/Help/TODO @@ -1,6 +1,5 @@ TODO items for Ripper -* Support cdparanoia and other ripper/encoders. * Provide list of alternate CDDB query results to allow user to choose (currently just picks one at random) * More options (e.g. where to put wav files, how to name dirs and mp3 files) \ No newline at end of file diff --git a/Options.xml b/Options.xml dissimilarity index 79% index 3a37ea2..e90b2b4 100644 --- a/Options.xml +++ b/Options.xml @@ -1,60 +1,41 @@ - - -
- - The (optional) default location of your music. - - - The full url to reach freedb or equivalent. By default this is 'http://freedb.freedb.org/~cddb/cddb.cgi', but there are mirrors. Leave blank to disable CDDB queries. - -
- -
- -The application used for ripping from CD. -If this is not in your PATH you need to give the full path to the executable. -To run at lower priority run with 'nice'. (e.g. 'nice cdda2wav') - - - Device name of cdrom. For example: /dev/cdrom. - - - SCSI-style device name. For example: 0,0,1 or ATAPI:0,1,0. - - - Options for the encoder. For example: quality, speed, etc. - - - Eject CD after ripping is complete (Encoding may still be running). - - -
- -
- - - - - - - - - -The application used for encoding to MP3. -If this is not in your PATH you need to give the full path to the executable. -To run at lower priority run with 'nice'. (e.g. 'nice lame') - - - Options for the encoder. For example: vbr, id3 tags, min bitrate. - - -The application used for encoding to OGG. -If this is not in your PATH you need to give the full path to the executable. -To run at lower priority run with 'nice'. (e.g. 'nice oggenc') - - - Options for the encoder. For example: ???. - -
- -
+ + +
+ + + + + + + + + The full url to reach freedb or equivalent. By default this is 'http://freedb.freedb.org/~cddb/cddb.cgi', but there are mirrors. Leave blank to disable CDDB queries. + + + The CD device (e.g. /dev/cdrom). Needed at least for CDDB, even if your rip command below doesn't need it. + + + Eject CD after ripping is complete (Encoding may still be running). + + + Do not delete the WAV files after Encoding. + + + Do not Encode, only Rip to WAV files. + + +
+
+ + + + + + + + + + + +
+
diff --git a/ripper.py b/ripper.py index 54b9104..cd71111 100644 --- a/ripper.py +++ b/ripper.py @@ -24,10 +24,10 @@ import gtk, os, sys, signal, re, string, socket, time, popen2, Queue from random import Random import rox -from rox import i18n, app_options, Menu, filer, InfoWin, tasks +from rox import i18n, app_options, Menu, filer, InfoWin, tasks, loading, fileutils from rox.options import Option -import PyCDDB, cd_logic, CDROM, genres +import PyCDDB, cd_logic, CDROM, genres, support try: import xattr @@ -49,36 +49,29 @@ rox.setup_app_options(APP_NAME, site='hayber.us') Menu.set_save_name(APP_NAME, site='hayber.us') #assume that everyone puts their music in ~/Music -LIBRARY = Option('library', '~/Music') +LIBRARY = Option('library', '~/Music/$A/$L/$S') #RIPPER options -RIPPER = Option('ripper', 'cdda2wav') -RIPPER_DEV = Option('ripper_dev', '/dev/cdrom') -RIPPER_LUN = Option('ripper_lun', 'ATAPI:0,1,0') -RIPPER_OPTS = Option('ripper_opts', '-x -H') +DEVICE = Option('device', '/dev/cdrom') +RIPPER = Option('ripper', 'nice cdda2wav -g -x -H -D$D -A$D -t $T "$F"') +ENCODER = Option('encoder', 'nice lame --ta "$A" --tt "$S" --tl "$L" --tg "$G" --tn $T --ty $Y "$F.wav" "$F.mp3"') EJECT_AFTER_RIP = Option('eject_after_rip', '0') +RIP_ONLY = Option('rip_only', '0') +KEEP_WAV = Option('keep_wav', '0') -#ENCODER options -ENCODER = Option('encoder', 'MP3') - -MP3_ENCODER = Option('mp3_encoder', 'lame') -MP3_ENCODER_OPTS = Option('mp3_encoder_opts', '--vbr-new -b160 --nohist --add-id3v2') - -OGG_ENCODER = Option('ogg_encoder', 'oggenc') -OGG_ENCODER_OPTS = Option('ogg_encoder_opts', '-q5') - -#CDDB Server and Options CDDB_SERVER = Option('cddb_server', 'http://freedb.freedb.org/~cddb/cddb.cgi') rox.app_options.notify() +COVER_SIZE = 96 #Column indicies COL_ENABLE = 0 COL_TRACK = 1 COL_TIME = 2 COL_STATUS = 3 +COL_PERCENT = 4 DEBUG = 0 def dbg(*args): @@ -119,16 +112,23 @@ def strip_illegal(instr): return str -class Ripper(rox.Window): +class Ripper(rox.Window, rox.loading.XDSLoader): '''Rip and Encode a CD''' def __init__(self): rox.Window.__init__(self) + # Support dropping Album Cover Art on our Window. + # Get types supported by gdk-pixbuf + mtypes = [] + for fmt in gtk.gdk.pixbuf_get_formats(): + mtypes += fmt['mime_types'] + rox.loading.XDSLoader.__init__(self, mtypes) + self.set_title(APP_NAME) self.set_default_size(450, 500) self.set_position(gtk.WIN_POS_MOUSE) - #capture wm delete event + # Capture wm delete event self.connect("delete_event", self.delete_event) # Update things when options change @@ -152,17 +152,13 @@ class Ripper(rox.Window): self.is_cddbing = False self.stop_request = False self.closing = False + self.artwork_saved = False - cd_logic.set_dev(RIPPER_DEV.value) - self.cd_status = cd_logic.check_dev() + cd_logic.set_dev(DEVICE.value) + self.cd_status = None self.disc_id = None self.cd_status_changed = False - if self.cd_status in [CDROM.CDS_TRAY_OPEN, CDROM.CDS_NO_DISC]: - self.no_disc() - else: - self.do_get_tracks() - tasks.Task(self.update_gui()) @@ -178,7 +174,7 @@ class Ripper(rox.Window): Menu.Action(_('Stop'), 'stop', '', gtk.STOCK_STOP), Menu.Separator(), Menu.Action(_('Options'), 'show_options', '', gtk.STOCK_PREFERENCES), - Menu.Action(_('Info'), 'get_info', '', gtk.STOCK_DIALOG_INFO), + Menu.Action(_('Help'), 'show_help', '', gtk.STOCK_HELP), Menu.Action(_("Quit"), 'close', '', gtk.STOCK_CLOSE), ]) self.menu.attach(self,self) @@ -187,6 +183,7 @@ class Ripper(rox.Window): def build_toolbar(self): self.toolbar = gtk.Toolbar() self.toolbar.set_style(gtk.TOOLBAR_ICONS) + self.toolbar.insert_stock(gtk.STOCK_HELP, _('Help'), None, self.show_help, None, 0) self.toolbar.insert_stock(gtk.STOCK_PREFERENCES, _('Settings'), None, self.show_options, None, 0) self.stop_btn = self.toolbar.insert_stock(gtk.STOCK_STOP, _('Stop'), None, self.stop, None, 0) self.rip_btn = self.toolbar.insert_stock(gtk.STOCK_EXECUTE, _('Rip & Encode'), None, self.rip_n_encode, None, 0) @@ -201,7 +198,7 @@ class Ripper(rox.Window): swin.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) swin.set_shadow_type(gtk.SHADOW_IN) - self.store = gtk.ListStore(int, str, str, str) + self.store = gtk.ListStore(int, str, str, str, int) view = gtk.TreeView(self.store) self.view = view swin.add(view) @@ -215,19 +212,19 @@ class Ripper(rox.Window): column.set_reorderable(False) cell = gtk.CellRendererText() - column = gtk.TreeViewColumn(_('Track'), cell, text = COL_TRACK) + column = gtk.TreeViewColumn(_('Track'), cell, text=COL_TRACK) view.append_column(column) column.set_resizable(True) column.set_reorderable(False) cell = gtk.CellRendererText() - column = gtk.TreeViewColumn(_('Time'), cell, text = COL_TIME) + column = gtk.TreeViewColumn(_('Time'), cell, text=COL_TIME) view.append_column(column) column.set_resizable(True) column.set_reorderable(False) - cell = gtk.CellRendererText() - column = gtk.TreeViewColumn(_('Status'), cell, text = COL_STATUS) + cell = gtk.CellRendererProgress() + column = gtk.TreeViewColumn(_('Status'), cell, text=COL_STATUS, value=COL_PERCENT) view.append_column(column) column.set_resizable(True) column.set_reorderable(False) @@ -236,10 +233,13 @@ class Ripper(rox.Window): self.selection = view.get_selection() self.handler = self.selection.connect('changed', self.set_selection) - self.table = gtk.Table(5, 2, False) + self.table = gtk.Table(5, 3, False) x_pad = 2 y_pad = 1 + self.artwork = gtk.Image() + self.table.attach(self.artwork, 2, 3, 0, 5, 0, 0, x_pad, y_pad) + self.artist_entry = gtk.Entry(max=255) self.artist_entry.connect('changed', self.stuff_changed) self.table.attach(gtk.Label(str=_('Artist')), 0, 1, 2, 3, 0, 0, 4, y_pad) @@ -250,18 +250,39 @@ class Ripper(rox.Window): self.table.attach(gtk.Label(str=_('Album')), 0, 1, 3, 4, 0, 0, 4, y_pad) self.table.attach(self.album_entry, 1, 2, 3, 4, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad) - genres.genre_list.sort() - self.genre_combo = gtk.Combo() - self.genre_combo.set_popdown_strings(genres.genre_list) - self.genre_combo.entry.connect('changed', self.stuff_changed) + self.genre_entry = gtk.Entry(max=255) + self.genre_entry.connect('changed', self.stuff_changed) self.table.attach(gtk.Label(str=_('Genre')), 0, 1, 4, 5, 0, 0, 4, y_pad) - self.table.attach(self.genre_combo, 1, 2, 4, 5, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad) + self.table.attach(self.genre_entry, 1, 2, 4, 5, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad) self.year_entry = gtk.Entry(max=4) self.year_entry.connect('changed', self.stuff_changed) self.table.attach(gtk.Label(str=_('Year')), 0, 1, 5, 6, 0, 0, 4, y_pad) self.table.attach(self.year_entry, 1, 2, 5, 6, gtk.EXPAND|gtk.FILL, 0, x_pad, y_pad) + hbox1 = gtk.HBox() + + ck = gtk.CheckButton(label=_('Eject after Rip')) + ck.set_active(bool(EJECT_AFTER_RIP.int_value)) + ck.connect('toggled', self.toggled, EJECT_AFTER_RIP) + hbox1.pack_start(ck, False, False, 5) + + ck = gtk.CheckButton(label=_('Keep WAV files')) + ck.set_active(bool(KEEP_WAV.int_value)) + ck.connect('toggled', self.toggled, KEEP_WAV) + hbox1.pack_start(ck, False, False, 5) + + ck = gtk.CheckButton(label=_('Rip Only')) + ck.set_active(bool(RIP_ONLY.int_value)) + ck.connect('toggled', self.toggled, RIP_ONLY) + hbox1.pack_start(ck, False, False, 5) + + self.table.attach(hbox1, 0, 3, 6, 7, True, True, x_pad, y_pad) + + + def toggled(self, button, param): + param.int_value = button.get_active() + def update_gui(self): '''Update UI based on current state''' @@ -270,7 +291,7 @@ class Ripper(rox.Window): if self.cd_status != cd_status: self.cd_status = cd_status self.cd_status_changed = True - + if self.is_ripping or self.is_encoding: self.stop_btn.set_sensitive(True) self.rip_btn.set_sensitive(False) @@ -278,7 +299,7 @@ class Ripper(rox.Window): if not self.is_ripping and not self.is_encoding and not self.is_cddbing: self.stop_btn.set_sensitive(False) - self.rip_btn.set_sensitive(True) + self.rip_btn.set_sensitive(bool(self.disc_id is not None)) self.refresh_btn.set_sensitive(True) #get tracks if cd changed and not doing other things @@ -297,14 +318,70 @@ class Ripper(rox.Window): break yield tasks.TimeoutBlocker(1) + + + def scale_cover(self, pixbuf): + w = pixbuf.get_width() + h = pixbuf.get_height() + if w < COVER_SIZE and h < COVER_SIZE: + return pixbuf + scale = 1.0 + if w < COVER_SIZE*2 and h < COVER_SIZE*2: + scale = 0.5 + else: + if w > h: + scale = float(COVER_SIZE)/float(w) + else: + scale = float(COVER_SIZE)/float(h) + + return pixbuf.scale_simple(int(scale*w), int(scale*h), gtk.gdk.INTERP_BILINEAR) + def xds_load_from_stream(self, name, type, stream): + loader = gtk.gdk.PixbufLoader() + buf = stream.read() + loader.write(buf) + pbuf = loader.get_pixbuf() + self.artwork_saved = False + self.set_artwork(pbuf) + + + def set_artwork(self, pbuf): + if pbuf: + self.artwork.set_from_pixbuf(self.scale_cover(pbuf)) + self.set_size_request(COVER_SIZE, COVER_SIZE) + self.save_artwork() + + + def save_artwork(self, force=False): + pbuf = self.artwork.get_pixbuf() + if not pbuf: return + + # We're not sure about the target dir yet, so skip it. + if not force and not self.is_ripping and not self.is_encoding: + return + + # OK, we're have the dir, check for existing file and ask to overwrite + filename = os.path.join(self.library, '.DirIcon') + if os.path.exists(filename): + # Don't keep asking + if self.artwork_saved: + return + if not rox.confirm(_("Overwrite current Artwork?"), gtk.STOCK_SAVE): + return + + # Save the damn thing already, will ya? + pbuf.save(filename, 'png') + self.artwork_saved = True + + def stuff_changed(self, button=None): '''Get new text from edit boxes and save it''' - self.genre = self.genre_combo.entry.get_text() + self.genre = self.genre_entry.get_text() self.artist = self.artist_entry.get_text() self.album = self.album_entry.get_text() self.year = self.year_entry.get_text() + self.parse_library() def stop(self, *it): @@ -312,10 +389,29 @@ class Ripper(rox.Window): self.stop_request = True + def parse_library(self): + library = os.path.expanduser(LIBRARY.value) + filespec = "" + if "$" in library: + (library, filespec) = os.path.split(library) + library = string.replace(library, '$A', strip_illegal(self.artist)) + library = string.replace(library, '$L', strip_illegal(self.album)) + self.library = library + self.filespec = filespec #still may contain $ parameters, we parse these later + + + def build_filename(self, track, tracknum): + fn = self.filespec + fn = string.replace(fn, '$A', strip_illegal(self.artist)) + fn = string.replace(fn, '$L', strip_illegal(self.album)) + fn = string.replace(fn, '$S', strip_illegal(track)) + fn = string.replace(fn, '$T', "%02d" % (tracknum+1)) + return fn + + def show_dir(self, *dummy): ''' Pops up a filer window. ''' - temp = os.path.join(os.path.expanduser(LIBRARY.value), self.artist, self.album) - filer.show_file(temp) + filer.show_file(self.library) def do_get_tracks(self, button=None): @@ -327,52 +423,54 @@ class Ripper(rox.Window): def no_disc(self): '''Clear all info and display ''' - dbg("no disc in tray?") + #dbg("no disc in tray?") self.store.clear() self.artist_entry.set_text(_('')) self.album_entry.set_text('') - self.genre_combo.entry.set_text('') + self.genre_entry.set_text('') self.year_entry.set_text('') self.view.columns_autosize() + self.stuff_changed() def get_tracks(self): '''Get the track info (cddb and cd)''' self.is_cddbing = True - stuff = self.get_cddb() + stuff = self.do_cddb() (count, artist, album, genre, year, tracklist) = stuff yield None - self.artist = artist self.count = count - self.album = album - self.genre = genre - self.year = year self.tracklist = tracklist if artist: self.artist_entry.set_text(artist) if album: self.album_entry.set_text(album) - if genre: self.genre_combo.entry.set_text(genre) + if genre: self.genre_entry.set_text(genre) if year: self.year_entry.set_text(year) + self.stuff_changed() self.store.clear() for track in tracklist: - dbg(track) + #dbg(track) iter = self.store.append(None) self.store.set(iter, COL_TRACK, track[0]) self.store.set(iter, COL_TIME, track[1]) self.store.set(iter, COL_ENABLE, True) + self.store.set(iter, COL_STATUS, None) + self.store.set(iter, COL_STATUS, "") yield None self.view.columns_autosize() self.is_cddbing = False - def get_cddb(self): + def do_cddb(self): '''Query cddb for track and cd info''' dlg = gtk.MessageDialog(buttons=gtk.BUTTONS_CANCEL, message_format="Getting Track Info.") dlg.show() + dlg.set_transient_for(self) + dlg.set_modal(True) while gtk.events_pending(): gtk.main_iteration() @@ -426,9 +524,9 @@ class Ripper(rox.Window): if genre in ['misc', 'data']: genre = 'Other' - #dbg(query_info['year']) - #dbg(read_info['EXTD']) - #dbg(read_info['YEARD']) + dbg(query_info['year']) + dbg(read_info['EXTD']) + dbg(read_info['YEARD']) #x = re.match(r'.*YEAR: (.+).*',read_info['EXTD']) #if x: @@ -456,81 +554,46 @@ class Ripper(rox.Window): return count, artist, album, genre, year, tracklist - def run_cdda2wav(self, tracknum, track): - '''Run cdda2wav to rip a track from the CD''' - cdda2wav_cmd = RIPPER.value - cdda2wav_dev = RIPPER_DEV.value - cdda2wav_lun = RIPPER_LUN.value - cdda2wav_args = '-g -D%s -A%s -t %d "%s"' % ( - cdda2wav_lun, cdda2wav_dev, tracknum+1, strip_illegal(track)) - cdda2wav_opts = RIPPER_OPTS.value - - thing = popen2.Popen4(cdda2wav_cmd+' '+cdda2wav_opts+' '+cdda2wav_args ) + def run_ripper(self, tracknum, track, filename): + '''Rip selected tracks from the CD''' + cmd = RIPPER.value + cmd = string.replace(cmd, '$D', DEVICE.value) + cmd = string.replace(cmd, '$T', str(tracknum+1)) + cmd = string.replace(cmd, '$F', filename) + thing = popen2.Popen4(cmd) return thing - def run_lame(self, tracknum, track, artist, genre, album, year): - '''Run lame to encode a wav file to mp3''' - try: - int_year = int(year) - except: - int_year = 1 - - lame_cmd = MP3_ENCODER.value - lame_opts = MP3_ENCODER_OPTS.value - lame_tags = '--ta "%s" --tt "%s" --tl "%s" --tg "%s" --tn %d --ty %d' % ( - artist, track, album, genre, tracknum+1, int_year) - lame_args = '"%s" "%s"' % (strip_illegal(track)+'.wav', strip_illegal(track)+'.mp3') - - #dbg(lame_opts, lame_tags, lame_args) - thing = popen2.Popen4(lame_cmd+' '+lame_opts+' '+lame_tags+' '+lame_args ) - return thing - - - def run_ogg(self, tracknum, track, artist, genre, album, year): - '''Run oggenc to encode a wav file to ogg''' - try: - int_year = int(year) - except: - int_year = 1 - - ogg_cmd = OGG_ENCODER.value - ogg_opts = OGG_ENCODER_OPTS.value - ogg_tags = '-a "%s" -t "%s" -l "%s" -G "%s" -N %d -d %d' % ( - artist, track, album, genre, tracknum+1, int_year) - ogg_args = '"%s"' % (strip_illegal(track)+'.wav') - - #dbg(ogg_opts, ogg_tags, ogg_args) - thing = popen2.Popen4(ogg_cmd+' '+ogg_opts+' '+ogg_tags+' '+ogg_args ) + def run_encoder(self, tracknum, track, filename, artist, genre, album, year): + '''Encode each WAV file''' + if year is None or not len(year): + year = "1900" + + cmd = ENCODER.value + cmd = string.replace(cmd, '$A', artist) + cmd = string.replace(cmd, '$L', album) + cmd = string.replace(cmd, '$S', track) + cmd = string.replace(cmd, '$G', genre) + cmd = string.replace(cmd, '$T', str(tracknum+1)) + cmd = string.replace(cmd, '$Y', year) + cmd = string.replace(cmd, '$F', filename) + thing = popen2.Popen4(cmd) return thing def rip_n_encode(self, button=None): '''Process all selected tracks (rip and encode)''' try: - os.chdir(os.path.expanduser(LIBRARY.value)) + rox.fileutils.makedirs(self.library) + os.chdir(self.library) except: - try: - os.mkdir(os.path.expanduser(LIBRARY.value)) - os.chdir(os.path.expanduser(LIBRARY.value)) - except: - rox.alert("Failed to find or create Library dir") - - if self.count and self.artist and self.album: - try: os.mkdir(self.artist) - except: pass - - try: os.mkdir(self.artist+'/'+self.album) - except: pass + rox.alert(_("Failed to find or create Library dir. Cannot save files.")) + return - try: os.chdir(self.artist+'/'+self.album) - except: pass + self.save_artwork(True) #force self.stop_request = False - - #the queue to feed tracks from ripper to encoder self.wavqueue = Queue.Queue(1000) - tasks.Task(self.ripit()) tasks.Task(self.encodeit()) @@ -538,14 +601,19 @@ class Ripper(rox.Window): def ripit(self): '''Thread to rip all selected tracks''' self.is_ripping = True + for tracknum in range(self.count): if self.stop_request: break; if self.store[tracknum][COL_ENABLE]: track = self.store[tracknum][COL_TRACK] - thing = self.run_cdda2wav(tracknum, track) + filename = self.build_filename(track, tracknum) + thing = self.run_ripper(tracknum, track, filename) outfile = thing.fromchild + percent = 0 + last_percent = -1 + handler = support.get_handler(RIPPER.value) while True: blocker = tasks.InputBlocker(outfile) yield blocker @@ -556,27 +624,26 @@ class Ripper(rox.Window): line = myreadline(outfile) if line: - x = re.match(".*([ 0-9][0-9])%", line) - if x: - percent = int(x.group(1)) + percent = handler.get_percent(line) + if percent <> last_percent: self.status_update(tracknum, 'rip', percent) + last_percent = percent else: break - status = thing.wait() self.status_update(tracknum, 'rip', 100) if status <> 0: - #dbg('cdda2wav died %d' % status) + #dbg('ripper died %d' % status) self.status_update(tracknum, 'rip_error', 0) else: #push this track on the queue for the encoder - if self.wavqueue: - self.wavqueue.put((track, tracknum)) + if self.wavqueue and not RIP_ONLY.int_value: + self.wavqueue.put((track, tracknum, filename)) #push None object to tell encoder we're done if self.wavqueue: - self.wavqueue.put((None, None)) + self.wavqueue.put((None, None, None)) self.is_ripping = False cd_logic.stop() @@ -587,12 +654,13 @@ class Ripper(rox.Window): def encodeit(self): '''Thread to encode all tracks from the wavqueue''' self.is_encoding = True + while True: if self.stop_request: break try: - (track, tracknum) = self.wavqueue.get(False) + (track, tracknum, filename) = self.wavqueue.get(False) except Queue.Empty: yield tasks.TimeoutBlocker(1) continue @@ -600,12 +668,10 @@ class Ripper(rox.Window): if track == None: break - if ENCODER.value == 'MP3': - thing = self.run_lame(tracknum, track, self.artist, self.genre, self.album, self.year) - else: - thing = self.run_ogg(tracknum, track, self.artist, self.genre, self.album, self.year) - + thing = self.run_encoder(tracknum, track, filename, self.artist, self.genre, self.album, self.year) outfile = thing.fromchild + + handler = support.get_handler(ENCODER.value) while True: blocker = tasks.InputBlocker(outfile) yield blocker @@ -617,16 +683,8 @@ class Ripper(rox.Window): line = myreadline(outfile) #dbg(line) if line: - if ENCODER.value == 'MP3': - x = re.match(r"^[\s]+([0-9]+)/([0-9]+)", line) - if x: - percent = int(100 * (float(x.group(1)) / float(x.group(2)))) - self.status_update(tracknum, 'enc', percent) - else: #OGG - x = re.match('^.*\[[\s]*([.0-9]+)%\]', line) - if x: - percent = float(x.group(1)) - self.status_update(tracknum, 'enc', percent) + percent = handler.get_percent(line) + self.status_update(tracknum, 'enc', percent) else: break @@ -634,35 +692,36 @@ class Ripper(rox.Window): self.status_update(tracknum, 'enc', 100) if status <> 0: - #dbg('encoder died %d' % status) + dbg('encoder died %d' % status) self.status_update(tracknum, 'enc_error', 0) - try: os.unlink(strip_illegal(track)+".wav") - except: pass - try: os.unlink(strip_illegal(track)+".inf") - except: pass + if not KEEP_WAV.int_value: + try: os.unlink(filename+".wav") + except: pass self.is_encoding = False def status_update(self, row, state, percent): '''Callback from rip/encode tasks to update display''' + if 'rip' in state: + msg1 = _('Ripping') + elif 'enc' in state: + msg1 = _('Encoding') + + if 'error' in state: + msg2 = ': %s' % _('error') +# elif percent == -1: +# pulse #how to handle unknown processes? + elif percent < 100: + msg2 = ': %d%%' % percent + else: + msg2 = ': %s' % _('done') + iter = self.store.get_iter((row,)) - if not iter: return - if state == 'rip': - if percent < 100: - self.store.set_value(iter, COL_STATUS, _('Ripping')+': %d%%' % percent) - else: - self.store.set_value(iter, COL_STATUS, _('Ripping')+': '+_('done')) - if state == 'enc': - if percent < 100: - self.store.set_value(iter, COL_STATUS, _('Encoding')+': %d%%' % percent) - else: - self.store.set_value(iter, COL_STATUS, _('Encoding')+': '+_('done')) - if state == 'rip_error': - self.store.set_value(iter, COL_STATUS, _('Ripping')+': '+_('error')) - if state == 'enc_error': - self.store.set_value(iter, COL_STATUS, _('Encoding')+': '+_('error')) + if iter: + self.store.set_value(iter, COL_STATUS, msg1+msg2) + self.store.set_value(iter, COL_PERCENT, percent) def activate(self, view, path, column): @@ -672,6 +731,8 @@ class Ripper(rox.Window): track = model.get_value(iter, COL_TRACK) dlg = gtk.Dialog(APP_NAME) dlg.set_default_size(350, 100) + dlg.set_transient_for(self) + dlg.set_modal(True) dlg.show() entry = gtk.Entry() @@ -703,6 +764,7 @@ class Ripper(rox.Window): def set_selection(self, thing): '''Get current selection''' + # Not needed, just here for future reference. #model, iter = self.view.get_selection().get_selected() #if iter: # track = model.get_value(iter, COL_TRACK) @@ -720,10 +782,11 @@ class Ripper(rox.Window): def get_options(self): '''Get changed Options''' - pass + #TODO Update toggle buttons + self.stuff_changed() - def get_info(self): - InfoWin.infowin(APP_NAME) + def show_help(self, button=None): + rox.filer.open_dir(os.path.join(rox.app_dir, 'Help')) def delete_event(self, ev, e1): '''Bye-bye''' @@ -733,6 +796,15 @@ class Ripper(rox.Window): '''We are outta here!''' self.stop() self.closing = True + if self.is_ripping or self.is_encoding: + dlg = gtk.MessageDialog(self, gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT) + dlg.set_title(_("Please Wait")) + dlg.set_markup(_("Stopping current operation...")) + dlg.show() + while self.is_ripping or self.is_encoding: + while gtk.events_pending(): + gtk.main_iteration() + dlg.destroy() self.destroy() diff --git a/support.py b/support.py new file mode 100644 index 0000000..f3c3a73 --- /dev/null +++ b/support.py @@ -0,0 +1,76 @@ +import re + +class generic: + def get_percent(self, line): + return -1 + +class cdda2wav: + def __init__(self): + self.re_progress = re.compile(".*([ 0-9][0-9])%") + self.percent = 0 + + def get_percent(self, line): + x = self.re_progress.match(line) + if x: + self.percent = int(x.group(1)) + return self.percent + +class cdparanoia: + def __init__(self): + self.start_sec = None + self.end_sec = None + self.re_start_sec = re.compile(".*from sector +([0-9]+)") + self.re_end_sec = re.compile(".*to sector +([0-9]+)") + self.re_progress = re.compile("##: -2 \[wrote\] @ ([0-9]+)") + self.percent = 0 + + def get_percent(self, line): + if self.start_sec is None: + x = self.re_start_sec.match(line) + if x: + self.start_sec = int(x.group(1)) + if self.end_sec is None: + x = self.re_end_sec.match(line) + if x: + self.end_sec = int(x.group(1)) + if self.start_sec is not None and self.end_sec is not None: + x = self.re_progress.match(line) + if x: + self.percent = 100 * (int(x.group(1))/1176-self.start_sec) / (self.end_sec-self.start_sec) + return self.percent + +class lame: + def __init__(self): + self.re_progress = re.compile(r"^[\s]+([0-9]+)/([0-9]+)") + self.percent = 0 + + def get_percent(self, line): + x = self.re_progress.match(line) + if x: + self.percent = int(100 * (float(x.group(1)) / float(x.group(2)))) + return self.percent + +class oggenc: + def __init__(self): + self.re_progress = re.compile('^.*\[[\s]*([.0-9]+)%\]') + self.percent = 0 + + def get_percent(self, line): + x = self.re_progress.match(line) + if x: + self.percent = int(float(x.group(1))) + return self.percent + +codecs = { + 'cdda2wav': cdda2wav, + 'cdparanoia': cdparanoia, + 'lame': lame, + 'oggenc': oggenc, + 'generic': generic +} + +def get_handler(text): + for codec in codecs: + if codec in text: + return codecs[codec]() + return codecs['generic']() -- 2.11.4.GIT