Virtual Thripp.com Subdomains

I’ve set up some virtual Thripp.com subdomains for pages on richardxthripp.thripp.com that used to subdirectories. All the old URLs 301 to these new URLs. Check out the first batch of subdomains:

about.thripp.com
contact.thripp.com
domains.thripp.com
index.thripp.com
gallery.thripp.com
portfolio.thripp.com
portraits.thripp.com

All of these are for me only and are completely part of richardxthripp.thripp.com. In 2008, I intended Thripp.com to be a social network built on WordPress MU with subdomains for each user, but that never worked out so I feel comfortable owning the *.thripp.com namespace for my own projects now. I will never be changing the URL of this blog from richardxthripp.thripp.com since it’s been that way for so long, though.

This was actually very hard to implement. I’m using WordPress MU 2.7 and the WP Subdomains plugin, but I had to do hacking on both and I can’t get any subdirectories on these virtual subdomains to work so I have to use query strings. For example, gallery.thripp.com is paginated so page 2 is gallery.thripp.com/?page=2/ because I couldn’t get gallery.thripp.com/2/ to work. However, I’m glad I set this up since I will be using Thripp.com subdomains for many pages and ad campaigns in the months to come.

Making WordPress Tag Balancing Work with Exec-PHP

I use the WordPress plugin Exec-PHP to use PHP in my posts, but under normal circumstances if I do this with “WordPress should correct invalidly nested XHTML automatically” (a.k.a. tag balancing) checked in Settings > Writing, I get this nasty error whenever I try to use PHP:

Parse error: syntax error, unexpected ‘?’ in /home/thripp/public_html/wp-content/plugins/exec-php/includes/runtime.php(42) : eval()’d code on line 1

The solution the developer provides is to simply disable that feature. That’s fine most of the time, but I encountered a tricky situation where I needed to use PHP and have WordPress close open HTML tags which I simply could not close.

My posts on this blog are usually photos with short descriptions, but occasionally I write long articles which may go on for thousands of words. Up until last year, my tag, category, and archive pages displayed the full content of my posts. WordPress excerpts were unacceptable for two reasons: 1.) they are always 55 words; 2.) I use Post Thumb Revisited to auto-convert 800×600 images to 400×300 thumbnails, but it only converts them through the_content filter, not the_excerpt. While the excerpt length is customizable in WP 2.8 or newer, I am unwilling to upgrade from WPMU 2.7. What I really needed was an excerpt that used the_content, respected all HTML tags, worked with Exec-PHP, and let me customize the excerpt length.

Enter The Excerpt Reloaded. The plugin was 5 years old, so I found an updated version with bugfixes that was only 3 years old. I quickly wrote this code for my theme’s index.php file and left it this way until today:

if(is_category() || is_archive()) the_excerpt_reloaded(200, ‘all’, ‘content’,
     true, “<strong>… continue reading</strong>”, false, 1, false, false,
     ‘p’, ‘Click to see whole entry.’, true);
else the_content(__(‘… CONTINUE READING’));

This has been the best of both worlds. It cuts off the content at 200 words, so most of my photos do not have a “continue reading” link because my descriptions are under 200 words. Longer posts are cut off after 200 words, so my archive pages do not become unnecessarily long. I had to set the 8th argument of the_excerpt_reloaded, $fix_tags, to false, because I would get the same old Exec-PHP error if it was set to true. “No problem,” I thought. I already have tag balancing disabled in WordPress, so what could it hurt to disable it here?

Recently, however, I encountered an insidious bug when a post was cut at 200 words in the middle of a <strong> tag. The tag would never be closed, meaning the rest of the page would be bold! Take a look at this screenshot:

Unbalanced tags

What do you do about something like this? Obviously, there are many solutions. I could rewrite the offending article so the 201st word is not in the middle of an HTML tag. All I would have to do is put in a few filler words earlier in the article. I could enable tag balancing, write some code to check if each post contains PHP, and not use the_excerpt_reloaded in those cases. I could use custom fields on posts to determine which mode of behavior should be used. I could upgrade WordPress (oh god no). All of these solutions seem suboptimal.

Instead, I went to the problem’s source. If $fix_tags is true, the_excerpt_reloaded runs the content of the excerpt through balanceTags. What is balanceTags? A WordPress function in /wp-includes/formatting.php which activates force_balance_tags. What is force_balance_tags? A WordPress function in the same file which looks like hieroglyphics. All I wanted to do was force the function to ignore PHP, but I couldn’t figure it out. It wasn’t a simple matter of ignoring <?php ?> tags. My PHP tags often appear in the middle of other HTML tags. Here is the source code of a typical photo on my blog:

<img src=”http://thripp.com/files/photos/flash.jpg” alt=”<?php the_title(); ?>” />

I took this picture of raindrops falling at night, and my camera’s flash reflected off one of the raindrops. It looks like a star going supernova.

[­sniplet fuji-a360], <?php fexf(); ?>

<a href=”[­sniplet photos-path]stock/<?php echo fprm(); ?>-stock.jpg”>[­sniplet stock-dl-text]</a> (<?php fsze(fprm() . ‘-stock.jpg’); ?>) or <a href=”[­sniplet photos-path]stock/source/<?php echo fprm(); ?>-ss.jpg”>[­sniplet ss-dl-text-lc]</a> (<?php fsze(‘source/’ . fprm() . ‘-ss.jpg’); ?>).

[­sniplet stock-rights]

Looks pretty terrible, huh? I’ve got sniplets in there, custom PHP functions, concatenation, nesting… you name it. This template creates the file paths for the photos, source files, and stock versions right from the post title, because I upload the photos by FTP and follow a rigid file structure. The only reason there’s a full IMG tag at the top of each post is because Post Thumb Revisted won’t create the thumbnails to automatically generate my gallery without it. The template also displays the size of the source files and then extracts and displays the Exif data from the photos in my preferred format, which was extremely difficult to set up and is something I used to do manually. It runs itself, and the functions are really quite interesting.

Anyway, what I needed was a way to bypass force_balance_tags entirely, but only in regard to PHP code. I need the function to close dangling tags like <strong>, <em>, and <u> if the_excerpt_reloaded cuts the post in the middle of a tag.

After a lot of unsuccessful Google searches, I remembered that I solved a similar problem at the beginning of October in Tweet This 1.8. On the “Write Tweet” page, Tweet This uses a modified version of Jeff Roberson’s Linkify URL to delimit URLs with a space on each side (function tt_delimit_urls). A tweet like “Check out http://www.google.com/!” becomes “Check out http://www.google.com/ !”. Then, I use Ext-Conv-Links by Muhammad Arfeen to convert all long URLs to short URLs if the tweet is over 140 characters (class tt_shorten_urls). This works great for most URLs, but I discovered it breaks URLs containing underscores. http://en.wikipedia.org/wiki/South_Africa gets sent to the URL shortener http://en.wikipedia.org/wiki/South, which gets converted into http://bit.ly/bzLvSK_Africa, which doesn’t work at all. Totally unacceptable.

After many hours of torment trying to fix Jeff or Muhammad’s code, I decided to approach the problem from a different angle. Why not just replace underscores with something else on the way in, and then change them back to something else on the way out? Good programming doesn’t dance around problems, but I’ll take a practical solution that works over an idealistic solution that fails, any day. But what string to replace underscores with? I can’t use a special character or something that might be used in a tweet on purpose, because it will get converted into an underscore. After some thought, I settled on t9WGb5. It doesn’t look pretty, but it works, and I doubt any URL containing “t9WGb5” is ever going to be purposefully included in a tweet. So I proceeded to write statements like str_replace(‘t9WGb5’, ‘_’, $url) and str_replace(‘_’, ‘t9WGb5’, $url) at the necessary places throughout the code, and URLs with underscores worked like a charm. As an Easter egg, try writing a tweet over 140 characters containing a URL where you replace an underscore with “t9WGb5” yourself, for example, “test test test test test test test test test test test test test test test test test test test http://en.wikipedia.org/wiki/Southt9WGb5Africa”, then preview it on the Write Tweet page. Check the preview page for the short URL, i.e. http://bit.ly/cRLAis+, and you’ll see that your “t9WGb5” was converted to an underscore before the long URL was even sent to Bit.ly, as an artifact of my kludge-like solution.

Couldn’t the tag balancing problem be approached in the same way? Of course it could. A simple modification to /wp-includes/formatting.php did the trick. Right at the start of the force_balance_tags function, I replaced “<?php” and “?>” with “[![?php” and “?]!]” using str_replace, as follows:

function force_balance_tags( $text ) {
     $text = str_replace(array(‘<?php’, ‘?>’), array(‘[![?php’, ‘?]!]’), $text);
     $tagstack = array(); $stacksize = 0; $tagqueue = ”; $newtext = ”;

Then, at the end of the function, I change it all back:

// WP fix for the bug with HTML comments
     $newtext = str_replace(“< !–“,”<!–“,$newtext);
     $newtext = str_replace(“< !–“,”< !–“,$newtext);
     $newtext = str_replace(array(‘[![?php’, ‘?]!]’), array(‘<?php’, ‘?>’), $newtext);
     return $newtext;
}

All this happens either before or after Exec-PHP executes. I’m not sure when, but it doesn’t matter. My goal of being able to use tag balancing with Exec-PHP has been reached. I now have $strip_tags set to true in the_excerpt_reloaded and “WordPress should correct invalidly nested XHTML automatically” enabled in Settings > Writing, and all I have to do is re-apply the hack when I upgrade WordPress. It’s amazing what thinking outside the box gets you.

I can’t actually write “[![?php” or “?]!]” inside any post on my site, because my hack will convert those strings to real PHP code and they won’t be displayed. How did I display the code above? My actual /wp-includes/formatting.php file uses underscores instead of exclamation points. How did I include the sniplets in the example post without the Sniplets plugin executing them? Breaking the parser with the &shy; HTML entity. Simple.

Earlier, I talked about the functions I use in my photo template to automate display of file size and Exif data. Here are those functions:

function fsze($f = ‘simplicity-stock.jpg’, $p =
     ‘/home/thripp/public_html/wp-content/blogs.dir/2/files/photos/stock/’)
     {$n = array(‘Bytes’, ‘KB’, ‘MB’, ‘GB’); $p = $p . $f;
     if(file_exists($p)) $b = filesize($p);
          else $b = ‘1000’;
     echo round($b/pow(1000, ($i = floor(log($b, 1000)))), 2) . $n[$i];}

function fprm() {
     return str_replace(‘photo-‘, ”, preg_replace(‘/-+/’, ‘-‘,
          preg_replace(‘/[^a-z0-9-]/’, ‘-‘,
          strtolower(trim(str_replace(array(‘?’, ‘…’),
          array(”, ”), get_the_title()))))));}

