diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs
new file mode 100644
index 0000000000..b3aad85cec
--- /dev/null
+++ b/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));
+ }
+ }
+}
diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj
new file mode 100644
index 0000000000..1053c0089f
--- /dev/null
+++ b/DvdLib/DvdLib.csproj
@@ -0,0 +1,20 @@
+
+
+
+
+ {713F42B5-878E-499D-A878-E4C652B1D5E8}
+
+
+
+
+
+
+
+ net7.0
+ false
+ true
+ AllDisabledByDefault
+ disable
+
+
+
diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs
new file mode 100644
index 0000000000..ea0b50e430
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/DvdLib/Ifo/CellPlaybackInfo.cs b/DvdLib/Ifo/CellPlaybackInfo.cs
new file mode 100644
index 0000000000..6e33a0ec5a
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/DvdLib/Ifo/CellPositionInfo.cs b/DvdLib/Ifo/CellPositionInfo.cs
new file mode 100644
index 0000000000..216aa0f77a
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/DvdLib/Ifo/Chapter.cs b/DvdLib/Ifo/Chapter.cs
new file mode 100644
index 0000000000..e786cb5536
--- /dev/null
+++ b/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;
+ }
+ }
+}
diff --git a/DvdLib/Ifo/Dvd.cs b/DvdLib/Ifo/Dvd.cs
new file mode 100644
index 0000000000..7f8ece47dc
--- /dev/null
+++ b/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
Titles;
+
+ private ushort _titleCount;
+ public readonly Dictionary VTSPaths = new Dictionary();
+ public Dvd(string path)
+ {
+ Titles = new List();
+ 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 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);
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs
new file mode 100644
index 0000000000..d231406106
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs
new file mode 100644
index 0000000000..3d94fa7dc1
--- /dev/null
+++ b/DvdLib/Ifo/Program.cs
@@ -0,0 +1,16 @@
+#pragma warning disable CS1591
+
+using System.Collections.Generic;
+
+namespace DvdLib.Ifo
+{
+ public class Program
+ {
+ public IReadOnlyList Cells { get; }
+
+ public Program(List cells)
+ {
+ Cells = cells;
+ }
+ }
+}
diff --git a/DvdLib/Ifo/ProgramChain.cs b/DvdLib/Ifo/ProgramChain.cs
new file mode 100644
index 0000000000..83c0051b90
--- /dev/null
+++ b/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 Programs;
+
+ private byte _cellCount;
+ public readonly List 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();
+ Programs = new List();
+ }
+
+ 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();
+ 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()));
+ }
+ }
+ }
+}
diff --git a/DvdLib/Ifo/Title.cs b/DvdLib/Ifo/Title.cs
new file mode 100644
index 0000000000..29a0b95c72
--- /dev/null
+++ b/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 ProgramChains;
+
+ public readonly List Chapters;
+
+ public Title(uint titleNum)
+ {
+ ProgramChains = new List();
+ Chapters = new List();
+ Chapters = new List();
+ 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);
+ }
+ }
+}
diff --git a/DvdLib/Ifo/UserOperation.cs b/DvdLib/Ifo/UserOperation.cs
new file mode 100644
index 0000000000..5d111ebc06
--- /dev/null
+++ b/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,
+ }
+}
diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..6acd571d68
--- /dev/null
+++ b/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)]
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 37a9e7715d..d43d368e0b 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/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();
+ serviceCollection.AddSingleton();
+
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
diff --git a/Jellyfin.sln b/Jellyfin.sln
index cad23fc5ee..ee772cbe40 100644
--- a/Jellyfin.sln
+++ b/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
diff --git a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
index bc6207ac51..fe8e9063ee 100644
--- a/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
+++ b/MediaBrowser.Controller/MediaEncoding/IMediaEncoder.cs
@@ -187,5 +187,13 @@ namespace MediaBrowser.Controller.MediaEncoding
/// The path.
/// The type of path.
void UpdateEncoderPath(string path, string pathType);
+
+ ///
+ /// Gets the primary playlist of .vob files.
+ ///
+ /// The to the .vob files.
+ /// The title number to start with.
+ /// A playlist.
+ IEnumerable GetPrimaryPlaylistVobFiles(string path, uint? titleNumber);
}
}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoDirectoryInfo.cs
new file mode 100644
index 0000000000..7e026b42e3
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoExaminer.cs
new file mode 100644
index 0000000000..3e53cbf29f
--- /dev/null
+++ b/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
+{
+ ///
+ /// Class BdInfoExaminer.
+ ///
+ public class BdInfoExaminer : IBlurayExaminer
+ {
+ private readonly IFileSystem _fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The filesystem.
+ public BdInfoExaminer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ ///
+ /// Gets the disc info.
+ ///
+ /// The path.
+ /// BlurayDiscInfo.
+ 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()
+ };
+
+ if (playlist is null)
+ {
+ return outputStream;
+ }
+
+ outputStream.Chapters = playlist.Chapters.ToArray();
+
+ outputStream.RunTimeTicks = TimeSpan.FromSeconds(playlist.TotalLength).Ticks;
+
+ var mediaStreams = new List();
+
+ 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;
+ }
+
+ ///
+ /// Adds the video stream.
+ ///
+ /// The streams.
+ /// The video stream.
+ private void AddVideoStream(List 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);
+ }
+
+ ///
+ /// Adds the audio stream.
+ ///
+ /// The streams.
+ /// The audio stream.
+ private void AddAudioStream(List 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);
+ }
+
+ ///
+ /// Adds the subtitle stream.
+ ///
+ /// The streams.
+ /// The text stream.
+ private void AddSubtitleStream(List streams, TSTextStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+
+ ///
+ /// Adds the subtitle stream.
+ ///
+ /// The streams.
+ /// The text stream.
+ private void AddSubtitleStream(List streams, TSGraphicsStream textStream)
+ {
+ streams.Add(new MediaStream
+ {
+ Language = textStream.LanguageCode,
+ Codec = textStream.CodecShortName,
+ Type = MediaStreamType.Subtitle,
+ Index = streams.Count
+ });
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs b/MediaBrowser.MediaEncoding/BdInfo/BdInfoFileInfo.cs
new file mode 100644
index 0000000000..d55688e3df
--- /dev/null
+++ b/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());
+ }
+ }
+}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index cef02d5f8f..4a795625d0 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -865,6 +865,85 @@ namespace MediaBrowser.MediaEncoding.Encoder
throw new NotImplementedException();
}
+ ///
+ public IEnumerable 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();
+ }
+
+ 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??
diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
index f4438fe194..a0624fe76b 100644
--- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
+++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj
@@ -22,6 +22,7 @@
+
diff --git a/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
new file mode 100644
index 0000000000..83f982a5c8
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/BlurayDiscInfo.cs
@@ -0,0 +1,39 @@
+#nullable disable
+#pragma warning disable CS1591
+
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Model.MediaInfo
+{
+ ///
+ /// Represents the result of BDInfo output.
+ ///
+ public class BlurayDiscInfo
+ {
+ ///
+ /// Gets or sets the media streams.
+ ///
+ /// The media streams.
+ public MediaStream[] MediaStreams { get; set; }
+
+ ///
+ /// Gets or sets the run time ticks.
+ ///
+ /// The run time ticks.
+ public long? RunTimeTicks { get; set; }
+
+ ///
+ /// Gets or sets the files.
+ ///
+ /// The files.
+ public string[] Files { get; set; }
+
+ public string PlaylistName { get; set; }
+
+ ///
+ /// Gets or sets the chapters.
+ ///
+ /// The chapters.
+ public double[] Chapters { get; set; }
+ }
+}
diff --git a/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
new file mode 100644
index 0000000000..5b7d1d03c3
--- /dev/null
+++ b/MediaBrowser.Model/MediaInfo/IBlurayExaminer.cs
@@ -0,0 +1,15 @@
+namespace MediaBrowser.Model.MediaInfo
+{
+ ///
+ /// Interface IBlurayExaminer.
+ ///
+ public interface IBlurayExaminer
+ {
+ ///
+ /// Gets the disc info.
+ ///
+ /// The path.
+ /// BlurayDiscInfo.
+ BlurayDiscInfo GetDiscInfo(string path);
+ }
+}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 6a40833d7f..a0f97df40d 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -8,6 +8,7 @@
+
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index 0f35c6a5ea..81434b8620 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/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 _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();
+
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 mediaStreams;
@@ -182,6 +218,10 @@ namespace MediaBrowser.Providers.MediaInfo
video.Container = mediaInfo.Container;
chapters = mediaInfo.Chapters ?? Array.Empty();
+ 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 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;
+ }
+
+ ///
+ /// Gets information about the longest playlist on a bdrom.
+ ///
+ /// The path.
+ /// VideoStream.
+ 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();
+ }
}
}
diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
index 31fa3da1ce..2800219552 100644
--- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs
@@ -53,6 +53,7 @@ namespace MediaBrowser.Providers.MediaInfo
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
+ /// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
/// Instance of the interface.
@@ -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,
| | | |