浏览代码

Revert "Remove DvdLib (#9068)"

This reverts commit db1913b08fac0749133634efebd1ee7a7876147a.
Shadowghost 2 年之前
父节点
当前提交
519709bf10

+ 25 - 0
DvdLib/BigEndianBinaryReader.cs

@@ -0,0 +1,25 @@
+#pragma warning disable CS1591
+
+using System.Buffers.Binary;
+using System.IO;
+
+namespace DvdLib
+{
+    public class BigEndianBinaryReader : BinaryReader
+    {
+        public BigEndianBinaryReader(Stream input)
+            : base(input)
+        {
+        }
+
+        public override ushort ReadUInt16()
+        {
+            return BinaryPrimitives.ReadUInt16BigEndian(base.ReadBytes(2));
+        }
+
+        public override uint ReadUInt32()
+        {
+            return BinaryPrimitives.ReadUInt32BigEndian(base.ReadBytes(4));
+        }
+    }
+}

+ 20 - 0
DvdLib/DvdLib.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
+  <PropertyGroup>
+    <ProjectGuid>{713F42B5-878E-499D-A878-E4C652B1D5E8}</ProjectGuid>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="..\SharedVersion.cs" />
+  </ItemGroup>
+
+  <PropertyGroup>
+    <TargetFramework>net7.0</TargetFramework>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <AnalysisMode>AllDisabledByDefault</AnalysisMode>
+    <Nullable>disable</Nullable>
+  </PropertyGroup>
+
+</Project>

+ 23 - 0
DvdLib/Ifo/Cell.cs

@@ -0,0 +1,23 @@
+#pragma warning disable CS1591
+
+using System.IO;
+
+namespace DvdLib.Ifo
+{
+    public class Cell
+    {
+        public CellPlaybackInfo PlaybackInfo { get; private set; }
+
+        public CellPositionInfo PositionInfo { get; private set; }
+
+        internal void ParsePlayback(BinaryReader br)
+        {
+            PlaybackInfo = new CellPlaybackInfo(br);
+        }
+
+        internal void ParsePosition(BinaryReader br)
+        {
+            PositionInfo = new CellPositionInfo(br);
+        }
+    }
+}

+ 52 - 0
DvdLib/Ifo/CellPlaybackInfo.cs

@@ -0,0 +1,52 @@
+#pragma warning disable CS1591
+
+using System.IO;
+
+namespace DvdLib.Ifo
+{
+    public enum BlockMode
+    {
+        NotInBlock = 0,
+        FirstCell = 1,
+        InBlock = 2,
+        LastCell = 3,
+    }
+
+    public enum BlockType
+    {
+        Normal = 0,
+        Angle = 1,
+    }
+
+    public enum PlaybackMode
+    {
+        Normal = 0,
+        StillAfterEachVOBU = 1,
+    }
+
+    public class CellPlaybackInfo
+    {
+        public readonly BlockMode Mode;
+        public readonly BlockType Type;
+        public readonly bool SeamlessPlay;
+        public readonly bool Interleaved;
+        public readonly bool STCDiscontinuity;
+        public readonly bool SeamlessAngle;
+        public readonly PlaybackMode PlaybackMode;
+        public readonly bool Restricted;
+        public readonly byte StillTime;
+        public readonly byte CommandNumber;
+        public readonly DvdTime PlaybackTime;
+        public readonly uint FirstSector;
+        public readonly uint FirstILVUEndSector;
+        public readonly uint LastVOBUStartSector;
+        public readonly uint LastSector;
+
+        internal CellPlaybackInfo(BinaryReader br)
+        {
+            br.BaseStream.Seek(0x4, SeekOrigin.Current);
+            PlaybackTime = new DvdTime(br.ReadBytes(4));
+            br.BaseStream.Seek(0x10, SeekOrigin.Current);
+        }
+    }
+}

+ 19 - 0
DvdLib/Ifo/CellPositionInfo.cs

@@ -0,0 +1,19 @@
+#pragma warning disable CS1591
+
+using System.IO;
+
+namespace DvdLib.Ifo
+{
+    public class CellPositionInfo
+    {
+        public readonly ushort VOBId;
+        public readonly byte CellId;
+
+        internal CellPositionInfo(BinaryReader br)
+        {
+            VOBId = br.ReadUInt16();
+            br.ReadByte();
+            CellId = br.ReadByte();
+        }
+    }
+}

+ 20 - 0
DvdLib/Ifo/Chapter.cs

@@ -0,0 +1,20 @@
+#pragma warning disable CS1591
+
+namespace DvdLib.Ifo
+{
+    public class Chapter
+    {
+        public ushort ProgramChainNumber { get; private set; }
+
+        public ushort ProgramNumber { get; private set; }
+
+        public uint ChapterNumber { get; private set; }
+
+        public Chapter(ushort pgcNum, ushort programNum, uint chapterNum)
+        {
+            ProgramChainNumber = pgcNum;
+            ProgramNumber = programNum;
+            ChapterNumber = chapterNum;
+        }
+    }
+}

+ 167 - 0
DvdLib/Ifo/Dvd.cs