function fexf() {
     $exif = exif_read_data(‘/home/thripp/public_html/wp-content/’ .
          ‘blogs.dir/2/files/photos/’ . fprm() . ‘.jpg’, 0, true);
     $shutter = $exif[‘EXIF’][‘ExposureTime’];
     $fnum = str_replace(‘f/’, ‘F’, $exif[‘COMPUTED’][‘ApertureFNumber’]);
     $focal = $exif[‘EXIF’][‘FocalLength’];
     $iso = $exif[‘EXIF’][‘ISOSpeedRatings’];
     $date = $exif[‘EXIF’][‘DateTimeOriginal’];
     $date = str_replace(‘:’, ‘-‘, substr($date, 0, 10)) . ‘T’ .
          substr($date, 11);
     if(substr($date, 0, 4) < = 2007) {           $id = substr($date, 0 , 10) . '_' . substr($date, 11, 2) .           'h' . substr($date, 14, 2) . 'm' . substr($date, 17);}      elseif(substr($date, 0, 4) >= 2008) {
          $id = str_replace(‘-‘, ”, substr($date, 0 , 10)) . ‘-‘ .
          str_replace(‘:’, ”, substr($date, 11)) . ‘rxt’;}
     $md = str_replace(‘-‘, ”, substr($date, 5, 5));
     $hms = str_replace(‘:’, ”, substr($date, 11));
     if(substr($date, 0, 4) == 2004) {
          if(($md < 0404) || ($md == '0404' && $hms < 020000) ||           ($md > 1031) || ($md == ‘1031’ && $hms > 020000))
               $ldate = $date . ‘-05’;
          else $ldate = $date . ‘-04’;}
     if(substr($date, 0, 4) == 2005) {
          if(($md < 0403) || ($md == '0403' && $hms < 020000) ||           ($md > 1030) || ($md == ‘1030’ && $hms > 020000))
               $ldate = $date . ‘-05’;
          else $ldate = $date . ‘-04’;}
     if(substr($date, 0, 4) == 2006) {
          if(($md < 0402) || ($md == '0402' && $hms < 020000) ||           ($md > 1029) || ($md == ‘1029’ && $hms > 020000))
               $ldate = $date . ‘-05’;
          else $ldate = $date . ‘-04’;}
     if(substr($date, 0, 4) == 2007) {
          if(($md < 0311) || ($md == '0311' && $hms < 070000) ||           ($md > 1104) || ($md == ‘1104’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(substr($date, 0, 4) == 2008) {
          if(($md < 0309) || ($md == '0309' && $hms < 070000) ||           ($md > 1102) || ($md == ‘1102’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(substr($date, 0, 4) == 2009) {
          if(($md < 0308) || ($md == '0308' && $hms < 070000) ||           ($md > 1101) || ($md == ‘1101’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(substr($date, 0, 4) == 2010) {
          if(($md < 0314) || ($md == '0314' && $hms < 070000) ||           ($md > 1107) || ($md == ‘1107’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(substr($date, 0, 4) == 2011) {
          if(($md < 0313) || ($md == '0313' && $hms < 070000) ||           ($md > 1106) || ($md == ‘1106’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(substr($date, 0, 4) == 2012) {
          if(($md < 0311) || ($md == '0311' && $hms < 070000) ||           ($md > 1104) || ($md == ‘1104’ && $hms > 070000))
               $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 18000)) . ‘-05’;
          else     $ldate = date(“Y-m-dTH:i:s”,
               (strtotime($date) – 14400)) . ‘-04’;}
     if(preg_match(“///”, $focal, $m)) {$pieces = explode(‘/’, $focal);
          $focal = intval($pieces[‘0’])/intval($pieces[‘1’]);}
     if(preg_match(“///”, $shutter, $m)) {$pieces = explode(‘/’, $shutter);
          $shutter = ‘1/’ . round($pieces[‘1’]/$pieces[‘0’]);}
     echo $shutter . ‘, ‘ . $fnum . ‘, ‘ . $focal . ‘mm, ISO’ . $iso .
          ‘, ‘ . $ldate . ‘, ‘ . $id . “n”;}

Those were tough to write. PHP’s native functions for calculating the size of files believe that a kilobyte is 1024 bytes and a megabyte is 1024*1024 bytes, which is completely false and unacceptable. I had to write my own function to calculate proper file sizes. I take all my pictures with my clock set to Greenwich Mean Time, but I still want to display the time in local time (Eastern) with the GMT offset. I couldn’t figure out how to write a generic function, so I just did it for each year up until 2012, using the United State’s Daylight Saving Time rules. I’ll have to update the function in 2013, but I hear the world is going to end in 2012 anyway.

If you think this adds to my page load time, you’re probably right. But I use W3 Total Cache to completely cache each page of my blog, so it doesn’t matter.

Next summer, I’m going to China with my Mom. I will be leaving the Eastern time zone for the first time ever. I will definitely need to update the functions above, and I will probably have to specify the time zones manually for all the photos I post from the trip. Should I worry about that now? Of course not.

WiredTree Hybrid Servers Upgraded — Game-Changer for Web Hosting Industry

WiredTree Hybrid specs

I’ve been hosting all my websites on a WiredTree Managed Hybrid VPS Server since July 2009, and the plan has not changed a bit until this week. For the past year and a half, WiredTree offered 1GB RAM, 80GB disk space, 2TB bandwidth, and 1 processor core for $99 per month. Starting Monday, this plan has changed to 2GB RAM, 100GB disk space, and 3TB bandwidth, with no increase in price. These stats are real—WiredTree does not oversell and this resources are always reserved for your account. If you aren’t using all your RAM or disk space, nobody else gets to use it—it stays unused and immediately available to you.

Hybrid Servers are actually beefed-up Virtual Private Servers—the name VPS is not used because they provide the resources of a low or medium end dedicated server without the expense. For instance, a WiredTree server might contain two 1TB hard drives, 16GB RAM, and two quad-core processors. Such a server might host 7 Hybrid Server accounts, giving each 2GB RAM, 100GB RAID10 mirrored disk space, and a processor core. The remaining core, RAM, and disk space would be used for software called Virtuozzo, which partitions the server into virtual machines. Such a powerful server may cost under $400 a month to operate—WiredTree would get almost $700 from seven different clients. While offering the server directly to clients is an option, most people do not need and cannot afford such a powerful machine. A Hybrid Server is a much better option.

Prior to this, WiredTree Hybrid Servers were not a spectacular deal—you could find faster servers at cheaper prices elsewhere. Thousands of people choose WiredTree not for their rock-bottom prices, but for their superior uptime, customer support, and reputation. You can send a support ticket to WiredTree any time of the night and you’ll get a response in under 30 minutes. While other web hosting companies make a point of hiding their phone number, you can call WiredTree toll-free at 866-523-8733 with any questions or problems. Combining 24/7 custom service with the new Hybrid Server specs, WiredTree blows all other mid-range hosting companies out of the water. This is truly a game-changing announcement.

My website gets over 1000 visitors a day, and I host other websites such as Composer’s Journey and Thripp.com on this Hybrid server too. I also host the Th8.us URL shortener here, which shortens 1.3 million URLs per month and receives over 10 million requests, including pageviews and API calls. Th8.us is not simply a URL shortener—I display Google AdSense ads on every short URL in an iFrame, which requires PHP. This alone generates over $100 per month. The Th8.us database is a single InnoDB table in a MySQL database with nearly 20 million rows. The size of the database is 3.2 gigabytes, and it’s never been corrupted. Th8.us barely makes a dent in my Hybrid server’s resources.

WiredTree provides detailed statistics about your server. Here are just a few of those from my server, for the past month:

WiredTree Hybrid stats

The above stats are with the OLD server specs. I had 1.5GB of RAM because I used the coupon code HYBRIDFREE512RAM when I signed up. The spike on August 18 is when I did a full backup of my cPanel account including all files and databases, which totaled 7GB and took only 30 minutes. I only use 150GB of bandwidth a month tops, and that’s with lots of large Canon RAW files (10MB each) hosted on my server for direct download by the public. With the new upgrades, I could use 20 times the bandwidth and still be fine.

WiredTree says all existing hybrid customers will be upgraded within a month, but if you’re a new customer, you can get a hybrid server with the new specs right now. If you use this affiliate link, I will get one month of free hosting a month after you sign up. You can register as an affiliate yourself and receive 100% of the first month of any sales as a hosting credit or 75% to your PayPal account. This could potentially pay your hosting bill every month.

WiredTree Hybrid servers run CentOS 5, a Linux distro that is the defacto standard for VPS hosting. Apache, MySQL, PHP, Perl, Python, FTP, SSH, cPanel, Web Host Manager, phpMyAdmin, Cubemail, and plenty of other stuff is available out of the box, and WiredTree technicians watch your server for problems and improve its security. You can have WordPress set up on your own domain in under an hour, and if you don’t know how, you can put in a support ticket. They’ll even transfer your files from your old host for you if you provide your usernames and passwords (they are trustworthy and I often provide them my root password).

The Radified blog has a great guide to VPS hosting, focused on WiredTree, and you can read Mr. Rad’s experience in making the switch here. It’s 2 and a half years old, but besides better specs and cheaper prices, WiredTree hasn’t changed a bit.

This is straight from the WiredTree “Grove” admin pages:

“We have made major investments easily in the tens of thousands of dollars this year upgrading our Hybrid infrastructure, retrofitting existing servers, and introducing new servers to support these increased allocations and to ensure you are getting the best value for your money.”

Though I don’t know how many clients they have, I wouldn’t be surprised if they spent $100,000 on these upgrades. RAM is cheap, but it isn’t dirt cheap, especially if you want quality. WiredTree performs automatic nightly backups of all your files and has multiple layers of redundancy, so giving a customer 20GB extra disk space might require 100GB extra disk space in their infrastructure. Ditto for RAM and bandwidth.

WiredTree has disabled the coupon codes HYBRIDFREE512RAM and 10PERCENTOFF on the new Hybrid plans, presumably because the prices are already rock-bottom, but you can still receive a 5% discount by paying for a year in advance. This is what I do, and I will be paying them $1128.60 in February unless I get a bunch of affiliate commissions. Start off paying by the month until you find out how special WiredTree is.

Unless you have a large, successful website already, you should start out with a Virtual Private Server ($49 per month) or even shared hosting, which WiredTree does not provide. At the VPS level, there are many WiredTree coupons available—don’t sign up without one.

You can host an unlimited number of websites on all WiredTree’s plans. Your only limits are how much RAM, disk space, bandwidth, and CPU power these sites consume. If you have a bunch of small sites, there’s no reason to pay a hosting bill for each one. Consolidate them under one VPS plan. You can even use your own DNS name servers like NS1.THRIPP.COM and NS2.THRIPP.COM.

While the new Hybrid Servers were announced two days ago, I couldn’t find any articles in the blogosphere about this ground-breaking change—just verbatim copies of the press release. I’m sure this will change before the week ends, and WiredTree will become one of the world leaders in web hosting. They may even need to open a second data-center outside of Chicago.

Check out WiredTree today. Even if you already have a good web host, WiredTree is better.

Tweet This 1.7 Released

Today I released Tweet This 1.7, the first update to my WordPress plugin in nearly a year. This version adds OAuth support, and it is an important upgrade because Twitter will be disabling basic authentication tomorrow, Aug. 31, 2010. I’ve also added support for the Bit.ly API and fixed many bugs, including problems with automatic tweeting.

Tweet This 1.7 is also available on the WordPress plugin repository and it has been downloaded 100 times since I released it an hour ago. Twitter requires each user of Tweet This to fill out a lengthy and complex application registration form and then copy and paste four API keys to the Tweet This settings, all of which long and confusing, like “5151540-ADGJeaa-dgaiojt-3ugeaei-ghq75gj-dwerty.” Even Alex King, creator of Twitter Tools, has complained about it. Unfortunately, it’s my only option, so I’ve included detailed instructions on the Tweet This options page.

Please leave me feedback and bug reports, as I will be actively developing Tweet This over the coming months.

The New Thripp.com

I’ve been absent from blogging lately, but the past two days I’ve been working on programming the new Thripp.com, a photography community. You can sign up there and upload your best photos to a gallery so other members can comment on them. The new Thripp.com replaces the old WordPress MU blogging service, and I deleted all the blogs and accounts I deemed as spam. The old Thripp.com (this) is closed to new registrations, and the 80 blogs it has will remain in place.

I posted 34 photos to Thripp.com. You can comment on them and other users’ profiles, there’s a page that shows all the comments you’ve received, and you can choose your own display name. Please sign up and post your best photos.

New Thripp.com

New Thripp.com

New Thripp.com

How to Create a Public Library

I’ve disappeared for the last few days because I’ve been working on The Thripp Public Library. While I can’t open it to the public yet (Dad won’t open our house to the world), I’m working on it now because I have time off and Dad’s generously donated lots of his books. Though I wanted to use Evergreen or Koha, I picked the simple and obscure OpenBiblio as my library system, because it’s the only thing I could find that would run on shared hosting. I was disappointed by the lack of features to start, but I’m starting to like the power and control with it, especially since the database makes sense, so adding new features is easy.

Before I even got started, the first step was to choose a barcoding format, classification system, and spine labeling format.

I don’t like Library of Congress (LC) classification because it’s arcane and confusing, so the Dewical Decimal system (DDC) was the default choice. But what to use it for? You can use it for everything, but I decided right away not to use it on fiction items, instead opting for “FICTION / *last name*” as the spine label and call number, which is the same as the Volusia County library system. It may not be ideal, but it’s much easier to use. Biographies are tougher. I chose DDC, but I put two lines above that say “BIO / *subject last name”. The DDC part is always “*numbers* *first three letters of the author’s last name*”. Large type items get “LT” at the top of the spine label. Spine labels == call numbers, always. I created a template file for spine labels in OpenOffice.org Writer, which I add to as I catalog books. Then when I get to twenty or thirty, I print out the whole sheet and start cutting them out with scissors and taping them to the books. Here’s part of a recent sheet:

Lots of spine labels

The numbers after the dot are sometimes six or more in Dewey Decimal classification. You can truncate (cut off) them, but I let them stay and just wrap the label around the book.

The next big step is barcoding. I don’t have a reader yet (too expensive), but it’s good to be ready ahead of time, and it gives a unique identifier for each item. Many libraries, including Volusia County, follow the format F LLLL XXXX XXXXC, where F is the flag, 2 for patrons or 3 for items, LLLL is the system identifier (2417 for Volusia), XXXX XXXX is an incremented number, and C is the check digit. The symbology is Codabar. This is nice, but the institutional identifiers are only useful if your part of a larger organization. I can’t find who oversees them, nor a list of each county / system and it’s code. If I picked one for my library, no one would respect it, plus it makes the barcodes unnecessarily long. So “normal” barcodes are out.

Codabar is old, and there’s no reason to use it if you aren’t follow the 14-digit format. So I picked the more robust Code 39. I looked in vain for open-source or free software that makes it easy to print up sheets of barcodes while generating the check digits, but gave up in disgust. I’m just doing it in OpenOffice.org Writer with this free Code 39 font. No check digits. My barcodes are only eight digits, and check digits aren’t needed anyway because the symbology is self-checking (so I’m told). Plus, forgoing check digits makes things much easier. I have a template of 5 pages with 20 barcodes each, numbered 1-100. When I want to print new ones, I can just find and replace every instance of “300300” with “300301” and it’s all done instantly. Here’s what that template looks like:

Lots of barcodes

This is awesomely cool, and it is a robust solution, even if it seems too simple. I invite you to use this template to print similar barcodes. Make sure you have the font installed first, as this .odt document expects it. I just print these on card stock, cut them out with scissors, and affix them to the books with clear packing tape. I thought I had to decide to put the barcodes on the outside of the books or the inside… but I have the best of both worlds! I printed two copies of the template and put a barcode in both places. This way, I have the convenience of outside barcodes, but my items can still be identified if the barcodes peel off or the covers are destroyed. Clear tape over the barcode won’t matter for any scanner worth its salt. These barcodes are big and easy-to-scan too. I don’t know why most libraries use ones so small.

My numbering format is eight digits; two groups of four. The font doesn’t allow a space between them, but they are visually separated by the zeroes. I start patrons with 2 and items with 3 like traditional library barcodes. The format is 2001XXXX for patrons and 3003XXXX for items. So far I have 5 patrons and 81 items, so the highest patron barcode is 20010005, and for items, 30030081. When I get to 9999, I’ll go up to 2005 and 3007. There’s no reason the last 7 digits of patrons vs. items need to ever clash, and no overlap makes it easy to use abbreviated barcodes for internal memos.

A record number is created by OpenBiblio when you add an item. It’s just a numeric counter starting at one. The record number is used in OpenBiblio’s URLs, and is an easy identifier for my patrons to communicate to me. Barcodes are good too, but the problem is that they are transient, while record numbers are static (as long as I don’t delete the record). Also, the record number stands for the whole record, while a barcode is just for one item. There could be 10 copies of one item, but there still is one record number. So it makes sense to divorce barcodes from record numbers.

Of course, since I’m printing barcodes myself on a home laser printer, there’s no reason for barcodes to be transient. If a patron loses his library card, I just print up a new one on the spot with the same barcode. Same for damaged barcodes on books. But I can also replace the code with a new one if that’s quicker or easier for me, if I use record numbers as the unique identifier for the record (instead of the first barcode or nothing). While OpenBiblio makes similar numbers for patrons, mainly to distinguish them in the MySQL tables, I see no reason to use them. Each patron will only ever have one barcode. Even if I offer keychain library cards, they’ll have the same numbers as the big versions.

Before we go on to cataloging, let’s take a look at library cards. I took what I learned from item barcodes and applied it here. I made a cut-out template in my graphics software, which I’m using to overly text onto in OpenOffice.org. A sheet of library cards looks like this:

Lots of library cards

Here’s the library card template (font required). To change things, I right-click the background image, click Wrap > No Wrap, go to page 2 and change the numbers or other text, then go back to page one, right-click the image again, choose Wrap > In Background, then print. This is in OpenOffice.org 2.2.0. I haven’t upgraded to the new version, but it should be the same. To use them, I print them on card stock (a.k.a. matte photo paper), cut them out with scissors, then laminate them with packing tape (carefully). Of course I enter them into OpenBiblio too. I created custom fields under Admin > Member Fields, for alternate library cards, driver’s licenses, notes, and dates of birth. Those look like this:

OpenBiblio custom fields

The logic with the alternate cards is this: a patron can get a card and add his Volusia and/or Flagler cards, so he can use those instead if he forgets his Thripp card. If he does that, I look him up by name because OpenBiblio won’t allow searching by custom fields (I may fix this later), and then approve the alternate card if the numbers match. Primary Card Origin is different; a patron can choose to use only his Volusia, Flagler, or other library card to identify himself in the Thripp system, in which case I can use the built-in barcode search on the third-party card, and the patron doesn’t even need a Thripp card. In that case, I type in the origin of the primary card in that special field (i.e. “Volusia”, “Flagler”, etc.).

Now that we have labels, barcodes, and cards out of the way, the next step is cataloging. I didn’t want to do all the work myself; instead I get some of the cataloging data from the Library of Congress. The problem with this is the LoC Lookup Patch won’t work with SYN Hosting because they won’t enable the PHP YAZ module because of security concerns. I tried the alternate LoC SRU, but I get this error:

Warning: fsockopen() [function.fsockopen]: unable to connect to z3950.loc.gov:7090 (Permission denied) in /home/richardx/public_html/lib/catalog/locsru_search.php on line 98

Notice: Socket error Permission denied (13) in /home/richardx/public_html/lib/catalog/locsru_search.php on line 125

I gave up on direct import and went to USMARC files. This was hard to figure out. The way to do it is to get MarcEdit 5.1. In the software, go to Add-ins > MarcEdit Z39.50/SRU Client, go to Modify Databases, click Add Database > Import from Master List, click the second in the list (Library of Congress), click Select Resource, go back to Search Mode, click Select Database, double-click Library of Congress, search for something, and double-click the item you want to import from the results. Then, click Download Record. Rinse and repeat, using the “Append” option. After you have, say, 30 records, go to Cataloging > Upload Marc Data back in OpenBiblio, and upload the files. It should say that 30 records are added, and then you can polish the data by searching and editing under Cataloging > Bibliography Search, book by book.

I edit the data to format the title my way, clear junk from the ISBN field, make the extent field a page counter, add the cover price as cost, and create the call number based on the Dewey Decimal code. Unfortunately there’s a lot of junk like “BOOKS” and “Copy 1” in the LoC Marc records, and I haven’t found a way to filter them out. I went through the MySQL database and cleared a lot of them recently.

This still saves me a lot of time, because coming up with the information myself would be too much work. The search doesn’t work well. If ISBN fails, I try title or author, or I go to the online search and get the control number to search by as “Record Number” in MarcEdit.

Sometimes there is no Dewey Decimal classification; just Library of Congress. I hunt down the item at another library in WorldCat to see what dot code they used. It’s easier than coming up with it on my own, and I wouldn’t get it right anyway.

As an alternate for when the Library of Congress has nothing, I installed the Amazon Lookup Module, which actually works. It just gets a few things like title, page count, publication date, author, and sometimes DDC code, but it helps.

Note that when I say “install,” this isn’t your typical WordPress plugin installation. This is getting down and dirty adding and editing pieces of code. Some modules are even distributed as hard-to-use-on-Windows .patch files, which scamper about editing two-dozen files in the OpenBiblio core. I’ve changed so much stuff, that when I upgrade to the next version, I’ll be merging the author’s changes with mine rather than mine with the author’s. It’s a completely different mindset.

Cataloging constructs are yours to set. Unfortunately, it’s rather inflexible because you have to work on a per-record basis, but this is expected with ILS’s. The standards are lower than with photo-cataloging software, because librarianship is considered a full-time job (time is cheap) and most libraries have fewer books than I have photos. I try to get things right the first time, meaning I use consistent formatting like putting a period at the end of the extended title and after the author’s name, keeping the ISBN field clean, using consistent capitalization, etc. This is a typical record. I let a lot of the Library of Congress’ stuff stay the same to save time, but the fields that I’m picky about are the ones that are shown in search results. Speaking of which…

This is the default search system:

OpenBiblio old search

And this is mine:

OpenBiblio new search

And mine has this, too:

OpenBiblio scoping

I realize no one knows what “federated” and “scoping” mean, but they’re such cool words I don’t care. You’ll learn it if you use my OPAC. OPAC stands for online public access catalog, for those of you not familiar with LIS jargon. LIS is library information science, and the OPAC is part of the ILS, or integrated library system. For scoping and federated search, I applied the Advanced Search by Title, Collection, Material Type .patch file using TortiseSVN, then modified it to fit my needs by removing the material type field (because it’s the same as collections in my library), changing “Search All” to “Federated,” and adding barcode and call-number search to it. This is an easy MySQL query addition, because the barcodes and call numbers are stored right in the biblio table in the database.

I like how the search works, because it matches partial words. I can type in “mel” as an author search, and I’ll get Typee by Herman Melville, which is the only book by him in my catalog right now. The Call Number search is good because I put useful data in the call number: “LT” for large type, “BIO” for biographies, “J DVD” for kids’ movies, “FICTION” for fiction, etc. So you can search by it. I haven’t figured out how to implement boolean queries yet, but what I have is pretty good.

This is the code behind the search. It’s in opac/index.php and shared/biblio_search.php, to be displayed below the search results so you can search again right from there. If you use it, do it after applying the patch I mentioned. This ASSUMES that you’re using mod_rewrite to change shared/biblio_search.php to search-results. Change action=”../search-results” to action=”../shared/biblio_search.php” on line 1 if the assumption is false.

<form name=”phrasesearch” method=”POST” action=”../search-results”>
<table class=”primary”><tr><th valign=”top” nowrap=”yes” align=”left”>Search the Catalog</td></tr><tr><td nowrap=”true” class=”primary”><select name=”searchType”>
<option value=”all” selected>Federated
<option value=”title”>Title
<option value=”author”>Author
<option value=”subject”>Subjects
<option value=”barcodeNmbr”>Barcode
<option value=”callnmbr”>Call Number
</select>
<input type=”text” name=”searchText” size=”55″ maxlength=”256″>
<input type=”hidden” name=”sortBy” value=”default”>
<input type=”hidden” name=”tab” value=”<?php echo H($tab); ?>”>
<input type=”hidden” name=”lookup” value=”<?php echo H($lookup); ?>”>
<input type=”submit” value=”Search!” class=”button”>
</td></tr></table><br /><table class=”primary”><tr><th valign=”top” nowrap=”yes” align=”left”>Advanced: Scoping (Optional)</td></tr><tr><font class=”small”><td nowrap=”true” class=”primary”><script type=”text/javascript” language=”JavaScript”>
function selectAll(ident) { var checkBoxes = document.getElementsByName(ident); for (i = 0; i < checkBoxes.length; i++) { if (checkBoxes[i].checked == true) { checkBoxes[i].checked = false; } else { checkBoxes[i].checked = true; } } }</script>
<input type=”checkbox” name=”selectall” value=”select_all” onclick=”selectAll(‘collec[]’);”>Flip<br /><?php $dmQ = new DmQuery(); $dmQ->connect(); $dms = $dmQ->get(“collection_dm”); $dmQ->close(); foreach ($dms as $dm) { echo ‘<input type=”checkbox” value=”‘.$dm->getCode().'” name=”collec[]”> ‘.H($dm->getDescription()).”<br />n”; } ?></td></tr></font></table></form>

I compressed some of it to one line, to make it really hard to read, because I like making things harder than necessary. :cool: It makes it easy to scroll through in the file, and I shouldn’t need to change that part. If I do, I’ll just look through it carefully. It seems to make more sense to my brain than regular, fluffy code.

In shared/global_constants.php, I have this:

define(“OBIB_SEARCH_BARCODE”,”1″);
define(“OBIB_SEARCH_TITLE”,”2″);
define(“OBIB_SEARCH_AUTHOR”,”3″);
define(“OBIB_SEARCH_SUBJECT”,”4″);
define(“OBIB_SEARCH_NAME”,”5″);
define(“OBIB_SEARCH_ALL”,”6″);
define(“OBIB_SEARCH_CALLNMBR”,”7″);

The beginning of the search function in my classes/BiblioSearchQuery.php file looks like this:

function search($type, &$words, $page, $sortBy,
$collecs=array(), $materials=array(), $opacFlg=true) {
# reset stats
$this->_rowNmbr = 0;
$this->_currentRowNmbr = 0;
$this->_currentPageNmbr = $page;
$this->_rowCount = 0;
$this->_pageCount = 0;

# setting sql join clause
$join = “from biblio left join biblio_copy on biblio.bibid=biblio_copy.bibid “;

# setting sql where clause
$criteria = “”;
if ((sizeof($words) == 0) || ($words[0] == “”)) {
if ($opacFlg) $criteria = “where opac_flg = ‘Y’ “;
} else {
if ($type == OBIB_SEARCH_BARCODE) {
$criteria = $this->_getCriteria(array(“biblio_copy.barcode_nmbr”),$words);
} elseif ($type == OBIB_SEARCH_AUTHOR) {
$join .= “left join biblio_field on biblio_field.bibid=biblio.bibid ”
. “and biblio_field.tag=’700′ ”
. “and (biblio_field.subfield_cd=’a’ or biblio_field.subfield_cd=’b’) “;
$criteria = $this->_getCriteria(array(“biblio.author”,”biblio.responsibility_stmt”,”biblio_field.field_data”),$words);
} elseif ($type == OBIB_SEARCH_SUBJECT) {
$criteria = $this->_getCriteria(array(“biblio.topic1″,”biblio.topic2″,”biblio.topic3″,”biblio.topic4″,”biblio.topic5”),$words);
} elseif ($type == OBIB_SEARCH_ALL) {
$criteria =
$this->_getCriteria(array(“biblio.topic1″,”biblio.topic2″,”biblio.topic3”,
“biblio.topic4″,”biblio.topic5”,
“biblio.title”,”biblio.title_remainder”,
“biblio.author”,”biblio.responsibility_stmt”,
“biblio.call_nmbr1″,”biblio.call_nmbr2″,”biblio.call_nmbr3″,”biblio_copy.barcode_nmbr”),$words);
} elseif ($type == OBIB_SEARCH_CALLNMBR) {
$criteria = $this->_getCriteria(array(“biblio.call_nmbr1″,”biblio.call_nmbr2″,”biblio.call_nmbr3”),$words);
} else {
$criteria =
$this->_getCriteria(array(“biblio.title”,”biblio.title_remainder”),$words);
}

And finally, this is the code that interprets the posted data, in shared/biblio_search.php:

#****************************************************************************
#* Retrieving post vars and scrubbing the data
#****************************************************************************
if (isset($_POST[“page”])) {
$currentPageNmbr = $_POST[“page”];
} else {
$currentPageNmbr = 1;
}
$searchType = $_POST[“searchType”];
$sortBy = $_POST[“sortBy”];
if ($sortBy == “default”) {
if ($searchType == “author”) {
$sortBy = “author”;
} else {
$sortBy = “title”;
}
}
$searchText = trim($_POST[“searchText”]);
# remove redundant whitespace
$searchText = eregi_replace(“[[:space:]]+”, ” “, $searchText);
if ($searchType == “barcodeNmbr”) {
$sType = OBIB_SEARCH_BARCODE;
$words[] = $searchText;
} else {
$words = explodeQuoted($searchText);
if ($searchType == “author”) {
$sType = OBIB_SEARCH_AUTHOR;
} elseif ($searchType == “subject”) {
$sType = OBIB_SEARCH_SUBJECT;
} elseif ($searchType == “all”) {
$sType = OBIB_SEARCH_ALL;
} elseif ($searchType == “callnmbr”) {
$sType = OBIB_SEARCH_CALLNMBR;
} else {
$sType = OBIB_SEARCH_TITLE;
}
}

// limit search results to collections and materials
$collecs = array();
if (is_array($_POST[‘collec’])) {
foreach ($_POST[‘collec’] as $value) {
array_push($collecs, $value);
}
}
$materials = array();
if (is_array($_POST[‘material’])) {
foreach ($_POST[‘material’] as $value) {
array_push($materials, $value);
}
}

Notice that I added CALLNMBR and BARCODE search, the logic for which was enumerated in classes/BiblioSearchQuery.php. It’s a good feature for my patrons to find an on-hand item in the OPAC, and for me it’s especially helpful.

I revamped OpenBiblio’s search results. A typical result looks like this:

OpenBiblio new search result

Compare to the old style:

OpenBiblio old search result

Notice all the new stuff in the top image? The link is bold. The extended title is below. No more small text. Material is gone (my collections’ titles make it self-evident). The record number is shown. The call number is important, so it’s bolded purple, and on the same line as the collection to save space. On Shelf status is bold and green; a great visual cue. It’s not “checked in” anymore, it’s the more sensible “On Shelf”. I changed that right in the database, in the biblio_status_dm table. Most importantly, do you notice the wonderful info line? Everything you could ever want to know is right there. It’s year / ISBN / pages or minutes / cost / Amazon.com link / # of circulations. That really puts a lot of power into the hands of your patrons.

I wrote/modified the code for my set-up, so you’ll have to change some things if you want to use it. This goes in shared/biblo_search.php, above the footer. Here it is:

<tr>
<td nowrap=”true” class=”primary” valign=”top” align=”center” rowspan=”2″>
<?php echo H($biblioQ->getCurrentRowNmbr());?>.<br />
<a target=”_blank” href=”http://lib.thripp.com/<?php if ($tab == “cataloging”) echo “e/”.HURL($biblio->getBibid()); else echo HURL($biblio->getBibid());?>”>
<img src=”../images/<?php echo HURL($materialImageFiles[$biblio->getMaterialCd()]);?>” width=”20″ height=”20″ border=”0″ align=”bottom” alt=”<?php echo H($materialTypeDm[$biblio->getMaterialCd()]);?>”></a>
</td>
<td class=”primary” valign=”top” colspan=”2″>
<table class=”primary” width=”100%”>
<tr>
<td class=”noborder” width=”1%” valign=”top”><strong><?php echo $loc->getText(“biblioSearchTitle”); ?>:</strong></td>
<td class=”noborder” colspan=”3″><strong><a target=”_blank” href=”http://lib.thripp.com/<?php if ($tab == “cataloging”) echo “e/”.HURL($biblio->getBibid()); else echo HURL($biblio->getBibid());?>”><?php echo H($biblio->getTitle());?></a></strong>
<?php $bid = HURL($biblio->getBibid());
$getxtitle = mysql_query(“SELECT title_remainder FROM biblio WHERE bibid = ‘$bid'”) or die(mysql_error());
$printxtitle = mysql_fetch_row($getxtitle);
if ($printxtitle[0] == “”) echo “”; else echo “<br />$printxtitle[0]”; ?></td>
</tr>
<tr>
<td class=”noborder” valign=”top”><strong><?php echo $loc->getText(“biblioSearchAuthor”); ?>:</strong></td>
<td class=”noborder” colspan=”3″><?php if ($biblio->getAuthor() != “”) echo H($biblio->getAuthor());?></td>
</tr>
<tr>
<td class=”noborder” valign=”top” nowrap=”yes”><strong>Ref. #<?php echo HURL($biblio->getBibid()); ?>:</strong></td>
<td class=”noborder” colspan=”3″><?php echo H($collectionDm[$biblio->getCollectionCd()]);?> / <strong><font color=”#640064″><?php echo H($biblio->getCallNmbr1().” “.$biblio->getCallNmbr2().” “.$biblio->getCallNmbr3()); ?></font></strong></td></tr><tr><td class=”noborder” valign=”top”><strong>Info:</strong></td><td class=”noborder” colspan=”3″>

<?php // RXT 20080723 code:
$bid = HURL($biblio->getBibid());
$getyear = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ‘260’ AND subfield_cd = ‘c’ AND bibid = ‘$bid'”)
or die(mysql_error());
$getisbn = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ’20’ AND subfield_cd = ‘a’ AND bibid = ‘$bid'”)
or die(mysql_error());
$getpages = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ‘300’ AND subfield_cd = ‘a’ AND bibid = ‘$bid'”)
or die(mysql_error());
$getminutes = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ’20’ AND subfield_cd = ‘c’ AND bibid = ‘$bid'”)
or die(mysql_error());
$getcost = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ‘541’ AND subfield_cd = ‘h’ AND bibid = ‘$bid'”)
or die(mysql_error());
$getamz = mysql_query(“SELECT field_data FROM biblio_field WHERE tag = ‘970’ AND subfield_cd = ‘a’ AND bibid = ‘$bid'”)
or die(mysql_error());
$printyear = mysql_fetch_row($getyear);
$printisbn = mysql_fetch_row($getisbn);
$printpages = mysql_fetch_row($getpages);
$printminutes = mysql_fetch_row($getminutes);
$printcost = mysql_fetch_row($getcost);
$printamz = mysql_fetch_row($getamz);
if ($printyear == “”) echo “Year Unknown”;
else echo $printyear[0];
echo ” / “;
if ($printisbn == “”) echo “ISBN Unavailable”;
else echo “ISBN: “.$printisbn[0];
if ($printpages == “”) echo “”;
else echo ” / “.$printpages[0];
if ($printminutes == “”) echo “”;
else echo ” / “.$printminutes[0];
echo ” / $”.$printcost[0];
if ($printamz == “Not on Amazon.com”) echo “”;
elseif ($printamz != “”) echo ” / <a target=”_blank” href=”http://www.amazon.com/exec/obidos/ASIN/”.$printamz[0].”/brilliaphotog-20″ title=”See this item on Amazon.com”>Amazon.com</a> / “;
elseif ($printisbn != “”) echo ” / <a target=”_blank” href=”http://www.amazon.com/exec/obidos/ASIN/”.$printisbn[0].”/brilliaphotog-20″ title=”See this item on Amazon.com”>Amazon.com</a> / “;
else echo ” / “;
$getCircs = mysql_query(“SELECT COUNT(bibid) FROM biblio_status_hist WHERE bibid = ‘$bid'”) or die(mysql_error());
$getCircsRes = mysql_fetch_row($getCircs); if ($getCircsRes[0] == ‘1’) echo ” (1 circ)”; else echo ” ($getCircsRes[0] circs)”; ?>

</td></tr></table></td></tr>
<?php
if ($biblio->getBarcodeNmbr() != “”) {
?>
<tr>
<td class=”primary” ><strong><?php echo $loc->getText(“biblioSearchCopyBCode”); ?></strong>: <?php echo H($biblio->getBarcodeNmbr());?>
<?php if ($lookup == ‘Y’) { ?>
<a href=”javascript:returnLookup(‘barcodesearch’,’barcodeNmbr’,'<?php echo H(addslashes($biblio->getBarcodeNmbr()));?>’)”><?php echo $loc->getText(“biblioSearchOutIn”); ?></a> | <a href=”javascript:returnLookup(‘holdForm’,’holdBarcodeNmbr’,'<?php echo H(addslashes($biblio->getBarcodeNmbr()));?>’)”><?php echo $loc->getText(“biblioSearchHold”); ?></a>
<?php } ?>
</td>
<td class=”primary” ><strong><?php echo $loc->getText(“biblioSearchCopyStatus”); ?></strong>: <?php $status = H($biblioStatusDm[$biblio->getStatusCd()]); if ($status == ‘On Shelf’) echo “<strong><font color=”#009900″>On Shelf</font></strong>”; elseif ($status == ‘On the Shelving Cart’) echo “<strong><font color=”#FF8000″>On the Shelving Cart</font></strong>”; else echo “<strong><font color=”#FF0000″>$status</font></strong>”; ?></td>
</tr>
<?php } else { ?>
<tr>
<td class=”primary” colspan=”2″ ><?php echo $loc->getText(“biblioSearchNoCopies”); ?></td>
</tr>
<?php
}
}
}
$biblioQ->close();
?>
</table><br />
<?php printResultPages($loc, $currentPageNmbr, $biblioQ->getPageCount(), $sortBy); ?>

This code assumes you’re using mod_rewrite to create friendly permalinks, in the format of YOURSITE/BIBID and YOURSITE/e/BIBID for the cataloging section. I’ll tell you how later in the article. It also assumes your site is http://lib.thripp.com and your Amazon.com affiliate code is brilliaphotog-20. Change the first one definitely. Leave the second alone if you want to donate to me. :cool:

This code also makes the assumption that your using my cataloging methods I defined earlier (year field, ISBN, pages are clean, etc.). For DVDs to show the minute count instead of pages, the number MUST be in “Terms of availability:”, which is the field I chose. You can change it easily if you examine the database structure and modify the code to fit your methods. The code defines the Amazon.com ASIN as the text in the ISBN field of the record, so your ISBN fields MUST be ISBN-10 and MUST be clean (no “(pbk.)” after the numbers). I’ve defined a contingency method: create a Marc field with tag 970, subfield a, with the ASIN if it differs from the ISBN or there is no ISBN. That will be used instead. If you enter “Not on Amazon.com” as the Marc field, no Amazon.com link will show even if there is an ISBN.

“On Shelf” statuses are shown in bold green, “On the Shelving Cart” is bold orange, and everything else is bold red. Make sure to change the statuses to those in the biblio_status_dm table, or do the opposite in the code. The number of circs includes checkouts and renewals for all copies attached to the record, past and present. Links open in new windows (I added target=”blank”). This is because the search results page uses POST data instead of URL parameters, so opening in the same window and clicking back prompts a warning. I wish it used URL parameters instead.

I like my search results format. It’s a lot more useful than what I see at most libraries.

I also upgraded shared/biblio_view.php to this:

OpenBiblio new item view

A typical record before would be this:

OpenBiblio old item view

Examples for the old style are from the Frances D. Still Learning Center OPAC. They use a stock OpenBiblio install. Nearing 3000 items. OpenBiblio scales failry well.

The code for the record view page is mostly copied from the search results page, so I won’t copy it for brevity.

I created a robust statistics system on the home page, which goes around aggregating numbers in the database so that it’s always up-to-date. It has one huge flaw: it assumes each record has one and only one item. Mine do, so it isn’t a problem, but I’ll have to re-work it when that changes.

The code requires you to create config.php and global.php in the OpenBiblio root. config.php should look like this:

<?php unset($config);
$config = array();
$config[‘db_hostname’] = “localhost”;
$config[‘db_port’] = “3306”;
$config[‘db_username’] = “yourDBusername”;
$config[‘db_password’] = “yourDBpassword”;
$config[‘db_name’] = “yourDBname”; ?>

Replace the database details with your own above, then make global.php exactly as below:

<?php function db_connect() { global $config; mysql_connect($config[‘db_hostname’].”:”.$config[‘db_port’], $config[‘db_username’], $config[‘db_password’]) or die(mysql_error()); mysql_select_db($config[‘db_name’]) or die(mysql_error()); } ?>

Finally, this huge block powers the statistics:

<p>
<?php require(“../config.php”); require(“../global.php”); db_connect();
$cCount = mysql_query(“SELECT COUNT(copyid) FROM biblio_copy”) or die(mysql_error());
$cCountRes = mysql_fetch_row($cCount);

$cPrice = mysql_query(“SELECT SUM(field_data) FROM biblio_field WHERE tag = ‘541’”) or die(mysql_error());
$cPriceRes = mysql_fetch_row($cPrice);

$cPages = mysql_query(“SELECT SUM(field_data) FROM biblio_field WHERE tag = ‘300’ AND subfield_cd = ‘a'”);
$cPagesRes = mysql_fetch_row($cPages);

$cNonFic = mysql_query(“SELECT COUNT(bibid) FROM biblio WHERE collection_cd = ‘2’”);
$cNonFicRes = mysql_fetch_row($cNonFic);

$cFic = mysql_query(“SELECT COUNT(bibid) FROM biblio WHERE collection_cd = ‘1’”);
$cFicRes = mysql_fetch_row($cFic);

$cDVDs = mysql_query(“SELECT COUNT(bibid) FROM biblio WHERE collection_cd = ’12′”);
$cDVDsRes = mysql_fetch_row($cDVDs);

$cOut = mysql_query(“SELECT COUNT(status_cd) FROM biblio_copy WHERE status_cd = ‘out'”) or die(mysql_error());
$cOutRes = mysql_fetch_row($cOut);

$cIn = mysql_query(“SELECT COUNT(status_cd) FROM biblio_copy WHERE status_cd = ‘in'”) or die(mysql_error());
$cInRes = mysql_fetch_row($cIn);

$circOuts = mysql_query(“SELECT COUNT(copyid) FROM biblio_status_hist WHERE status_cd = ‘out'”) or die(mysql_error());
$circOutsRes = mysql_fetch_row($circOuts);

$circRens = mysql_query(“SELECT COUNT(copyid) FROM biblio_status_hist WHERE status_cd = ‘crt'”) or die(mysql_error());
$circRensRes = mysql_fetch_row($circRens);

$circTotal = mysql_query(“SELECT COUNT(copyid) FROM biblio_status_hist”) or die(mysql_error());
$circTotalRes = mysql_fetch_row($circTotal);

$pTotal = mysql_query(“SELECT COUNT(mbrid) FROM member”) or die(mysql_error());
$pTotalRes = mysql_fetch_row($pTotal);

$pTotalAdults = mysql_query(“SELECT COUNT(classification) FROM member WHERE classification = ‘1’”) or die(mysql_error());
$pTotalAdultsRes = mysql_fetch_row($pTotalAdults);

$pTotalChildren = mysql_query(“SELECT COUNT(classification) FROM member WHERE classification = ‘2’”) or die(mysql_error());
$pTotalChildrenRes = mysql_fetch_row($pTotalChildren);

$pPhones = mysql_query(“SELECT COUNT(mbrid) FROM member WHERE home_phone != ””) or die(mysql_error());
$pPhonesRes = mysql_fetch_row($pPhones);

$pEmails = mysql_query(“SELECT COUNT(mbrid) FROM member WHERE email != ””) or die(mysql_error());
$pEmailsRes = mysql_fetch_row($pEmails);

$cAllBooks = ($cFicRes[0]+$cNonFicRes[0]);

echo “<strong>Live Statistics:</strong><br />

The Thripp Public Library has <strong>$pTotalRes[0]</strong> patrons: <strong>$pTotalAdultsRes[0]</strong> adults and <strong>$pTotalChildrenRes[0]</strong> children.<br />

There are <strong>”.$cCountRes[0].”</strong> items: <strong>”.$cNonFicRes[0].”</strong> nonfiction books, <strong>”.$cFicRes[0].”</strong> fiction books, and <strong>”.$cDVDsRes[0].”</strong> DVDs.<br />

Stats: Nonfiction books: <strong>”; printf (“%01.2f”,(($cNonFicRes[0]/$cCountRes[0])*100)); echo “%</strong>; Fiction books: <strong>”; printf (“%01.2f”,(($cFicRes[0]/$cCountRes[0])*100)); echo “%</strong>; DVDs: <strong>”; printf (“%01.2f”,(($cDVDsRes[0]/$cCountRes[0])*100)); echo “%</strong>.<br />

<strong>”; if ($cOutRes[0] == ‘1’) echo “1</strong> item is”; else echo “$cOutRes[0]</strong> items are”; echo ” checked out and <strong>”.$cInRes[0].”</strong> are on shelf.<br />

<strong>”; printf (“%01.2f”,(($cOutRes[0]/$cCountRes[0])*100)); echo “%</strong> of the catalog is checked out. The average patron has <strong>”; printf (“%01.2f”,($cOutRes[0]/$pTotalRes[0])); echo “</strong> items out.<br />

There are <strong>”; printf (“%01.2f”,($cCountRes[0]/$pTotalRes[0])); echo “</strong> items for every <strong>1</strong> patron.<br />

The collection is worth <strong>$$cPriceRes[0]</strong>, or about <strong>$”; printf (“%01.2f”,($cPriceRes[0]/$cCountRes[0])); echo “</strong> per item.<br />

There have been <strong>$circOutsRes[0]</strong> checkouts and <strong>$circRensRes[0]</strong> renewals; a total of <strong>$circTotalRes[0]</strong>.<br />

&nbsp;&nbsp;&nbsp; This is an average of <strong>”; printf (“%01.2f”,($circTotalRes[0]/$pTotalRes[0])); echo “</strong> per patron, or <strong>”; printf (“%01.2f”,($circTotalRes[0]/$cCountRes[0])); echo “</strong> per item.<br />

There are <strong>”.$cAllBooks.”</strong> books with <strong>”. number_format($cPagesRes[0]).”</strong> pages. The average book has <strong>”; printf (“%01.2f”,($cPagesRes[0]/($cFicRes[0]+$cNonFicRes[0]))); echo “</strong> pages.<br />

The ratio of fiction to nonfiction books is <strong>”; printf (“%01.2f”,(($cFicRes[0]/$cNonFicRes[0])*100)); echo “%</strong>.<br />

The collection represents <strong>$”; printf (“%01.2f”,($cPriceRes[0]/$pTotalRes[0])); echo “</strong> of value per patron.<br />

<strong>”; printf (“%01.2f”,(($pPhonesRes[0]/$pTotalRes[0])*100)); echo “%</strong> of my patrons have telephones and <strong>”; printf (“%01.2f”,(($pEmailsRes[0]/$pTotalRes[0])*100)); echo “%</strong> have email accounts.”;

?>
</p>

I do know this is all over the place, it’s a mess, it’s inefficient, and it probably should all be cached. It’s working great, so I’ll cross the “I have to fix this now!” bridge when I come to it. Feeding the library address into this speed test, it’s 0.4 seconds; the same as thripp.com which is totally cached. That might slow down as the database gets bigger, but for now it’s fine.

You can use this code for your OpenBiblio site, by adding it to opac/index.php, but you have to change some stuff. Notice “collection_cd” and “classification”? The arguments there are hard-coded for my database, so change them for yours. Otherwise, you have to make sure to enter the cost for each item, and the number of pages as “###” or “### pages” in “Physical description (Extent):” and nothing else. If you do this, it’s cool because you get the total number of pages in your library.

Right now, the stats look like this:

Live Statistics:
The Thripp Public Library has 5 patrons: 3 adults and 2 children.
There are 81 items: 63 nonfiction books, 15 fiction books, and 3 DVDs.
Stats: Nonfiction books: 77.78%; Fiction books: 18.52%; DVDs: 3.70%.
2 items are checked out and 79 are on shelf.
2.47% of the catalog is checked out. The average patron has 0.40 items out.
There are 16.20 items for every 1 patron.
The collection is worth $1681.82, or about $20.76 per item.
There have been 19 checkouts and 15 renewals; a total of 34.
    This is an average of 6.80 per patron, or 0.42 per item.
There are 78 books with 29,786 pages. The average book has 381.87 pages.
The ratio of fiction to nonfiction books is 23.81%.
The collection represents $336.36 of value per patron.
100.00% of my patrons have telephones and 100.00% have email accounts.

It’s amazing what computers can do, no? In the print age, or even in an out-of-the-box ILS, it would take hours to compile this report, and you’d have to do it every time something changed. If anyone made reports like this, it would be once a month at best, and still it would be a great drudgery and expense. Not so anymore.

The stats are a bit messed up when you have records you’ve just imported from the Library of Congress, but not processed. Nothing fatal; the numbers are just wrong. Once you’ve done all the cataloging, it’s fine, though.

A good OPAC needs good URLs. My URLs are like lib.thripp.com/93. No titles in the URLs keeps them nice and simple-to-implement. I’m using Linux + Apache, so fixing this is easy. Let me show you my .htaccess file:


RewriteEngine On
RewriteBase /
RewriteRule ^([0-9]+)$ shared/biblio_view.php?bibid=$1&tab=opac [NC]
RewriteRule ^e/([0-9]+)$ shared/biblio_view.php?bibid=$1&tab=cataloging [NC]
RewriteRule ^$ opac/index.php [L,NC]
RewriteRule ^search-results shared/biblio_search.php [L,NC]
RewriteCond %{HTTP_HOST} www.lib.thripp.com$ [NC]
RewriteRule ^(.*)$ http://lib.thripp.com/$1 [L,R=301]

That does all the magic. Change http://lib.thripp.com, of course. This requires changes in opac/index.php and shared/biblio_search.php to match. If you’ve used my code in this article, it’s already done, though. Now, you can jump from a regular view to the view with the edit links by adding “e/” before the bibid. And the OPAC is mapped to the root instead of home/index.php, which is cluttered and has a lot of staff functions. I don’t know why it’s the default home page. Anyway, after the change you can still get there by URL by adding “/home” to your URL. That’s the best way, because not having a link to the staff area from the home page is a bit of security through obscurity.

I’m happy to have started my library, despite being just for family and friends for now. I need to buy a house or a warehouse to host it at, and then open it up to the world. OpenBiblio has good facilities for checkouts, renewals, limits, fines, and even receipt printing, and I feel I can build upon them through my own programming, so on the tech site I’m ready. I plan to have a lending library with a limit of 5 items out per person at a time. You can renew and place holds, but only by phone or by coming in (this is OpenBiblio’s limitation; other OPACs have these features). You get three weeks on everything, except DVDs which are one week. Late fees are 15 cents per item per day.

It might be 5 years before I’m making enough money from advertising on this website to find a space to open the library. I’m not too worried. It’s better to start now than to start later. Here’s a photo of the stacks now:

The Thripp Library shelves

These books displaced my photos, but it’s worth it. I used to have stacks of photos on these shelves, but I crammed them in with other photos on my other bookshelf to make way for the library. If you’re a good friend, feel free to come over to my house, get a library card, and check something out. Make a donation to get this off the ground. If you make a donation, you’re a good friend.

I’m seeing a big gap between what public libraries are… and what they could be. There are no charismatic leaders in librarianship. Most everyone is dull and unoriginal; even the software and basics like online catalogs need lots of work. This is because most libraries are government-funded; even many academic branches are not immune. So the strategy is “let’s waste as much money as possible and leech from the taxpayers,” not “let’s work efficiently and make a real contribution to the community.” More frighteningly, libraries are becoming elitist, discarding old, unpopular, or “offensive” books and rejecting self-published books or anything without an ISBN number. I’ve written a mission statement for the Thripp Public Library to address this:

The Thripp Public Library is founded on a healthy attitude of dissension and skepticism, a distaste for lies and fallacy, and a love of learning from history. To know history and avoid 1984-style revisionism, it’s important to keep old books around. Unfortunately the Volusia County library system doesn’t do this, as I’ve gotten many of my library’s gems right from their book sales. These are my library’s universal principles:

1. Timelessness eschews popularity.
2. The message trumps the medium.
3. Truth is independent of source.

This means that good information can come from any person or organization in any form, be it a book, magazine, CD, DVD, website, etc. I’ve founded the library on timelessness, meaning that I refuse to destroy parts of the collection that are rarely looked at, because they are often the most important. Popular movies circulate more, but are fleeting and unscholarly. I’ll include them if they’re cheap and terribly entertaining, though.

Here’s an example item, with the Thripp barcode:

Thomas Jefferson book

And here’s what a Thripp library card looks like:

Richard X. Thripp's Thripp library card

Cataloging is on hold till I go to the store, because I’ve run out of clear packing tape to affix barcodes and spine labels.

Take a look at the catalog. Just click Search! to see everything. Then come back and tell me you’re not impressed. :wink2: