Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

MP3 Rearrange

0.00/5 (No votes)
23 Aug 2012 4  
Prepares play list elements for burning to CD or DVD or loading onto a MP3 player.

Introduction

This windows application allows the user to select a Winamp play list file with the suffix .m3u and perform various folder and filename operations to facilitate creating a directory for the play list MP3s to reside in a sequential flat file structure. Then after the burning of a MP3 CD or DVD or loading into a MP3 player such as a Sansa Clip, the MP3s will play back in the play list sequence you wanted.

Background

Over time a persons MP3 collection can grow quite large. The MP3s can be organized by a wide variety of folder structures. In my collection there is a root folder "MP3" which contains subfolders such as Artist, Album, and then the actual MP3 files. However, there are other folders such as Misc, Classical, ForDad, FullAlbums etc. Interspersed throughout this folder tree structure are Play List files which contain the song sequence with the path and filenames for each song.

Music player software such as Winamp can read these playlists and display and play the songs in the specified sequence. Some DVD players will also play MP3 files which have been burned on to CDs or DVDs in the data format. In MP3 format the content of approximately ten music CDs can fit onto a single MP3 data CD.

Some DVD players will only play MP3s in a flat folder structure and then play those songs in an alphanumerical sequence.

You can choose one of these four operations.

  1. Copy
  2. Delete Destination
  3. Move
  4. Move Back

For the Copy and Move operations additional File Name manipulations can also be executed across all the files in the Play List.

  • Replace (CheckBox and two TextBoxes for Find and Replace strings)
  • Swap around dash
  • Insert Numbers
  • Insert Numbers at Start

Once an Operation is selected the Proceed Button is enabled. 

For Copy or Move operations the Preview form is shown which contains two side by side checked list boxes and some other controls. The left side shows the current list of filenames and the right side shows prospective manipulated file names. Initially, all the boxes in both list boxes are checked.

This gives you a chance to review the filenames, then you can "Cancel"  and make adjustments or "Finish".

When the Finish button is clicked the files are moved or copied to the destination folder with a flat file structure with the manipulated file names. Only the checked files will be processed.

Upon completion, summarized results will be displayed.  

The Delete Destination operation, provides a summery of directory contents and an option to cancel.

The Move operation stores the processed original and modified full file names, so the Move Back operation will work even if you quit the application after the move.  

Speed of Operations

Copy can be slow if there are lots of files to copy. Move and Move back is very fast, if the destination is on the same volume as the source. Delete is also fast.

The Solution

The Visual Studio 2008 project contains two forms FormMP3Main.cs, and FormPreview.cs as shown above. The main class is MP3Rearrange.cs which inherits five levels deep, from FormUtil.cs, RecursiveIO.cs, RegistryWrapper.cs, Logging.cs, and BasicUtil.cs. Many of my software application projects use these inherited classes, especially the last three. I try to minimize the amount of code in the Forms and use code reuse as much as possible.

MP3Rearrange.cs also uses classes PersistControls.cs, XMLDictionary.cs and TwoString.cs.

The major class construction sequence is Program.cs constructs FormMP3Main which in its OnLoad event constructs MP3Rearrange. MP3Rearrange in turn constructs all other instances of classes as needed.

FormPreview is run as a Dialog from MP3Rearrange.ShowPreview.

Class PersistControls in method Persist() uses class RegistryWrapper to store details of the passed form and some of it's controls to the windows registry. Items persisted are the Form size, location and state, and the content of any TextBoxes and the state of any CheckBoxes. Method Restore() retrieves the previously persisted items. Both Persist() and Restore() call method StartPersistControls(Form f) with var bPersist set accordingly.

