[Alexandria-list] userland cuecut support

Christopher Cyll ccyll at wso.williams.edu
Mon Feb 7 00:29:44 EST 2005


Hi,

First of all, Alexandria is excellent. I cataloged my whole collection
this weekend!

One of the big draws for me was that Alexandria supported CueCat
(since I didn't want to type all the ISBNs in by hand). Unfortunately,
I run Linux 2.6 which means the CueCat driver wasn't an option for me.

There didn't seem to any other way to do this (I hope I didn't miss
anything), so I put together a patch that implements userland CueCat
scanning for Alexandria. In addition to letting me to use the CueCat
with Alexandria under Linux 2.6, it would also allow it work for other
operating systems that don't necessarily have a driver.

I wondered if there was any interest in this patch?

Here a few gotchas with it, though:

First it's against 0.4.0, not CVS. I'd be willing to redo this,
though.

Second, I've tried to write it in a way so that interpreters for other
scanners were easy to drop in. I just did it the first way that came
to mind, but if there's a better Ruby idiom for this, I'd be happy to
switch to it.

Lastly, I wonder if this is the optimal UI for the feature? I've added
a scanner radio button below the ISBN option, with a drop down box for
selecting scanner type and a text field for receiving scanner
data. However, it occurs to me that a better interface might be to
change the ISBN field to a more generic number/code label and then
have a drop down box which defaulted to ISBN, but could be changed to
CueCat, or other methods. Or maybe the right way to do it, is to take
the length limit off the ISBN field and just let it accept all kinds
of data and automagically do the right thing. What do you think?

I'd be happy to work with you to get this patch in if you're
interested, but I understand that userland interpretation of scanner
code might not be the direction you want to head in (though I think
the cross platform capability is a huge win).

Additionally, I noticed that when adding a book manually it requires
an ISBN. What's the rational behind this? I have a thesis, some
conference proceedings, and some just really old books that lack a
number. I'd love to be able to make manual entries for them, but
Alexandria's insistence on an ISBN has me at something of a loss. Is
there an alternate way to do this?

Anyways, great software and let me know if there's interest in the
patch!

Topher Cyll
-------------- next part --------------
diff -rup alexandria-0.4.0/data/alexandria/glade/new_book_dialog.glade alexandria-0.4.0-toph2/data/alexandria/glade/new_book_dialog.glade
--- alexandria-0.4.0/data/alexandria/glade/new_book_dialog.glade	2004-11-05 13:01:48.000000000 -0800
+++ alexandria-0.4.0-toph2/data/alexandria/glade/new_book_dialog.glade	2005-02-06 20:57:01.000000000 -0800
@@ -2,7 +2,6 @@
 <!DOCTYPE glade-interface SYSTEM "http://glade.gnome.org/glade-2.0.dtd">
 
 <glade-interface>
-<requires lib="gnome"/>
 
 <widget class="GtkDialog" id="new_book_dialog">
   <property name="border_width">8</property>
@@ -73,7 +72,7 @@
 	<widget class="GtkTable" id="table1">
 	  <property name="border_width">6</property>
 	  <property name="visible">True</property>
-	  <property name="n_rows">4</property>
+	  <property name="n_rows">5</property>
 	  <property name="n_columns">2</property>
 	  <property name="homogeneous">False</property>
 	  <property name="row_spacing">6</property>
@@ -119,8 +118,8 @@
 	    <packing>
 	      <property name="left_attach">0</property>
 	      <property name="right_attach">1</property>
-	      <property name="top_attach">1</property>
-	      <property name="bottom_attach">2</property>
+	      <property name="top_attach">2</property>
+	      <property name="bottom_attach">3</property>
 	      <property name="x_options">fill</property>
 	      <property name="y_options"></property>
 	    </packing>
@@ -150,8 +149,8 @@
 	    <packing>
 	      <property name="left_attach">0</property>
 	      <property name="right_attach">2</property>
-	      <property name="top_attach">2</property>
-	      <property name="bottom_attach">3</property>
+	      <property name="top_attach">3</property>
+	      <property name="bottom_attach">4</property>
 	      <property name="x_options">fill</property>
 	    </packing>
 	  </child>
@@ -203,7 +202,7 @@ by keyword</property>
 		      <property name="max_length">0</property>
 		      <property name="text" translatable="yes"></property>
 		      <property name="has_frame">True</property>
-		      <property name="invisible_char" translatable="yes">*</property>
+		      <property name="invisible_char">*</property>
 		      <property name="activates_default">False</property>
 		      <property name="width_chars">30</property>
 		      <signal name="changed" handler="on_changed" last_modification_time="Mon, 14 Jun 2004 16:24:17 GMT"/>
@@ -239,8 +238,8 @@ by keyword</property>
 	    <packing>
 	      <property name="left_attach">1</property>
 	      <property name="right_attach">2</property>
-	      <property name="top_attach">1</property>
-	      <property name="bottom_attach">2</property>
+	      <property name="top_attach">2</property>
+	      <property name="bottom_attach">3</property>
 	      <property name="x_options">fill</property>
 	      <property name="y_options">fill</property>
 	    </packing>
@@ -263,8 +262,8 @@ by keyword</property>
 	    <packing>
 	      <property name="left_attach">0</property>
 	      <property name="right_attach">1</property>
-	      <property name="top_attach">3</property>
-	      <property name="bottom_attach">4</property>
+	      <property name="top_attach">4</property>
+	      <property name="bottom_attach">5</property>
 	      <property name="y_padding">12</property>
 	      <property name="x_options">fill</property>
 	      <property name="y_options"></property>
@@ -293,7 +292,7 @@ by keyword</property>
 		      <property name="max_length">18</property>
 		      <property name="text" translatable="yes"></property>
 		      <property name="has_frame">True</property>
-		      <property name="invisible_char" translatable="yes">*</property>
+		      <property name="invisible_char">*</property>
 		      <property name="activates_default">False</property>
 		      <property name="width_chars">13</property>
 		      <signal name="changed" handler="on_changed" last_modification_time="Thu, 18 Mar 2004 23:45:37 GMT"/>
@@ -341,12 +340,104 @@ by keyword</property>
 	    <packing>
 	      <property name="left_attach">1</property>
 	      <property name="right_attach">2</property>
-	      <property name="top_attach">3</property>
-	      <property name="bottom_attach">4</property>
+	      <property name="top_attach">4</property>
+	      <property name="bottom_attach">5</property>
 	      <property name="x_options">expand|shrink|fill</property>
 	      <property name="y_options"></property>
 	    </packing>
 	  </child>
+
+	  <child>
+	    <widget class="GtkRadioButton" id="scanner_radiobutton">
+	      <property name="visible">True</property>
+	      <property name="can_focus">True</property>
+	      <property name="label" translatable="yes">S_canner:</property>
+	      <property name="use_underline">True</property>
+	      <property name="relief">GTK_RELIEF_NORMAL</property>
+	      <property name="focus_on_click">True</property>
+	      <property name="active">False</property>
+	      <property name="inconsistent">False</property>
+	      <property name="draw_indicator">True</property>
+	      <property name="group">isbn_radiobutton</property>
+	      <signal name="toggled" handler="on_criterion_toggled" last_modification_time="Mon, 14 Jun 2004 15:26:35 GMT"/>
+	    </widget>
+	    <packing>
+	      <property name="left_attach">0</property>
+	      <property name="right_attach">1</property>
+	      <property name="top_attach">1</property>
+	      <property name="bottom_attach">2</property>
+	      <property name="x_options">fill</property>
+	      <property name="y_options"></property>
+	    </packing>
+	  </child>
+
+	  <child>
+	    <widget class="GtkHBox" id="hbox4">
+	      <property name="visible">True</property>
+	      <property name="homogeneous">False</property>
+	      <property name="spacing">6</property>
+
+	      <child>
+		<widget class="GtkEventBox" id="eventbox_combo_scanner">
+		  <property name="visible">True</property>
+		  <property name="visible_window">False</property>
+		  <property name="above_child">True</property>
+		  <signal name="button_press_event" handler="on_clicked" last_modification_time="Sun, 06 Feb 2005 07:29:28 GMT"/>
+
+		  <child>
+		    <widget class="GtkComboBox" id="combo_scanner">
+		      <property name="visible">True</property>
+		      <property name="items" translatable="yes"></property>
+		    </widget>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">False</property>
+		  <property name="fill">False</property>
+		</packing>
+	      </child>
+
+	      <child>
+		<widget class="GtkEventBox" id="eventbox_entry_scanner">
+		  <property name="visible">True</property>
+		  <property name="visible_window">False</property>
+		  <property name="above_child">True</property>
+		  <signal name="button_press_event" handler="on_clicked" last_modification_time="Sun, 06 Feb 2005 07:29:42 GMT"/>
+
+		  <child>
+		    <widget class="GtkEntry" id="entry_scanner">
+		      <property name="visible">True</property>
+		      <property name="sensitive">False</property>
+		      <property name="can_focus">True</property>
+		      <property name="editable">True</property>
+		      <property name="visibility">True</property>
+		      <property name="max_length">0</property>
+		      <property name="text" translatable="yes"></property>
+		      <property name="has_frame">True</property>
+		      <property name="invisible_char">*</property>
+		      <property name="activates_default">False</property>
+		      <signal name="changed" handler="on_changed" last_modification_time="Thu, 18 Mar 2004 23:45:37 GMT"/>
+		      <signal name="activate" handler="on_add" last_modification_time="Sat, 19 Jun 2004 17:33:18 GMT"/>
+		    </widget>
+		  </child>
+		</widget>
+		<packing>
+		  <property name="padding">0</property>
+		  <property name="expand">True</property>
+		  <property name="fill">True</property>
+		</packing>
+	      </child>
+	    </widget>
+	    <packing>
+	      <property name="left_attach">1</property>
+	      <property name="right_attach">2</property>
+	      <property name="top_attach">1</property>
+	      <property name="bottom_attach">2</property>
+	      <property name="x_options">fill</property>
+	      <property name="y_options">fill</property>
+	    </packing>
+	  </child>
 	</widget>
 	<packing>
 	  <property name="padding">0</property>