@@ -0,0 +1,167 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+
+namespace DvdLib.Ifo
+{
+    public class Dvd
+    {
+        private readonly ushort _titleSetCount;
+        public readonly List<Title> Titles;
+
+        private ushort _titleCount;
+        public readonly Dictionary<ushort, string> VTSPaths = new Dictionary<ushort, string>();
+        public Dvd(string path)
+        {
+            Titles = new List<Title>();
+            var allFiles = new DirectoryInfo(path).GetFiles(path, SearchOption.AllDirectories);
+
+            var vmgPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.IFO", StringComparison.OrdinalIgnoreCase)) ??
+                allFiles.FirstOrDefault(i => string.Equals(i.Name, "VIDEO_TS.BUP", StringComparison.OrdinalIgnoreCase));
+
+            if (vmgPath == null)
+            {
+                foreach (var ifo in allFiles)
+                {
+                    if (!string.Equals(ifo.Extension, ".ifo", StringComparison.OrdinalIgnoreCase))
+                    {
+                        continue;
+                    }
+
+                    var nums = ifo.Name.Split('_', StringSplitOptions.RemoveEmptyEntries);
+                    if (nums.Length >= 2 && ushort.TryParse(nums[1], out var ifoNumber))
+                    {
+                        ReadVTS(ifoNumber, ifo.FullName);
+                    }
+                }
+            }
+            else
+            {
+                using (var vmgFs = new FileStream(vmgPath.FullName, FileMode.Open, FileAccess.Read, FileShare.Read))
+                {
+                    using (var vmgRead = new BigEndianBinaryReader(vmgFs))
+                    {
+                        vmgFs.Seek(0x3E, SeekOrigin.Begin);
+                        _titleSetCount = vmgRead.ReadUInt16();
+
+                        // read address of TT_SRPT
+                        vmgFs.Seek(0xC4, SeekOrigin.Begin);
+                        uint ttSectorPtr = vmgRead.ReadUInt32();
+                        vmgFs.Seek(ttSectorPtr * 2048, SeekOrigin.Begin);
+                        ReadTT_SRPT(vmgRead);
+                    }
+                }
+
+                for (ushort titleSetNum = 1; titleSetNum <= _titleSetCount; titleSetNum++)
+                {
+                    ReadVTS(titleSetNum, allFiles);
+                }
+            }
+        }
+
+        private void ReadTT_SRPT(BinaryReader read)
+        {
+            _titleCount = read.ReadUInt16();
+            read.BaseStream.Seek(6, SeekOrigin.Current);
+            for (uint titleNum = 1; titleNum <= _titleCount; titleNum++)
+            {
+                var t = new Title(titleNum);
+                t.ParseTT_SRPT(read);
+                Titles.Add(t);
+            }
+        }
+
+        private void ReadVTS(ushort vtsNum, IReadOnlyList<FileInfo> allFiles)
+        {
+            var filename = string.Format(CultureInfo.InvariantCulture, "VTS_{0:00}_0.IFO", vtsNum);
+
+            var vtsPath = allFiles.FirstOrDefault(i => string.Equals(i.Name, filename, StringComparison.OrdinalIgnoreCase)) ??
+                allFiles.FirstOrDefault(i => string.Equals(i.Name, Path.ChangeExtension(filename, ".bup"), StringComparison.OrdinalIgnoreCase));
+
+            if (vtsPath == null)
+            {
+                throw new FileNotFoundException("Unable to find VTS IFO file");
+            }
+
+            ReadVTS(vtsNum, vtsPath.FullName);
+        }
+
+        private void ReadVTS(ushort vtsNum, string vtsPath)
+        {
+            VTSPaths[vtsNum] = vtsPath;
+
+            using (var vtsFs = new FileStream(vtsPath, FileMode.Open, FileAccess.Read, FileShare.Read))
+            {
+                using (var vtsRead = new BigEndianBinaryReader(vtsFs))
+                {
+                    // Read VTS_PTT_SRPT
+                    vtsFs.Seek(0xC8, SeekOrigin.Begin);
+                    uint vtsPttSrptSecPtr = vtsRead.ReadUInt32();
+                    uint baseAddr = (vtsPttSrptSecPtr * 2048);
+                    vtsFs.Seek(baseAddr, SeekOrigin.Begin);
+
+                    ushort numTitles = vtsRead.ReadUInt16();
+                    vtsRead.ReadUInt16();
+                    uint endaddr = vtsRead.ReadUInt32();
+                    uint[] offsets = new uint[numTitles];
+                    for (ushort titleNum = 0; titleNum < numTitles; titleNum++)
+                    {
+                        offsets[titleNum] = vtsRead.ReadUInt32();
+                    }
+
+                    for (uint titleNum = 0; titleNum < numTitles; titleNum++)
+                    {
+                        uint chapNum = 1;
+                        vtsFs.Seek(baseAddr + offsets[titleNum], SeekOrigin.Begin);
+                        var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum + 1));
+                        if (t == null)
+                        {
+                            continue;
+                        }
+
+                        do
+                        {
+                            t.Chapters.Add(new Chapter(vtsRead.ReadUInt16(), vtsRead.ReadUInt16(), chapNum));
+                            if (titleNum + 1 < numTitles && vtsFs.Position == (baseAddr + offsets[titleNum + 1]))
+                            {
+                                break;
+                            }
+
+                            chapNum++;
+                        }
+                        while (vtsFs.Position < (baseAddr + endaddr));
+                    }
+
+                    // Read VTS_PGCI
+                    vtsFs.Seek(0xCC, SeekOrigin.Begin);
+                    uint vtsPgciSecPtr = vtsRead.ReadUInt32();
+                    vtsFs.Seek(vtsPgciSecPtr * 2048, SeekOrigin.Begin);
+
+                    long startByte = vtsFs.Position;
+
+                    ushort numPgcs = vtsRead.ReadUInt16();
+                    vtsFs.Seek(6, SeekOrigin.Current);
+                    for (ushort pgcNum = 1; pgcNum <= numPgcs; pgcNum++)
+                    {
+                        byte pgcCat = vtsRead.ReadByte();
+                        bool entryPgc = (pgcCat & 0x80) != 0;
+                        uint titleNum = (uint)(pgcCat & 0x7F);
+
+                        vtsFs.Seek(3, SeekOrigin.Current);
+                        uint vtsPgcOffset = vtsRead.ReadUInt32();
+
+                        var t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum));
+                        if (t != null)
+                        {
+                            t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}

+ 39 - 0
DvdLib/Ifo/DvdTime.cs

@@ -0,0 +1,39 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace DvdLib.Ifo
+{
+    public class DvdTime
+    {
+        public readonly byte Hour, Minute, Second, Frames, FrameRate;
+
+        public DvdTime(byte[] data)
+        {
+            Hour = GetBCDValue(data[0]);
+            Minute = GetBCDValue(data[1]);
+            Second = GetBCDValue(data[2]);
+            Frames = GetBCDValue((byte)(data[3] & 0x3F));
+
+            if ((data[3] & 0x80) != 0)
+            {
+                FrameRate = 30;
+            }
+            else if ((data[3] & 0x40) != 0)
+            {
+                FrameRate = 25;
+            }
+        }
+
+        private static byte GetBCDValue(byte data)
+        {
+            return (byte)((((data & 0xF0) >> 4) * 10) + (data & 0x0F));
+        }
+
+        public static explicit operator TimeSpan(DvdTime time)
+        {
+            int ms = (int)(((1.0 / (double)time.FrameRate) * time.Frames) * 1000.0);
+            return new TimeSpan(0, time.Hour, time.Minute, time.Second, ms);
+        }
+    }
+}

+ 16 - 0
DvdLib/Ifo/Program.cs

@@ -0,0 +1,16 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+
+namespace DvdLib.Ifo
+{
+    public class Program
+    {
+        public IReadOnlyList<Cell> Cells { get; }
+
+        public Program(List<Cell> cells)
+        {
+            Cells = cells;
+        }
+    }
+}

+ 121 - 0
DvdLib/Ifo/ProgramChain.cs

@@ -0,0 +1,121 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace DvdLib.Ifo
+{
+    public enum ProgramPlaybackMode
+    {
+        Sequential,
+        Random,
+        Shuffle
+    }
+
+    public class ProgramChain
+    {
+        private byte _programCount;
+        public readonly List<Program> Programs;
+
+        private byte _cellCount;
+        public readonly List<Cell> Cells;
+
+        public DvdTime PlaybackTime { get; private set; }
+
+        public UserOperation ProhibitedUserOperations { get; private set; }
+
+        public byte[] AudioStreamControl { get; private set; } // 8*2 entries
+        public byte[] SubpictureStreamControl { get; private set; } // 32*4 entries
+
+        private ushort _nextProgramNumber;
+
+        private ushort _prevProgramNumber;
+
+        private ushort _goupProgramNumber;
+
+        public ProgramPlaybackMode PlaybackMode { get; private set; }
+
+        public uint ProgramCount { get; private set; }
+
+        public byte StillTime { get; private set; }
+
+        public byte[] Palette { get; private set; } // 16*4 entries
+
+        private ushort _commandTableOffset;
+
+        private ushort _programMapOffset;
+        private ushort _cellPlaybackOffset;
+        private ushort _cellPositionOffset;
+
+        public readonly uint VideoTitleSetIndex;
+
+        internal ProgramChain(uint vtsPgcNum)
+        {
+            VideoTitleSetIndex = vtsPgcNum;
+            Cells = new List<Cell>();
+            Programs = new List<Program>();
+        }
+
+        internal void ParseHeader(BinaryReader br)
+        {
+            long startPos = br.BaseStream.Position;
+
+            br.ReadUInt16();
+            _programCount = br.ReadByte();
+            _cellCount = br.ReadByte();
+            PlaybackTime = new DvdTime(br.ReadBytes(4));
+            ProhibitedUserOperations = (UserOperation)br.ReadUInt32();
+            AudioStreamControl = br.ReadBytes(16);
+            SubpictureStreamControl = br.ReadBytes(128);
+
+            _nextProgramNumber = br.ReadUInt16();
+            _prevProgramNumber = br.ReadUInt16();
+            _goupProgramNumber = br.ReadUInt16();
+
+            StillTime = br.ReadByte();
+            byte pbMode = br.ReadByte();
+            if (pbMode == 0)
+            {
+                PlaybackMode = ProgramPlaybackMode.Sequential;
+            }
+            else
+            {
+                PlaybackMode = ((pbMode & 0x80) == 0) ? ProgramPlaybackMode.Random : ProgramPlaybackMode.Shuffle;
+            }
+
+            ProgramCount = (uint)(pbMode & 0x7F);
+
+            Palette = br.ReadBytes(64);
+            _commandTableOffset = br.ReadUInt16();
+            _programMapOffset = br.ReadUInt16();
+            _cellPlaybackOffset = br.ReadUInt16();
+            _cellPositionOffset = br.ReadUInt16();
+
+            // read position info
+            br.BaseStream.Seek(startPos + _cellPositionOffset, SeekOrigin.Begin);
+            for (int cellNum = 0; cellNum < _cellCount; cellNum++)
+            {
+                var c = new Cell();
+                c.ParsePosition(br);
+                Cells.Add(c);
+            }
+
+            br.BaseStream.Seek(startPos + _cellPlaybackOffset, SeekOrigin.Begin);
+            for (int cellNum = 0; cellNum < _cellCount; cellNum++)
+            {
+                Cells[cellNum].ParsePlayback(br);
+            }
+
+            br.BaseStream.Seek(startPos + _programMapOffset, SeekOrigin.Begin);
+            var cellNumbers = new List<int>();
+            for (int progNum = 0; progNum < _programCount; progNum++) cellNumbers.Add(br.ReadByte() - 1);
+
+            for (int i = 0; i < cellNumbers.Count; i++)
+            {
+                int max = (i + 1 == cellNumbers.Count) ? _cellCount : cellNumbers[i + 1];
+                Programs.Add(new Program(Cells.Where((c, idx) => idx >= cellNumbers[i] && idx < max).ToList()));
+            }
+        }
+    }
+}

+ 70 - 0
DvdLib/Ifo/Title.cs

@@ -0,0 +1,70 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+using System.IO;
+
+namespace DvdLib.Ifo
+{
+    public class Title
+    {
+        public uint TitleNumber { get; private set; }
+
+        public uint AngleCount { get; private set; }
+
+        public ushort ChapterCount { get; private set; }
+
+        public byte VideoTitleSetNumber { get; private set; }
+
+        private ushort _parentalManagementMask;
+        private byte _titleNumberInVTS;
+        private uint _vtsStartSector; // relative to start of entire disk
+
+        public ProgramChain EntryProgramChain { get; private set; }
+
+        public readonly List<ProgramChain> ProgramChains;
+
+        public readonly List<Chapter> Chapters;
+
+        public Title(uint titleNum)
+        {
+            ProgramChains = new List<ProgramChain>();
+            Chapters = new List<Chapter>();
+            Chapters = new List<Chapter>();
+            TitleNumber = titleNum;
+        }
+
+        public bool IsVTSTitle(uint vtsNum, uint vtsTitleNum)
+        {
+            return (vtsNum == VideoTitleSetNumber && vtsTitleNum == _titleNumberInVTS);
+        }
+
+        internal void ParseTT_SRPT(BinaryReader br)
+        {
+            byte titleType = br.ReadByte();
+            // TODO parse Title Type
+
+            AngleCount = br.ReadByte();
+            ChapterCount = br.ReadUInt16();
+            _parentalManagementMask = br.ReadUInt16();
+            VideoTitleSetNumber = br.ReadByte();
+            _titleNumberInVTS = br.ReadByte();
+            _vtsStartSector = br.ReadUInt32();
+        }
+
+        internal void AddPgc(BinaryReader br, long startByte, bool entryPgc, uint pgcNum)
+        {
+            long curPos = br.BaseStream.Position;
+            br.BaseStream.Seek(startByte, SeekOrigin.Begin);
+
+            var pgc = new ProgramChain(pgcNum);
+            pgc.ParseHeader(br);
+            ProgramChains.Add(pgc);
+            if (entryPgc)
+            {
+                EntryProgramChain = pgc;
+            }
+
+            br.BaseStream.Seek(curPos, SeekOrigin.Begin);
+        }
+    }
+}

+ 37 - 0
DvdLib/Ifo/UserOperation.cs

@@ -0,0 +1,37 @@
+#pragma warning disable CS1591
+
+using System;
+
+namespace DvdLib.Ifo
+{
+    [Flags]
+    public enum UserOperation
+    {
+        None = 0,
+        TitleOrTimePlay = 1,
+        ChapterSearchOrPlay = 2,
+        TitlePlay = 4,
+        Stop = 8,
+        GoUp = 16,
+        TimeOrChapterSearch = 32,
+        PrevOrTopProgramSearch = 64,
+        NextProgramSearch = 128,
+        ForwardScan = 256,
+        BackwardScan = 512,
+        TitleMenuCall = 1024,
+        RootMenuCall = 2048,
+        SubpictureMenuCall = 4096,
+        AudioMenuCall = 8192,
+        AngleMenuCall = 16384,
+        ChapterMenuCall = 32768,
+        Resume = 65536,
+        ButtonSelectOrActive = 131072,
+        StillOff = 262144,
+        PauseOn = 524288,
+        AudioStreamChange = 1048576,
+        SubpictureStreamChange = 2097152,
+        AngleChange = 4194304,
+        KaraokeAudioPresentationModeChange = 8388608,
+        VideoPresentationModeChange = 16777216,
+    }
+}

+ 21 - 0
DvdLib/Properties/AssemblyInfo.cs

@@ -0,0 +1,21 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("DvdLib")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Jellyfin Project")]
+[assembly: AssemblyProduct("Jellyfin Server")]
+[assembly: AssemblyCopyright("Copyright ©  2019 Jellyfin Contributors. Code released under the GNU General Public License")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+[assembly: NeutralResourcesLanguage("en")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components.  If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]

+ 4 - 0
Emby.Server.Implementations/ApplicationHost.cs

@@ -80,11 +80,13 @@ using MediaBrowser.Controller.Subtitles;
 using MediaBrowser.Controller.SyncPlay;
 using MediaBrowser.Controller.TV;
 using MediaBrowser.LocalMetadata.Savers;
+using MediaBrowser.MediaEncoding.BdInfo;
 using MediaBrowser.MediaEncoding.Subtitles;
 using MediaBrowser.Model.Cryptography;
 using MediaBrowser.Model.Dlna;
 using MediaBrowser.Model.Globalization;
 using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
 using MediaBrowser.Model.Net;
 using MediaBrowser.Model.Serialization;
 using MediaBrowser.Model.System;
@@ -529,6 +531,8 @@ namespace Emby.Server.Implementations
 
             serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
 
+            serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
+
             serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
             serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
 

+ 6 - 0
Jellyfin.sln

@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jel
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DvdLib", "DvdLib\DvdLib.csproj", "{713F42B5-878E-499D-A878-E4C652B1D5E8}"
+EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
@@ -135,6 +137,10 @@ Global
 		{89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = Release|Any CPU
+		{713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{713F42B5-878E-499D-A878-E4C652B1D5E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{713F42B5-878E-499D-A878-E4C652B1D5E8}.Release|Any CPU.Build.0 = Release|Any CPU
 		{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU

+ 8 - 0
MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs

@@ -187,5 +187,13 @@ namespace MediaBrowser.Controller.MediaEncoding
         /// <param name="path">The path.</param>
         /// <param name="pathType">The type of path.</param>
         void UpdateEncoderPath(string path, string pathType);
+
+        /// <summary>
+        /// Gets the primary playlist of .vob files.
+        /// </summary>
+        /// <param name="path">The to the .vob files.</param>
+        /// <param name="titleNumber">The title number to start with.</param>
+        /// <returns>A playlist.</returns>
+        IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
     }
 }

+ 83 - 0
MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs

@@ -0,0 +1,83 @@
+#pragma warning disable CS1591
+
+using System;
+using System.Linq;
+using BDInfo.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.MediaEncoding.BdInfo
+{
+    public class BdInfoDirectoryInfo : IDirectoryInfo
+    {
+        private readonly IFileSystem _fileSystem;
+
+        private readonly FileSystemMetadata _impl;
+
+        public BdInfoDirectoryInfo(IFileSystem fileSystem, string path)
+        {
+            _fileSystem = fileSystem;
+            _impl = _fileSystem.GetDirectoryInfo(path);
+        }
+
+        private BdInfoDirectoryInfo(IFileSystem fileSystem, FileSystemMetadata impl)
+        {
+            _fileSystem = fileSystem;
+            _impl = impl;
+        }
+
+        public string Name => _impl.Name;
+
+        public string FullName => _impl.FullName;
+
+        public IDirectoryInfo? Parent
+        {
+            get
+            {
+                var parentFolder = System.IO.Path.GetDirectoryName(_impl.FullName);
+                if (parentFolder is not null)
+                {
+                    return new BdInfoDirectoryInfo(_fileSystem, parentFolder);
+                }
+
+                return null;
+            }
+        }
+
+        public IDirectoryInfo[] GetDirectories()
+        {
+            return Array.ConvertAll(
+                _fileSystem.GetDirectories(_impl.FullName).ToArray(),
+                x => new BdInfoDirectoryInfo(_fileSystem, x));
+        }
+
+        public IFileInfo[] GetFiles()
+        {
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(_impl.FullName).ToArray(),
+                x => new BdInfoFileInfo(x));
+        }
+
+        public IFileInfo[] GetFiles(string searchPattern)
+        {
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(_impl.FullName, new[] { searchPattern }, false, false).ToArray(),
+                x => new BdInfoFileInfo(x));
+        }
+
+        public IFileInfo[] GetFiles(string searchPattern, System.IO.SearchOption searchOption)
+        {
+            return Array.ConvertAll(
+                _fileSystem.GetFiles(
+                    _impl.FullName,
+                    new[] { searchPattern },
+                    false,
+                    (searchOption & System.IO.SearchOption.AllDirectories) == System.IO.SearchOption.AllDirectories).ToArray(),
+                x => new BdInfoFileInfo(x));
+        }
+
+        public static IDirectoryInfo FromFileSystemPath(IFileSystem fs, string path)
+        {
+            return new BdInfoDirectoryInfo(fs, path);
+        }
+    }
+}

+ 194 - 0
MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs

@@ -0,0 +1,194 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BDInfo;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.MediaInfo;
+
+namespace MediaBrowser.MediaEncoding.BdInfo
+{
+    /// <summary>
+    /// Class BdInfoExaminer.
+    /// </summary>
+    public class BdInfoExaminer : IBlurayExaminer
+    {
+        private readonly IFileSystem _fileSystem;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BdInfoExaminer" /> class.
+        /// </summary>
+        /// <param name="fileSystem">The filesystem.</param>
+        public BdInfoExaminer(IFileSystem fileSystem)
+        {
+            _fileSystem = fileSystem;
+        }
+
+        /// <summary>
+        /// Gets the disc info.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>BlurayDiscInfo.</returns>
+        public BlurayDiscInfo GetDiscInfo(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                throw new ArgumentNullException(nameof(path));
+            }
+
+            var bdrom = new BDROM(BdInfoDirectoryInfo.FromFileSystemPath(_fileSystem, path));
+
+            bdrom.Scan();
+
+            // Get the longest playlist
+            var playlist = bdrom.PlaylistFiles.Values.OrderByDescending(p => p.TotalLength).FirstOrDefault(p => p.IsValid);
+
+            var outputStream = new BlurayDiscInfo
+            {
+                MediaStreams = Array.Empty<MediaStream>()
+            };
+
+            if (playlist is null)
+            {
+                return outputStream;
+            }
+
+            outputStream.Chapters = playlist.Chapters.ToArray();
+
+            outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+
+            var mediaStreams = new List<MediaStream>();
+
+            foreach (var stream in playlist.SortedStreams)
+            {
+                if (stream is TSVideoStream videoStream)
+                {
+                    AddVideoStream(mediaStreams, videoStream);
+                    continue;
+                }
+
+                if (stream is TSAudioStream audioStream)
+                {
+                    AddAudioStream(mediaStreams, audioStream);
+                    continue;
+                }
+
+                if (stream is TSTextStream textStream)
+                {
+                    AddSubtitleStream(mediaStreams, textStream);
+                    continue;
+                }
+
+                if (stream is TSGraphicsStream graphicsStream)
+                {
+                    AddSubtitleStream(mediaStreams, graphicsStream);
+                }
+            }
+
+            outputStream.MediaStreams = mediaStreams.ToArray();
+
+            outputStream.PlaylistName = playlist.Name;
+
+            if (playlist.StreamClips is not null && playlist.StreamClips.Any())
+            {
+                // Get the files in the playlist
+                outputStream.Files = playlist.StreamClips.Select(i => i.StreamFile.Name).ToArray();
+            }
+
+            return outputStream;
+        }
+
+        /// <summary>
+        /// Adds the video stream.
+        /// </summary>
+        /// <param name="streams">The streams.</param>
+        /// <param name="videoStream">The video stream.</param>
+        private void AddVideoStream(List<MediaStream> streams, TSVideoStream videoStream)
+        {
+            var mediaStream = new MediaStream
+            {
+                BitRate = Convert.ToInt32(videoStream.BitRate),
+                Width = videoStream.Width,
+                Height = videoStream.Height,
+                Codec = videoStream.CodecShortName,
+                IsInterlaced = videoStream.IsInterlaced,
+                Type = MediaStreamType.Video,
+                Index = streams.Count
+            };
+
+            if (videoStream.FrameRateDenominator > 0)
+            {
+                float frameRateEnumerator = videoStream.FrameRateEnumerator;
+                float frameRateDenominator = videoStream.FrameRateDenominator;
+
+                mediaStream.AverageFrameRate = mediaStream.RealFrameRate = frameRateEnumerator / frameRateDenominator;
+            }
+
+            streams.Add(mediaStream);
+        }
+
+        /// <summary>
+        /// Adds the audio stream.
+        /// </summary>
+        /// <param name="streams">The streams.</param>
+        /// <param name="audioStream">The audio stream.</param>
+        private void AddAudioStream(List<MediaStream> streams, TSAudioStream audioStream)
+        {
+            var stream = new MediaStream
+            {
+                Codec = audioStream.CodecShortName,
+                Language = audioStream.LanguageCode,
+                Channels = audioStream.ChannelCount,
+                SampleRate = audioStream.SampleRate,
+                Type = MediaStreamType.Audio,
+                Index = streams.Count
+            };
+
+            var bitrate = Convert.ToInt32(audioStream.BitRate);
+
+            if (bitrate > 0)
+            {
+                stream.BitRate = bitrate;
+            }
+
+            if (audioStream.LFE > 0)
+            {
+                stream.Channels = audioStream.ChannelCount + 1;
+            }
+
+            streams.Add(stream);
+        }
+
+        /// <summary>
+        /// Adds the subtitle stream.
+        /// </summary>
+        /// <param name="streams">The streams.</param>
+        /// <param name="textStream">The text stream.</param>
+        private void AddSubtitleStream(List<MediaStream> streams, TSTextStream textStream)
+        {
+            streams.Add(new MediaStream
+            {
+                Language = textStream.LanguageCode,
+                Codec = textStream.CodecShortName,
+                Type = MediaStreamType.Subtitle,
+                Index = streams.Count
+            });
+        }
+
+        /// <summary>
+        /// Adds the subtitle stream.
+        /// </summary>
+        /// <param name="streams">The streams.</param>
+        /// <param name="textStream">The text stream.</param>
+        private void AddSubtitleStream(List<MediaStream> streams, TSGraphicsStream textStream)
+        {
+            streams.Add(new MediaStream
+            {
+                Language = textStream.LanguageCode,
+                Codec = textStream.CodecShortName,
+                Type = MediaStreamType.Subtitle,
+                Index = streams.Count
+            });
+        }
+    }
+}

+ 41 - 0
MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs

@@ -0,0 +1,41 @@
+#pragma warning disable CS1591
+
+using System.IO;
+using MediaBrowser.Model.IO;
+
+namespace MediaBrowser.MediaEncoding.BdInfo
+{
+    public class BdInfoFileInfo : BDInfo.IO.IFileInfo
+    {
+        private FileSystemMetadata _impl;
+
+        public BdInfoFileInfo(FileSystemMetadata impl)
+        {
+            _impl = impl;
+        }
+
+        public string Name => _impl.Name;
+
+        public string FullName => _impl.FullName;
+
+        public string Extension => _impl.Extension;
+
+        public long Length => _impl.Length;
+
+        public bool IsDir => _impl.IsDirectory;
+
+        public Stream OpenRead()
+        {
+            return new FileStream(
+                FullName,
+                FileMode.Open,
+                FileAccess.Read,
+                FileShare.Read);
+        }
+
+        public StreamReader OpenText()
+        {
+            return new StreamReader(OpenRead());
+        }
+    }
+}

+ 79 - 0
MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs

@@ -865,6 +865,85 @@ namespace MediaBrowser.MediaEncoding.Encoder
             throw new NotImplementedException();
         }
 
+        /// <inheritdoc />
+        public IEnumerable<string> GetPrimaryPlaylistVobFiles(string path, uint? titleNumber)
+        {
+            // min size 300 mb
+            const long MinPlayableSize = 314572800;
+
+            // Try to eliminate menus and intros by skipping all files at the front of the list that are less than the minimum size
+            // Once we reach a file that is at least the minimum, return all subsequent ones
+            var allVobs = _fileSystem.GetFiles(path, true)
+                .Where(file => string.Equals(file.Extension, ".vob", StringComparison.OrdinalIgnoreCase))
+                .OrderBy(i => i.FullName)
+                .ToList();
+
+            // If we didn't find any satisfying the min length, just take them all
+            if (allVobs.Count == 0)
+            {
+                _logger.LogWarning("No vobs found in dvd structure.");
+                return Enumerable.Empty<string>();
+            }
+
+            if (titleNumber.HasValue)
+            {
+                var prefix = string.Format(
+                    CultureInfo.InvariantCulture,
+                    titleNumber.Value >= 10 ? "VTS_{0}_" : "VTS_0{0}_",
+                    titleNumber.Value);
+                var vobs = allVobs.Where(i => i.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToList();
+
+                if (vobs.Count > 0)
+                {
+                    var minSizeVobs = vobs
+                        .SkipWhile(f => f.Length < MinPlayableSize)
+                        .ToList();
+
+                    return minSizeVobs.Count == 0 ? vobs.Select(i => i.FullName) : minSizeVobs.Select(i => i.FullName);
+                }
+
+                _logger.LogWarning("Could not determine vob file list for {Path} using DvdLib. Will scan using file sizes.", path);
+            }
+
+            var files = allVobs
+                .SkipWhile(f => f.Length < MinPlayableSize)
+                .ToList();
+
+            // If we didn't find any satisfying the min length, just take them all
+            if (files.Count == 0)
+            {
+                _logger.LogWarning("Vob size filter resulted in zero matches. Taking all vobs.");
+                files = allVobs;
+            }
+
+            // Assuming they're named "vts_05_01", take all files whose second part matches that of the first file
+            if (files.Count > 0)
+            {
+                var parts = _fileSystem.GetFileNameWithoutExtension(files[0]).Split('_');
+
+                if (parts.Length == 3)
+                {
+                    var title = parts[1];
+
+                    files = files.TakeWhile(f =>
+                    {
+                        var fileParts = _fileSystem.GetFileNameWithoutExtension(f).Split('_');
+
+                        return fileParts.Length == 3 && string.Equals(title, fileParts[1], StringComparison.OrdinalIgnoreCase);
+                    }).ToList();
+
+                    // If this resulted in not getting any vobs, just take them all
+                    if (files.Count == 0)
+                    {
+                        _logger.LogWarning("Vob filename filter resulted in zero matches. Taking all vobs.");
+                        files = allVobs;
+                    }
+                }
+            }
+
+            return files.Select(i => i.FullName);
+        }
+
         public bool CanExtractSubtitles(string codec)
         {
             // TODO is there ever a case when a subtitle can't be extracted??

+ 1 - 0
MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj

@@ -22,6 +22,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <PackageReference Include="BDInfo" />
     <PackageReference Include="libse" />
     <PackageReference Include="Microsoft.Extensions.Http" />
     <PackageReference Include="System.Text.Encoding.CodePages" />

+ 39 - 0
MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs

@@ -0,0 +1,39 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+    /// <summary>
+    /// Represents the result of BDInfo output.
+    /// </summary>
+    public class BlurayDiscInfo
+    {
+        /// <summary>
+        /// Gets or sets the media streams.
+        /// </summary>
+        /// <value>The media streams.</value>
+        public MediaStream[] MediaStreams { get; set; }
+
+        /// <summary>
+        /// Gets or sets the run time ticks.
+        /// </summary>
+        /// <value>The run time ticks.</value>
+        public long? RunTimeTicks { get; set; }
+
+        /// <summary>
+        /// Gets or sets the files.
+        /// </summary>
+        /// <value>The files.</value>
+        public string[] Files { get; set; }
+
+        public string PlaylistName { get; set; }
+
+        /// <summary>
+        /// Gets or sets the chapters.
+        /// </summary>
+        /// <value>The chapters.</value>
+        public double[] Chapters { get; set; }
+    }
+}

+ 15 - 0
MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs

@@ -0,0 +1,15 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+    /// <summary>
+    /// Interface IBlurayExaminer.
+    /// </summary>
+    public interface IBlurayExaminer
+    {
+        /// <summary>
+        /// Gets the disc info.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>BlurayDiscInfo.</returns>
+        BlurayDiscInfo GetDiscInfo(string path);
+    }
+}

+ 1 - 0
MediaBrowser.Providers/MediaBrowser.Providers.csproj

@@ -8,6 +8,7 @@
   <ItemGroup>
     <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
     <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
+    <ProjectReference Include="..\DvdLib\DvdLib.csproj" />
   </ItemGroup>
 
   <ItemGroup>

+ 154 - 1
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -9,6 +9,7 @@ using System.IO;
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
+using DvdLib.Ifo;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
@@ -36,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo
         private readonly ILogger<FFProbeVideoInfo> _logger;
         private readonly IMediaEncoder _mediaEncoder;
         private readonly IItemRepository _itemRepo;
+        private readonly IBlurayExaminer _blurayExaminer;
         private readonly ILocalizationManager _localization;
         private readonly IEncodingManager _encodingManager;
         private readonly IServerConfigurationManager _config;
@@ -51,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
+            IBlurayExaminer blurayExaminer,
             ILocalizationManager localization,
             IEncodingManager encodingManager,
             IServerConfigurationManager config,
@@ -64,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
             _mediaSourceManager = mediaSourceManager;
             _mediaEncoder = mediaEncoder;
             _itemRepo = itemRepo;
+            _blurayExaminer = blurayExaminer;
             _localization = localization;
             _encodingManager = encodingManager;
             _config = config;
@@ -80,16 +84,47 @@ namespace MediaBrowser.Providers.MediaInfo
             CancellationToken cancellationToken)
             where T : Video
         {
+            BlurayDiscInfo blurayDiscInfo = null;
+
             Model.MediaInfo.MediaInfo mediaInfoResult = null;
 
             if (!item.IsShortcut || options.EnableRemoteContentProbe)
             {
+                string[] streamFileNames = null;
+
+                if (item.VideoType == VideoType.Dvd)
+                {
+                    streamFileNames = FetchFromDvdLib(item);
+
+                    if (streamFileNames.Length == 0)
+                    {
+                        _logger.LogError("No playable vobs found in dvd structure, skipping ffprobe.");
+                        return ItemUpdateType.MetadataImport;
+                    }
+                }
+                else if (item.VideoType == VideoType.BluRay)
+                {
+                    var inputPath = item.Path;
+
+                    blurayDiscInfo = GetBDInfo(inputPath);
+
+                    streamFileNames = blurayDiscInfo.Files;
+
+                    if (streamFileNames.Length == 0)
+                    {
+                        _logger.LogError("No playable vobs found in bluray structure, skipping ffprobe.");
+                        return ItemUpdateType.MetadataImport;
+                    }
+                }
+
+                streamFileNames ??= Array.Empty<string>();
+
                 mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
 
                 cancellationToken.ThrowIfCancellationRequested();
             }
 
-            await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false);
+            await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false);
 
             return ItemUpdateType.MetadataImport;
         }
