Initial commit — M3U playlist tool with MP3/AAC encoding

This commit is contained in:
2026-05-10 03:02:53 +00:00
commit b14531362b
114 changed files with 14184 additions and 0 deletions
+361
View File
@@ -0,0 +1,361 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace m3uTool
{
/// <summary>
/// A generic encoder. Creates an external process, sends it argument options,
/// and acts as a wrapper for streaming reads and writes
/// </summary>
public abstract class ProcessStreamWrapper : IDisposable
{
#region Protected Variables
/// <summary>
/// How much data to buffer when reading and writing
/// </summary>
protected const int mBufferLength = 1000;
/// <summary>
/// The output stream writer. If it is null, it is assumed
/// that the process will take care of output itself.
/// </summary>
protected StreamWriter mOutputStreamWriter;
/// <summary>
/// The process
/// </summary>
protected Process mProcess;
/// <summary>
/// The arguments to use for the process
/// </summary>
protected ProcessArguments mProcessArguments;
/// <summary>
/// Progress call-back, for status updates outside this class.
/// </summary>
protected IProgressCallback mProgressCallback = new DoNothingProgressCallback();
#endregion
#region Properties
/// <summary>
/// Gets or sets the progress callback.
/// </summary>
/// <value>The progress callback.</value>
public IProgressCallback ProgressCallback
{
get { return mProgressCallback; }
set { mProgressCallback = value; }
}
/// <summary>
/// Gets the process executable filename, such as "LAME.EXE", "FAAC.EXE", etc.
/// </summary>
/// <value>The process executable filename.</value>
protected abstract string ProcessExecutableFilename { get; }
#endregion
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="ProcessStreamWrapper"/> class.
/// </summary>
/// <param name="encOpt">The enc opt.</param>
public ProcessStreamWrapper(ProcessArguments encOpt) : this(encOpt, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProcessStreamWrapper"/> class.
/// </summary>
/// <param name="encOpt">The enc opt.</param>
/// <param name="outputStream">Output to this stream</param>
public ProcessStreamWrapper(ProcessArguments encOpt, Stream outputStream)
{
mProcessArguments = encOpt;
mProcess = CreateProcess(mProcessArguments, ProcessExecutableFilename);
if (outputStream != null)
{
mOutputStreamWriter = new StreamWriter(outputStream, Encoding.Default);
mProcess.StartInfo.RedirectStandardOutput = true;
mProcess.StartInfo.StandardOutputEncoding = Encoding.Default;
}
mProcess.StartInfo.RedirectStandardError = true;
mProcess.Exited += Process_Exited;
mProcess.Start();
if (mOutputStreamWriter != null)
{
var thread = new Thread(GetStandardOutput);
thread.Start();
}
var standardErrorThread = new Thread(GetStandardError);
standardErrorThread.Start();
}
#endregion
#region Static methods
/// <summary>
/// Creates a process given an executable and some process arguments
/// </summary>
/// <param name="processArguments">The enc opt.</param>
/// <param name="executable">The executable filename.</param>
/// <returns></returns>
private static Process CreateProcess(ProcessArguments processArguments, string executable)
{
var process = new Process();
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.FileName = executable;
process.StartInfo.Arguments = processArguments.GetCommandLineArguments();
process.StartInfo.WorkingDirectory = Environment.CurrentDirectory;
process.StartInfo.CreateNoWindow = true;
process.EnableRaisingEvents = true;
return process;
}
#endregion
#region Event handlers
/// <summary>
/// Handles the Exited event of the Process.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
private void Process_Exited(object sender, EventArgs e)
{
mProgressCallback.SetText("Process_Exited");
}
#endregion
#region Utility methods
private static int GetPercentDone(long previousLeft, long currentLeft, long currentDone, int lastDone)
{
// if currentLeft > previousLeft - lastDone,
// addedLeft = previousLeft - lastDone - currentLeft
// totalLength = previousLeft + currentDone + addedLeft
long totalLength = currentLeft;
if (currentLeft != previousLeft)
{
long leftDelta = previousLeft - lastDone - currentLeft;
totalLength = previousLeft + currentDone + leftDelta;
}
try
{
return Convert.ToInt32((currentDone/(double) totalLength)*100);
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
}
return 0;
}
#endregion
#region Public methods
/// <summary>
/// Sends the data to the underlying process through its input stream.
/// </summary>
/// <param name="buffer">The data to encode</param>
/// <param name="count">How many characters to write</param>
/// <param name="startIndex">The index to begin writing from</param>
public void ProcessInput(char[] buffer, int startIndex, int count)
{
mProcess.StandardInput.Write(buffer, startIndex, count);
}
/// <summary>
/// Sends the data from the input stream to the process
/// </summary>
/// <param name="inputStream"></param>
public void ProcessInput(Stream inputStream)
{
if (!inputStream.CanRead)
throw new ArgumentException("Can't read from inputStream");
mProgressCallback.SetText("ProcessInput: StreamReader sr");
long inputCount = inputStream.Length;
long previousInputCount = inputStream.Length;
long wroteCount = 0;
int percentageComplete = 0;
var sr = new StreamReader(inputStream, Encoding.Default);
//using (StreamReader sr = new StreamReader(inputStream, Encoding.Default))
{
mProgressCallback.Begin(0, 100);
mProgressCallback.SetText(mProcessArguments.GetCommandLineArguments());
var buffer = new char[mBufferLength];
while (!sr.EndOfStream)
{
int readLength = sr.Read(buffer, 0, mBufferLength);
ProcessInput(buffer, 0, readLength);
// update progress
wroteCount += readLength;
previousInputCount = inputCount;
inputCount = inputStream.Length;
int currentPercentageComplete = GetPercentDone(previousInputCount, inputCount, wroteCount,
readLength);
if (currentPercentageComplete != percentageComplete)
{
percentageComplete = currentPercentageComplete;
mProgressCallback.StepTo(percentageComplete);
}
}
}
mInputDone = true;
mProgressCallback.SetText("ProcessInput: Done. " + String.Format("Wrote: {0}", wroteCount));
}
/// <summary>
/// Open this file and encode it.
/// </summary>
/// <param name="filename"></param>
public void ProcessInput(string filename)
{
using (FileStream inputStream = File.OpenRead(filename))
{
ProcessInput(inputStream);
}
}
#endregion
#region Threaded methods
/// <summary>
/// TEMP: this should be removed once GetStandardOutput is working
/// </summary>
private bool mInputDone;
/// <summary>
/// Collects all the data from Process.StandardOutput and sends it to the outputstream writer
/// </summary>
private void GetStandardOutput()
{
int readTotal = 0;
var writeBuffer = new char[mBufferLength];
try
{
while (!mProcess.StandardOutput.EndOfStream)
//while (mProcess.StandardOutput.Peek() > -1)
{
mProgressCallback.SetText("- Before read");
int readLength = mProcess.StandardOutput.Read(writeBuffer, 0, writeBuffer.Length);
mProgressCallback.SetText("- After read: " + readLength);
mOutputStreamWriter.Write(writeBuffer, 0, readLength);
if (mInputDone)
mProgressCallback.SetText(" StandardOutput: " + new string(writeBuffer, 0, readLength));
readTotal += readLength;
mProgressCallback.SetText("Read total = " + readTotal);
//mProcess.StandardInput.Flush();
//mProcess.StandardOutput.BaseStream.Flush();
}
}
catch (Exception ex)
{
mProgressCallback.SetText(ex.ToString());
}
// mOutputStreamWriter.Flush();
mProgressCallback.SetText("GetStandardOutput() " + "Done!" + " StandardOutput.EndOfStream = " +
mProcess.StandardOutput.EndOfStream);
}
/* Asynchronous implementation of GetStandardOutput
//int mReadTotal = 0;
//private byte[] mBuffer = new byte[mBufferLength];
//private void GetStandardOutputAsync()
//{
// mProcess.StandardOutput.BaseStream.BeginRead(mBuffer, 0, mBufferLength, GetStandardOutputCallback, 0);
//}
//private void GetStandardOutputCallback(IAsyncResult asyncResult)
//{
// mProgressCallback.SetText("- Before read:");
// int readLength = mProcess.StandardOutput.BaseStream.EndRead(asyncResult);
// mProgressCallback.SetText("- After read:" + readLength);
// mOutputStreamWriter.BaseStream.Write(mBuffer, 0, readLength);
// mReadTotal += readLength;
// mProgressCallback.SetText("Read total = " + mReadTotal);
// mProcess.StandardOutput.BaseStream.BeginRead(mBuffer, 0, mBufferLength, GetStandardOutputCallback, 0);
//}
*/
/// <summary>
/// Collects all the data from Process.StandardError and sends it to the progress callback
/// </summary>
private void GetStandardError()
{
try
{
while (!mProcess.StandardError.EndOfStream)
{
string line = mProcess.StandardError.ReadLine();
mProgressCallback.SetText("GetStandardError() " + line);
}
}
catch (Exception ex)
{
mProgressCallback.SetText(ex.ToString());
}
mProgressCallback.SetText("GetStandardError() " + "Done!");
}
#endregion
#region IDisposable Members
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
int count = 0;
mProgressCallback.SetText("Dispose()");
while (mProcess != null && !mProcess.HasExited)
{
mProcess.WaitForExit(2);
if (count++ > mBufferLength && !mProcess.HasExited)
{
mProcess.Kill();
mProgressCallback.SetText("!!! Process.Kill()");
}
else
{
// mProgressCallback.SetText("Writing EOF");
mProcess.StandardInput.Write(new string(new[] {(char) 0x1A, '\n'}));
}
}
mProgressCallback.SetText("Dispose() with exit code " + mProcess.ExitCode);
}
#endregion
}
}