Index: millennium.admin.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/Attic/millennium.admin.inc,v
retrieving revision 1.1.2.4
diff -u -r1.1.2.4 millennium.admin.inc
--- millennium.admin.inc	21 Oct 2009 21:47:18 -0000	1.1.2.4
+++ millennium.admin.inc	23 Oct 2009 19:32:46 -0000
@@ -13,7 +13,7 @@
   $chart_prefix = "http://chart.apis.google.com/chart?cht=ls&chm=o,FF9900,0,-1,5.0&chs=200x100&chdlp=b&chma=10,10,10,10&chd=t:";
   #&chf=bg,s,EFEFEF
 
-  $s[t("Total items imported")] = db_result(db_query("SELECT count(id) FROM {millennium_node_opacitem}"));
+  $s[t("Total items imported")] = db_result(db_query("SELECT count(id) FROM {millennium_node_bib}"));
 
   // Autocrawl
   $s[t("Crawl activated?")] = ( variable_get('millennium_crawl_flag', 0)? t('yes') : t('no'));
@@ -37,7 +37,7 @@
   $s[t('Queue: items pending')] = db_result(db_query("SELECT count(id) FROM {millennium_import_queue}"));
 
   // Import activity per day
-  $result = db_query_range("SELECT count(*) as n, date(created) as day FROM {millennium_node_opacitem} GROUP BY day ORDER BY day DESC", 0, 10);
+  $result = db_query_range("SELECT count(*) as n, date(created) as day FROM {millennium_node_bib} GROUP BY day ORDER BY day DESC", 0, 10);
   $import_history = "<table><tr><th>Date<th>Nodes created\n";
   $chd = array();
   $chd_min = 9999;
@@ -182,7 +182,13 @@
     '#description' => t('Enabling this will start (or continue) the automatic record import/update. Disabling this does not stop importing items queued manually for import.'),
     '#default_value' => variable_get('millennium_crawl_flag', 0),
   );