@@ -129,6 +164,7 @@ namespace MediaBrowser.Providers.MediaInfo
             Video video,
             CancellationToken cancellationToken,
             Model.MediaInfo.MediaInfo mediaInfo,
+            BlurayDiscInfo blurayInfo,
             MetadataRefreshOptions options)
         {
             List<MediaStream> mediaStreams;
@@ -182,6 +218,10 @@ namespace MediaBrowser.Providers.MediaInfo
                 video.Container = mediaInfo.Container;
 
                 chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
+                if (blurayInfo is not null)
+                {
+                    FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
+                }
             }
             else
             {
@@ -277,6 +317,91 @@ namespace MediaBrowser.Providers.MediaInfo
             }
         }
 
+        private void FetchBdInfo(BaseItem item, ref ChapterInfo[] chapters, List<MediaStream> mediaStreams, BlurayDiscInfo blurayInfo)
+        {
+            var video = (Video)item;
+
+            // video.PlayableStreamFileNames = blurayInfo.Files.ToList();
+
+            // Use BD Info if it has multiple m2ts. Otherwise, treat it like a video file and rely more on ffprobe output
+            if (blurayInfo.Files.Length > 1)
+            {
+                int? currentHeight = null;
+                int? currentWidth = null;
+                int? currentBitRate = null;
+
+                var videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+                // Grab the values that ffprobe recorded
+                if (videoStream is not null)
+                {
+                    currentBitRate = videoStream.BitRate;
+                    currentWidth = videoStream.Width;
+                    currentHeight = videoStream.Height;
+                }
+
+                // Fill video properties from the BDInfo result
+                mediaStreams.Clear();
+                mediaStreams.AddRange(blurayInfo.MediaStreams);
+
+                if (blurayInfo.RunTimeTicks.HasValue && blurayInfo.RunTimeTicks.Value > 0)
+                {
+                    video.RunTimeTicks = blurayInfo.RunTimeTicks;
+                }
+
+                if (blurayInfo.Chapters is not null)
+                {
+                    double[] brChapter = blurayInfo.Chapters;
+                    chapters = new ChapterInfo[brChapter.Length];
+                    for (int i = 0; i < brChapter.Length; i++)
+                    {
+                        chapters[i] = new ChapterInfo
+                        {
+                            StartPositionTicks = TimeSpan.FromSeconds(brChapter[i]).Ticks
+                        };
+                    }
+                }
+
+                videoStream = mediaStreams.FirstOrDefault(s => s.Type == MediaStreamType.Video);
+
+                // Use the ffprobe values if these are empty
+                if (videoStream is not null)
+                {
+                    videoStream.BitRate = IsEmpty(videoStream.BitRate) ? currentBitRate : videoStream.BitRate;
+                    videoStream.Width = IsEmpty(videoStream.Width) ? currentWidth : videoStream.Width;
+                    videoStream.Height = IsEmpty(videoStream.Height) ? currentHeight : videoStream.Height;
+                }
+            }
+        }
+
+        private bool IsEmpty(int? num)
+        {
+            return !num.HasValue || num.Value == 0;
+        }
+
+        /// <summary>
+        /// Gets information about the longest playlist on a bdrom.
+        /// </summary>
+        /// <param name="path">The path.</param>
+        /// <returns>VideoStream.</returns>
+        private BlurayDiscInfo GetBDInfo(string path)
+        {
+            if (string.IsNullOrWhiteSpace(path))
+            {
+                throw new ArgumentNullException(nameof(path));
+            }
+
+            try
+            {
+                return _blurayExaminer.GetDiscInfo(path);
+            }
+            catch (Exception ex)
+            {
+                _logger.LogError(ex, "Error getting BDInfo");
+                return null;
+            }
+        }
+
         private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
         {
             var replaceData = refreshOptions.ReplaceAllMetadata;
@@ -558,5 +683,33 @@ namespace MediaBrowser.Providers.MediaInfo
 
             return chapters;
         }
