MP3 Rearrange






4.76/5 (9 votes)
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.
- Copy
- Delete Destination
- Move
- 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.