mirror of https://github.com/jellyfin/jellyfin.git
Merge branch 'master' into network-rewrite
This commit is contained in:
commit
a728f0993e
|
@ -19,6 +19,7 @@ jobs:
|
||||||
days-before-pr-stale: -1
|
days-before-pr-stale: -1
|
||||||
days-before-close: 21
|
days-before-close: 21
|
||||||
days-before-pr-close: -1
|
days-before-pr-close: -1
|
||||||
|
operations-per-run: 75
|
||||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||||
stale-issue-label: stale
|
stale-issue-label: stale
|
||||||
stale-issue-message: |-
|
stale-issue-message: |-
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
#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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
<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>
|
|
|
@ -1,23 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,52 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
#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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,121 +0,0 @@
|
||||||
#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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
#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,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
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)]
|
|
|
@ -83,13 +83,11 @@ using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Controller.SyncPlay;
|
using MediaBrowser.Controller.SyncPlay;
|
||||||
using MediaBrowser.Controller.TV;
|
using MediaBrowser.Controller.TV;
|
||||||
using MediaBrowser.LocalMetadata.Savers;
|
using MediaBrowser.LocalMetadata.Savers;
|
||||||
using MediaBrowser.MediaEncoding.BdInfo;
|
|
||||||
using MediaBrowser.MediaEncoding.Subtitles;
|
using MediaBrowser.MediaEncoding.Subtitles;
|
||||||
using MediaBrowser.Model.Cryptography;
|
using MediaBrowser.Model.Cryptography;
|
||||||
using MediaBrowser.Model.Dlna;
|
using MediaBrowser.Model.Dlna;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using MediaBrowser.Model.IO;
|
using MediaBrowser.Model.IO;
|
||||||
using MediaBrowser.Model.MediaInfo;
|
|
||||||
using MediaBrowser.Model.Net;
|
using MediaBrowser.Model.Net;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.System;
|
using MediaBrowser.Model.System;
|
||||||
|
@ -562,8 +560,6 @@ namespace Emby.Server.Implementations
|
||||||
|
|
||||||
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
serviceCollection.AddSingleton<ILocalizationManager, LocalizationManager>();
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IBlurayExaminer, BdInfoExaminer>();
|
|
||||||
|
|
||||||
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
|
||||||
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
|
||||||
|
|
||||||
|
|
|
@ -123,5 +123,6 @@
|
||||||
"TaskOptimizeDatabase": "Оптимизирай базата данни",
|
"TaskOptimizeDatabase": "Оптимизирай базата данни",
|
||||||
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
|
"TaskKeyframeExtractorDescription": "Извличат се ключови кадри от видеофайловете ,за да се създаде по точен ХЛС списък . Задачата може да отнеме много време.",
|
||||||
"TaskKeyframeExtractor": "Извличане на ключови кадри",
|
"TaskKeyframeExtractor": "Извличане на ключови кадри",
|
||||||
"External": "Външен"
|
"External": "Външен",
|
||||||
|
"HearingImpaired": "Увреден слух"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jellyfin.Drawing", "src\Jel
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Photos", "Emby.Photos\Emby.Photos.csproj", "{89AB4548-770D-41FD-A891-8DAFF44F452C}"
|
||||||
EndProject
|
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}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emby.Server.Implementations", "Emby.Server.Implementations\Emby.Server.Implementations.csproj", "{E383961B-9356-4D5D-8233-9A1079D03055}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RSSDP", "RSSDP\RSSDP.csproj", "{21002819-C39A-4D3E-BE83-2A276A77FB1F}"
|
||||||
|
@ -139,10 +137,6 @@ Global
|
||||||
{89AB4548-770D-41FD-A891-8DAFF44F452C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{89AB4548-770D-41FD-A891-8DAFF44F452C}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{E383961B-9356-4D5D-8233-9A1079D03055}.Debug|Any CPU.Build.0 = 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
|
{E383961B-9356-4D5D-8233-9A1079D03055}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|
|
@ -187,13 +187,5 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||||
/// <param name="path">The path.</param>
|
/// <param name="path">The path.</param>
|
||||||
/// <param name="pathType">The type of path.</param>
|
/// <param name="pathType">The type of path.</param>
|
||||||
void UpdateEncoderPath(string path, string pathType);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
#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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -862,85 +862,6 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||||
throw new NotImplementedException();
|
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)
|
public bool CanExtractSubtitles(string codec)
|
||||||
{
|
{
|
||||||
// TODO is there ever a case when a subtitle can't be extracted??
|
// TODO is there ever a case when a subtitle can't be extracted??
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BDInfo" Version="0.7.6.2" />
|
|
||||||
<PackageReference Include="libse" Version="3.6.10" />
|
<PackageReference Include="libse" Version="3.6.10" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="7.0.0" />
|
||||||
|
|
|
@ -625,17 +625,6 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||||
return attachment;
|
return attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether a stream code time base is double the frame rate.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="averageFrameRate">average frame rate.</param>
|
|
||||||
/// <param name="codecTimeBase">codec time base string.</param>
|
|
||||||
/// <returns>true if the codec time base is double the frame rate.</returns>
|
|
||||||
internal static bool IsCodecTimeBaseDoubleTheFrameRate(float? averageFrameRate, string codecTimeBase)
|
|
||||||
{
|
|
||||||
return MathF.Abs(((averageFrameRate ?? 0) * (GetFrameRate(codecTimeBase) ?? 0)) - 0.5f) <= float.Epsilon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts ffprobe stream info to our MediaStream class.
|
/// Converts ffprobe stream info to our MediaStream class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -748,22 +737,9 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||||
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
|
stream.AverageFrameRate = GetFrameRate(streamInfo.AverageFrameRate);
|
||||||
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
|
stream.RealFrameRate = GetFrameRate(streamInfo.RFrameRate);
|
||||||
|
|
||||||
bool videoInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
|
stream.IsInterlaced = !string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
|
||||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// Some interlaced H.264 files in mp4 containers using MBAFF coding aren't flagged as being interlaced by FFprobe,
|
|
||||||
// so for H.264 files we also calculate the frame rate from the codec time base and check if it is double the reported
|
|
||||||
// frame rate to determine if the file is interlaced
|
|
||||||
|
|
||||||
bool h264MbaffCoded = string.Equals(stream.Codec, "h264", StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& string.IsNullOrWhiteSpace(streamInfo.FieldOrder)
|
|
||||||
&& IsCodecTimeBaseDoubleTheFrameRate(stream.AverageFrameRate, stream.CodecTimeBase);
|
|
||||||
|
|
||||||
if (videoInterlaced || h264MbaffCoded)
|
|
||||||
{
|
|
||||||
stream.IsInterlaced = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAudio
|
if (isAudio
|
||||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
#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; }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,7 +8,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" />
|
||||||
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
|
||||||
<ProjectReference Include="..\DvdLib\DvdLib.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -9,7 +9,6 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DvdLib.Ifo;
|
|
||||||
using MediaBrowser.Common.Configuration;
|
using MediaBrowser.Common.Configuration;
|
||||||
using MediaBrowser.Controller.Chapters;
|
using MediaBrowser.Controller.Chapters;
|
||||||
using MediaBrowser.Controller.Configuration;
|
using MediaBrowser.Controller.Configuration;
|
||||||
|
@ -37,7 +36,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
private readonly ILogger<FFProbeVideoInfo> _logger;
|
private readonly ILogger<FFProbeVideoInfo> _logger;
|
||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly IItemRepository _itemRepo;
|
private readonly IItemRepository _itemRepo;
|
||||||
private readonly IBlurayExaminer _blurayExaminer;
|
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly IEncodingManager _encodingManager;
|
private readonly IEncodingManager _encodingManager;
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
|
@ -53,7 +51,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
IBlurayExaminer blurayExaminer,
|
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IEncodingManager encodingManager,
|
IEncodingManager encodingManager,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
|
@ -67,7 +64,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
_mediaSourceManager = mediaSourceManager;
|
_mediaSourceManager = mediaSourceManager;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepo = itemRepo;
|
_itemRepo = itemRepo;
|
||||||
_blurayExaminer = blurayExaminer;
|
|
||||||
_localization = localization;
|
_localization = localization;
|
||||||
_encodingManager = encodingManager;
|
_encodingManager = encodingManager;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
@ -84,47 +80,16 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
where T : Video
|
where T : Video
|
||||||
{
|
{
|
||||||
BlurayDiscInfo blurayDiscInfo = null;
|
|
||||||
|
|
||||||
Model.MediaInfo.MediaInfo mediaInfoResult = null;
|
Model.MediaInfo.MediaInfo mediaInfoResult = null;
|
||||||
|
|
||||||
if (!item.IsShortcut || options.EnableRemoteContentProbe)
|
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);
|
mediaInfoResult = await GetMediaInfo(item, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
await Fetch(item, cancellationToken, mediaInfoResult, blurayDiscInfo, options).ConfigureAwait(false);
|
await Fetch(item, cancellationToken, mediaInfoResult, options).ConfigureAwait(false);
|
||||||
|
|
||||||
return ItemUpdateType.MetadataImport;
|
return ItemUpdateType.MetadataImport;
|
||||||
}
|
}
|
||||||
|
@ -164,7 +129,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
Video video,
|
Video video,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Model.MediaInfo.MediaInfo mediaInfo,
|
Model.MediaInfo.MediaInfo mediaInfo,
|
||||||
BlurayDiscInfo blurayInfo,
|
|
||||||
MetadataRefreshOptions options)
|
MetadataRefreshOptions options)
|
||||||
{
|
{
|
||||||
List<MediaStream> mediaStreams;
|
List<MediaStream> mediaStreams;
|
||||||
|
@ -218,10 +182,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
video.Container = mediaInfo.Container;
|
video.Container = mediaInfo.Container;
|
||||||
|
|
||||||
chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
|
chapters = mediaInfo.Chapters ?? Array.Empty<ChapterInfo>();
|
||||||
if (blurayInfo is not null)
|
|
||||||
{
|
|
||||||
FetchBdInfo(video, ref chapters, mediaStreams, blurayInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -317,91 +277,6 @@ 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)
|
private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
|
||||||
{
|
{
|
||||||
var replaceData = refreshOptions.ReplaceAllMetadata;
|
var replaceData = refreshOptions.ReplaceAllMetadata;
|
||||||
|
@ -683,33 +558,5 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
|
|
||||||
return chapters;
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||||
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> 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="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="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
|
||||||
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
|
/// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param>
|
||||||
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
/// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||||
|
@ -67,7 +66,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
IBlurayExaminer blurayExaminer,
|
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IEncodingManager encodingManager,
|
IEncodingManager encodingManager,
|
||||||
IServerConfigurationManager config,
|
IServerConfigurationManager config,
|
||||||
|
@ -87,7 +85,6 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||||
mediaSourceManager,
|
mediaSourceManager,
|
||||||
mediaEncoder,
|
mediaEncoder,
|
||||||
itemRepo,
|
itemRepo,
|
||||||
blurayExaminer,
|
|
||||||
localization,
|
localization,
|
||||||
encodingManager,
|
encodingManager,
|
||||||
config,
|
config,
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# restart.sh - Jellyfin server restart script
|
|
||||||
# Part of the Jellyfin project (https://github.com/jellyfin)
|
|
||||||
#
|
|
||||||
# This script restarts the Jellyfin daemon on Linux when using
|
|
||||||
# the Restart button on the admin dashboard. It supports the
|
|
||||||
# systemctl, service, and traditional /etc/init.d (sysv) restart
|
|
||||||
# methods, chosen automatically by which one is found first (in
|
|
||||||
# that order).
|
|
||||||
#
|
|
||||||
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
|
|
||||||
|
|
||||||
# This is the Right Way(tm) to check if we are booted with
|
|
||||||
# systemd, according to sd_booted(3)
|
|
||||||
if [ -d /run/systemd/system ]; then
|
|
||||||
cmd=systemctl
|
|
||||||
else
|
|
||||||
# Everything else is really hard to figure out, so we just use
|
|
||||||
# service(8) if it's available - that works with most init
|
|
||||||
# systems/distributions I know of, including FreeBSD
|
|
||||||
if type service >/dev/null 2>&1; then
|
|
||||||
cmd=service
|
|
||||||
else
|
|
||||||
# If even service(8) isn't available, we just try /etc/init.d
|
|
||||||
# and hope for the best
|
|
||||||
if [ -d /etc/init.d ]; then
|
|
||||||
cmd=sysv
|
|
||||||
else
|
|
||||||
echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
|
|
||||||
echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if type sudo >/dev/null 2>&1; then
|
|
||||||
sudo_command=sudo
|
|
||||||
else
|
|
||||||
sudo_command=
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
|
|
||||||
case $cmd in
|
|
||||||
'systemctl')
|
|
||||||
# Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
|
|
||||||
$sudo_command systemd-run systemctl restart jellyfin
|
|
||||||
;;
|
|
||||||
'service')
|
|
||||||
echo "sleep 0.5; $sudo_command service jellyfin start" | at now
|
|
||||||
;;
|
|
||||||
'sysv')
|
|
||||||
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
|
@ -21,9 +21,6 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
|
||||||
# web client path, installed by the jellyfin-web package
|
# web client path, installed by the jellyfin-web package
|
||||||
JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
|
JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin/web"
|
||||||
|
|
||||||
# Restart script for in-app server control
|
|
||||||
JELLYFIN_RESTART_OPT="--restartpath=/usr/lib/jellyfin/restart.sh"
|
|
||||||
|
|
||||||
# ffmpeg binary paths, overriding the system values
|
# ffmpeg binary paths, overriding the system values
|
||||||
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/lib/jellyfin-ffmpeg/ffmpeg"
|
||||||
|
|
||||||
|
@ -50,4 +47,4 @@ JELLYFIN_ADDITIONAL_OPTS=""
|
||||||
# Application username
|
# Application username
|
||||||
JELLYFIN_USER="jellyfin"
|
JELLYFIN_USER="jellyfin"
|
||||||
# Full application command
|
# Full application command
|
||||||
JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
|
JELLYFIN_ARGS="$JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLFIN_ADDITIONAL_OPTS"
|
||||||
|
|
|
@ -3,4 +3,3 @@ debian/conf/jellyfin etc/default/
|
||||||
debian/conf/logging.json etc/jellyfin/
|
debian/conf/logging.json etc/jellyfin/
|
||||||
debian/conf/jellyfin.service.conf etc/systemd/system/jellyfin.service.d/
|
debian/conf/jellyfin.service.conf etc/systemd/system/jellyfin.service.d/
|
||||||
debian/conf/jellyfin-sudoers etc/sudoers.d/
|
debian/conf/jellyfin-sudoers etc/sudoers.d/
|
||||||
debian/bin/restart.sh usr/lib/jellyfin/
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ EnvironmentFile = /etc/default/jellyfin
|
||||||
User = jellyfin
|
User = jellyfin
|
||||||
Group = jellyfin
|
Group = jellyfin
|
||||||
WorkingDirectory = /var/lib/jellyfin
|
WorkingDirectory = /var/lib/jellyfin
|
||||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||||
Restart = on-failure
|
Restart = on-failure
|
||||||
TimeoutSec = 15
|
TimeoutSec = 15
|
||||||
SuccessExitStatus=0 143
|
SuccessExitStatus=0 143
|
||||||
|
|
|
@ -59,8 +59,6 @@ case "$1" in
|
||||||
chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
chgrp adm $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
||||||
chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
chmod 0750 $PROGRAMDATA $CONFIGDATA $LOGDATA $CACHEDATA
|
||||||
|
|
||||||
chmod +x /usr/lib/jellyfin/restart.sh > /dev/null 2>&1 || true
|
|
||||||
|
|
||||||
# Install jellyfin symlink into /usr/bin
|
# Install jellyfin symlink into /usr/bin
|
||||||
ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
|
ln -sf /usr/lib/jellyfin/bin/jellyfin /usr/bin/jellyfin
|
||||||
|
|
||||||
|
|
|
@ -23,9 +23,6 @@ JELLYFIN_CACHE_DIR="/var/cache/jellyfin"
|
||||||
# web client path, installed by the jellyfin-web package
|
# web client path, installed by the jellyfin-web package
|
||||||
# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
|
# JELLYFIN_WEB_OPT="--webdir=/usr/share/jellyfin-web"
|
||||||
|
|
||||||
# In-App service control
|
|
||||||
JELLYFIN_RESTART_OPT="--restartpath=/usr/libexec/jellyfin/restart.sh"
|
|
||||||
|
|
||||||
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
|
# [OPTIONAL] ffmpeg binary paths, overriding the UI-configured values
|
||||||
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
|
#JELLYFIN_FFMPEG_OPT="--ffmpeg=/usr/bin/ffmpeg"
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
|
||||||
User = jellyfin
|
User = jellyfin
|
||||||
Group = jellyfin
|
Group = jellyfin
|
||||||
WorkingDirectory = /var/lib/jellyfin
|
WorkingDirectory = /var/lib/jellyfin
|
||||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_RESTART_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||||
Restart = on-failure
|
Restart = on-failure
|
||||||
TimeoutSec = 15
|
TimeoutSec = 15
|
||||||
SuccessExitStatus=0 143
|
SuccessExitStatus=0 143
|
||||||
|
|
|
@ -17,10 +17,9 @@ Source0: jellyfin-server-%{version}.tar.gz
|
||||||
Source11: jellyfin.service
|
Source11: jellyfin.service
|
||||||
Source12: jellyfin.env
|
Source12: jellyfin.env
|
||||||
Source13: jellyfin.sudoers
|
Source13: jellyfin.sudoers
|
||||||
Source14: restart.sh
|
Source14: jellyfin.override.conf
|
||||||
Source15: jellyfin.override.conf
|
Source15: jellyfin-firewalld.xml
|
||||||
Source16: jellyfin-firewalld.xml
|
Source16: jellyfin-server-lowports.conf
|
||||||
Source17: jellyfin-server-lowports.conf
|
|
||||||
|
|
||||||
%{?systemd_requires}
|
%{?systemd_requires}
|
||||||
BuildRequires: systemd
|
BuildRequires: systemd
|
||||||
|
@ -76,16 +75,15 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
|
||||||
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
|
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
|
||||||
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
|
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
|
||||||
ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
|
ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
|
||||||
%{__install} -D %{SOURCE14} %{buildroot}%{_libexecdir}/jellyfin/restart.sh
|
|
||||||
|
|
||||||
# Jellyfin config
|
# Jellyfin config
|
||||||
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
|
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
|
||||||
%{__install} -D %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
|
%{__install} -D %{SOURCE12} %{buildroot}%{_sysconfdir}/sysconfig/jellyfin
|
||||||
|
|
||||||
# system config
|
# system config
|
||||||
%{__install} -D %{SOURCE16} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
|
%{__install} -D %{SOURCE15} %{buildroot}%{_prefix}/lib/firewalld/services/jellyfin.xml
|
||||||
%{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
|
%{__install} -D %{SOURCE13} %{buildroot}%{_sysconfdir}/sudoers.d/jellyfin-sudoers
|
||||||
%{__install} -D %{SOURCE15} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
|
%{__install} -D %{SOURCE14} %{buildroot}%{_sysconfdir}/systemd/system/jellyfin.service.d/override.conf
|
||||||
%{__install} -D %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
|
%{__install} -D %{SOURCE11} %{buildroot}%{_unitdir}/jellyfin.service
|
||||||
|
|
||||||
# empty directories
|
# empty directories
|
||||||
|
@ -95,7 +93,7 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
|
||||||
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
|
%{__mkdir} -p %{buildroot}%{_var}/log/jellyfin
|
||||||
|
|
||||||
# jellyfin-server-lowports subpackage
|
# jellyfin-server-lowports subpackage
|
||||||
%{__install} -D -m 0644 %{SOURCE17} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
|
%{__install} -D -m 0644 %{SOURCE16} %{buildroot}%{_unitdir}/jellyfin.service.d/jellyfin-server-lowports.conf
|
||||||
|
|
||||||
|
|
||||||
%files
|
%files
|
||||||
|
@ -110,7 +108,6 @@ ln -srf %{_libdir}/jellyfin/jellyfin %{buildroot}%{_bindir}/jellyfin
|
||||||
%attr(755,root,root) %{_libdir}/jellyfin/createdump
|
%attr(755,root,root) %{_libdir}/jellyfin/createdump
|
||||||
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
|
%attr(755,root,root) %{_libdir}/jellyfin/jellyfin
|
||||||
%{_libdir}/jellyfin/*
|
%{_libdir}/jellyfin/*
|
||||||
%attr(755,root,root) %{_libexecdir}/jellyfin/restart.sh
|
|
||||||
|
|
||||||
# Jellyfin config
|
# Jellyfin config
|
||||||
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
|
%config(noreplace) %attr(644,jellyfin,jellyfin) %{_sysconfdir}/jellyfin/logging.json
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# restart.sh - Jellyfin server restart script
|
|
||||||
# Part of the Jellyfin project (https://github.com/jellyfin)
|
|
||||||
#
|
|
||||||
# This script restarts the Jellyfin daemon on Linux when using
|
|
||||||
# the Restart button on the admin dashboard. It supports the
|
|
||||||
# systemctl, service, and traditional /etc/init.d (sysv) restart
|
|
||||||
# methods, chosen automatically by which one is found first (in
|
|
||||||
# that order).
|
|
||||||
#
|
|
||||||
# This script is used by the Debian/Ubuntu/Fedora/CentOS packages.
|
|
||||||
|
|
||||||
# This is the Right Way(tm) to check if we are booted with
|
|
||||||
# systemd, according to sd_booted(3)
|
|
||||||
if [ -d /run/systemd/system ]; then
|
|
||||||
cmd=systemctl
|
|
||||||
else
|
|
||||||
# Everything else is really hard to figure out, so we just use
|
|
||||||
# service(8) if it's available - that works with most init
|
|
||||||
# systems/distributions I know of, including FreeBSD
|
|
||||||
if type service >/dev/null 2>&1; then
|
|
||||||
cmd=service
|
|
||||||
else
|
|
||||||
# If even service(8) isn't available, we just try /etc/init.d
|
|
||||||
# and hope for the best
|
|
||||||
if [ -d /etc/init.d ]; then
|
|
||||||
cmd=sysv
|
|
||||||
else
|
|
||||||
echo "Unable to detect a way to restart Jellyfin; bailing out" 1>&2
|
|
||||||
echo "Please report this bug to https://github.com/jellyfin/jellyfin/issues" 1>&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if type sudo >/dev/null 2>&1; then
|
|
||||||
sudo_command=sudo
|
|
||||||
else
|
|
||||||
sudo_command=
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Detected service control platform '$cmd'; using it to restart Jellyfin..."
|
|
||||||
case $cmd in
|
|
||||||
'systemctl')
|
|
||||||
# Without systemd-run here, `jellyfin.service`'s shutdown terminates this process too
|
|
||||||
$sudo_command systemd-run systemctl restart jellyfin
|
|
||||||
;;
|
|
||||||
'service')
|
|
||||||
echo "sleep 0.5; $sudo_command service jellyfin start" | at now
|
|
||||||
;;
|
|
||||||
'sysv')
|
|
||||||
echo "sleep 0.5; /usr/bin/sudo /etc/init.d/jellyfin start" | at now
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
exit 0
|
|
|
@ -31,16 +31,6 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||||
public void GetFrameRate_Success(string value, float? expected)
|
public void GetFrameRate_Success(string value, float? expected)
|
||||||
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
|
=> Assert.Equal(expected, ProbeResultNormalizer.GetFrameRate(value));
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0.5f, "0/1", false)]
|
|
||||||
[InlineData(24.5f, "8/196", false)]
|
|
||||||
[InlineData(63.5f, "1/127", true)]
|
|
||||||
[InlineData(null, "1/60", false)]
|
|
||||||
[InlineData(30f, "2/120", true)]
|
|
||||||
[InlineData(59.999996f, "1563/187560", true)]
|
|
||||||
public void IsCodecTimeBaseDoubleTheFrameRate_Success(float? frameRate, string codecTimeBase, bool expected)
|
|
||||||
=> Assert.Equal(expected, ProbeResultNormalizer.IsCodecTimeBaseDoubleTheFrameRate(frameRate, codecTimeBase));
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetMediaInfo_MetaData_Success()
|
public void GetMediaInfo_MetaData_Success()
|
||||||
{
|
{
|
||||||
|
@ -158,6 +148,99 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
|
||||||
Assert.False(res.MediaStreams[5].IsHearingImpaired);
|
Assert.False(res.MediaStreams[5].IsHearingImpaired);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
|
||||||
|
{
|
||||||
|
var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order.json");
|
||||||
|
|
||||||
|
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
|
||||||
|
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order.mp4", MediaProtocol.File);
|
||||||
|
|
||||||
|
Assert.Equal(2, res.MediaStreams.Count);
|
||||||
|
|
||||||
|
Assert.NotNull(res.VideoStream);
|
||||||
|
Assert.Equal(res.MediaStreams[0], res.VideoStream);
|
||||||
|
Assert.Equal(0, res.VideoStream.Index);
|
||||||
|
Assert.Equal("h264", res.VideoStream.Codec);
|
||||||
|
Assert.Equal("Main", res.VideoStream.Profile);
|
||||||
|
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
|
||||||
|
Assert.Equal(1080, res.VideoStream.Height);
|
||||||
|
Assert.Equal(1920, res.VideoStream.Width);
|
||||||
|
Assert.False(res.VideoStream.IsInterlaced);
|
||||||
|
Assert.Equal("16:9", res.VideoStream.AspectRatio);
|
||||||
|
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
|
||||||
|
Assert.Equal(41d, res.VideoStream.Level);
|
||||||
|
Assert.Equal(1, res.VideoStream.RefFrames);
|
||||||
|
Assert.True(res.VideoStream.IsAVC);
|
||||||
|
Assert.Equal(23.9760246f, res.VideoStream.RealFrameRate);
|
||||||
|
Assert.Equal("1/24000", res.VideoStream.TimeBase);
|
||||||
|
Assert.Equal(3948341, res.VideoStream.BitRate);
|
||||||
|
Assert.Equal(8, res.VideoStream.BitDepth);
|
||||||
|
Assert.True(res.VideoStream.IsDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMediaInfo_ProgressiveVideoNoFieldOrder2_Success()
|
||||||
|
{
|
||||||
|
var bytes = File.ReadAllBytes("Test Data/Probing/video_progressive_no_field_order2.json");
|
||||||
|
|
||||||
|
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
|
||||||
|
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_progressive_no_field_order2.mp4", MediaProtocol.File);
|
||||||
|
|
||||||
|
Assert.Single(res.MediaStreams);
|
||||||
|
|
||||||
|
Assert.NotNull(res.VideoStream);
|
||||||
|
Assert.Equal(res.MediaStreams[0], res.VideoStream);
|
||||||
|
Assert.Equal(0, res.VideoStream.Index);
|
||||||
|
Assert.Equal("h264", res.VideoStream.Codec);
|
||||||
|
Assert.Equal("High", res.VideoStream.Profile);
|
||||||
|
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
|
||||||
|
Assert.Equal(720, res.VideoStream.Height);
|
||||||
|
Assert.Equal(1280, res.VideoStream.Width);
|
||||||
|
Assert.False(res.VideoStream.IsInterlaced);
|
||||||
|
Assert.Equal("16:9", res.VideoStream.AspectRatio);
|
||||||
|
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
|
||||||
|
Assert.Equal(31d, res.VideoStream.Level);
|
||||||
|
Assert.Equal(1, res.VideoStream.RefFrames);
|
||||||
|
Assert.True(res.VideoStream.IsAVC);
|
||||||
|
Assert.Equal(25f, res.VideoStream.RealFrameRate);
|
||||||
|
Assert.Equal("1/12800", res.VideoStream.TimeBase);
|
||||||
|
Assert.Equal(53288, res.VideoStream.BitRate);
|
||||||
|
Assert.Equal(8, res.VideoStream.BitDepth);
|
||||||
|
Assert.True(res.VideoStream.IsDefault);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetMediaInfo_InterlacedVideo_Success()
|
||||||
|
{
|
||||||
|
var bytes = File.ReadAllBytes("Test Data/Probing/video_interlaced.json");
|
||||||
|
|
||||||
|
var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions);
|
||||||
|
MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_interlaced.mp4", MediaProtocol.File);
|
||||||
|
|
||||||
|
Assert.Single(res.MediaStreams);
|
||||||
|
|
||||||
|
Assert.NotNull(res.VideoStream);
|
||||||
|
Assert.Equal(res.MediaStreams[0], res.VideoStream);
|
||||||
|
Assert.Equal(0, res.VideoStream.Index);
|
||||||
|
Assert.Equal("h264", res.VideoStream.Codec);
|
||||||
|
Assert.Equal("High", res.VideoStream.Profile);
|
||||||
|
Assert.Equal(MediaStreamType.Video, res.VideoStream.Type);
|
||||||
|
Assert.Equal(720, res.VideoStream.Height);
|
||||||
|
Assert.Equal(1280, res.VideoStream.Width);
|
||||||
|
Assert.True(res.VideoStream.IsInterlaced);
|
||||||
|
Assert.Equal("16:9", res.VideoStream.AspectRatio);
|
||||||
|
Assert.Equal("yuv420p", res.VideoStream.PixelFormat);
|
||||||
|
Assert.Equal(40d, res.VideoStream.Level);
|
||||||
|
Assert.Equal(1, res.VideoStream.RefFrames);
|
||||||
|
Assert.True(res.VideoStream.IsAVC);
|
||||||
|
Assert.Equal(25f, res.VideoStream.RealFrameRate);
|
||||||
|
Assert.Equal("1/12800", res.VideoStream.TimeBase);
|
||||||
|
Assert.Equal(56945, res.VideoStream.BitRate);
|
||||||
|
Assert.Equal(8, res.VideoStream.BitDepth);
|
||||||
|
Assert.True(res.VideoStream.IsDefault);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GetMediaInfo_MusicVideo_Success()
|
public void GetMediaInfo_MusicVideo_Success()
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
||||||
|
"profile": "High",
|
||||||
|
"codec_type": "video",
|
||||||
|
"codec_tag_string": "avc1",
|
||||||
|
"codec_tag": "0x31637661",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"coded_width": 1280,
|
||||||
|
"coded_height": 720,
|
||||||
|
"closed_captions": 0,
|
||||||
|
"film_grain": 0,
|
||||||
|
"has_b_frames": 2,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"level": 40,
|
||||||
|
"chroma_location": "left",
|
||||||
|
"field_order": "tt",
|
||||||
|
"refs": 1,
|
||||||
|
"is_avc": "true",
|
||||||
|
"nal_length_size": "4",
|
||||||
|
"id": "0x1",
|
||||||
|
"r_frame_rate": "25/1",
|
||||||
|
"avg_frame_rate": "25/1",
|
||||||
|
"time_base": "1/12800",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration_ts": 3840000,
|
||||||
|
"duration": "300.000000",
|
||||||
|
"bit_rate": "56945",
|
||||||
|
"bits_per_raw_sample": "8",
|
||||||
|
"nb_frames": "7500",
|
||||||
|
"extradata_size": 42,
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0,
|
||||||
|
"captions": 0,
|
||||||
|
"descriptions": 0,
|
||||||
|
"metadata": 0,
|
||||||
|
"dependent": 0,
|
||||||
|
"still_image": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"language": "und",
|
||||||
|
"handler_name": "VideoHandler",
|
||||||
|
"vendor_id": "[0][0][0][0]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"filename": "test-gray.720i.mp4",
|
||||||
|
"nb_streams": 1,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
|
"format_long_name": "QuickTime / MOV",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration": "300.000000",
|
||||||
|
"size": "2223957",
|
||||||
|
"bit_rate": "59305",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"major_brand": "isom",
|
||||||
|
"minor_version": "512",
|
||||||
|
"compatible_brands": "isomiso2avc1mp41",
|
||||||
|
"encoder": "Lavf58.20.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
||||||
|
"profile": "Main",
|
||||||
|
"codec_type": "video",
|
||||||
|
"codec_time_base": "1001/48000",
|
||||||
|
"codec_tag_string": "avc1",
|
||||||
|
"codec_tag": "0x31637661",
|
||||||
|
"width": 1920,
|
||||||
|
"height": 1080,
|
||||||
|
"coded_width": 1920,
|
||||||
|
"coded_height": 1088,
|
||||||
|
"closed_captions": 0,
|
||||||
|
"has_b_frames": 1,
|
||||||
|
"sample_aspect_ratio": "1:1",
|
||||||
|
"display_aspect_ratio": "16:9",
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"level": 41,
|
||||||
|
"chroma_location": "left",
|
||||||
|
"refs": 1,
|
||||||
|
"is_avc": "true",
|
||||||
|
"nal_length_size": "4",
|
||||||
|
"r_frame_rate": "24000/1001",
|
||||||
|
"avg_frame_rate": "24000/1001",
|
||||||
|
"time_base": "1/24000",
|
||||||
|
"start_pts": 1000,
|
||||||
|
"start_time": "0.041667",
|
||||||
|
"duration_ts": 29095066,
|
||||||
|
"duration": "1212.294417",
|
||||||
|
"bit_rate": "3948341",
|
||||||
|
"bits_per_raw_sample": "8",
|
||||||
|
"nb_frames": "29066",
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"creation_time": "2020-01-20T13:56:34.000000Z",
|
||||||
|
"language": "eng",
|
||||||
|
"handler_name": "\fVideoHandler",
|
||||||
|
"encoder": "h264"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"codec_name": "ac3",
|
||||||
|
"codec_long_name": "ATSC A/52A (AC-3)",
|
||||||
|
"codec_type": "audio",
|
||||||
|
"codec_time_base": "1/48000",
|
||||||
|
"codec_tag_string": "ac-3",
|
||||||
|
"codec_tag": "0x332d6361",
|
||||||
|
"sample_fmt": "fltp",
|
||||||
|
"sample_rate": "48000",
|
||||||
|
"channels": 2,
|
||||||
|
"channel_layout": "stereo",
|
||||||
|
"bits_per_sample": 0,
|
||||||
|
"dmix_mode": "-1",
|
||||||
|
"ltrt_cmixlev": "-1.000000",
|
||||||
|
"ltrt_surmixlev": "-1.000000",
|
||||||
|
"loro_cmixlev": "-1.000000",
|
||||||
|
"loro_surmixlev": "-1.000000",
|
||||||
|
"r_frame_rate": "0/0",
|
||||||
|
"avg_frame_rate": "0/0",
|
||||||
|
"time_base": "1/48000",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration_ts": 58232832,
|
||||||
|
"duration": "1213.184000",
|
||||||
|
"bit_rate": "224000",
|
||||||
|
"nb_frames": "37912",
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"creation_time": "2020-01-20T13:56:34.000000Z",
|
||||||
|
"language": "eng",
|
||||||
|
"handler_name": "\fSoundHandler"
|
||||||
|
},
|
||||||
|
"side_data_list": [
|
||||||
|
{
|
||||||
|
"side_data_type": "Audio Service Type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"filename": "The Big Bang Theory - S01E17.mp4",
|
||||||
|
"nb_streams": 2,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
|
"format_long_name": "QuickTime / MOV",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration": "1213.184000",
|
||||||
|
"size": "633084606",
|
||||||
|
"bit_rate": "4174698",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"major_brand": "mp42",
|
||||||
|
"minor_version": "512",
|
||||||
|
"compatible_brands": "mp42",
|
||||||
|
"creation_time": "2020-01-20T13:56:34.000000Z",
|
||||||
|
"media_type": "9",
|
||||||
|
"season_number": "0",
|
||||||
|
"episode_sort": "0",
|
||||||
|
"hd_video": "0",
|
||||||
|
"iTunMOVI": "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>studio</key><string>studio</string><key>cast</key><array><dict><key>name</key><string></string></dict></array><key>directors</key><array><dict><key>name</key><string></string></dict></array><key>producers</key><array><dict><key>name</key><string></string></dict></array><key>codirectors</key><array><dict><key>name</key><string>codirector</string></dict></array><key>screenwriters</key><array><dict><key>name</key><string></string></dict></array></dict></plist>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
{
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"codec_name": "h264",
|
||||||
|
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
|
||||||
|
"profile": "High",
|
||||||
|
"codec_type": "video",
|
||||||
|
"codec_time_base": "1/50",
|
||||||
|
"codec_tag_string": "avc1",
|
||||||
|
"codec_tag": "0x31637661",
|
||||||
|
"width": 1280,
|
||||||
|
"height": 720,
|
||||||
|
"coded_width": 1280,
|
||||||
|
"coded_height": 720,
|
||||||
|
"closed_captions": 0,
|
||||||
|
"has_b_frames": 2,
|
||||||
|
"pix_fmt": "yuv420p",
|
||||||
|
"level": 31,
|
||||||
|
"chroma_location": "left",
|
||||||
|
"refs": 1,
|
||||||
|
"is_avc": "true",
|
||||||
|
"nal_length_size": "4",
|
||||||
|
"r_frame_rate": "25/1",
|
||||||
|
"avg_frame_rate": "25/1",
|
||||||
|
"time_base": "1/12800",
|
||||||
|
"start_pts": 0,
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration_ts": 3840000,
|
||||||
|
"duration": "300.000000",
|
||||||
|
"bit_rate": "53288",
|
||||||
|
"bits_per_raw_sample": "8",
|
||||||
|
"nb_frames": "7500",
|
||||||
|
"disposition": {
|
||||||
|
"default": 1,
|
||||||
|
"dub": 0,
|
||||||
|
"original": 0,
|
||||||
|
"comment": 0,
|
||||||
|
"lyrics": 0,
|
||||||
|
"karaoke": 0,
|
||||||
|
"forced": 0,
|
||||||
|
"hearing_impaired": 0,
|
||||||
|
"visual_impaired": 0,
|
||||||
|
"clean_effects": 0,
|
||||||
|
"attached_pic": 0,
|
||||||
|
"timed_thumbnails": 0
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"language": "und",
|
||||||
|
"handler_name": "VideoHandler"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"format": {
|
||||||
|
"filename": "test-gray.720p.mp4",
|
||||||
|
"nb_streams": 1,
|
||||||
|
"nb_programs": 0,
|
||||||
|
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||||
|
"format_long_name": "QuickTime / MOV",
|
||||||
|
"start_time": "0.000000",
|
||||||
|
"duration": "300.000000",
|
||||||
|
"size": "2086818",
|
||||||
|
"bit_rate": "55648",
|
||||||
|
"probe_score": 100,
|
||||||
|
"tags": {
|
||||||
|
"major_brand": "isom",
|
||||||
|
"minor_version": "512",
|
||||||
|
"compatible_brands": "isomiso2avc1mp41",
|
||||||
|
"encoder": "Lavf58.20.100"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue