MP3 重新排序






4.76/5 (9投票s)
为刻录到 CD 或 DVD 或加载到 MP3 播放器准备播放列表元素。
引言
这款 Windows 应用程序允许用户选择一个后缀名为 .m3u 的 Winamp 播放列表文件,并执行各种文件夹和文件名操作,以方便地创建一个目录,供播放列表中的 MP3 文件按顺序平坦化存储。然后,在刻录 MP3 CD 或 DVD 或加载到 MP3 播放器(如 Sansa Clip)后,MP3 文件将按照您想要的播放列表顺序播放。
背景
随着时间的推移,一个人的 MP3 收藏可能会变得非常庞大。MP3 文件可以按照各种不同的文件夹结构进行组织。在我的收藏中,有一个根文件夹“MP3”,其中包含子文件夹,如“艺术家”和“专辑”,然后是实际的 MP3 文件。但是,还有其他文件夹,如“杂项”、“古典”、“给爸爸”、“整张专辑”等。在整个文件夹树结构中穿插着播放列表文件,其中包含每个歌曲的歌曲顺序以及路径和文件名。
像 Winamp 这样的音乐播放器软件可以读取这些播放列表,并按照指定的顺序显示和播放歌曲。一些 DVD 播放器也可以播放刻录到 CD 或 DVD 中的 MP3 文件(数据格式)。以 MP3 格式,一张 CD 的内容大约可以存储到一张 MP3 数据 CD 中。
一些 DVD 播放器只能在扁平化的文件夹结构中播放 MP3,然后按字母数字顺序播放这些歌曲。
您可以选择以下四种操作之一。
- 复制
- 删除目标
- 移动
- 回退
对于复制和移动操作,还可以对播放列表中的所有文件执行额外的文件名操作。
- 替换(复选框和两个用于查找和替换字符串的文本框)
- 替换下划线
- 插入编号
- 在开头插入编号
选择操作后,“执行”按钮将被启用。
对于复制或移动操作,将显示“预览”窗体,其中包含两个并排的复选列表框和其他一些控件。左侧显示当前文件名列表,右侧显示潜在的操作后的文件名。最初,两个列表框中的所有框都已被选中。
这给了您一个审查文件名的机会,然后您可以“取消”进行调整或“完成”。
当单击“完成”按钮时,文件将被移动或复制到目标文件夹,形成一个扁平化的文件结构,并带有操作后的文件名。只有选中的文件才会被处理。
完成后,将显示汇总结果。
“删除目标”操作提供目录内容的摘要和一个取消选项。
“移动”操作会存储处理过的原始和修改后的完整文件名,因此即使您在移动后退出应用程序,“回退”操作仍然可以工作。
操作速度
如果需要复制大量文件,复制可能会很慢。如果目标与源在同一卷上,移动和回退速度非常快。删除也很快。
解决方案
Visual Studio 2008 项目包含两个窗体 FormMP3Main.cs 和 FormPreview.cs,如上所示。主类是MP3Rearrange.cs,它从FormUtil.cs、RecursiveIO.cs、RegistryWrapper.cs、Logging.cs和BasicUtil.cs继承了五层。我的许多软件应用程序项目都使用了这些继承的类,特别是后三个。我尽量减少窗体中的代码量,并尽可能多地使用代码重用。
MP3Rearrange.cs 还使用了PersistControls.cs、XMLDictionary.cs和TwoString.cs 类。
主要的类构造顺序是:Program.cs 构造 FormMP3Main,它在其 OnLoad 事件中构造 MP3Rearrange。MP3Rearrange 再根据需要构造所有其他类的实例。
FormPreview
作为对话框从 MP3Rearrange.ShowPreview
运行。
PersistControls
类在 Persist()
方法中使用 RegistryWrapper
类将传递的窗体及其部分控件的详细信息存储到 Windows 注册表中。持久化的项目包括窗体的大小、位置和状态,以及任何文本框的内容和任何复选框的状态。Restore()
方法检索先前持久化的项目。Persist()
和 Restore()
都调用 StartPersistControls(Form f)
方法,并将变量 bPersist
设置为相应的值。
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);
}
PersistControl
方法会递归调用自身。它负责持久化 TextBox 和 CheckBox 控件。
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
}
对于“移动”操作,我们需要持久化原始的完整文件路径和修改后的目标文件名,以便能够通过“回退”操作进行恢复。
我们使用 XMLDictionary
类将播放列表的每个元素持久化到一个 XML 文件中。
TableName 和 NameSpace 在构造函数中指定。
下面显示了 WriteTbl
方法。
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");
}
下面显示了 ReadTbl
方法。
/// <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
事件处理程序通过 FolderUtils.BrowseForDirectory
显示标准的 Windows FolderBrowserDialog
,但有一个节省时间的区别。浏览开始的根文件夹是通过 SelectedPath
属性指定的。我将 SelectedPath
设置得尽可能接近之前的目标目录。
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;
}
MP3Rearrange.DeleteDir()
方法显示了目录内容的漂亮摘要。在获得批准后,目录将被删除。
RecursiveIO.StartRecurse
方法会遍历树中的所有目录和文件,在此过程中,会在每个合适的节点上调用虚拟方法 ProcessFileName
和 ProcessDirName
。此外,还会设置 FileCnt
和 DirCnt
属性。DeleteDir
还调用 BasicUtil
类中一些方便的小方法,如 NiceByteSize
、Pluralize
和 PluralizeYIES
,这些方法使得目录摘要消息对用户更友好。
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
在下面重写的 ProcessFileName
方法中计算。
/// 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
以避免日志文件中出现不必要的条目。
/// Override method in inherited class RecursiveIO.
/// Called for every directory in the directory tree.
protected override bool ProcessDirName(DirectoryInfo Dir)
{
return true;
}
关注点
MP3Rearrange 的编写风格是我喜欢的,并且我认为它适用于独奏程序员的中小型项目。对于团队编程工作或大型项目,其他组织风格可能更好。我用于实现应用程序运行之间持久化的技术非常有趣。
使用“工具提示”提供了更直观的图形用户界面。编写这个软件、记录它,以及为 codeproject.com 撰写这篇文章都很有趣。
已在 Windows 7、Vista、MS Server 2003 和 XP 上测试。