Monday, August 24, 2009

Here's something I threw together the other day to help me organize my ripped audio books.  I have several audio books (Harry Potter, LOTR, Dresden Files, etc) that I've been ripping to file so that I could gather an entire 10-20 CD set onto a single disc for easy listening while driving without needing to switch discs.  I like to use WMA files for easy ripping (via Media Player) and the small footprint.

I've found, much to my chagrin, that the information retrieved online for these books (via CDDB, FreeDB, or All Music Guide (which WMP uses)) is unreliable, frequently contains typos and misspellings, and is extremely inconsistent with respect to formatting and convention.

I found that when I inserted a CD I would spend a good amount of time entering the book name, the genre, the year it was recorded, the author, and the performer, and I'd take the time to rename the tracks.  After a handful of CDs this was almost unbearable and extremely tedious.

I therefore set out to write a script that would do this, updating all of the ID3 tags after the fact.  The only thing I would need to concern myself with was that the book name (I like the form Book Name - Disc XX) was correct.  This script belows makes a few assumptions about the folder and file names:

  • each folder contains a single disk and that they are named according to the pattern "Book Name - Disc XX"
  • all books will be collected into a single folder with the name pattern "Book Set - Book Name"

For example, I ripped a book by Jim Butcher named Small Favor.  Each disc went into a folder named "Small Favor - Disc 01", "Small Favor - Disc 02", etc.  I would then collect all the ripped files into a single directory named "Dresden Files - Book 10 - Small Favor".  This way, when all was said and done, I'd have all the books in order and grouped.

I decided to use PowerShell as the language of preference for automating this procedure.  Not only does it provide a very powerful scripting environment, but I can also leverage the .NET Framework (which I love dearly).  As such, I was able to take advantage of TagLib#'s ability to edit the tags within the files.  This script assumes that the taglib-sharp.dll is in the same directory as the .ps1 script file.

The following PowerShell scripts performs all the collecting, enumerating, and tagging of my files.  Feel free to adapt it and tweak it according to your own preferences.  If you have suggestions, I'd welcome them.  Like I said, I threw it together in a very short time so it's probably weak in many regards but seems to get the job done.

Taking my example above, my command line would be:

.\collectaudiobook "Small Favor" "Dresden Files - Book 10" "Jim Butcher" "James Marsters" 2008

Now I don't need to worry about what information was loaded from the CDDB-esque service, the script updates it all after-the-fact. :)

CollectAudioBook.ps1

$a = $args.length

if ( $a -lt 2 ) {
  write @"

USAGE:

`t.\CollectAudioBook.ps1 titlePrefix targetFolderPrefix [author] [artist] [year]

REQUIRED PARAMETERS:

titlePrefix
`tSpecifies the subfolders to process as a single book.
`t(e.g., 'Small Favor' would locate 'Small Favor - Disc 01', 'Small Favor - Disc 02', etc)
`tThe folders are processed in order by name, assuming that to be the proper sequencing of the resulting files.

targetFolderPrefix
`tSpecifies the target folder to which all tracks are collected.  If this
`tfolder doesn't exist, it is created.
`tNOTE: The folder's full name will be comprised of the targetFolderPrefix, a hyphen, and the titlePrefix.
`t(e.g., a titlePrefix of 'Small Favor' and a targetFolderPrefix of 'Dresden Files - Book 10' becomes
`tDresden Files - Book 10 - Small Favor)

author
`tIdentifies the author of the book.

artist
`tIdentifies the name of the performer.

year
`tIdentifies the year of the recording.

"@
  exit
}

$bookName = $args[0]
$bookSet = $args[1] + ' - ' + $bookName
$author = $args[2]
$artist = $args[3]
$year = $args[4]

$target = '.\' + $bookSet
$partNum = 0

# load taglib-sharp.dll which allows for the manipulation on the media tags in the files
$asm = [Reflection.Assembly]::LoadFrom((Resolve-Path ".\taglib-sharp.dll"))

# create the target directory
$dir = New-Item -path "$target" -type directory -force

# enumerate all files in each folder that start with the prefix, copying and renaming each file
foreach ( $folder in ( Get-ChildItem .\$bookName* | Where-Object { $_.Mode.StartsWith('d') } | Sort Name ) ) {
  foreach ( $file in ( Get-ChildItem $folder | Sort Name ) ) {
    ++$partNum

    $targetPrefix = 'Part ' + $partNum.ToString("00")
    $targetFileName = $targetPrefix + '.wma'
    write "Copying $file`t-->`t$targetFileName"
    Copy-Item $folder\$file $target\$targetFileName

    # update the media information in the file
    $media = [TagLib.File]::Create((Resolve-Path "$target\$targetFileName"))
    $media.Tag.Title = $bookName + ' - ' + $targetPrefix
    if ( $author -ne $null ) { $media.Tag.AlbumArtists = $author }
    if ( $artist -ne $null ) { $media.Tag.Performers = $artist }
    $media.Tag.Genres = { Audio Book }
    if ( $year -ne $null ) { $media.Tag.Year = $year }
    $media.Save()
  }
}

Monday, August 24, 2009 10:20:53 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, July 22, 2009

http://windowsteamblog.com/blogs/windows7/archive/2009/07/21/when-will-you-get-windows-7-rtm.aspx

Can't wait! I've been using Windows 7 in the Beta and RC (Release Candidate) builds for some time now and I LOVE it! It's been rock solid (particularly for it's pre-release form) and very fast. It's been my primary OS now on 3 separate computers so I'm really excited about being able to get my hands on the RTM version in the next few weeks.