Only in alexandria-0.4.0-toph2/lib/alexandria: scanners.rb
diff -rup alexandria-0.4.0/lib/alexandria/ui/new_book_dialog.rb alexandria-0.4.0-toph2/lib/alexandria/ui/new_book_dialog.rb
--- alexandria-0.4.0/lib/alexandria/ui/new_book_dialog.rb	2004-11-05 13:01:48.000000000 -0800
+++ alexandria-0.4.0-toph2/lib/alexandria/ui/new_book_dialog.rb	2005-02-06 20:56:39.000000000 -0800
@@ -59,6 +59,10 @@ module UI
             @treeview_results.append_column(col)
             @entry_isbn.grab_focus
             @combo_search.active = 0
+            Alexandria::Scanners.keys.sort.each { |key|
+                @combo_scanner.append_text(key)
+            }
+            @combo_scanner.active = 0
             
             if File.exist?(Preferences.instance.cuecat_device)
               @cuecat_image.pixbuf = Icons::CUECAT
@@ -70,20 +74,35 @@ module UI
    
         def on_criterion_toggled(item)
             return unless item.active?
-            if is_isbn = item == @isbn_radiobutton
+
+            is_isbn    = item == @isbn_radiobutton
+            is_scanner = item == @scanner_radiobutton
+            is_search  = item == @title_radiobutton
+
+            changed = nil
+            if is_isbn
                 @latest_size = @new_book_dialog.size
-                @new_book_dialog.resizable = false 
-            else
+            elsif is_scanner
+                changed = @entry_scanner
+            elsif is_search
                 @new_book_dialog.resizable = true 
                 @new_book_dialog.resize(*@latest_size) unless @latest_size.nil?
+                changed = @entry_search
             end
-            @entry_isbn.sensitive = is_isbn 
-            @combo_search.sensitive = !is_isbn 
-            @entry_search.sensitive = !is_isbn 
-            @button_find.sensitive = !is_isbn
-            @scrolledwindow.visible = !is_isbn
-            on_changed(is_isbn ? @entry_isbn : @entry_search)
-            unless is_isbn
+
+            unless is_search
+                @new_book_dialog.resizable = false 
+                changed = @entry_isbn
+            end
+
+            @entry_isbn.sensitive    = is_isbn 
+            @entry_scanner.sensitive = is_scanner
+            @entry_search.sensitive  = is_search
+            @button_find.sensitive   = is_search
+            @scrolledwindow.visible  = is_search
+
+            on_changed(changed)
+            if is_search
                 @button_add.sensitive = 
                     @treeview_results.selection.count_selected_rows > 0 
             end
@@ -91,7 +110,12 @@ module UI
 
         def on_changed(entry)
             ok = !entry.text.strip.empty?
-            (entry == @entry_isbn ? @button_add : @button_find).sensitive = ok
+
+            if entry == @entry_isbn || entry == @entry_scanner
+                @button_add.sensitive = ok
+            else
+                @button_find.sensitive = ok
+            end
         end
 
         def on_find
@@ -146,16 +170,26 @@ module UI
                 books_to_add = []                
 
                 if @isbn_radiobutton.active?
+                    isbn = @entry_isbn.text
+                elsif @scanner_radiobutton.active?
+                    # FIXME
+                    names = Alexandria::Scanners.keys.sort
+                    name  = names[@combo_scanner.active]
+                    func  = Alexandria::Scanners[name]
+                    isbn  = func.call(@entry_scanner.text)
+                end
+
+                if isbn
                     # Perform the ISBN search via the providers.
-                    isbn = begin
-                        Library.canonicalise_isbn(@entry_isbn.text)
+                    cannonized_isbn = begin
+                        Library.canonicalise_isbn(isbn)
                     rescue
                         raise _("Couldn't validate the EAN/ISBN you " +
                                 "provided.  Make sure it is written " +
                                 "correcty, and try again.")
                     end
-                    assert_not_exist(library, @entry_isbn.text)
-                    books_to_add << Alexandria::BookProviders.isbn_search(isbn)
+                    assert_not_exist(library, isbn)
+                    books_to_add << Alexandria::BookProviders.isbn_search(cannonized_isbn)
                 else
                     @treeview_results.selection.selected_each do |model, path, 
                                                                   iter| 
@@ -202,23 +236,47 @@ module UI
             if event.event_type == Gdk::Event::BUTTON_PRESS and
                event.button == 1
             
-                radio, target_widget, box2, box3 = case widget
+                radio, target_widget, others = case widget
                     when @eventbox_entry_search
                         [@title_radiobutton, @entry_search, 
-                         @eventbox_combo_search, @eventbox_entry_isbn]
+                         [@eventbox_combo_search,
+                          @eventbox_entry_scanner,
+                          @eventbox_combo_scanner,
+                          @eventbox_entry_isbn]]
 
                     when @eventbox_combo_search 
                         [@title_radiobutton, @combo_search, 
-                         @eventbox_entry_search, @eventbox_entry_isbn]
+                         [@eventbox_entry_search,
+                          @eventbox_entry_scanner,
+                          @eventbox_combo_scanner,
+                          @eventbox_entry_isbn]]
+
+                    when @eventbox_entry_scanner
+                        [@scanner_radiobutton, @entry_scanner,
+                         [@eventbox_entry_search,
+                          @eventbox_combo_search,
+                          @eventbox_combo_scanner,
+                          @eventbox_entry_isbn]]
+
+                    when @eventbox_combo_scanner
+                        [@scanner_radiobutton, @combo_scanner,
+                         [@eventbox_entry_search,
+                          @eventbox_combo_search,
+                          @eventbox_entry_scanner,
+                          @eventbox_entry_isbn]]
 
                     when @eventbox_entry_isbn 
-                        [@isbn_radiobutton, @entry_isbn, 
-                         @eventbox_entry_search, @eventbox_combo_search]
+                        [@isbn_radiobutton, @entry_isbn,
+                         [@eventbox_entry_search,
+                          @eventbox_combo_search,
+                          @eventbox_entry_scanner,
+                          @eventbox_combo_scanner]]
                 end
+
                 radio.active = true
                 target_widget.grab_focus 
                 widget.above_child = false
-                box2.above_child = box3.above_child = true
+                others.each {|other| other.above_child = true}
             end
         end
  
diff -rup alexandria-0.4.0/lib/alexandria.rb alexandria-0.4.0-toph2/lib/alexandria.rb
--- alexandria-0.4.0/lib/alexandria.rb	2004-11-05 13:01:48.000000000 -0800
+++ alexandria-0.4.0-toph2/lib/alexandria.rb	2005-02-06 20:55:49.000000000 -0800
@@ -55,3 +55,4 @@ require 'alexandria/library'
 require 'alexandria/book_providers'
 require 'alexandria/preferences'
 require 'alexandria/ui'
+require 'alexandria/scanners'
-------------- next part --------------
module Alexandria

  class CueCat
    def translate_cuecat(data)
      data.chomp!
      fields = data.split('.')
      fields.shift # First part is gibberish
      fields.shift # Second part is serial number
      type, code = fields.map {|field| decode_field(field) }

      if type == 'IB5':
          type = 'IBN'
        code = code[0, 13]
      end

      return code if type == 'IBN'
      
      raise "Don't know how to handle type #{type} (barcode: #{code})"
    end

    def decode_field (encoded)
      seq = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-';
      
      chars   = encoded.split(//)
      values  = chars.map {|c| seq.index(c) }

      padding = pad(values)
      result  = calc(values)
      result  = result[0, result.length - padding]
      return result
    end
    
    def calc (values)
      result = ''
      while values.length > 0
        num = ((values[0] << 6 | values[1]) << 6 | values[2]) << 6 | values[3]
        result += ((num >> 16) ^ 67).chr
        result += ((num >> 8 & 255) ^ 67).chr
        result += ((num & 255) ^ 67).chr
        
        values = values[4, values.length]
      end
      return result
    end
    
    def pad (array)
      length = array.length % 4
      
      if length != 0
        raise "Error parsing CueCat input" if length == 1
        
        length = 4 - length
        length.times { array.push(0) }
      end
      
      return length
    end
  end
  
  Scanners = {'CueCat' => CueCat.new.method(:translate_cuecat)}
end


More information about the Alexandria-list mailing list