+
+        private string[] FetchFromDvdLib(Video item)
+        {
+            var path = item.Path;
+            var dvd = new Dvd(path);
+
+            var primaryTitle = dvd.Titles.OrderByDescending(GetRuntime).FirstOrDefault();
+
+            byte? titleNumber = null;
+
+            if (primaryTitle is not null)
+            {
+                titleNumber = primaryTitle.VideoTitleSetNumber;
+                item.RunTimeTicks = GetRuntime(primaryTitle);
+            }
+
+            return _mediaEncoder.GetPrimaryPlaylistVobFiles(item.Path, titleNumber)
+                .Select(Path.GetFileName)
+                .ToArray();
+        }
+
+        private long GetRuntime(Title title)
+        {
+            return title.ProgramChains
+                    .Select(i => (TimeSpan)i.PlaybackTime)
+                    .Select(i => i.Ticks)
+                    .Sum();
+        }
     }
 }

+ 3 - 0
MediaBrowser.Providers/MediaInfo/ProbeProvider.cs

@@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo
         /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
         /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
         /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
+        /// <param name="blurayExaminer">Instance of the <see cref="IBlurayExaminer"/> interface.</param>
         /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
         /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
         /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
@@ -66,6 +67,7 @@ namespace MediaBrowser.Providers.MediaInfo
             IMediaSourceManager mediaSourceManager,
             IMediaEncoder mediaEncoder,
             IItemRepository itemRepo,
+            IBlurayExaminer blurayExaminer,
             ILocalizationManager localization,
             IEncodingManager encodingManager,
             IServerConfigurationManager config,
@@ -85,6 +87,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 mediaSourceManager,
                 mediaEncoder,
                 itemRepo,
+                blurayExaminer,
                 localization,
                 encodingManager,
                 config,