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<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); + } + } + } + } + } + } +} 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<Cell> Cells { get; } + + public Program(List<Cell> 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<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())); + } + } + } +} 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<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); + } + } +} 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<ILocalizationManager, LocalizationManager>(); + serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>(); + serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>(); serviceCollection.AddSingleton<IUserDataManager, UserDataManager>(); 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 /// <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); } } 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 +{ + /// <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 + }); + } + } +} 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(); } + /// <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?? 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 @@ </ItemGroup> <ItemGroup> + <PackageReference Include="BDInfo" /> <PackageReference Include="libse" /> <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="System.Text.Encoding.CodePages" /> 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 +{ + /// <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; } + } +} 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 +{ + /// <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); + } +} 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 @@ <ItemGroup> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\DvdLib\DvdLib.csproj" /> </ItemGroup> <ItemGroup> 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<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(); + } } } 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 /// <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,