Wednesday, July 22, 2009 12:11:47 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, June 17, 2009
Well, I've always loved classical music...too bad I'm not on Windows 95 anymore.

Wednesday, June 17, 2009 12:20:03 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Friday, June 12, 2009
This is definitely not news, but I wanted to blog it anyway.

Having recently just reset a development server to use Windows Server 2008, I needed to open the firewall to allow ICMP packets through.  Essentially, I wanted to have the server respond to PING requests.  On a fresh Windows Vista or Windows Server 2008 install (and I imagine Windows 7, though I've not checked), the ICMP protocol is blocked by default; the server won't respond to PINGs.  When you ping an IP address and don't get a reply that doesn't mean that there isn't a computer on the other end, but it sure is a handy way to check.

In Windows Vista/2008 enabling ICMP through the firewall isn't difficult, but if you want to use a GUI, you have to do it through an inbound rule in the Windows Firewall with Advanced Security option.

Personally, I prefer to do it via the command-line through this simple operation: netsh firewall set icmpsetting 8

Friday, June 12, 2009 3:41:21 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Sunday, May 03, 2009

As you may or may not be aware IE 7+ (and other browsers) support using CSS 2.1 attribute selectors.  Attribute selectors allow you to specify a style on an element whose element matches a particular pattern.  For instance:

td[x] { font-weight:bold; }

This selector will bold the text of any TD element on the page that has an attribute "x" (regardless of value).  Other attribute selector styles include:

...[x="value"] matches where x is exactly 'value'
...[x~="value"] matches where x contains a space-separated list of values, one of which is exactly 'value'
...[x^="value"] matches where the attribute x begins with 'value'
...[x$="value"] matches where the attribute x ends with 'value'
...[x*="value"] matches where the attribute x contains 'value'
...[x|="value"] matches where the attribute x begins with either 'value' or 'value-'

In and of themselves, these are pretty darn cool.  You can do some neat things with CSS and HTML.

I ran into an interested scenario this past week that I'd like to share.  I have some JavaScript that alters the value of attributes at runtime.  I found that the page doesn't automatically update according to the stylesheet specification.  Curiously, it would update when I moused-over the element in question.

I found, however, that I could force the issue by assigning the CSS classname to the element that it already has.  This is enough to trigger the change and have the element update according to its attribute values.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
   <style type="text/css">
      td[req] { font-weight:bold; }
      td[err="1"] { color:red !important; }
   </style>
   <script type="text/javascript">
      function fn() {
         var td = document.getElementById("t");
         td.err = "0";
         td.className = td.className;  // trigger the change by reassigning the CSS class
      }
   </script>
</head>
<body>
   <table>
      <tr><td class="test" err="1" req="1" id="t">Cell</td></tr>
   </table>
   <button onclick="javascript:fn();">Remove Error</button>
</body>
</html>

Interestingly, for this to work properly in a non-IE browser (such as Firefox), I found I could not use the object.property=value syntax.  Instead, I had to use the SetAttribute() function.  Also, with Firefox, reassigning the className was unnecessary.

td.setAttribute("err", "0");

That's probably the best way to handle it then, for cross-browser compliance.

CSS | Web
Sunday, May 03, 2009 9:28:44 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, April 23, 2009

My buddy Rob Bagby has announced this exciting event called XamlFest which will be in Salt Lake City on May 1st.  If you have the chance, I'd encourage you to sign up and attend.

Here's the info: http://blogs.msdn.com/bags/archive/2009/03/31/xamlfest-in-salt-lake-city-on-may-1.aspx

Thursday, April 23, 2009 12:35:44 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Monday, April 06, 2009

I was putting together a WinForms utility application today that consists of a TabStrip control on the main dialog box.  On a themed system the TabStrip displays a nice gradual gradient on the page.  As part of this application I wanted to output statistical and detailed information about the user's selections in a readonly fashion, but I wanted to have the text appear as though it were written directly on the TabStrip page rather than within a control.

Of course my first thought was to use a Label control, but I almost immediately wrote that off as the amount of information to be displayed was prohibitive and variable.  The data being displayed might exceed the space on the TabStrip, so scrolling the information is a must.

Next I thought of using a TextBox control, but without some pretty intense subclassing and overriding, I wouldn't be able to achieve the 'written-on' look I was after.  The TextBox will always render an opaque background color.  Sure, there are ways of faking transparency, like overriding TextBox and adding a PictureBox and telling the parent control to WM_PRINT into it, but that seems to be a kludgy way of getting the results I wanted.

Additionally, there are mechanisms to use a RichTextBox (v5.0) to achieve transparency that are well documented online, but try as I might with these implementations, I found them to be quite buggy.  In particular, the appearance of the scrollbars was erratic and sometimes didn't render properly with the control was resized.  After battling with it for a few hours I gave up.  It wasn't for lack of desire, it just wasn't worth it given the scope of the project I was working on.

So this morning I went back to the drawing board and came up with a solution in about 45 minutes that seems to do exactly what I wanted.  I've called this control the ScrollableLabel.  Essentially, it offers the readonlyness of a Label control and the scrollability of a TextBox.  This implementation here is very fundamental.  I plan on enhancing it some more to incorporate user-defined colors, headers, and much more.

I made some executive decisions about how this control should render.  For instance, it will always use your system's colors for text and I set the BackColor property of the UserControl to Transparent via the designer.  For both of these I plan on making them user-selectable choices, but for not, they're the look I was after.

public partial class ScrollableLabel : UserControl {
   public ScrollableLabel() {
      InitializeComponent();
      SetStyle(ControlStyles.Selectable, false);
      // the preferred mechanism for enabling double-buffering is to set the DoubleBuffered property
      // of the control rather than the equivalent SetStyle(DoubleBuffer | UserPaint | AllPaintingInWmPaint, true).
      DoubleBuffered = true;
      AutoScroll = true;
      AutoScrollMinSize = Size.Empty;
   }

   private SizeF _actualTextSize;

   public override string Text {
      get { return base.Text; }
      set {
         base.Text = value;
         calcLabelDimensions();
         Invalidate();
      }
   }

   protected override void OnScroll(ScrollEventArgs se) {
      base.OnScroll(se);
      Invalidate();
   }

   public void Clear() {
      Text = string.Empty;
   }

   #region Hidden design-time properties
   [Browsable(false)]
   public override bool AutoScroll {
      get { return true; }
      set { /* do nothing for now; control is always AutoScroll */ }
   }


   [Browsable(false)]
   public override Color ForeColor {
      get { return SystemColors.ControlText; }
      set { /* do nothing for now; only ControlText is currently supported */ }
   }
   #endregion

   private void calcLabelDimensions() {
      using ( Graphics g = CreateGraphics() ) {
         // assume that no text should wrap
         _actualTextSize = g.MeasureString(Text, Font);
         AutoScrollMinSize = Size.Round(_actualTextSize);
      }
   }

   protected override void OnPaint(PaintEventArgs e) {
      RectangleF rct = new RectangleF(AutoScrollPosition, _actualTextSize);
     
Brush br = Enabled
                    ? SystemBrushes.ControlText
                    : SystemBrushes.GrayText;
      e.Graphics.DrawString(Text, Font, br, rct);
   }
}

When this control is added to a Form it will automatically add scrollbars if the contents exceed the dimensions of the control.  At design-time, however I wanted to make sure that I could see where the ScrollableLabel was positioned so I created a simple Designer.

public sealed class BorderlessBorderDesigner : ControlDesigner {
   protected override void OnPaintAdornments(PaintEventArgs pe) {
      base.OnPaintAdornments(pe);
      if ( BorderStyle.None == getBorderStyle() ) {
         Rectangle rct = Control.ClientRectangle;
         rct.Width -= 1;
         rct.Height -= 1;
         using ( Pen p = new Pen(SystemColors.ControlDark) ) {
            p.DashStyle = DashStyle.Dash;
            pe.Graphics.DrawRectangle(p, rct);
         }
      }
   }

   private BorderStyle getBorderStyle() {
      UserControl ctl = Control as UserControl;
      if ( null != ctl ) return ctl.BorderStyle;
      return BorderStyle.None;
   }
}

Then, I added [Designer(typeof(BorderlessBorderDesigner)] to my ScrollableLabel class declaration.

It's a simple control that achieves a simple goal.  I'll post my updates to it as I get a chance to modify it and make it all the more flexible and powerful.

Enjoy!

.NET | C# | WinForms | Controls
Monday, April 06, 2009 9:19:13 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Tuesday, March 17, 2009

I have an external HDD that I use to store various files and products that I download.  Today, while downloading one such application my computer decided to freeze.  I've been having a sporatic issue with my hardware lately, so this freeze (while not entirely unexpected) happened at a most inopportune moment.

When my computer 'came-to' after rebooting, my HDD was put into a read-only state.  Any change I tried to make to the drive was greeted with a "The media is write protected" error message.  The only fix I could come up with was the following:

1. Open the command prompt.
2. Type DISKPART
3. LIST VOLUME (to identify the volume in question)
4. SELECT VOLUME # (where # represents the volume identifier)
5. ATTRIBUTES VOLUME CLEAR READONLY

After this, I could select the folders on my drive and unmark their read-only state.  Note that I had to clear the read-only flag even though DISKPART reported the volume as not being read-only.

We'll see whether the drive falls back into its read-only state in the future.

Tuesday, March 17, 2009 4:31:52 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback