Add Inspection command
This commit is contained in:
parent
af2d4d6139
commit
3a48988dfb
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
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; }
|
||||||
|
}
|
||||||
54
src/Tasks/InspectorFactory.cs
Normal file
54
src/Tasks/InspectorFactory.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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