-
+  $form['crawl']['millennium_crawl_type'] = array(
+    '#type' => 'radios',
+    '#default_value' => variable_get('millennium_crawl_type', 'b'),
+    '#options' => array('i' => t('Item records'), 'b' => t('Bibliographic records')),
+    '#title' => t('Record types to crawl'),
+    '#description' => t('The recommended setting is "Bibliographic". Crawling item records allows gathering number of checkouts and other information but generally takes longer.'),
+  );
   $form['crawl']['millennium_webopac_start_itemrecord'] = array(
     '#type' => 'textfield',
     '#title' => t('Starting item record number for crawl'),
@@ -306,11 +312,11 @@
  * Callback for drupal_get_form that shows admin options for manually queueing items.
  */
 function millennium_admin_queue() {
-  $tot_items = db_result(db_query("SELECT count(item_recnum) FROM {millennium_node_opacitem}"));
+  $tot_items = db_result(db_query("SELECT count(*) FROM {millennium_node_bib} WHERE base_url = '%s'", millennium_get_real_baseurl() ));
   $form['source'] = array(
     '#type' => 'radios',
-    '#title' => t("Items to import"),
-    '#options' => array('list' => t('Item number listing'), 'range' => t('Item number range'), 'existing' => t('All already-imported records (@num items)', array("@num" => $tot_items))),
+    '#title' => t("Records to import"),
+    '#options' => array('list' => t('Record number listing'), 'range' => t('Record number range'), 'existing' => t('All already-imported records (@num items)', array("@num" => $tot_items))),
     '#default_value' => variable_get('millennium_import_form_source', 'list'),
     '#prefix' => '
 <script>
@@ -341,13 +347,13 @@
     );
   $form['list'] = array(
     '#type' => 'fieldset',
-    '#title' => t('Item number listing'),
+    '#title' => t('Record number listing'),
     '#attributes' =>  array('id' => 'list-fieldset'),
     );
   $form['list']['queue'] = array(
     '#type' => 'textarea',
-    '#title' => t('Item record numbers to queue for import (one per line)'),
-    '#description' => t('Record numbers are written like this: i123456, i123456x. You can get item numbers from Millennium Client\'s "Create Lists". NOTE: Bibliographic record numbers are NOT allowed.'),
+    '#title' => t('Record numbers to queue for import (one per line)'),
+    '#description' => t('Record numbers are written like this: b123456, i123456x. You can get bibliographic or item record numbers from Millennium Client\'s "Create Lists".'),
     '#default_value' => '',
   );
   $form['range'] = array(
@@ -355,29 +361,35 @@
     '#title' => t('Record number range'),
     '#attributes' =>  array('id' => 'range-fieldset'),
     );
+  $form['range']["type"] = array(
+    '#type' => 'radios',
+    '#default_value' => variable_get('millennium_range_form_type', 'b'),
+    '#options' => array('i' => t('Item records'), 'b' => t('Bibliographic records')),
+    '#title' => t('Record type'),
+  );
   $form['range']["start"] = array(
     '#type' => 'textfield',
     '#default_value' => variable_get('millennium_range_form_start', '100000'),
     '#size' => 7,
-    '#title' => t('Starting item number'),
+    '#title' => t('Starting number'),
   );
   $form['range']["end"] = array(
     '#type' => 'textfield',
-    '#default_value' => variable_get('millennium_range_form_end', '100001'),
+    '#default_value' => variable_get('millennium_range_form_end', '100010'),
     '#size' => 7,
-    '#title' => t('Ending item number'),
+    '#title' => t('Ending number'),
   );
   $form['options'] = array(
     '#type' => 'fieldset',
     '#title' => t('Advanced options'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
+    #'#collapsible' => TRUE,
+    #'#collapsed' => TRUE,
   );
   $form['options']['force_update'] = array(
     '#type' => 'radios',
     '#title' => t('Action for existing nodes'),
     '#options' => array(0 => t("Skip, leave untouched"), 1 => t("Update with current bibliographic data")),
-    '#default_value' => 1,
+    '#default_value' => variable_get('millennium_import_form_force_update', 1),
   );
     $form['options']['priority'] = array(
     '#type' => 'radios',
@@ -410,29 +422,27 @@
         form_set_error($element, t("Must be equal or higher than 10000"));
       }
     }
-    if ($form_state["values"]["end"] <= $form_state["values"]["start"]) {
-      form_set_error("end", t("Ending number must be higher than starting number."));
+    if ($form_state["values"]["end"] < $form_state["values"]["start"]) {
+      form_set_error("end", t("Ending number must be higher or equal than starting number."));
     }
   }
 
   if ($form_state["values"]["source"] == "list") {
     if (trim($form_state['values']['queue']) == "") {
-      form_set_error("queue", t("You must enter at least one item number in the list."));
+      form_set_error("queue", t("You must enter at least one record number (e.g.: b123456x, i123456x) in the list."));
       return;
     }
     $lines = explode("\n", $form_state['values']['queue']);
     $errors = array();
     foreach ($lines as $line) {
       $recnum = trim($line);
-      if ($recnum == "") {
-        continue;
-      }
-      if (!preg_match('/^i[0-9]+x*$/i', $recnum)) {
-        $errors[] = $line;
+      if ($recnum) {
+        $recnum = drupal_substr($recnum, 0, drupal_strlen($recnum)-1);
+        $item_recnums[$recnum] = $recnum;
       }
     }
     if ($errors) {
-      form_set_error("queue", t("These are not Millennium item record numbers:") . theme('item_list', $errors), "error");
+      form_set_error("queue", t("These are not Millennium record numbers:") . theme('item_list', $errors), "error");
     }
   }
 }
@@ -448,7 +458,9 @@
     
     // Store some values for later
     variable_set('millennium_import_form_source', $form_state["values"]["source"]);
+    variable_set('millennium_import_form_force_update', $form_state["values"]["force_update"]);
     if ($form_state["values"]["source"] == "range") {
+      variable_set('millennium_range_form_type', $form_state["values"]["type"]);
       variable_set('millennium_range_form_start', $form_state["values"]["start"]);
       variable_set('millennium_range_form_end', $form_state["values"]["end"]);
     }
@@ -462,12 +474,15 @@
         $queued = 0;
         foreach ($lines as $line) {
           $recnum = trim($line);
-          if (!preg_match('/^i[0-9]+x*$/i', $recnum)) {
-            drupal_set_message("Ignoring '$recnum': not a Millennium item record number", "error");
+          if ($recnum == "") {
+            continue;
+          }
+          if (!preg_match('/^[bi][0-9]{4-8}+x*$/i', $recnum)) {
+            drupal_set_message("Ignoring '$recnum': not a Millennium record number", "error");
           }
           else {
             $recnum = drupal_substr($recnum, 0, drupal_strlen($recnum)-1);
-            $ok = db_query("INSERT INTO {millennium_import_queue} (item_recnum, priority, force_update) VALUES ('%s', %d, %d)",
+            $ok = db_query("INSERT INTO {millennium_import_queue} (recnum, priority, force_update) VALUES ('%s', %d, %d)",
               $recnum,
               $form_state['values']['priority'],
               $form_state['values']['force_update']);
@@ -479,11 +494,12 @@
 
       if ($form_state["values"]["source"] == "range") {
         // Import from range
+        $type = $form_state["values"]["type"];
         $start = $form_state["values"]["start"];
         $end = $form_state["values"]["end"];
-        for ($num = $start; $num < $end; $num++) {
-          $ok = db_query("INSERT INTO {millennium_import_queue} (item_recnum, priority, force_update) VALUES ('%s', %d, %d)",
-            "i" . $num,
+        for ($num = $start; $num <= $end; $num++) {
+          $ok = db_query("INSERT INTO {millennium_import_queue} (recnum, priority, force_update) VALUES ('%s', %d, %d)",
+            $type . $num,
             $form_state['values']['priority'],
             $form_state['values']['force_update']);
           if ($ok) {
@@ -493,9 +509,9 @@
       }
       
       if ($form_state["values"]["source"] == "existing") {
-        $result = db_query("SELECT item_recnum FROM {millennium_node_opacitem}");
+        $result = db_query("SELECT bib_recnum FROM {millennium_node_bib} WHERE base_url = '%s'", millennium_get_real_baseurl());
         while ($data = db_fetch_object($result)) {
-          $ok = db_query("INSERT INTO {millennium_import_queue} (item_recnum, priority, force_update) VALUES ('%s', %d, %d)",
+          $ok = db_query("INSERT INTO {millennium_import_queue} (recnum, priority, force_update) VALUES ('%s', %d, %d)",
             $data->item_recnum,
             $form_state['values']['priority'],
             $form_state['values']['force_update']
@@ -518,10 +534,7 @@
         $queued = 0;
         foreach ($lines as $line) {
           $recnum = trim($line);
-          if (!preg_match('/^i[0-9]+x*$/i', $recnum)) {
-            drupal_set_message("Ignoring '$recnum': not a Millennium item record number", "error");
-          }
-          else {
+          if ($recnum) {
             $recnum = drupal_substr($recnum, 0, drupal_strlen($recnum)-1);
             $item_recnums[$recnum] = $recnum;
           }
@@ -530,17 +543,18 @@
 
       if ($form_state["values"]["source"] == "range") {
         // Import from range
+        $type = $form_state["values"]["type"];
         $start = $form_state["values"]["start"];
         $end = $form_state["values"]["end"];
-        for ($num = $start; $num < $end; $num++) {
-          $item_recnums["i{$num}"] = "i{$num}";
+        for ($num = $start; $num <= $end; $num++) {
+          $item_recnums["{$type}{$num}"] = "{$type}{$num}";
         }
       }
       
       if ($form_state["values"]["source"] == "existing") {
-        $result = db_query("SELECT item_recnum FROM {millennium_node_opacitem}");
+        $result = db_query("SELECT bib_recnum FROM {millennium_node_bib} WHERE base_url = '%s'", millennium_get_real_baseurl());
         while ($data = db_fetch_object($result)) {
-          $item_recnums[$data->item_recnum] = $data->item_recnum;
+          $item_recnums[$data->bib_recnum] = $data->bib_recnum;
         }
       }
 
@@ -601,12 +615,7 @@
   $context['sandbox']['tot_notfound'] += sizeof($fetched['not_found']);
   // Import successful fetches only
   foreach ($fetched['found'] as $data) {
-    $result = millennium_import_update_item(
-      $data['item_recnum'],
-      $force_update,
-      $data['marc'],
-      $data['bib_recnum']
-    );
+    $result = millennium_process_record($data, $force_update);
 
     if ($result["success"] !== false) {
       $context['sandbox']['tot_imported']++;
Index: millennium.cron.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/Attic/millennium.cron.inc,v
retrieving revision 1.1.2.4
diff -u -r1.1.2.4 millennium.cron.inc
--- millennium.cron.inc	21 Oct 2009 21:47:18 -0000	1.1.2.4
+++ millennium.cron.inc	23 Oct 2009 19:15:01 -0000
@@ -51,36 +51,31 @@
   );
   foreach ($queue_jobs as $priority => $limit) {
     // Get items to import from DB
-    $handle = db_query("SELECT id, item_recnum, force_update FROM {millennium_import_queue} WHERE priority=%d LIMIT {$limit}", $priority);
+    $handle = db_query("SELECT id, recnum, force_update FROM {millennium_import_queue} WHERE priority=%d LIMIT {$limit}", $priority);
     $stop = $tot_imported + $limit;
-    $item_recnums = array();
+    $recnums = array();
     while ($data = db_fetch_object($handle)) {
-      $item_recnums[] = $data->item_recnum;
-      $force_update[$data->item_recnum] = $data->force_update;
-      $queue_id[$data->item_recnum] = $data->id;
+      $recnums[] = $data->recnum;
+      $force_update[$data->recnum] = $data->force_update;
+      $queue_id[$data->recnum] = $data->id;
     }
-    $tot_attempted += sizeof($item_recnums);
+    $tot_attempted += sizeof($recnums);
     // Chunk array into groups of 50 (maximum number allowed by bookcart)
-    $item_recnums_chunks = array_chunk($item_recnums, 50);
-    foreach ($item_recnums_chunks as $item_recnums_chunk) {
-      $fetched = millennium_fetch_records_via_bookcart($item_recnums_chunk);
+    $recnums_chunks = array_chunk($recnums, 50);
+    foreach ($recnums_chunks as $recnums_chunk) {
+      $fetched = millennium_fetch_records_via_bookcart($recnums_chunk);
 
       // Remove not-found items from queue
-      foreach ($fetched['not_found'] as $item_recnum) {
-        db_query("DELETE FROM {millennium_import_queue} where id=%d", $queue_id[$item_recnum]);
+      foreach ($fetched['not_found'] as $recnum) {
+        db_query("DELETE FROM {millennium_import_queue} where id=%d", $queue_id[$recnum]);
         $tot_notfound++;
-        $import_errors[] = array("recnum" => $item_recnum, "error" => 'Not found');
+        $import_errors[] = array("recnum" => $recnum, "error" => 'Not found');
       }
 
       // Import successful fetches only
       foreach ($fetched['found'] as $data) {
-        db_query("DELETE FROM {millennium_import_queue} where id=%d", $queue_id[$data['item_recnum']]);
-        $result = millennium_import_update_item(
-          $data['item_recnum'],
-          $force_update[$data['item_recnum']],
-          $data['marc'],
-          $data['bib_recnum']
-        );
+        db_query("DELETE FROM {millennium_import_queue} where id=%d", $queue_id[$data['recnum']]);
+        $result = millennium_process_record($data, $force_update[$data['recnum']]);
 
         if ($result["success"] !== false) {
           $tot_imported++;
@@ -88,10 +83,10 @@
           $tot_fail++;
           watchdog("Millennium",
             "Queue #@id: Failed to import node: item #@recnum, error: @error",
-            array("@id" => $ids[$data['item_recnum']], "@recnum" => $data['item_recnum'], "@error" => $result["error"])
+            array("@id" => $ids[$data['recnum']], "@recnum" => $data['recnum'], "@error" => $result["error"])
           );
           // Build a list of errors
-          $import_errors[] = array("recnum" => $data['item_recnum'], "error" => $result["error"]);
+          $import_errors[] = array("recnum" => $data['recnum'], "error" => $result["error"]);
         }
 
         if ($tot_imported > $stop || $tot_imported > $max_to_import) {
@@ -99,7 +94,7 @@
         }
 
       }
-    } // foreach ($item_recnums_chunks as $item_recnums_chunk)
+    } // foreach ($recnums_chunks as $recnums_chunk)
   }
 
   // Send errors via email
@@ -124,31 +119,27 @@
   if ($will_auto_crawl) {
 
     // AUTO CRAWL ================================================================
+    $record_type = variable_get('millennium_crawl_type', "b");
     $rollover_limit = intval($default_end + ($default_end - $default_start)*.05);
-    $item_recnums = array();
+    $recnums = array();
     for ($num = $beginning_rec_num; $num < $beginning_rec_num + $max_to_import; $num++) {
-      $item_recnums["i{$num}"] = "i{$num}";
+      $recnums["{$record_type}{$num}"] = "{$record_type}{$num}";
     }
-    $tot_attempted += sizeof($item_recnums);
+    $tot_attempted += sizeof($recnums);
 
     // Chunk array into groups of 50 (maximum number allowed by bookcart)
-    $item_recnums_chunks = array_chunk($item_recnums, 50);
-    foreach ($item_recnums_chunks as $item_recnums_chunk) {
-      $fetched = millennium_fetch_records_via_bookcart($item_recnums_chunk);
+    $recnums_chunks = array_chunk($recnums, 50);
+    foreach ($recnums_chunks as $recnums_chunk) {
+      $fetched = millennium_fetch_records_via_bookcart($recnums_chunk);
       #dpm($fetched);
       foreach ($fetched['found'] as $data) {
-        $result = millennium_import_update_item(
-          $data['item_recnum'],
-          $force_update[$data['item_recnum']],
-          $data['marc'],
-          $data['bib_recnum']
-        );
+        $result = millennium_process_record($data);
         if ($result["success"] !== false) {
           $tot_imported++;
         } else {
           $tot_fail++;
           watchdog("Millennium",
-            "Crawl: Failed to import node: item #@recnum, error: @error",
+            "Crawl: Failed to import node: record #@recnum, error: @error",
             array("@recnum" => $data['item_recnum'], "@error" => $result["error"])
           );
         }
@@ -180,7 +171,7 @@
       $tot_notfound += sizeof($fetched['not_found']);
 
       // Store last tried item
-      $last_tried_itemnum = substr(array_pop($item_recnums_chunk), 1) + 0;
+      $last_tried_itemnum = substr(array_pop($recnums_chunk), 1) + 0;
       variable_set('millennium_webopac_current_itemrecord', $last_tried_itemnum);
     }
 
Index: millennium.import.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/Attic/millennium.import.inc,v
retrieving revision 1.1.2.5
diff -u -r1.1.2.5 millennium.import.inc
--- millennium.import.inc	21 Oct 2009 21:47:18 -0000	1.1.2.5
+++ millennium.import.inc	23 Oct 2009 19:20:56 -0000
@@ -3,9 +3,9 @@
 
 /**
  * Gets a sequential number of records obeying PHP's max_execution_time setting.
- * @param array $item_recnums Array of item numbers to fetch.
+ * @param array $recnums Array of item or bib numbers to fetch.
  */
-function millennium_mass_fetch($item_recnums) {
+function millennium_mass_fetch($recnums) {
   $max_records_for_bookcart = 50; // This seems to be the limit for WebOPACs, do not change =)
 
   if (ini_get('max_execution_time')<20) {
@@ -16,7 +16,7 @@
   $max_time = ini_get('max_execution_time') * 0.75;
 
   // Chunk array into groups of $max_records_for_bookcart
-  $chunks = array_chunk($item_recnums, $max_records_for_bookcart);
+  $chunks = array_chunk($recnums, $max_records_for_bookcart);
   $results = array('found' => array(), 'not_found' => array());
   foreach ($chunks as $chunk) {
     $result = millennium_fetch_records_via_bookcart($chunk);
@@ -35,10 +35,10 @@
 /**
  * Gets item information (bib number & MARC record) using the III's book cart.
  * Recieves an array of item numbers and returns an array of found item data including bib number and MARC keyed by item number, and an unkeyed array of not found items.
- * @param array $item_recnums An unkeyed array of item numbers = array('i100000', 'i100002', ...)
+ * @param array $recnums An unkeyed array of item or bib numbers = array('i123456', 'b123456', ...)
  * @param bool $complete_holdings A flag that specifies if an extra request should be done if the holdings table is incomplete.
  */
-function millennium_fetch_records_via_bookcart($item_recnums, $complete_holdings = true) {
+function millennium_fetch_records_via_bookcart($recnums, $complete_holdings = true) {
   static $headers = array();
 
   // Start timer to measure average performance
@@ -70,9 +70,9 @@
   }
 
   // Issue N requests to add items to the bookcart, 25 records at a time
-  $chunks = array_chunk($item_recnums, 25);
+  $chunks = array_chunk($recnums, 25);
   foreach ($chunks as $chunk) {
-    // Add items from $item_recnums array to the cart
+    // Add items from $recnums array to the cart
     $path = '/search?/X&searchscope=0&SORT=D/X&searchscope=0&SORT=D&SUBKEY=/1%2C7175%2C7175%2CE/2browse';
     $post = "jumpref=X&save=" . implode("&save=", $chunk) . "&save_func=save_marked";
     $result = drupal_http_request("{$baseurl}{$path}", $headers, 'POST', $post);
@@ -85,7 +85,7 @@
   $path = "/search/?/++export/1,-1,-1,B/export";
   $result = drupal_http_request("{$baseurl}{$path}", $headers, 'GET');
   $ok = preg_match_all(
-    '/name="save" value="(b[0-9]+)".*?browseEntryData.*?record=(i[0-9]+)/si',
+    '/name="save" value="(b[0-9]+)".*?browseEntryData.*?record=([bi][0-9]+)/si',
     $result->data,
     $matches,
     PREG_SET_ORDER
@@ -94,22 +94,25 @@
   #return;
 
   // Start off assuming no items have been found
-  foreach ($item_recnums as $num) {
+  foreach ($recnums as $num) {
     $not_found_items[$num] = $num;
   }
-  // Store each found item's item <=> bib relationship
+
   $found_items = array();
   foreach ($matches as $match) {
     $found_bib_recnum = $match[1];
-    $found_item_recnum = $match[2];
+    $found_recnum = $match[2];
     // Remove found items off $not_found_items list
-    unset($not_found_items[$found_item_recnum]);
-    $found_items[$found_item_recnum] = array(
-      'item_recnum' => $found_item_recnum,
+    unset($not_found_items[$found_recnum]);
+    $found_items[$found_recnum] = array(
       'bib_recnum' => $found_bib_recnum
     );
+    // If supplied recnum is an item record, store item <=> bib relationship
+    if (substr($found_recnum, 0, 1) == "i") {
+      $found_items[$found_recnum]['item_recnum'] = $found_recnum;
+    }
   }
-  #dpm($item_to_bib);
+  #dpm($found_items);
 
   // Get MARC for all!
   $path = "/search/?/++export/1%2C-1%2C-1%2CB/export/";
@@ -126,15 +129,15 @@
   #dpm($matches);
   // Assign marc to item numbers
   $index = 0;
-  foreach ($found_items as $item => $dummy) {
-    $found_items[$item]['marc'] = $matches[$index][1];
+  foreach ($found_items as $recnum => $dummy) {
+    $found_items[$recnum]['marc'] = $matches[$index][1];
 
     // Add holdings information; determine if an extra request is needed
-    $this_bib_recnum = $found_items[$item]['bib_recnum'];
+    $this_bib_recnum = $found_items[$recnum]['bib_recnum'];
     if ($matches[$index][3] != "" && $complete_holdings == true) {
-      $found_items[$item]['holdings'] = millennium_get_holdings_info($this_bib_recnum);
+      $found_items[$recnum]['holdings'] = millennium_get_holdings_info($this_bib_recnum);
     } else {
-      $found_items[$item]['holdings'] = millennium_get_holdings_info($this_bib_recnum, $matches[$index][2]);
+      $found_items[$recnum]['holdings'] = millennium_get_holdings_info($this_bib_recnum, $matches[$index][2]);
     }
     $index++;
   }
@@ -154,7 +157,7 @@
 
   // Read timer.
   $elapsed = round(timer_read("millennium_fetch_records_via_bookcart") / 1000, 3);
-  #drupal_set_message("Tried to fetch:" . sizeof($item_recnums) . ". Found: " . sizeof($found_items) . ". Elapsed time: {$elapsed}s");
+  #drupal_set_message("Tried to fetch:" . sizeof($recnums) . ". Found: " . sizeof($found_items) . ". Elapsed time: {$elapsed}s");
 
   // Return results
   $results = array('found' => $found_items, 'not_found' => $not_found_items);
@@ -164,13 +167,24 @@
 
   /* $results = array(
       'found' => array(
-        'i100001' => array(
-          'item_recnum' => 'i100001',
+        'b100001' => array(
+          'bib_recnum' => 'b100001',
+          'marc' => 'LEADER 00000cam 2200000 a 4500 001 tec042...
+          'holdings' => '...'
+        ),
+        'i222222' => array(
+          'item_recnum' => 'i222222',
           'bib_recnum' => 'b426763',
           'marc' => 'LEADER 00000cam 2200000 a 4500 001 tec042...
+          'holdings' => array(
+            0 => array(
+              'location' => 'MTY 4to. Piso',
+              'classnumber' => 'QH541.15.B56 E95 2009',
+              'classvolume' => '',
+              'status' => 'CATALOGACION',
+            ),
+          ),
         ),
-        'i100003' => array(...)
-        [...]
       ),
       'not_found' => array('i100000', 'i100005', [...])
     );
Index: millennium.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/millennium.install,v
retrieving revision 1.1.2.5.2.1.2.2
diff -u -r1.1.2.5.2.1.2.2 millennium.install
--- millennium.install	28 Nov 2008 23:10:02 -0000	1.1.2.5.2.1.2.2
+++ millennium.install	22 Oct 2009 22:51:45 -0000
@@ -14,7 +14,7 @@
         'unsigned' => TRUE,
         'not null' => TRUE,
       ),
-      'item_recnum' => array(
+      'recnum' => array(
         // 'description' => t('TODO'),
         'type' => 'char',
         'length' => 15,
@@ -41,7 +41,7 @@
     'primary key' => array('id'),
   );
 
-  $schema['millennium_node_opacitem'] = array(
+  $schema['millennium_node_bib'] = array(
     // 'description' => t('TODO'),
     'fields' => array(
       'id' => array(
@@ -56,36 +56,79 @@
         'unsigned' => TRUE,
         'not null' => TRUE,
       ),
-      'item_recnum' => array(
+      'bib_recnum' => array(
         // 'description' => t('TODO'),
         'type' => 'char',
         'length' => 15,
         'not null' => TRUE,
       ),
+      'updated' => array(
+        // 'description' => t('TODO'),
+        'type' => 'datetime',
+        'not null' => TRUE,
+      ),
+      'created' => array(
+        // 'description' => t('TODO'),
+        'type' => 'datetime',
+        'not null' => TRUE,
+      ),
+      'biblio_data' => array(
+        // 'description' => t('TODO'),
+        'type' => 'text',
+        'not null' => TRUE,
+      ),
+      'base_url' => array(
+        // 'description' => t('TODO'),
+        'type' => 'char',
+        'length' => 80,
+        'not null' => TRUE,
+      ),
+    ),
+    'indexes' => array(
+      'id_2' => array('bib_recnum', 'base_url'),
+      'nid' => array('nid'),
+    ),
+    'unique keys' => array(
+      'id' => array('id'),
+    ),
+    'primary key' => array('id'),
+  );
+  
+  $schema['millennium_bib_item'] = array(
+    // 'description' => t('TODO'),
+    'fields' => array(
+      'id' => array(
+        // 'description' => t('TODO'),
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'nid' => array(
+        // 'description' => t('TODO'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
       'bib_recnum' => array(
         // 'description' => t('TODO'),
         'type' => 'char',
         'length' => 15,
         'not null' => TRUE,
       ),
-      'item_updated' => array(
+      'item_recnum' => array(
         // 'description' => t('TODO'),
-        'type' => 'datetime',
+        'type' => 'char',
+        'length' => 15,
         'not null' => TRUE,
       ),
-      'created' => array(
+      'updated' => array(
         // 'description' => t('TODO'),
         'type' => 'datetime',
         'not null' => TRUE,
       ),
-      'bib_updated' => array(
+      'created' => array(
         // 'description' => t('TODO'),
         'type' => 'datetime',
-        'not null' => FALSE,
-      ),
-      'biblio_data' => array(
-        // 'description' => t('TODO'),
-        'type' => 'text',
         'not null' => TRUE,
       ),
       'base_url' => array(
@@ -96,9 +139,9 @@
       ),
     ),
     'indexes' => array(
-      'id_2' => array('id', 'nid', 'item_recnum', 'item_updated'),
+      'id_3' => array('item_recnum', 'base_url'),
+      'id_2' => array('bib_recnum', 'base_url'),
       'nid' => array('nid'),
-      'bib_recnum' => array('bib_recnum'),
     ),
     'unique keys' => array(
       'id' => array('id'),
Index: millennium.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/millennium.module,v
retrieving revision 1.13.2.33.2.2.2.34
diff -u -r1.13.2.33.2.2.2.34 millennium.module
--- millennium.module	22 Oct 2009 19:48:20 -0000	1.13.2.33.2.2.2.34
+++ millennium.module	23 Oct 2009 20:17:55 -0000
@@ -114,16 +114,37 @@
     'type' => MENU_DEFAULT_LOCAL_TASK,
     'weight' => -10,
   );
-
-  $items[MILLENNIUM_SETTINGS_PATH .'/manual-import'] = array(
-    'title' => 'Manual import',
-    'description' => 'Add item record numbers to import during subsequent cron calls.',
+  $items['admin/content/millennium'] = array(
+    'title' => 'Millennium import',
+    'description' => 'Configure WebOPAC crawl and MARC import settings.',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('millennium_admin_queue'),
+    'access callback' => 'user_access',
     'access arguments' => array('administer millennium'),
-    'type' => MENU_LOCAL_TASK,
     'file' => 'millennium.admin.inc',
   );
+  $items['admin/content/millennium/batch-import'] = array(
+    'title' => 'Batch import',
+    'access arguments' => array('administer millennium'),
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items['millennium/import/%'] = array(
+    'title' => 'Millennium node import',
+    'page callback' => 'millennium_node_import' ,
+    'page arguments' => array(2),
+    'access callback' => 'user_access',
+    'access arguments' => array('administer millennium'),
+    'type' => MENU_CALLBACK,
+  );
+  $items['millennium/preview/%'] = array(
+    'title' => 'Millennium node import preview',
+    'page callback' => 'millennium_preview_import' ,
+    'page arguments' => array(2),
+    'access callback' => 'user_access',
+    'access arguments' => array('administer millennium'),
+    'type' => MENU_CALLBACK,
+  );
 
   $items['admin/reports/millennium'] = array(
     'title' => 'Millennium import statistics',
@@ -138,7 +159,6 @@
     'type' => MENU_CALLBACK,
     'access callback' => true
   );
-  // TODO this should only show up in certain nodes!! See http://drupal.org/node/218692
   $items['node/%node/millennium'] = array(
     'title' => 'Millennium Tools',
     'page callback' => 'millennium_node_tools' ,
@@ -148,7 +168,6 @@
     'weight' => 10,
     'type' => MENU_LOCAL_TASK,
   );
-  // TODO this should only show up in certain nodes!! See http://drupal.org/node/218692
   $items['node/%node/marc'] = array(
     'title' => 'MARC display',
     'page callback' => 'millennium_node_marc' ,
@@ -158,18 +177,10 @@
     'weight' => 0,
     'type' => MENU_LOCAL_TASK,
   );
-  $items['millennium/reimport/%'] = array(
+  $items['node/%node/millennium/reimport'] = array(
     'title' => 'Millennium node reimport',
     'page callback' => 'millennium_node_reimport' ,
-    'page arguments' => array(2),
-    'access callback' => 'user_access',
-    'access arguments' => array('administer millennium'),
-    'type' => MENU_CALLBACK,
-  );
-   $items['millennium/preview/%'] = array(
-    'title' => 'Millennium record import preview',
-    'page callback' => 'millennium_preview_import' ,
-    'page arguments' => array(2),
+    'page arguments' => array(1),
     'access callback' => 'user_access',
     'access arguments' => array('administer millennium'),
     'type' => MENU_CALLBACK,
@@ -180,8 +191,8 @@
 /**
  * Callback function from hook_menu to show how an millennium record would be imported
  */
-function millennium_preview_import($item_recnum) {
-  return _millennium_show_conversion($item_recnum);
+function millennium_preview_import($recnum) {
+  return _millennium_show_conversion($recnum);
 }
 
 /**
@@ -198,7 +209,7 @@
  * Callback function from hook_menu to determine if millennium tools tab should be shown
  */
 function millennium_tools_tab_access($node) {
-  if ($item_recnum = millennium_item_recnum_from_node($node->nid) && user_access("administer millennium")) {
+  if ($bib_recnum = millennium_bib_recnum_from_node($node->nid) && user_access("administer millennium")) {
     return true;
   }
   return false;
@@ -208,60 +219,58 @@
  * Callback function from hook_menu that shows import and conversion information for node $nid
  */
 function millennium_node_tools($node) {
-  $item_recnum = millennium_item_recnum_from_node($node->nid);
-  if (! $item_recnum) {
+  $bib_recnum = millennium_bib_recnum_from_node($node->nid);
+  if (! $bib_recnum) {
     drupal_set_message(t('This node (!nid) does not have any Millennium record number associated with it.', array('!nid' => $node->nid)));
     drupal_goto('node/'. $node->nid);
   }
 
   // Show record info
-  $result = db_query("SELECT * from {millennium_node_opacitem} WHERE nid=%d", $node->nid);
-  $data = db_fetch_object($result);
+  $data = db_fetch_object(db_query("SELECT * from {millennium_node_bib} WHERE nid = %d", $node->nid));
 
   $output = "<h2>". t("Import history") . "</h2>";
 
-  $bib_recnum = millennium_bib_recnum_from_node($node->nid);
   $items[] = t("WebOPAC: !link", array("!link" => l($data->base_url, $data->base_url)));
   $items[] = t("Bibliographic record #: !link", array("!link" => l($bib_recnum, millennium_permalink($bib_recnum))));
-  $items[] = t("Item record #: !link", array("!link" => l($item_recnum, millennium_permalink($item_recnum))));
+  $recnums = millennium_item_recnums_from_node($node->nid);
+  if ($recnums) {
+    $items[] = t("Item records: @items", array("@items" => implode(", ", $recnums)));
+  }
   $items[] = t("Node first import date: @date", array("@date" => $data->created));
-  $items[] = t("Node last update date: @date", array("@date" => $data->item_updated));
-  $items[] = l(t('Click here to reimport this node'), 'millennium/reimport/'. $node->nid);
+  $items[] = t("Node last update date: @date", array("@date" => $data->updated));
+  $items[] = l(t('Click here to reimport this node'), 'node/'. $node->nid . '/millennium/reimport');
   $output .= theme('item_list', $items);
 
   $output .= "<p></p>";
   $output .= "<h2>". t('MARC record and conversion') ."</h2>";
-  $output .= _millennium_show_conversion($item_recnum, $node->millennium_biblio_data["marc"]);
+  $output .= _millennium_show_conversion($bib_recnum, $node->millennium_biblio_data["marc"]);
   return $output;
 }
 
 /**
  * Diagnostics to show conversion of MARC into fields for node object
  */
-function _millennium_show_conversion($item_recnum, $marc = false) {
+function _millennium_show_conversion($recnum, $marc_text = false) {
   $output = "";
 
-  if ($marc === false) {
-    // Get bib number from item recnum
-    $result = millennium_fetch_recordpage($item_recnum);
-    $record_html = $result->data;
-    $ok = preg_match('/\/record=(b[0-9]+)/si', $record_html, $matches);
-    $bib_recnum = $matches[1];
-    $marc = millennium_fetch_marc($bib_recnum);
-    $output .= t("Item record = @item, Bib recnum = @bib", array("@item" => $item_recnum, "@bib" => $bib_recnum));
+  $marc_text_msg = "";
+  if ($marc_text === false) {
+    $marc_text = millennium_fetch_marc($recnum);
+    #$output .= t("Bib recnum = @bib", array("@bib" => $recnum));
+    $marc_text_msg = t('This MARC record was just fetched from the WebOPAC.');
   }
 
   $output .= "<h3>" .t('MARC record') ."</h3>";
-  $output .= t('This MARC record was just fetched from the WebOPAC.');
-  $output .= "<pre class='millennium marc'>$marc</pre>";
+  $output .= $marc_text_msg;
+  $output .= "<pre class='millennium marc'>$marc_text</pre>";
 
-  $tmp_node = millennium_marc_to_nodeobject($item_recnum, $marc);
+  $tmp_node = millennium_marc_to_nodeobject($marc_text, $recnum);
 
   $output .= "<h3>" .t('Conversion results') ."</h3>";
 
   $output .= "<h4>" . $GLOBALS["_millennium_field_labels"]["type"] ."</h4>";
   $output .= sprintf("%s (%d)",
-    $tmp_node->millennium_biblio_data["type"],
+    t($tmp_node->millennium_biblio_data["type"]),
     $tmp_node->millennium_biblio_data["type_num"]
     );
 
@@ -287,18 +296,19 @@
 }
 
 /**
- * Callback function from hook_menu that reimports a node using its stored item number in table millennium_node_opacitem
+ * Callback function from hook_menu that reimports a node using its previously stored bib data
  */
 function millennium_node_reimport($nid) {
-  $item_recnum = millennium_item_recnum_from_node($nid);
-  if (! $item_recnum) {
+  $bib_recnum = millennium_bib_recnum_from_node($nid);
+  if (! $bib_recnum) {
     drupal_set_message(t('This node does not have any Millennium record number associated with it.'));
     drupal_goto('node/'. $nid);
   }
-  $result = millennium_import_update_item($item_recnum, true); #true == force update
+
+  $result = millennium_process_bib_record($bib_recnum, true);
 
   if ($result["success"] !== false) {
-    drupal_set_message("Node reimported from Millennium item #$item_recnum.");
+    drupal_set_message("Node reimported from Millennium item $bib_recnum.");
   }
   else {
     drupal_set_message("Failed to reimport from Millennium: ". $result["error"]);
@@ -307,6 +317,33 @@
 }
 
 /**
+ * Callback function from hook_menu that imports a new node based on a given item or bib record number.
+ */
+function millennium_node_import($recnum) {
+  if (!preg_match('/^[bi][1-9][0-9][0-9][0-9][0-9]+$/i', $recnum)) {
+    drupal_set_message( t("@recnum is not a valid record number", array('@recnum' => $recnum)), "error");
+    drupal_goto("<front>");
+  }
+
+  switch (substr($recnum,0,1)) {
+    case "i":
+      $result = millennium_process_item_record($recnum, true);
+      break;
+    case "b":
+      $result = millennium_process_bib_record($recnum, true);
+  }
+  dpm($result);
+
+  if ($result["success"] == false) {
+    drupal_set_message("Failed to import from Millennium: ". $result["error"]);
+    drupal_goto("<front>");
+  } else {
+    drupal_set_message("Record $recnum successfully imported from Millennium.");
+    drupal_goto('node/' . $result['node']->nid);
+  }
+}
+
+/**
  * Helper function that resets values for restarting the automatic crawl process.
  */
 function _millennium_crawl_restart() {
@@ -325,55 +362,101 @@
   millennium_cron_do();
 }
 
+
+/**
+ * Main function that imports, updates or deletes item or bibliographic records.
+ * It can optionally take in a MARC record to bypass harvesting from the WebOPAC.
+ * @param $data
+ *   An associative array of data for a record, with the following keys:
+ *     - 'bib_recnum' (optional)
+ *       Millennium record number (b123456) for the record to process.
+ *     - 'item_recnum' (optional)
+ *       A Millennium record number (i123456) for the record to process.
+ *       If bib_recnum is also given, it is assumed it is the bib record
+ *       associated with this item.
+ *     - 'marc' (optional)
+ *       MARC text as it is exported from the WebOPAC.
+ * @param bool $force_update (Optional) States if the function should always update existing data.
+ */
+function millennium_process_record(&$data, $force_update = false) {
+  // Set defaults
+  if (!isset($data['marc'])) {
+    $data['marc'] = null;
+  }
+  if (!isset($data['item_recnum'])) {
+    $data['item_recnum'] = null;
+  }
+  if (!isset($data['bib_recnum'])) {
+    $data['bib_recnum'] = null;
+  }
+  #drupal_set_message("millennium_process_record():");
+  #dpm($data);
+
+  $type = "";
+  // If only bib_recnum
+  if ($data['bib_recnum'] && !$data['item_recnum']) {
+    $type = "bib";
+  }
+  // If only item_recnum set, or if both record numbers set:
+  if (
+    (!$data['bib_recnum'] && $data['item_recnum']) ||
+    ($data['bib_recnum'] && $data['item_recnum'])) {
+    $type = "item";
+  }
+
+  switch($type) {
+    case "bib":
+      $result = millennium_process_bib_record(
+        $data['bib_recnum'],
+        $force_update,
+        $data['marc']
+      );
+      break;
+    case "item":
+      $result = millennium_process_item_record(
+        $data['item_recnum'],
+        $force_update,
+        $data['marc'],
+        $data['bib_recnum']
+      );
+      break;
+    default:
+      return array("success" => false, "error" => "Could not determine function to call.");
+  }
+
+  if ($result["node"]) {
+    $data["node"] = $result["node"];
+  }
+  #dpm($result);
+  return array("success" => $result['result'], 'status' => $result['status'], 'error' => $result['error']);
+}
+
 /**
- * Main function that imports a Millennium record into Drupal
+ * Imports or updates Millennium item records in Drupal
  * @param string $item_recnum Millennium record number (i123456) to import
  * @param bool $force_update (Optional) States if an existing node for that item record number should be updated.
  * @param string $marc_text (Optional) A MARC record to import from.
  */
-function millennium_import_update_item($item_recnum, $force_update = true, $marc_text = null, $bib_recnum = null) {
-  // Assume item exists when marc_text is given.
-  $item_exists = true;
-  if ($marc_text == null) {
-    // Check if item exists on server
-    $result = millennium_fetch_recordpage($item_recnum, "xml");
-    if (! $result->data) {
-      return array("success" => false, "error" => "Could not fetch XML for $item_recnum");
-    }
-    $item_xml = $result->data;
-    if (stripos($item_xml, "Cannot find record=")) {
-      $item_exists = false;
-    }
-  }
-
-  if (! $item_exists) {
-    // Item does not exist in Millennium.
-    millennium_handle_deleted_item($item_recnum);
-    return array("success" => false, "error" => "Item #$item_recnum does not exist.");
-  }
+function millennium_process_item_record($item_recnum, $force_update = true, $marc_text = null, $bib_recnum = null) {
+  #drupal_set_message("millennium_process_item_record($item_recnum, $force_update, $marc_text, $bib_recnum)");
 
-  // Record exists.
   // Had we already imported this item record?
-  $import_history = db_fetch_object(db_query(
-    "SELECT * FROM {millennium_node_opacitem} WHERE item_recnum='%s' AND base_url = '%s'", 
-    $item_recnum, 
+  $item_import_history = db_fetch_object(db_query(
+    "SELECT * FROM {millennium_bib_item} WHERE item_recnum='%s' AND base_url = '%s'",
+    $item_recnum,
     millennium_get_real_baseurl()
     ));
-  if ($import_history->nid) {
+  if ($item_import_history->nid) {
+    #drupal_set_message("Item $item_recnum was imported before");
     // Item was imported before.
-    // Collect the bib_recnum for the item.
-    $bib_recnum = $import_history->bib_recnum;
-    
-    // Decide what to do depending on $force_update flag.
-    if ($force_update == false) {
-      // TODO: What do we update with any changes to the item record? 
-      // From xrecord: Use ICODEs? LOCATION? OUT DATE? YTDCIRC? TOTCIRC?
-      // From imported history: Expiry time? (millennium_node_opacitem.item_updated)
-      return array(
-        "success" => true,
-        "error" => t("This item #@item_number had already been imported under nid @nid", array("@item_number" => $item_recnum, "@nid" => $import_history->nid))
-      );
-    }
+    $status = millennium_process_item_record_prev_imported($item_recnum, $item_import_history, $force_update = true, $marc_text = null);
+    $node = node_load($item_import_history->nid);
+    return array(
+      "success" => true,
+      "status" => $status['result'],
+      "node" => $node,
+      #"error" => t("This item #@item_number had already been imported under nid @nid", array("@item_number" => $item_recnum, "@nid" => $item_import_history->nid))
+    );
   } else {
     // Item was not imported before.
     // If not given as the function's argument, get the item's bib record number.
@@ -381,115 +464,215 @@
       $result = millennium_fetch_recordpage($item_recnum);
       $record_html = $result->data;
       $ok = preg_match('/\/record=(b[0-9]+)/si', $record_html, $matches);
-      if (!$ok) {
-        return array("success" => false, "error" => "Could not get bib_recnum for item #$item_recnum");
+      if ($ok) {
+        $bib_recnum = $matches[1];
       }
-      $bib_recnum = $matches[1];
     }
   }
-  return millennium_update_bibrecord($bib_recnum, $item_recnum, $force_update, $marc_text);
+
+  if (!$bib_recnum) {
+    return array("success" => false, "error" => "Could not get bib_recnum for item #$item_recnum");
+  }
+
+  // We have a bib_number by now.
+  // Is there an existing node with this bib_number?
+  $bib_import_history = db_fetch_object(db_query(
+    "SELECT * FROM {millennium_node_bib} WHERE bib_recnum='%s' AND base_url = '%s'",
+    $bib_recnum,
+    millennium_get_real_baseurl()
+    ));
+  if (!$bib_import_history->nid) {
+    // New item, new bib.
+    // Must the node for the bib before the item can be imported.
+    $result = millennium_process_bib_record($bib_recnum, $force_update, $marc_text);
+    if ($result["success"] != true) {
+      return array("success" => false, "error" => "Could not create a new node for item: $item_recnum and bib: $bib_recnum");
+    } else {
+      $nid = $result["node"]->nid;
+    }
+  } else {
+    $nid = $bib_import_history->nid;
+  }
+
+  // Do actual import of new item.
+  $ok = millennium_process_item_record_new_import($item_recnum, $bib_recnum, $nid);
+  if ($ok) {
+    return array('success' => true, 'status' => 'item_created');
+  } else {
+    return array('success' => false, 'error' => "Could not create a new item for item $item_recnum");
+  }
 }
 
 /**
- * Imports a parent bibliographic record for an item record.
- * Requires the item and bibliographic record number to be known.
- * Optionally, if the full marc record is passed, it is used directly and will not be fetched
- * from the webpac.
- *
- * @param bib_recnum Millennium bibliographic record number
- * @param item_recnum An item record number for the record
- * @param force_update Forces the record to be updated if it already exists
- * @param marc_text The full marc record in plain text (same format as the webpac marc view)
+ * Handle processing an item record when it had not been previously imported.
  */
-function millennium_update_bibrecord($bib_recnum, $item_recnum, $force_update = true, $marc_text = null) {
-  static $xml_disabled = FALSE;
+function millennium_process_item_record_new_import($item_recnum, $bib_recnum, $nid) {
+  // Link item to bib, node
+  $record = array('nid' => $nid, 'bib_recnum' => $bib_recnum, 'item_recnum' => $item_recnum,
+        'updated' => date("Y-m-d H:i:s"), 'created' => date("Y-m-d H:i:s"), 'base_url' => millennium_get_real_baseurl());
+  $result = drupal_write_record('millennium_bib_item', $record);
+  if (!$result) {
+    drupal_set_message("millennium_process_item_record_new_import: drupal_write_record('millennium_bib_item') returned false");
+    return false;
+  }
+  return true;
+}
 
-  $bib_import_history = db_fetch_object(db_query("SELECT * FROM {millennium_node_opacitem} WHERE bib_recnum='%s' AND base_url = '%s'", $bib_recnum,millennium_get_real_baseurl()));
+/**
+ * Handle processing an item record when it had been previously imported.
+ */
+function millennium_process_item_record_prev_imported($item_recnum, $import_history, $force_update = true, $marc_text = null) {
+static $xml_disabled = false;
 
-  // Has this bib record been imported before?
-  if (! $bib_import_history) {
-    // It hasn't! Make nodeobject from the MARC record, import resulting nodeobject, then add taxonomy
+  $stale_record_age = (3600*24*30); // 30 days
+  $is_stale = (time() > (strtotime($import_history->updated) + $stale_record_age));
+  if ($is_stale || $force_update) {
+    // Update the item record.
+
+    if ($xml_disabled == false) {
+      // Get the item's data
+      $result = millennium_fetch_recordpage($item_recnum, "xml");
+      $bib_xml = $result->data;
+      if (stripos($item_xml, "403 Forbidden")) {
+        drupal_set_message("XML disabled in OPAC.");
+      }
 
-    // Make nodeobject from the MARC record
-    $nodeobject = millennium_item_to_nodeobject($bib_recnum, $item_recnum, $marc_text);
+      // Handle item not found
+      if (stripos($item_xml, "Cannot find record=")>0) {
+        millennium_handle_deleted_item($item_recnum);
+        return array(
+          "result" => "item_deleted",
+          "error" => "Item {$item_recnum} was deleted.",
+          );
+      }
 
-    if ($nodeobject->success === false) {
-      #drupal_set_message("millennium_update_bibrecord(): millennium_item_to_nodeobject() returned error ". $result["error"]);
-      return array("success" => false, "error" => $nodeobject->error);
+      // TODO: Do actual updating here...
+      // From xrecord: Use ICODEs? LOCATION? OUT DATE? YTDCIRC? TOTCIRC?
+      // From imported history: Expiry time? (millennium_bib_item.updated)
+      // Extract last update date
+      //  <LASTUPDATEDATE>26-09-2007</LASTUPDATEDATE>
+      /*
+      $ok = preg_match('/<LASTUPDATEDATE>(..)-(..)-(....)<\/LASTUPDATEDATE>/si', $bib_xml, $matches);
+      if (!$ok) {
+        #drupal_set_message("millennium_update_bibrecord(): Couldn't parse LASTUPDATEDATE from XML");
+        return array(
+          "success" => false,
+          "error" => "Couldn't parse LASTUPDATEDATE from XML for ". $bib_import_history->bib_recnum ." from item $item_recnum"
+          );
+      }
+      $bib_lastupdate = $matches[3] ."-". $matches[2] ."-". $matches[1];
+      */
     }
 
-    #$nodeobject = $result[0];
-    #$marc_parsed = $result[1];
-
-    // Add the new node
-    millennium_node_add($nodeobject, $item_recnum, $bib_recnum);
-
-    // Add taxonomy to new node
-    #millennium_add_taxonomy_to_node($nodeobject, $marc_parsed);
-
-    return array("success" => true, "node" => $nodeobject);
+    // Update the node-bib relationship
+    $record = array('id' => $import_history->id, 'updated' => date("Y-m-d H:i:s"));
+    // Update millennium_node_bib using key 'id'
+    $result = drupal_write_record('millennium_bib_item', $record, array('id'));
+    if (!$result) {
+      drupal_set_message("millennium_process_item_record_prev_imported: drupal_write_record('millennium_bib_item') returned false");
+      dpm($record);
+    }
+    return array(
+      "result" => "item_updated",
+    );
   }
-  else {
-    // There is an existing node for this item's parent bibrecord
 
-    // Check if there is need to update this record.
-    /*
-    if ($force_update == false) {
-      $result = millennium_fetch_recordpage($bib_import_history->bib_recnum, "xml");
-      $bib_xml = $result->data;
-      // TODO; Handle disabled XRECORD in OPAC, like catnyp.nypl.org
-      #drupal_set_message($bib_xml);
-      if (stripos($bib_xml, "403 Forbidden")) {
-        drupal_set_message("XML disabled in OPAC.");
-        $xml_disabled = true;
-        $force_update = true;
-      } else {
+  return array(
+    "result" => "item_untouched",
+  );
+}
 
-        // Handle item not found
-        if (stripos($bib_xml, "Cannot find record=")>0) {
-          #drupal_set_message("millennium_update_bibrecord(): millennium_fetch_recordpage() Couldn't fetch XML for". $bib_import_history->bib_recnum ." from item $item_recnum");
-          return array(
-            "success" => false,
-            "error" => "Couldn't fetch XML for ". $bib_import_history->bib_recnum ." from item $item_recnum"
-            );
-        }
+/**
+ * Imports a bibliographic record into a node from a given bib_recnum, updating existing nodes if stale,
+ * and links the nodes, bib records and item record together.
+ * Requires the bibliographic record number to be known.
+ * Optionally, if the full marc record is passed, it is used directly instead of being fetched
+ * from the WebOPAC.
+ *
+ * @param string $bib_recnum Required. Millennium bibliographic record number.
+ * @param boolean $force_update Optional. If true, forces the node to be updated when it already exists.
+ * @param string $marc_text The full marc record in plain text (same format as the WebOPAC marc view)
+ */
+function millennium_process_bib_record($bib_recnum, $force_update = false, $marc_text = null) {
+  static $xml_disabled = false;
 
-        // Extract last update date
-        //  <LASTUPDATEDATE>26-09-2007</LASTUPDATEDATE>
-        $ok = preg_match('/<LASTUPDATEDATE>(..)-(..)-(....)<\/LASTUPDATEDATE>/si', $bib_xml, $matches);
-        if (!$ok) {
-          #drupal_set_message("millennium_update_bibrecord(): Couldn't parse LASTUPDATEDATE from XML");
-          return array(
-            "success" => false,
-            "error" => "Couldn't parse LASTUPDATEDATE from XML for ". $bib_import_history->bib_recnum ." from item $item_recnum"
-            );
-        }
-        $bib_lastupdate = $matches[3] ."-". $matches[2] ."-". $matches[1];
-      }
-    }
-    */
+  $bib_import_history = db_fetch_object(db_query("SELECT * FROM {millennium_node_bib} WHERE bib_recnum='%s' AND base_url = '%s'", $bib_recnum, millennium_get_real_baseurl()));
 
-    // Is this newer than the existing bib record found for this item?
-    if ($force_update == true || $bib_lastupdate > substr($bib_import_history->bib_updated, 0, 10)) {
+  // Has this bib record been imported before?
+  if ($bib_import_history->nid) {
+    // There is an existing node for this bib record
+    // Check if there is need to update the node.
+    $stale_record_age = (3600*24*30); // 30 days
+    $is_stale = (time() > (strtotime($bib_import_history->updated) + $stale_record_age));
+
+    if ($force_update != true && !$is_stale) {
+      $node = node_load($bib_import_history->nid);
+      return array("success" => true, 'status' => 'bib_untouched', "node" => $node);
+    }
+  }
 
-      // Millennium newer than imported, re-create a nodeobject from the MARC record
-      $nodeobject = millennium_item_to_nodeobject($bib_import_history->bib_recnum, $item_recnum, $marc_text);
-      if ($nodeobject->success === false) {
-        #drupal_set_message("millennium_update_bibrecord(): millennium_item_to_nodeobject() returned error ". $result["error"]);
-        return array("success" => false, "error" => $nodeobject->error);
-      }
-
-      // insert existing node's nid into nodeobject and save node
-      $nodeobject->nid = $bib_import_history->nid;
-      
-      // Update
-      millennium_node_update($nodeobject, $item_recnum, $bib_recnum);
+  // We are here because an update is needed or was not previously imported.
+  // Further down decide if the node is updated or created; the previous steps are the same:
+  // - if no marc given, get marc is possible, if not handle deleted bib record
+  // - convert to node
+  // - Insert or update node
+  if ($marc_text == null) {
+    // No marc given, try to fetch.
+    $marc_text = millennium_fetch_marc($bib_recnum);
+    if ($marc_text == false) {
+      // Bib has been deleted!
+      millennium_handle_deleted_bib($bib_recnum);
+      return array("success" => false, "error" => "Could not fetch MARC record for $bib_recnum");
+    }
+  }
 
-      return array("success" => true, "node" => $nodeobject);
-    } else {
-      // TODO: Use Success?
-      return array("success" => true, "error" => "Item belongs to existing bib item ". $bib_import_history->bib_recnum ." in node ". $bib_import_history->nid);
+  // Re-create a nodeobject from the MARC record
+  $new_node = millennium_record_to_nodeobject($bib_recnum, $marc_text);
+  if ($new_node->success === false) {
+    #drupal_set_message("millennium_process_bib_record(): millennium_record_to_nodeobject() returned error ". $result["error"]);
+    return array("success" => false, "error" => $new_node->error);
+  }
+
+  // Now, decide if we are going to insert a new node, or update the existing node.
+  if ($bib_import_history->nid) {
+    // insert existing node's nid into new_node and save node
+    $new_node->nid = $bib_import_history->nid;
+    // Clear node cache
+    $oldnode = node_load($new_node->nid, NULL, true);
+    // Get vid from existing node.
+    $new_node->vid = $oldnode->vid;
+    // Fill in missing node info, and allow other modules to make changes
+    node_submit($new_node);
+    // Store node in MySQL
+    node_save($new_node);
+
+    // Update the node-bib relationship
+    $record = array('nid' => $bib_import_history->nid, 'bib_recnum' => $bib_recnum,
+      'biblio_data' => serialize($new_node->millennium_biblio_data),
+      'updated' => date("Y-m-d H:i:s"), 'base_url' => millennium_get_real_baseurl());
+    // Update millennium_node_bib using key 'nid'
+    $result = drupal_write_record('millennium_node_bib', $record, array('nid'));
+    if (!$result) {
+      drupal_set_message("drupal_write_record('millennium_node_bib') returned false");
+      dpm($record);
     }
+    return array("success" => true, 'status' => 'bib_updated', "node" => $new_node);
+  }
+  else {
+    // Not imported before, insert a new node
+    $new_node = node_submit($new_node);
+    node_save($new_node);
+
+    // Add node-bib relationship
+    $record = array('nid' => $new_node->nid, 'bib_recnum' => $bib_recnum,
+      'biblio_data' => serialize($new_node->millennium_biblio_data),
+      'updated' => date("Y-m-d H:i:s"), 'created' => date("Y-m-d H:i:s"), 'base_url' => millennium_get_real_baseurl());
+    $result = drupal_write_record('millennium_node_bib', $record);
+    if (!$result) {
+      drupal_set_message("millennium_process_bib_record drupal_write_record('millennium_node_bib') returned false");
+      dpm($record);
+    }
+    return array("success" => true, 'status' => 'bib_created', "node" => $new_node);
   }
 }
 
@@ -500,13 +683,13 @@
  */
 function millennium_handle_deleted_item($item_recnum) {
   // Get bibrecord number and erase any existing node-item relationships
-  $data = db_fetch_object(db_query("SELECT nid, bib_recnum FROM {millennium_node_opacitem} WHERE item_recnum='%s' AND base_url='%s'", $item_recnum, millennium_get_real_baseurl()));
+  $data = db_fetch_object(db_query("SELECT nid, bib_recnum FROM {millennium_bib_item} WHERE item_recnum='%s' AND base_url='%s'", $item_recnum, millennium_get_real_baseurl()));
 
   if ($data->bib_recnum) {
-    db_query("DELETE FROM {millennium_node_opacitem} WHERE item_recnum='%s' AND base_url = '%s'", $item_recnum, millennium_get_real_baseurl());
+    db_query("DELETE FROM {millennium_bib_item} WHERE item_recnum='%s' AND base_url = '%s'", $item_recnum, millennium_get_real_baseurl());
     watchdog("Millennium", "Deleted node-item relationship for node @nid and item #@item.", array("@item" => $item_recnum, "@nid" => $data->nid));
     // Delete the node if no items remain for same bibrecord
-    $count = db_result(db_query("SELECT count(*) FROM {millennium_node_opacitem} where bib_recnum='%s' AND base_url ='%s'", $data->bib_recnum, millennium_get_real_baseurl()));
+    $count = db_result(db_query("SELECT count(*) FROM {millennium_bib_item} where bib_recnum='%s' AND base_url ='%s'", $data->bib_recnum, millennium_get_real_baseurl()));
     if ($count == 0) {
       node_delete($data->nid);
       watchdog("Millennium", "Deleted node @nid because item #@item was deleted, and there were no remaining imported nodes for bib record #@bib.",
@@ -517,26 +700,43 @@
 }
 
 /**
+ * Take action based on a bib no longer found on the WebOPAC; delete node and item-node, bib-node relationships
+ * @param string $bib_recnum The bib record number (i123456x).
+ */
+function millennium_handle_deleted_bib($bib_recnum) {
+  db_query("DELETE FROM {millennium_bib_item} WHERE bib_recnum='%s' AND base_url = '%s'", $bib_recnum, millennium_get_real_baseurl());
+  watchdog("Millennium", "Deleted bib-item relationships for bib_recnum #@bib.", array("@bib" => $bib_recnum));
+  // Delete the node if no items remain for same bibrecord
+  $data = db_fetch_object(db_query("SELECT * FROM {millennium_node_bib} where bib_recnum='%s' AND base_url ='%s'", $bib_recnum, millennium_get_real_baseurl()));
+  if ($data->nid) {
+    node_delete($data->nid);
+    watchdog("Millennium", "Deleted node @nid because bib record #@bib was not found on WebOPAC",
+      array("@bib" => $data->bib_recnum, "@nid" => $data->nid)
+    );
+  }
+}
+
+/**
  * Fetches information for a Millennium record and returns a populated nodeobject
- * @param bib_recnum Millennium record number (b123456) to fetch
+ * @param recnum Millennium record number (b123456, i123456) to fetch
  * @param string $marc_text An optional MARC record to import from.
  */
-function millennium_item_to_nodeobject($bib_recnum, $item_recnum, $marc_text = null) {
+function millennium_record_to_nodeobject($recnum, $marc_text = null) {
   $result = new stdClass();
   if ($marc_text == null) {
-    $marc_text = millennium_fetch_marc($bib_recnum);
+    $marc_text = millennium_fetch_marc($recnum);
     if (!$marc_text) {
       $result->success = FALSE;
-      $result->error = "Could not fetch MARC for $item_recnum";
+      $result->error = "Could not fetch MARC for $recnum";
       return $result;
     }
   }
 
   //Create a nodeobject from parsed MARC
-  $nodeobject = millennium_marc_to_nodeobject($item_recnum, $marc_text);
+  $nodeobject = millennium_marc_to_nodeobject($marc_text, $recnum);
   if (!$nodeobject) {
     $result->success = false;
-    $result->error = "Could not create node object from parsed MARC for $item_recnum";
+    $result->error = "Could not create node object from parsed MARC for $recnum";
     return $result;
   }
 
@@ -562,62 +762,74 @@
 
 /**
  * Grabs Millennium record's HTML or XML
- * @param item_recnum Millennium record number (b123456, i123456) to fetch
+ * @param recnum Millennium record number (b123456, i123456) to fetch
  * @param mode String specifying type of view: plain, marc, items, xml
  */
-function millennium_fetch_recordpage($item_recnum, $mode = "plain") {
+function millennium_fetch_recordpage($recnum, $mode = "plain") {
   global $millennium_fetch_recordpage_cache;
-  $key = $item_recnum . $mode;
-  #drupal_set_message("millennium_fetch_recordpage($item_recnum, $mode): key = $key");
+  $key = $recnum . $mode;
+  #drupal_set_message("millennium_fetch_recordpage($recnum, $mode): key = $key");
   if (isset($millennium_fetch_recordpage_cache[$key])) {
     $result["data"] = $millennium_fetch_recordpage_cache[$key];
     return (object)$result;
   }
 
   // Get URL to fetch
-  $url = millennium_permalink($item_recnum, $mode);
+  $url = millennium_permalink($recnum, $mode);
   if ($url == false) {
     return false;
   }
-  #drupal_set_message("millennium_fetch_recordpage($item_recnum, $mode): URL = $url");
+  #drupal_set_message("millennium_fetch_recordpage($recnum, $mode): URL = $url");
 
   // Fetch bib record (if it exists) from URL
-  $start_microtime = microtime(true);
+  #$start_microtime = microtime(true);
   $result = drupal_http_request($url);
-  $elapsed_microtime = microtime(true) - $start_microtime;
-  #drupal_set_message(sprintf("millennium_fetch_recordpage($item_recnum, $mode): Elapsed time: %2.3f seconds", $elapsed_microtime));
-  $millennium_fetch_recordpage_cache[$item_recnum . $mode] = $result->data;
+  #$elapsed_microtime = microtime(true) - $start_microtime;
+  #drupal_set_message(sprintf("millennium_fetch_recordpage($recnum, $mode): Elapsed time: %2.3f seconds", $elapsed_microtime));
+  $millennium_fetch_recordpage_cache[$recnum . $mode] = $result->data;
   return $result;
 }
+
 /**
  * Returns a URL for a certain item record view in Millennium
  */
-function millennium_permalink($item_recnum, $mode = 'plain') {
+function millennium_permalink($recnum, $mode = 'plain') {
   $baseurl = millennium_get_real_baseurl();
   if ($baseurl == false) {
     return false;
   }
+  // Determine if record is for an item or bib
+  $recnum_type = substr($recnum, 0, 1);
+
   switch ($mode) {
     case "plain":
-      $url = "{$baseurl}/record={$item_recnum}&searchscope=0";
-      # TODO check if this one is better:
-      #$url = "{$baseurl}/search*eng/?searchtype=.&searcharg={$item_recnum}&searchscope=0";
+      $url = "{$baseurl}/record={$recnum}&searchscope=0";
       break;
     case "xml":
-      $url = "{$baseurl}/xrecord={$item_recnum}&searchscope=0";
-      break;
-    case "hold":
-      $bib_recnum = db_result(db_query("SELECT bib_recnum FROM {millennium_node_opacitem} WHERE item_recnum='%s' AND base_url = '%s'", $item_recnum, millennium_get_real_baseurl()));
-      $url = "{$baseurl}/search*eng?/.{$item_recnum}/.{$bib_recnum}/1%2C1%2C1%2CB/request~{$bib_recnum}";
+      $url = "{$baseurl}/xrecord={$recnum}&searchscope=0";
       break;
     case "marc":
-      $url = "{$baseurl}/search*eng?/.{$item_recnum}/.{$item_recnum}/1,1,1,B/marc~{$item_recnum}";
-      break;
+    case "hold":
     case "items":
-      $bib_recnum = db_result(db_query("SELECT bib_recnum FROM {millennium_node_opacitem} WHERE item_recnum='%s' AND base_url = '%s'", $item_recnum, millennium_get_real_baseurl()));
-      $num = drupal_substr($item_recnum, 1); // e.g.: 123456
-      $url = $baseurl ."/search*eng?/.{$item_recnum}/.{$bib_recnum}/1,1,1,B/holdings~{$num}&FF=&1,0,";
-      break;
+      if ($recnum_type == "i") {
+        $bib_recnum = db_result(db_query("SELECT bib_recnum FROM {millennium_bib_item} WHERE item_recnum='%s' AND base_url = '%s'", $recnum, millennium_get_real_baseurl()));
+        $item_recnum = $recnum;
+      } else {
+        $bib_recnum = $recnum;
+        $item_recnum = $recnum;
+      }
+      switch($mode) {
+        case "hold":
+          $url = "{$baseurl}/search*eng?/.{$item_recnum}/.{$bib_recnum}/1%2C1%2C1%2CB/request~{$bib_recnum}";
+          break;
+        case "marc":
+          $url = "{$baseurl}/search*eng?/.{$item_recnum}/.{$item_recnum}/1,1,1,B/marc~{$item_recnum}";
+          break;
+        case "items":
+          $num = drupal_substr($item_recnum, 1); // e.g.: 123456
+          $url = $baseurl ."/search*eng?/.{$item_recnum}/.{$bib_recnum}/1,1,1,B/holdings~{$num}&FF=&1,0,";
+          break;
+      }
   }
   #drupal_set_message("millennium_permalink($item_recnum, $mode): url: $url");
   return $url;
@@ -663,15 +875,15 @@
 
 /**
  * Takes a parsed marc record array and generates a nodeobject with type 'biblio'
- * @param item_recnum Millennium record number (i123456) that contains this MARC
+ * @param bib_recnum Millennium record number (b123456) that contains this MARC
  * @param marc_text MARC record (plaintext)
  */
-function millennium_marc_to_nodeobject($item_recnum, $marc_text) {
+function millennium_marc_to_nodeobject($marc_text, $recnum = false) {
   global $_millennium_field_labels;
 
   $marc = millennium_parse_marc($marc_text);
   if (! $marc) {
-    #return array("success" => false, "error" => "Could not parse MARC for $item_recnum");
+    #return array("success" => false, "error" => "Could not parse MARC for $recnum");
     return false;
   }
 
@@ -680,8 +892,8 @@
   // LCC
   $lcc = millennium_getFieldPlain($marc, "05...");
   // If no LCC, try to get from items
-  if (trim($lcc) == "") {
-    $holdings = millennium_get_holdings_info($item_recnum);
+  if (trim($lcc) == "" && $recnum) {
+    $holdings = millennium_get_holdings_info($recnum);
     // Look for first "classnumber" in items
     if (is_array($holdings)) {
       foreach ($holdings as $holding) {
@@ -879,7 +1091,7 @@
   $tmpfields = millennium_getFields($marc, "008..");  #get all 008s
   if (is_array($tmpfields)) {
     $lang_code = drupal_substr($tmpfields[0]["rawdata"], 35, 3);
-    $biblio["lang"] = _millennium_langcode_to_human($lang_code);
+    $biblio['lang'] = _millennium_langcode_to_human($lang_code);
   }
 
   if ($biblio["title"] == "") {
@@ -887,9 +1099,9 @@
   }
 
   // Others
-  $biblio["item_recnum"] = $item_recnum;
+  #$biblio["bib_recnum"] = $bib_recnum;
   $biblio["imprint"] = $biblio["imprint_place"] ." ". $biblio["imprint_name"] ." ". $biblio["imprint_date"];
-  $biblio["url"] = l(t('Link to original record'), millennium_permalink($item_recnum, "plain"));
+  $biblio["url"] = l(t('Link to original record'), millennium_permalink($recnum, "plain"));
   $biblio["marc"] = $marc_text;
 
   // Clean up (almost) every $biblio[] value
@@ -940,7 +1152,7 @@
     "biblio_call_number" => $biblio["lcc"],
     "biblio_lang" => $biblio["lang"],
     "biblio_url" => $biblio["url"],
-    "biblio_custom7" => $biblio["item_recnum"],
+    "biblio_custom7" => $biblio["bib_recnum"],
     */
   );
 
@@ -973,16 +1185,20 @@
     'teaser' => 'series,translated_title,edition,imprint,lang',
     );
 
+  $translateable_fields = array('type', 'lang');
   $content = "";
   foreach (explode(",", $map[$mode]) as $fieldname) {
-    $fieldvalue = $biblio_data[$fieldname];
-    if (trim($fieldvalue) == "") {
+    $fieldvalue = trim($biblio_data[$fieldname]);
+    if ($fieldvalue == "") {
       continue;
     }
+    if (in_array($fieldname, $translateable_fields)) {
+      $fieldvalue = t($fieldvalue);
+    }
     $rows[] = array(
       'data' => array(
         array('data' => $_millennium_field_labels[$fieldname], 'class' => 'fieldname'),
-        array('data' => trim($fieldvalue), 'class' => 'fieldvalue')
+        array('data' => $fieldvalue, 'class' => 'fieldvalue')
       )
     );
   }
@@ -1012,67 +1228,6 @@
 }
 
 /**
- * Saves a node object to the database and associates it with the Millennium record # that originated it.
- */
-function millennium_node_add($node, $item_recnum, $bib_recnum) {
-  $base_url = millennium_get_real_baseurl();
-  $node = node_submit($node);
-  node_save($node);
-  $nid = $node->nid;
-
-  // Add relationship to millennium_node_opacitem table
-  $result = db_query("INSERT INTO {millennium_node_opacitem} (nid, item_recnum, bib_recnum, item_updated, created, bib_updated, biblio_data, base_url) VALUES (%d, '%s', '%s', now(), now(), now(), '%s', '%s')",
-      $node->nid,
-      $item_recnum,
-      $bib_recnum,
-      serialize($node->millennium_biblio_data),
-      $base_url
-  );
-  return $node;
-}
-
-/**
- * Updates an existing node object imported from a Millennium record
- */
-function millennium_node_update(&$node, $item_recnum, $bib_recnum) {
-  
-  // If no node->nid, get it from recnum
-  if (empty($node->nid)) {
-    $node->nid = db_result(db_query("SELECT nid FROM {millennium_node_opacitem} WHERE item_recnum = '%s' AND base_url = '%s'", $item_recnum, millennium_get_real_baseurl())); 
-  }
-  
-  // Clear node cache
-  $oldnode = node_load($node->nid, NULL, true);
-  
-  // Get vid from existing node.
-  $node->vid = $oldnode->vid;
-  
-  // Fill in missing node info, and allow other modules to make changes
-  node_submit($node);
-  
-  // Store node in MySQL
-  node_save($node);
-
-  // Add or update relationship to millennium_node_opacitem table
-  $result = db_fetch_object(db_query("SELECT * from {millennium_node_opacitem} WHERE item_recnum = '%s' AND bib_recnum= '%s' AND base_url = '%s'", $item_recnum, $bib_recnum, millennium_get_real_baseurl()));
-  if ($result->item_recnum) {
-    $result = db_query("UPDATE {millennium_node_opacitem} SET bib_updated = NOW(), biblio_data='%s' WHERE bib_recnum='%s'",
-      serialize($node->millennium_biblio_data),
-      $bib_recnum);
-  } else {
-    // Add relationship to millennium_node_opacitem table
-    $result = db_query("INSERT INTO {millennium_node_opacitem} (nid, item_recnum, bib_recnum, item_updated, created, bib_updated, biblio_data, base_url) VALUES (%d, '%s', '%s', NOW(), NOW(), NOW(), '%s', '%s')",
-        $node->nid,
-        $item_recnum,
-        $bib_recnum,
-        serialize($node->millennium_biblio_data),
-        millennium_get_real_baseurl()
-    );
-  }
-  return $node;
-}
-
-/**
  * Scans a parsed MARC record array for important access points and relates them with taxonomy terms in vocabularies stored in module's config.
  * @param nodeobject A drupal node object
  * @param record Array containing parsed marc record
@@ -1443,8 +1598,8 @@
 function millennium_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
   static $googleapi_js_included = false;
 
-  $item_recnum = millennium_item_recnum_from_node($node->nid);
-  if (! $item_recnum) {
+  $bib_recnum = millennium_bib_recnum_from_node($node->nid);
+  if (! $bib_recnum) {
     return;
   }
   switch ($op) {
@@ -1470,7 +1625,8 @@
       */
       break;
     case 'delete':
-      db_query("DELETE FROM {millennium_node_opacitem} WHERE nid = %d", $node->nid);
+      db_query("DELETE FROM {millennium_node_bib} WHERE nid = %d", $node->nid);
+      db_query("DELETE FROM {millennium_bib_item} WHERE nid = %d", $node->nid);
       break;
 
     case 'rss item':
@@ -1624,12 +1780,12 @@
   //    http://example.com/search?/.i[item rec num]/.b[bib rec num]/1%2C1%2C1%2CB/request%7Eb[bib rec num]
   // Another example:
   //    http://example.com/search~S16?/.i[item rec num]/.b[bib rec num]/1%2C1%2C1%2CB/request~b2405160
-  $item_recnum = millennium_item_recnum_from_node($nid);
-  if (! $item_recnum) {
+  $bib_recnum = millennium_bib_recnum_from_node($nid);
+  if (! $bib_recnum) {
     return;
   }
 
-  $hold_url = millennium_permalink($item_recnum, "hold");
+  $hold_url = millennium_permalink($bib_recnum, "hold");
   $links["millennium_holds"] = array(
     'title' => t('Place hold or request delivery'),
     'href' => $hold_url,
@@ -1676,7 +1832,7 @@
 
       switch ($delta) {
         case "marc":
-          if (!$marc_text = millennium_fetch_marc($bib_recnum) 
+          if (!$marc_text = millennium_fetch_marc($bib_recnum)
             || !user_access("view marc")) {
             return "";
           }
@@ -1751,19 +1907,23 @@
 }
 
 /**
- * Return the item record number for node
- * @param nid node id
+ * Return an array of  item record numbers for this node.
+ * @param nid The node id.
  */
-function millennium_item_recnum_from_node($nid) {
+function millennium_item_recnums_from_node($nid) {
   static $cache;
   if (isset($cache[$nid])) {
     return $cache[$nid];
   }
   else {
-    $recnum = db_result(db_query("SELECT item_recnum FROM {millennium_node_opacitem} WHERE nid=%d", $nid));
-    if ($recnum) {
-      $cache[$nid] = $recnum;
-      return $recnum;
+    $recnums = array();
+    $result = db_query("SELECT item_recnum FROM {millennium_bib_item} WHERE nid = %d", $nid);
+    while ($data = db_fetch_object($result)) {
+      $recnums[] = $data->item_recnum;
+    }
+    if (!empty($recnums)) {
+      $cache[$nid] = $recnums;
+      return $recnums;
     }
     else {
       return false;
@@ -1781,7 +1941,7 @@
     return $cache[$nid];
   }
   else {
-    $recnum = db_result(db_query("SELECT bib_recnum FROM {millennium_node_opacitem} WHERE nid=%d", $nid));
+    $recnum = db_result(db_query("SELECT bib_recnum FROM {millennium_node_bib} WHERE nid=%d", $nid));
     if ($recnum) {
       $cache[$nid] = $recnum;
       return $recnum;
@@ -1841,7 +2001,7 @@
       }
       $mil_page = $page ? "page" : "teaser";
 
-      $item_recnum = millennium_item_recnum_from_node($nid);
+      $bib_recnum = millennium_bib_recnum_from_node($nid);
       drupal_add_js("var millenniumModulePath = ". drupal_to_js(drupal_get_path('module', 'millennium')) .";", 'inline');
       drupal_add_js("var millenniumLoadingText = ". drupal_to_js(t('Loading...')) .";", 'inline');
       drupal_add_js("var millenniumLocale = ". drupal_to_js($mil_locale) .";", 'inline');
@@ -1850,7 +2010,7 @@
       $baseurl = millennium_get_real_baseurl();
       if ($baseurl) {
         $output .= "<div class='millennium holdings load' id='nid-$nid'>";
-        $output .= l( $msg, millennium_permalink($item_recnum));
+        $output .= l( $msg, millennium_permalink($bib_recnum));
         $output .= "</div>";
       }
       break;
@@ -1861,9 +2021,9 @@
       break;
 
     case "recordlink":
-      $item_recnum = millennium_item_recnum_from_node($nid);
+      $bib_recnum = millennium_bib_recnum_from_node($nid);
       $baseurl = millennium_get_real_baseurl();
-      $record_link = l( $msg, millennium_permalink($item_recnum));
+      $record_link = l( $msg, millennium_permalink($bib_recnum));
       $output .= "<div class='millennium holdings'>". $record_link ."</div>";
       break;
   }
@@ -2739,8 +2899,8 @@
     $output = unserialize($cache->data);
   }
   else {
-    $item_recnum = millennium_item_recnum_from_node($nid);
-    $holdings = millennium_get_holdings_info($item_recnum);
+    $bib_recnum = millennium_bib_recnum_from_node($nid);
+    $holdings = millennium_get_holdings_info($bib_recnum);
     $output = theme('millennium_holdings', $nid, $holdings, $page);
     cache_set($cid, serialize($output), 'cache', time() + 900); // 15 minutes
   }
@@ -2809,7 +2969,7 @@
     return $cache[$nid];
   }
   $data = false;
-  $result = db_result(db_query("SELECT biblio_data FROM {millennium_node_opacitem} WHERE nid = %d", $nid));
+  $result = db_result(db_query("SELECT biblio_data FROM {millennium_node_bib} WHERE nid = %d", $nid));
   if ($result) {
     $data = unserialize($result);
   }
@@ -2837,15 +2997,15 @@
 function millennium_token_values($type, $object = NULL) {
   switch ($type) {
     case 'node':
-      $item_recnum = millennium_item_recnum_from_node($object->nid);
-      if (! $item_recnum) {
+      $bib_recnum = millennium_bib_recnum_from_node($object->nid);
+      if (! $bib_recnum) {
         break;
       }
       $bib_recnum = millennium_bib_recnum_from_node($object->nid);
       $biblio_data = millennium_get_biblio_data($object->nid);
       $year = intval($biblio_data["imprint_date"]);
       $values['millennium-year']    = check_plain($year);
-      $values['millennium-item-recnum']    = check_plain($item_recnum);
+      #TODO: How to handle several values? # $values['millennium-item-recnum']    = check_plain($item_recnum);
       $values['millennium-bib-recnum']     = check_plain($bib_recnum);
       $values['millennium-authors'] = check_plain($biblio_data["authors"]);
       $values['millennium-type']    = check_plain($biblio_data["type"]);
Index: millennium_auth.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/millennium/millennium_auth.module,v
retrieving revision 1.1.2.5
diff -u -r1.1.2.5 millennium_auth.module
--- millennium_auth.module	17 Apr 2009 16:04:37 -0000	1.1.2.5
+++ millennium_auth.module	22 Oct 2009 22:01:35 -0000
@@ -160,7 +160,7 @@
   }
   foreach($items as $item) {
     // Has record been imported into Drupal?
-    $nid = db_result(db_query("SELECT nid FROM {millennium_node_opacitem} where item_recnum='%s'", $item["item_recnum"]));
+    $nid = db_result(db_query("SELECT nid FROM {millennium_bib_item} where item_recnum='%s' AND base_url = '%s'", $item["item_recnum"], millennium_get_real_baseurl()));
     if ($nid) {
       $link = l($item["title"], "node/$nid");
     }