private void StartPersistControls(Form f)
{
 // Store or Restore some elements of a Form and its child controls
 FormName = f.Name;

 if (bPersist && f.WindowState != FormWindowState.Minimized)
 {
  regWrap.PutString(FormName + "WindowState", f.WindowState.ToString());
 }

 if (f.WindowState == FormWindowState.Normal)
 {
  bool rv;
  if (bPersist)
  {
   // Persist Form size and location
   regWrap.PutInt(FormName + "Height", f.Height);
   regWrap.PutInt(FormName + "Width", f.Width);
   regWrap.PutInt(FormName + "LocX", f.Location.X);
   regWrap.PutInt(FormName + "LocY", f.Location.Y);
  }
  else
  {
   // Restore Form size and location
   int H, W, X, Y;
   rv = regWrap.GetIntKey(FormName + "Height", out H);
   if (rv)
   {
    f.WindowState =
     (FormWindowState) Enum.Parse(typeof (FormWindowState), 
            regWrap.GetStringKey(FormName + "WindowState", "Normal"));
            
    if (f.WindowState == FormWindowState.Normal)
    {
     regWrap.GetIntKey(FormName + "Width", out W);
     regWrap.GetIntKey(FormName + "LocX", out X);
     regWrap.GetIntKey(FormName + "LocY", out Y);
     Rectangle ScreenRect = Screen.FromControl(f).Bounds;
     var rect = new Rectangle(X, Y, W, H);
     // To avoid the form being restored to a location off the screen
     // we do the "Contains" test.
     // This can happen when using remote desktop, or using a 
     // different size screen.
     // On not contained stay with default values.

     if (ScreenRect.Contains(rect))
     {
      f.Height = H;
      f.Width = W;
      f.Location = new Point(X, Y);
     }
    }
   }
  }
 }

 PersistControl(f);
}

Method PersistControl calls itself recursively. It handles persisting TextBox and CheckBox controls.

private void PersistControl(Control Paren)
{
 String CheckedDefault;
 foreach (Control ctrl in Paren.Controls)
 {
  if (bPersist)
  {
   // Persist
   if (ctrl is TextBox)
   {
    regWrap.PutString(FormName + "_" + ctrl.Name + "_Text", ctrl.Text);
   }

   if (ctrl is CheckBox)
   {
    var chkbox = (CheckBox) ctrl;
    regWrap.PutString(FormName + "_" + ctrl.Name + "_Checked", 
                      regWrap.BoolToString(chkbox.Checked));
   }
  }
  else
  {
   // Restore
   if (ctrl is TextBox)
   {
    ctrl.Text = regWrap.GetStringKey(FormName + "_" + ctrl.Name + "_Text",
                                     String.Empty);
   }
   else if (ctrl is CheckBox)
   {
    var chkbox = (CheckBox) ctrl;
    CheckedDefault = regWrap.BoolToString(chkbox.Checked);
    chkbox.Checked = regWrap.StringToBool(
           regWrap.GetStringKey(FormName + "_" + ctrl.Name + "_Checked",
                                CheckedDefault));
   }
  }

  if (ctrl.Controls.Count > 0)
  {
   // Recursive
   PersistControl(ctrl);
  }
 } // end foreach
}

For a "Move" operation we need to persist the original full file path and the modified destination file name so we can restore via a "Move Back" operation.

We use Class XMLDictionary to persist each element of the play list to an XML file.

The TableName and NameSpace are specified in the constructor.

The method WriteTbl is shown below.

public void WriteTbl(Dictionary<String, String> htDictionary)
{
 // create table
 var table = new DataTable(TblName, NameSpace);
 table.MinimumCapacity = 10;
 table.CaseSensitive = false;

 Type aTyp = typeof (String);
 // define columns
 DataColumn col = table.Columns.Add(C1Name, aTyp);
 col.AllowDBNull = true;
 col = table.Columns.Add(C2Name, aTyp);
 col.AllowDBNull = true;

 String Key;
 String Val;
 // Add Rows
 DataRow row;
 Dictionary<string, string>.Enumerator en =
             htDictionary.GetEnumerator();
 while (en.MoveNext())
 {
  Key = en.Current.Key;
  Val = en.Current.Value;
  row = table.NewRow();
  row[0] = Key;
  row[1] = Val;
  table.Rows.Add(row);
 }
 // Write XML and XML Scheme
 String FFN = Path.Combine(AppDir, TblName);
 var fi = new FileInfo(FFN + ".xsd");
 if (!fi.Exists)
 {
  table.WriteXmlSchema(FFN + ".xsd");
 }
 table.WriteXml(FFN + ".xml");
}

Method ReadTbl is shown below.

/// <summary>
///  Read data from an XML file and load into a Dictionary.
/// </summary>
/// <returns>Dictionary<String,String></returns>
public Dictionary<String, String} ReadTbl()
{
 var rv = new Dictionary<string, string>();
 String FFN = Path.Combine(AppDir, TblName);

 // Create DataTable
 var table = new DataTable();
 table.MinimumCapacity = 10;
 table.CaseSensitive = false;
 var fixsd = new FileInfo(FFN + ".xsd");
 var fixml = new FileInfo(FFN + ".xml");
 if (fixsd.Exists && fixml.Exists)
 {
  // Read data from files into DataTable
  table.ReadXmlSchema(FFN + ".xsd");
  table.ReadXml(FFN + ".xml");
  NameSpace = table.Namespace;
  TblName = table.TableName;
  C1Name = table.Columns[0].ToString();
  C2Name = table.Columns[1].ToString();

  String Key;
  String Val;
  int RowCnt = table.Rows.Count;
  // Load Dictionary from DataTable
  for (int i = 0; i < RowCnt; ++i)
  {
   Key = table.Rows[i][C1Name].ToString();
   Val = table.Rows[i][C2Name].ToString();
   rv.Add(Key, Val);
  }
 }
 return rv;
}

OnDestination event handler via FolderUtils.BrowseForDirectory displays the standard windows FolderBrowserDialog with a time saving difference. The root folder where the browsing starts from is via property SelectedPath. I set SelectedPath as close as possible to the previous destination directory.

public String BrowseForDirectory(String InitPath, String Description)
{
 int ix;
 // set initial directory path as close as possible to last path
 while (!Directory.Exists(InitPath))
 {
  // directory does not exist
  ix = InitPath.LastIndexOf(@"\");
  if (ix > -1)
  {
   // move up one directory level
   InitPath = InitPath.Substring(0, ix);
  }
  else
  {
   break;
  }
 }

 var DirPicker = new FolderBrowserDialog();
 DirPicker.SelectedPath = InitPath;
 DirPicker.Description = Description;
 DialogResult dr = DirPicker.ShowDialog();

 String SelectedPath = String.Empty;
 if (dr == DialogResult.OK)
 {
  SelectedPath = DirPicker.SelectedPath;
 }

 DirPicker.Dispose();
 return SelectedPath;
}

Method MP3Rearrange.DeleteDir() displays a nice summary of the directory contents. Upon approval the directory is deleted.

Method RecursiveIO.StartRecurse navigates all the directories and files in the tree, during which virtual methods ProcessFileName and ProcessDirName are called on each appropriate node. Furthermore, the properties FileCnt and DirCnt are set. DeleteDir also calls handy little methods in class BasicUtil like NiceByteSize, Pluralize, and PluralizeYIES which makes the directory summary message more user friendly.

private void DeleteDir()
{
 // Delete directory and subdirectories and files therein, after confirmation.

 // Collect directory details
 StartRecurse(TBDestDir.Text);

 int dc = DirCnt - 1;
 String fmt = "Are you sure you want to delete all the contents ({0})";
 fmt += " of directory '{1}'  including {2} subdirector{3} and {4} file{5} ?";
 String sMsg = String.Format(fmt, NiceByteSize(DirSizeBytes), TBDestDir.Text,
                           dc, PluralizeYIES(dc), FileCnt, Pluralize(FileCnt));
 DialogResult rv = MessageBox.Show(sMsg, ProgName,
             MessageBoxButtons.OKCancel, MessageBoxIcon.Question);

 if (rv == DialogResult.OK)
 {
  Directory.Delete(TBDestDir.Text, true);
 }
}

DirSizeBytes is calculated in overridden method ProcessFileName below.

/// Override method in inherited class RecursiveIO.
/// Called for every file in the directory tree.
/// Calculate total size in bytes of all files in directory tree
protected override bool ProcessFileName(FileInfo fi)
{
 DirSizeBytes += fi.Length; // Size in Bytes
 return true;
}

ProcessDirName is overridden to avoid unwanted entries in the log file.

/// Override method in inherited class RecursiveIO.
/// Called for every directory in the directory tree.
protected override bool ProcessDirName(DirectoryInfo Dir)
{
 return true;
}

Points of Interest

MP3Rearrange is written in a style that I like and feel is appropriate for small to medium size projects for a sole programmer. Other styles of organization may be better for a team programming effort or for larger projects.

The techniques I used to implement Persistence from one run of the App to the next were interesting.

The use of "Tool Tips" provides a more intuitive graphical user interface. It was fun writing this software, documenting it, and authoring this article for codeproject.com.

Tested on Windows 7, Vista, MS Server 2003, and XP.

History

First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here