diff --git a/src/Commands/Source/InspectCommand.cs b/src/Commands/Source/InspectCommand.cs
new file mode 100755
index 0000000..5b5a7c5
--- /dev/null
+++ b/src/Commands/Source/InspectCommand.cs
@@ -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
+{
+ /// Provides detailed information about a media source.
+ /// Path to DVD image (.iso), BluRay folder (parent of BDMV), FFmpeg bluray-protocol (bluray:), or BluRay Movie Playlist File (mpls).
+ /// -p|--playlist|-t|--title, Specify Title, or Playlist within a DVD image, or BluRay folder.
+ [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;
+ }
+ }
+
+ ///
+ /// Track {0}
+ ///
+ private static CompositeFormat TrackIndexFormat => field ??= CompositeFormat.Parse("Track {0}");
+ ///
+ /// Resolution: {0}x{1}
+ ///
+ private static CompositeFormat ResolutionFormat => field ??= CompositeFormat.Parse("Resolution: {0}x{1}");
+ ///
+ /// Duration: {0}
+ ///
+ private static CompositeFormat DurationFormat => field ??= CompositeFormat.Parse("Duration: {0}");
+ ///
+ /// FPSFormat: {0}
+ ///
+ private static CompositeFormat FPSFormat => field ??= CompositeFormat.Parse("FPS: {0}");
+ ///
+ /// Audio Track {0}
+ ///
+ private static CompositeFormat AudioTrackFormat => field ??= CompositeFormat.Parse("Audio Track {0}");
+ ///
+ /// Language: {0} ({1})
+ ///
+ private static CompositeFormat AudioLanguageFormat => field ??= CompositeFormat.Parse("Language: {0} ({1})");
+ ///
+ /// Codec: {0}
+ ///
+ private static CompositeFormat AudioCodecFormat => field ??= CompositeFormat.Parse("Codec: {0}");
+}
diff --git a/src/System/IO/CompositeFormatTextWriter.cs b/src/System/IO/CompositeFormatTextWriter.cs
new file mode 100644
index 0000000..5723ac4
--- /dev/null
+++ b/src/System/IO/CompositeFormatTextWriter.cs
@@ -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));
+ }
+}
diff --git a/src/Tasks/InspectFactory.cs b/src/Tasks/InspectFactory.cs
new file mode 100644
index 0000000..1782fe4
--- /dev/null
+++ b/src/Tasks/InspectFactory.cs
@@ -0,0 +1,54 @@
+using MediaOrganizer.Tasks.Inspection;
+
+namespace MediaOrganizer.Tasks;
+
+public class InspectorFactory
+{
+ private static ReadOnlySpan BluRayProtocol => "bluray:";
+ private static ReadOnlySpan IsoExtension => ".iso";
+ private static ReadOnlySpan 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();
+ }
+ }
+}
diff --git a/src/Tasks/Inspection/BluRayInspector.cs b/src/Tasks/Inspection/BluRayInspector.cs
new file mode 100644
index 0000000..4353afa
--- /dev/null
+++ b/src/Tasks/Inspection/BluRayInspector.cs
@@ -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 RunAsync() => ValueTask.FromResult(default(InspectionResult)!);
+}
diff --git a/src/Tasks/Inspection/IInspector.cs b/src/Tasks/Inspection/IInspector.cs
new file mode 100644
index 0000000..e76327d
--- /dev/null
+++ b/src/Tasks/Inspection/IInspector.cs
@@ -0,0 +1,13 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace MediaOrganizer.Tasks.Inspection;
+
+public interface IInspector
+{
+ [DisallowNull]
+ int? Index { get; set; }
+
+ string Source { get; }
+
+ ValueTask RunAsync();
+}
diff --git a/src/Tasks/Inspection/InspectionResult.cs b/src/Tasks/Inspection/InspectionResult.cs
new file mode 100644
index 0000000..bf5bc34
--- /dev/null
+++ b/src/Tasks/Inspection/InspectionResult.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace MediaOrganizer.Tasks.Inspection;
+
+public partial record class InspectionResult
+{
+ public required IReadOnlyList Tracks { get; set; }
+
+ [JsonSourceGenerationOptions(
+ GenerationMode = JsonSourceGenerationMode.Metadata
+ )]
+ [JsonSerializable(typeof(InspectionResult))]
+ internal partial class InspectionResultSerializerContext : JsonSerializerContext;
+}
diff --git a/src/Tasks/Inspection/InspectionTrack.cs b/src/Tasks/Inspection/InspectionTrack.cs
new file mode 100644
index 0000000..0aa33b4
--- /dev/null
+++ b/src/Tasks/Inspection/InspectionTrack.cs
@@ -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 Playlist { get; set; }
+
+ public required IReadOnlyList Audio { get; set; }
+
+ public required IReadOnlyList Subtitles { get; set; }
+}
diff --git a/src/Tasks/Inspection/LsDvdInspector.cs b/src/Tasks/Inspection/LsDvdInspector.cs
new file mode 100644
index 0000000..a86a76e
--- /dev/null
+++ b/src/Tasks/Inspection/LsDvdInspector.cs
@@ -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 RunAsync()
+ {
+ LsDvd lsDvd = new(path)
+ {
+ Title = Index
+ };
+
+ return ToResult(await lsDvd.RunAsync());
+ }
+
+ private static List ToAudio(List