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)
{
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)
{
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
{
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);
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)
{
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
{
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)
{
PersistControl(ctrl);
}
} }
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)
{
var table = new DataTable(TblName, NameSpace);
table.MinimumCapacity = 10;
table.CaseSensitive = false;
Type aTyp = typeof (String);
DataColumn col = table.Columns.Add(C1Name, aTyp);
col.AllowDBNull = true;
col = table.Columns.Add(C2Name, aTyp);
col.AllowDBNull = true;
String Key;
String Val;
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);
}
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.
public Dictionary<String, String} ReadTbl()
{
var rv = new Dictionary<string, string>();
String FFN = Path.Combine(AppDir, TblName);
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)
{
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;
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;
while (!Directory.Exists(InitPath))
{
ix = InitPath.LastIndexOf(@"\");
if (ix > -1)
{
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()
{
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.
protected override bool ProcessFileName(FileInfo fi)
{
DirSizeBytes += fi.Length; return true;
}
ProcessDirName
is overridden to avoid unwanted entries in the log file.
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