Add Inspection command
This commit is contained in:
parent
af2d4d6139
commit
1919ba1fe2
12 changed files with 456 additions and 0 deletions
79
src/Commands/Source/InspectCommand.cs
Executable file
79
src/Commands/Source/InspectCommand.cs
Executable file
|
|
@ -0,0 +1,79 @@
|
|||
using System.CodeDom.Compiler;
|
||||
using System.Text;
|
||||
|
||||
using ConsoleAppFramework;
|
||||
|
||||
using MediaOrganizer.Tasks;
|
||||
using MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
namespace MediaOrganizer.Commands.Source;
|
||||
|
||||
[RegisterCommands("source")]
|
||||
internal class InspectCommand
|
||||
{
|
||||
/// <summary>Provides detailed information about a media source.</summary>
|
||||
/// <param name="sourceFile">Path to DVD image (.iso), BluRay folder (parent of BDMV), FFmpeg bluray-protocol (bluray:), or BluRay Movie Playlist File (mpls).</param>
|
||||
/// <param name="index">-p|--playlist|-t|--title, Specify Title, or Playlist within a DVD image, or BluRay folder.</param>
|
||||
[Command("inspect")]
|
||||
public async Task Execute(
|
||||
string sourceFile,
|
||||
int? index = null)
|
||||
{
|
||||
IInspector inspector = InspectorFactory.CreateInspector(sourceFile);
|
||||
if (index is not null)
|
||||
{
|
||||
inspector.Index = index;
|
||||
}
|
||||
|
||||
var result = await inspector.RunAsync();
|
||||
using IndentedTextWriter writer = new(Console.Out);
|
||||
writer.WriteLine($"Inspection Result for {inspector.Source}:");
|
||||
|
||||
foreach (var track in result.Tracks)
|
||||
{
|
||||
writer.WriteLine(TrackIndexFormat, track.Index);
|
||||
writer.Indent += 1;
|
||||
writer.WriteLine(DurationFormat, track.Duration);
|
||||
writer.WriteLine(FPSFormat, track.FPS);
|
||||
foreach (var audio in track.Audio)
|
||||
{
|
||||
writer.WriteLine(AudioTrackFormat, audio.Index);
|
||||
writer.Indent += 1;
|
||||
writer.WriteLine(AudioLanguageFormat, audio.Language, audio.LangCode);
|
||||
writer.WriteLine(AudioCodecFormat, audio.Codec);
|
||||
writer.Indent -= 1;
|
||||
}
|
||||
|
||||
writer.Indent -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Track {0}
|
||||
/// </summary>
|
||||
private static CompositeFormat TrackIndexFormat => field ??= CompositeFormat.Parse("Track {0}");
|
||||
/// <summary>
|
||||
/// Resolution: {0}x{1}
|
||||
/// </summary>
|
||||
private static CompositeFormat ResolutionFormat => field ??= CompositeFormat.Parse("Resolution: {0}x{1}");
|
||||
/// <summary>
|
||||
/// Duration: {0}
|
||||
/// </summary>
|
||||
private static CompositeFormat DurationFormat => field ??= CompositeFormat.Parse("Duration: {0}");
|
||||
/// <summary>
|
||||
/// FPSFormat: {0}
|
||||
/// </summary>
|
||||
private static CompositeFormat FPSFormat => field ??= CompositeFormat.Parse("FPS: {0}");
|
||||
/// <summary>
|
||||
/// Audio Track {0}
|
||||
/// </summary>
|
||||
private static CompositeFormat AudioTrackFormat => field ??= CompositeFormat.Parse("Audio Track {0}");
|
||||
/// <summary>
|
||||
/// Language: {0} ({1})
|
||||
/// </summary>
|
||||
private static CompositeFormat AudioLanguageFormat => field ??= CompositeFormat.Parse("Language: {0} ({1})");
|
||||
/// <summary>
|
||||
/// Codec: {0}
|
||||
/// </summary>
|
||||
private static CompositeFormat AudioCodecFormat => field ??= CompositeFormat.Parse("Codec: {0}");
|
||||
}
|
||||
26
src/System/IO/CompositeFormatTextWriter.cs
Normal file
26
src/System/IO/CompositeFormatTextWriter.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using System.Text;
|
||||
|
||||
namespace System.IO;
|
||||
|
||||
public static class TextWriterCompositeFormat
|
||||
{
|
||||
public static void WriteLine(this TextWriter writer, CompositeFormat format, params object?[] args)
|
||||
{
|
||||
WriteLine(writer, null, format, args);
|
||||
}
|
||||
|
||||
public static void WriteLine(this TextWriter writer, IFormatProvider? provider, CompositeFormat format, params object?[] args)
|
||||
{
|
||||
writer.WriteLine(string.Format(provider, format, args));
|
||||
}
|
||||
|
||||
public static Task WriteLineAsync(this TextWriter writer, CompositeFormat format, params object?[] args)
|
||||
{
|
||||
return WriteLineAsync(writer, null, format, args);
|
||||
}
|
||||
|
||||
public static Task WriteLineAsync(this TextWriter writer, IFormatProvider? provider, CompositeFormat format, params object?[] args)
|
||||
{
|
||||
return writer.WriteLineAsync(string.Format(provider, format, args));
|
||||
}
|
||||
}
|
||||
54
src/Tasks/InspectFactory.cs
Normal file
54
src/Tasks/InspectFactory.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
namespace MediaOrganizer.Tasks;
|
||||
|
||||
public class InspectorFactory
|
||||
{
|
||||
private static ReadOnlySpan<char> BluRayProtocol => "bluray:";
|
||||
private static ReadOnlySpan<char> IsoExtension => ".iso";
|
||||
private static ReadOnlySpan<char> MplsExtension => ".mpls";
|
||||
|
||||
public static IInspector CreateInspector(string path)
|
||||
{
|
||||
if (path.StartsWith(BluRayProtocol, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FindBluRayInspector(path[BluRayProtocol.Length..]);
|
||||
}
|
||||
|
||||
if (!Path.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException(null, path);
|
||||
}
|
||||
|
||||
if ((File.GetAttributes(path) & FileAttributes.Directory) != 0)
|
||||
{
|
||||
return FindBluRayInspector(path[BluRayProtocol.Length..]);
|
||||
}
|
||||
|
||||
if (path.EndsWith(IsoExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new LsDvdInspector(path);
|
||||
}
|
||||
else if (path.EndsWith(MplsExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
throw new ArgumentException(null, nameof(path));
|
||||
|
||||
static IInspector FindBluRayInspector(string path)
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException(path);
|
||||
}
|
||||
|
||||
if (!Directory.Exists(Path.Join(path, "BDMV")))
|
||||
{
|
||||
throw new FileNotFoundException("Specified directory doesn't contain BDMV folder", path);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/Tasks/Inspection/BluRayInspector.cs
Normal file
11
src/Tasks/Inspection/BluRayInspector.cs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public class BluRayInspector(string path) : IInspector
|
||||
{
|
||||
public int? Index { get; set; }
|
||||
|
||||
public string Source => path;
|
||||
|
||||
public ValueTask<InspectionResult> RunAsync() => ValueTask.FromResult(default(InspectionResult)!);
|
||||
}
|
||||
13
src/Tasks/Inspection/IInspector.cs
Normal file
13
src/Tasks/Inspection/IInspector.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public interface IInspector
|
||||
{
|
||||
[DisallowNull]
|
||||
int? Index { get; set; }
|
||||
|
||||
string Source { get; }
|
||||
|
||||
ValueTask<InspectionResult> RunAsync();
|
||||
}
|
||||
14
src/Tasks/Inspection/InspectionResult.cs
Normal file
14
src/Tasks/Inspection/InspectionResult.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public partial record class InspectionResult
|
||||
{
|
||||
public required IReadOnlyList<InspectionTrack> Tracks { get; set; }
|
||||
|
||||
[JsonSourceGenerationOptions(
|
||||
GenerationMode = JsonSourceGenerationMode.Metadata
|
||||
)]
|
||||
[JsonSerializable(typeof(InspectionResult))]
|
||||
internal partial class InspectionResultSerializerContext : JsonSerializerContext;
|
||||
}
|
||||
16
src/Tasks/Inspection/InspectionTrack.cs
Normal file
16
src/Tasks/Inspection/InspectionTrack.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public sealed record class InspectionTrack
|
||||
{
|
||||
public int Index { get; set; }
|
||||
|
||||
public TimeSpan Duration { get; set; }
|
||||
|
||||
public double? FPS { get; set; }
|
||||
|
||||
public required IReadOnlyList<TrackCell> Playlist { get; set; }
|
||||
|
||||
public required IReadOnlyList<TrackAudio> Audio { get; set; }
|
||||
|
||||
public required IReadOnlyList<TrackSubtitle> Subtitles { get; set; }
|
||||
}
|
||||
83
src/Tasks/Inspection/LsDvdInspector.cs
Normal file
83
src/Tasks/Inspection/LsDvdInspector.cs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
using MediaOrganizer.Tools;
|
||||
using MediaOrganizer.Tools.Types.LsDvd;
|
||||
|
||||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public class LsDvdInspector(string path) : IInspector
|
||||
{
|
||||
public int? Index { get; set; }
|
||||
|
||||
public string Source => path;
|
||||
|
||||
public async ValueTask<InspectionResult> RunAsync()
|
||||
{
|
||||
LsDvd lsDvd = new(path)
|
||||
{
|
||||
Title = Index
|
||||
};
|
||||
|
||||
return ToResult(await lsDvd.RunAsync());
|
||||
}
|
||||
|
||||
private static List<TrackAudio> ToAudio(List<Audio> audio)
|
||||
{
|
||||
List<TrackAudio> result = [];
|
||||
foreach (var item in audio)
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
Index = item.Index ?? -1,
|
||||
Codec = item.Format ?? string.Empty,
|
||||
LangCode = item.LangCode ?? string.Empty,
|
||||
Language = item.Language ?? string.Empty,
|
||||
Channels = item.Channels ?? -1
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<TrackSubtitle> ToSubtitles(List<Subtitle> subtitles)
|
||||
{
|
||||
List<TrackSubtitle> result = [];
|
||||
foreach (var item in subtitles)
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
LangCode = item.LangCode ?? string.Empty,
|
||||
Language = item.Language ?? string.Empty
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<InspectionTrack> ToTracks(List<Track> tracks)
|
||||
{
|
||||
List<InspectionTrack> result = [];
|
||||
foreach (var item in tracks)
|
||||
{
|
||||
result.Add(new()
|
||||
{
|
||||
Index = item.Index ?? -1,
|
||||
Duration = ToTimespan(item.Length),
|
||||
FPS = item.FPS,
|
||||
Audio = ToAudio(item.Audio),
|
||||
Subtitles = ToSubtitles(item.Subtitles),
|
||||
Playlist = [],
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
static TimeSpan ToTimespan(double? length) => length.HasValue ? TimeSpan.FromSeconds(length.Value) : TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private InspectionResult ToResult(LsDvdResult result)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Tracks = ToTracks(result.Tracks)
|
||||
};
|
||||
}
|
||||
}
|
||||
14
src/Tasks/Inspection/TrackAudio.cs
Normal file
14
src/Tasks/Inspection/TrackAudio.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public sealed record class TrackAudio
|
||||
{
|
||||
public int Index { get; set; }
|
||||
|
||||
public required string LangCode { get; set; }
|
||||
|
||||
public required string Language { get; set; }
|
||||
|
||||
public required string Codec { get; set; }
|
||||
|
||||
public int Channels { get; set; }
|
||||
}
|
||||
6
src/Tasks/Inspection/TrackCell.cs
Normal file
6
src/Tasks/Inspection/TrackCell.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public sealed record class TrackCell
|
||||
{
|
||||
public TimeSpan Length { get; set; }
|
||||
}
|
||||
8
src/Tasks/Inspection/TrackSubtitle.cs
Normal file
8
src/Tasks/Inspection/TrackSubtitle.cs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
namespace MediaOrganizer.Tasks.Inspection;
|
||||
|
||||
public sealed record class TrackSubtitle
|
||||
{
|
||||
public required string LangCode { get; set; }
|
||||
|
||||
public required string Language { get; set; }
|
||||
}
|
||||
132
src/Tools/LsDvd.cs
Executable file
132
src/Tools/LsDvd.cs
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
using System.Buffers;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Pipelines;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
using MediaOrganizer.Tools.Types.LsDvd;
|
||||
|
||||
using Microsoft.Xml.Serialization.GeneratedAssembly;
|
||||
|
||||
namespace MediaOrganizer.Tools;
|
||||
|
||||
public sealed class LsDvd(string path)
|
||||
{
|
||||
private static ReadOnlySpan<byte> XmlDeclaration => "<?xml"u8;
|
||||
|
||||
public int? Title { get; init; }
|
||||
|
||||
public async ValueTask<LsDvdResult> RunAsync()
|
||||
{
|
||||
StringBuilder builder = new("-q -Ox -x ");
|
||||
if (Title is not null)
|
||||
{
|
||||
builder.Append("-t ");
|
||||
builder.Append(Title);
|
||||
builder.Append(' ');
|
||||
}
|
||||
builder.Append('"');
|
||||
builder.Append(path);
|
||||
builder.Append('"');
|
||||
|
||||
ProcessStartInfo processStartInfo = new("lsdvd")
|
||||
{
|
||||
Arguments = builder.ToString(),
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardError = true,
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
using Process process = new()
|
||||
{
|
||||
StartInfo = processStartInfo
|
||||
};
|
||||
|
||||
StringBuilder errorText = new();
|
||||
process.ErrorDataReceived += (s, e) => errorText.AppendLine(e.Data);
|
||||
|
||||
LsDvdResult result;
|
||||
bool error;
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
PipeReader streamReader = PipeReader.Create(process.StandardOutput.BaseStream);
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
if (!await ReadToXml(streamReader))
|
||||
{
|
||||
throw new XmlException();
|
||||
}
|
||||
|
||||
await using (var serializerStream = streamReader.AsStream())
|
||||
{
|
||||
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
XmlSerializer serializer = new LsDvdResultSerializer();
|
||||
result = (LsDvdResult)serializer.Deserialize(serializerStream)!;
|
||||
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||
}
|
||||
|
||||
await process.WaitForExitAsync().ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (error = !process.HasExited)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
return error ? throw new Exception(errorText.ToString()) : result;
|
||||
|
||||
static async ValueTask<bool> ReadToXml(PipeReader streamReader)
|
||||
{
|
||||
bool xmlDeclaration = false;
|
||||
for (ReadResult read = default; !(xmlDeclaration || read.IsCompleted);)
|
||||
{
|
||||
read = await streamReader.ReadAsync();
|
||||
SequencePosition
|
||||
consumed = read.Buffer.Start,
|
||||
examined = read.Buffer.Start;
|
||||
|
||||
SequenceReader<byte> reader = new(read.Buffer);
|
||||
while (!(xmlDeclaration = IsXmlDeclaration(reader, consumed, examined)) && reader.TryRead(out var symbol))
|
||||
{
|
||||
if (symbol == '\r' || symbol == '\n')
|
||||
{
|
||||
consumed = reader.Position;
|
||||
}
|
||||
|
||||
examined = reader.Position;
|
||||
}
|
||||
|
||||
streamReader.AdvanceTo(consumed, examined);
|
||||
}
|
||||
|
||||
return xmlDeclaration;
|
||||
|
||||
static bool IsXmlDeclaration(in SequenceReader<byte> reader, SequencePosition left, SequencePosition right)
|
||||
{
|
||||
if (XmlDeclaration.Length > reader.Remaining)
|
||||
{
|
||||
goto exit;
|
||||
}
|
||||
|
||||
if (!left.Equals(right))
|
||||
{
|
||||
goto exit;
|
||||
}
|
||||
|
||||
return reader.IsNext(XmlDeclaration, false);
|
||||
|
||||
exit:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue