diff --git a/.gitignore b/.gitignore index 1598a7ac36..c74a54d71e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ tmp/ *.bak *.swp *~.nib +project.fragment.lock.json +project.lock.json local.properties .classpath .settings/ diff --git a/BDInfo/BDInfo.csproj b/BDInfo/BDInfo.csproj new file mode 100644 index 0000000000..e7013f341b --- /dev/null +++ b/BDInfo/BDInfo.csproj @@ -0,0 +1,77 @@ + + + + + 11.0 + Debug + AnyCPU + {88AE38DF-19D7-406F-A6A9-09527719A21E} + Library + Properties + BDInfo + BDInfo + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/BDInfo/BDInfo.nuget.targets b/BDInfo/BDInfo.nuget.targets new file mode 100644 index 0000000000..e69ce0e64f --- /dev/null +++ b/BDInfo/BDInfo.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BDInfo/BDInfoSettings.cs b/BDInfo/BDInfoSettings.cs new file mode 100644 index 0000000000..7abb67499f --- /dev/null +++ b/BDInfo/BDInfoSettings.cs @@ -0,0 +1,105 @@ + +namespace BDInfo +{ + class BDInfoSettings + { + public static bool GenerateStreamDiagnostics + { + get + { + return true; + } + } + + public static bool EnableSSIF + { + get + { + return true; + } + } + + public static bool AutosaveReport + { + get + { + return false; + } + } + + public static bool GenerateFrameDataFile + { + get + { + return false; + } + } + + public static bool FilterLoopingPlaylists + { + get + { + return true; + } + } + + public static bool FilterShortPlaylists + { + get + { + return false; + } + } + + public static int FilterShortPlaylistsValue + { + get + { + return 0; + } + } + + public static bool UseImagePrefix + { + get + { + return false; + } + } + + public static string UseImagePrefixValue + { + get + { + return null; + } + } + + /// + /// Setting this to false throws an IComparer error on some discs. + /// + public static bool KeepStreamOrder + { + get + { + return true; + } + } + + public static bool GenerateTextSummary + { + get + { + return false; + } + } + + public static string LastPath + { + get + { + return string.Empty; + } + } + } +} diff --git a/BDInfo/BDROM.cs b/BDInfo/BDROM.cs new file mode 100644 index 0000000000..97dbfbf3bd --- /dev/null +++ b/BDInfo/BDROM.cs @@ -0,0 +1,437 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; + +namespace BDInfo +{ + public class BDROM + { + public FileSystemMetadata DirectoryRoot = null; + public FileSystemMetadata DirectoryBDMV = null; + public FileSystemMetadata DirectoryBDJO = null; + public FileSystemMetadata DirectoryCLIPINF = null; + public FileSystemMetadata DirectoryPLAYLIST = null; + public FileSystemMetadata DirectorySNP = null; + public FileSystemMetadata DirectorySSIF = null; + public FileSystemMetadata DirectorySTREAM = null; + + public string VolumeLabel = null; + public ulong Size = 0; + public bool IsBDPlus = false; + public bool IsBDJava = false; + public bool IsDBOX = false; + public bool IsPSP = false; + public bool Is3D = false; + public bool Is50Hz = false; + + private readonly IFileSystem _fileSystem; + + public Dictionary PlaylistFiles = + new Dictionary(); + public Dictionary StreamClipFiles = + new Dictionary(); + public Dictionary StreamFiles = + new Dictionary(); + public Dictionary InterleavedFiles = + new Dictionary(); + + private static List ExcludeDirs = new List { "ANY!", "AACS", "BDSVM", "ANYVM", "SLYVM" }; + + public delegate bool OnStreamClipFileScanError( + TSStreamClipFile streamClipFile, Exception ex); + + public event OnStreamClipFileScanError StreamClipFileScanError; + + public delegate bool OnStreamFileScanError( + TSStreamFile streamClipFile, Exception ex); + + public event OnStreamFileScanError StreamFileScanError; + + public delegate bool OnPlaylistFileScanError( + TSPlaylistFile playlistFile, Exception ex); + + public event OnPlaylistFileScanError PlaylistFileScanError; + + public BDROM( + string path, IFileSystem fileSystem, ITextEncoding textEncoding) + { + _fileSystem = fileSystem; + // + // Locate BDMV directories. + // + + DirectoryBDMV = + GetDirectoryBDMV(path); + + if (DirectoryBDMV == null) + { + throw new Exception("Unable to locate BD structure."); + } + + DirectoryRoot = + _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(DirectoryBDMV.FullName)); + DirectoryBDJO = + GetDirectory("BDJO", DirectoryBDMV, 0); + DirectoryCLIPINF = + GetDirectory("CLIPINF", DirectoryBDMV, 0); + DirectoryPLAYLIST = + GetDirectory("PLAYLIST", DirectoryBDMV, 0); + DirectorySNP = + GetDirectory("SNP", DirectoryRoot, 0); + DirectorySTREAM = + GetDirectory("STREAM", DirectoryBDMV, 0); + DirectorySSIF = + GetDirectory("SSIF", DirectorySTREAM, 0); + + if (DirectoryCLIPINF == null + || DirectoryPLAYLIST == null) + { + throw new Exception("Unable to locate BD structure."); + } + + // + // Initialize basic disc properties. + // + + VolumeLabel = GetVolumeLabel(DirectoryRoot); + Size = (ulong)GetDirectorySize(DirectoryRoot); + + if (null != GetDirectory("BDSVM", DirectoryRoot, 0)) + { + IsBDPlus = true; + } + if (null != GetDirectory("SLYVM", DirectoryRoot, 0)) + { + IsBDPlus = true; + } + if (null != GetDirectory("ANYVM", DirectoryRoot, 0)) + { + IsBDPlus = true; + } + + if (DirectoryBDJO != null && + _fileSystem.GetFiles(DirectoryBDJO.FullName).Any()) + { + IsBDJava = true; + } + + if (DirectorySNP != null && + GetFiles(DirectorySNP.FullName, ".mnv").Any()) + { + IsPSP = true; + } + + if (DirectorySSIF != null && + _fileSystem.GetFiles(DirectorySSIF.FullName).Any()) + { + Is3D = true; + } + + if (_fileSystem.FileExists(Path.Combine(DirectoryRoot.FullName, "FilmIndex.xml"))) + { + IsDBOX = true; + } + + // + // Initialize file lists. + // + + if (DirectoryPLAYLIST != null) + { + FileSystemMetadata[] files = GetFiles(DirectoryPLAYLIST.FullName, ".mpls").ToArray(); + foreach (FileSystemMetadata file in files) + { + PlaylistFiles.Add( + file.Name.ToUpper(), new TSPlaylistFile(this, file, _fileSystem, textEncoding)); + } + } + + if (DirectorySTREAM != null) + { + FileSystemMetadata[] files = GetFiles(DirectorySTREAM.FullName, ".m2ts").ToArray(); + foreach (FileSystemMetadata file in files) + { + StreamFiles.Add( + file.Name.ToUpper(), new TSStreamFile(file, _fileSystem)); + } + } + + if (DirectoryCLIPINF != null) + { + FileSystemMetadata[] files = GetFiles(DirectoryCLIPINF.FullName, ".clpi").ToArray(); + foreach (FileSystemMetadata file in files) + { + StreamClipFiles.Add( + file.Name.ToUpper(), new TSStreamClipFile(file, _fileSystem, textEncoding)); + } + } + + if (DirectorySSIF != null) + { + FileSystemMetadata[] files = GetFiles(DirectorySSIF.FullName, ".ssif").ToArray(); + foreach (FileSystemMetadata file in files) + { + InterleavedFiles.Add( + file.Name.ToUpper(), new TSInterleavedFile(file)); + } + } + } + + private IEnumerable GetFiles(string path, string extension) + { + return _fileSystem.GetFiles(path).Where(i => string.Equals(i.Extension, extension, StringComparison.OrdinalIgnoreCase)); + } + + public void Scan() + { + List errorStreamClipFiles = new List(); + foreach (TSStreamClipFile streamClipFile in StreamClipFiles.Values) + { + try + { + streamClipFile.Scan(); + } + catch (Exception ex) + { + errorStreamClipFiles.Add(streamClipFile); + if (StreamClipFileScanError != null) + { + if (StreamClipFileScanError(streamClipFile, ex)) + { + continue; + } + else + { + break; + } + } + else throw ex; + } + } + + foreach (TSStreamFile streamFile in StreamFiles.Values) + { + string ssifName = Path.GetFileNameWithoutExtension(streamFile.Name) + ".SSIF"; + if (InterleavedFiles.ContainsKey(ssifName)) + { + streamFile.InterleavedFile = InterleavedFiles[ssifName]; + } + } + + TSStreamFile[] streamFiles = new TSStreamFile[StreamFiles.Count]; + StreamFiles.Values.CopyTo(streamFiles, 0); + Array.Sort(streamFiles, CompareStreamFiles); + + List errorPlaylistFiles = new List(); + foreach (TSPlaylistFile playlistFile in PlaylistFiles.Values) + { + try + { + playlistFile.Scan(StreamFiles, StreamClipFiles); + } + catch (Exception ex) + { + errorPlaylistFiles.Add(playlistFile); + if (PlaylistFileScanError != null) + { + if (PlaylistFileScanError(playlistFile, ex)) + { + continue; + } + else + { + break; + } + } + else throw ex; + } + } + + List errorStreamFiles = new List(); + foreach (TSStreamFile streamFile in streamFiles) + { + try + { + List playlists = new List(); + foreach (TSPlaylistFile playlist in PlaylistFiles.Values) + { + foreach (TSStreamClip streamClip in playlist.StreamClips) + { + if (streamClip.Name == streamFile.Name) + { + playlists.Add(playlist); + break; + } + } + } + streamFile.Scan(playlists, false); + } + catch (Exception ex) + { + errorStreamFiles.Add(streamFile); + if (StreamFileScanError != null) + { + if (StreamFileScanError(streamFile, ex)) + { + continue; + } + else + { + break; + } + } + else throw ex; + } + } + + foreach (TSPlaylistFile playlistFile in PlaylistFiles.Values) + { + playlistFile.Initialize(); + if (!Is50Hz) + { + foreach (TSVideoStream videoStream in playlistFile.VideoStreams) + { + if (videoStream.FrameRate == TSFrameRate.FRAMERATE_25 || + videoStream.FrameRate == TSFrameRate.FRAMERATE_50) + { + Is50Hz = true; + } + } + } + } + } + + private FileSystemMetadata GetDirectoryBDMV( + string path) + { + FileSystemMetadata dir = _fileSystem.GetDirectoryInfo(path); + + while (dir != null) + { + if (dir.Name == "BDMV") + { + return dir; + } + dir = _fileSystem.GetDirectoryInfo(Path.GetDirectoryName(dir.FullName)); + } + + return GetDirectory("BDMV", _fileSystem.GetDirectoryInfo(path), 0); + } + + private FileSystemMetadata GetDirectory( + string name, + FileSystemMetadata dir, + int searchDepth) + { + if (dir != null) + { + FileSystemMetadata[] children = _fileSystem.GetDirectories(dir.FullName).ToArray(); + foreach (FileSystemMetadata child in children) + { + if (child.Name == name) + { + return child; + } + } + if (searchDepth > 0) + { + foreach (FileSystemMetadata child in children) + { + GetDirectory( + name, child, searchDepth - 1); + } + } + } + return null; + } + + private long GetDirectorySize(FileSystemMetadata directoryInfo) + { + long size = 0; + + //if (!ExcludeDirs.Contains(directoryInfo.Name.ToUpper())) // TODO: Keep? + { + FileSystemMetadata[] pathFiles = _fileSystem.GetFiles(directoryInfo.FullName).ToArray(); + foreach (FileSystemMetadata pathFile in pathFiles) + { + if (pathFile.Extension.ToUpper() == ".SSIF") + { + continue; + } + size += pathFile.Length; + } + + FileSystemMetadata[] pathChildren = _fileSystem.GetDirectories(directoryInfo.FullName).ToArray(); + foreach (FileSystemMetadata pathChild in pathChildren) + { + size += GetDirectorySize(pathChild); + } + } + + return size; + } + + private string GetVolumeLabel(FileSystemMetadata dir) + { + return dir.Name; + } + + public static int CompareStreamFiles( + TSStreamFile x, + TSStreamFile y) + { + // TODO: Use interleaved file sizes + + if ((x == null || x.FileInfo == null) && (y == null || y.FileInfo == null)) + { + return 0; + } + else if ((x == null || x.FileInfo == null) && (y != null && y.FileInfo != null)) + { + return 1; + } + else if ((x != null || x.FileInfo != null) && (y == null || y.FileInfo == null)) + { + return -1; + } + else + { + if (x.FileInfo.Length > y.FileInfo.Length) + { + return 1; + } + else if (y.FileInfo.Length > x.FileInfo.Length) + { + return -1; + } + else + { + return 0; + } + } + } + + } +} diff --git a/BDInfo/BitVector32.cs b/BDInfo/BitVector32.cs new file mode 100644 index 0000000000..1ac94bfbeb --- /dev/null +++ b/BDInfo/BitVector32.cs @@ -0,0 +1,308 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BDInfo +{ + using System.Diagnostics; + using System.Text; + using System; + + /// + /// Provides a simple light bit vector with easy integer or Boolean access to + /// a 32 bit storage. + /// + public struct BitVector32 + { + private uint data; + + /// + /// Initializes a new instance of the BitVector32 structure with the specified internal data. + /// + public BitVector32(int data) + { + this.data = (uint)data; + } + + /// + /// Initializes a new instance of the BitVector32 structure with the information in the specified + /// value. + /// + public BitVector32(BitVector32 value) + { + this.data = value.data; + } + + /// + /// Gets or sets a value indicating whether all the specified bits are set. + /// + public bool this[int bit] + { + get + { + return (data & bit) == (uint)bit; + } + set + { + if (value) + { + data |= (uint)bit; + } + else + { + data &= ~(uint)bit; + } + } + } + + /// + /// Gets or sets the value for the specified section. + /// + public int this[Section section] + { + get + { + return (int)((data & (uint)(section.Mask << section.Offset)) >> section.Offset); + } + set + { + value <<= section.Offset; + int offsetMask = (0xFFFF & (int)section.Mask) << section.Offset; + data = (data & ~(uint)offsetMask) | ((uint)value & (uint)offsetMask); + } + } + + /// + /// returns the raw data stored in this bit vector... + /// + public int Data + { + get + { + return (int)data; + } + } + + private static short CountBitsSet(short mask) + { + + // yes, I know there are better algorithms, however, we know the + // bits are always right aligned, with no holes (i.e. always 00000111, + // never 000100011), so this is just fine... + // + short value = 0; + while ((mask & 0x1) != 0) + { + value++; + mask >>= 1; + } + return value; + } + + /// + /// Creates the first mask in a series. + /// + public static int CreateMask() + { + return CreateMask(0); + } + + /// + /// Creates the next mask in a series. + /// + public static int CreateMask(int previous) + { + if (previous == 0) + { + return 1; + } + + if (previous == unchecked((int)0x80000000)) + { + throw new InvalidOperationException("Bit vector full"); + } + + return previous << 1; + } + + /// + /// Given a highValue, creates the mask + /// + private static short CreateMaskFromHighValue(short highValue) + { + short required = 16; + while ((highValue & 0x8000) == 0) + { + required--; + highValue <<= 1; + } + + ushort value = 0; + while (required > 0) + { + required--; + value <<= 1; + value |= 0x1; + } + + return unchecked((short)value); + } + + /// + /// Creates the first section in a series, with the specified maximum value. + /// + public static Section CreateSection(short maxValue) + { + return CreateSectionHelper(maxValue, 0, 0); + } + + /// + /// Creates the next section in a series, with the specified maximum value. + /// + public static Section CreateSection(short maxValue, Section previous) + { + return CreateSectionHelper(maxValue, previous.Mask, previous.Offset); + } + + private static Section CreateSectionHelper(short maxValue, short priorMask, short priorOffset) + { + if (maxValue < 1) + { + throw new ArgumentOutOfRangeException("maxValue"); + } +#if DEBUG + int maskCheck = CreateMaskFromHighValue(maxValue); + int offsetCheck = priorOffset + CountBitsSet(priorMask); + Debug.Assert(maskCheck <= short.MaxValue && offsetCheck < 32, "Overflow on BitVector32"); +#endif + short offset = (short)(priorOffset + CountBitsSet(priorMask)); + if (offset >= 32) + { + throw new InvalidOperationException("Bit vector full"); + } + return new Section(CreateMaskFromHighValue(maxValue), offset); + } + + public override bool Equals(object o) + { + if (!(o is BitVector32)) + { + return false; + } + + return data == ((BitVector32)o).data; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// + public static string ToString(BitVector32 value) + { + StringBuilder sb = new StringBuilder(/*"BitVector32{".Length*/12 + /*32 bits*/32 + /*"}".Length"*/1); + sb.Append("BitVector32{"); + int locdata = (int)value.data; + for (int i = 0; i < 32; i++) + { + if ((locdata & 0x80000000) != 0) + { + sb.Append("1"); + } + else + { + sb.Append("0"); + } + locdata <<= 1; + } + sb.Append("}"); + return sb.ToString(); + } + + /// + /// + public override string ToString() + { + return BitVector32.ToString(this); + } + + /// + /// + /// Represents an section of the vector that can contain a integer number. + /// + public struct Section + { + private readonly short mask; + private readonly short offset; + + internal Section(short mask, short offset) + { + this.mask = mask; + this.offset = offset; + } + + public short Mask + { + get + { + return mask; + } + } + + public short Offset + { + get + { + return offset; + } + } + + public override bool Equals(object o) + { + if (o is Section) + return Equals((Section)o); + else + return false; + } + + public bool Equals(Section obj) + { + return obj.mask == mask && obj.offset == offset; + } + + public static bool operator ==(Section a, Section b) + { + return a.Equals(b); + } + + public static bool operator !=(Section a, Section b) + { + return !(a == b); + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + + /// + /// + public static string ToString(Section value) + { + return "Section{0x" + Convert.ToString(value.Mask, 16) + ", 0x" + Convert.ToString(value.Offset, 16) + "}"; + } + + /// + /// + public override string ToString() + { + return Section.ToString(this); + } + + } + } +} diff --git a/BDInfo/LanguageCodes.cs b/BDInfo/LanguageCodes.cs new file mode 100644 index 0000000000..90d0bccc43 --- /dev/null +++ b/BDInfo/LanguageCodes.cs @@ -0,0 +1,493 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class LanguageCodes + { + public static string GetName(string code) + { + switch (code) + { + case "abk": return "Abkhazian"; + case "ace": return "Achinese"; + case "ach": return "Acoli"; + case "ada": return "Adangme"; + case "aar": return "Afar"; + case "afh": return "Afrihili"; + case "afr": return "Afrikaans"; + case "afa": return "Afro-Asiatic (Other)"; + case "aka": return "Akan"; + case "akk": return "Akkadian"; + case "alb": return "Albanian"; + case "sqi": return "Albanian"; + case "ale": return "Aleut"; + case "alg": return "Algonquian languages"; + case "tut": return "Altaic (Other)"; + case "amh": return "Amharic"; + case "apa": return "Apache languages"; + case "ara": return "Arabic"; + case "arc": return "Aramaic"; + case "arp": return "Arapaho"; + case "arn": return "Araucanian"; + case "arw": return "Arawak"; + case "arm": return "Armenian"; + case "hye": return "Armenian"; + case "art": return "Artificial (Other)"; + case "asm": return "Assamese"; + case "ath": return "Athapascan languages"; + case "aus": return "Australian languages"; + case "map": return "Austronesian (Other)"; + case "ava": return "Avaric"; + case "ave": return "Avestan"; + case "awa": return "Awadhi"; + case "aym": return "Aymara"; + case "aze": return "Azerbaijani"; + case "ban": return "Balinese"; + case "bat": return "Baltic (Other)"; + case "bal": return "Baluchi"; + case "bam": return "Bambara"; + case "bai": return "Bamileke languages"; + case "bad": return "Banda"; + case "bnt": return "Bantu (Other)"; + case "bas": return "Basa"; + case "bak": return "Bashkir"; + case "baq": return "Basque"; + case "eus": return "Basque"; + case "btk": return "Batak (Indonesia)"; + case "bej": return "Beja"; + case "bel": return "Belarusian"; + case "bem": return "Bemba"; + case "ben": return "Bengali"; + case "ber": return "Berber (Other)"; + case "bho": return "Bhojpuri"; + case "bih": return "Bihari"; + case "bik": return "Bikol"; + case "bin": return "Bini"; + case "bis": return "Bislama"; + case "bos": return "Bosnian"; + case "bra": return "Braj"; + case "bre": return "Breton"; + case "bug": return "Buginese"; + case "bul": return "Bulgarian"; + case "bua": return "Buriat"; + case "bur": return "Burmese"; + case "mya": return "Burmese"; + case "cad": return "Caddo"; + case "car": return "Carib"; + case "cat": return "Catalan"; + case "cau": return "Caucasian (Other)"; + case "ceb": return "Cebuano"; + case "cel": return "Celtic (Other)"; + case "cai": return "Central American Indian (Other)"; + case "chg": return "Chagatai"; + case "cmc": return "Chamic languages"; + case "cha": return "Chamorro"; + case "che": return "Chechen"; + case "chr": return "Cherokee"; + case "chy": return "Cheyenne"; + case "chb": return "Chibcha"; + case "chi": return "Chinese"; + case "zho": return "Chinese"; + case "chn": return "Chinook jargon"; + case "chp": return "Chipewyan"; + case "cho": return "Choctaw"; + case "chu": return "Church Slavic"; + case "chk": return "Chuukese"; + case "chv": return "Chuvash"; + case "cop": return "Coptic"; + case "cor": return "Cornish"; + case "cos": return "Corsican"; + case "cre": return "Cree"; + case "mus": return "Creek"; + case "crp": return "Creoles and pidgins (Other)"; + case "cpe": return "Creoles and pidgins,"; + case "cpf": return "Creoles and pidgins,"; + case "cpp": return "Creoles and pidgins,"; + case "scr": return "Croatian"; + case "hrv": return "Croatian"; + case "cus": return "Cushitic (Other)"; + case "cze": return "Czech"; + case "ces": return "Czech"; + case "dak": return "Dakota"; + case "dan": return "Danish"; + case "day": return "Dayak"; + case "del": return "Delaware"; + case "din": return "Dinka"; + case "div": return "Divehi"; + case "doi": return "Dogri"; + case "dgr": return "Dogrib"; + case "dra": return "Dravidian (Other)"; + case "dua": return "Duala"; + case "dut": return "Dutch"; + case "nld": return "Dutch"; + case "dum": return "Dutch, Middle (ca. 1050-1350)"; + case "dyu": return "Dyula"; + case "dzo": return "Dzongkha"; + case "efi": return "Efik"; + case "egy": return "Egyptian (Ancient)"; + case "eka": return "Ekajuk"; + case "elx": return "Elamite"; + case "eng": return "English"; + case "enm": return "English, Middle (1100-1500)"; + case "ang": return "English, Old (ca.450-1100)"; + case "epo": return "Esperanto"; + case "est": return "Estonian"; + case "ewe": return "Ewe"; + case "ewo": return "Ewondo"; + case "fan": return "Fang"; + case "fat": return "Fanti"; + case "fao": return "Faroese"; + case "fij": return "Fijian"; + case "fin": return "Finnish"; + case "fiu": return "Finno-Ugrian (Other)"; + case "fon": return "Fon"; + case "fre": return "French"; + case "fra": return "French"; + case "frm": return "French, Middle (ca.1400-1600)"; + case "fro": return "French, Old (842-ca.1400)"; + case "fry": return "Frisian"; + case "fur": return "Friulian"; + case "ful": return "Fulah"; + case "gaa": return "Ga"; + case "glg": return "Gallegan"; + case "lug": return "Ganda"; + case "gay": return "Gayo"; + case "gba": return "Gbaya"; + case "gez": return "Geez"; + case "geo": return "Georgian"; + case "kat": return "Georgian"; + case "ger": return "German"; + case "deu": return "German"; + case "nds": return "Saxon"; + case "gmh": return "German, Middle High (ca.1050-1500)"; + case "goh": return "German, Old High (ca.750-1050)"; + case "gem": return "Germanic (Other)"; + case "gil": return "Gilbertese"; + case "gon": return "Gondi"; + case "gor": return "Gorontalo"; + case "got": return "Gothic"; + case "grb": return "Grebo"; + case "grc": return "Greek, Ancient (to 1453)"; + case "gre": return "Greek"; + case "ell": return "Greek"; + case "grn": return "Guarani"; + case "guj": return "Gujarati"; + case "gwi": return "Gwich´in"; + case "hai": return "Haida"; + case "hau": return "Hausa"; + case "haw": return "Hawaiian"; + case "heb": return "Hebrew"; + case "her": return "Herero"; + case "hil": return "Hiligaynon"; + case "him": return "Himachali"; + case "hin": return "Hindi"; + case "hmo": return "Hiri Motu"; + case "hit": return "Hittite"; + case "hmn": return "Hmong"; + case "hun": return "Hungarian"; + case "hup": return "Hupa"; + case "iba": return "Iban"; + case "ice": return "Icelandic"; + case "isl": return "Icelandic"; + case "ibo": return "Igbo"; + case "ijo": return "Ijo"; + case "ilo": return "Iloko"; + case "inc": return "Indic (Other)"; + case "ine": return "Indo-European (Other)"; + case "ind": return "Indonesian"; + case "ina": return "Interlingua (International"; + case "ile": return "Interlingue"; + case "iku": return "Inuktitut"; + case "ipk": return "Inupiaq"; + case "ira": return "Iranian (Other)"; + case "gle": return "Irish"; + case "mga": return "Irish, Middle (900-1200)"; + case "sga": return "Irish, Old (to 900)"; + case "iro": return "Iroquoian languages"; + case "ita": return "Italian"; + case "jpn": return "Japanese"; + case "jav": return "Javanese"; + case "jrb": return "Judeo-Arabic"; + case "jpr": return "Judeo-Persian"; + case "kab": return "Kabyle"; + case "kac": return "Kachin"; + case "kal": return "Kalaallisut"; + case "kam": return "Kamba"; + case "kan": return "Kannada"; + case "kau": return "Kanuri"; + case "kaa": return "Kara-Kalpak"; + case "kar": return "Karen"; + case "kas": return "Kashmiri"; + case "kaw": return "Kawi"; + case "kaz": return "Kazakh"; + case "kha": return "Khasi"; + case "khm": return "Khmer"; + case "khi": return "Khoisan (Other)"; + case "kho": return "Khotanese"; + case "kik": return "Kikuyu"; + case "kmb": return "Kimbundu"; + case "kin": return "Kinyarwanda"; + case "kir": return "Kirghiz"; + case "kom": return "Komi"; + case "kon": return "Kongo"; + case "kok": return "Konkani"; + case "kor": return "Korean"; + case "kos": return "Kosraean"; + case "kpe": return "Kpelle"; + case "kro": return "Kru"; + case "kua": return "Kuanyama"; + case "kum": return "Kumyk"; + case "kur": return "Kurdish"; + case "kru": return "Kurukh"; + case "kut": return "Kutenai"; + case "lad": return "Ladino"; + case "lah": return "Lahnda"; + case "lam": return "Lamba"; + case "lao": return "Lao"; + case "lat": return "Latin"; + case "lav": return "Latvian"; + case "ltz": return "Letzeburgesch"; + case "lez": return "Lezghian"; + case "lin": return "Lingala"; + case "lit": return "Lithuanian"; + case "loz": return "Lozi"; + case "lub": return "Luba-Katanga"; + case "lua": return "Luba-Lulua"; + case "lui": return "Luiseno"; + case "lun": return "Lunda"; + case "luo": return "Luo (Kenya and Tanzania)"; + case "lus": return "Lushai"; + case "mac": return "Macedonian"; + case "mkd": return "Macedonian"; + case "mad": return "Madurese"; + case "mag": return "Magahi"; + case "mai": return "Maithili"; + case "mak": return "Makasar"; + case "mlg": return "Malagasy"; + case "may": return "Malay"; + case "msa": return "Malay"; + case "mal": return "Malayalam"; + case "mlt": return "Maltese"; + case "mnc": return "Manchu"; + case "mdr": return "Mandar"; + case "man": return "Mandingo"; + case "mni": return "Manipuri"; + case "mno": return "Manobo languages"; + case "glv": return "Manx"; + case "mao": return "Maori"; + case "mri": return "Maori"; + case "mar": return "Marathi"; + case "chm": return "Mari"; + case "mah": return "Marshall"; + case "mwr": return "Marwari"; + case "mas": return "Masai"; + case "myn": return "Mayan languages"; + case "men": return "Mende"; + case "mic": return "Micmac"; + case "min": return "Minangkabau"; + case "mis": return "Miscellaneous languages"; + case "moh": return "Mohawk"; + case "mol": return "Moldavian"; + case "mkh": return "Mon-Khmer (Other)"; + case "lol": return "Mongo"; + case "mon": return "Mongolian"; + case "mos": return "Mossi"; + case "mul": return "Multiple languages"; + case "mun": return "Munda languages"; + case "nah": return "Nahuatl"; + case "nau": return "Nauru"; + case "nav": return "Navajo"; + case "nde": return "Ndebele, North"; + case "nbl": return "Ndebele, South"; + case "ndo": return "Ndonga"; + case "nep": return "Nepali"; + case "new": return "Newari"; + case "nia": return "Nias"; + case "nic": return "Niger-Kordofanian (Other)"; + case "ssa": return "Nilo-Saharan (Other)"; + case "niu": return "Niuean"; + case "non": return "Norse, Old"; + case "nai": return "North American Indian (Other)"; + case "sme": return "Northern Sami"; + case "nor": return "Norwegian"; + case "nob": return "Norwegian Bokmål"; + case "nno": return "Norwegian Nynorsk"; + case "nub": return "Nubian languages"; + case "nym": return "Nyamwezi"; + case "nya": return "Nyanja"; + case "nyn": return "Nyankole"; + case "nyo": return "Nyoro"; + case "nzi": return "Nzima"; + case "oci": return "Occitan"; + case "oji": return "Ojibwa"; + case "ori": return "Oriya"; + case "orm": return "Oromo"; + case "osa": return "Osage"; + case "oss": return "Ossetian"; + case "oto": return "Otomian languages"; + case "pal": return "Pahlavi"; + case "pau": return "Palauan"; + case "pli": return "Pali"; + case "pam": return "Pampanga"; + case "pag": return "Pangasinan"; + case "pan": return "Panjabi"; + case "pap": return "Papiamento"; + case "paa": return "Papuan (Other)"; + case "per": return "Persian"; + case "fas": return "Persian"; + case "peo": return "Persian, Old (ca.600-400 B.C.)"; + case "phi": return "Philippine (Other)"; + case "phn": return "Phoenician"; + case "pon": return "Pohnpeian"; + case "pol": return "Polish"; + case "por": return "Portuguese"; + case "pra": return "Prakrit languages"; + case "pro": return "Provençal"; + case "pus": return "Pushto"; + case "que": return "Quechua"; + case "roh": return "Raeto-Romance"; + case "raj": return "Rajasthani"; + case "rap": return "Rapanui"; + case "rar": return "Rarotongan"; + case "roa": return "Romance (Other)"; + case "rum": return "Romanian"; + case "ron": return "Romanian"; + case "rom": return "Romany"; + case "run": return "Rundi"; + case "rus": return "Russian"; + case "sal": return "Salishan languages"; + case "sam": return "Samaritan Aramaic"; + case "smi": return "Sami languages (Other)"; + case "smo": return "Samoan"; + case "sad": return "Sandawe"; + case "sag": return "Sango"; + case "san": return "Sanskrit"; + case "sat": return "Santali"; + case "srd": return "Sardinian"; + case "sas": return "Sasak"; + case "sco": return "Scots"; + case "gla": return "Gaelic"; + case "sel": return "Selkup"; + case "sem": return "Semitic (Other)"; + case "scc": return "Serbian"; + case "srp": return "Serbian"; + case "srr": return "Serer"; + case "shn": return "Shan"; + case "sna": return "Shona"; + case "sid": return "Sidamo"; + case "sgn": return "Sign languages"; + case "bla": return "Siksika"; + case "snd": return "Sindhi"; + case "sin": return "Sinhalese"; + case "sit": return "Sino-Tibetan (Other)"; + case "sio": return "Siouan languages"; + case "den": return "Slave (Athapascan)"; + case "sla": return "Slavic (Other)"; + case "slo": return "Slovak"; + case "slk": return "Slovak"; + case "slv": return "Slovenian"; + case "sog": return "Sogdian"; + case "som": return "Somali"; + case "son": return "Songhai"; + case "snk": return "Soninke"; + case "wen": return "Sorbian languages"; + case "nso": return "Sotho, Northern"; + case "sot": return "Sotho, Southern"; + case "sai": return "South American Indian (Other)"; + case "spa": return "Spanish"; + case "suk": return "Sukuma"; + case "sux": return "Sumerian"; + case "sun": return "Sundanese"; + case "sus": return "Susu"; + case "swa": return "Swahili"; + case "ssw": return "Swati"; + case "swe": return "Swedish"; + case "syr": return "Syriac"; + case "tgl": return "Tagalog"; + case "tah": return "Tahitian"; + case "tai": return "Tai (Other)"; + case "tgk": return "Tajik"; + case "tmh": return "Tamashek"; + case "tam": return "Tamil"; + case "tat": return "Tatar"; + case "tel": return "Telugu"; + case "ter": return "Tereno"; + case "tet": return "Tetum"; + case "tha": return "Thai"; + case "tib": return "Tibetan"; + case "bod": return "Tibetan"; + case "tig": return "Tigre"; + case "tir": return "Tigrinya"; + case "tem": return "Timne"; + case "tiv": return "Tiv"; + case "tli": return "Tlingit"; + case "tpi": return "Tok Pisin"; + case "tkl": return "Tokelau"; + case "tog": return "Tonga (Nyasa)"; + case "ton": return "Tonga (Tonga Islands)"; + case "tsi": return "Tsimshian"; + case "tso": return "Tsonga"; + case "tsn": return "Tswana"; + case "tum": return "Tumbuka"; + case "tur": return "Turkish"; + case "ota": return "Turkish, Ottoman (1500-1928)"; + case "tuk": return "Turkmen"; + case "tvl": return "Tuvalu"; + case "tyv": return "Tuvinian"; + case "twi": return "Twi"; + case "uga": return "Ugaritic"; + case "uig": return "Uighur"; + case "ukr": return "Ukrainian"; + case "umb": return "Umbundu"; + case "und": return "Undetermined"; + case "urd": return "Urdu"; + case "uzb": return "Uzbek"; + case "vai": return "Vai"; + case "ven": return "Venda"; + case "vie": return "Vietnamese"; + case "vol": return "Volapük"; + case "vot": return "Votic"; + case "wak": return "Wakashan languages"; + case "wal": return "Walamo"; + case "war": return "Waray"; + case "was": return "Washo"; + case "wel": return "Welsh"; + case "cym": return "Welsh"; + case "wol": return "Wolof"; + case "xho": return "Xhosa"; + case "sah": return "Yakut"; + case "yao": return "Yao"; + case "yap": return "Yapese"; + case "yid": return "Yiddish"; + case "yor": return "Yoruba"; + case "ypk": return "Yupik languages"; + case "znd": return "Zande"; + case "zap": return "Zapotec"; + case "zen": return "Zenaga"; + case "zha": return "Zhuang"; + case "zul": return "Zulu"; + case "zun": return "Zuni"; + + default: return code; + } + } + } +} diff --git a/MediaBrowser.Model.Portable/Properties/AssemblyInfo.cs b/BDInfo/Properties/AssemblyInfo.cs similarity index 60% rename from MediaBrowser.Model.Portable/Properties/AssemblyInfo.cs rename to BDInfo/Properties/AssemblyInfo.cs index a24dd014e4..aa44da1909 100644 --- a/MediaBrowser.Model.Portable/Properties/AssemblyInfo.cs +++ b/BDInfo/Properties/AssemblyInfo.cs @@ -1,15 +1,17 @@ using System.Resources; using System.Reflection; +using System.Runtime.CompilerServices; +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("MediaBrowser.Model.Portable")] +[assembly: AssemblyTitle("BDInfo")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MediaBrowser.Model.Portable")] -[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyProduct("BDInfo")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] [assembly: NeutralResourcesLanguage("en")] @@ -20,4 +22,8 @@ using System.Reflection; // Minor Version // Build Number // Revision -// \ No newline at end of file +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.1")] \ No newline at end of file diff --git a/BDInfo/ReadMe.txt b/BDInfo/ReadMe.txt new file mode 100644 index 0000000000..68326d560a --- /dev/null +++ b/BDInfo/ReadMe.txt @@ -0,0 +1,5 @@ +The source is taken from the BDRom folder of this project: + +http://www.cinemasquid.com/blu-ray/tools/bdinfo + +BDInfoSettings was taken from the FormSettings class, and changed so that the settings all return defaults. \ No newline at end of file diff --git a/BDInfo/TSCodecAC3.cs b/BDInfo/TSCodecAC3.cs new file mode 100644 index 0000000000..144141c302 --- /dev/null +++ b/BDInfo/TSCodecAC3.cs @@ -0,0 +1,309 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +#undef DEBUG +using System.IO; + +namespace BDInfo +{ + public abstract class TSCodecAC3 + { + private static byte[] eac3_blocks = new byte[] { 1, 2, 3, 6 }; + + public static void Scan( + TSAudioStream stream, + TSStreamBuffer buffer, + ref string tag) + { + if (stream.IsInitialized) return; + + byte[] sync = buffer.ReadBytes(2); + if (sync == null || + sync[0] != 0x0B || + sync[1] != 0x77) + { + return; + } + + int sr_code = 0; + int frame_size = 0; + int frame_size_code = 0; + int channel_mode = 0; + int lfe_on = 0; + int dial_norm = 0; + int num_blocks = 0; + + byte[] hdr = buffer.ReadBytes(4); + int bsid = (hdr[3] & 0xF8) >> 3; + buffer.Seek(-4, SeekOrigin.Current); + if (bsid <= 10) + { + byte[] crc = buffer.ReadBytes(2); + sr_code = buffer.ReadBits(2); + frame_size_code = buffer.ReadBits(6); + bsid = buffer.ReadBits(5); + int bsmod = buffer.ReadBits(3); + + channel_mode = buffer.ReadBits(3); + int cmixlev = 0; + if (((channel_mode & 0x1) > 0) && (channel_mode != 0x1)) + { + cmixlev = buffer.ReadBits(2); + } + int surmixlev = 0; + if ((channel_mode & 0x4) > 0) + { + surmixlev = buffer.ReadBits(2); + } + int dsurmod = 0; + if (channel_mode == 0x2) + { + dsurmod = buffer.ReadBits(2); + if (dsurmod == 0x2) + { + stream.AudioMode = TSAudioMode.Surround; + } + } + lfe_on = buffer.ReadBits(1); + dial_norm = buffer.ReadBits(5); + int compr = 0; + if (1 == buffer.ReadBits(1)) + { + compr = buffer.ReadBits(8); + } + int langcod = 0; + if (1 == buffer.ReadBits(1)) + { + langcod = buffer.ReadBits(8); + } + int mixlevel = 0; + int roomtyp = 0; + if (1 == buffer.ReadBits(1)) + { + mixlevel = buffer.ReadBits(5); + roomtyp = buffer.ReadBits(2); + } + if (channel_mode == 0) + { + int dialnorm2 = buffer.ReadBits(5); + int compr2 = 0; + if (1 == buffer.ReadBits(1)) + { + compr2 = buffer.ReadBits(8); + } + int langcod2 = 0; + if (1 == buffer.ReadBits(1)) + { + langcod2 = buffer.ReadBits(8); + } + int mixlevel2 = 0; + int roomtyp2 = 0; + if (1 == buffer.ReadBits(1)) + { + mixlevel2 = buffer.ReadBits(5); + roomtyp2 = buffer.ReadBits(2); + } + } + int copyrightb = buffer.ReadBits(1); + int origbs = buffer.ReadBits(1); + if (bsid == 6) + { + if (1 == buffer.ReadBits(1)) + { + int dmixmod = buffer.ReadBits(2); + int ltrtcmixlev = buffer.ReadBits(3); + int ltrtsurmixlev = buffer.ReadBits(3); + int lorocmixlev = buffer.ReadBits(3); + int lorosurmixlev = buffer.ReadBits(3); + } + if (1 == buffer.ReadBits(1)) + { + int dsurexmod = buffer.ReadBits(2); + int dheadphonmod = buffer.ReadBits(2); + if (dheadphonmod == 0x2) + { + // TODO + } + int adconvtyp = buffer.ReadBits(1); + int xbsi2 = buffer.ReadBits(8); + int encinfo = buffer.ReadBits(1); + if (dsurexmod == 2) + { + stream.AudioMode = TSAudioMode.Extended; + } + } + } + } + else + { + int frame_type = buffer.ReadBits(2); + int substreamid = buffer.ReadBits(3); + frame_size = (buffer.ReadBits(11) + 1) << 1; + + sr_code = buffer.ReadBits(2); + if (sr_code == 3) + { + sr_code = buffer.ReadBits(2); + } + else + { + num_blocks = buffer.ReadBits(2); + } + channel_mode = buffer.ReadBits(3); + lfe_on = buffer.ReadBits(1); + } + + switch (channel_mode) + { + case 0: // 1+1 + stream.ChannelCount = 2; + if (stream.AudioMode == TSAudioMode.Unknown) + { + stream.AudioMode = TSAudioMode.DualMono; + } + break; + case 1: // 1/0 + stream.ChannelCount = 1; + break; + case 2: // 2/0 + stream.ChannelCount = 2; + if (stream.AudioMode == TSAudioMode.Unknown) + { + stream.AudioMode = TSAudioMode.Stereo; + } + break; + case 3: // 3/0 + stream.ChannelCount = 3; + break; + case 4: // 2/1 + stream.ChannelCount = 3; + break; + case 5: // 3/1 + stream.ChannelCount = 4; + break; + case 6: // 2/2 + stream.ChannelCount = 4; + break; + case 7: // 3/2 + stream.ChannelCount = 5; + break; + default: + stream.ChannelCount = 0; + break; + } + + switch (sr_code) + { + case 0: + stream.SampleRate = 48000; + break; + case 1: + stream.SampleRate = 44100; + break; + case 2: + stream.SampleRate = 32000; + break; + default: + stream.SampleRate = 0; + break; + } + + if (bsid <= 10) + { + switch (frame_size_code >> 1) + { + case 18: + stream.BitRate = 640000; + break; + case 17: + stream.BitRate = 576000; + break; + case 16: + stream.BitRate = 512000; + break; + case 15: + stream.BitRate = 448000; + break; + case 14: + stream.BitRate = 384000; + break; + case 13: + stream.BitRate = 320000; + break; + case 12: + stream.BitRate = 256000; + break; + case 11: + stream.BitRate = 224000; + break; + case 10: + stream.BitRate = 192000; + break; + case 9: + stream.BitRate = 160000; + break; + case 8: + stream.BitRate = 128000; + break; + case 7: + stream.BitRate = 112000; + break; + case 6: + stream.BitRate = 96000; + break; + case 5: + stream.BitRate = 80000; + break; + case 4: + stream.BitRate = 64000; + break; + case 3: + stream.BitRate = 56000; + break; + case 2: + stream.BitRate = 48000; + break; + case 1: + stream.BitRate = 40000; + break; + case 0: + stream.BitRate = 32000; + break; + default: + stream.BitRate = 0; + break; + } + } + else + { + stream.BitRate = (long) + (4.0 * frame_size * stream.SampleRate / (num_blocks * 256)); + } + + stream.LFE = lfe_on; + if (stream.StreamType != TSStreamType.AC3_PLUS_AUDIO && + stream.StreamType != TSStreamType.AC3_PLUS_SECONDARY_AUDIO) + { + stream.DialNorm = dial_norm - 31; + } + stream.IsVBR = false; + stream.IsInitialized = true; + } + } +} diff --git a/BDInfo/TSCodecAVC.cs b/BDInfo/TSCodecAVC.cs new file mode 100644 index 0000000000..43c6d6f857 --- /dev/null +++ b/BDInfo/TSCodecAVC.cs @@ -0,0 +1,148 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecAVC + { + public static void Scan( + TSVideoStream stream, + TSStreamBuffer buffer, + ref string tag) + { + uint parse = 0; + byte accessUnitDelimiterParse = 0; + byte sequenceParameterSetParse = 0; + string profile = null; + string level = null; + byte constraintSet0Flag = 0; + byte constraintSet1Flag = 0; + byte constraintSet2Flag = 0; + byte constraintSet3Flag = 0; + + for (int i = 0; i < buffer.Length; i++) + { + parse = (parse << 8) + buffer.ReadByte(); + + if (parse == 0x00000109) + { + accessUnitDelimiterParse = 1; + } + else if (accessUnitDelimiterParse > 0) + { + --accessUnitDelimiterParse; + if (accessUnitDelimiterParse == 0) + { + switch ((parse & 0xFF) >> 5) + { + case 0: // I + case 3: // SI + case 5: // I, SI + tag = "I"; + break; + + case 1: // I, P + case 4: // SI, SP + case 6: // I, SI, P, SP + tag = "P"; + break; + + case 2: // I, P, B + case 7: // I, SI, P, SP, B + tag = "B"; + break; + } + if (stream.IsInitialized) return; + } + } + else if (parse == 0x00000127 || parse == 0x00000167) + { + sequenceParameterSetParse = 3; + } + else if (sequenceParameterSetParse > 0) + { + --sequenceParameterSetParse; + switch (sequenceParameterSetParse) + { + case 2: + switch (parse & 0xFF) + { + case 66: + profile = "Baseline Profile"; + break; + case 77: + profile = "Main Profile"; + break; + case 88: + profile = "Extended Profile"; + break; + case 100: + profile = "High Profile"; + break; + case 110: + profile = "High 10 Profile"; + break; + case 122: + profile = "High 4:2:2 Profile"; + break; + case 144: + profile = "High 4:4:4 Profile"; + break; + default: + profile = "Unknown Profile"; + break; + } + break; + + case 1: + constraintSet0Flag = (byte) + ((parse & 0x80) >> 7); + constraintSet1Flag = (byte) + ((parse & 0x40) >> 6); + constraintSet2Flag = (byte) + ((parse & 0x20) >> 5); + constraintSet3Flag = (byte) + ((parse & 0x10) >> 4); + break; + + case 0: + byte b = (byte)(parse & 0xFF); + if (b == 11 && constraintSet3Flag == 1) + { + level = "1b"; + } + else + { + level = string.Format( + "{0:D}.{1:D}", + b / 10, (b - ((b / 10) * 10))); + } + stream.EncodingProfile = string.Format( + "{0} {1}", profile, level); + stream.IsVBR = true; + stream.IsInitialized = true; + break; + } + } + } + return; + } + } +} diff --git a/BDInfo/TSCodecDTS.cs b/BDInfo/TSCodecDTS.cs new file mode 100644 index 0000000000..58eb60fc59 --- /dev/null +++ b/BDInfo/TSCodecDTS.cs @@ -0,0 +1,159 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecDTS + { + private static int[] dca_sample_rates = + { + 0, 8000, 16000, 32000, 0, 0, 11025, 22050, 44100, 0, 0, + 12000, 24000, 48000, 96000, 192000 + }; + + private static int[] dca_bit_rates = + { + 32000, 56000, 64000, 96000, 112000, 128000, + 192000, 224000, 256000, 320000, 384000, + 448000, 512000, 576000, 640000, 768000, + 896000, 1024000, 1152000, 1280000, 1344000, + 1408000, 1411200, 1472000, 1509000, 1920000, + 2048000, 3072000, 3840000, 1/*open*/, 2/*variable*/, 3/*lossless*/ + }; + + private static int[] dca_channels = + { + 1, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 6, 6, 7, 8, 8 + }; + + private static int[] dca_bits_per_sample = + { + 16, 16, 20, 20, 0, 24, 24 + }; + + public static void Scan( + TSAudioStream stream, + TSStreamBuffer buffer, + long bitrate, + ref string tag) + { + if (stream.IsInitialized) return; + + bool syncFound = false; + uint sync = 0; + for (int i = 0; i < buffer.Length; i++) + { + sync = (sync << 8) + buffer.ReadByte(); + if (sync == 0x7FFE8001) + { + syncFound = true; + break; + } + } + if (!syncFound) return; + + int frame_type = buffer.ReadBits(1); + int samples_deficit = buffer.ReadBits(5); + int crc_present = buffer.ReadBits(1); + int sample_blocks = buffer.ReadBits(7); + int frame_size = buffer.ReadBits(14); + if (frame_size < 95) + { + return; + } + int amode = buffer.ReadBits(6); + int sample_rate = buffer.ReadBits(4); + if (sample_rate < 0 || sample_rate >= dca_sample_rates.Length) + { + return; + } + int bit_rate = buffer.ReadBits(5); + if (bit_rate < 0 || bit_rate >= dca_bit_rates.Length) + { + return; + } + int downmix = buffer.ReadBits(1); + int dynrange = buffer.ReadBits(1); + int timestamp = buffer.ReadBits(1); + int aux_data = buffer.ReadBits(1); + int hdcd = buffer.ReadBits(1); + int ext_descr = buffer.ReadBits(3); + int ext_coding = buffer.ReadBits(1); + int aspf = buffer.ReadBits(1); + int lfe = buffer.ReadBits(2); + int predictor_history = buffer.ReadBits(1); + if (crc_present == 1) + { + int crc = buffer.ReadBits(16); + } + int multirate_inter = buffer.ReadBits(1); + int version = buffer.ReadBits(4); + int copy_history = buffer.ReadBits(2); + int source_pcm_res = buffer.ReadBits(3); + int front_sum = buffer.ReadBits(1); + int surround_sum = buffer.ReadBits(1); + int dialog_norm = buffer.ReadBits(4); + if (source_pcm_res < 0 || source_pcm_res >= dca_bits_per_sample.Length) + { + return; + } + int subframes = buffer.ReadBits(4); + int total_channels = buffer.ReadBits(3) + 1 + ext_coding; + + stream.SampleRate = dca_sample_rates[sample_rate]; + stream.ChannelCount = total_channels; + stream.LFE = (lfe > 0 ? 1 : 0); + stream.BitDepth = dca_bits_per_sample[source_pcm_res]; + stream.DialNorm = -dialog_norm; + if ((source_pcm_res & 0x1) == 0x1) + { + stream.AudioMode = TSAudioMode.Extended; + } + + stream.BitRate = (uint)dca_bit_rates[bit_rate]; + switch (stream.BitRate) + { + case 1: + if (bitrate > 0) + { + stream.BitRate = bitrate; + stream.IsVBR = false; + stream.IsInitialized = true; + } + else + { + stream.BitRate = 0; + } + break; + + case 2: + case 3: + stream.IsVBR = true; + stream.IsInitialized = true; + break; + + default: + stream.IsVBR = false; + stream.IsInitialized = true; + break; + } + } + } +} diff --git a/BDInfo/TSCodecDTSHD.cs b/BDInfo/TSCodecDTSHD.cs new file mode 100644 index 0000000000..169a8077f2 --- /dev/null +++ b/BDInfo/TSCodecDTSHD.cs @@ -0,0 +1,246 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecDTSHD + { + private static int[] SampleRates = new int[] + { 0x1F40, 0x3E80, 0x7D00, 0x0FA00, 0x1F400, 0x5622, 0x0AC44, 0x15888, 0x2B110, 0x56220, 0x2EE0, 0x5DC0, 0x0BB80, 0x17700, 0x2EE00, 0x5DC00 }; + + public static void Scan( + TSAudioStream stream, + TSStreamBuffer buffer, + long bitrate, + ref string tag) + { + if (stream.IsInitialized && + (stream.StreamType == TSStreamType.DTS_HD_SECONDARY_AUDIO || + (stream.CoreStream != null && + stream.CoreStream.IsInitialized))) return; + + bool syncFound = false; + uint sync = 0; + for (int i = 0; i < buffer.Length; i++) + { + sync = (sync << 8) + buffer.ReadByte(); + if (sync == 0x64582025) + { + syncFound = true; + break; + } + } + + if (!syncFound) + { + tag = "CORE"; + if (stream.CoreStream == null) + { + stream.CoreStream = new TSAudioStream(); + stream.CoreStream.StreamType = TSStreamType.DTS_AUDIO; + } + if (!stream.CoreStream.IsInitialized) + { + buffer.BeginRead(); + TSCodecDTS.Scan(stream.CoreStream, buffer, bitrate, ref tag); + } + return; + } + + tag = "HD"; + int temp1 = buffer.ReadBits(8); + int nuSubStreamIndex = buffer.ReadBits(2); + int nuExtSSHeaderSize = 0; + int nuExtSSFSize = 0; + int bBlownUpHeader = buffer.ReadBits(1); + if (1 == bBlownUpHeader) + { + nuExtSSHeaderSize = buffer.ReadBits(12) + 1; + nuExtSSFSize = buffer.ReadBits(20) + 1; + } + else + { + nuExtSSHeaderSize = buffer.ReadBits(8) + 1; + nuExtSSFSize = buffer.ReadBits(16) + 1; + } + int nuNumAudioPresent = 1; + int nuNumAssets = 1; + int bStaticFieldsPresent = buffer.ReadBits(1); + if (1 == bStaticFieldsPresent) + { + int nuRefClockCode = buffer.ReadBits(2); + int nuExSSFrameDurationCode = buffer.ReadBits(3) + 1; + long nuTimeStamp = 0; + if (1 == buffer.ReadBits(1)) + { + nuTimeStamp = (buffer.ReadBits(18) << 18) + buffer.ReadBits(18); + } + nuNumAudioPresent = buffer.ReadBits(3) + 1; + nuNumAssets = buffer.ReadBits(3) + 1; + int[] nuActiveExSSMask = new int[nuNumAudioPresent]; + for (int i = 0; i < nuNumAudioPresent; i++) + { + nuActiveExSSMask[i] = buffer.ReadBits(nuSubStreamIndex + 1); //? + } + for (int i = 0; i < nuNumAudioPresent; i++) + { + for (int j = 0; j < nuSubStreamIndex + 1; j++) + { + if (((j + 1) % 2) == 1) + { + int mask = buffer.ReadBits(8); + } + } + } + if (1 == buffer.ReadBits(1)) + { + int nuMixMetadataAdjLevel = buffer.ReadBits(2); + int nuBits4MixOutMask = buffer.ReadBits(2) * 4 + 4; + int nuNumMixOutConfigs = buffer.ReadBits(2) + 1; + int[] nuMixOutChMask = new int[nuNumMixOutConfigs]; + for (int i = 0; i < nuNumMixOutConfigs; i++) + { + nuMixOutChMask[i] = buffer.ReadBits(nuBits4MixOutMask); + } + } + } + int[] AssetSizes = new int[nuNumAssets]; + for (int i = 0; i < nuNumAssets; i++) + { + if (1 == bBlownUpHeader) + { + AssetSizes[i] = buffer.ReadBits(20) + 1; + } + else + { + AssetSizes[i] = buffer.ReadBits(16) + 1; + } + } + for (int i = 0; i < nuNumAssets; i++) + { + long bufferPosition = buffer.Position; + int nuAssetDescriptorFSIZE = buffer.ReadBits(9) + 1; + int DescriptorDataForAssetIndex = buffer.ReadBits(3); + if (1 == bStaticFieldsPresent) + { + int AssetTypeDescrPresent = buffer.ReadBits(1); + if (1 == AssetTypeDescrPresent) + { + int AssetTypeDescriptor = buffer.ReadBits(4); + } + int LanguageDescrPresent = buffer.ReadBits(1); + if (1 == LanguageDescrPresent) + { + int LanguageDescriptor = buffer.ReadBits(24); + } + int bInfoTextPresent = buffer.ReadBits(1); + if (1 == bInfoTextPresent) + { + int nuInfoTextByteSize = buffer.ReadBits(10) + 1; + int[] InfoText = new int[nuInfoTextByteSize]; + for (int j = 0; j < nuInfoTextByteSize; j++) + { + InfoText[j] = buffer.ReadBits(8); + } + } + int nuBitResolution = buffer.ReadBits(5) + 1; + int nuMaxSampleRate = buffer.ReadBits(4); + int nuTotalNumChs = buffer.ReadBits(8) + 1; + int bOne2OneMapChannels2Speakers = buffer.ReadBits(1); + int nuSpkrActivityMask = 0; + if (1 == bOne2OneMapChannels2Speakers) + { + int bEmbeddedStereoFlag = 0; + if (nuTotalNumChs > 2) + { + bEmbeddedStereoFlag = buffer.ReadBits(1); + } + int bEmbeddedSixChFlag = 0; + if (nuTotalNumChs > 6) + { + bEmbeddedSixChFlag = buffer.ReadBits(1); + } + int bSpkrMaskEnabled = buffer.ReadBits(1); + int nuNumBits4SAMask = 0; + if (1 == bSpkrMaskEnabled) + { + nuNumBits4SAMask = buffer.ReadBits(2); + nuNumBits4SAMask = nuNumBits4SAMask * 4 + 4; + nuSpkrActivityMask = buffer.ReadBits(nuNumBits4SAMask); + } + // TODO... + } + stream.SampleRate = SampleRates[nuMaxSampleRate]; + stream.BitDepth = nuBitResolution; + + stream.LFE = 0; + if ((nuSpkrActivityMask & 0x8) == 0x8) + { + ++stream.LFE; + } + if ((nuSpkrActivityMask & 0x1000) == 0x1000) + { + ++stream.LFE; + } + stream.ChannelCount = nuTotalNumChs - stream.LFE; + } + if (nuNumAssets > 1) + { + // TODO... + break; + } + } + + // TODO + if (stream.CoreStream != null) + { + TSAudioStream coreStream = (TSAudioStream)stream.CoreStream; + if (coreStream.AudioMode == TSAudioMode.Extended && + stream.ChannelCount == 5) + { + stream.AudioMode = TSAudioMode.Extended; + } + /* + if (coreStream.DialNorm != 0) + { + stream.DialNorm = coreStream.DialNorm; + } + */ + } + + if (stream.StreamType == TSStreamType.DTS_HD_MASTER_AUDIO) + { + stream.IsVBR = true; + stream.IsInitialized = true; + } + else if (bitrate > 0) + { + stream.IsVBR = false; + stream.BitRate = bitrate; + if (stream.CoreStream != null) + { + stream.BitRate += stream.CoreStream.BitRate; + stream.IsInitialized = true; + } + stream.IsInitialized = (stream.BitRate > 0 ? true : false); + } + } + } +} diff --git a/BDInfo/TSCodecLPCM.cs b/BDInfo/TSCodecLPCM.cs new file mode 100644 index 0000000000..d12674f0e8 --- /dev/null +++ b/BDInfo/TSCodecLPCM.cs @@ -0,0 +1,123 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecLPCM + { + public static void Scan( + TSAudioStream stream, + TSStreamBuffer buffer, + ref string tag) + { + if (stream.IsInitialized) return; + + byte[] header = buffer.ReadBytes(4); + int flags = (header[2] << 8) + header[3]; + + switch ((flags & 0xF000) >> 12) + { + case 1: // 1/0/0 + stream.ChannelCount = 1; + stream.LFE = 0; + break; + case 3: // 2/0/0 + stream.ChannelCount = 2; + stream.LFE = 0; + break; + case 4: // 3/0/0 + stream.ChannelCount = 3; + stream.LFE = 0; + break; + case 5: // 2/1/0 + stream.ChannelCount = 3; + stream.LFE = 0; + break; + case 6: // 3/1/0 + stream.ChannelCount = 4; + stream.LFE = 0; + break; + case 7: // 2/2/0 + stream.ChannelCount = 4; + stream.LFE = 0; + break; + case 8: // 3/2/0 + stream.ChannelCount = 5; + stream.LFE = 0; + break; + case 9: // 3/2/1 + stream.ChannelCount = 5; + stream.LFE = 1; + break; + case 10: // 3/4/0 + stream.ChannelCount = 7; + stream.LFE = 0; + break; + case 11: // 3/4/1 + stream.ChannelCount = 7; + stream.LFE = 1; + break; + default: + stream.ChannelCount = 0; + stream.LFE = 0; + break; + } + + switch ((flags & 0xC0) >> 6) + { + case 1: + stream.BitDepth = 16; + break; + case 2: + stream.BitDepth = 20; + break; + case 3: + stream.BitDepth = 24; + break; + default: + stream.BitDepth = 0; + break; + } + + switch ((flags & 0xF00) >> 8) + { + case 1: + stream.SampleRate = 48000; + break; + case 4: + stream.SampleRate = 96000; + break; + case 5: + stream.SampleRate = 192000; + break; + default: + stream.SampleRate = 0; + break; + } + + stream.BitRate = (uint) + (stream.SampleRate * stream.BitDepth * + (stream.ChannelCount + stream.LFE)); + + stream.IsVBR = false; + stream.IsInitialized = true; + } + } +} diff --git a/BDInfo/TSCodecMPEG2.cs b/BDInfo/TSCodecMPEG2.cs new file mode 100644 index 0000000000..3413a06e92 --- /dev/null +++ b/BDInfo/TSCodecMPEG2.cs @@ -0,0 +1,208 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +#undef DEBUG + + +namespace BDInfo +{ + public abstract class TSCodecMPEG2 + { + public static void Scan( + TSVideoStream stream, + TSStreamBuffer buffer, + ref string tag) + { + int parse = 0; + int pictureParse = 0; + int sequenceHeaderParse = 0; + int extensionParse = 0; + int sequenceExtensionParse = 0; + + for (int i = 0; i < buffer.Length; i++) + { + parse = (parse << 8) + buffer.ReadByte(); + + if (parse == 0x00000100) + { + pictureParse = 2; + } + else if (parse == 0x000001B3) + { + sequenceHeaderParse = 7; + } + else if (sequenceHeaderParse > 0) + { + --sequenceHeaderParse; + switch (sequenceHeaderParse) + { +#if DEBUG + case 6: + break; + + case 5: + break; + + case 4: + stream.Width = + (int)((parse & 0xFFF000) >> 12); + stream.Height = + (int)(parse & 0xFFF); + break; + + case 3: + stream.AspectRatio = + (TSAspectRatio)((parse & 0xF0) >> 4); + + switch ((parse & 0xF0) >> 4) + { + case 0: // Forbidden + break; + case 1: // Square + break; + case 2: // 4:3 + break; + case 3: // 16:9 + break; + case 4: // 2.21:1 + break; + default: // Reserved + break; + } + + switch (parse & 0xF) + { + case 0: // Forbidden + break; + case 1: // 23.976 + stream.FrameRateEnumerator = 24000; + stream.FrameRateDenominator = 1001; + break; + case 2: // 24 + stream.FrameRateEnumerator = 24000; + stream.FrameRateDenominator = 1000; + break; + case 3: // 25 + stream.FrameRateEnumerator = 25000; + stream.FrameRateDenominator = 1000; + break; + case 4: // 29.97 + stream.FrameRateEnumerator = 30000; + stream.FrameRateDenominator = 1001; + break; + case 5: // 30 + stream.FrameRateEnumerator = 30000; + stream.FrameRateDenominator = 1000; + break; + case 6: // 50 + stream.FrameRateEnumerator = 50000; + stream.FrameRateDenominator = 1000; + break; + case 7: // 59.94 + stream.FrameRateEnumerator = 60000; + stream.FrameRateDenominator = 1001; + break; + case 8: // 60 + stream.FrameRateEnumerator = 60000; + stream.FrameRateDenominator = 1000; + break; + default: // Reserved + stream.FrameRateEnumerator = 0; + stream.FrameRateDenominator = 0; + break; + } + break; + + case 2: + break; + + case 1: + break; +#endif + + case 0: +#if DEBUG + stream.BitRate = + (((parse & 0xFFFFC0) >> 6) * 200); +#endif + stream.IsVBR = true; + stream.IsInitialized = true; + break; + } + } + else if (pictureParse > 0) + { + --pictureParse; + if (pictureParse == 0) + { + switch ((parse & 0x38) >> 3) + { + case 1: + tag = "I"; + break; + case 2: + tag = "P"; + break; + case 3: + tag = "B"; + break; + default: + break; + } + if (stream.IsInitialized) return; + } + } + else if (parse == 0x000001B5) + { + extensionParse = 1; + } + else if (extensionParse > 0) + { + --extensionParse; + if (extensionParse == 0) + { + if ((parse & 0xF0) == 0x10) + { + sequenceExtensionParse = 1; + } + } + } + else if (sequenceExtensionParse > 0) + { + --sequenceExtensionParse; +#if DEBUG + if (sequenceExtensionParse == 0) + { + uint sequenceExtension = + ((parse & 0x8) >> 3); + if (sequenceExtension == 0) + { + stream.IsInterlaced = true; + } + else + { + stream.IsInterlaced = false; + } + } +#endif + } + } + } + } +} diff --git a/BDInfo/TSCodecMVC.cs b/BDInfo/TSCodecMVC.cs new file mode 100644 index 0000000000..80fed3886c --- /dev/null +++ b/BDInfo/TSCodecMVC.cs @@ -0,0 +1,36 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + // TODO: Do something more interesting here... + + public abstract class TSCodecMVC + { + public static void Scan( + TSVideoStream stream, + TSStreamBuffer buffer, + ref string tag) + { + stream.IsVBR = true; + stream.IsInitialized = true; + } + } +} diff --git a/BDInfo/TSCodecTrueHD.cs b/BDInfo/TSCodecTrueHD.cs new file mode 100644 index 0000000000..baf4fa3dfe --- /dev/null +++ b/BDInfo/TSCodecTrueHD.cs @@ -0,0 +1,186 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecTrueHD + { + public static void Scan( + TSAudioStream stream, + TSStreamBuffer buffer, + ref string tag) + { + if (stream.IsInitialized && + stream.CoreStream != null && + stream.CoreStream.IsInitialized) return; + + bool syncFound = false; + uint sync = 0; + for (int i = 0; i < buffer.Length; i++) + { + sync = (sync << 8) + buffer.ReadByte(); + if (sync == 0xF8726FBA) + { + syncFound = true; + break; + } + } + + if (!syncFound) + { + tag = "CORE"; + if (stream.CoreStream == null) + { + stream.CoreStream = new TSAudioStream(); + stream.CoreStream.StreamType = TSStreamType.AC3_AUDIO; + } + if (!stream.CoreStream.IsInitialized) + { + buffer.BeginRead(); + TSCodecAC3.Scan(stream.CoreStream, buffer, ref tag); + } + return; + } + + tag = "HD"; + int ratebits = buffer.ReadBits(4); + if (ratebits != 0xF) + { + stream.SampleRate = + (((ratebits & 8) > 0 ? 44100 : 48000) << (ratebits & 7)); + } + int temp1 = buffer.ReadBits(8); + int channels_thd_stream1 = buffer.ReadBits(5); + int temp2 = buffer.ReadBits(2); + + stream.ChannelCount = 0; + stream.LFE = 0; + int c_LFE2 = buffer.ReadBits(1); + if (c_LFE2 == 1) + { + stream.LFE += 1; + } + int c_Cvh = buffer.ReadBits(1); + if (c_Cvh == 1) + { + stream.ChannelCount += 1; + } + int c_LRw = buffer.ReadBits(1); + if (c_LRw == 1) + { + stream.ChannelCount += 2; + } + int c_LRsd = buffer.ReadBits(1); + if (c_LRsd == 1) + { + stream.ChannelCount += 2; + } + int c_Ts = buffer.ReadBits(1); + if (c_Ts == 1) + { + stream.ChannelCount += 1; + } + int c_Cs = buffer.ReadBits(1); + if (c_Cs == 1) + { + stream.ChannelCount += 1; + } + int c_LRrs = buffer.ReadBits(1); + if (c_LRrs == 1) + { + stream.ChannelCount += 2; + } + int c_LRc = buffer.ReadBits(1); + if (c_LRc == 1) + { + stream.ChannelCount += 2; + } + int c_LRvh = buffer.ReadBits(1); + if (c_LRvh == 1) + { + stream.ChannelCount += 2; + } + int c_LRs = buffer.ReadBits(1); + if (c_LRs == 1) + { + stream.ChannelCount += 2; + } + int c_LFE = buffer.ReadBits(1); + if (c_LFE == 1) + { + stream.LFE += 1; + } + int c_C = buffer.ReadBits(1); + if (c_C == 1) + { + stream.ChannelCount += 1; + } + int c_LR = buffer.ReadBits(1); + if (c_LR == 1) + { + stream.ChannelCount += 2; + } + + int access_unit_size = 40 << (ratebits & 7); + int access_unit_size_pow2 = 64 << (ratebits & 7); + + int a1 = buffer.ReadBits(16); + int a2 = buffer.ReadBits(16); + int a3 = buffer.ReadBits(16); + + int is_vbr = buffer.ReadBits(1); + int peak_bitrate = buffer.ReadBits(15); + peak_bitrate = (peak_bitrate * stream.SampleRate) >> 4; + + double peak_bitdepth = + (double)peak_bitrate / + (stream.ChannelCount + stream.LFE) / + stream.SampleRate; + if (peak_bitdepth > 14) + { + stream.BitDepth = 24; + } + else + { + stream.BitDepth = 16; + } + +#if DEBUG + System.Diagnostics.Debug.WriteLine(string.Format( + "{0}\t{1}\t{2:F2}", + stream.PID, peak_bitrate, peak_bitdepth)); +#endif + /* + // TODO: Get THD dialnorm from metadata + if (stream.CoreStream != null) + { + TSAudioStream coreStream = (TSAudioStream)stream.CoreStream; + if (coreStream.DialNorm != 0) + { + stream.DialNorm = coreStream.DialNorm; + } + } + */ + + stream.IsVBR = true; + stream.IsInitialized = true; + } + } +} diff --git a/BDInfo/TSCodecVC1.cs b/BDInfo/TSCodecVC1.cs new file mode 100644 index 0000000000..164ef8c47e --- /dev/null +++ b/BDInfo/TSCodecVC1.cs @@ -0,0 +1,131 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + + +namespace BDInfo +{ + public abstract class TSCodecVC1 + { + public static void Scan( + TSVideoStream stream, + TSStreamBuffer buffer, + ref string tag) + { + int parse = 0; + byte frameHeaderParse = 0; + byte sequenceHeaderParse = 0; + bool isInterlaced = false; + + for (int i = 0; i < buffer.Length; i++) + { + parse = (parse << 8) + buffer.ReadByte(); + + if (parse == 0x0000010D) + { + frameHeaderParse = 4; + } + else if (frameHeaderParse > 0) + { + --frameHeaderParse; + if (frameHeaderParse == 0) + { + uint pictureType = 0; + if (isInterlaced) + { + if ((parse & 0x80000000) == 0) + { + pictureType = + (uint)((parse & 0x78000000) >> 13); + } + else + { + pictureType = + (uint)((parse & 0x3c000000) >> 12); + } + } + else + { + pictureType = + (uint)((parse & 0xf0000000) >> 14); + } + + if ((pictureType & 0x20000) == 0) + { + tag = "P"; + } + else if ((pictureType & 0x10000) == 0) + { + tag = "B"; + } + else if ((pictureType & 0x8000) == 0) + { + tag = "I"; + } + else if ((pictureType & 0x4000) == 0) + { + tag = "BI"; + } + else + { + tag = null; + } + if (stream.IsInitialized) return; + } + } + else if (parse == 0x0000010F) + { + sequenceHeaderParse = 6; + } + else if (sequenceHeaderParse > 0) + { + --sequenceHeaderParse; + switch (sequenceHeaderParse) + { + case 5: + int profileLevel = ((parse & 0x38) >> 3); + if (((parse & 0xC0) >> 6) == 3) + { + stream.EncodingProfile = string.Format( + "Advanced Profile {0}", profileLevel); + } + else + { + stream.EncodingProfile = string.Format( + "Main Profile {0}", profileLevel); + } + break; + + case 0: + if (((parse & 0x40) >> 6) > 0) + { + isInterlaced = true; + } + else + { + isInterlaced = false; + } + break; + } + stream.IsVBR = true; + stream.IsInitialized = true; + } + } + } + } +} diff --git a/BDInfo/TSInterleavedFile.cs b/BDInfo/TSInterleavedFile.cs new file mode 100644 index 0000000000..44a16bbaa6 --- /dev/null +++ b/BDInfo/TSInterleavedFile.cs @@ -0,0 +1,38 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +using System.IO; +using MediaBrowser.Model.IO; + +// TODO: Do more interesting things here... + +namespace BDInfo +{ + public class TSInterleavedFile + { + public FileSystemMetadata FileInfo = null; + public string Name = null; + + public TSInterleavedFile(FileSystemMetadata fileInfo) + { + FileInfo = fileInfo; + Name = fileInfo.Name.ToUpper(); + } + } +} diff --git a/BDInfo/TSPlaylistFile.cs b/BDInfo/TSPlaylistFile.cs new file mode 100644 index 0000000000..46d66f513e --- /dev/null +++ b/BDInfo/TSPlaylistFile.cs @@ -0,0 +1,1292 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +#undef DEBUG +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; + +namespace BDInfo +{ + public class TSPlaylistFile + { + private readonly IFileSystem _fileSystem; + private readonly ITextEncoding _textEncoding; + private FileSystemMetadata FileInfo = null; + public string FileType = null; + public bool IsInitialized = false; + public string Name = null; + public BDROM BDROM = null; + public bool HasHiddenTracks = false; + public bool HasLoops = false; + public bool IsCustom = false; + + public List Chapters = new List(); + + public Dictionary Streams = + new Dictionary(); + public Dictionary PlaylistStreams = + new Dictionary(); + public List StreamClips = + new List(); + public List> AngleStreams = + new List>(); + public List> AngleClips = + new List>(); + public int AngleCount = 0; + + public List SortedStreams = + new List(); + public List VideoStreams = + new List(); + public List AudioStreams = + new List(); + public List TextStreams = + new List(); + public List GraphicsStreams = + new List(); + + public TSPlaylistFile( + BDROM bdrom, + FileSystemMetadata fileInfo, IFileSystem fileSystem, ITextEncoding textEncoding) + { + BDROM = bdrom; + FileInfo = fileInfo; + _fileSystem = fileSystem; + _textEncoding = textEncoding; + Name = fileInfo.Name.ToUpper(); + } + + public TSPlaylistFile( + BDROM bdrom, + string name, + List clips, IFileSystem fileSystem, ITextEncoding textEncoding) + { + BDROM = bdrom; + Name = name; + _fileSystem = fileSystem; + _textEncoding = textEncoding; + IsCustom = true; + foreach (TSStreamClip clip in clips) + { + TSStreamClip newClip = new TSStreamClip( + clip.StreamFile, clip.StreamClipFile); + + newClip.Name = clip.Name; + newClip.TimeIn = clip.TimeIn; + newClip.TimeOut = clip.TimeOut; + newClip.Length = newClip.TimeOut - newClip.TimeIn; + newClip.RelativeTimeIn = TotalLength; + newClip.RelativeTimeOut = newClip.RelativeTimeIn + newClip.Length; + newClip.AngleIndex = clip.AngleIndex; + newClip.Chapters.Add(clip.TimeIn); + StreamClips.Add(newClip); + + if (newClip.AngleIndex > AngleCount) + { + AngleCount = newClip.AngleIndex; + } + if (newClip.AngleIndex == 0) + { + Chapters.Add(newClip.RelativeTimeIn); + } + } + LoadStreamClips(); + IsInitialized = true; + } + + public override string ToString() + { + return Name; + } + + public ulong InterleavedFileSize + { + get + { + ulong size = 0; + foreach (TSStreamClip clip in StreamClips) + { + size += clip.InterleavedFileSize; + } + return size; + } + } + public ulong FileSize + { + get + { + ulong size = 0; + foreach (TSStreamClip clip in StreamClips) + { + size += clip.FileSize; + } + return size; + } + } + public double TotalLength + { + get + { + double length = 0; + foreach (TSStreamClip clip in StreamClips) + { + if (clip.AngleIndex == 0) + { + length += clip.Length; + } + } + return length; + } + } + + public double TotalAngleLength + { + get + { + double length = 0; + foreach (TSStreamClip clip in StreamClips) + { + length += clip.Length; + } + return length; + } + } + + public ulong TotalSize + { + get + { + ulong size = 0; + foreach (TSStreamClip clip in StreamClips) + { + if (clip.AngleIndex == 0) + { + size += clip.PacketSize; + } + } + return size; + } + } + + public ulong TotalAngleSize + { + get + { + ulong size = 0; + foreach (TSStreamClip clip in StreamClips) + { + size += clip.PacketSize; + } + return size; + } + } + + public ulong TotalBitRate + { + get + { + if (TotalLength > 0) + { + return (ulong)Math.Round(((TotalSize * 8.0) / TotalLength)); + } + return 0; + } + } + + public ulong TotalAngleBitRate + { + get + { + if (TotalAngleLength > 0) + { + return (ulong)Math.Round(((TotalAngleSize * 8.0) / TotalAngleLength)); + } + return 0; + } + } + + public void Scan( + Dictionary streamFiles, + Dictionary streamClipFiles) + { + Stream fileStream = null; + BinaryReader fileReader = null; + + try + { + Streams.Clear(); + StreamClips.Clear(); + + fileStream = _fileSystem.OpenRead(FileInfo.FullName); + fileReader = new BinaryReader(fileStream); + + byte[] data = new byte[fileStream.Length]; + int dataLength = fileReader.Read(data, 0, data.Length); + + int pos = 0; + + FileType = ReadString(data, 8, ref pos); + if (FileType != "MPLS0100" && FileType != "MPLS0200") + { + throw new Exception(string.Format( + "Playlist {0} has an unknown file type {1}.", + FileInfo.Name, FileType)); + } + + int playlistOffset = ReadInt32(data, ref pos); + int chaptersOffset = ReadInt32(data, ref pos); + int extensionsOffset = ReadInt32(data, ref pos); + + pos = playlistOffset; + + int playlistLength = ReadInt32(data, ref pos); + int playlistReserved = ReadInt16(data, ref pos); + int itemCount = ReadInt16(data, ref pos); + int subitemCount = ReadInt16(data, ref pos); + + List chapterClips = new List(); + for (int itemIndex = 0; itemIndex < itemCount; itemIndex++) + { + int itemStart = pos; + int itemLength = ReadInt16(data, ref pos); + string itemName = ReadString(data, 5, ref pos); + string itemType = ReadString(data, 4, ref pos); + + TSStreamFile streamFile = null; + string streamFileName = string.Format( + "{0}.M2TS", itemName); + if (streamFiles.ContainsKey(streamFileName)) + { + streamFile = streamFiles[streamFileName]; + } + if (streamFile == null) + { + // Error condition + } + + TSStreamClipFile streamClipFile = null; + string streamClipFileName = string.Format( + "{0}.CLPI", itemName); + if (streamClipFiles.ContainsKey(streamClipFileName)) + { + streamClipFile = streamClipFiles[streamClipFileName]; + } + if (streamClipFile == null) + { + throw new Exception(string.Format( + "Playlist {0} referenced missing file {1}.", + FileInfo.Name, streamFileName)); + } + + pos += 1; + int multiangle = (data[pos] >> 4) & 0x01; + int condition = data[pos] & 0x0F; + pos += 2; + + int inTime = ReadInt32(data, ref pos); + if (inTime < 0) inTime &= 0x7FFFFFFF; + double timeIn = (double)inTime / 45000; + + int outTime = ReadInt32(data, ref pos); + if (outTime < 0) outTime &= 0x7FFFFFFF; + double timeOut = (double)outTime / 45000; + + TSStreamClip streamClip = new TSStreamClip( + streamFile, streamClipFile); + + streamClip.Name = streamFileName; //TODO + streamClip.TimeIn = timeIn; + streamClip.TimeOut = timeOut; + streamClip.Length = streamClip.TimeOut - streamClip.TimeIn; + streamClip.RelativeTimeIn = TotalLength; + streamClip.RelativeTimeOut = streamClip.RelativeTimeIn + streamClip.Length; + StreamClips.Add(streamClip); + chapterClips.Add(streamClip); + + pos += 12; + if (multiangle > 0) + { + int angles = data[pos]; + pos += 2; + for (int angle = 0; angle < angles - 1; angle++) + { + string angleName = ReadString(data, 5, ref pos); + string angleType = ReadString(data, 4, ref pos); + pos += 1; + + TSStreamFile angleFile = null; + string angleFileName = string.Format( + "{0}.M2TS", angleName); + if (streamFiles.ContainsKey(angleFileName)) + { + angleFile = streamFiles[angleFileName]; + } + if (angleFile == null) + { + throw new Exception(string.Format( + "Playlist {0} referenced missing angle file {1}.", + FileInfo.Name, angleFileName)); + } + + TSStreamClipFile angleClipFile = null; + string angleClipFileName = string.Format( + "{0}.CLPI", angleName); + if (streamClipFiles.ContainsKey(angleClipFileName)) + { + angleClipFile = streamClipFiles[angleClipFileName]; + } + if (angleClipFile == null) + { + throw new Exception(string.Format( + "Playlist {0} referenced missing angle file {1}.", + FileInfo.Name, angleClipFileName)); + } + + TSStreamClip angleClip = + new TSStreamClip(angleFile, angleClipFile); + angleClip.AngleIndex = angle + 1; + angleClip.TimeIn = streamClip.TimeIn; + angleClip.TimeOut = streamClip.TimeOut; + angleClip.RelativeTimeIn = streamClip.RelativeTimeIn; + angleClip.RelativeTimeOut = streamClip.RelativeTimeOut; + angleClip.Length = streamClip.Length; + StreamClips.Add(angleClip); + } + if (angles - 1 > AngleCount) AngleCount = angles - 1; + } + + int streamInfoLength = ReadInt16(data, ref pos); + pos += 2; + int streamCountVideo = data[pos++]; + int streamCountAudio = data[pos++]; + int streamCountPG = data[pos++]; + int streamCountIG = data[pos++]; + int streamCountSecondaryAudio = data[pos++]; + int streamCountSecondaryVideo = data[pos++]; + int streamCountPIP = data[pos++]; + pos += 5; + +#if DEBUG + Debug.WriteLine(string.Format( + "{0} : {1} -> V:{2} A:{3} PG:{4} IG:{5} 2A:{6} 2V:{7} PIP:{8}", + Name, streamFileName, streamCountVideo, streamCountAudio, streamCountPG, streamCountIG, + streamCountSecondaryAudio, streamCountSecondaryVideo, streamCountPIP)); +#endif + + for (int i = 0; i < streamCountVideo; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + } + for (int i = 0; i < streamCountAudio; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + } + for (int i = 0; i < streamCountPG; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + } + for (int i = 0; i < streamCountIG; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + } + for (int i = 0; i < streamCountSecondaryAudio; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + pos += 2; + } + for (int i = 0; i < streamCountSecondaryVideo; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + pos += 6; + } + /* + * TODO + * + for (int i = 0; i < streamCountPIP; i++) + { + TSStream stream = CreatePlaylistStream(data, ref pos); + if (stream != null) PlaylistStreams[stream.PID] = stream; + } + */ + + pos += itemLength - (pos - itemStart) + 2; + } + + pos = chaptersOffset + 4; + + int chapterCount = ReadInt16(data, ref pos); + + for (int chapterIndex = 0; + chapterIndex < chapterCount; + chapterIndex++) + { + int chapterType = data[pos+1]; + + if (chapterType == 1) + { + int streamFileIndex = + ((int)data[pos + 2] << 8) + data[pos + 3]; + + long chapterTime = + ((long)data[pos + 4] << 24) + + ((long)data[pos + 5] << 16) + + ((long)data[pos + 6] << 8) + + ((long)data[pos + 7]); + + TSStreamClip streamClip = chapterClips[streamFileIndex]; + + double chapterSeconds = (double)chapterTime / 45000; + + double relativeSeconds = + chapterSeconds - + streamClip.TimeIn + + streamClip.RelativeTimeIn; + + // TODO: Ignore short last chapter? + if (TotalLength - relativeSeconds > 1.0) + { + streamClip.Chapters.Add(chapterSeconds); + this.Chapters.Add(relativeSeconds); + } + } + else + { + // TODO: Handle other chapter types? + } + pos += 14; + } + } + finally + { + if (fileReader != null) + { + fileReader.Dispose(); + } + if (fileStream != null) + { + fileStream.Dispose(); + } + } + } + + public void Initialize() + { + LoadStreamClips(); + + Dictionary> clipTimes = new Dictionary>(); + foreach (TSStreamClip clip in StreamClips) + { + if (clip.AngleIndex == 0) + { + if (clipTimes.ContainsKey(clip.Name)) + { + if (clipTimes[clip.Name].Contains(clip.TimeIn)) + { + HasLoops = true; + break; + } + else + { + clipTimes[clip.Name].Add(clip.TimeIn); + } + } + else + { + clipTimes[clip.Name] = new List { clip.TimeIn }; + } + } + } + ClearBitrates(); + IsInitialized = true; + } + + protected TSStream CreatePlaylistStream(byte[] data, ref int pos) + { + TSStream stream = null; + + int start = pos; + + int headerLength = data[pos++]; + int headerPos = pos; + int headerType = data[pos++]; + + int pid = 0; + int subpathid = 0; + int subclipid = 0; + + switch (headerType) + { + case 1: + pid = ReadInt16(data, ref pos); + break; + case 2: + subpathid = data[pos++]; + subclipid = data[pos++]; + pid = ReadInt16(data, ref pos); + break; + case 3: + subpathid = data[pos++]; + pid = ReadInt16(data, ref pos); + break; + case 4: + subpathid = data[pos++]; + subclipid = data[pos++]; + pid = ReadInt16(data, ref pos); + break; + default: + break; + } + + pos = headerPos + headerLength; + + int streamLength = data[pos++]; + int streamPos = pos; + + TSStreamType streamType = (TSStreamType)data[pos++]; + switch (streamType) + { + case TSStreamType.MVC_VIDEO: + // TODO + break; + + case TSStreamType.AVC_VIDEO: + case TSStreamType.MPEG1_VIDEO: + case TSStreamType.MPEG2_VIDEO: + case TSStreamType.VC1_VIDEO: + + TSVideoFormat videoFormat = (TSVideoFormat) + (data[pos] >> 4); + TSFrameRate frameRate = (TSFrameRate) + (data[pos] & 0xF); + TSAspectRatio aspectRatio = (TSAspectRatio) + (data[pos + 1] >> 4); + + stream = new TSVideoStream(); + ((TSVideoStream)stream).VideoFormat = videoFormat; + ((TSVideoStream)stream).AspectRatio = aspectRatio; + ((TSVideoStream)stream).FrameRate = frameRate; + +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2} {3} {4}", + pid, + streamType, + videoFormat, + frameRate, + aspectRatio)); +#endif + + break; + + case TSStreamType.AC3_AUDIO: + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + case TSStreamType.AC3_TRUE_HD_AUDIO: + case TSStreamType.DTS_AUDIO: + case TSStreamType.DTS_HD_AUDIO: + case TSStreamType.DTS_HD_MASTER_AUDIO: + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + case TSStreamType.LPCM_AUDIO: + case TSStreamType.MPEG1_AUDIO: + case TSStreamType.MPEG2_AUDIO: + + int audioFormat = ReadByte(data, ref pos); + + TSChannelLayout channelLayout = (TSChannelLayout) + (audioFormat >> 4); + TSSampleRate sampleRate = (TSSampleRate) + (audioFormat & 0xF); + + string audioLanguage = ReadString(data, 3, ref pos); + + stream = new TSAudioStream(); + ((TSAudioStream)stream).ChannelLayout = channelLayout; + ((TSAudioStream)stream).SampleRate = TSAudioStream.ConvertSampleRate(sampleRate); + ((TSAudioStream)stream).LanguageCode = audioLanguage; + +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2} {3} {4}", + pid, + streamType, + audioLanguage, + channelLayout, + sampleRate)); +#endif + + break; + + case TSStreamType.INTERACTIVE_GRAPHICS: + case TSStreamType.PRESENTATION_GRAPHICS: + + string graphicsLanguage = ReadString(data, 3, ref pos); + + stream = new TSGraphicsStream(); + ((TSGraphicsStream)stream).LanguageCode = graphicsLanguage; + + if (data[pos] != 0) + { + } + +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2}", + pid, + streamType, + graphicsLanguage)); +#endif + + break; + + case TSStreamType.SUBTITLE: + + int code = ReadByte(data, ref pos); // TODO + string textLanguage = ReadString(data, 3, ref pos); + + stream = new TSTextStream(); + ((TSTextStream)stream).LanguageCode = textLanguage; + +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2}", + pid, + streamType, + textLanguage)); +#endif + + break; + + default: + break; + } + + pos = streamPos + streamLength; + + if (stream != null) + { + stream.PID = (ushort)pid; + stream.StreamType = streamType; + } + + return stream; + } + + private void LoadStreamClips() + { + AngleClips.Clear(); + if (AngleCount > 0) + { + for (int angleIndex = 0; angleIndex < AngleCount; angleIndex++) + { + AngleClips.Add(new Dictionary()); + } + } + + TSStreamClip referenceClip = null; + if (StreamClips.Count > 0) + { + referenceClip = StreamClips[0]; + } + foreach (TSStreamClip clip in StreamClips) + { + if (clip.StreamClipFile.Streams.Count > referenceClip.StreamClipFile.Streams.Count) + { + referenceClip = clip; + } + else if (clip.Length > referenceClip.Length) + { + referenceClip = clip; + } + if (AngleCount > 0) + { + if (clip.AngleIndex == 0) + { + for (int angleIndex = 0; angleIndex < AngleCount; angleIndex++) + { + AngleClips[angleIndex][clip.RelativeTimeIn] = clip; + } + } + else + { + AngleClips[clip.AngleIndex - 1][clip.RelativeTimeIn] = clip; + } + } + } + + foreach (TSStream clipStream + in referenceClip.StreamClipFile.Streams.Values) + { + if (!Streams.ContainsKey(clipStream.PID)) + { + TSStream stream = clipStream.Clone(); + Streams[clipStream.PID] = stream; + + if (!IsCustom && !PlaylistStreams.ContainsKey(stream.PID)) + { + stream.IsHidden = true; + HasHiddenTracks = true; + } + + if (stream.IsVideoStream) + { + VideoStreams.Add((TSVideoStream)stream); + } + else if (stream.IsAudioStream) + { + AudioStreams.Add((TSAudioStream)stream); + } + else if (stream.IsGraphicsStream) + { + GraphicsStreams.Add((TSGraphicsStream)stream); + } + else if (stream.IsTextStream) + { + TextStreams.Add((TSTextStream)stream); + } + } + } + + if (referenceClip.StreamFile != null) + { + // TODO: Better way to add this in? + if (BDInfoSettings.EnableSSIF && + referenceClip.StreamFile.InterleavedFile != null && + referenceClip.StreamFile.Streams.ContainsKey(4114) && + !Streams.ContainsKey(4114)) + { + TSStream stream = referenceClip.StreamFile.Streams[4114].Clone(); + Streams[4114] = stream; + if (stream.IsVideoStream) + { + VideoStreams.Add((TSVideoStream)stream); + } + } + + foreach (TSStream clipStream + in referenceClip.StreamFile.Streams.Values) + { + if (Streams.ContainsKey(clipStream.PID)) + { + TSStream stream = Streams[clipStream.PID]; + + if (stream.StreamType != clipStream.StreamType) continue; + + if (clipStream.BitRate > stream.BitRate) + { + stream.BitRate = clipStream.BitRate; + } + stream.IsVBR = clipStream.IsVBR; + + if (stream.IsVideoStream && + clipStream.IsVideoStream) + { + ((TSVideoStream)stream).EncodingProfile = + ((TSVideoStream)clipStream).EncodingProfile; + } + else if (stream.IsAudioStream && + clipStream.IsAudioStream) + { + TSAudioStream audioStream = (TSAudioStream)stream; + TSAudioStream clipAudioStream = (TSAudioStream)clipStream; + + if (clipAudioStream.ChannelCount > audioStream.ChannelCount) + { + audioStream.ChannelCount = clipAudioStream.ChannelCount; + } + if (clipAudioStream.LFE > audioStream.LFE) + { + audioStream.LFE = clipAudioStream.LFE; + } + if (clipAudioStream.SampleRate > audioStream.SampleRate) + { + audioStream.SampleRate = clipAudioStream.SampleRate; + } + if (clipAudioStream.BitDepth > audioStream.BitDepth) + { + audioStream.BitDepth = clipAudioStream.BitDepth; + } + if (clipAudioStream.DialNorm < audioStream.DialNorm) + { + audioStream.DialNorm = clipAudioStream.DialNorm; + } + if (clipAudioStream.AudioMode != TSAudioMode.Unknown) + { + audioStream.AudioMode = clipAudioStream.AudioMode; + } + if (clipAudioStream.CoreStream != null && + audioStream.CoreStream == null) + { + audioStream.CoreStream = (TSAudioStream) + clipAudioStream.CoreStream.Clone(); + } + } + } + } + } + + for (int i = 0; i < AngleCount; i++) + { + AngleStreams.Add(new Dictionary()); + } + + if (!BDInfoSettings.KeepStreamOrder) + { + VideoStreams.Sort(CompareVideoStreams); + } + foreach (TSStream stream in VideoStreams) + { + SortedStreams.Add(stream); + for (int i = 0; i < AngleCount; i++) + { + TSStream angleStream = stream.Clone(); + angleStream.AngleIndex = i + 1; + AngleStreams[i][angleStream.PID] = angleStream; + SortedStreams.Add(angleStream); + } + } + + if (!BDInfoSettings.KeepStreamOrder) + { + AudioStreams.Sort(CompareAudioStreams); + } + foreach (TSStream stream in AudioStreams) + { + SortedStreams.Add(stream); + } + + if (!BDInfoSettings.KeepStreamOrder) + { + GraphicsStreams.Sort(CompareGraphicsStreams); + } + foreach (TSStream stream in GraphicsStreams) + { + SortedStreams.Add(stream); + } + + if (!BDInfoSettings.KeepStreamOrder) + { + TextStreams.Sort(CompareTextStreams); + } + foreach (TSStream stream in TextStreams) + { + SortedStreams.Add(stream); + } + } + + public void ClearBitrates() + { + foreach (TSStreamClip clip in StreamClips) + { + clip.PayloadBytes = 0; + clip.PacketCount = 0; + clip.PacketSeconds = 0; + + if (clip.StreamFile != null) + { + foreach (TSStream stream in clip.StreamFile.Streams.Values) + { + stream.PayloadBytes = 0; + stream.PacketCount = 0; + stream.PacketSeconds = 0; + } + + if (clip.StreamFile != null && + clip.StreamFile.StreamDiagnostics != null) + { + clip.StreamFile.StreamDiagnostics.Clear(); + } + } + } + + foreach (TSStream stream in SortedStreams) + { + stream.PayloadBytes = 0; + stream.PacketCount = 0; + stream.PacketSeconds = 0; + } + } + + public bool IsValid + { + get + { + if (!IsInitialized) return false; + + if (BDInfoSettings.FilterShortPlaylists && + TotalLength < BDInfoSettings.FilterShortPlaylistsValue) + { + return false; + } + + if (HasLoops && + BDInfoSettings.FilterLoopingPlaylists) + { + return false; + } + + return true; + } + } + + public static int CompareVideoStreams( + TSVideoStream x, + TSVideoStream y) + { + if (x == null && y == null) + { + return 0; + } + else if (x == null && y != null) + { + return 1; + } + else if (x != null && y == null) + { + return -1; + } + else + { + if (x.Height > y.Height) + { + return -1; + } + else if (y.Height > x.Height) + { + return 1; + } + else if (x.PID > y.PID) + { + return 1; + } + else if (y.PID > x.PID) + { + return -1; + } + else + { + return 0; + } + } + } + + public static int CompareAudioStreams( + TSAudioStream x, + TSAudioStream y) + { + if (x == y) + { + return 0; + } + else if (x == null && y == null) + { + return 0; + } + else if (x == null && y != null) + { + return -1; + } + else if (x != null && y == null) + { + return 1; + } + else + { + if (x.ChannelCount > y.ChannelCount) + { + return -1; + } + else if (y.ChannelCount > x.ChannelCount) + { + return 1; + } + else + { + int sortX = GetStreamTypeSortIndex(x.StreamType); + int sortY = GetStreamTypeSortIndex(y.StreamType); + + if (sortX > sortY) + { + return -1; + } + else if (sortY > sortX) + { + return 1; + } + else + { + if (x.LanguageCode == "eng") + { + return -1; + } + else if (y.LanguageCode == "eng") + { + return 1; + } + else if (x.LanguageCode != y.LanguageCode) + { + return string.Compare( + x.LanguageName, y.LanguageName); + } + else if (x.PID < y.PID) + { + return -1; + } + else if (y.PID < x.PID) + { + return 1; + } + return 0; + } + } + } + } + + public static int CompareTextStreams( + TSTextStream x, + TSTextStream y) + { + if (x == y) + { + return 0; + } + else if (x == null && y == null) + { + return 0; + } + else if (x == null && y != null) + { + return -1; + } + else if (x != null && y == null) + { + return 1; + } + else + { + if (x.LanguageCode == "eng") + { + return -1; + } + else if (y.LanguageCode == "eng") + { + return 1; + } + else + { + if (x.LanguageCode == y.LanguageCode) + { + if (x.PID > y.PID) + { + return 1; + } + else if (y.PID > x.PID) + { + return -1; + } + else + { + return 0; + } + } + else + { + return string.Compare( + x.LanguageName, y.LanguageName); + } + } + } + } + + private static int CompareGraphicsStreams( + TSGraphicsStream x, + TSGraphicsStream y) + { + if (x == y) + { + return 0; + } + else if (x == null && y == null) + { + return 0; + } + else if (x == null && y != null) + { + return -1; + } + else if (x != null && y == null) + { + return 1; + } + else + { + int sortX = GetStreamTypeSortIndex(x.StreamType); + int sortY = GetStreamTypeSortIndex(y.StreamType); + + if (sortX > sortY) + { + return -1; + } + else if (sortY > sortX) + { + return 1; + } + else if (x.LanguageCode == "eng") + { + return -1; + } + else if (y.LanguageCode == "eng") + { + return 1; + } + else + { + if (x.LanguageCode == y.LanguageCode) + { + if (x.PID > y.PID) + { + return 1; + } + else if (y.PID > x.PID) + { + return -1; + } + else + { + return 0; + } + } + else + { + return string.Compare(x.LanguageName, y.LanguageName); + } + } + } + } + + private static int GetStreamTypeSortIndex(TSStreamType streamType) + { + switch (streamType) + { + case TSStreamType.Unknown: + return 0; + case TSStreamType.MPEG1_VIDEO: + return 1; + case TSStreamType.MPEG2_VIDEO: + return 2; + case TSStreamType.AVC_VIDEO: + return 3; + case TSStreamType.VC1_VIDEO: + return 4; + case TSStreamType.MVC_VIDEO: + return 5; + + case TSStreamType.MPEG1_AUDIO: + return 1; + case TSStreamType.MPEG2_AUDIO: + return 2; + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + return 3; + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + return 4; + case TSStreamType.AC3_AUDIO: + return 5; + case TSStreamType.DTS_AUDIO: + return 6; + case TSStreamType.AC3_PLUS_AUDIO: + return 7; + case TSStreamType.DTS_HD_AUDIO: + return 8; + case TSStreamType.AC3_TRUE_HD_AUDIO: + return 9; + case TSStreamType.DTS_HD_MASTER_AUDIO: + return 10; + case TSStreamType.LPCM_AUDIO: + return 11; + + case TSStreamType.SUBTITLE: + return 1; + case TSStreamType.INTERACTIVE_GRAPHICS: + return 2; + case TSStreamType.PRESENTATION_GRAPHICS: + return 3; + + default: + return 0; + } + } + + protected string ReadString( + byte[] data, + int count, + ref int pos) + { + string val = + _textEncoding.GetASCIIEncoding().GetString(data, pos, count); + + pos += count; + + return val; + } + + protected int ReadInt32( + byte[] data, + ref int pos) + { + int val = + ((int)data[pos] << 24) + + ((int)data[pos + 1] << 16) + + ((int)data[pos + 2] << 8) + + ((int)data[pos + 3]); + + pos += 4; + + return val; + } + + protected int ReadInt16( + byte[] data, + ref int pos) + { + int val = + ((int)data[pos] << 8) + + ((int)data[pos + 1]); + + pos += 2; + + return val; + } + + protected byte ReadByte( + byte[] data, + ref int pos) + { + return data[pos++]; + } + } +} diff --git a/BDInfo/TSStream.cs b/BDInfo/TSStream.cs new file mode 100644 index 0000000000..5afb81c5e4 --- /dev/null +++ b/BDInfo/TSStream.cs @@ -0,0 +1,801 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +using System; +using System.Collections.Generic; + +namespace BDInfo +{ + public enum TSStreamType : byte + { + Unknown = 0, + MPEG1_VIDEO = 0x01, + MPEG2_VIDEO = 0x02, + AVC_VIDEO = 0x1b, + MVC_VIDEO = 0x20, + VC1_VIDEO = 0xea, + MPEG1_AUDIO = 0x03, + MPEG2_AUDIO = 0x04, + LPCM_AUDIO = 0x80, + AC3_AUDIO = 0x81, + AC3_PLUS_AUDIO = 0x84, + AC3_PLUS_SECONDARY_AUDIO = 0xA1, + AC3_TRUE_HD_AUDIO = 0x83, + DTS_AUDIO = 0x82, + DTS_HD_AUDIO = 0x85, + DTS_HD_SECONDARY_AUDIO = 0xA2, + DTS_HD_MASTER_AUDIO = 0x86, + PRESENTATION_GRAPHICS = 0x90, + INTERACTIVE_GRAPHICS = 0x91, + SUBTITLE = 0x92 + } + + public enum TSVideoFormat : byte + { + Unknown = 0, + VIDEOFORMAT_480i = 1, + VIDEOFORMAT_576i = 2, + VIDEOFORMAT_480p = 3, + VIDEOFORMAT_1080i = 4, + VIDEOFORMAT_720p = 5, + VIDEOFORMAT_1080p = 6, + VIDEOFORMAT_576p = 7, + } + + public enum TSFrameRate : byte + { + Unknown = 0, + FRAMERATE_23_976 = 1, + FRAMERATE_24 = 2, + FRAMERATE_25 = 3, + FRAMERATE_29_97 = 4, + FRAMERATE_50 = 6, + FRAMERATE_59_94 = 7 + } + + public enum TSChannelLayout : byte + { + Unknown = 0, + CHANNELLAYOUT_MONO = 1, + CHANNELLAYOUT_STEREO = 3, + CHANNELLAYOUT_MULTI = 6, + CHANNELLAYOUT_COMBO = 12 + } + + public enum TSSampleRate : byte + { + Unknown = 0, + SAMPLERATE_48 = 1, + SAMPLERATE_96 = 4, + SAMPLERATE_192 = 5, + SAMPLERATE_48_192 = 12, + SAMPLERATE_48_96 = 14 + } + + public enum TSAspectRatio + { + Unknown = 0, + ASPECT_4_3 = 2, + ASPECT_16_9 = 3, + ASPECT_2_21 = 4 + } + + public class TSDescriptor + { + public byte Name; + public byte[] Value; + + public TSDescriptor(byte name, byte length) + { + Name = name; + Value = new byte[length]; + } + + public TSDescriptor Clone() + { + TSDescriptor descriptor = + new TSDescriptor(Name, (byte)Value.Length); + Value.CopyTo(descriptor.Value, 0); + return descriptor; + } + } + + public abstract class TSStream + { + public TSStream() + { + } + + public override string ToString() + { + return string.Format("{0} ({1})", CodecShortName, PID); + } + + public ushort PID; + public TSStreamType StreamType; + public List Descriptors = null; + public long BitRate = 0; + public long ActiveBitRate = 0; + public bool IsVBR = false; + public bool IsInitialized = false; + public string LanguageName; + public bool IsHidden = false; + + public ulong PayloadBytes = 0; + public ulong PacketCount = 0; + public double PacketSeconds = 0; + public int AngleIndex = 0; + + public ulong PacketSize + { + get + { + return PacketCount * 192; + } + } + + private string _LanguageCode; + public string LanguageCode + { + get + { + return _LanguageCode; + } + set + { + _LanguageCode = value; + LanguageName = LanguageCodes.GetName(value); + } + } + + public bool IsVideoStream + { + get + { + switch (StreamType) + { + case TSStreamType.MPEG1_VIDEO: + case TSStreamType.MPEG2_VIDEO: + case TSStreamType.AVC_VIDEO: + case TSStreamType.MVC_VIDEO: + case TSStreamType.VC1_VIDEO: + return true; + + default: + return false; + } + } + } + + public bool IsAudioStream + { + get + { + switch (StreamType) + { + case TSStreamType.MPEG1_AUDIO: + case TSStreamType.MPEG2_AUDIO: + case TSStreamType.LPCM_AUDIO: + case TSStreamType.AC3_AUDIO: + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + case TSStreamType.AC3_TRUE_HD_AUDIO: + case TSStreamType.DTS_AUDIO: + case TSStreamType.DTS_HD_AUDIO: + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + case TSStreamType.DTS_HD_MASTER_AUDIO: + return true; + + default: + return false; + } + } + } + + public bool IsGraphicsStream + { + get + { + switch (StreamType) + { + case TSStreamType.PRESENTATION_GRAPHICS: + case TSStreamType.INTERACTIVE_GRAPHICS: + return true; + + default: + return false; + } + } + } + + public bool IsTextStream + { + get + { + switch (StreamType) + { + case TSStreamType.SUBTITLE: + return true; + + default: + return false; + } + } + } + + public string CodecName + { + get + { + switch (StreamType) + { + case TSStreamType.MPEG1_VIDEO: + return "MPEG-1 Video"; + case TSStreamType.MPEG2_VIDEO: + return "MPEG-2 Video"; + case TSStreamType.AVC_VIDEO: + return "MPEG-4 AVC Video"; + case TSStreamType.MVC_VIDEO: + return "MPEG-4 MVC Video"; + case TSStreamType.VC1_VIDEO: + return "VC-1 Video"; + case TSStreamType.MPEG1_AUDIO: + return "MP1 Audio"; + case TSStreamType.MPEG2_AUDIO: + return "MP2 Audio"; + case TSStreamType.LPCM_AUDIO: + return "LPCM Audio"; + case TSStreamType.AC3_AUDIO: + if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended) + return "Dolby Digital EX Audio"; + else + return "Dolby Digital Audio"; + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + return "Dolby Digital Plus Audio"; + case TSStreamType.AC3_TRUE_HD_AUDIO: + return "Dolby TrueHD Audio"; + case TSStreamType.DTS_AUDIO: + if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended) + return "DTS-ES Audio"; + else + return "DTS Audio"; + case TSStreamType.DTS_HD_AUDIO: + return "DTS-HD High-Res Audio"; + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + return "DTS Express"; + case TSStreamType.DTS_HD_MASTER_AUDIO: + return "DTS-HD Master Audio"; + case TSStreamType.PRESENTATION_GRAPHICS: + return "Presentation Graphics"; + case TSStreamType.INTERACTIVE_GRAPHICS: + return "Interactive Graphics"; + case TSStreamType.SUBTITLE: + return "Subtitle"; + default: + return "UNKNOWN"; + } + } + } + + public string CodecAltName + { + get + { + switch (StreamType) + { + case TSStreamType.MPEG1_VIDEO: + return "MPEG-1"; + case TSStreamType.MPEG2_VIDEO: + return "MPEG-2"; + case TSStreamType.AVC_VIDEO: + return "AVC"; + case TSStreamType.MVC_VIDEO: + return "MVC"; + case TSStreamType.VC1_VIDEO: + return "VC-1"; + case TSStreamType.MPEG1_AUDIO: + return "MP1"; + case TSStreamType.MPEG2_AUDIO: + return "MP2"; + case TSStreamType.LPCM_AUDIO: + return "LPCM"; + case TSStreamType.AC3_AUDIO: + return "DD AC3"; + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + return "DD AC3+"; + case TSStreamType.AC3_TRUE_HD_AUDIO: + return "Dolby TrueHD"; + case TSStreamType.DTS_AUDIO: + return "DTS"; + case TSStreamType.DTS_HD_AUDIO: + return "DTS-HD Hi-Res"; + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + return "DTS Express"; + case TSStreamType.DTS_HD_MASTER_AUDIO: + return "DTS-HD Master"; + case TSStreamType.PRESENTATION_GRAPHICS: + return "PGS"; + case TSStreamType.INTERACTIVE_GRAPHICS: + return "IGS"; + case TSStreamType.SUBTITLE: + return "SUB"; + default: + return "UNKNOWN"; + } + } + } + + public string CodecShortName + { + get + { + switch (StreamType) + { + case TSStreamType.MPEG1_VIDEO: + return "MPEG-1"; + case TSStreamType.MPEG2_VIDEO: + return "MPEG-2"; + case TSStreamType.AVC_VIDEO: + return "AVC"; + case TSStreamType.MVC_VIDEO: + return "MVC"; + case TSStreamType.VC1_VIDEO: + return "VC-1"; + case TSStreamType.MPEG1_AUDIO: + return "MP1"; + case TSStreamType.MPEG2_AUDIO: + return "MP2"; + case TSStreamType.LPCM_AUDIO: + return "LPCM"; + case TSStreamType.AC3_AUDIO: + if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended) + return "AC3-EX"; + else + return "AC3"; + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + return "AC3+"; + case TSStreamType.AC3_TRUE_HD_AUDIO: + return "TrueHD"; + case TSStreamType.DTS_AUDIO: + if (((TSAudioStream)this).AudioMode == TSAudioMode.Extended) + return "DTS-ES"; + else + return "DTS"; + case TSStreamType.DTS_HD_AUDIO: + return "DTS-HD HR"; + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + return "DTS Express"; + case TSStreamType.DTS_HD_MASTER_AUDIO: + return "DTS-HD MA"; + case TSStreamType.PRESENTATION_GRAPHICS: + return "PGS"; + case TSStreamType.INTERACTIVE_GRAPHICS: + return "IGS"; + case TSStreamType.SUBTITLE: + return "SUB"; + default: + return "UNKNOWN"; + } + } + } + + public virtual string Description + { + get + { + return ""; + } + } + + public abstract TSStream Clone(); + + protected void CopyTo(TSStream stream) + { + stream.PID = PID; + stream.StreamType = StreamType; + stream.IsVBR = IsVBR; + stream.BitRate = BitRate; + stream.IsInitialized = IsInitialized; + stream.LanguageCode = _LanguageCode; + if (Descriptors != null) + { + stream.Descriptors = new List(); + foreach (TSDescriptor descriptor in Descriptors) + { + stream.Descriptors.Add(descriptor.Clone()); + } + } + } + } + + public class TSVideoStream : TSStream + { + public TSVideoStream() + { + } + + public int Width; + public int Height; + public bool IsInterlaced; + public int FrameRateEnumerator; + public int FrameRateDenominator; + public TSAspectRatio AspectRatio; + public string EncodingProfile; + + private TSVideoFormat _VideoFormat; + public TSVideoFormat VideoFormat + { + get + { + return _VideoFormat; + } + set + { + _VideoFormat = value; + switch (value) + { + case TSVideoFormat.VIDEOFORMAT_480i: + Height = 480; + IsInterlaced = true; + break; + case TSVideoFormat.VIDEOFORMAT_480p: + Height = 480; + IsInterlaced = false; + break; + case TSVideoFormat.VIDEOFORMAT_576i: + Height = 576; + IsInterlaced = true; + break; + case TSVideoFormat.VIDEOFORMAT_576p: + Height = 576; + IsInterlaced = false; + break; + case TSVideoFormat.VIDEOFORMAT_720p: + Height = 720; + IsInterlaced = false; + break; + case TSVideoFormat.VIDEOFORMAT_1080i: + Height = 1080; + IsInterlaced = true; + break; + case TSVideoFormat.VIDEOFORMAT_1080p: + Height = 1080; + IsInterlaced = false; + break; + } + } + } + + private TSFrameRate _FrameRate; + public TSFrameRate FrameRate + { + get + { + return _FrameRate; + } + set + { + _FrameRate = value; + switch (value) + { + case TSFrameRate.FRAMERATE_23_976: + FrameRateEnumerator = 24000; + FrameRateDenominator = 1001; + break; + case TSFrameRate.FRAMERATE_24: + FrameRateEnumerator = 24000; + FrameRateDenominator = 1000; + break; + case TSFrameRate.FRAMERATE_25: + FrameRateEnumerator = 25000; + FrameRateDenominator = 1000; + break; + case TSFrameRate.FRAMERATE_29_97: + FrameRateEnumerator = 30000; + FrameRateDenominator = 1001; + break; + case TSFrameRate.FRAMERATE_50: + FrameRateEnumerator = 50000; + FrameRateDenominator = 1000; + break; + case TSFrameRate.FRAMERATE_59_94: + FrameRateEnumerator = 60000; + FrameRateDenominator = 1001; + break; + } + } + } + + public override string Description + { + get + { + string description = ""; + + if (Height > 0) + { + description += string.Format("{0:D}{1} / ", + Height, + IsInterlaced ? "i" : "p"); + } + if (FrameRateEnumerator > 0 && + FrameRateDenominator > 0) + { + if (FrameRateEnumerator % FrameRateDenominator == 0) + { + description += string.Format("{0:D} fps / ", + FrameRateEnumerator / FrameRateDenominator); + } + else + { + description += string.Format("{0:F3} fps / ", + (double)FrameRateEnumerator / FrameRateDenominator); + } + + } + if (AspectRatio == TSAspectRatio.ASPECT_4_3) + { + description += "4:3 / "; + } + else if (AspectRatio == TSAspectRatio.ASPECT_16_9) + { + description += "16:9 / "; + } + if (EncodingProfile != null) + { + description += EncodingProfile + " / "; + } + if (description.EndsWith(" / ")) + { + description = description.Substring(0, description.Length - 3); + } + return description; + } + } + + public override TSStream Clone() + { + TSVideoStream stream = new TSVideoStream(); + CopyTo(stream); + + stream.VideoFormat = _VideoFormat; + stream.FrameRate = _FrameRate; + stream.Width = Width; + stream.Height = Height; + stream.IsInterlaced = IsInterlaced; + stream.FrameRateEnumerator = FrameRateEnumerator; + stream.FrameRateDenominator = FrameRateDenominator; + stream.AspectRatio = AspectRatio; + stream.EncodingProfile = EncodingProfile; + + return stream; + } + } + + public enum TSAudioMode + { + Unknown, + DualMono, + Stereo, + Surround, + Extended + } + + public class TSAudioStream : TSStream + { + public TSAudioStream() + { + } + + public int SampleRate; + public int ChannelCount; + public int BitDepth; + public int LFE; + public int DialNorm; + public TSAudioMode AudioMode; + public TSAudioStream CoreStream; + public TSChannelLayout ChannelLayout; + + public static int ConvertSampleRate( + TSSampleRate sampleRate) + { + switch (sampleRate) + { + case TSSampleRate.SAMPLERATE_48: + return 48000; + + case TSSampleRate.SAMPLERATE_96: + case TSSampleRate.SAMPLERATE_48_96: + return 96000; + + case TSSampleRate.SAMPLERATE_192: + case TSSampleRate.SAMPLERATE_48_192: + return 192000; + } + return 0; + } + + public string ChannelDescription + { + get + { + if (ChannelLayout == TSChannelLayout.CHANNELLAYOUT_MONO && + ChannelCount == 2) + { + } + + string description = ""; + if (ChannelCount > 0) + { + description += string.Format( + "{0:D}.{1:D}", + ChannelCount, LFE); + } + else + { + switch (ChannelLayout) + { + case TSChannelLayout.CHANNELLAYOUT_MONO: + description += "1.0"; + break; + case TSChannelLayout.CHANNELLAYOUT_STEREO: + description += "2.0"; + break; + case TSChannelLayout.CHANNELLAYOUT_MULTI: + description += "5.1"; + break; + } + } + if (AudioMode == TSAudioMode.Extended) + { + if (StreamType == TSStreamType.AC3_AUDIO) + { + description += "-EX"; + } + if (StreamType == TSStreamType.DTS_AUDIO || + StreamType == TSStreamType.DTS_HD_AUDIO || + StreamType == TSStreamType.DTS_HD_MASTER_AUDIO) + { + description += "-ES"; + } + } + return description; + } + } + + public override string Description + { + get + { + string description = ChannelDescription; + + if (SampleRate > 0) + { + description += string.Format( + " / {0:D} kHz", SampleRate / 1000); + } + if (BitRate > 0) + { + description += string.Format( + " / {0:D} kbps", (uint)Math.Round((double)BitRate / 1000)); + } + if (BitDepth > 0) + { + description += string.Format( + " / {0:D}-bit", BitDepth); + } + if (DialNorm != 0) + { + description += string.Format( + " / DN {0}dB", DialNorm); + } + if (ChannelCount == 2) + { + switch (AudioMode) + { + case TSAudioMode.DualMono: + description += " / Dual Mono"; + break; + + case TSAudioMode.Surround: + description += " / Dolby Surround"; + break; + } + } + if (description.EndsWith(" / ")) + { + description = description.Substring(0, description.Length - 3); + } + if (CoreStream != null) + { + string codec = ""; + switch (CoreStream.StreamType) + { + case TSStreamType.AC3_AUDIO: + codec = "AC3 Embedded"; + break; + case TSStreamType.DTS_AUDIO: + codec = "DTS Core"; + break; + } + description += string.Format( + " ({0}: {1})", + codec, + CoreStream.Description); + } + return description; + } + } + + public override TSStream Clone() + { + TSAudioStream stream = new TSAudioStream(); + CopyTo(stream); + + stream.SampleRate = SampleRate; + stream.ChannelLayout = ChannelLayout; + stream.ChannelCount = ChannelCount; + stream.BitDepth = BitDepth; + stream.LFE = LFE; + stream.DialNorm = DialNorm; + stream.AudioMode = AudioMode; + if (CoreStream != null) + { + stream.CoreStream = (TSAudioStream)CoreStream.Clone(); + } + + return stream; + } + } + + public class TSGraphicsStream : TSStream + { + public TSGraphicsStream() + { + IsVBR = true; + IsInitialized = true; + } + + public override TSStream Clone() + { + TSGraphicsStream stream = new TSGraphicsStream(); + CopyTo(stream); + return stream; + } + } + + public class TSTextStream : TSStream + { + public TSTextStream() + { + IsVBR = true; + IsInitialized = true; + } + + public override TSStream Clone() + { + TSTextStream stream = new TSTextStream(); + CopyTo(stream); + return stream; + } + } +} diff --git a/BDInfo/TSStreamBuffer.cs b/BDInfo/TSStreamBuffer.cs new file mode 100644 index 0000000000..2f9094876e --- /dev/null +++ b/BDInfo/TSStreamBuffer.cs @@ -0,0 +1,142 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +using System; +using System.Collections.Specialized; +using System.IO; + +namespace BDInfo +{ + public class TSStreamBuffer + { + private MemoryStream Stream = new MemoryStream(); + private int SkipBits = 0; + private byte[] Buffer; + private int BufferLength = 0; + public int TransferLength = 0; + + public TSStreamBuffer() + { + Buffer = new byte[4096]; + Stream = new MemoryStream(Buffer); + } + + public long Length + { + get + { + return (long)BufferLength; + } + } + + public long Position + { + get + { + return Stream.Position; + } + } + + public void Add( + byte[] buffer, + int offset, + int length) + { + TransferLength += length; + + if (BufferLength + length >= Buffer.Length) + { + length = Buffer.Length - BufferLength; + } + if (length > 0) + { + Array.Copy(buffer, offset, Buffer, BufferLength, length); + BufferLength += length; + } + } + + public void Seek( + long offset, + SeekOrigin loc) + { + Stream.Seek(offset, loc); + } + + public void Reset() + { + BufferLength = 0; + TransferLength = 0; + } + + public void BeginRead() + { + SkipBits = 0; + Stream.Seek(0, SeekOrigin.Begin); + } + + public void EndRead() + { + } + + public byte[] ReadBytes(int bytes) + { + if (Stream.Position + bytes >= BufferLength) + { + return null; + } + + byte[] value = new byte[bytes]; + Stream.Read(value, 0, bytes); + return value; + } + + public byte ReadByte() + { + return (byte)Stream.ReadByte(); + } + + public int ReadBits(int bits) + { + long pos = Stream.Position; + + int shift = 24; + int data = 0; + for (int i = 0; i < 4; i++) + { + if (pos + i >= BufferLength) break; + data += (Stream.ReadByte() << shift); + shift -= 8; + } + BitVector32 vector = new BitVector32(data); + + int value = 0; + for (int i = SkipBits; i < SkipBits + bits; i++) + { + value <<= 1; + value += (vector[1 << (32 - i - 1)] ? 1 : 0); + } + + SkipBits += bits; + Stream.Seek(pos + (SkipBits >> 3), SeekOrigin.Begin); + SkipBits = SkipBits % 8; + + return value; + } + } +} diff --git a/BDInfo/TSStreamClip.cs b/BDInfo/TSStreamClip.cs new file mode 100644 index 0000000000..d7592a71aa --- /dev/null +++ b/BDInfo/TSStreamClip.cs @@ -0,0 +1,113 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +using System; +using System.Collections.Generic; + +namespace BDInfo +{ + public class TSStreamClip + { + public int AngleIndex = 0; + public string Name; + public double TimeIn; + public double TimeOut; + public double RelativeTimeIn; + public double RelativeTimeOut; + public double Length; + + public ulong FileSize = 0; + public ulong InterleavedFileSize = 0; + public ulong PayloadBytes = 0; + public ulong PacketCount = 0; + public double PacketSeconds = 0; + + public List Chapters = new List(); + + public TSStreamFile StreamFile = null; + public TSStreamClipFile StreamClipFile = null; + + public TSStreamClip( + TSStreamFile streamFile, + TSStreamClipFile streamClipFile) + { + if (streamFile != null) + { + Name = streamFile.Name; + StreamFile = streamFile; + FileSize = (ulong)StreamFile.FileInfo.Length; + if (StreamFile.InterleavedFile != null) + { + InterleavedFileSize = (ulong)StreamFile.InterleavedFile.FileInfo.Length; + } + } + StreamClipFile = streamClipFile; + } + + public string DisplayName + { + get + { + if (StreamFile != null && + StreamFile.InterleavedFile != null && + BDInfoSettings.EnableSSIF) + { + return StreamFile.InterleavedFile.Name; + } + return Name; + } + } + + public ulong PacketSize + { + get + { + return PacketCount * 192; + } + } + + public ulong PacketBitRate + { + get + { + if (PacketSeconds > 0) + { + return (ulong)Math.Round(((PacketSize * 8.0) / PacketSeconds)); + } + return 0; + } + } + + public bool IsCompatible(TSStreamClip clip) + { + foreach (TSStream stream1 in StreamFile.Streams.Values) + { + if (clip.StreamFile.Streams.ContainsKey(stream1.PID)) + { + TSStream stream2 = clip.StreamFile.Streams[stream1.PID]; + if (stream1.StreamType != stream2.StreamType) + { + return false; + } + } + } + return true; + } + } +} diff --git a/BDInfo/TSStreamClipFile.cs b/BDInfo/TSStreamClipFile.cs new file mode 100644 index 0000000000..f2accb88d8 --- /dev/null +++ b/BDInfo/TSStreamClipFile.cs @@ -0,0 +1,253 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +#undef DEBUG +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; + +namespace BDInfo +{ + public class TSStreamClipFile + { + private readonly IFileSystem _fileSystem; + private readonly ITextEncoding _textEncoding; + public FileSystemMetadata FileInfo = null; + public string FileType = null; + public bool IsValid = false; + public string Name = null; + + public Dictionary Streams = + new Dictionary(); + + public TSStreamClipFile( + FileSystemMetadata fileInfo, IFileSystem fileSystem, ITextEncoding textEncoding) + { + FileInfo = fileInfo; + _fileSystem = fileSystem; + _textEncoding = textEncoding; + Name = fileInfo.Name.ToUpper(); + } + + public void Scan() + { + Stream fileStream = null; + BinaryReader fileReader = null; + + try + { +#if DEBUG + Debug.WriteLine(string.Format( + "Scanning {0}...", Name)); +#endif + Streams.Clear(); + + fileStream = _fileSystem.OpenRead(FileInfo.FullName); + fileReader = new BinaryReader(fileStream); + + byte[] data = new byte[fileStream.Length]; + fileReader.Read(data, 0, data.Length); + + byte[] fileType = new byte[8]; + Array.Copy(data, 0, fileType, 0, fileType.Length); + + FileType = _textEncoding.GetASCIIEncoding().GetString(fileType, 0, fileType.Length); + if (FileType != "HDMV0100" && + FileType != "HDMV0200") + { + throw new Exception(string.Format( + "Clip info file {0} has an unknown file type {1}.", + FileInfo.Name, FileType)); + } +#if DEBUG + Debug.WriteLine(string.Format( + "\tFileType: {0}", FileType)); +#endif + int clipIndex = + ((int)data[12] << 24) + + ((int)data[13] << 16) + + ((int)data[14] << 8) + + ((int)data[15]); + + int clipLength = + ((int)data[clipIndex] << 24) + + ((int)data[clipIndex + 1] << 16) + + ((int)data[clipIndex + 2] << 8) + + ((int)data[clipIndex + 3]); + + byte[] clipData = new byte[clipLength]; + Array.Copy(data, clipIndex + 4, clipData, 0, clipData.Length); + + int streamCount = clipData[8]; +#if DEBUG + Debug.WriteLine(string.Format( + "\tStreamCount: {0}", streamCount)); +#endif + int streamOffset = 10; + for (int streamIndex = 0; + streamIndex < streamCount; + streamIndex++) + { + TSStream stream = null; + + ushort PID = (ushort) + ((clipData[streamOffset] << 8) + + clipData[streamOffset + 1]); + + streamOffset += 2; + + TSStreamType streamType = (TSStreamType) + clipData[streamOffset + 1]; + switch (streamType) + { + case TSStreamType.MVC_VIDEO: + // TODO + break; + + case TSStreamType.AVC_VIDEO: + case TSStreamType.MPEG1_VIDEO: + case TSStreamType.MPEG2_VIDEO: + case TSStreamType.VC1_VIDEO: + { + TSVideoFormat videoFormat = (TSVideoFormat) + (clipData[streamOffset + 2] >> 4); + TSFrameRate frameRate = (TSFrameRate) + (clipData[streamOffset + 2] & 0xF); + TSAspectRatio aspectRatio = (TSAspectRatio) + (clipData[streamOffset + 3] >> 4); + + stream = new TSVideoStream(); + ((TSVideoStream)stream).VideoFormat = videoFormat; + ((TSVideoStream)stream).AspectRatio = aspectRatio; + ((TSVideoStream)stream).FrameRate = frameRate; +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2} {3} {4}", + PID, + streamType, + videoFormat, + frameRate, + aspectRatio)); +#endif + } + break; + + case TSStreamType.AC3_AUDIO: + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + case TSStreamType.AC3_TRUE_HD_AUDIO: + case TSStreamType.DTS_AUDIO: + case TSStreamType.DTS_HD_AUDIO: + case TSStreamType.DTS_HD_MASTER_AUDIO: + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + case TSStreamType.LPCM_AUDIO: + case TSStreamType.MPEG1_AUDIO: + case TSStreamType.MPEG2_AUDIO: + { + byte[] languageBytes = new byte[3]; + Array.Copy(clipData, streamOffset + 3, + languageBytes, 0, languageBytes.Length); + string languageCode = + _textEncoding.GetASCIIEncoding().GetString(languageBytes, 0, languageBytes.Length); + + TSChannelLayout channelLayout = (TSChannelLayout) + (clipData[streamOffset + 2] >> 4); + TSSampleRate sampleRate = (TSSampleRate) + (clipData[streamOffset + 2] & 0xF); + + stream = new TSAudioStream(); + ((TSAudioStream)stream).LanguageCode = languageCode; + ((TSAudioStream)stream).ChannelLayout = channelLayout; + ((TSAudioStream)stream).SampleRate = TSAudioStream.ConvertSampleRate(sampleRate); + ((TSAudioStream)stream).LanguageCode = languageCode; +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2} {3} {4}", + PID, + streamType, + languageCode, + channelLayout, + sampleRate)); +#endif + } + break; + + case TSStreamType.INTERACTIVE_GRAPHICS: + case TSStreamType.PRESENTATION_GRAPHICS: + { + byte[] languageBytes = new byte[3]; + Array.Copy(clipData, streamOffset + 2, + languageBytes, 0, languageBytes.Length); + string languageCode = + _textEncoding.GetASCIIEncoding().GetString(languageBytes, 0, languageBytes.Length); + + stream = new TSGraphicsStream(); + stream.LanguageCode = languageCode; +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2}", + PID, + streamType, + languageCode)); +#endif + } + break; + + case TSStreamType.SUBTITLE: + { + byte[] languageBytes = new byte[3]; + Array.Copy(clipData, streamOffset + 3, + languageBytes, 0, languageBytes.Length); + string languageCode = + _textEncoding.GetASCIIEncoding().GetString(languageBytes, 0, languageBytes.Length); +#if DEBUG + Debug.WriteLine(string.Format( + "\t{0} {1} {2}", + PID, + streamType, + languageCode)); +#endif + stream = new TSTextStream(); + stream.LanguageCode = languageCode; + } + break; + } + + if (stream != null) + { + stream.PID = PID; + stream.StreamType = streamType; + Streams.Add(PID, stream); + } + + streamOffset += clipData[streamOffset] + 1; + } + IsValid = true; + } + finally + { + if (fileReader != null) fileReader.Dispose(); + if (fileStream != null) fileStream.Dispose(); + } + } + } +} diff --git a/BDInfo/TSStreamFile.cs b/BDInfo/TSStreamFile.cs new file mode 100644 index 0000000000..31020cbf40 --- /dev/null +++ b/BDInfo/TSStreamFile.cs @@ -0,0 +1,1553 @@ +//============================================================================ +// BDInfo - Blu-ray Video and Audio Analysis Tool +// Copyright © 2010 Cinema Squid +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +//============================================================================= + +#undef DEBUG +using System; +using System.Collections.Generic; +using System.IO; +using MediaBrowser.Model.IO; + +namespace BDInfo +{ + public class TSStreamState + { + public ulong TransferCount = 0; + + public string StreamTag = null; + + public ulong TotalPackets = 0; + public ulong WindowPackets = 0; + + public ulong TotalBytes = 0; + public ulong WindowBytes = 0; + + public long PeakTransferLength = 0; + public long PeakTransferRate = 0; + + public double TransferMarker = 0; + public double TransferInterval = 0; + + public TSStreamBuffer StreamBuffer = new TSStreamBuffer(); + + public uint Parse = 0; + public bool TransferState = false; + public int TransferLength = 0; + public int PacketLength = 0; + public byte PacketLengthParse = 0; + public byte PacketParse = 0; + + public byte PTSParse = 0; + public ulong PTS = 0; + public ulong PTSTemp = 0; + public ulong PTSLast = 0; + public ulong PTSPrev = 0; + public ulong PTSDiff = 0; + public ulong PTSCount = 0; + public ulong PTSTransfer = 0; + + public byte DTSParse = 0; + public ulong DTSTemp = 0; + public ulong DTSPrev = 0; + + public byte PESHeaderLength = 0; + public byte PESHeaderFlags = 0; +#if DEBUG + public byte PESHeaderIndex = 0; + public byte[] PESHeader = new byte[256 + 9]; +#endif + } + + public class TSPacketParser + { + public bool SyncState = false; + public byte TimeCodeParse = 4; + public byte PacketLength = 0; + public byte HeaderParse = 0; + + public uint TimeCode; + public byte TransportErrorIndicator; + public byte PayloadUnitStartIndicator; + public byte TransportPriority; + public ushort PID; + public byte TransportScramblingControl; + public byte AdaptionFieldControl; + + public bool AdaptionFieldState = false; + public byte AdaptionFieldParse = 0; + public byte AdaptionFieldLength = 0; + + public ushort PCRPID = 0xFFFF; + public byte PCRParse = 0; + public ulong PreviousPCR = 0; + public ulong PCR = 0; + public ulong PCRCount = 0; + public ulong PTSFirst = ulong.MaxValue; + public ulong PTSLast = ulong.MinValue; + public ulong PTSDiff = 0; + + public byte[] PAT = new byte[1024]; + public bool PATSectionStart = false; + public byte PATPointerField = 0; + public uint PATOffset = 0; + public byte PATSectionLengthParse = 0; + public ushort PATSectionLength = 0; + public uint PATSectionParse = 0; + public bool PATTransferState = false; + public byte PATSectionNumber = 0; + public byte PATLastSectionNumber = 0; + + public ushort TransportStreamId = 0xFFFF; + + public List PMTProgramDescriptors = new List(); + public ushort PMTPID = 0xFFFF; + public Dictionary PMT = new Dictionary(); + public bool PMTSectionStart = false; + public ushort PMTProgramInfoLength = 0; + public byte PMTProgramDescriptor = 0; + public byte PMTProgramDescriptorLengthParse = 0; + public byte PMTProgramDescriptorLength = 0; + public ushort PMTStreamInfoLength = 0; + public uint PMTStreamDescriptorLengthParse = 0; + public uint PMTStreamDescriptorLength = 0; + public byte PMTPointerField = 0; + public uint PMTOffset = 0; + public uint PMTSectionLengthParse = 0; + public ushort PMTSectionLength = 0; + public uint PMTSectionParse = 0; + public bool PMTTransferState = false; + public byte PMTSectionNumber = 0; + public byte PMTLastSectionNumber = 0; + + public byte PMTTemp = 0; + + public TSStream Stream = null; + public TSStreamState StreamState = null; + + public ulong TotalPackets = 0; + } + + public class TSStreamDiagnostics + { + public ulong Bytes = 0; + public ulong Packets = 0; + public double Marker = 0; + public double Interval = 0; + public string Tag = null; + } + + public class TSStreamFile + { + public FileSystemMetadata FileInfo = null; + public string Name = null; + public long Size = 0; + public double Length = 0; + + public TSInterleavedFile InterleavedFile = null; + + private Dictionary StreamStates = + new Dictionary(); + + public Dictionary Streams = + new Dictionary(); + + public Dictionary> StreamDiagnostics = + new Dictionary>(); + + private List Playlists = null; + + private readonly IFileSystem _fileSystem; + + public TSStreamFile(FileSystemMetadata fileInfo, IFileSystem fileSystem) + { + FileInfo = fileInfo; + _fileSystem = fileSystem; + Name = fileInfo.Name.ToUpper(); + } + + public string DisplayName + { + get + { + if (BDInfoSettings.EnableSSIF && + InterleavedFile != null) + { + return InterleavedFile.Name; + } + return Name; + } + } + + private bool ScanStream( + TSStream stream, + TSStreamState streamState, + TSStreamBuffer buffer) + { + streamState.StreamTag = null; + + long bitrate = 0; + if (stream.IsAudioStream && + streamState.PTSTransfer > 0) + { + bitrate = (long)Math.Round( + (buffer.TransferLength * 8.0) / + ((double)streamState.PTSTransfer / 90000)); + + if (bitrate > streamState.PeakTransferRate) + { + streamState.PeakTransferRate = bitrate; + } + } + if (buffer.TransferLength > streamState.PeakTransferLength) + { + streamState.PeakTransferLength = buffer.TransferLength; + } + + buffer.BeginRead(); + switch (stream.StreamType) + { + case TSStreamType.MPEG2_VIDEO: + TSCodecMPEG2.Scan( + (TSVideoStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.AVC_VIDEO: + TSCodecAVC.Scan( + (TSVideoStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.MVC_VIDEO: + TSCodecMVC.Scan( + (TSVideoStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.VC1_VIDEO: + TSCodecVC1.Scan( + (TSVideoStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.AC3_AUDIO: + TSCodecAC3.Scan( + (TSAudioStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + TSCodecAC3.Scan( + (TSAudioStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.AC3_TRUE_HD_AUDIO: + TSCodecTrueHD.Scan( + (TSAudioStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.LPCM_AUDIO: + TSCodecLPCM.Scan( + (TSAudioStream)stream, buffer, ref streamState.StreamTag); + break; + + case TSStreamType.DTS_AUDIO: + TSCodecDTS.Scan( + (TSAudioStream)stream, buffer, bitrate, ref streamState.StreamTag); + break; + + case TSStreamType.DTS_HD_AUDIO: + case TSStreamType.DTS_HD_MASTER_AUDIO: + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + TSCodecDTSHD.Scan( + (TSAudioStream)stream, buffer, bitrate, ref streamState.StreamTag); + break; + + default: + stream.IsInitialized = true; + break; + } + buffer.EndRead(); + streamState.StreamBuffer.Reset(); + + bool isAVC = false; + bool isMVC = false; + foreach (TSStream finishedStream in Streams.Values) + { + if (!finishedStream.IsInitialized) + { + return false; + } + if (finishedStream.StreamType == TSStreamType.AVC_VIDEO) + { + isAVC = true; + } + if (finishedStream.StreamType == TSStreamType.MVC_VIDEO) + { + isMVC = true; + } + } + if (isMVC && !isAVC) + { + return false; + } + return true; + } + + private void UpdateStreamBitrates( + ushort PTSPID, + ulong PTS, + ulong PTSDiff) + { + if (Playlists == null) return; + + foreach (ushort PID in StreamStates.Keys) + { + if (Streams.ContainsKey(PID) && + Streams[PID].IsVideoStream && + PID != PTSPID) + { + continue; + } + if (StreamStates[PID].WindowPackets == 0) + { + continue; + } + UpdateStreamBitrate(PID, PTSPID, PTS, PTSDiff); + } + + foreach (TSPlaylistFile playlist in Playlists) + { + double packetSeconds = 0; + foreach (TSStreamClip clip in playlist.StreamClips) + { + if (clip.AngleIndex == 0) + { + packetSeconds += clip.PacketSeconds; + } + } + if (packetSeconds > 0) + { + foreach (TSStream playlistStream in playlist.SortedStreams) + { + if (playlistStream.IsVBR) + { + playlistStream.BitRate = (long)Math.Round( + ((playlistStream.PayloadBytes * 8.0) / packetSeconds)); + + if (playlistStream.StreamType == TSStreamType.AC3_TRUE_HD_AUDIO && + ((TSAudioStream)playlistStream).CoreStream != null) + { + playlistStream.BitRate -= + ((TSAudioStream)playlistStream).CoreStream.BitRate; + } + } + } + } + } + } + + private void UpdateStreamBitrate( + ushort PID, + ushort PTSPID, + ulong PTS, + ulong PTSDiff) + { + if (Playlists == null) return; + + TSStreamState streamState = StreamStates[PID]; + double streamTime = (double)PTS / 90000; + double streamInterval = (double)PTSDiff / 90000; + double streamOffset = streamTime + streamInterval; + + foreach (TSPlaylistFile playlist in Playlists) + { + foreach (TSStreamClip clip in playlist.StreamClips) + { + if (clip.Name != this.Name) continue; + + if (streamTime == 0 || + (streamTime >= clip.TimeIn && + streamTime <= clip.TimeOut)) + { + clip.PayloadBytes += streamState.WindowBytes; + clip.PacketCount += streamState.WindowPackets; + + if (streamOffset > clip.TimeIn && + streamOffset - clip.TimeIn > clip.PacketSeconds) + { + clip.PacketSeconds = streamOffset - clip.TimeIn; + } + + Dictionary playlistStreams = playlist.Streams; + if (clip.AngleIndex > 0 && + clip.AngleIndex < playlist.AngleStreams.Count + 1) + { + playlistStreams = playlist.AngleStreams[clip.AngleIndex - 1]; + } + if (playlistStreams.ContainsKey(PID)) + { + TSStream stream = playlistStreams[PID]; + + stream.PayloadBytes += streamState.WindowBytes; + stream.PacketCount += streamState.WindowPackets; + + if (stream.IsVideoStream) + { + stream.PacketSeconds += streamInterval; + + stream.ActiveBitRate = (long)Math.Round( + ((stream.PayloadBytes * 8.0) / + stream.PacketSeconds)); + } + + if (stream.StreamType == TSStreamType.AC3_TRUE_HD_AUDIO && + ((TSAudioStream)stream).CoreStream != null) + { + stream.ActiveBitRate -= + ((TSAudioStream)stream).CoreStream.BitRate; + } + } + } + } + } + + if (Streams.ContainsKey(PID)) + { + TSStream stream = Streams[PID]; + stream.PayloadBytes += streamState.WindowBytes; + stream.PacketCount += streamState.WindowPackets; + + if (stream.IsVideoStream) + { + TSStreamDiagnostics diag = new TSStreamDiagnostics(); + diag.Marker = (double)PTS / 90000; + diag.Interval = (double)PTSDiff / 90000; + diag.Bytes = streamState.WindowBytes; + diag.Packets = streamState.WindowPackets; + diag.Tag = streamState.StreamTag; + StreamDiagnostics[PID].Add(diag); + + stream.PacketSeconds += streamInterval; + } + } + streamState.WindowPackets = 0; + streamState.WindowBytes = 0; + } + + public void Scan(List playlists, bool isFullScan) + { + if (playlists == null || playlists.Count == 0) + { + return; + } + + Playlists = playlists; + int dataSize = 16384; + Stream fileStream = null; + try + { + string fileName; + if (BDInfoSettings.EnableSSIF && + InterleavedFile != null) + { + fileName = InterleavedFile.FileInfo.FullName; + } + else + { + fileName = FileInfo.FullName; + } + fileStream = _fileSystem.GetFileStream( + fileName, + FileOpenMode.Open, + FileAccessMode.Read, + FileShareMode.Read, + false); + + Size = 0; + Length = 0; + + Streams.Clear(); + StreamStates.Clear(); + StreamDiagnostics.Clear(); + + TSPacketParser parser = + new TSPacketParser(); + + long fileLength = (uint)fileStream.Length; + byte[] buffer = new byte[dataSize]; + int bufferLength = 0; + while ((bufferLength = + fileStream.Read(buffer, 0, buffer.Length)) > 0) + { + int offset = 0; + for (int i = 0; i < bufferLength; i++) + { + if (parser.SyncState == false) + { + if (parser.TimeCodeParse > 0) + { + parser.TimeCodeParse--; + switch (parser.TimeCodeParse) + { + case 3: + parser.TimeCode = 0; + parser.TimeCode |= + ((uint)buffer[i] & 0x3F) << 24; + break; + case 2: + parser.TimeCode |= + ((uint)buffer[i] & 0xFF) << 16; + break; + case 1: + parser.TimeCode |= + ((uint)buffer[i] & 0xFF) << 8; + break; + case 0: + parser.TimeCode |= + ((uint)buffer[i] & 0xFF); + break; + } + } + else if (buffer[i] == 0x47) + { + parser.SyncState = true; + parser.PacketLength = 187; + parser.TimeCodeParse = 4; + parser.HeaderParse = 3; + } + } + else if (parser.HeaderParse > 0) + { + parser.PacketLength--; + parser.HeaderParse--; + + switch (parser.HeaderParse) + { + case 2: + { + parser.TransportErrorIndicator = + (byte)((buffer[i] >> 7) & 0x1); + parser.PayloadUnitStartIndicator = + (byte)((buffer[i] >> 6) & 0x1); + parser.TransportPriority = + (byte)((buffer[i] >> 5) & 0x1); + parser.PID = + (ushort)((buffer[i] & 0x1f) << 8); + } + break; + + case 1: + { + parser.PID |= (ushort)buffer[i]; + if (Streams.ContainsKey(parser.PID)) + { + parser.Stream = Streams[parser.PID]; + } + else + { + parser.Stream = null; + } + if (!StreamStates.ContainsKey(parser.PID)) + { + StreamStates[parser.PID] = new TSStreamState(); + } + parser.StreamState = StreamStates[parser.PID]; + parser.StreamState.TotalPackets++; + parser.StreamState.WindowPackets++; + parser.TotalPackets++; + } + break; + + case 0: + { + parser.TransportScramblingControl = + (byte)((buffer[i] >> 6) & 0x3); + parser.AdaptionFieldControl = + (byte)((buffer[i] >> 4) & 0x3); + + if ((parser.AdaptionFieldControl & 0x2) == 0x2) + { + parser.AdaptionFieldState = true; + } + if (parser.PayloadUnitStartIndicator == 1) + { + if (parser.PID == 0) + { + parser.PATSectionStart = true; + } + else if (parser.PID == parser.PMTPID) + { + parser.PMTSectionStart = true; + } + else if (parser.StreamState != null && + parser.StreamState.TransferState) + { + parser.StreamState.TransferState = false; + parser.StreamState.TransferCount++; + + bool isFinished = ScanStream( + parser.Stream, + parser.StreamState, + parser.StreamState.StreamBuffer); + + if (!isFullScan && isFinished) + { + return; + } + } + } + } + break; + } + } + else if (parser.AdaptionFieldState) + { + parser.PacketLength--; + parser.AdaptionFieldParse = buffer[i]; + parser.AdaptionFieldLength = buffer[i]; + parser.AdaptionFieldState = false; + } + else if (parser.AdaptionFieldParse > 0) + { + parser.PacketLength--; + parser.AdaptionFieldParse--; + if ((parser.AdaptionFieldLength - parser.AdaptionFieldParse) == 1) + { + if ((buffer[i] & 0x10) == 0x10) + { + parser.PCRParse = 6; + parser.PCR = 0; + } + } + else if (parser.PCRParse > 0) + { + parser.PCRParse--; + parser.PCR = (parser.PCR << 8) + (ulong)buffer[i]; + if (parser.PCRParse == 0) + { + parser.PreviousPCR = parser.PCR; + parser.PCR = (parser.PCR & 0x1FF) + + ((parser.PCR >> 15) * 300); + } + parser.PCRCount++; + } + if (parser.PacketLength == 0) + { + parser.SyncState = false; + } + } + else if (parser.PID == 0) + { + if (parser.PATTransferState) + { + if ((bufferLength - i) > parser.PATSectionLength) + { + offset = parser.PATSectionLength; + } + else + { + offset = (bufferLength - i); + } + if (parser.PacketLength <= offset) + { + offset = parser.PacketLength; + } + + for (int k = 0; k < offset; k++) + { + parser.PAT[parser.PATOffset++] = buffer[i++]; + parser.PATSectionLength--; + parser.PacketLength--; + } --i; + + if (parser.PATSectionLength == 0) + { + parser.PATTransferState = false; + if (parser.PATSectionNumber == parser.PATLastSectionNumber) + { + for (int k = 0; k < (parser.PATOffset - 4); k += 4) + { + uint programNumber = (uint) + ((parser.PAT[k] << 8) + + parser.PAT[k + 1]); + + ushort programPID = (ushort) + (((parser.PAT[k + 2] & 0x1F) << 8) + + parser.PAT[k + 3]); + + if (programNumber == 1) + { + parser.PMTPID = programPID; + } + } + } + } + } + else + { + --parser.PacketLength; + if (parser.PATSectionStart) + { + parser.PATPointerField = buffer[i]; + if (parser.PATPointerField == 0) + { + parser.PATSectionLengthParse = 3; + } + parser.PATSectionStart = false; + } + else if (parser.PATPointerField > 0) + { + --parser.PATPointerField; + if (parser.PATPointerField == 0) + { + parser.PATSectionLengthParse = 3; + } + } + else if (parser.PATSectionLengthParse > 0) + { + --parser.PATSectionLengthParse; + switch (parser.PATSectionLengthParse) + { + case 2: + break; + case 1: + parser.PATSectionLength = (ushort) + ((buffer[i] & 0xF) << 8); + break; + case 0: + parser.PATSectionLength |= buffer[i]; + if (parser.PATSectionLength > 1021) + { + parser.PATSectionLength = 0; + } + else + { + parser.PATSectionParse = 5; + } + break; + } + } + else if (parser.PATSectionParse > 0) + { + --parser.PATSectionLength; + --parser.PATSectionParse; + + switch (parser.PATSectionParse) + { + case 4: + parser.TransportStreamId = (ushort) + (buffer[i] << 8); + break; + case 3: + parser.TransportStreamId |= buffer[i]; + break; + case 2: + break; + case 1: + parser.PATSectionNumber = buffer[i]; + if (parser.PATSectionNumber == 0) + { + parser.PATOffset = 0; + } + break; + case 0: + parser.PATLastSectionNumber = buffer[i]; + parser.PATTransferState = true; + break; + } + } + } + if (parser.PacketLength == 0) + { + parser.SyncState = false; + } + } + else if (parser.PID == parser.PMTPID) + { + if (parser.PMTTransferState) + { + if ((bufferLength - i) >= parser.PMTSectionLength) + { + offset = parser.PMTSectionLength; + } + else + { + offset = (bufferLength - i); + } + if (parser.PacketLength <= offset) + { + offset = parser.PacketLength; + } + if (!parser.PMT.ContainsKey(parser.PID)) + { + parser.PMT[parser.PID] = new byte[1024]; + } + + byte[] PMT = parser.PMT[parser.PID]; + for (int k = 0; k < offset; k++) + { + PMT[parser.PMTOffset++] = buffer[i++]; + --parser.PMTSectionLength; + --parser.PacketLength; + } --i; + + if (parser.PMTSectionLength == 0) + { + parser.PMTTransferState = false; + if (parser.PMTSectionNumber == parser.PMTLastSectionNumber) + { + //Console.WriteLine("PMT Start: " + parser.PMTTemp); + try + { + for (int k = 0; k < (parser.PMTOffset - 4); k += 5) + { + byte streamType = PMT[k]; + + ushort streamPID = (ushort) + (((PMT[k + 1] & 0x1F) << 8) + + PMT[k + 2]); + + ushort streamInfoLength = (ushort) + (((PMT[k + 3] & 0xF) << 8) + + PMT[k + 4]); + + /* + if (streamInfoLength == 2) + { + // TODO: Cleanup + //streamInfoLength = 0; + } + + Console.WriteLine(string.Format( + "Type: {0} PID: {1} Length: {2}", + streamType, streamPID, streamInfoLength)); + */ + + if (!Streams.ContainsKey(streamPID)) + { + List streamDescriptors = + new List(); + + /* + * TODO: Getting bad streamInfoLength + if (streamInfoLength > 0) + { + for (int d = 0; d < streamInfoLength; d++) + { + byte name = PMT[k + d + 5]; + byte length = PMT[k + d + 6]; + TSDescriptor descriptor = + new TSDescriptor(name, length); + for (int v = 0; v < length; v++) + { + descriptor.Value[v] = + PMT[k + d + v + 7]; + } + streamDescriptors.Add(descriptor); + d += (length + 1); + } + } + */ + CreateStream(streamPID, streamType, streamDescriptors); + } + k += streamInfoLength; + } + } + catch (Exception ex) + { + // TODO + //Console.WriteLine(ex.Message); + } + } + } + } + else + { + --parser.PacketLength; + if (parser.PMTSectionStart) + { + parser.PMTPointerField = buffer[i]; + if (parser.PMTPointerField == 0) + { + parser.PMTSectionLengthParse = 3; + } + parser.PMTSectionStart = false; + } + else if (parser.PMTPointerField > 0) + { + --parser.PMTPointerField; + if (parser.PMTPointerField == 0) + { + parser.PMTSectionLengthParse = 3; + } + } + else if (parser.PMTSectionLengthParse > 0) + { + --parser.PMTSectionLengthParse; + switch (parser.PMTSectionLengthParse) + { + case 2: + if (buffer[i] != 0x2) + { + parser.PMTSectionLengthParse = 0; + } + break; + case 1: + parser.PMTSectionLength = (ushort) + ((buffer[i] & 0xF) << 8); + break; + case 0: + parser.PMTSectionLength |= buffer[i]; + if (parser.PMTSectionLength > 1021) + { + parser.PMTSectionLength = 0; + } + else + { + parser.PMTSectionParse = 9; + } + break; + } + } + else if (parser.PMTSectionParse > 0) + { + --parser.PMTSectionLength; + --parser.PMTSectionParse; + + switch (parser.PMTSectionParse) + { + case 8: + case 7: + break; + case 6: + parser.PMTTemp = buffer[i]; + break; + case 5: + parser.PMTSectionNumber = buffer[i]; + if (parser.PMTSectionNumber == 0) + { + parser.PMTOffset = 0; + } + break; + case 4: + parser.PMTLastSectionNumber = buffer[i]; + break; + case 3: + parser.PCRPID = (ushort) + ((buffer[i] & 0x1F) << 8); + break; + case 2: + parser.PCRPID |= buffer[i]; + break; + case 1: + parser.PMTProgramInfoLength = (ushort) + ((buffer[i] & 0xF) << 8); + break; + case 0: + parser.PMTProgramInfoLength |= buffer[i]; + if (parser.PMTProgramInfoLength == 0) + { + parser.PMTTransferState = true; + } + else + { + parser.PMTProgramDescriptorLengthParse = 2; + } + break; + } + } + else if (parser.PMTProgramInfoLength > 0) + { + --parser.PMTSectionLength; + --parser.PMTProgramInfoLength; + + if (parser.PMTProgramDescriptorLengthParse > 0) + { + --parser.PMTProgramDescriptorLengthParse; + switch (parser.PMTProgramDescriptorLengthParse) + { + case 1: + parser.PMTProgramDescriptor = buffer[i]; + break; + case 0: + parser.PMTProgramDescriptorLength = buffer[i]; + parser.PMTProgramDescriptors.Add( + new TSDescriptor( + parser.PMTProgramDescriptor, + parser.PMTProgramDescriptorLength)); + break; + } + } + else if (parser.PMTProgramDescriptorLength > 0) + { + --parser.PMTProgramDescriptorLength; + + TSDescriptor descriptor = parser.PMTProgramDescriptors[ + parser.PMTProgramDescriptors.Count - 1]; + + int valueIndex = + descriptor.Value.Length - + parser.PMTProgramDescriptorLength - 1; + + descriptor.Value[valueIndex] = buffer[i]; + + if (parser.PMTProgramDescriptorLength == 0 && + parser.PMTProgramInfoLength > 0) + { + parser.PMTProgramDescriptorLengthParse = 2; + } + } + if (parser.PMTProgramInfoLength == 0) + { + parser.PMTTransferState = true; + } + } + } + if (parser.PacketLength == 0) + { + parser.SyncState = false; + } + } + else if (parser.Stream != null && + parser.StreamState != null && + parser.TransportScramblingControl == 0) + { + TSStream stream = parser.Stream; + TSStreamState streamState = parser.StreamState; + + streamState.Parse = + (streamState.Parse << 8) + buffer[i]; + + if (streamState.TransferState) + { + if ((bufferLength - i) >= streamState.PacketLength && + streamState.PacketLength > 0) + { + offset = streamState.PacketLength; + } + else + { + offset = (bufferLength - i); + } + if (parser.PacketLength <= offset) + { + offset = parser.PacketLength; + } + streamState.TransferLength = offset; + + if (!stream.IsInitialized || + stream.IsVideoStream) + { + streamState.StreamBuffer.Add( + buffer, i, offset); + } + else + { + streamState.StreamBuffer.TransferLength += offset; + } + + i += (int)(streamState.TransferLength - 1); + streamState.PacketLength -= streamState.TransferLength; + parser.PacketLength -= (byte)streamState.TransferLength; + + streamState.TotalBytes += (ulong)streamState.TransferLength; + streamState.WindowBytes += (ulong)streamState.TransferLength; + + if (streamState.PacketLength == 0) + { + streamState.TransferState = false; + streamState.TransferCount++; + bool isFinished = ScanStream( + stream, + streamState, + streamState.StreamBuffer); + + if (!isFullScan && isFinished) + { + return; + } + } + } + else + { + --parser.PacketLength; + + bool headerFound = false; + if (stream.IsVideoStream && + streamState.Parse == 0x000001FD) + { + headerFound = true; + } + if (stream.IsVideoStream && + streamState.Parse >= 0x000001E0 && + streamState.Parse <= 0x000001EF) + { + headerFound = true; + } + if (stream.IsAudioStream && + streamState.Parse == 0x000001BD) + { + headerFound = true; + } + if (stream.IsAudioStream && + (streamState.Parse == 0x000001FA || + streamState.Parse == 0x000001FD)) + { + headerFound = true; + } + + if (!stream.IsVideoStream && + !stream.IsAudioStream && + (streamState.Parse == 0x000001FA || + streamState.Parse == 0x000001FD || + streamState.Parse == 0x000001BD || + (streamState.Parse >= 0x000001E0 && + streamState.Parse <= 0x000001EF))) + { + headerFound = true; + } + + if (headerFound) + { + streamState.PacketLengthParse = 2; +#if DEBUG + streamState.PESHeaderIndex = 0; + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)((streamState.Parse >> 24) & 0xFF); + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)((streamState.Parse >> 16) & 0xFF); + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)((streamState.Parse >> 8) & 0xFF); + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + } + else if (streamState.PacketLengthParse > 0) + { + --streamState.PacketLengthParse; + switch (streamState.PacketLengthParse) + { + case 1: +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 0: + streamState.PacketLength = + (int)(streamState.Parse & 0xFFFF); + streamState.PacketParse = 3; +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + } + } + else if (streamState.PacketParse > 0) + { + --streamState.PacketLength; + --streamState.PacketParse; + + switch (streamState.PacketParse) + { + case 2: +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 1: + streamState.PESHeaderFlags = + (byte)(streamState.Parse & 0xFF); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 0: + streamState.PESHeaderLength = + (byte)(streamState.Parse & 0xFF); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + if ((streamState.PESHeaderFlags & 0xC0) == 0x80) + { + streamState.PTSParse = 5; + } + else if ((streamState.PESHeaderFlags & 0xC0) == 0xC0) + { + streamState.DTSParse = 10; + } + if (streamState.PESHeaderLength == 0) + { + streamState.TransferState = true; + } + break; + } + } + else if (streamState.PTSParse > 0) + { + --streamState.PacketLength; + --streamState.PESHeaderLength; + --streamState.PTSParse; + + switch (streamState.PTSParse) + { + case 4: + streamState.PTSTemp = + ((streamState.Parse & 0xE) << 29); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + break; + + case 3: + streamState.PTSTemp |= + ((streamState.Parse & 0xFF) << 22); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 2: + streamState.PTSTemp |= + ((streamState.Parse & 0xFE) << 14); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 1: + streamState.PTSTemp |= + ((streamState.Parse & 0xFF) << 7); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 0: + streamState.PTSTemp |= + ((streamState.Parse & 0xFE) >> 1); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + streamState.PTS = streamState.PTSTemp; + + if (streamState.PTS > streamState.PTSLast) + { + if (streamState.PTSLast > 0) + { + streamState.PTSTransfer = (streamState.PTS - streamState.PTSLast); + } + streamState.PTSLast = streamState.PTS; + } + + streamState.PTSDiff = streamState.PTS - streamState.DTSPrev; + + if (streamState.PTSCount > 0 && + stream.IsVideoStream) + { + UpdateStreamBitrates(stream.PID, streamState.PTS, streamState.PTSDiff); + if (streamState.DTSTemp < parser.PTSFirst) + { + parser.PTSFirst = streamState.DTSTemp; + } + if (streamState.DTSTemp > parser.PTSLast) + { + parser.PTSLast = streamState.DTSTemp; + } + Length = (double)(parser.PTSLast - parser.PTSFirst) / 90000; + } + + streamState.DTSPrev = streamState.PTS; + streamState.PTSCount++; + if (streamState.PESHeaderLength == 0) + { + streamState.TransferState = true; + } + break; + } + } + else if (streamState.DTSParse > 0) + { + --streamState.PacketLength; + --streamState.PESHeaderLength; + --streamState.DTSParse; + + switch (streamState.DTSParse) + { + case 9: + streamState.PTSTemp = + ((streamState.Parse & 0xE) << 29); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 8: + streamState.PTSTemp |= + ((streamState.Parse & 0xFF) << 22); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 7: + streamState.PTSTemp |= + ((streamState.Parse & 0xFE) << 14); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + break; + + case 6: + streamState.PTSTemp |= + ((streamState.Parse & 0xFF) << 7); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 5: + streamState.PTSTemp |= + ((streamState.Parse & 0xFE) >> 1); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + streamState.PTS = streamState.PTSTemp; + if (streamState.PTS > streamState.PTSLast) + { + streamState.PTSLast = streamState.PTS; + } + break; + + case 4: + streamState.DTSTemp = + ((streamState.Parse & 0xE) << 29); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + break; + + case 3: + streamState.DTSTemp |= + ((streamState.Parse & 0xFF) << 22); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + break; + + case 2: + streamState.DTSTemp |= + ((streamState.Parse & 0xFE) << 14); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + break; + + case 1: + streamState.DTSTemp |= + ((streamState.Parse & 0xFF) << 7); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + break; + + case 0: + streamState.DTSTemp |= + ((streamState.Parse & 0xFE) >> 1); +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xff); +#endif + streamState.PTSDiff = streamState.DTSTemp - streamState.DTSPrev; + + if (streamState.PTSCount > 0 && + stream.IsVideoStream) + { + UpdateStreamBitrates(stream.PID, streamState.DTSTemp, streamState.PTSDiff); + if (streamState.DTSTemp < parser.PTSFirst) + { + parser.PTSFirst = streamState.DTSTemp; + } + if (streamState.DTSTemp > parser.PTSLast) + { + parser.PTSLast = streamState.DTSTemp; + } + Length = (double)(parser.PTSLast - parser.PTSFirst) / 90000; + } + streamState.DTSPrev = streamState.DTSTemp; + streamState.PTSCount++; + if (streamState.PESHeaderLength == 0) + { + streamState.TransferState = true; + } + break; + } + } + else if (streamState.PESHeaderLength > 0) + { + --streamState.PacketLength; + --streamState.PESHeaderLength; +#if DEBUG + streamState.PESHeader[streamState.PESHeaderIndex++] = + (byte)(streamState.Parse & 0xFF); +#endif + if (streamState.PESHeaderLength == 0) + { + streamState.TransferState = true; + } + } + } + if (parser.PacketLength == 0) + { + parser.SyncState = false; + } + } + else + { + parser.PacketLength--; + if ((bufferLength - i) >= parser.PacketLength) + { + i = i + parser.PacketLength; + parser.PacketLength = 0; + } + else + { + parser.PacketLength -= (byte)((bufferLength - i) + 1); + i = bufferLength; + } + if (parser.PacketLength == 0) + { + parser.SyncState = false; + } + } + } + Size += bufferLength; + } + + ulong PTSLast = 0; + ulong PTSDiff = 0; + foreach (TSStream stream in Streams.Values) + { + if (!stream.IsVideoStream) continue; + + if (StreamStates.ContainsKey(stream.PID) && + StreamStates[stream.PID].PTSLast > PTSLast) + { + PTSLast = StreamStates[stream.PID].PTSLast; + PTSDiff = PTSLast - StreamStates[stream.PID].DTSPrev; + } + UpdateStreamBitrates(stream.PID, PTSLast, PTSDiff); + } + } + finally + { + if (fileStream != null) + { + fileStream.Dispose(); + } + } + } + + private TSStream CreateStream( + ushort streamPID, + byte streamType, + List streamDescriptors) + { + TSStream stream = null; + + switch ((TSStreamType)streamType) + { + case TSStreamType.MVC_VIDEO: + case TSStreamType.AVC_VIDEO: + case TSStreamType.MPEG1_VIDEO: + case TSStreamType.MPEG2_VIDEO: + case TSStreamType.VC1_VIDEO: + { + stream = new TSVideoStream(); + } + break; + + case TSStreamType.AC3_AUDIO: + case TSStreamType.AC3_PLUS_AUDIO: + case TSStreamType.AC3_PLUS_SECONDARY_AUDIO: + case TSStreamType.AC3_TRUE_HD_AUDIO: + case TSStreamType.DTS_AUDIO: + case TSStreamType.DTS_HD_AUDIO: + case TSStreamType.DTS_HD_MASTER_AUDIO: + case TSStreamType.DTS_HD_SECONDARY_AUDIO: + case TSStreamType.LPCM_AUDIO: + case TSStreamType.MPEG1_AUDIO: + case TSStreamType.MPEG2_AUDIO: + { + stream = new TSAudioStream(); + } + break; + + case TSStreamType.INTERACTIVE_GRAPHICS: + case TSStreamType.PRESENTATION_GRAPHICS: + { + stream = new TSGraphicsStream(); + } + break; + + case TSStreamType.SUBTITLE: + { + stream = new TSTextStream(); + } + break; + + default: + break; + } + + if (stream != null && + !Streams.ContainsKey(streamPID)) + { + stream.PID = streamPID; + stream.StreamType = (TSStreamType)streamType; + stream.Descriptors = streamDescriptors; + Streams[stream.PID] = stream; + } + if (!StreamDiagnostics.ContainsKey(streamPID)) + { + StreamDiagnostics[streamPID] = + new List(); + } + + return stream; + } + } +} diff --git a/BDInfo/project.json b/BDInfo/project.json new file mode 100644 index 0000000000..fbbe9eaf32 --- /dev/null +++ b/BDInfo/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/DvdLib/BigEndianBinaryReader.cs b/DvdLib/BigEndianBinaryReader.cs new file mode 100644 index 0000000000..56d9db8255 --- /dev/null +++ b/DvdLib/BigEndianBinaryReader.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace DvdLib +{ + public class BigEndianBinaryReader : BinaryReader + { + public BigEndianBinaryReader(Stream input) + : base(input) + { + } + + public override ushort ReadUInt16() + { + return BitConverter.ToUInt16(ReadAndReverseBytes(2), 0); + } + + public override uint ReadUInt32() + { + return BitConverter.ToUInt32(ReadAndReverseBytes(4), 0); + } + + private byte[] ReadAndReverseBytes(int count) + { + byte[] val = base.ReadBytes(count); + Array.Reverse(val, 0, count); + return val; + } + } +} diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj new file mode 100644 index 0000000000..ba63e77f0f --- /dev/null +++ b/DvdLib/DvdLib.csproj @@ -0,0 +1,71 @@ + + + + + 11.0 + Debug + AnyCPU + {713F42B5-878E-499D-A878-E4C652B1D5E8} + Library + Properties + DvdLib + DvdLib + en-US + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + + + \ No newline at end of file diff --git a/DvdLib/DvdLib.nuget.targets b/DvdLib/DvdLib.nuget.targets new file mode 100644 index 0000000000..e69ce0e64f --- /dev/null +++ b/DvdLib/DvdLib.nuget.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/DvdLib/Ifo/AudioAttributes.cs b/DvdLib/Ifo/AudioAttributes.cs new file mode 100644 index 0000000000..5b3b9fd9a0 --- /dev/null +++ b/DvdLib/Ifo/AudioAttributes.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DvdLib.Ifo +{ + public enum AudioCodec + { + AC3 = 0, + MPEG1 = 2, + MPEG2ext = 3, + LPCM = 4, + DTS = 6, + } + + public enum ApplicationMode + { + Unspecified = 0, + Karaoke = 1, + Surround = 2, + } + + public class AudioAttributes + { + public readonly AudioCodec Codec; + public readonly bool MultichannelExtensionPresent; + public readonly ApplicationMode Mode; + public readonly byte QuantDRC; + public readonly byte SampleRate; + public readonly byte Channels; + public readonly ushort LanguageCode; + public readonly byte LanguageExtension; + public readonly byte CodeExtension; + } + + public class MultiChannelExtension + { + + } +} diff --git a/DvdLib/Ifo/Cell.cs b/DvdLib/Ifo/Cell.cs new file mode 100644 index 0000000000..d0f442e362 --- /dev/null +++ b/DvdLib/Ifo/Cell.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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..ae3883eaa0 --- /dev/null +++ b/DvdLib/Ifo/CellPlaybackInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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..2e07159402 --- /dev/null +++ b/DvdLib/Ifo/CellPositionInfo.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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..802c6ce62a --- /dev/null +++ b/DvdLib/Ifo/Chapter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +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..508c23db45 --- /dev/null +++ b/DvdLib/Ifo/Dvd.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Diagnostics; +using MediaBrowser.Model.IO; + +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>(); + private readonly IFileSystem _fileSystem; + + public Dvd(string path, IFileSystem fileSystem) + { + _fileSystem = fileSystem; + Titles = new List<Title>(); + var allFiles = _fileSystem.GetFiles(path, true).ToList(); + + 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) + { + var allIfos = allFiles.Where(i => string.Equals(i.Extension, ".ifo", StringComparison.OrdinalIgnoreCase)); + + foreach (var ifo in allIfos) + { + var num = ifo.Name.Split('_').ElementAtOrDefault(1); + ushort ifoNumber; + var numbersRead = new List<ushort>(); + + if (!string.IsNullOrEmpty(num) && ushort.TryParse(num, out ifoNumber) && !numbersRead.Contains(ifoNumber)) + { + ReadVTS(ifoNumber, ifo.FullName); + numbersRead.Add(ifoNumber); + } + } + } + else + { + using (var vmgFs = _fileSystem.GetFileStream(vmgPath.FullName, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + { + using (BigEndianBinaryReader 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++) + { + Title t = new Title(titleNum); + t.ParseTT_SRPT(read); + Titles.Add(t); + } + } + + private void ReadVTS(ushort vtsNum, List<FileSystemMetadata> allFiles) + { + var filename = String.Format("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 = _fileSystem.GetFileStream(vtsPath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read)) + { + using (BigEndianBinaryReader 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); + Title 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(); + + Title t = Titles.FirstOrDefault(vtst => vtst.IsVTSTitle(vtsNum, titleNum)); + if (t != null) t.AddPgc(vtsRead, startByte + vtsPgcOffset, entryPgc, pgcNum); + } + } + } + } + } +} \ No newline at end of file diff --git a/DvdLib/Ifo/DvdTime.cs b/DvdLib/Ifo/DvdTime.cs new file mode 100644 index 0000000000..f565f5fdf7 --- /dev/null +++ b/DvdLib/Ifo/DvdTime.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +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 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/PgcCommandTable.cs b/DvdLib/Ifo/PgcCommandTable.cs new file mode 100644 index 0000000000..2ead78cef9 --- /dev/null +++ b/DvdLib/Ifo/PgcCommandTable.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DvdLib.Ifo +{ + public class ProgramChainCommandTable + { + public readonly ushort LastByteAddress; + public readonly List<VirtualMachineCommand> PreCommands; + public readonly List<VirtualMachineCommand> PostCommands; + public readonly List<VirtualMachineCommand> CellCommands; + } + + public class VirtualMachineCommand + { + public readonly byte[] Command; + } +} diff --git a/DvdLib/Ifo/Program.cs b/DvdLib/Ifo/Program.cs new file mode 100644 index 0000000000..48870d9dd0 --- /dev/null +++ b/DvdLib/Ifo/Program.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DvdLib.Ifo +{ + public class Program + { + public readonly List<Cell> Cells; + + 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..3179f73cd6 --- /dev/null +++ b/DvdLib/Ifo/ProgramChain.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace DvdLib.Ifo +{ + public enum ProgramPlaybackMode + { + Sequential, + Random, + Shuffle + } + + public class ProgramChain + { + private ushort _unknown1; + + 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; + public readonly ProgramChain Next; + + private ushort _prevProgramNumber; + public readonly ProgramChain Previous; + + private ushort _goupProgramNumber; + public readonly ProgramChain Goup; // ?? maybe Group + + private byte _playbackMode; + 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; + public readonly ProgramChainCommandTable CommandTable; + + 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++) + { + Cell 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); + List<int> 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..70deb45bfa --- /dev/null +++ b/DvdLib/Ifo/Title.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +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); + + ProgramChain 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..c3cffd4870 --- /dev/null +++ b/DvdLib/Ifo/UserOperation.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +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/Ifo/VideoAttributes.cs b/DvdLib/Ifo/VideoAttributes.cs new file mode 100644 index 0000000000..b2d3759426 --- /dev/null +++ b/DvdLib/Ifo/VideoAttributes.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace DvdLib.Ifo +{ + public enum VideoCodec + { + MPEG1 = 0, + MPEG2 = 1, + } + + public enum VideoFormat + { + NTSC = 0, + PAL = 1, + } + + public enum AspectRatio + { + ar4to3 = 0, + ar16to9 = 3 + } + + public enum FilmMode + { + None = -1, + Camera = 0, + Film = 1, + } + + public class VideoAttributes + { + public readonly VideoCodec Codec; + public readonly VideoFormat Format; + public readonly AspectRatio Aspect; + public readonly bool AutomaticPanScan; + public readonly bool AutomaticLetterBox; + public readonly bool Line21CCField1; + public readonly bool Line21CCField2; + public readonly int Width; + public readonly int Height; + public readonly bool Letterboxed; + public readonly FilmMode FilmMode; + + public VideoAttributes() + { + } + } +} diff --git a/DvdLib/Properties/AssemblyInfo.cs b/DvdLib/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..cca792684f --- /dev/null +++ b/DvdLib/Properties/AssemblyInfo.cs @@ -0,0 +1,29 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +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("")] +[assembly: AssemblyProduct("DvdLib")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.1")] \ No newline at end of file diff --git a/DvdLib/project.json b/DvdLib/project.json new file mode 100644 index 0000000000..fbbe9eaf32 --- /dev/null +++ b/DvdLib/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Common.Implementations/Archiving/ZipClient.cs b/Emby.Common.Implementations/Archiving/ZipClient.cs similarity index 84% rename from MediaBrowser.Common.Implementations/Archiving/ZipClient.cs rename to Emby.Common.Implementations/Archiving/ZipClient.cs index 1272e43b90..791c6678cd 100644 --- a/MediaBrowser.Common.Implementations/Archiving/ZipClient.cs +++ b/Emby.Common.Implementations/Archiving/ZipClient.cs @@ -1,21 +1,20 @@ -using MediaBrowser.Model.IO; -using SharpCompress.Archive.Rar; -using SharpCompress.Archive.SevenZip; -using SharpCompress.Archive.Tar; +using System.IO; +using MediaBrowser.Model.IO; +using SharpCompress.Archives.Rar; +using SharpCompress.Archives.SevenZip; +using SharpCompress.Archives.Tar; using SharpCompress.Common; -using SharpCompress.Reader; -using SharpCompress.Reader.Zip; -using System.IO; -using CommonIO; +using SharpCompress.Readers; +using SharpCompress.Readers.Zip; -namespace MediaBrowser.Common.Implementations.Archiving +namespace Emby.Common.Implementations.Archiving { /// <summary> /// Class DotNetZipClient /// </summary> public class ZipClient : IZipClient { - private IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; public ZipClient(IFileSystem fileSystem) { @@ -46,11 +45,12 @@ namespace MediaBrowser.Common.Implementations.Archiving { using (var reader = ReaderFactory.Open(source)) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -61,11 +61,12 @@ namespace MediaBrowser.Common.Implementations.Archiving { using (var reader = ZipReader.Open(source)) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -98,11 +99,12 @@ namespace MediaBrowser.Common.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -137,11 +139,12 @@ namespace MediaBrowser.Common.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); @@ -175,11 +178,12 @@ namespace MediaBrowser.Common.Implementations.Archiving { using (var reader = archive.ExtractAllEntries()) { - var options = ExtractOptions.ExtractFullPath; + var options = new ExtractionOptions(); + options.ExtractFullPath = true; if (overwriteExistingFiles) { - options = options | ExtractOptions.Overwrite; + options.Overwrite = true; } reader.WriteAllToDirectory(targetPath, options); diff --git a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs b/Emby.Common.Implementations/BaseApplicationHost.cs similarity index 77% rename from MediaBrowser.Common.Implementations/BaseApplicationHost.cs rename to Emby.Common.Implementations/BaseApplicationHost.cs index e68fee829d..02d7cb31fd 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationHost.cs +++ b/Emby.Common.Implementations/BaseApplicationHost.cs @@ -1,16 +1,12 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Implementations.Archiving; -using MediaBrowser.Common.Implementations.Devices; -using MediaBrowser.Common.Implementations.IO; -using MediaBrowser.Common.Implementations.ScheduledTasks; -using MediaBrowser.Common.Implementations.Security; -using MediaBrowser.Common.Implementations.Serialization; -using MediaBrowser.Common.Implementations.Updates; +using Emby.Common.Implementations.Devices; +using Emby.Common.Implementations.IO; +using Emby.Common.Implementations.ScheduledTasks; +using Emby.Common.Implementations.Serialization; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Progress; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Security; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Events; @@ -18,27 +14,42 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Updates; -using ServiceStack; -using SimpleInjector; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.Extensions; +using Emby.Common.Implementations.Cryptography; +using Emby.Common.Implementations.Diagnostics; +using Emby.Common.Implementations.Net; +using Emby.Common.Implementations.EnvironmentInfo; +using Emby.Common.Implementations.Threading; +using MediaBrowser.Common; using MediaBrowser.Common.IO; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Diagnostics; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Common.Implementations +#if NETSTANDARD1_6 +using System.Runtime.Loader; +#endif + +namespace Emby.Common.Implementations { /// <summary> /// Class BaseApplicationHost /// </summary> /// <typeparam name="TApplicationPathsType">The type of the T application paths type.</typeparam> - public abstract class BaseApplicationHost<TApplicationPathsType> : IApplicationHost, IDependencyContainer + public abstract class BaseApplicationHost<TApplicationPathsType> : IApplicationHost where TApplicationPathsType : class, IApplicationPaths { /// <summary> @@ -67,7 +78,7 @@ namespace MediaBrowser.Common.Implementations /// Gets or sets the plugins. /// </summary> /// <value>The plugins.</value> - public IEnumerable<IPlugin> Plugins { get; protected set; } + public IPlugin[] Plugins { get; protected set; } /// <summary> /// Gets or sets the log manager. @@ -81,11 +92,6 @@ namespace MediaBrowser.Common.Implementations /// <value>The application paths.</value> protected TApplicationPathsType ApplicationPaths { get; private set; } - /// <summary> - /// The container - /// </summary> - protected readonly Container Container = new Container(); - /// <summary> /// The json serializer /// </summary> @@ -125,11 +131,6 @@ namespace MediaBrowser.Common.Implementations /// <value>The kernel.</value> protected ITaskManager TaskManager { get; private set; } /// <summary> - /// Gets the security manager. - /// </summary> - /// <value>The security manager.</value> - protected ISecurityManager SecurityManager { get; private set; } - /// <summary> /// Gets the HTTP client. /// </summary> /// <value>The HTTP client.</value> @@ -146,22 +147,14 @@ namespace MediaBrowser.Common.Implementations /// <value>The configuration manager.</value> protected IConfigurationManager ConfigurationManager { get; private set; } - /// <summary> - /// Gets or sets the installation manager. - /// </summary> - /// <value>The installation manager.</value> - protected IInstallationManager InstallationManager { get; private set; } - protected IFileSystem FileSystemManager { get; private set; } - /// <summary> - /// Gets or sets the zip client. - /// </summary> - /// <value>The zip client.</value> - protected IZipClient ZipClient { get; private set; } - protected IIsoManager IsoManager { get; private set; } + protected IProcessFactory ProcessFactory { get; private set; } + protected ITimerFactory TimerFactory { get; private set; } + protected ISocketFactory SocketFactory { get; private set; } + /// <summary> /// Gets the name. /// </summary> @@ -174,6 +167,10 @@ namespace MediaBrowser.Common.Implementations /// <value><c>true</c> if this instance is running as service; otherwise, <c>false</c>.</value> public abstract bool IsRunningAsService { get; } + protected ICryptoProvider CryptographyProvider = new CryptographyProvider(); + + protected IEnvironmentInfo EnvironmentInfo { get; private set; } + private DeviceId _deviceId; public string SystemId { @@ -183,26 +180,44 @@ namespace MediaBrowser.Common.Implementations { _deviceId = new DeviceId(ApplicationPaths, LogManager.GetLogger("SystemId"), FileSystemManager); } - + return _deviceId.Value; } } public virtual string OperatingSystemDisplayName { - get { return Environment.OSVersion.VersionString; } + get { return EnvironmentInfo.OperatingSystemName; } } - public IMemoryStreamProvider MemoryStreamProvider { get; set; } + /// <summary> + /// The container + /// </summary> + protected readonly SimpleInjector.Container Container = new SimpleInjector.Container(); + + protected ISystemEvents SystemEvents { get; private set; } + protected IMemoryStreamFactory MemoryStreamFactory { get; private set; } /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationHost{TApplicationPathsType}"/> class. /// </summary> - protected BaseApplicationHost(TApplicationPathsType applicationPaths, - ILogManager logManager, - IFileSystem fileSystem) + protected BaseApplicationHost(TApplicationPathsType applicationPaths, + ILogManager logManager, + IFileSystem fileSystem, + IEnvironmentInfo environmentInfo, + ISystemEvents systemEvents, + IMemoryStreamFactory memoryStreamFactory, + INetworkManager networkManager) { - XmlSerializer = new XmlSerializer (fileSystem, logManager.GetLogger("XmlSerializer")); + NetworkManager = networkManager; + EnvironmentInfo = environmentInfo; + SystemEvents = systemEvents; + MemoryStreamFactory = memoryStreamFactory; + + // hack alert, until common can target .net core + BaseExtensions.CryptographyProvider = CryptographyProvider; + + XmlSerializer = new MyXmlSerializer(fileSystem, logManager.GetLogger("XmlSerializer")); FailedAssemblies = new List<string>(); ApplicationPaths = applicationPaths; @@ -221,28 +236,10 @@ namespace MediaBrowser.Common.Implementations /// <returns>Task.</returns> public virtual async Task Init(IProgress<double> progress) { - try - { - // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 - Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); - } - catch - { - // Failing under mono - } progress.Report(1); JsonSerializer = CreateJsonSerializer(); - if (Environment.OSVersion.Platform == PlatformID.Win32NT) - { - MemoryStreamProvider = new RecyclableMemoryStreamProvider(); - } - else - { - MemoryStreamProvider = new MemoryStreamProvider(); - } - OnLoggerLoaded(true); LogManager.LoggerLoaded += (s, e) => OnLoggerLoaded(false); @@ -310,11 +307,10 @@ namespace MediaBrowser.Common.Implementations builder.AppendLine(string.Format("Command line: {0}", string.Join(" ", Environment.GetCommandLineArgs()))); +#if NET46 builder.AppendLine(string.Format("Operating system: {0}", Environment.OSVersion)); - builder.AppendLine(string.Format("Processor count: {0}", Environment.ProcessorCount)); builder.AppendLine(string.Format("64-Bit OS: {0}", Environment.Is64BitOperatingSystem)); builder.AppendLine(string.Format("64-Bit Process: {0}", Environment.Is64BitProcess)); - builder.AppendLine(string.Format("Program data path: {0}", appPaths.ProgramDataPath)); Type type = Type.GetType("Mono.Runtime"); if (type != null) @@ -325,23 +321,25 @@ namespace MediaBrowser.Common.Implementations builder.AppendLine("Mono: " + displayName.Invoke(null, null)); } } +#endif - builder.AppendLine(string.Format("Application Path: {0}", appPaths.ApplicationPath)); + builder.AppendLine(string.Format("Processor count: {0}", Environment.ProcessorCount)); + builder.AppendLine(string.Format("Program data path: {0}", appPaths.ProgramDataPath)); + builder.AppendLine(string.Format("Application directory: {0}", appPaths.ProgramSystemPath)); return builder; } - protected virtual IJsonSerializer CreateJsonSerializer() - { - return new JsonSerializer(FileSystemManager, LogManager.GetLogger("JsonSerializer")); - } + protected abstract IJsonSerializer CreateJsonSerializer(); private void SetHttpLimit() { try { // Increase the max http request limit +#if NET46 ServicePointManager.DefaultConnectionLimit = Math.Max(96, ServicePointManager.DefaultConnectionLimit); +#endif } catch (Exception ex) { @@ -386,13 +384,13 @@ namespace MediaBrowser.Common.Implementations /// <returns>Task.</returns> public virtual Task RunStartupTasks() { - Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); + Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); - ConfigureAutorun (); + ConfigureAutorun(); - ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; + ConfigurationManager.ConfigurationUpdated += OnConfigurationUpdated; - return Task.FromResult (true); + return Task.FromResult(true); } /// <summary> @@ -427,10 +425,56 @@ namespace MediaBrowser.Common.Implementations /// </summary> protected virtual void FindParts() { - RegisterModules(); - ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - Plugins = GetExports<IPlugin>(); + Plugins = GetExports<IPlugin>().Select(LoadPlugin).Where(i => i != null).ToArray(); + } + + private IPlugin LoadPlugin(IPlugin plugin) + { + try + { + var assemblyPlugin = plugin as IPluginAssembly; + + if (assemblyPlugin != null) + { +#if NET46 + var assembly = plugin.GetType().Assembly; + var assemblyName = assembly.GetName(); + + var attribute = (GuidAttribute)assembly.GetCustomAttributes(typeof(GuidAttribute), true)[0]; + var assemblyId = new Guid(attribute.Value); + + var assemblyFileName = assemblyName.Name + ".dll"; + var assemblyFilePath = Path.Combine(ApplicationPaths.PluginsPath, assemblyFileName); + + assemblyPlugin.SetAttributes(assemblyFilePath, assemblyFileName, assemblyName.Version, assemblyId); +#elif NETSTANDARD1_6 + var typeInfo = plugin.GetType().GetTypeInfo(); + var assembly = typeInfo.Assembly; + var assemblyName = assembly.GetName(); + + var attribute = (GuidAttribute)assembly.GetCustomAttribute(typeof(GuidAttribute)); + var assemblyId = new Guid(attribute.Value); + + var assemblyFileName = assemblyName.Name + ".dll"; + var assemblyFilePath = Path.Combine(ApplicationPaths.PluginsPath, assemblyFileName); + + assemblyPlugin.SetAttributes(assemblyFilePath, assemblyFileName, assemblyName.Version, assemblyId); +#else +return null; +#endif + } + + var isFirstRun = !File.Exists(plugin.ConfigurationFilePath); + plugin.SetStartupInfo(isFirstRun, File.GetLastWriteTimeUtc, s => Directory.CreateDirectory(s)); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading plugin {0}", ex, plugin.GetType().FullName); + return null; + } + + return plugin; } /// <summary> @@ -449,7 +493,17 @@ namespace MediaBrowser.Common.Implementations AllConcreteTypes = assemblies .SelectMany(GetTypes) - .Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType) + .Where(t => + { +#if NET46 + return t.IsClass && !t.IsAbstract && !t.IsInterface && !t.IsGenericType; +#endif +#if NETSTANDARD1_6 + var typeInfo = t.GetTypeInfo(); + return typeInfo.IsClass && !typeInfo.IsAbstract && !typeInfo.IsInterface && !typeInfo.IsGenericType; +#endif + return false; + }) .ToArray(); } @@ -459,62 +513,46 @@ namespace MediaBrowser.Common.Implementations /// <returns>Task.</returns> protected virtual Task RegisterResources(IProgress<double> progress) { - RegisterSingleInstance(ConfigurationManager); - RegisterSingleInstance<IApplicationHost>(this); + RegisterSingleInstance(ConfigurationManager); + RegisterSingleInstance<IApplicationHost>(this); - RegisterSingleInstance<IApplicationPaths>(ApplicationPaths); + RegisterSingleInstance<IApplicationPaths>(ApplicationPaths); - TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LogManager.GetLogger("TaskManager"), FileSystemManager); + TaskManager = new TaskManager(ApplicationPaths, JsonSerializer, LogManager.GetLogger("TaskManager"), FileSystemManager, SystemEvents); - RegisterSingleInstance(JsonSerializer); - RegisterSingleInstance(XmlSerializer); - RegisterSingleInstance(MemoryStreamProvider); + RegisterSingleInstance(JsonSerializer); + RegisterSingleInstance(XmlSerializer); + RegisterSingleInstance(MemoryStreamFactory); + RegisterSingleInstance(SystemEvents); - RegisterSingleInstance(LogManager); - RegisterSingleInstance(Logger); + RegisterSingleInstance(LogManager); + RegisterSingleInstance(Logger); - RegisterSingleInstance(TaskManager); + RegisterSingleInstance(TaskManager); + RegisterSingleInstance(EnvironmentInfo); - RegisterSingleInstance(FileSystemManager); + RegisterSingleInstance(FileSystemManager); - HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamProvider); - RegisterSingleInstance(HttpClient); + HttpClient = new HttpClientManager.HttpClientManager(ApplicationPaths, LogManager.GetLogger("HttpClient"), FileSystemManager, MemoryStreamFactory); + RegisterSingleInstance(HttpClient); - NetworkManager = CreateNetworkManager(LogManager.GetLogger("NetworkManager")); - RegisterSingleInstance(NetworkManager); + RegisterSingleInstance(NetworkManager); - SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager); - RegisterSingleInstance(SecurityManager); + IsoManager = new IsoManager(); + RegisterSingleInstance(IsoManager); - InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager); - RegisterSingleInstance(InstallationManager); + ProcessFactory = new ProcessFactory(); + RegisterSingleInstance(ProcessFactory); - ZipClient = new ZipClient(FileSystemManager); - RegisterSingleInstance(ZipClient); + TimerFactory = new TimerFactory(); + RegisterSingleInstance(TimerFactory); - IsoManager = new IsoManager(); - RegisterSingleInstance(IsoManager); + SocketFactory = new SocketFactory(LogManager.GetLogger("SocketFactory")); + RegisterSingleInstance(SocketFactory); - return Task.FromResult (true); - } + RegisterSingleInstance(CryptographyProvider); - private void RegisterModules() - { - var moduleTypes = GetExportTypes<IDependencyModule>(); - - foreach (var type in moduleTypes) - { - try - { - var instance = Activator.CreateInstance(type) as IDependencyModule; - if (instance != null) - instance.BindDependencies(this); - } - catch (Exception ex) - { - Logger.ErrorException("Error setting up dependency bindings for " + type.Name, ex); - } - } + return Task.FromResult(true); } /// <summary> @@ -544,14 +582,12 @@ namespace MediaBrowser.Common.Implementations Logger.Error("LoaderException: " + loaderException.Message); } } - + // If it fails we can still get a list of the Types it was able to resolve return ex.Types.Where(t => t != null); } } - protected abstract INetworkManager CreateNetworkManager(ILogger logger); - /// <summary> /// Creates an instance of type and resolves all constructor dependancies /// </summary> @@ -565,7 +601,7 @@ namespace MediaBrowser.Common.Implementations } catch (Exception ex) { - Logger.ErrorException("Error creating {0}", ex, type.Name); + Logger.ErrorException("Error creating {0}", ex, type.FullName); throw; } @@ -584,17 +620,12 @@ namespace MediaBrowser.Common.Implementations } catch (Exception ex) { - Logger.ErrorException("Error creating {0}", ex, type.Name); + Logger.ErrorException("Error creating {0}", ex, type.FullName); // Don't blow up in release mode return null; } } - void IDependencyContainer.RegisterSingleInstance<T>(T obj, bool manageLifetime) - { - RegisterSingleInstance(obj, manageLifetime); - } - /// <summary> /// Registers the specified obj. /// </summary> @@ -617,11 +648,6 @@ namespace MediaBrowser.Common.Implementations } } - void IDependencyContainer.RegisterSingleInstance<T>(Func<T> func) - { - RegisterSingleInstance(func); - } - /// <summary> /// Registers the single instance. /// </summary> @@ -633,11 +659,6 @@ namespace MediaBrowser.Common.Implementations Container.RegisterSingleton(func); } - void IDependencyContainer.Register(Type typeInterface, Type typeImplementation) - { - Container.Register(typeInterface, typeImplementation); - } - /// <summary> /// Resolves this instance. /// </summary> @@ -673,7 +694,13 @@ namespace MediaBrowser.Common.Implementations { try { +#if NET46 return Assembly.Load(File.ReadAllBytes(file)); +#elif NETSTANDARD1_6 + + return AssemblyLoadContext.Default.LoadFromStream(new MemoryStream(File.ReadAllBytes(file))); +#endif + return null; } catch (Exception ex) { @@ -692,7 +719,14 @@ namespace MediaBrowser.Common.Implementations { var currentType = typeof(T); - return AllConcreteTypes.AsParallel().Where(currentType.IsAssignableFrom); +#if NET46 + return AllConcreteTypes.Where(currentType.IsAssignableFrom); +#elif NETSTANDARD1_6 + var currentTypeInfo = currentType.GetTypeInfo(); + + return AllConcreteTypes.Where(currentTypeInfo.IsAssignableFrom); +#endif + return new List<Type>(); } /// <summary> @@ -747,7 +781,7 @@ namespace MediaBrowser.Common.Implementations { var list = Plugins.ToList(); list.Remove(plugin); - Plugins = list; + Plugins = list.ToArray(); } /// <summary> diff --git a/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs b/Emby.Common.Implementations/BaseApplicationPaths.cs similarity index 92% rename from MediaBrowser.Common.Implementations/BaseApplicationPaths.cs rename to Emby.Common.Implementations/BaseApplicationPaths.cs index 9ba2effd3a..8792778ba6 100644 --- a/MediaBrowser.Common.Implementations/BaseApplicationPaths.cs +++ b/Emby.Common.Implementations/BaseApplicationPaths.cs @@ -1,7 +1,7 @@ -using MediaBrowser.Common.Configuration; -using System.IO; +using System.IO; +using MediaBrowser.Common.Configuration; -namespace MediaBrowser.Common.Implementations +namespace Emby.Common.Implementations { /// <summary> /// Provides a base class to hold common application paths used by both the Ui and Server. @@ -12,22 +12,18 @@ namespace MediaBrowser.Common.Implementations /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// </summary> - protected BaseApplicationPaths(string programDataPath, string applicationPath) + protected BaseApplicationPaths(string programDataPath, string appFolderPath) { ProgramDataPath = programDataPath; - ApplicationPath = applicationPath; + ProgramSystemPath = appFolderPath; } - public string ApplicationPath { get; private set; } public string ProgramDataPath { get; private set; } /// <summary> /// Gets the path to the system folder /// </summary> - public string ProgramSystemPath - { - get { return Path.GetDirectoryName(ApplicationPath); } - } + public string ProgramSystemPath { get; private set; } /// <summary> /// The _data directory diff --git a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs b/Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs similarity index 95% rename from MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs rename to Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs index fa15023ca5..27c9fe6157 100644 --- a/MediaBrowser.Common.Implementations/Configuration/BaseConfigurationManager.cs +++ b/Emby.Common.Implementations/Configuration/BaseConfigurationManager.cs @@ -1,18 +1,19 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Events; -using MediaBrowser.Model.Configuration; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; +using Emby.Common.Implementations; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Common.Implementations.Configuration +namespace Emby.Common.Implementations.Configuration { /// <summary> /// Class BaseConfigurationManager @@ -79,7 +80,7 @@ namespace MediaBrowser.Common.Implementations.Configuration get { // Lazy load - LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer)); + LazyInitializer.EnsureInitialized(ref _configuration, ref _configurationLoaded, ref _configurationSyncLock, () => (BaseApplicationConfiguration)ConfigurationHelper.GetXmlConfiguration(ConfigurationType, CommonApplicationPaths.SystemConfigurationFilePath, XmlSerializer, FileSystem)); return _configuration; } protected set @@ -126,7 +127,7 @@ namespace MediaBrowser.Common.Implementations.Configuration Logger.Info("Saving system configuration"); var path = CommonApplicationPaths.SystemConfigurationFilePath; - Directory.CreateDirectory(Path.GetDirectoryName(path)); + FileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_configurationSyncLock) { @@ -196,9 +197,9 @@ namespace MediaBrowser.Common.Implementations.Configuration && !string.Equals(CommonConfiguration.CachePath ?? string.Empty, newPath)) { // Validate - if (!Directory.Exists(newPath)) + if (!FileSystem.DirectoryExists(newPath)) { - throw new DirectoryNotFoundException(string.Format("{0} does not exist.", newPath)); + throw new FileNotFoundException(string.Format("{0} does not exist.", newPath)); } EnsureWriteAccess(newPath); @@ -253,7 +254,7 @@ namespace MediaBrowser.Common.Implementations.Configuration { return Activator.CreateInstance(configurationType); } - catch (DirectoryNotFoundException) + catch (IOException) { return Activator.CreateInstance(configurationType); } @@ -293,7 +294,7 @@ namespace MediaBrowser.Common.Implementations.Configuration _configurations.AddOrUpdate(key, configuration, (k, v) => configuration); var path = GetConfigurationFile(key); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + FileSystem.CreateDirectory(Path.GetDirectoryName(path)); lock (_configurationSyncLock) { diff --git a/MediaBrowser.Common.Implementations/Configuration/ConfigurationHelper.cs b/Emby.Common.Implementations/Configuration/ConfigurationHelper.cs similarity index 80% rename from MediaBrowser.Common.Implementations/Configuration/ConfigurationHelper.cs rename to Emby.Common.Implementations/Configuration/ConfigurationHelper.cs index 419b85fa73..0d43a651ea 100644 --- a/MediaBrowser.Common.Implementations/Configuration/ConfigurationHelper.cs +++ b/Emby.Common.Implementations/Configuration/ConfigurationHelper.cs @@ -1,9 +1,10 @@ -using MediaBrowser.Model.Serialization; -using System; +using System; using System.IO; using System.Linq; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Common.Implementations.Configuration +namespace Emby.Common.Implementations.Configuration { /// <summary> /// Class ConfigurationHelper @@ -18,7 +19,7 @@ namespace MediaBrowser.Common.Implementations.Configuration /// <param name="path">The path.</param> /// <param name="xmlSerializer">The XML serializer.</param> /// <returns>System.Object.</returns> - public static object GetXmlConfiguration(Type type, string path, IXmlSerializer xmlSerializer) + public static object GetXmlConfiguration(Type type, string path, IXmlSerializer xmlSerializer, IFileSystem fileSystem) { object configuration; @@ -27,7 +28,7 @@ namespace MediaBrowser.Common.Implementations.Configuration // Use try/catch to avoid the extra file system lookup using File.Exists try { - buffer = File.ReadAllBytes(path); + buffer = fileSystem.ReadAllBytes(path); configuration = xmlSerializer.DeserializeFromBytes(type, buffer); } @@ -46,10 +47,10 @@ namespace MediaBrowser.Common.Implementations.Configuration // If the file didn't exist before, or if something has changed, re-save if (buffer == null || !buffer.SequenceEqual(newBytes)) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + fileSystem.CreateDirectory(Path.GetDirectoryName(path)); // Save it after load in case we got new items - File.WriteAllBytes(path, newBytes); + fileSystem.WriteAllBytes(path, newBytes); } return configuration; diff --git a/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs new file mode 100644 index 0000000000..01a31bcc03 --- /dev/null +++ b/Emby.Common.Implementations/Cryptography/CryptographyProvider.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using MediaBrowser.Model.Cryptography; + +namespace Emby.Common.Implementations.Cryptography +{ + public class CryptographyProvider : ICryptoProvider + { + public Guid GetMD5(string str) + { + return new Guid(ComputeMD5(Encoding.Unicode.GetBytes(str))); + } + + public byte[] ComputeSHA1(byte[] bytes) + { + using (var provider = SHA1.Create()) + { + return provider.ComputeHash(bytes); + } + } + + public byte[] ComputeMD5(Stream str) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(str); + } + } + + public byte[] ComputeMD5(byte[] bytes) + { + using (var provider = MD5.Create()) + { + return provider.ComputeHash(bytes); + } + } + } +} diff --git a/MediaBrowser.Common.Implementations/Devices/DeviceId.cs b/Emby.Common.Implementations/Devices/DeviceId.cs similarity index 94% rename from MediaBrowser.Common.Implementations/Devices/DeviceId.cs rename to Emby.Common.Implementations/Devices/DeviceId.cs index f1581704b8..3d23ab872b 100644 --- a/MediaBrowser.Common.Implementations/Devices/DeviceId.cs +++ b/Emby.Common.Implementations/Devices/DeviceId.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.Logging; -using System; +using System; using System.IO; using System.Text; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; -namespace MediaBrowser.Common.Implementations.Devices +namespace Emby.Common.Implementations.Devices { public class DeviceId { diff --git a/Emby.Common.Implementations/Diagnostics/CommonProcess.cs b/Emby.Common.Implementations/Diagnostics/CommonProcess.cs new file mode 100644 index 0000000000..462345ced5 --- /dev/null +++ b/Emby.Common.Implementations/Diagnostics/CommonProcess.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Diagnostics; + +namespace Emby.Common.Implementations.Diagnostics +{ + public class CommonProcess : IProcess + { + public event EventHandler Exited; + + private readonly ProcessOptions _options; + private readonly Process _process; + + public CommonProcess(ProcessOptions options) + { + _options = options; + + var startInfo = new ProcessStartInfo + { + Arguments = options.Arguments, + FileName = options.FileName, + WorkingDirectory = options.WorkingDirectory, + UseShellExecute = options.UseShellExecute, + CreateNoWindow = options.CreateNoWindow, + RedirectStandardError = options.RedirectStandardError, + RedirectStandardInput = options.RedirectStandardInput, + RedirectStandardOutput = options.RedirectStandardOutput + }; + +#if NET46 + startInfo.ErrorDialog = options.ErrorDialog; + + if (options.IsHidden) + { + startInfo.WindowStyle = ProcessWindowStyle.Hidden; + } +#endif + + _process = new Process + { + StartInfo = startInfo + }; + + if (options.EnableRaisingEvents) + { + _process.EnableRaisingEvents = true; + _process.Exited += _process_Exited; + } + } + + private void _process_Exited(object sender, EventArgs e) + { + if (Exited != null) + { + Exited(this, e); + } + } + + public ProcessOptions StartInfo + { + get { return _options; } + } + + public StreamWriter StandardInput + { + get { return _process.StandardInput; } + } + + public StreamReader StandardError + { + get { return _process.StandardError; } + } + + public StreamReader StandardOutput + { + get { return _process.StandardOutput; } + } + + public int ExitCode + { + get { return _process.ExitCode; } + } + + public void Start() + { + _process.Start(); + } + + public void Kill() + { + _process.Kill(); + } + + public bool WaitForExit(int timeMs) + { + return _process.WaitForExit(timeMs); + } + + public void Dispose() + { + _process.Dispose(); + } + } +} diff --git a/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs b/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs new file mode 100644 index 0000000000..292da023c7 --- /dev/null +++ b/Emby.Common.Implementations/Diagnostics/ProcessFactory.cs @@ -0,0 +1,12 @@ +using MediaBrowser.Model.Diagnostics; + +namespace Emby.Common.Implementations.Diagnostics +{ + public class ProcessFactory : IProcessFactory + { + public IProcess Create(ProcessOptions options) + { + return new CommonProcess(options); + } + } +} diff --git a/Emby.Common.Implementations/Emby.Common.Implementations.xproj b/Emby.Common.Implementations/Emby.Common.Implementations.xproj new file mode 100644 index 0000000000..5bb6e4e589 --- /dev/null +++ b/Emby.Common.Implementations/Emby.Common.Implementations.xproj @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> + <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> + </PropertyGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> + <PropertyGroup Label="Globals"> + <ProjectGuid>5a27010a-09c6-4e86-93ea-437484c10917</ProjectGuid> + <RootNamespace>Emby.Common.Implementations</RootNamespace> + <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> + <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> + <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup> + <SchemaVersion>2.0</SchemaVersion> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + </ItemGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> +</Project> \ No newline at end of file diff --git a/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs new file mode 100644 index 0000000000..6cc4626eaf --- /dev/null +++ b/Emby.Common.Implementations/EnvironmentInfo/EnvironmentInfo.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using MediaBrowser.Model.System; + +namespace Emby.Common.Implementations.EnvironmentInfo +{ + public class EnvironmentInfo : IEnvironmentInfo + { + public MediaBrowser.Model.System.Architecture? CustomArchitecture { get; set; } + + public MediaBrowser.Model.System.OperatingSystem OperatingSystem + { + get + { +#if NET46 + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return MediaBrowser.Model.System.OperatingSystem.OSX; + case PlatformID.Win32NT: + return MediaBrowser.Model.System.OperatingSystem.Windows; + case PlatformID.Unix: + return MediaBrowser.Model.System.OperatingSystem.Linux; + } +#elif NETSTANDARD1_6 + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return OperatingSystem.OSX; + } + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return OperatingSystem.Windows; + } + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return OperatingSystem.Linux; + } +#endif + return MediaBrowser.Model.System.OperatingSystem.Windows; + } + } + + public string OperatingSystemName + { + get + { +#if NET46 + return Environment.OSVersion.Platform.ToString(); +#elif NETSTANDARD1_6 + return System.Runtime.InteropServices.RuntimeInformation.OSDescription; +#endif + return "Operating System"; + } + } + + public string OperatingSystemVersion + { + get + { +#if NET46 + return Environment.OSVersion.Version.ToString() + " " + Environment.OSVersion.ServicePack.ToString(); +#elif NETSTANDARD1_6 + return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription; +#endif + return "1.0"; + } + } + + public MediaBrowser.Model.System.Architecture SystemArchitecture + { + get + { + if (CustomArchitecture.HasValue) + { + return CustomArchitecture.Value; + } +#if NET46 + return Environment.Is64BitOperatingSystem ? MediaBrowser.Model.System.Architecture.X64 : MediaBrowser.Model.System.Architecture.X86; +#elif NETSTANDARD1_6 + switch(System.Runtime.InteropServices.RuntimeInformation.OSArchitecture) + { + case System.Runtime.InteropServices.Architecture.Arm: + return MediaBrowser.Model.System.Architecture.Arm; + case System.Runtime.InteropServices.Architecture.Arm64: + return MediaBrowser.Model.System.Architecture.Arm64; + case System.Runtime.InteropServices.Architecture.X64: + return MediaBrowser.Model.System.Architecture.X64; + case System.Runtime.InteropServices.Architecture.X86: + return MediaBrowser.Model.System.Architecture.X86; + } +#endif + return MediaBrowser.Model.System.Architecture.X64; + } + } + + public string GetEnvironmentVariable(string name) + { + return Environment.GetEnvironmentVariable(name); + } + + public virtual string GetUserId() + { + return null; + } + + public string StackTrace + { + get { return Environment.StackTrace; } + } + + public void SetProcessEnvironmentVariable(string name, string value) + { + Environment.SetEnvironmentVariable(name, value); + } + } +} diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientInfo.cs b/Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs similarity index 83% rename from MediaBrowser.Common.Implementations/HttpClientManager/HttpClientInfo.cs rename to Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs index 8af6ef6c6e..ca481b33e7 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientInfo.cs +++ b/Emby.Common.Implementations/HttpClientManager/HttpClientInfo.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Common.Implementations.HttpClientManager +namespace Emby.Common.Implementations.HttpClientManager { /// <summary> /// Class HttpClientInfo diff --git a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs similarity index 91% rename from MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs rename to Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs index eec18e9857..06af5af536 100644 --- a/MediaBrowser.Common.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Common.Implementations/HttpClientManager/HttpClientManager.cs @@ -13,13 +13,13 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Cache; using System.Text; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using Emby.Common.Implementations.HttpClientManager; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Common.Implementations.HttpClientManager +namespace Emby.Common.Implementations.HttpClientManager { /// <summary> /// Class HttpClientManager @@ -42,7 +42,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private readonly IApplicationPaths _appPaths; private readonly IFileSystem _fileSystem; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IMemoryStreamFactory _memoryStreamProvider; /// <summary> /// Initializes a new instance of the <see cref="HttpClientManager" /> class. @@ -53,7 +53,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager /// <exception cref="System.ArgumentNullException">appPaths /// or /// logger</exception> - public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamProvider memoryStreamProvider) + public HttpClientManager(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem, IMemoryStreamFactory memoryStreamProvider) { if (appPaths == null) { @@ -69,11 +69,13 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager _memoryStreamProvider = memoryStreamProvider; _appPaths = appPaths; +#if NET46 // http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c ServicePointManager.Expect100Continue = false; // Trakt requests sometimes fail without this ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls; +#endif } /// <summary> @@ -130,6 +132,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private void AddIpv4Option(HttpWebRequest request, HttpRequestOptions options) { +#if NET46 request.ServicePoint.BindIPEndPointDelegate = (servicePount, remoteEndPoint, retryCount) => { if (remoteEndPoint.AddressFamily == AddressFamily.InterNetwork) @@ -138,6 +141,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } throw new InvalidOperationException("no IPv4 address"); }; +#endif } private WebRequest GetRequest(HttpRequestOptions options, string method) @@ -164,34 +168,66 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager AddRequestHeaders(httpWebRequest, options); - httpWebRequest.AutomaticDecompression = options.EnableHttpCompression ? - (options.DecompressionMethod ?? DecompressionMethods.Deflate) : - DecompressionMethods.None; +#if NET46 + if (options.EnableHttpCompression) + { + if (options.DecompressionMethod.HasValue) + { + httpWebRequest.AutomaticDecompression = options.DecompressionMethod.Value == CompressionMethod.Gzip + ? DecompressionMethods.GZip + : DecompressionMethods.Deflate; + } + else + { + httpWebRequest.AutomaticDecompression = DecompressionMethods.Deflate; + } + } + else + { + httpWebRequest.AutomaticDecompression = DecompressionMethods.None; + } +#endif } - request.CachePolicy = new RequestCachePolicy(RequestCacheLevel.BypassCache); + + +#if NET46 + request.CachePolicy = new System.Net.Cache.RequestCachePolicy(System.Net.Cache.RequestCacheLevel.BypassCache); +#endif if (httpWebRequest != null) { if (options.EnableKeepAlive) { +#if NET46 httpWebRequest.KeepAlive = true; +#endif } } request.Method = method; +#if NET46 request.Timeout = options.TimeoutMs; - +#endif + if (httpWebRequest != null) { if (!string.IsNullOrEmpty(options.Host)) { +#if NET46 httpWebRequest.Host = options.Host; +#elif NETSTANDARD1_6 + httpWebRequest.Headers["Host"] = options.Host; +#endif } if (!string.IsNullOrEmpty(options.Referer)) { +#if NET46 httpWebRequest.Referer = options.Referer; +#elif NETSTANDARD1_6 + httpWebRequest.Headers["Referer"] = options.Referer; +#endif } } @@ -201,7 +237,10 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager if (parts.Length == 2) { request.Credentials = GetCredential(url, parts[0], parts[1]); + // TODO: .net core ?? +#if NET46 request.PreAuthenticate = true; +#endif } } @@ -226,11 +265,19 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager } else if (string.Equals(header.Key, "User-Agent", StringComparison.OrdinalIgnoreCase)) { +#if NET46 request.UserAgent = header.Value; +#elif NETSTANDARD1_6 + request.Headers["User-Agent"] = header.Value; +#endif } else { +#if NET46 request.Headers.Set(header.Key, header.Value); +#elif NETSTANDARD1_6 + request.Headers[header.Key] = header.Value; +#endif } } } @@ -330,7 +377,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager { if (_fileSystem.GetLastWriteTimeUtc(responseCachePath).Add(cacheLength) > DateTime.UtcNow) { - using (var stream = _fileSystem.GetFileStream(responseCachePath, FileMode.Open, FileAccess.Read, FileShare.Read, true)) + using (var stream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) { var memoryStream = _memoryStreamProvider.CreateNew(); @@ -342,7 +389,6 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager ResponseUrl = url, Content = memoryStream, StatusCode = HttpStatusCode.OK, - Headers = new NameValueCollection(), ContentLength = memoryStream.Length }; } @@ -370,7 +416,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; - using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileMode.Create, FileAccess.Write, FileShare.None, true)) + using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) { await memoryStream.CopyToAsync(fileStream).ConfigureAwait(false); @@ -407,8 +453,10 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager httpWebRequest.ContentType = options.RequestContentType ?? "application/x-www-form-urlencoded"; +#if NET46 httpWebRequest.ContentLength = bytes.Length; - httpWebRequest.GetRequestStream().Write(bytes, 0, bytes.Length); +#endif + (await httpWebRequest.GetRequestStreamAsync().ConfigureAwait(false)).Write(bytes, 0, bytes.Length); } if (options.ResourcePool != null) @@ -487,7 +535,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, Stream content, long? contentLength, IDisposable disposable) { - return new HttpResponseInfo(disposable) + var responseInfo = new HttpResponseInfo(disposable) { Content = content, @@ -495,17 +543,22 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager ContentType = httpResponse.ContentType, - Headers = new NameValueCollection(httpResponse.Headers), - ContentLength = contentLength, ResponseUrl = httpResponse.ResponseUri.ToString() }; + + if (httpResponse.Headers != null) + { + SetHeaders(httpResponse.Headers, responseInfo); + } + + return responseInfo; } private HttpResponseInfo GetResponseInfo(HttpWebResponse httpResponse, string tempFile, long? contentLength) { - return new HttpResponseInfo + var responseInfo = new HttpResponseInfo { TempFilePath = tempFile, @@ -513,10 +566,23 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager ContentType = httpResponse.ContentType, - Headers = httpResponse.Headers, - ContentLength = contentLength }; + + if (httpResponse.Headers != null) + { + SetHeaders(httpResponse.Headers, responseInfo); + } + + return responseInfo; + } + + private void SetHeaders(WebHeaderCollection headers, HttpResponseInfo responseInfo) + { + foreach (var key in headers.AllKeys) + { + responseInfo.Headers[key] = headers[key]; + } } public Task<HttpResponseInfo> Post(HttpRequestOptions options) @@ -621,7 +687,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager // We're not able to track progress using (var stream = httpResponse.GetResponseStream()) { - using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); } @@ -631,7 +697,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager { using (var stream = ProgressStream.CreateReadProgressStream(httpResponse.GetResponseStream(), options.Progress.Report, contentLength.Value)) { - using (var fs = _fileSystem.GetFileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.Read, true)) + using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) { await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); } @@ -867,6 +933,7 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager private Task<WebResponse> GetResponseAsync(WebRequest request, TimeSpan timeout) { +#if NET46 var taskCompletion = new TaskCompletionSource<WebResponse>(); Task<WebResponse> asyncTask = Task.Factory.FromAsync<WebResponse>(request.BeginGetResponse, request.EndGetResponse, null); @@ -879,6 +946,9 @@ namespace MediaBrowser.Common.Implementations.HttpClientManager asyncTask.ContinueWith(callback.OnError, TaskContinuationOptions.OnlyOnFaulted); return taskCompletion.Task; +#endif + + return request.GetResponseAsync(); } private static void TimeoutCallback(object state, bool timedOut) diff --git a/MediaBrowser.Common.Implementations/IO/IsoManager.cs b/Emby.Common.Implementations/IO/IsoManager.cs similarity index 96% rename from MediaBrowser.Common.Implementations/IO/IsoManager.cs rename to Emby.Common.Implementations/IO/IsoManager.cs index de88ddadab..14614acaf8 100644 --- a/MediaBrowser.Common.Implementations/IO/IsoManager.cs +++ b/Emby.Common.Implementations/IO/IsoManager.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Model.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Common.Implementations.IO +namespace Emby.Common.Implementations.IO { /// <summary> /// Class IsoManager diff --git a/Emby.Common.Implementations/IO/ManagedFileSystem.cs b/Emby.Common.Implementations/IO/ManagedFileSystem.cs new file mode 100644 index 0000000000..62d285072c --- /dev/null +++ b/Emby.Common.Implementations/IO/ManagedFileSystem.cs @@ -0,0 +1,794 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; + +namespace Emby.Common.Implementations.IO +{ + /// <summary> + /// Class ManagedFileSystem + /// </summary> + public class ManagedFileSystem : IFileSystem + { + protected ILogger Logger; + + private readonly bool _supportsAsyncFileStreams; + private char[] _invalidFileNameChars; + private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>(); + private bool EnableFileSystemRequestConcat = true; + + public ManagedFileSystem(ILogger logger, bool supportsAsyncFileStreams, bool enableManagedInvalidFileNameChars, bool enableFileSystemRequestConcat) + { + Logger = logger; + _supportsAsyncFileStreams = supportsAsyncFileStreams; + EnableFileSystemRequestConcat = enableFileSystemRequestConcat; + SetInvalidFileNameChars(enableManagedInvalidFileNameChars); + } + + public void AddShortcutHandler(IShortcutHandler handler) + { + _shortcutHandlers.Add(handler); + } + + protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars) + { + if (enableManagedInvalidFileNameChars) + { + _invalidFileNameChars = Path.GetInvalidFileNameChars(); + } + else + { + // GetInvalidFileNameChars is less restrictive in Linux/Mac than Windows, this mimic Windows behavior for mono under Linux/Mac. + _invalidFileNameChars = new char[41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', + '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', + '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' }; + } + } + + public char DirectorySeparatorChar + { + get + { + return Path.DirectorySeparatorChar; + } + } + + public char PathSeparator + { + get + { + return Path.PathSeparator; + } + } + + public string GetFullPath(string path) + { + return Path.GetFullPath(path); + } + + /// <summary> + /// Determines whether the specified filename is shortcut. + /// </summary> + /// <param name="filename">The filename.</param> + /// <returns><c>true</c> if the specified filename is shortcut; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">filename</exception> + public virtual bool IsShortcut(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var extension = Path.GetExtension(filename); + return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + } + + /// <summary> + /// Resolves the shortcut. + /// </summary> + /// <param name="filename">The filename.</param> + /// <returns>System.String.</returns> + /// <exception cref="System.ArgumentNullException">filename</exception> + public virtual string ResolveShortcut(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var extension = Path.GetExtension(filename); + var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + + if (handler != null) + { + return handler.Resolve(filename); + } + + return null; + } + + /// <summary> + /// Creates the shortcut. + /// </summary> + /// <param name="shortcutPath">The shortcut path.</param> + /// <param name="target">The target.</param> + /// <exception cref="System.ArgumentNullException"> + /// shortcutPath + /// or + /// target + /// </exception> + public void CreateShortcut(string shortcutPath, string target) + { + if (string.IsNullOrEmpty(shortcutPath)) + { + throw new ArgumentNullException("shortcutPath"); + } + + if (string.IsNullOrEmpty(target)) + { + throw new ArgumentNullException("target"); + } + + var extension = Path.GetExtension(shortcutPath); + var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); + + if (handler != null) + { + handler.Create(shortcutPath, target); + } + else + { + throw new NotImplementedException(); + } + } + + /// <summary> + /// Returns a <see cref="FileSystemMetadata"/> object for the specified file or directory path. + /// </summary> + /// <param name="path">A path to a file or directory.</param> + /// <returns>A <see cref="FileSystemMetadata"/> object.</returns> + /// <remarks>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's + /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and all other properties will reflect the properties of the directory.</remarks> + public FileSystemMetadata GetFileSystemInfo(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists + if (Path.HasExtension(path)) + { + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + return GetFileSystemMetadata(fileInfo); + } + + return GetFileSystemMetadata(new DirectoryInfo(path)); + } + else + { + var fileInfo = new DirectoryInfo(path); + + if (fileInfo.Exists) + { + return GetFileSystemMetadata(fileInfo); + } + + return GetFileSystemMetadata(new FileInfo(path)); + } + } + + /// <summary> + /// Returns a <see cref="FileSystemMetadata"/> object for the specified file path. + /// </summary> + /// <param name="path">A path to a file.</param> + /// <returns>A <see cref="FileSystemMetadata"/> object.</returns> + /// <remarks><para>If the specified path points to a directory, the returned <see cref="FileSystemMetadata"/> object's + /// <see cref="FileSystemMetadata.IsDirectory"/> property and the <see cref="FileSystemMetadata.Exists"/> property will both be set to false.</para> + /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks> + public FileSystemMetadata GetFileInfo(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var fileInfo = new FileInfo(path); + + return GetFileSystemMetadata(fileInfo); + } + + /// <summary> + /// Returns a <see cref="FileSystemMetadata"/> object for the specified directory path. + /// </summary> + /// <param name="path">A path to a directory.</param> + /// <returns>A <see cref="FileSystemMetadata"/> object.</returns> + /// <remarks><para>If the specified path points to a file, the returned <see cref="FileSystemMetadata"/> object's + /// <see cref="FileSystemMetadata.IsDirectory"/> property will be set to true and the <see cref="FileSystemMetadata.Exists"/> property will be set to false.</para> + /// <para>For automatic handling of files <b>and</b> directories, use <see cref="GetFileSystemInfo"/>.</para></remarks> + public FileSystemMetadata GetDirectoryInfo(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var fileInfo = new DirectoryInfo(path); + + return GetFileSystemMetadata(fileInfo); + } + + private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) + { + var result = new FileSystemMetadata(); + + result.Exists = info.Exists; + result.FullName = info.FullName; + result.Extension = info.Extension; + result.Name = info.Name; + + if (result.Exists) + { + var attributes = info.Attributes; + result.IsDirectory = info is DirectoryInfo || (attributes & FileAttributes.Directory) == FileAttributes.Directory; + result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + + var fileInfo = info as FileInfo; + if (fileInfo != null) + { + result.Length = fileInfo.Length; + result.DirectoryName = fileInfo.DirectoryName; + } + + result.CreationTimeUtc = GetCreationTimeUtc(info); + result.LastWriteTimeUtc = GetLastWriteTimeUtc(info); + } + else + { + result.IsDirectory = info is DirectoryInfo; + } + + return result; + } + + /// <summary> + /// The space char + /// </summary> + private const char SpaceChar = ' '; + + /// <summary> + /// Takes a filename and removes invalid characters + /// </summary> + /// <param name="filename">The filename.</param> + /// <returns>System.String.</returns> + /// <exception cref="System.ArgumentNullException">filename</exception> + public string GetValidFilename(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var builder = new StringBuilder(filename); + + foreach (var c in _invalidFileNameChars) + { + builder = builder.Replace(c, SpaceChar); + } + + return builder.ToString(); + } + + /// <summary> + /// Gets the creation time UTC. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>DateTime.</returns> + public DateTime GetCreationTimeUtc(FileSystemInfo info) + { + // This could throw an error on some file systems that have dates out of range + try + { + return info.CreationTimeUtc; + } + catch (Exception ex) + { + Logger.ErrorException("Error determining CreationTimeUtc for {0}", ex, info.FullName); + return DateTime.MinValue; + } + } + + /// <summary> + /// Gets the creation time UTC. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>DateTime.</returns> + public DateTime GetCreationTimeUtc(string path) + { + return GetCreationTimeUtc(GetFileSystemInfo(path)); + } + + public DateTime GetCreationTimeUtc(FileSystemMetadata info) + { + return info.CreationTimeUtc; + } + + public DateTime GetLastWriteTimeUtc(FileSystemMetadata info) + { + return info.LastWriteTimeUtc; + } + + /// <summary> + /// Gets the creation time UTC. + /// </summary> + /// <param name="info">The info.</param> + /// <returns>DateTime.</returns> + public DateTime GetLastWriteTimeUtc(FileSystemInfo info) + { + // This could throw an error on some file systems that have dates out of range + try + { + return info.LastWriteTimeUtc; + } + catch (Exception ex) + { + Logger.ErrorException("Error determining LastAccessTimeUtc for {0}", ex, info.FullName); + return DateTime.MinValue; + } + } + + /// <summary> + /// Gets the last write time UTC. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>DateTime.</returns> + public DateTime GetLastWriteTimeUtc(string path) + { + return GetLastWriteTimeUtc(GetFileSystemInfo(path)); + } + + /// <summary> + /// Gets the file stream. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="mode">The mode.</param> + /// <param name="access">The access.</param> + /// <param name="share">The share.</param> + /// <param name="isAsync">if set to <c>true</c> [is asynchronous].</param> + /// <returns>FileStream.</returns> + public Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) + { + if (_supportsAsyncFileStreams && isAsync) + { + return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144, true); + } + + return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144); + } + + private FileMode GetFileMode(FileOpenMode mode) + { + switch (mode) + { + case FileOpenMode.Append: + return FileMode.Append; + case FileOpenMode.Create: + return FileMode.Create; + case FileOpenMode.CreateNew: + return FileMode.CreateNew; + case FileOpenMode.Open: + return FileMode.Open; + case FileOpenMode.OpenOrCreate: + return FileMode.OpenOrCreate; + case FileOpenMode.Truncate: + return FileMode.Truncate; + default: + throw new Exception("Unrecognized FileOpenMode"); + } + } + + private FileAccess GetFileAccess(FileAccessMode mode) + { + switch (mode) + { + case FileAccessMode.ReadWrite: + return FileAccess.ReadWrite; + case FileAccessMode.Write: + return FileAccess.Write; + case FileAccessMode.Read: + return FileAccess.Read; + default: + throw new Exception("Unrecognized FileAccessMode"); + } + } + + private FileShare GetFileShare(FileShareMode mode) + { + switch (mode) + { + case FileShareMode.ReadWrite: + return FileShare.ReadWrite; + case FileShareMode.Write: + return FileShare.Write; + case FileShareMode.Read: + return FileShare.Read; + case FileShareMode.None: + return FileShare.None; + default: + throw new Exception("Unrecognized FileShareMode"); + } + } + + public void SetHidden(string path, bool isHidden) + { + var info = GetFileInfo(path); + + if (info.Exists && info.IsHidden != isHidden) + { + if (isHidden) + { + File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); + } + else + { + FileAttributes attributes = File.GetAttributes(path); + attributes = RemoveAttribute(attributes, FileAttributes.Hidden); + File.SetAttributes(path, attributes); + } + } + } + + public void SetReadOnly(string path, bool isReadOnly) + { + var info = GetFileInfo(path); + + if (info.Exists && info.IsReadOnly != isReadOnly) + { + if (isReadOnly) + { + File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); + } + else + { + FileAttributes attributes = File.GetAttributes(path); + attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); + File.SetAttributes(path, attributes); + } + } + } + + private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove) + { + return attributes & ~attributesToRemove; + } + + /// <summary> + /// Swaps the files. + /// </summary> + /// <param name="file1">The file1.</param> + /// <param name="file2">The file2.</param> + public void SwapFiles(string file1, string file2) + { + if (string.IsNullOrEmpty(file1)) + { + throw new ArgumentNullException("file1"); + } + + if (string.IsNullOrEmpty(file2)) + { + throw new ArgumentNullException("file2"); + } + + var temp1 = Path.GetTempFileName(); + + // Copying over will fail against hidden files + RemoveHiddenAttribute(file1); + RemoveHiddenAttribute(file2); + + CopyFile(file1, temp1, true); + + CopyFile(file2, file1, true); + CopyFile(temp1, file2, true); + + DeleteFile(temp1); + } + + /// <summary> + /// Removes the hidden attribute. + /// </summary> + /// <param name="path">The path.</param> + private void RemoveHiddenAttribute(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var currentFile = new FileInfo(path); + + // This will fail if the file is hidden + if (currentFile.Exists) + { + if ((currentFile.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) + { + currentFile.Attributes &= ~FileAttributes.Hidden; + } + } + } + + public bool ContainsSubPath(string parentPath, string path) + { + if (string.IsNullOrEmpty(parentPath)) + { + throw new ArgumentNullException("parentPath"); + } + + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + return path.IndexOf(parentPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) != -1; + } + + public bool IsRootPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var parent = Path.GetDirectoryName(path); + + if (!string.IsNullOrEmpty(parent)) + { + return false; + } + + return true; + } + + public string NormalizePath(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) + { + return path; + } + + return path.TrimEnd(Path.DirectorySeparatorChar); + } + + public string GetFileNameWithoutExtension(FileSystemMetadata info) + { + if (info.IsDirectory) + { + return info.Name; + } + + return Path.GetFileNameWithoutExtension(info.FullName); + } + + public string GetFileNameWithoutExtension(string path) + { + return Path.GetFileNameWithoutExtension(path); + } + + public bool IsPathFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + + // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ + + if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && + !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + return true; + + //return Path.IsPathRooted(path); + } + + public void DeleteFile(string path) + { + var fileInfo = GetFileInfo(path); + + if (fileInfo.Exists) + { + if (fileInfo.IsHidden) + { + SetHidden(path, false); + } + if (fileInfo.IsReadOnly) + { + SetReadOnly(path, false); + } + } + + File.Delete(path); + } + + public void DeleteDirectory(string path, bool recursive) + { + Directory.Delete(path, recursive); + } + + public void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } + + public List<FileSystemMetadata> GetDrives() + { + // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout + return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata + { + Name = GetName(d), + FullName = d.RootDirectory.FullName, + IsDirectory = true + + }).ToList(); + } + + private string GetName(DriveInfo drive) + { + return drive.Name; + } + + public IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) + { + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + + return ToMetadata(path, new DirectoryInfo(path).EnumerateDirectories("*", searchOption)); + } + + public IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) + { + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + + return ToMetadata(path, new DirectoryInfo(path).EnumerateFiles("*", searchOption)); + } + + public IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false) + { + var directoryInfo = new DirectoryInfo(path); + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + + if (EnableFileSystemRequestConcat) + { + return ToMetadata(path, directoryInfo.EnumerateDirectories("*", searchOption)) + .Concat(ToMetadata(path, directoryInfo.EnumerateFiles("*", searchOption))); + } + + return ToMetadata(path, directoryInfo.EnumerateFileSystemInfos("*", searchOption)); + } + + private IEnumerable<FileSystemMetadata> ToMetadata(string parentPath, IEnumerable<FileSystemInfo> infos) + { + return infos.Select(i => + { + try + { + return GetFileSystemMetadata(i); + } + catch (PathTooLongException) + { + // Can't log using the FullName because it will throw the PathTooLongExceptiona again + //Logger.Warn("Path too long: {0}", i.FullName); + Logger.Warn("File or directory path too long. Parent folder: {0}", parentPath); + return null; + } + + }).Where(i => i != null); + } + + public string[] ReadAllLines(string path) + { + return File.ReadAllLines(path); + } + + public void WriteAllLines(string path, IEnumerable<string> lines) + { + File.WriteAllLines(path, lines); + } + + public Stream OpenRead(string path) + { + return File.OpenRead(path); + } + + public void CopyFile(string source, string target, bool overwrite) + { + File.Copy(source, target, overwrite); + } + + public void MoveFile(string source, string target) + { + File.Move(source, target); + } + + public void MoveDirectory(string source, string target) + { + Directory.Move(source, target); + } + + public bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + public bool FileExists(string path) + { + return File.Exists(path); + } + + public string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public byte[] ReadAllBytes(string path) + { + return File.ReadAllBytes(path); + } + + public void WriteAllText(string path, string text, Encoding encoding) + { + File.WriteAllText(path, text, encoding); + } + + public void WriteAllText(string path, string text) + { + File.WriteAllText(path, text); + } + + public void WriteAllBytes(string path, byte[] bytes) + { + File.WriteAllBytes(path, bytes); + } + + public string ReadAllText(string path, Encoding encoding) + { + return File.ReadAllText(path, encoding); + } + + public IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false) + { + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + return Directory.EnumerateDirectories(path, "*", searchOption); + } + + public IEnumerable<string> GetFilePaths(string path, bool recursive = false) + { + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + return Directory.EnumerateFiles(path, "*", searchOption); + } + + public IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false) + { + var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; + return Directory.EnumerateFileSystemEntries(path, "*", searchOption); + } + + public virtual void SetExecutable(string path) + { + + } + } +} diff --git a/MediaBrowser.Common.Implementations/Logging/NLogger.cs b/Emby.Common.Implementations/Logging/NLogger.cs similarity index 99% rename from MediaBrowser.Common.Implementations/Logging/NLogger.cs rename to Emby.Common.Implementations/Logging/NLogger.cs index 11f41261a2..8abd3d0d92 100644 --- a/MediaBrowser.Common.Implementations/Logging/NLogger.cs +++ b/Emby.Common.Implementations/Logging/NLogger.cs @@ -2,7 +2,7 @@ using System; using System.Text; -namespace MediaBrowser.Common.Implementations.Logging +namespace Emby.Common.Implementations.Logging { /// <summary> /// Class NLogger diff --git a/Emby.Common.Implementations/Logging/NlogManager.cs b/Emby.Common.Implementations/Logging/NlogManager.cs new file mode 100644 index 0000000000..f7b723e8bc --- /dev/null +++ b/Emby.Common.Implementations/Logging/NlogManager.cs @@ -0,0 +1,544 @@ +using System; +using System.IO; +using System.Linq; +using System.Xml; +using NLog; +using NLog.Config; +using NLog.Filters; +using NLog.Targets; +using NLog.Targets.Wrappers; +using MediaBrowser.Model.Logging; + +namespace Emby.Common.Implementations.Logging +{ + /// <summary> + /// Class NlogManager + /// </summary> + public class NlogManager : ILogManager + { + #region Private Fields + + private LogSeverity _severity = LogSeverity.Debug; + + /// <summary> + /// Gets or sets the log directory. + /// </summary> + /// <value>The log directory.</value> + private readonly string LogDirectory; + + /// <summary> + /// Gets or sets the log file prefix. + /// </summary> + /// <value>The log file prefix.</value> + private readonly string LogFilePrefix; + + #endregion + + #region Event Declarations + + /// <summary> + /// Occurs when [logger loaded]. + /// </summary> + public event EventHandler LoggerLoaded; + + #endregion + + #region Public Properties + + /// <summary> + /// Gets the log file path. + /// </summary> + /// <value>The log file path.</value> + public string LogFilePath { get; private set; } + + /// <summary> + /// Gets or sets the exception message prefix. + /// </summary> + /// <value>The exception message prefix.</value> + public string ExceptionMessagePrefix { get; set; } + + public string NLogConfigurationFilePath { get; set; } + + public LogSeverity LogSeverity + { + + get + { + return _severity; + } + + set + { + DebugFileWriter( + LogDirectory, String.Format( + "SET LogSeverity, _severity = [{0}], value = [{1}]", + _severity.ToString(), + value.ToString() + )); + + var changed = _severity != value; + + _severity = value; + + if (changed) + { + UpdateLogLevel(value); + } + + } + } + + #endregion + + #region Constructor(s) + + /// <summary> + /// Initializes a new instance of the <see cref="NlogManager" /> class. + /// </summary> + /// <param name="logDirectory">The log directory.</param> + /// <param name="logFileNamePrefix">The log file name prefix.</param> + public NlogManager(string logDirectory, string logFileNamePrefix) + { + DebugFileWriter( + logDirectory, String.Format( + "NlogManager constructor called, logDirectory is [{0}], logFileNamePrefix is [{1}], _severity is [{2}].", + logDirectory, + logFileNamePrefix, + _severity.ToString() + )); + + LogDirectory = logDirectory; + LogFilePrefix = logFileNamePrefix; + + LogManager.Configuration = new LoggingConfiguration(); + } + + /// <summary> + /// Initializes a new instance of the <see cref="NlogManager" /> class. + /// </summary> + /// <param name="logDirectory">The log directory.</param> + /// <param name="logFileNamePrefix">The log file name prefix.</param> + public NlogManager(string logDirectory, string logFileNamePrefix, LogSeverity initialSeverity) : this(logDirectory, logFileNamePrefix) + { + _severity = initialSeverity; + + DebugFileWriter( + logDirectory, String.Format( + "NlogManager constructor called, logDirectory is [{0}], logFileNamePrefix is [{1}], _severity is [{2}].", + logDirectory, + logFileNamePrefix, + _severity.ToString() + )); + } + + #endregion + + #region Private Methods + + /// <summary> + /// Adds the file target. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="level">The level.</param> + private void AddFileTarget(string path, LogSeverity level) + { + + DebugFileWriter( + LogDirectory, String.Format( + "AddFileTarget called, path = [{0}], level = [{1}].", + path, + level.ToString() + )); + + RemoveTarget("ApplicationLogFileWrapper"); + + var wrapper = new AsyncTargetWrapper(); + wrapper.Name = "ApplicationLogFileWrapper"; + + var logFile = new FileTarget + { + FileName = path, + Layout = "${longdate} ${level} ${logger}: ${message}" + }; + + logFile.Name = "ApplicationLogFile"; + + wrapper.WrappedTarget = logFile; + + AddLogTarget(wrapper, level); + + } + + /// <summary> + /// Gets the log level. + /// </summary> + /// <param name="severity">The severity.</param> + /// <returns>LogLevel.</returns> + /// <exception cref="System.ArgumentException">Unrecognized LogSeverity</exception> + private LogLevel GetLogLevel(LogSeverity severity) + { + switch (severity) + { + case LogSeverity.Debug: + return LogLevel.Debug; + case LogSeverity.Error: + return LogLevel.Error; + case LogSeverity.Fatal: + return LogLevel.Fatal; + case LogSeverity.Info: + return LogLevel.Info; + case LogSeverity.Warn: + return LogLevel.Warn; + default: + throw new ArgumentException("Unrecognized LogSeverity"); + } + } + + private void UpdateLogLevel(LogSeverity newLevel) + { + DebugFileWriter( + LogDirectory, String.Format( + "UpdateLogLevel called, newLevel = [{0}].", + newLevel.ToString() + )); + + var level = GetLogLevel(newLevel); + + var rules = LogManager.Configuration.LoggingRules; + + foreach (var rule in rules) + { + if (!rule.IsLoggingEnabledForLevel(level)) + { + rule.EnableLoggingForLevel(level); + } + foreach (var lev in rule.Levels.ToArray()) + { + if (lev < level) + { + rule.DisableLoggingForLevel(lev); + } + } + } + } + + private void AddCustomFilters(string defaultLoggerNamePattern, LoggingRule defaultRule) + { + DebugFileWriter( + LogDirectory, String.Format( + "AddCustomFilters called, defaultLoggerNamePattern = [{0}], defaultRule.LoggerNamePattern = [{1}].", + defaultLoggerNamePattern, + defaultRule.LoggerNamePattern + )); + + try + { + var customConfig = new NLog.Config.XmlLoggingConfiguration(NLogConfigurationFilePath); + + DebugFileWriter( + LogDirectory, String.Format( + "Custom Configuration Loaded, Rule Count = [{0}].", + customConfig.LoggingRules.Count.ToString() + )); + + foreach (var customRule in customConfig.LoggingRules) + { + + DebugFileWriter( + LogDirectory, String.Format( + "Read Custom Rule, LoggerNamePattern = [{0}], Targets = [{1}].", + customRule.LoggerNamePattern, + string.Join(",", customRule.Targets.Select(x => x.Name).ToList()) + )); + + if (customRule.LoggerNamePattern.Equals(defaultLoggerNamePattern)) + { + + if (customRule.Targets.Any((arg) => arg.Name.Equals(defaultRule.Targets.First().Name))) + { + + DebugFileWriter( + LogDirectory, String.Format( + "Custom rule filters can be applied to this target, Filter Count = [{0}].", + customRule.Filters.Count.ToString() + )); + + foreach (ConditionBasedFilter customFilter in customRule.Filters) + { + + DebugFileWriter( + LogDirectory, String.Format( + "Read Custom Filter, Filter = [{0}], Action = [{1}], Type = [{2}].", + customFilter.Condition.ToString(), + customFilter.Action.ToString(), + customFilter.GetType().ToString() + )); + + defaultRule.Filters.Add(customFilter); + + } + } + else + { + + DebugFileWriter( + LogDirectory, String.Format( + "Ignoring custom rule as [Target] does not match." + )); + + } + + } + else + { + + DebugFileWriter( + LogDirectory, String.Format( + "Ignoring custom rule as [LoggerNamePattern] does not match." + )); + + } + } + } + catch (Exception ex) + { + // Intentionally do nothing, prevent issues affecting normal execution. + DebugFileWriter( + LogDirectory, String.Format( + "Exception in AddCustomFilters, ex.Message = [{0}].", + ex.Message + ) + ); + + } + } + + #endregion + + #region Public Methods + + /// <summary> + /// Gets the logger. + /// </summary> + /// <param name="name">The name.</param> + /// <returns>ILogger.</returns> + public MediaBrowser.Model.Logging.ILogger GetLogger(string name) + { + + DebugFileWriter( + LogDirectory, String.Format( + "GetLogger called, name = [{0}].", + name + )); + + return new NLogger(name, this); + + } + + /// <summary> + /// Adds the log target. + /// </summary> + /// <param name="target">The target.</param> + /// <param name="level">The level.</param> + public void AddLogTarget(Target target, LogSeverity level) + { + + DebugFileWriter( + LogDirectory, String.Format( + "AddLogTarget called, target.Name = [{0}], level = [{1}].", + target.Name, + level.ToString() + )); + + string loggerNamePattern = "*"; + var config = LogManager.Configuration; + var rule = new LoggingRule(loggerNamePattern, GetLogLevel(level), target); + + config.AddTarget(target.Name, target); + + AddCustomFilters(loggerNamePattern, rule); + + config.LoggingRules.Add(rule); + + LogManager.Configuration = config; + + } + + /// <summary> + /// Removes the target. + /// </summary> + /// <param name="name">The name.</param> + public void RemoveTarget(string name) + { + + DebugFileWriter( + LogDirectory, String.Format( + "RemoveTarget called, name = [{0}].", + name + )); + + var config = LogManager.Configuration; + + var target = config.FindTargetByName(name); + + if (target != null) + { + foreach (var rule in config.LoggingRules.ToList()) + { + var contains = rule.Targets.Contains(target); + + rule.Targets.Remove(target); + + if (contains) + { + config.LoggingRules.Remove(rule); + } + } + + config.RemoveTarget(name); + LogManager.Configuration = config; + } + } + + public void AddConsoleOutput() + { + + DebugFileWriter( + LogDirectory, String.Format( + "AddConsoleOutput called." + )); + + RemoveTarget("ConsoleTargetWrapper"); + + var wrapper = new AsyncTargetWrapper(); + wrapper.Name = "ConsoleTargetWrapper"; + + var target = new ConsoleTarget() + { + Layout = "${level}, ${logger}, ${message}", + Error = false + }; + + target.Name = "ConsoleTarget"; + + wrapper.WrappedTarget = target; + + AddLogTarget(wrapper, LogSeverity); + + } + + public void RemoveConsoleOutput() + { + + DebugFileWriter( + LogDirectory, String.Format( + "RemoveConsoleOutput called." + )); + + RemoveTarget("ConsoleTargetWrapper"); + + } + + /// <summary> + /// Reloads the logger, maintaining the current log level. + /// </summary> + public void ReloadLogger() + { + ReloadLogger(LogSeverity); + } + + /// <summary> + /// Reloads the logger, using the specified logging level. + /// </summary> + /// <param name="level">The level.</param> + public void ReloadLogger(LogSeverity level) + { + + DebugFileWriter( + LogDirectory, String.Format( + "ReloadLogger called, level = [{0}], LogFilePath (existing) = [{1}].", + level.ToString(), + LogFilePath + )); + + LogFilePath = Path.Combine(LogDirectory, LogFilePrefix + "-" + decimal.Floor(DateTime.Now.Ticks / 10000000) + ".txt"); + + Directory.CreateDirectory(Path.GetDirectoryName(LogFilePath)); + + AddFileTarget(LogFilePath, level); + + LogSeverity = level; + + if (LoggerLoaded != null) + { + try + { + + DebugFileWriter( + LogDirectory, String.Format( + "ReloadLogger called, raised event LoggerLoaded." + )); + + LoggerLoaded(this, EventArgs.Empty); + + } + catch (Exception ex) + { + GetLogger("Logger").ErrorException("Error in LoggerLoaded event", ex); + } + } + } + + /// <summary> + /// Flushes this instance. + /// </summary> + public void Flush() + { + + DebugFileWriter( + LogDirectory, String.Format( + "Flush called." + )); + + LogManager.Flush(); + + } + + #endregion + + #region Conditional Debug Methods + + /// <summary> + /// DEBUG: Standalone method to write out debug to assist with logger development/troubleshooting. + /// <list type="bullet"> + /// <item><description>The output file will be written to the server's log directory.</description></item> + /// <item><description>Calls to the method are safe and will never throw any exceptions.</description></item> + /// <item><description>Method calls will be omitted unless the library is compiled with DEBUG defined.</description></item> + /// </list> + /// </summary> + private static void DebugFileWriter(string logDirectory, string message) + { +#if DEBUG + try + { + + System.IO.File.AppendAllText( + Path.Combine(logDirectory, "NlogManager.txt"), + String.Format( + "{0} : {1}{2}", + System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"), + message, + System.Environment.NewLine + ) + ); + + } + catch (Exception ex) + { + // Intentionally do nothing, prevent issues affecting normal execution. + } +#endif + } + #endregion + } +} \ No newline at end of file diff --git a/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs new file mode 100644 index 0000000000..8476cea326 --- /dev/null +++ b/Emby.Common.Implementations/Net/DisposableManagedObjectBase.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Emby.Common.Implementations.Net +{ + /// <summary> + /// Correclty implements the <see cref="IDisposable"/> interface and pattern for an object containing only managed resources, and adds a few common niceities not on the interface such as an <see cref="IsDisposed"/> property. + /// </summary> + public abstract class DisposableManagedObjectBase : IDisposable + { + + #region Public Methods + + /// <summary> + /// Override this method and dispose any objects you own the lifetime of if disposing is true; + /// </summary> + /// <param name="disposing">True if managed objects should be disposed, if false, only unmanaged resources should be released.</param> + protected abstract void Dispose(bool disposing); + + /// <summary> + /// Throws and <see cref="System.ObjectDisposedException"/> if the <see cref="IsDisposed"/> property is true. + /// </summary> + /// <seealso cref="IsDisposed"/> + /// <exception cref="System.ObjectDisposedException">Thrown if the <see cref="IsDisposed"/> property is true.</exception> + /// <seealso cref="Dispose()"/> + protected virtual void ThrowIfDisposed() + { + if (this.IsDisposed) throw new ObjectDisposedException(this.GetType().FullName); + } + + #endregion + + #region Public Properties + + /// <summary> + /// Sets or returns a boolean indicating whether or not this instance has been disposed. + /// </summary> + /// <seealso cref="Dispose()"/> + public bool IsDisposed + { + get; + private set; + } + + #endregion + + #region IDisposable Members + + /// <summary> + /// Disposes this object instance and all internally managed resources. + /// </summary> + /// <remarks> + /// <para>Sets the <see cref="IsDisposed"/> property to true. Does not explicitly throw an exception if called multiple times, but makes no promises about behaviour of derived classes.</para> + /// </remarks> + /// <seealso cref="IsDisposed"/> + public void Dispose() + { + try + { + IsDisposed = true; + + Dispose(true); + } + finally + { + GC.SuppressFinalize(this); + } + } + + #endregion + } +} diff --git a/Emby.Common.Implementations/Net/NetSocket.cs b/Emby.Common.Implementations/Net/NetSocket.cs new file mode 100644 index 0000000000..bc012dfe2d --- /dev/null +++ b/Emby.Common.Implementations/Net/NetSocket.cs @@ -0,0 +1,97 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Logging; + +namespace Emby.Common.Implementations.Net +{ + public class NetSocket : ISocket + { + public Socket Socket { get; private set; } + private readonly ILogger _logger; + + public bool DualMode { get; private set; } + + public NetSocket(Socket socket, ILogger logger, bool isDualMode) + { + if (socket == null) + { + throw new ArgumentNullException("socket"); + } + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + Socket = socket; + _logger = logger; + DualMode = isDualMode; + } + + public IpEndPointInfo LocalEndPoint + { + get + { + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.LocalEndPoint); + } + } + + public IpEndPointInfo RemoteEndPoint + { + get + { + return NetworkManager.ToIpEndPointInfo((IPEndPoint)Socket.RemoteEndPoint); + } + } + + public void Close() + { +#if NET46 + Socket.Close(); +#else + Socket.Dispose(); +#endif + } + + public void Shutdown(bool both) + { + if (both) + { + Socket.Shutdown(SocketShutdown.Both); + } + else + { + // Change interface if ever needed + throw new NotImplementedException(); + } + } + + public void Listen(int backlog) + { + Socket.Listen(backlog); + } + + public void Bind(IpEndPointInfo endpoint) + { + var nativeEndpoint = NetworkManager.ToIPEndPoint(endpoint); + + Socket.Bind(nativeEndpoint); + } + + private SocketAcceptor _acceptor; + public void StartAccept(Action<ISocket> onAccept, Func<bool> isClosed) + { + _acceptor = new SocketAcceptor(_logger, Socket, onAccept, isClosed, DualMode); + + _acceptor.StartAccept(); + } + + public void Dispose() + { + Socket.Dispose(); + } + } +} diff --git a/Emby.Common.Implementations/Net/SocketAcceptor.cs b/Emby.Common.Implementations/Net/SocketAcceptor.cs new file mode 100644 index 0000000000..d4c6d33e52 --- /dev/null +++ b/Emby.Common.Implementations/Net/SocketAcceptor.cs @@ -0,0 +1,127 @@ +using System; +using System.Net.Sockets; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + public class SocketAcceptor + { + private readonly ILogger _logger; + private readonly Socket _originalSocket; + private readonly Func<bool> _isClosed; + private readonly Action<ISocket> _onAccept; + private readonly bool _isDualMode; + + public SocketAcceptor(ILogger logger, Socket originalSocket, Action<ISocket> onAccept, Func<bool> isClosed, bool isDualMode) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + if (originalSocket == null) + { + throw new ArgumentNullException("originalSocket"); + } + if (onAccept == null) + { + throw new ArgumentNullException("onAccept"); + } + if (isClosed == null) + { + throw new ArgumentNullException("isClosed"); + } + + _logger = logger; + _originalSocket = originalSocket; + _isClosed = isClosed; + _isDualMode = isDualMode; + _onAccept = onAccept; + } + + public void StartAccept() + { + Socket dummy = null; + StartAccept(null, ref dummy); + } + + public void StartAccept(SocketAsyncEventArgs acceptEventArg, ref Socket accepted) + { + if (acceptEventArg == null) + { + acceptEventArg = new SocketAsyncEventArgs(); + acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed); + } + else + { + // socket must be cleared since the context object is being reused + acceptEventArg.AcceptSocket = null; + } + + try + { + bool willRaiseEvent = _originalSocket.AcceptAsync(acceptEventArg); + + if (!willRaiseEvent) + { + ProcessAccept(acceptEventArg); + } + } + catch (Exception ex) + { + if (accepted != null) + { + try + { +#if NET46 + accepted.Close(); +#else + accepted.Dispose(); +#endif + } + catch + { + } + accepted = null; + } + } + } + + // This method is the callback method associated with Socket.AcceptAsync + // operations and is invoked when an accept operation is complete + // + void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e) + { + ProcessAccept(e); + } + + private void ProcessAccept(SocketAsyncEventArgs e) + { + if (_isClosed()) + { + return; + } + + // http://msdn.microsoft.com/en-us/library/system.net.sockets.socket.acceptasync%28v=vs.110%29.aspx + // Under certain conditions ConnectionReset can occur + // Need to attept to re-accept + if (e.SocketError == SocketError.ConnectionReset) + { + _logger.Error("SocketError.ConnectionReset reported. Attempting to re-accept."); + Socket dummy = null; + StartAccept(e, ref dummy); + return; + } + + var acceptSocket = e.AcceptSocket; + if (acceptSocket != null) + { + //ProcessAccept(acceptSocket); + _onAccept(new NetSocket(acceptSocket, _logger, _isDualMode)); + } + + // Accept the next connection request + StartAccept(e, ref acceptSocket); + } + } +} diff --git a/Emby.Common.Implementations/Net/SocketFactory.cs b/Emby.Common.Implementations/Net/SocketFactory.cs new file mode 100644 index 0000000000..70c7ba8458 --- /dev/null +++ b/Emby.Common.Implementations/Net/SocketFactory.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + public class SocketFactory : ISocketFactory + { + // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS + // Be careful to check any changes compile and work for all platform projects it is shared in. + + // Not entirely happy with this. Would have liked to have done something more generic/reusable, + // but that wasn't really the point so kept to YAGNI principal for now, even if the + // interfaces are a bit ugly, specific and make assumptions. + + private readonly ILogger _logger; + + public SocketFactory(ILogger logger) + { + if (logger == null) + { + throw new ArgumentNullException("logger"); + } + + _logger = logger; + } + + public ISocket CreateSocket(IpAddressFamily family, MediaBrowser.Model.Net.SocketType socketType, MediaBrowser.Model.Net.ProtocolType protocolType, bool dualMode) + { + try + { + var addressFamily = family == IpAddressFamily.InterNetwork + ? AddressFamily.InterNetwork + : AddressFamily.InterNetworkV6; + + var socket = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); + + if (dualMode) + { + socket.DualMode = true; + } + + return new NetSocket(socket, _logger, dualMode); + } + catch (SocketException ex) + { + throw new SocketCreateException(ex.SocketErrorCode.ToString(), ex); + } + } + + #region ISocketFactory Members + + /// <summary> + /// Creates a new UDP socket and binds it to the specified local port. + /// </summary> + /// <param name="localPort">An integer specifying the local port to bind the socket to.</param> + public IUdpSocket CreateUdpSocket(int localPort) + { + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + try + { + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + return new UdpSocket(retVal, localPort, IPAddress.Any); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + /// <summary> + /// Creates a new UDP socket that is a member of the SSDP multicast local admin group and binds it to the specified local port. + /// </summary> + /// <returns>An implementation of the <see cref="IUdpSocket"/> interface used by RSSDP components to perform socket operations.</returns> + public IUdpSocket CreateSsdpUdpSocket(IpAddressInfo localIpAddress, int localPort) + { + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + try + { + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 4); + + var localIp = NetworkManager.ToIPAddress(localIpAddress); + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse("239.255.255.250"), localIp)); + return new UdpSocket(retVal, localPort, localIp); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + /// <summary> + /// Creates a new UDP socket that is a member of the specified multicast IP address, and binds it to the specified local port. + /// </summary> + /// <param name="ipAddress">The multicast IP address to make the socket a member of.</param> + /// <param name="multicastTimeToLive">The multicast time to live value for the socket.</param> + /// <param name="localPort">The number of the local port to bind to.</param> + /// <returns></returns> + public IUdpSocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) + { + if (ipAddress == null) throw new ArgumentNullException("ipAddress"); + if (ipAddress.Length == 0) throw new ArgumentException("ipAddress cannot be an empty string.", "ipAddress"); + if (multicastTimeToLive <= 0) throw new ArgumentException("multicastTimeToLive cannot be zero or less.", "multicastTimeToLive"); + if (localPort < 0) throw new ArgumentException("localPort cannot be less than zero.", "localPort"); + + var retVal = new Socket(AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Dgram, System.Net.Sockets.ProtocolType.Udp); + + try + { +#if NET46 + retVal.ExclusiveAddressUse = false; +#else + // The ExclusiveAddressUse socket option is a Windows-specific option that, when set to "true," tells Windows not to allow another socket to use the same local address as this socket + // See https://github.com/dotnet/corefx/pull/11509 for more details + if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)) + { + retVal.ExclusiveAddressUse = false; + } +#endif + //retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Broadcast, true); + retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); + + var localIp = IPAddress.Any; + + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp)); + retVal.MulticastLoopback = true; + + return new UdpSocket(retVal, localPort, localIp); + } + catch + { + if (retVal != null) + retVal.Dispose(); + + throw; + } + } + + #endregion + } +} diff --git a/Emby.Common.Implementations/Net/UdpSocket.cs b/Emby.Common.Implementations/Net/UdpSocket.cs new file mode 100644 index 0000000000..b2af9d162e --- /dev/null +++ b/Emby.Common.Implementations/Net/UdpSocket.cs @@ -0,0 +1,242 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security; +using System.Threading.Tasks; +using Emby.Common.Implementations.Networking; +using MediaBrowser.Model.Net; + +namespace Emby.Common.Implementations.Net +{ + // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS + // Be careful to check any changes compile and work for all platform projects it is shared in. + + internal sealed class UdpSocket : DisposableManagedObjectBase, IUdpSocket + { + + #region Fields + + private Socket _Socket; + private int _LocalPort; + #endregion + + #region Constructors + + public UdpSocket(Socket socket, int localPort, IPAddress ip) + { + if (socket == null) throw new ArgumentNullException("socket"); + + _Socket = socket; + _LocalPort = localPort; + LocalIPAddress = NetworkManager.ToIpAddressInfo(ip); + + _Socket.Bind(new IPEndPoint(ip, _LocalPort)); + } + + #endregion + + public IpAddressInfo LocalIPAddress + { + get; + private set; + } + + #region IUdpSocket Members + + public Task<SocketReceiveResult> ReceiveAsync() + { + ThrowIfDisposed(); + + var tcs = new TaskCompletionSource<SocketReceiveResult>(); + + EndPoint receivedFromEndPoint = new IPEndPoint(IPAddress.Any, 0); + var state = new AsyncReceiveState(_Socket, receivedFromEndPoint); + state.TaskCompletionSource = tcs; + +#if NETSTANDARD1_6 + _Socket.ReceiveFromAsync(new ArraySegment<Byte>(state.Buffer),SocketFlags.None, state.RemoteEndPoint) + .ContinueWith((task, asyncState) => + { + if (task.Status != TaskStatus.Faulted) + { + var receiveState = asyncState as AsyncReceiveState; + receiveState.RemoteEndPoint = task.Result.RemoteEndPoint; + ProcessResponse(receiveState, () => task.Result.ReceivedBytes, LocalIPAddress); + } + }, state); +#else + _Socket.BeginReceiveFrom(state.Buffer, 0, state.Buffer.Length, SocketFlags.None, ref state.RemoteEndPoint, ProcessResponse, state); +#endif + + return tcs.Task; + } + + public Task SendAsync(byte[] buffer, int size, IpEndPointInfo endPoint) + { + ThrowIfDisposed(); + + if (buffer == null) throw new ArgumentNullException("messageData"); + if (endPoint == null) throw new ArgumentNullException("endPoint"); + + var ipEndPoint = NetworkManager.ToIPEndPoint(endPoint); + +#if NETSTANDARD1_6 + + if (size != buffer.Length) + { + byte[] copy = new byte[size]; + Buffer.BlockCopy(buffer, 0, copy, 0, size); + buffer = copy; + } + + _Socket.SendTo(buffer, ipEndPoint); + return Task.FromResult(true); +#else + var taskSource = new TaskCompletionSource<bool>(); + + try + { + _Socket.BeginSendTo(buffer, 0, size, SocketFlags.None, ipEndPoint, result => + { + try + { + _Socket.EndSend(result); + taskSource.TrySetResult(true); + } + catch (Exception ex) + { + taskSource.TrySetException(ex); + } + + }, null); + } + catch (Exception ex) + { + taskSource.TrySetException(ex); + } + + //_Socket.SendTo(messageData, new System.Net.IPEndPoint(IPAddress.Parse(RemoteEndPoint.IPAddress), RemoteEndPoint.Port)); + + return taskSource.Task; +#endif + } + + #endregion + + #region Overrides + + protected override void Dispose(bool disposing) + { + if (disposing) + { + var socket = _Socket; + if (socket != null) + socket.Dispose(); + } + } + + #endregion + + #region Private Methods + + private static void ProcessResponse(AsyncReceiveState state, Func<int> receiveData, IpAddressInfo localIpAddress) + { + try + { + var bytesRead = receiveData(); + + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; + state.TaskCompletionSource.SetResult( + new SocketReceiveResult + { + Buffer = state.Buffer, + ReceivedBytes = bytesRead, + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = localIpAddress + } + ); + } + catch (ObjectDisposedException) + { + state.TaskCompletionSource.SetCanceled(); + } + catch (SocketException se) + { + if (se.SocketErrorCode != SocketError.Interrupted && se.SocketErrorCode != SocketError.OperationAborted && se.SocketErrorCode != SocketError.Shutdown) + state.TaskCompletionSource.SetException(se); + else + state.TaskCompletionSource.SetCanceled(); + } + catch (Exception ex) + { + state.TaskCompletionSource.SetException(ex); + } + } + + private static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) + { + if (endpoint == null) + { + return null; + } + + return NetworkManager.ToIpEndPointInfo(endpoint); + } + + private void ProcessResponse(IAsyncResult asyncResult) + { +#if NET46 + var state = asyncResult.AsyncState as AsyncReceiveState; + try + { + var bytesRead = state.Socket.EndReceiveFrom(asyncResult, ref state.RemoteEndPoint); + + var ipEndPoint = state.RemoteEndPoint as IPEndPoint; + state.TaskCompletionSource.SetResult( + new SocketReceiveResult + { + Buffer = state.Buffer, + ReceivedBytes = bytesRead, + RemoteEndPoint = ToIpEndPointInfo(ipEndPoint), + LocalIPAddress = LocalIPAddress + } + ); + } + catch (ObjectDisposedException) + { + state.TaskCompletionSource.SetCanceled(); + } + catch (Exception ex) + { + state.TaskCompletionSource.SetException(ex); + } +#endif + } + + #endregion + + #region Private Classes + + private class AsyncReceiveState + { + public AsyncReceiveState(Socket socket, EndPoint remoteEndPoint) + { + this.Socket = socket; + this.RemoteEndPoint = remoteEndPoint; + } + + public EndPoint RemoteEndPoint; + public byte[] Buffer = new byte[8192]; + + public Socket Socket { get; private set; } + + public TaskCompletionSource<SocketReceiveResult> TaskCompletionSource { get; set; } + + } + + #endregion + + } +} diff --git a/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs b/Emby.Common.Implementations/Networking/NetworkManager.cs similarity index 58% rename from MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs rename to Emby.Common.Implementations/Networking/NetworkManager.cs index 1b5e260d71..4485e8b14e 100644 --- a/MediaBrowser.Common.Implementations/Networking/BaseNetworkManager.cs +++ b/Emby.Common.Implementations/Networking/NetworkManager.cs @@ -6,28 +6,28 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; -using MoreLinq; +using System.Threading.Tasks; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.IO; +using MediaBrowser.Common.Net; -namespace MediaBrowser.Common.Implementations.Networking +namespace Emby.Common.Implementations.Networking { - public abstract class BaseNetworkManager + public class NetworkManager : INetworkManager { protected ILogger Logger { get; private set; } private DateTime _lastRefresh; - protected BaseNetworkManager(ILogger logger) + public NetworkManager(ILogger logger) { Logger = logger; } - private List<IPAddress> _localIpAddresses; + private List<IpAddressInfo> _localIpAddresses; private readonly object _localIpAddressSyncLock = new object(); - /// <summary> - /// Gets the machine's local ip address - /// </summary> - /// <returns>IPAddress.</returns> - public IEnumerable<IPAddress> GetLocalIpAddresses() + public List<IpAddressInfo> GetLocalIpAddresses() { const int cacheMinutes = 5; @@ -37,7 +37,7 @@ namespace MediaBrowser.Common.Implementations.Networking if (_localIpAddresses == null || forceRefresh) { - var addresses = GetLocalIpAddressesInternal().ToList(); + var addresses = GetLocalIpAddressesInternal().Select(ToIpAddressInfo).ToList(); _localIpAddresses = addresses; _lastRefresh = DateTime.UtcNow; @@ -49,24 +49,24 @@ namespace MediaBrowser.Common.Implementations.Networking return _localIpAddresses; } - private IEnumerable<IPAddress> GetLocalIpAddressesInternal() + private IEnumerable<IPAddress> GetLocalIpAddressesInternal() { var list = GetIPsDefault() .ToList(); if (list.Count == 0) { - list.AddRange(GetLocalIpAddressesFallback()); + list.AddRange(GetLocalIpAddressesFallback().Result); } - return list.Where(FilterIpAddress).DistinctBy(i => i.ToString()); + return list.Where(FilterIpAddress).DistinctBy(i => i.ToString()); } - private bool FilterIpAddress(IPAddress address) + private bool FilterIpAddress(IPAddress address) { - var addressString = address.ToString (); + var addressString = address.ToString(); - if (addressString.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) + if (addressString.StartsWith("169.", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -154,12 +154,12 @@ namespace MediaBrowser.Common.Implementations.Networking { var prefix = addressString.Substring(0, lengthMatch); - if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) + if (GetLocalIpAddresses().Any(i => i.ToString().StartsWith(prefix, StringComparison.OrdinalIgnoreCase))) { return true; } } - } + } else if (resolveHost) { Uri uri; @@ -170,7 +170,7 @@ namespace MediaBrowser.Common.Implementations.Networking var host = uri.DnsSafeHost; Logger.Debug("Resolving host {0}", host); - address = GetIpAddresses(host).FirstOrDefault(); + address = GetIpAddresses(host).Result.FirstOrDefault(); if (address != null) { @@ -193,52 +193,76 @@ namespace MediaBrowser.Common.Implementations.Networking return false; } - public IEnumerable<IPAddress> GetIpAddresses(string hostName) + private Task<IPAddress[]> GetIpAddresses(string hostName) { - return Dns.GetHostAddresses(hostName); + return Dns.GetHostAddressesAsync(hostName); } - private List<IPAddress> GetIPsDefault() - { - NetworkInterface[] interfaces; + private readonly List<NetworkInterfaceType> _validNetworkInterfaceTypes = new List<NetworkInterfaceType> + { + NetworkInterfaceType.Ethernet, + NetworkInterfaceType.Wireless80211 + }; - try - { - interfaces = NetworkInterface.GetAllNetworkInterfaces(); - } - catch (Exception ex) - { - Logger.ErrorException("Error in GetAllNetworkInterfaces", ex); - return new List<IPAddress>(); - } + private List<IPAddress> GetIPsDefault() + { + NetworkInterface[] interfaces; - return interfaces.SelectMany(network => { + try + { + var validStatuses = new[] { OperationalStatus.Up, OperationalStatus.Unknown }; - try - { + interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(i => validStatuses.Contains(i.OperationalStatus)) + .ToArray(); + } + catch (Exception ex) + { + Logger.ErrorException("Error in GetAllNetworkInterfaces", ex); + return new List<IPAddress>(); + } + + return interfaces.SelectMany(network => + { + + try + { Logger.Debug("Querying interface: {0}. Type: {1}. Status: {2}", network.Name, network.NetworkInterfaceType, network.OperationalStatus); - var properties = network.GetIPProperties(); + var ipProperties = network.GetIPProperties(); - return properties.UnicastAddresses - .Where(i => i.IsDnsEligible) + // Try to exclude virtual adapters + // http://stackoverflow.com/questions/8089685/c-sharp-finding-my-machines-local-ip-address-and-not-the-vms + var addr = ipProperties.GatewayAddresses.FirstOrDefault(); + if (addr == null|| string.Equals(addr.Address.ToString(), "0.0.0.0", StringComparison.OrdinalIgnoreCase)) + { + return new List<IPAddress>(); + } + + //if (!_validNetworkInterfaceTypes.Contains(network.NetworkInterfaceType)) + //{ + // return new List<IPAddress>(); + //} + + return ipProperties.UnicastAddresses + //.Where(i => i.IsDnsEligible) .Select(i => i.Address) .Where(i => i.AddressFamily == AddressFamily.InterNetwork) - .ToList(); - } - catch (Exception ex) - { - Logger.ErrorException("Error querying network interface", ex); - return new List<IPAddress>(); - } + .ToList(); + } + catch (Exception ex) + { + Logger.ErrorException("Error querying network interface", ex); + return new List<IPAddress>(); + } - }).DistinctBy(i => i.ToString()) - .ToList(); - } + }).DistinctBy(i => i.ToString()) + .ToList(); + } - private IEnumerable<IPAddress> GetLocalIpAddressesFallback() + private async Task<IEnumerable<IPAddress>> GetLocalIpAddressesFallback() { - var host = Dns.GetHostEntry(Dns.GetHostName()); + var host = await Dns.GetHostEntryAsync(Dns.GetHostName()).ConfigureAwait(false); // Reverse them because the last one is usually the correct one // It's not fool-proof so ultimately the consumer will have to examine them and decide @@ -279,7 +303,7 @@ namespace MediaBrowser.Common.Implementations.Networking /// <returns>IPEndPoint.</returns> public IPEndPoint Parse(string endpointstring) { - return Parse(endpointstring, -1); + return Parse(endpointstring, -1).Result; } /// <summary> @@ -290,7 +314,7 @@ namespace MediaBrowser.Common.Implementations.Networking /// <returns>IPEndPoint.</returns> /// <exception cref="System.ArgumentException">Endpoint descriptor may not be empty.</exception> /// <exception cref="System.FormatException"></exception> - private static IPEndPoint Parse(string endpointstring, int defaultport) + private static async Task<IPEndPoint> Parse(string endpointstring, int defaultport) { if (String.IsNullOrEmpty(endpointstring) || endpointstring.Trim().Length == 0) @@ -316,7 +340,7 @@ namespace MediaBrowser.Common.Implementations.Networking //try to use the address as IPv4, otherwise get hostname if (!IPAddress.TryParse(values[0], out ipaddy)) - ipaddy = GetIPfromHost(values[0]); + ipaddy = await GetIPfromHost(values[0]).ConfigureAwait(false); } else if (values.Length > 2) //ipv6 { @@ -372,14 +396,130 @@ namespace MediaBrowser.Common.Implementations.Networking /// <param name="p">The p.</param> /// <returns>IPAddress.</returns> /// <exception cref="System.ArgumentException"></exception> - private static IPAddress GetIPfromHost(string p) + private static async Task<IPAddress> GetIPfromHost(string p) { - var hosts = Dns.GetHostAddresses(p); + var hosts = await Dns.GetHostAddressesAsync(p).ConfigureAwait(false); if (hosts == null || hosts.Length == 0) throw new ArgumentException(String.Format("Host not found: {0}", p)); return hosts[0]; } + + public IpAddressInfo ParseIpAddress(string ipAddress) + { + IpAddressInfo info; + if (TryParseIpAddress(ipAddress, out info)) + { + return info; + } + + throw new ArgumentException("Invalid ip address: " + ipAddress); + } + + public bool TryParseIpAddress(string ipAddress, out IpAddressInfo ipAddressInfo) + { + IPAddress address; + if (IPAddress.TryParse(ipAddress, out address)) + { + ipAddressInfo = ToIpAddressInfo(address); + return true; + } + + ipAddressInfo = null; + return false; + } + + public static IpEndPointInfo ToIpEndPointInfo(IPEndPoint endpoint) + { + if (endpoint == null) + { + return null; + } + + return new IpEndPointInfo(ToIpAddressInfo(endpoint.Address), endpoint.Port); + } + + public static IPEndPoint ToIPEndPoint(IpEndPointInfo endpoint) + { + if (endpoint == null) + { + return null; + } + + return new IPEndPoint(ToIPAddress(endpoint.IpAddress), endpoint.Port); + } + + public static IPAddress ToIPAddress(IpAddressInfo address) + { + if (address.Equals(IpAddressInfo.Any)) + { + return IPAddress.Any; + } + if (address.Equals(IpAddressInfo.IPv6Any)) + { + return IPAddress.IPv6Any; + } + if (address.Equals(IpAddressInfo.Loopback)) + { + return IPAddress.Loopback; + } + if (address.Equals(IpAddressInfo.IPv6Loopback)) + { + return IPAddress.IPv6Loopback; + } + + return IPAddress.Parse(address.Address); + } + + public static IpAddressInfo ToIpAddressInfo(IPAddress address) + { + if (address.Equals(IPAddress.Any)) + { + return IpAddressInfo.Any; + } + if (address.Equals(IPAddress.IPv6Any)) + { + return IpAddressInfo.IPv6Any; + } + if (address.Equals(IPAddress.Loopback)) + { + return IpAddressInfo.Loopback; + } + if (address.Equals(IPAddress.IPv6Loopback)) + { + return IpAddressInfo.IPv6Loopback; + } + return new IpAddressInfo + { + Address = address.ToString(), + AddressFamily = address.AddressFamily == AddressFamily.InterNetworkV6 ? IpAddressFamily.InterNetworkV6 : IpAddressFamily.InterNetwork + }; + } + + public async Task<IpAddressInfo[]> GetHostAddressesAsync(string host) + { + var addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false); + return addresses.Select(ToIpAddressInfo).ToArray(); + } + + /// <summary> + /// Gets the network shares. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>IEnumerable{NetworkShare}.</returns> + public virtual IEnumerable<NetworkShare> GetNetworkShares(string path) + { + return new List<NetworkShare>(); + } + + /// <summary> + /// Gets available devices within the domain + /// </summary> + /// <returns>PC's in the Domain</returns> + public virtual IEnumerable<FileSystemEntryInfo> GetNetworkDevices() + { + return new List<FileSystemEntryInfo>(); + } } } diff --git a/Emby.Common.Implementations/Properties/AssemblyInfo.cs b/Emby.Common.Implementations/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..1a5abcb274 --- /dev/null +++ b/Emby.Common.Implementations/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Common.Implementations")] +[assembly: AssemblyTrademark("")] + +// 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)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5a27010a-09c6-4e86-93ea-437484c10917")] diff --git a/Emby.Common.Implementations/Reflection/AssemblyInfo.cs b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs new file mode 100644 index 0000000000..7a92f02d6b --- /dev/null +++ b/Emby.Common.Implementations/Reflection/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using MediaBrowser.Model.Reflection; +using System.Reflection; + +namespace Emby.Common.Implementations.Reflection +{ + public class AssemblyInfo : IAssemblyInfo + { + public Stream GetManifestResourceStream(Type type, string resource) + { +#if NET46 + return type.Assembly.GetManifestResourceStream(resource); +#endif + return type.GetTypeInfo().Assembly.GetManifestResourceStream(resource); + } + + public string[] GetManifestResourceNames(Type type) + { +#if NET46 + return type.Assembly.GetManifestResourceNames(); +#endif + return type.GetTypeInfo().Assembly.GetManifestResourceNames(); + } + + public Assembly[] GetCurrentAssemblies() + { + return AppDomain.CurrentDomain.GetAssemblies(); + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs similarity index 96% rename from MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs rename to Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs index 3d33e958de..5735f80260 100644 --- a/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs +++ b/Emby.Common.Implementations/ScheduledTasks/DailyTrigger.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using System; +using System; using System.Globalization; using System.Threading; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Represents a task trigger that fires everyday diff --git a/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs similarity index 96% rename from MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs rename to Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs index 8038d5551d..4d2769d8fb 100644 --- a/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs +++ b/Emby.Common.Implementations/ScheduledTasks/IntervalTrigger.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using System; +using System; using System.Linq; using System.Threading; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Represents a task trigger that runs repeatedly on an interval diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs similarity index 80% rename from MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs rename to Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index ced85780f8..cbc7c7c2d8 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Common.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -1,20 +1,20 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Events; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Tasks; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Model.Events; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.Implementations.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Class ScheduledTaskWorker @@ -53,6 +53,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <value>The task manager.</value> private ITaskManager TaskManager { get; set; } private readonly IFileSystem _fileSystem; + private readonly ISystemEvents _systemEvents; /// <summary> /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class. @@ -73,7 +74,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// or /// logger /// </exception> - public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem) + public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents) { if (scheduledTask == null) { @@ -102,6 +103,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks JsonSerializer = jsonSerializer; Logger = logger; _fileSystem = fileSystem; + _systemEvents = systemEvents; InitTriggerEvents(); } @@ -232,13 +234,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <summary> /// The _triggers /// </summary> - private List<ITaskTrigger> _triggers; + private Tuple<TaskTriggerInfo,ITaskTrigger>[] _triggers; /// <summary> /// Gets the triggers that define when the task will run /// </summary> /// <value>The triggers.</value> - /// <exception cref="System.ArgumentNullException">value</exception> - public IEnumerable<ITaskTrigger> Triggers + private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers { get { @@ -257,11 +258,33 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks DisposeTriggers(); } - _triggers = value.ToList(); + _triggers = value.ToArray(); ReloadTriggerEvents(false); + } + } - SaveTriggers(_triggers); + /// <summary> + /// Gets the triggers that define when the task will run + /// </summary> + /// <value>The triggers.</value> + /// <exception cref="System.ArgumentNullException">value</exception> + public TaskTriggerInfo[] Triggers + { + get + { + return InternalTriggers.Select(i => i.Item1).ToArray(); + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + + SaveTriggers(value); + + InternalTriggers = value.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(); } } @@ -304,8 +327,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param> private void ReloadTriggerEvents(bool isApplicationStartup) { - foreach (var trigger in Triggers) + foreach (var triggerInfo in InternalTriggers) { + var trigger = triggerInfo.Item2; + trigger.Stop(); trigger.Triggered -= trigger_Triggered; @@ -362,6 +387,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks finally { _currentTask = null; + GC.Collect(); } } @@ -507,23 +533,29 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// Loads the triggers. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - private List<ITaskTrigger> LoadTriggers() + private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers() + { + var settings = LoadTriggerSettings(); + + return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(); + } + + private TaskTriggerInfo[] LoadTriggerSettings() { try { return JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(GetConfigurationFilePath()) - .Select(ScheduledTaskHelpers.GetTrigger) - .ToList(); + .ToArray(); } catch (FileNotFoundException) { // File doesn't exist. No biggie. Return defaults. - return ScheduledTask.GetDefaultTriggers().ToList(); + return ScheduledTask.GetDefaultTriggers().ToArray(); } catch (DirectoryNotFoundException) { // File doesn't exist. No biggie. Return defaults. - return ScheduledTask.GetDefaultTriggers().ToList(); + return ScheduledTask.GetDefaultTriggers().ToArray(); } } @@ -531,13 +563,13 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// Saves the triggers. /// </summary> /// <param name="triggers">The triggers.</param> - private void SaveTriggers(IEnumerable<ITaskTrigger> triggers) + private void SaveTriggers(TaskTriggerInfo[] triggers) { var path = GetConfigurationFilePath(); _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); - JsonSerializer.SerializeToFile(triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), path); + JsonSerializer.SerializeToFile(triggers, path); } /// <summary> @@ -561,11 +593,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks Id = Id }; - var hasKey = ScheduledTask as IHasKey; - if (hasKey != null) - { - result.Key = hasKey.Key; - } + result.Key = ScheduledTask.Key; if (ex != null) { @@ -655,13 +683,98 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks } } + /// <summary> + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger + /// </summary> + /// <param name="info">The info.</param> + /// <returns>BaseTaskTrigger.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception> + private ITaskTrigger GetTrigger(TaskTriggerInfo info) + { + var options = new TaskExecutionOptions + { + MaxRuntimeMs = info.MaxRuntimeMs + }; + + if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentNullException(); + } + + return new DailyTrigger + { + TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value), + TaskOptions = options + }; + } + + if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentNullException(); + } + + if (!info.DayOfWeek.HasValue) + { + throw new ArgumentNullException(); + } + + return new WeeklyTrigger + { + TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value), + DayOfWeek = info.DayOfWeek.Value, + TaskOptions = options + }; + } + + if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.IntervalTicks.HasValue) + { + throw new ArgumentNullException(); + } + + return new IntervalTrigger + { + Interval = TimeSpan.FromTicks(info.IntervalTicks.Value), + TaskOptions = options + }; + } + + if (info.Type.Equals(typeof(SystemEventTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.SystemEvent.HasValue) + { + throw new ArgumentNullException(); + } + + return new SystemEventTrigger(_systemEvents) + { + SystemEvent = info.SystemEvent.Value, + TaskOptions = options + }; + } + + if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + return new StartupTrigger(); + } + + throw new ArgumentException("Unrecognized trigger type: " + info.Type); + } + /// <summary> /// Disposes each trigger /// </summary> private void DisposeTriggers() { - foreach (var trigger in Triggers) + foreach (var triggerInfo in InternalTriggers) { + var trigger = triggerInfo.Item2; trigger.Triggered -= trigger_Triggered; trigger.Stop(); } diff --git a/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs similarity index 94% rename from MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs rename to Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs index 41f58a7ad5..8aae644bc9 100644 --- a/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs +++ b/Emby.Common.Implementations/ScheduledTasks/StartupTrigger.cs @@ -1,10 +1,10 @@ -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using System; +using System; using System.Threading.Tasks; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Class StartupTaskTrigger diff --git a/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs similarity index 73% rename from MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs rename to Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs index 9972dc8044..a136a975ae 100644 --- a/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs +++ b/Emby.Common.Implementations/ScheduledTasks/SystemEventTrigger.cs @@ -1,11 +1,11 @@ -using MediaBrowser.Model.Events; -using MediaBrowser.Model.Tasks; -using Microsoft.Win32; -using System; +using System; using System.Threading.Tasks; +using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Class SystemEventTrigger @@ -26,6 +26,13 @@ namespace MediaBrowser.Common.ScheduledTasks /// </value> public TaskExecutionOptions TaskOptions { get; set; } + private readonly ISystemEvents _systemEvents; + + public SystemEventTrigger(ISystemEvents systemEvents) + { + _systemEvents = systemEvents; + } + /// <summary> /// Stars waiting for the trigger action /// </summary> @@ -36,33 +43,28 @@ namespace MediaBrowser.Common.ScheduledTasks switch (SystemEvent) { case SystemEvent.WakeFromSleep: - SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + _systemEvents.Resume += _systemEvents_Resume; break; } } + private async void _systemEvents_Resume(object sender, EventArgs e) + { + if (SystemEvent == SystemEvent.WakeFromSleep) + { + // This value is a bit arbitrary, but add a delay to help ensure network connections have been restored before running the task + await Task.Delay(10000).ConfigureAwait(false); + + OnTriggered(); + } + } + /// <summary> /// Stops waiting for the trigger action /// </summary> public void Stop() { - SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; - } - - /// <summary> - /// Handles the PowerModeChanged event of the SystemEvents control. - /// </summary> - /// <param name="sender">The source of the event.</param> - /// <param name="e">The <see cref="PowerModeChangedEventArgs" /> instance containing the event data.</param> - async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) - { - if (e.Mode == PowerModes.Resume && SystemEvent == SystemEvent.WakeFromSleep) - { - // This value is a bit arbitrary, but add a delay to help ensure network connections have been restored before running the task - await Task.Delay(10000).ConfigureAwait(false); - - OnTriggered(); - } + _systemEvents.Resume -= _systemEvents_Resume; } /// <summary> diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs similarity index 91% rename from MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs rename to Emby.Common.Implementations/ScheduledTasks/TaskManager.cs index b3a00b35f5..b0153c5882 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Common.Implementations/ScheduledTasks/TaskManager.cs @@ -1,6 +1,5 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; @@ -10,10 +9,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using CommonIO; -using Microsoft.Win32; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.System; -namespace MediaBrowser.Common.Implementations.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Class TaskManager @@ -47,6 +46,8 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <value>The application paths.</value> private IApplicationPaths ApplicationPaths { get; set; } + private readonly ISystemEvents _systemEvents; + /// <summary> /// Gets the logger. /// </summary> @@ -54,25 +55,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks private ILogger Logger { get; set; } private readonly IFileSystem _fileSystem; - private bool _suspendTriggers; - - public bool SuspendTriggers - { - get { return _suspendTriggers; } - set - { - Logger.Info("Setting SuspendTriggers to {0}", value); - var executeQueued = _suspendTriggers && !value; - - _suspendTriggers = value; - - if (executeQueued) - { - ExecuteQueuedTasks(); - } - } - } - /// <summary> /// Initializes a new instance of the <see cref="TaskManager" /> class. /// </summary> @@ -80,29 +62,23 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> /// <exception cref="System.ArgumentException">kernel</exception> - public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem) + public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents) { ApplicationPaths = applicationPaths; JsonSerializer = jsonSerializer; Logger = logger; _fileSystem = fileSystem; + _systemEvents = systemEvents; ScheduledTasks = new IScheduledTaskWorker[] { }; } private void BindToSystemEvent() { - try - { - SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; - } - catch - { - - } + _systemEvents.Resume += _systemEvents_Resume; } - void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) + private void _systemEvents_Resume(object sender, EventArgs e) { foreach (var task in ScheduledTasks) { @@ -235,7 +211,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks lock (_taskQueue) { - if (task.State == TaskState.Idle && !SuspendTriggers) + if (task.State == TaskState.Idle) { Execute(task, options); return; @@ -254,7 +230,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks var myTasks = ScheduledTasks.ToList(); var list = tasks.ToList(); - myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, _fileSystem))); + myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, _fileSystem, _systemEvents))); ScheduledTasks = myTasks.ToArray(); @@ -327,11 +303,6 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks /// </summary> private void ExecuteQueuedTasks() { - if (SuspendTriggers) - { - return; - } - Logger.Info("ExecuteQueuedTasks"); // Execute queued tasks diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs similarity index 90% rename from MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs rename to Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 0a2b9222a0..1cad2e9b83 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -1,15 +1,15 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Model.Logging; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks +namespace Emby.Common.Implementations.ScheduledTasks.Tasks { /// <summary> /// Deletes old cache files @@ -40,13 +40,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// Creates the triggers that define when the task will run /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<ITaskTrigger> GetDefaultTriggers() + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - // Until we can vary these default triggers per server and MBT, we need something that makes sense for both - return new ITaskTrigger[] { + return new[] { // Every so often - new IntervalTrigger { Interval = TimeSpan.FromHours(24)} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; } @@ -95,7 +94,7 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// <param name="progress">The progress.</param> private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress) { - var filesToDelete = _fileSystem.GetFiles(directory, true) + var filesToDelete = _fileSystem.GetFiles(directory, true) .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) .ToList(); @@ -168,6 +167,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks get { return "Cache file cleanup"; } } + public string Key + { + get { return "DeleteCacheFiles"; } + } + /// <summary> /// Gets the description. /// </summary> @@ -202,5 +206,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks { get { return true; } } + + public bool IsLogged + { + get { return true; } + } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs similarity index 86% rename from MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs rename to Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index b18ea03b1e..3f43fa8894 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -1,13 +1,13 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.ScheduledTasks; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks +namespace Emby.Common.Implementations.ScheduledTasks.Tasks { /// <summary> /// Deletes old log files @@ -36,13 +36,12 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// Creates the triggers that define when the task will run /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<ITaskTrigger> GetDefaultTriggers() + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - // Until we can vary these default triggers per server and MBT, we need something that makes sense for both - return new ITaskTrigger[] { + return new[] { // Every so often - new IntervalTrigger { Interval = TimeSpan.FromHours(24)} + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; } @@ -82,6 +81,11 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks return Task.FromResult(true); } + public string Key + { + get { return "CleanLogFiles"; } + } + /// <summary> /// Gets the name of the task /// </summary> @@ -125,5 +129,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks { get { return true; } } + + public bool IsLogged + { + get { return true; } + } } } diff --git a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs b/Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs similarity index 86% rename from MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs rename to Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs index 78f60632fa..80411de055 100644 --- a/MediaBrowser.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs +++ b/Emby.Common.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs @@ -1,12 +1,12 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Model.Logging; -using System; +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks +namespace Emby.Common.Implementations.ScheduledTasks.Tasks { /// <summary> /// Class ReloadLoggerFileTask @@ -39,9 +39,9 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks /// Gets the default triggers. /// </summary> /// <returns>IEnumerable{BaseTaskTrigger}.</returns> - public IEnumerable<ITaskTrigger> GetDefaultTriggers() + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(0) }; //12am + var trigger = new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(0).Ticks }; //12am return new[] { trigger }; } @@ -74,6 +74,8 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks get { return "Start new log file"; } } + public string Key { get; } + /// <summary> /// Gets the description. /// </summary> @@ -101,5 +103,10 @@ namespace MediaBrowser.Common.Implementations.ScheduledTasks.Tasks { get { return true; } } + + public bool IsLogged + { + get { return true; } + } } } diff --git a/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs b/Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs similarity index 98% rename from MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs rename to Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs index 318802e07d..91540ba164 100644 --- a/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs +++ b/Emby.Common.Implementations/ScheduledTasks/WeeklyTrigger.cs @@ -4,7 +4,7 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Common.ScheduledTasks +namespace Emby.Common.Implementations.ScheduledTasks { /// <summary> /// Represents a task trigger that fires on a weekly basis diff --git a/MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs b/Emby.Common.Implementations/Serialization/JsonSerializer.cs similarity index 96% rename from MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs rename to Emby.Common.Implementations/Serialization/JsonSerializer.cs index 5dbbe53731..c9db336890 100644 --- a/MediaBrowser.Common.Implementations/Serialization/JsonSerializer.cs +++ b/Emby.Common.Implementations/Serialization/JsonSerializer.cs @@ -1,10 +1,10 @@ -using MediaBrowser.Model.Serialization; -using System; +using System; using System.IO; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; -namespace MediaBrowser.Common.Implementations.Serialization +namespace Emby.Common.Implementations.Serialization { /// <summary> /// Provides a wrapper around third party json serialization. @@ -60,7 +60,7 @@ namespace MediaBrowser.Common.Implementations.Serialization throw new ArgumentNullException("file"); } - using (Stream stream = _fileSystem.GetFileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (Stream stream = _fileSystem.GetFileStream(file, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { SerializeToStream(obj, stream); } diff --git a/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs b/Emby.Common.Implementations/Serialization/XmlSerializer.cs similarity index 78% rename from MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs rename to Emby.Common.Implementations/Serialization/XmlSerializer.cs index 77f65b0c7a..3583f998e5 100644 --- a/MediaBrowser.Common.Implementations/Serialization/XmlSerializer.cs +++ b/Emby.Common.Implementations/Serialization/XmlSerializer.cs @@ -4,20 +4,22 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Xml; -using CommonIO; +using System.Xml.Serialization; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; -namespace MediaBrowser.Common.Implementations.Serialization +namespace Emby.Common.Implementations.Serialization { /// <summary> /// Provides a wrapper around third party xml serialization. /// </summary> - public class XmlSerializer : IXmlSerializer + public class MyXmlSerializer : IXmlSerializer { - private readonly IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; private readonly ILogger _logger; - public XmlSerializer(IFileSystem fileSystem, ILogger logger) + public MyXmlSerializer(IFileSystem fileSystem, ILogger logger) { _fileSystem = fileSystem; _logger = logger; @@ -25,18 +27,18 @@ namespace MediaBrowser.Common.Implementations.Serialization // Need to cache these // http://dotnetcodebox.blogspot.com/2013/01/xmlserializer-class-may-result-in.html - private readonly Dictionary<string, System.Xml.Serialization.XmlSerializer> _serializers = - new Dictionary<string, System.Xml.Serialization.XmlSerializer>(); + private readonly Dictionary<string, XmlSerializer> _serializers = + new Dictionary<string, XmlSerializer>(); - private System.Xml.Serialization.XmlSerializer GetSerializer(Type type) + private XmlSerializer GetSerializer(Type type) { var key = type.FullName; lock (_serializers) { - System.Xml.Serialization.XmlSerializer serializer; + XmlSerializer serializer; if (!_serializers.TryGetValue(key, out serializer)) { - serializer = new System.Xml.Serialization.XmlSerializer(type); + serializer = new XmlSerializer(type); _serializers[key] = serializer; } return serializer; @@ -48,9 +50,8 @@ namespace MediaBrowser.Common.Implementations.Serialization /// </summary> /// <param name="obj">The obj.</param> /// <param name="writer">The writer.</param> - private void SerializeToWriter(object obj, XmlTextWriter writer) + private void SerializeToWriter(object obj, XmlWriter writer) { - writer.Formatting = Formatting.Indented; var netSerializer = GetSerializer(obj.GetType()); netSerializer.Serialize(writer, obj); } @@ -63,7 +64,7 @@ namespace MediaBrowser.Common.Implementations.Serialization /// <returns>System.Object.</returns> public object DeserializeFromStream(Type type, Stream stream) { - using (var reader = new XmlTextReader(stream)) + using (var reader = XmlReader.Create(stream)) { var netSerializer = GetSerializer(type); return netSerializer.Deserialize(reader); @@ -77,10 +78,18 @@ namespace MediaBrowser.Common.Implementations.Serialization /// <param name="stream">The stream.</param> public void SerializeToStream(object obj, Stream stream) { - using (var writer = new XmlTextWriter(stream, null)) +#if NET46 + using (var writer = new XmlTextWriter(stream, null)) + { + writer.Formatting = Formatting.Indented; + SerializeToWriter(obj, writer); + } +#else + using (var writer = XmlWriter.Create(stream)) { SerializeToWriter(obj, writer); } +#endif } /// <summary> diff --git a/Emby.Common.Implementations/TextEncoding/TextEncoding.cs b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs new file mode 100644 index 0000000000..254d352224 --- /dev/null +++ b/Emby.Common.Implementations/TextEncoding/TextEncoding.cs @@ -0,0 +1,43 @@ +using System.Text; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Text; + +namespace Emby.Common.Implementations.TextEncoding +{ + public class TextEncoding : ITextEncoding + { + private readonly IFileSystem _fileSystem; + + public TextEncoding(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } + + public Encoding GetASCIIEncoding() + { + return Encoding.ASCII; + } + + public Encoding GetFileEncoding(string srcFile) + { + // *** Detect byte order mark if any - otherwise assume default + var buffer = new byte[5]; + + using (var file = _fileSystem.OpenRead(srcFile)) + { + file.Read(buffer, 0, 5); + } + + if (buffer[0] == 0xef && buffer[1] == 0xbb && buffer[2] == 0xbf) + return Encoding.UTF8; + if (buffer[0] == 0xfe && buffer[1] == 0xff) + return Encoding.Unicode; + if (buffer[0] == 0 && buffer[1] == 0 && buffer[2] == 0xfe && buffer[3] == 0xff) + return Encoding.UTF32; + if (buffer[0] == 0x2b && buffer[1] == 0x2f && buffer[2] == 0x76) + return Encoding.UTF7; + + return null; + } + } +} diff --git a/Emby.Common.Implementations/Threading/CommonTimer.cs b/Emby.Common.Implementations/Threading/CommonTimer.cs new file mode 100644 index 0000000000..8895f6798a --- /dev/null +++ b/Emby.Common.Implementations/Threading/CommonTimer.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Threading; + +namespace Emby.Common.Implementations.Threading +{ + public class CommonTimer : ITimer + { + private readonly Timer _timer; + + public CommonTimer(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period) + { + _timer = new Timer(new TimerCallback(callback), state, dueTime, period); + } + + public CommonTimer(Action<object> callback, object state, int dueTimeMs, int periodMs) + { + _timer = new Timer(new TimerCallback(callback), state, dueTimeMs, periodMs); + } + + public void Change(TimeSpan dueTime, TimeSpan period) + { + _timer.Change(dueTime, period); + } + + public void Change(int dueTimeMs, int periodMs) + { + _timer.Change(dueTimeMs, periodMs); + } + + public void Dispose() + { + _timer.Dispose(); + } + } +} diff --git a/Emby.Common.Implementations/Threading/TimerFactory.cs b/Emby.Common.Implementations/Threading/TimerFactory.cs new file mode 100644 index 0000000000..028dd09639 --- /dev/null +++ b/Emby.Common.Implementations/Threading/TimerFactory.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.Threading; + +namespace Emby.Common.Implementations.Threading +{ + public class TimerFactory : ITimerFactory + { + public ITimer Create(Action<object> callback, object state, TimeSpan dueTime, TimeSpan period) + { + return new CommonTimer(callback, state, dueTime, period); + } + + public ITimer Create(Action<object> callback, object state, int dueTimeMs, int periodMs) + { + return new CommonTimer(callback, state, dueTimeMs, periodMs); + } + } +} diff --git a/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs new file mode 100644 index 0000000000..806290cf44 --- /dev/null +++ b/Emby.Common.Implementations/Xml/XmlReaderSettingsFactory.cs @@ -0,0 +1,22 @@ +using System.Xml; +using MediaBrowser.Model.Xml; + +namespace Emby.Common.Implementations.Xml +{ + public class XmlReaderSettingsFactory : IXmlReaderSettingsFactory + { + public XmlReaderSettings Create(bool enableValidation) + { + var settings = new XmlReaderSettings(); + + if (!enableValidation) + { +#if NET46 + settings.ValidationType = ValidationType.None; +#endif + } + + return settings; + } + } +} diff --git a/Emby.Common.Implementations/project.json b/Emby.Common.Implementations/project.json new file mode 100644 index 0000000000..674101e8a7 --- /dev/null +++ b/Emby.Common.Implementations/project.json @@ -0,0 +1,71 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + + }, + + "frameworks": { + "net46": { + "frameworkAssemblies": { + "System.Collections": "4.0.0.0", + "System.IO": "4.0.0.0", + "System.Net": "4.0.0.0", + "System.Net.Http": "4.0.0.0", + "System.Net.Primitives": "4.0.0.0", + "System.Net.Http.WebRequest": "4.0.0.0", + "System.Reflection": "4.0.0.0", + "System.Runtime": "4.0.0.0", + "System.Runtime.Extensions": "4.0.0.0", + "System.Text.Encoding": "4.0.0.0", + "System.Threading": "4.0.0.0", + "System.Threading.Tasks": "4.0.0.0", + "System.Xml.ReaderWriter": "4.0.0" + }, + "dependencies": { + "SimpleInjector": "3.2.4", + "ServiceStack.Text": "4.5.4", + "NLog": "4.4.0-betaV15", + "sharpcompress": "0.14.0", + "MediaBrowser.Model": { + "target": "project" + }, + "MediaBrowser.Common": { + "target": "project" + } + } + }, + "netstandard1.6": { + "imports": "dnxcore50", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.IO.FileSystem.DriveInfo": "4.3.0", + "System.Diagnostics.Process": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Net.Requests": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XmlSerializer": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.Net.NetworkInformation": "4.3.0", + "System.Net.NameResolution": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime.Loader": "4.3.0", + "SimpleInjector": "3.2.4", + "ServiceStack.Text.Core": "1.0.27", + "NLog": "4.4.0-betaV15", + "sharpcompress": "0.14.0", + "System.AppDomain": "2.0.11", + "MediaBrowser.Model": { + "target": "project" + }, + "MediaBrowser.Common": { + "target": "project" + } + } + } + } +} diff --git a/MediaBrowser.Dlna/Common/Argument.cs b/Emby.Dlna/Common/Argument.cs similarity index 84% rename from MediaBrowser.Dlna/Common/Argument.cs rename to Emby.Dlna/Common/Argument.cs index a3ff8ecc88..7e61c3d6d5 100644 --- a/MediaBrowser.Dlna/Common/Argument.cs +++ b/Emby.Dlna/Common/Argument.cs @@ -1,5 +1,5 @@  -namespace MediaBrowser.Dlna.Common +namespace Emby.Dlna.Common { public class Argument { diff --git a/MediaBrowser.Dlna/Common/DeviceIcon.cs b/Emby.Dlna/Common/DeviceIcon.cs similarity index 91% rename from MediaBrowser.Dlna/Common/DeviceIcon.cs rename to Emby.Dlna/Common/DeviceIcon.cs index bec10dcc5a..27ae92d6f0 100644 --- a/MediaBrowser.Dlna/Common/DeviceIcon.cs +++ b/Emby.Dlna/Common/DeviceIcon.cs @@ -1,5 +1,5 @@  -namespace MediaBrowser.Dlna.Common +namespace Emby.Dlna.Common { public class DeviceIcon { diff --git a/MediaBrowser.Dlna/Common/DeviceService.cs b/Emby.Dlna/Common/DeviceService.cs similarity index 92% rename from MediaBrowser.Dlna/Common/DeviceService.cs rename to Emby.Dlna/Common/DeviceService.cs index 8f8b175a42..0d91dbe76d 100644 --- a/MediaBrowser.Dlna/Common/DeviceService.cs +++ b/Emby.Dlna/Common/DeviceService.cs @@ -1,5 +1,5 @@  -namespace MediaBrowser.Dlna.Common +namespace Emby.Dlna.Common { public class DeviceService { diff --git a/MediaBrowser.Dlna/Common/ServiceAction.cs b/Emby.Dlna/Common/ServiceAction.cs similarity index 91% rename from MediaBrowser.Dlna/Common/ServiceAction.cs rename to Emby.Dlna/Common/ServiceAction.cs index 7685e217e8..1bcc6a1d68 100644 --- a/MediaBrowser.Dlna/Common/ServiceAction.cs +++ b/Emby.Dlna/Common/ServiceAction.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace MediaBrowser.Dlna.Common +namespace Emby.Dlna.Common { public class ServiceAction { diff --git a/MediaBrowser.Dlna/Common/StateVariable.cs b/Emby.Dlna/Common/StateVariable.cs similarity index 92% rename from MediaBrowser.Dlna/Common/StateVariable.cs rename to Emby.Dlna/Common/StateVariable.cs index 21771e7b8e..7e0bc6ae8b 100644 --- a/MediaBrowser.Dlna/Common/StateVariable.cs +++ b/Emby.Dlna/Common/StateVariable.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace MediaBrowser.Dlna.Common +namespace Emby.Dlna.Common { public class StateVariable { diff --git a/MediaBrowser.Dlna/ConfigurationExtension.cs b/Emby.Dlna/ConfigurationExtension.cs similarity index 96% rename from MediaBrowser.Dlna/ConfigurationExtension.cs rename to Emby.Dlna/ConfigurationExtension.cs index 821e21ccfb..cec885e4a7 100644 --- a/MediaBrowser.Dlna/ConfigurationExtension.cs +++ b/Emby.Dlna/ConfigurationExtension.cs @@ -2,7 +2,7 @@ using MediaBrowser.Model.Configuration; using System.Collections.Generic; -namespace MediaBrowser.Dlna +namespace Emby.Dlna { public static class ConfigurationExtension { diff --git a/MediaBrowser.Dlna/ConnectionManager/ConnectionManager.cs b/Emby.Dlna/ConnectionManager/ConnectionManager.cs similarity index 67% rename from MediaBrowser.Dlna/ConnectionManager/ConnectionManager.cs rename to Emby.Dlna/ConnectionManager/ConnectionManager.cs index 62cd3904dc..3f33f3ebf1 100644 --- a/MediaBrowser.Dlna/ConnectionManager/ConnectionManager.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManager.cs @@ -1,24 +1,27 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Service; using MediaBrowser.Model.Logging; using System.Collections.Generic; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.ConnectionManager +namespace Emby.Dlna.ConnectionManager { public class ConnectionManager : BaseService, IConnectionManager { private readonly IDlnaManager _dlna; private readonly ILogger _logger; private readonly IServerConfigurationManager _config; + protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient) + public ConnectionManager(IDlnaManager dlna, IServerConfigurationManager config, ILogger logger, IHttpClient httpClient, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, httpClient) { _dlna = dlna; _config = config; _logger = logger; + XmlReaderSettingsFactory = xmlReaderSettingsFactory; } public string GetServiceXml(IDictionary<string, string> headers) @@ -31,7 +34,7 @@ namespace MediaBrowser.Dlna.ConnectionManager var profile = _dlna.GetProfile(request.Headers) ?? _dlna.GetDefaultProfile(); - return new ControlHandler(_logger, profile, _config).ProcessControlRequest(request); + return new ControlHandler(_config, _logger, XmlReaderSettingsFactory, profile).ProcessControlRequest(request); } } } diff --git a/MediaBrowser.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs similarity index 96% rename from MediaBrowser.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs rename to Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs index 4efa111591..2a415c58e0 100644 --- a/MediaBrowser.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ConnectionManagerXmlBuilder.cs @@ -1,8 +1,8 @@ -using MediaBrowser.Dlna.Common; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Common; +using Emby.Dlna.Service; using System.Collections.Generic; -namespace MediaBrowser.Dlna.ConnectionManager +namespace Emby.Dlna.ConnectionManager { public class ConnectionManagerXmlBuilder { diff --git a/MediaBrowser.Dlna/ConnectionManager/ControlHandler.cs b/Emby.Dlna/ConnectionManager/ControlHandler.cs similarity index 66% rename from MediaBrowser.Dlna/ConnectionManager/ControlHandler.cs rename to Emby.Dlna/ConnectionManager/ControlHandler.cs index 958d71a2b6..ae983c5e75 100644 --- a/MediaBrowser.Dlna/ConnectionManager/ControlHandler.cs +++ b/Emby.Dlna/ConnectionManager/ControlHandler.cs @@ -1,25 +1,20 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Dlna.Server; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Server; +using Emby.Dlna.Service; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.ConnectionManager +namespace Emby.Dlna.ConnectionManager { public class ControlHandler : BaseControlHandler { private readonly DeviceProfile _profile; - public ControlHandler(ILogger logger, DeviceProfile profile, IServerConfigurationManager config) - : base(config, logger) - { - _profile = profile; - } - - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams) + protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) { if (string.Equals(methodName, "GetProtocolInfo", StringComparison.OrdinalIgnoreCase)) { @@ -31,11 +26,16 @@ namespace MediaBrowser.Dlna.ConnectionManager private IEnumerable<KeyValuePair<string, string>> HandleGetProtocolInfo() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Source", _profile.ProtocolInfo }, { "Sink", "" } }; } + + public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory, DeviceProfile profile) : base(config, logger, xmlReaderSettingsFactory) + { + _profile = profile; + } } } diff --git a/MediaBrowser.Dlna/ConnectionManager/ServiceActionListBuilder.cs b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs similarity index 98% rename from MediaBrowser.Dlna/ConnectionManager/ServiceActionListBuilder.cs rename to Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs index 9dbd4e0e23..9b22b77734 100644 --- a/MediaBrowser.Dlna/ConnectionManager/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ConnectionManager/ServiceActionListBuilder.cs @@ -1,7 +1,7 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System.Collections.Generic; -namespace MediaBrowser.Dlna.ConnectionManager +namespace Emby.Dlna.ConnectionManager { public class ServiceActionListBuilder { diff --git a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs b/Emby.Dlna/ContentDirectory/ContentDirectory.cs similarity index 88% rename from MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs rename to Emby.Dlna/ContentDirectory/ContentDirectory.cs index 093b37df3e..4a36a16eb6 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ContentDirectory.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectory.cs @@ -5,16 +5,17 @@ using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Service; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Linq; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.ContentDirectory +namespace Emby.Dlna.ContentDirectory { public class ContentDirectory : BaseService, IContentDirectory, IDisposable { @@ -29,6 +30,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly IMediaSourceManager _mediaSourceManager; private readonly IUserViewManager _userViewManager; private readonly Func<IMediaEncoder> _mediaEncoder; + protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; public ContentDirectory(IDlnaManager dlna, IUserDataManager userDataManager, @@ -37,7 +39,7 @@ namespace MediaBrowser.Dlna.ContentDirectory IServerConfigurationManager config, IUserManager userManager, ILogger logger, - IHttpClient httpClient, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, Func<IMediaEncoder> mediaEncoder) + IHttpClient httpClient, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, Func<IMediaEncoder> mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, httpClient) { _dlna = dlna; @@ -51,6 +53,7 @@ namespace MediaBrowser.Dlna.ContentDirectory _mediaSourceManager = mediaSourceManager; _userViewManager = userViewManager; _mediaEncoder = mediaEncoder; + XmlReaderSettingsFactory = xmlReaderSettingsFactory; } private int SystemUpdateId @@ -93,7 +96,8 @@ namespace MediaBrowser.Dlna.ContentDirectory _channelManager, _mediaSourceManager, _userViewManager, - _mediaEncoder()) + _mediaEncoder(), + XmlReaderSettingsFactory) .ProcessControlRequest(request); } @@ -122,7 +126,8 @@ namespace MediaBrowser.Dlna.ContentDirectory } // No configuration so it's going to be pretty arbitrary - return _userManager.Users.First(); + return _userManager.Users.FirstOrDefault(i => i.Policy.IsAdministrator) ?? + _userManager.Users.First(); } public void Dispose() diff --git a/MediaBrowser.Dlna/ContentDirectory/ContentDirectoryBrowser.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryBrowser.cs similarity index 92% rename from MediaBrowser.Dlna/ContentDirectory/ContentDirectoryBrowser.cs rename to Emby.Dlna/ContentDirectory/ContentDirectoryBrowser.cs index 2618873669..2b421794ab 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ContentDirectoryBrowser.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryBrowser.cs @@ -10,8 +10,9 @@ using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; +using Emby.Dlna.Server; -namespace MediaBrowser.Dlna.ContentDirectory +namespace Emby.Dlna.ContentDirectory { public class ContentDirectoryBrowser { @@ -90,7 +91,7 @@ namespace MediaBrowser.Dlna.ContentDirectory request.ParentId = "1"; } - builder.AppendFormat("<ObjectID>{0}</ObjectID>", SecurityElement.Escape(request.ParentId)); + builder.AppendFormat("<ObjectID>{0}</ObjectID>", DescriptionXmlBuilder.Escape(request.ParentId)); builder.Append("<BrowseFlag>BrowseDirectChildren</BrowseFlag>"); //builder.Append("<BrowseFlag>BrowseMetadata</BrowseFlag>"); @@ -98,12 +99,12 @@ namespace MediaBrowser.Dlna.ContentDirectory builder.Append("<Filter>*</Filter>"); request.StartIndex = request.StartIndex ?? 0; - builder.AppendFormat("<StartingIndex>{0}</StartingIndex>", SecurityElement.Escape(request.StartIndex.Value.ToString(CultureInfo.InvariantCulture))); + builder.AppendFormat("<StartingIndex>{0}</StartingIndex>", DescriptionXmlBuilder.Escape(request.StartIndex.Value.ToString(CultureInfo.InvariantCulture))); request.Limit = request.Limit ?? 20; if (request.Limit.HasValue) { - builder.AppendFormat("<RequestedCount>{0}</RequestedCount>", SecurityElement.Escape(request.Limit.Value.ToString(CultureInfo.InvariantCulture))); + builder.AppendFormat("<RequestedCount>{0}</RequestedCount>", DescriptionXmlBuilder.Escape(request.Limit.Value.ToString(CultureInfo.InvariantCulture))); } builder.Append("<SortCriteria></SortCriteria>"); diff --git a/MediaBrowser.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs similarity index 97% rename from MediaBrowser.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs rename to Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs index 0e5a2671c9..7a9bc18ad8 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ContentDirectoryXmlBuilder.cs @@ -1,8 +1,8 @@ -using MediaBrowser.Dlna.Common; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Common; +using Emby.Dlna.Service; using System.Collections.Generic; -namespace MediaBrowser.Dlna.ContentDirectory +namespace Emby.Dlna.ContentDirectory { public class ContentDirectoryXmlBuilder { diff --git a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs similarity index 67% rename from MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs rename to Emby.Dlna/ContentDirectory/ControlHandler.cs index 3d1e073f8e..bba8c53d9b 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -6,10 +6,9 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Dlna.Didl; -using MediaBrowser.Dlna.Server; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Didl; +using Emby.Dlna.Server; +using Emby.Dlna.Service; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; @@ -18,14 +17,17 @@ using MediaBrowser.Model.Querying; using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.ContentDirectory +namespace Emby.Dlna.ContentDirectory { public class ControlHandler : BaseControlHandler { @@ -35,7 +37,6 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly IServerConfigurationManager _config; private readonly User _user; private readonly IUserViewManager _userViewManager; - private readonly IMediaEncoder _mediaEncoder; private const string NS_DC = "http://purl.org/dc/elements/1.1/"; private const string NS_DIDL = "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"; @@ -49,8 +50,8 @@ namespace MediaBrowser.Dlna.ContentDirectory private readonly DeviceProfile _profile; - public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder) - : base(config, logger) + public ControlHandler(ILogger logger, ILibraryManager libraryManager, DeviceProfile profile, string serverAddress, string accessToken, IImageProcessor imageProcessor, IUserDataManager userDataManager, User user, int systemUpdateId, IServerConfigurationManager config, ILocalizationManager localization, IChannelManager channelManager, IMediaSourceManager mediaSourceManager, IUserViewManager userViewManager, IMediaEncoder mediaEncoder, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + : base(config, logger, xmlReaderSettingsFactory) { _libraryManager = libraryManager; _userDataManager = userDataManager; @@ -58,14 +59,13 @@ namespace MediaBrowser.Dlna.ContentDirectory _systemUpdateId = systemUpdateId; _channelManager = channelManager; _userViewManager = userViewManager; - _mediaEncoder = mediaEncoder; _profile = profile; _config = config; - _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, libraryManager, _mediaEncoder); + _didlBuilder = new DidlBuilder(profile, user, imageProcessor, serverAddress, accessToken, userDataManager, localization, mediaSourceManager, Logger, libraryManager, mediaEncoder); } - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams) + protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) { var deviceId = "test"; @@ -98,6 +98,9 @@ namespace MediaBrowser.Dlna.ContentDirectory if (string.Equals(methodName, "Search", StringComparison.OrdinalIgnoreCase)) return HandleSearch(methodParams, user, deviceId).Result; + if (string.Equals(methodName, "X_BrowseByLetter", StringComparison.OrdinalIgnoreCase)) + return HandleX_BrowseByLetter(methodParams, user, deviceId).Result; + throw new ResourceNotFoundException("Unexpected control request name: " + methodName); } @@ -118,17 +121,20 @@ namespace MediaBrowser.Dlna.ContentDirectory _userDataManager.SaveUserData(user.Id, item, userdata, UserDataSaveReason.TogglePlayed, CancellationToken.None); - return new Headers(); + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); } private IEnumerable<KeyValuePair<string, string>> HandleGetSearchCapabilities() { - return new Headers(true) { { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } }; + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) + { + { "SearchCaps", "res@resolution,res@size,res@duration,dc:title,dc:creator,upnp:actor,upnp:artist,upnp:genre,upnp:album,dc:date,upnp:class,@id,@refID,@protocolInfo,upnp:author,dc:description,pv:avKeywords" } + }; } private IEnumerable<KeyValuePair<string, string>> HandleGetSortCapabilities() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "SortCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } }; @@ -136,7 +142,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private IEnumerable<KeyValuePair<string, string>> HandleGetSortExtensionCapabilities() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "SortExtensionCaps", "res@duration,res@size,res@bitrate,dc:date,dc:title,dc:size,upnp:album,upnp:artist,upnp:albumArtist,upnp:episodeNumber,upnp:genre,upnp:originalTrackNumber,upnp:rating" } }; @@ -144,14 +150,14 @@ namespace MediaBrowser.Dlna.ContentDirectory private IEnumerable<KeyValuePair<string, string>> HandleGetSystemUpdateID() { - var headers = new Headers(true); + var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); headers.Add("Id", _systemUpdateId.ToString(_usCulture)); return headers; } private IEnumerable<KeyValuePair<string, string>> HandleGetFeatureList() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "FeatureList", GetFeatureListXml() } }; @@ -159,7 +165,7 @@ namespace MediaBrowser.Dlna.ContentDirectory private IEnumerable<KeyValuePair<string, string>> HandleXGetFeatureList() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "FeatureList", GetFeatureListXml() } }; @@ -183,12 +189,24 @@ namespace MediaBrowser.Dlna.ContentDirectory return builder.ToString(); } - private async Task<IEnumerable<KeyValuePair<string, string>>> HandleBrowse(Headers sparams, User user, string deviceId) + public string GetValueOrDefault(IDictionary<string, string> sparams, string key, string defaultValue) + { + string val; + + if (sparams.TryGetValue(key, out val)) + { + return val; + } + + return defaultValue; + } + + private async Task<IEnumerable<KeyValuePair<string, string>>> HandleBrowse(IDictionary<string, string> sparams, User user, string deviceId) { var id = sparams["ObjectID"]; var flag = sparams["BrowseFlag"]; - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); + var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); var provided = 0; @@ -209,80 +227,102 @@ namespace MediaBrowser.Dlna.ContentDirectory start = startVal; } - //var root = GetItem(id) as IMediaFolder; - var result = new XmlDocument(); + var settings = new XmlWriterSettings + { + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; - var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL); - didl.SetAttribute("xmlns:dc", NS_DC); - didl.SetAttribute("xmlns:dlna", NS_DLNA); - didl.SetAttribute("xmlns:upnp", NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); - result.AppendChild(didl); - - var serverItem = GetItemFromObjectId(id, user); - var item = serverItem.Item; + StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); int totalCount; - if (string.Equals(flag, "BrowseMetadata")) + using (XmlWriter writer = XmlWriter.Create(builder, settings)) { - totalCount = 1; + //writer.WriteStartDocument(); - if (item.IsFolder || serverItem.StubType.HasValue) + writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + + writer.WriteAttributeString("xmlns", "dc", null, NS_DC); + writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); + writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + //didl.SetAttribute("xmlns:sec", NS_SEC); + + DidlBuilder.WriteXmlRootAttributes(_profile, writer); + + var serverItem = GetItemFromObjectId(id, user); + var item = serverItem.Item; + + if (string.Equals(flag, "BrowseMetadata")) { - var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); + totalCount = 1; - result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id)); - } - else - { - result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, item, null, null, deviceId, filter)); - } - - provided++; - } - else - { - var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); - totalCount = childrenResult.TotalRecordCount; - - provided = childrenResult.Items.Length; - - foreach (var i in childrenResult.Items) - { - var childItem = i.Item; - var displayStubType = i.StubType; - - if (childItem.IsFolder || displayStubType.HasValue) + if (item.IsFolder || serverItem.StubType.HasValue) { - var childCount = (await GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0).ConfigureAwait(false)) - .TotalRecordCount; + var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); - result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, childItem, displayStubType, item, childCount, filter)); + _didlBuilder.WriteFolderElement(writer, item, serverItem.StubType, null, childrenResult.TotalRecordCount, filter, id); } else { - result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, childItem, item, serverItem.StubType, deviceId, filter)); + _didlBuilder.WriteItemElement(_config.GetDlnaConfiguration(), writer, item, null, null, deviceId, filter); + } + + provided++; + } + else + { + var childrenResult = (await GetUserItems(item, serverItem.StubType, user, sortCriteria, start, requestedCount).ConfigureAwait(false)); + totalCount = childrenResult.TotalRecordCount; + + provided = childrenResult.Items.Length; + + foreach (var i in childrenResult.Items) + { + var childItem = i.Item; + var displayStubType = i.StubType; + + if (childItem.IsFolder || displayStubType.HasValue) + { + var childCount = (await GetUserItems(childItem, displayStubType, user, sortCriteria, null, 0).ConfigureAwait(false)) + .TotalRecordCount; + + _didlBuilder.WriteFolderElement(writer, childItem, displayStubType, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(_config.GetDlnaConfiguration(), writer, childItem, item, serverItem.StubType, deviceId, filter); + } } } + writer.WriteFullEndElement(); + //writer.WriteEndDocument(); } - var resXML = result.OuterXml; + var resXML = builder.ToString(); return new List<KeyValuePair<string, string>> - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + { + new KeyValuePair<string,string>("Result", resXML), + new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), + new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), + new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) + }; } - private async Task<IEnumerable<KeyValuePair<string, string>>> HandleSearch(Headers sparams, User user, string deviceId) + private Task<IEnumerable<KeyValuePair<string, string>>> HandleX_BrowseByLetter(IDictionary<string, string> sparams, User user, string deviceId) { - var searchCriteria = new SearchCriteria(sparams.GetValueOrDefault("SearchCriteria", "")); - var sortCriteria = new SortCriteria(sparams.GetValueOrDefault("SortCriteria", "")); - var filter = new Filter(sparams.GetValueOrDefault("Filter", "*")); + // TODO: Implement this method + return HandleSearch(sparams, user, deviceId); + } + + private async Task<IEnumerable<KeyValuePair<string, string>>> HandleSearch(IDictionary<string, string> sparams, User user, string deviceId) + { + var searchCriteria = new SearchCriteria(GetValueOrDefault(sparams, "SearchCriteria", "")); + var sortCriteria = new SortCriteria(GetValueOrDefault(sparams, "SortCriteria", "")); + var filter = new Filter(GetValueOrDefault(sparams, "Filter", "*")); // sort example: dc:title, dc:date @@ -303,55 +343,69 @@ namespace MediaBrowser.Dlna.ContentDirectory start = startVal; } - //var root = GetItem(id) as IMediaFolder; - var result = new XmlDocument(); - - var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL); - didl.SetAttribute("xmlns:dc", NS_DC); - didl.SetAttribute("xmlns:dlna", NS_DLNA); - didl.SetAttribute("xmlns:upnp", NS_UPNP); - - foreach (var att in _profile.XmlRootAttributes) + var settings = new XmlWriterSettings { - didl.SetAttribute(att.Name, att.Value); + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; + + StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); + int totalCount = 0; + int provided = 0; + + using (XmlWriter writer = XmlWriter.Create(builder, settings)) + { + //writer.WriteStartDocument(); + + writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + + writer.WriteAttributeString("xmlns", "dc", null, NS_DC); + writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); + writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + //didl.SetAttribute("xmlns:sec", NS_SEC); + + DidlBuilder.WriteXmlRootAttributes(_profile, writer); + + var serverItem = GetItemFromObjectId(sparams["ContainerID"], user); + + var item = serverItem.Item; + + var childrenResult = (await GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount).ConfigureAwait(false)); + + totalCount = childrenResult.TotalRecordCount; + + provided = childrenResult.Items.Length; + + foreach (var i in childrenResult.Items) + { + if (i.IsFolder) + { + var childCount = (await GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0).ConfigureAwait(false)) + .TotalRecordCount; + + _didlBuilder.WriteFolderElement(writer, i, null, item, childCount, filter); + } + else + { + _didlBuilder.WriteItemElement(_config.GetDlnaConfiguration(), writer, i, item, serverItem.StubType, deviceId, filter); + } + } + + writer.WriteFullEndElement(); + //writer.WriteEndDocument(); } - result.AppendChild(didl); - - var serverItem = GetItemFromObjectId(sparams["ContainerID"], user); - - var item = serverItem.Item; - - var childrenResult = (await GetChildrenSorted(item, user, searchCriteria, sortCriteria, start, requestedCount).ConfigureAwait(false)); - - var totalCount = childrenResult.TotalRecordCount; - - var provided = childrenResult.Items.Length; - - foreach (var i in childrenResult.Items) - { - if (i.IsFolder) - { - var childCount = (await GetChildrenSorted(i, user, searchCriteria, sortCriteria, null, 0).ConfigureAwait(false)) - .TotalRecordCount; - - result.DocumentElement.AppendChild(_didlBuilder.GetFolderElement(result, i, null, item, childCount, filter)); - } - else - { - result.DocumentElement.AppendChild(_didlBuilder.GetItemElement(_config.GetDlnaConfiguration(), result, i, item, serverItem.StubType, deviceId, filter)); - } - } - - var resXML = result.OuterXml; + var resXML = builder.ToString(); return new List<KeyValuePair<string, string>> - { - new KeyValuePair<string,string>("Result", resXML), - new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), - new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), - new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) - }; + { + new KeyValuePair<string,string>("Result", resXML), + new KeyValuePair<string,string>("NumberReturned", provided.ToString(_usCulture)), + new KeyValuePair<string,string>("TotalMatches", totalCount.ToString(_usCulture)), + new KeyValuePair<string,string>("UpdateID", _systemUpdateId.ToString(_usCulture)) + }; } private Task<QueryResult<BaseItem>> GetChildrenSorted(BaseItem item, User user, SearchCriteria search, SortCriteria sort, int? startIndex, int? limit) @@ -479,7 +533,7 @@ namespace MediaBrowser.Dlna.ContentDirectory { var itemsResult = _libraryManager.GetItemsResult(new InternalItemsQuery(user) { - Person = person.Name, + PersonIds = new[] { person.Id.ToString("N") }, IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name, typeof(Trailer).Name }, SortBy = new[] { ItemSortBy.SortName }, Limit = limit, @@ -508,20 +562,6 @@ namespace MediaBrowser.Dlna.ContentDirectory return result; } - private bool EnablePeopleDisplay(BaseItem item) - { - if (_libraryManager.GetPeopleNames(new InternalPeopleQuery - { - ItemId = item.Id - - }).Count > 0) - { - return item is Movie; - } - - return false; - } - private ServerItem GetItemFromObjectId(string id, User user) { return DidlBuilder.IsIdRoot(id) diff --git a/MediaBrowser.Dlna/ContentDirectory/ServiceActionListBuilder.cs b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs similarity index 99% rename from MediaBrowser.Dlna/ContentDirectory/ServiceActionListBuilder.cs rename to Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs index cd8119048e..8e3821e7c8 100644 --- a/MediaBrowser.Dlna/ContentDirectory/ServiceActionListBuilder.cs +++ b/Emby.Dlna/ContentDirectory/ServiceActionListBuilder.cs @@ -1,7 +1,7 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System.Collections.Generic; -namespace MediaBrowser.Dlna.ContentDirectory +namespace Emby.Dlna.ContentDirectory { public class ServiceActionListBuilder { diff --git a/MediaBrowser.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs similarity index 69% rename from MediaBrowser.Dlna/Didl/DidlBuilder.cs rename to Emby.Dlna/Didl/DidlBuilder.cs index a5091bf01e..3dcdaf2efc 100644 --- a/MediaBrowser.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -6,9 +6,8 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Playlists; -using MediaBrowser.Dlna.ContentDirectory; +using Emby.Dlna.ContentDirectory; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Entities; @@ -18,11 +17,13 @@ using System; using System.Globalization; using System.IO; using System.Linq; +using System.Text; using System.Xml; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Globalization; -namespace MediaBrowser.Dlna.Didl +namespace Emby.Dlna.Didl { public class DidlBuilder { @@ -62,50 +63,79 @@ namespace MediaBrowser.Dlna.Didl public string GetItemDidl(DlnaOptions options, BaseItem item, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo) { - var result = new XmlDocument(); - - var didl = result.CreateElement(string.Empty, "DIDL-Lite", NS_DIDL); - didl.SetAttribute("xmlns:dc", NS_DC); - didl.SetAttribute("xmlns:dlna", NS_DLNA); - didl.SetAttribute("xmlns:upnp", NS_UPNP); - //didl.SetAttribute("xmlns:sec", NS_SEC); - - foreach (var att in _profile.XmlRootAttributes) + var settings = new XmlWriterSettings { - didl.SetAttribute(att.Name, att.Value); + Encoding = Encoding.UTF8, + CloseOutput = false, + OmitXmlDeclaration = true, + ConformanceLevel = ConformanceLevel.Fragment + }; + + StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); + + using (XmlWriter writer = XmlWriter.Create(builder, settings)) + { + //writer.WriteStartDocument(); + + writer.WriteStartElement(string.Empty, "DIDL-Lite", NS_DIDL); + + writer.WriteAttributeString("xmlns", "dc", null, NS_DC); + writer.WriteAttributeString("xmlns", "dlna", null, NS_DLNA); + writer.WriteAttributeString("xmlns", "upnp", null, NS_UPNP); + //didl.SetAttribute("xmlns:sec", NS_SEC); + + WriteXmlRootAttributes(_profile, writer); + + WriteItemElement(options, writer, item, context, null, deviceId, filter, streamInfo); + + writer.WriteFullEndElement(); + //writer.WriteEndDocument(); } - result.AppendChild(didl); - - result.DocumentElement.AppendChild(GetItemElement(options, result, item, context, null, deviceId, filter, streamInfo)); - - return result.DocumentElement.OuterXml; + return builder.ToString(); } - public XmlElement GetItemElement(DlnaOptions options, XmlDocument doc, BaseItem item, BaseItem context, StubType? contextStubType, string deviceId, Filter filter, StreamInfo streamInfo = null) + public static void WriteXmlRootAttributes(DeviceProfile profile, XmlWriter writer) + { + foreach (var att in profile.XmlRootAttributes) + { + var parts = att.Name.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 2) + { + writer.WriteAttributeString(parts[0], parts[1], null, att.Value); + } + else + { + writer.WriteAttributeString(att.Name, att.Value); + } + } + } + + public void WriteItemElement(DlnaOptions options, XmlWriter writer, BaseItem item, BaseItem context, StubType? contextStubType, string deviceId, Filter filter, StreamInfo streamInfo = null) { var clientId = GetClientId(item, null); - var element = doc.CreateElement(string.Empty, "item", NS_DIDL); - element.SetAttribute("restricted", "1"); - element.SetAttribute("id", clientId); + writer.WriteStartElement(string.Empty, "item", NS_DIDL); + + writer.WriteAttributeString("restricted", "1"); + writer.WriteAttributeString("id", clientId); if (context != null) { - element.SetAttribute("parentID", GetClientId(context, contextStubType)); + writer.WriteAttributeString("parentID", GetClientId(context, contextStubType)); } else { var parent = item.DisplayParentId; if (parent.HasValue) { - element.SetAttribute("parentID", GetClientId(parent.Value, null)); + writer.WriteAttributeString("parentID", GetClientId(parent.Value, null)); } } - //AddBookmarkInfo(item, user, element); + AddGeneralProperties(item, null, context, writer, filter); - AddGeneralProperties(item, null, context, element, filter); + //AddBookmarkInfo(item, user, element); // refID? // storeAttribute(itemNode, object, ClassProperties.REF_ID, false); @@ -116,17 +146,16 @@ namespace MediaBrowser.Dlna.Didl { if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - AddAudioResource(options, element, hasMediaSources, deviceId, filter, streamInfo); + AddAudioResource(options, writer, hasMediaSources, deviceId, filter, streamInfo); } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { - AddVideoResource(options, element, hasMediaSources, deviceId, filter, streamInfo); + AddVideoResource(options, writer, hasMediaSources, deviceId, filter, streamInfo); } } - AddCover(item, context, null, element); - - return element; + AddCover(item, context, null, writer); + writer.WriteFullEndElement(); } private ILogger GetStreamBuilderLogger(DlnaOptions options) @@ -139,7 +168,7 @@ namespace MediaBrowser.Dlna.Didl return new NullLogger(); } - private void AddVideoResource(DlnaOptions options, XmlElement container, IHasMediaSources video, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddVideoResource(DlnaOptions options, XmlWriter writer, IHasMediaSources video, string deviceId, Filter filter, StreamInfo streamInfo = null) { if (streamInfo == null) { @@ -182,24 +211,25 @@ namespace MediaBrowser.Dlna.Didl foreach (var contentFeature in contentFeatureList) { - AddVideoResource(container, video, deviceId, filter, contentFeature, streamInfo); + AddVideoResource(writer, video, deviceId, filter, contentFeature, streamInfo); } - foreach (var subtitle in streamInfo.GetSubtitleProfiles(false, _serverAddress, _accessToken)) - { - if (subtitle.DeliveryMethod == SubtitleDeliveryMethod.External) - { - var subtitleAdded = AddSubtitleElement(container, subtitle); + var subtitleProfiles = streamInfo.GetSubtitleProfiles(false, _serverAddress, _accessToken) + .Where(subtitle => subtitle.DeliveryMethod == SubtitleDeliveryMethod.External) + .ToList(); - if (subtitleAdded && _profile.EnableSingleSubtitleLimit) - { - break; - } + foreach (var subtitle in subtitleProfiles) + { + var subtitleAdded = AddSubtitleElement(writer, subtitle); + + if (subtitleAdded && _profile.EnableSingleSubtitleLimit) + { + break; } } } - private bool AddSubtitleElement(XmlElement container, SubtitleStreamInfo info) + private bool AddSubtitleElement(XmlWriter writer, SubtitleStreamInfo info) { var subtitleProfile = _profile.SubtitleProfiles .FirstOrDefault(i => string.Equals(info.Format, i.Format, StringComparison.OrdinalIgnoreCase) && i.Method == SubtitleDeliveryMethod.External); @@ -216,52 +246,45 @@ namespace MediaBrowser.Dlna.Didl // <sec:CaptionInfoEx sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfoEx> // <sec:CaptionInfo sec:type="srt">http://192.168.1.3:9999/video.srt</sec:CaptionInfo> - var res = container.OwnerDocument.CreateElement("CaptionInfoEx", "sec"); + writer.WriteStartElement("sec", "CaptionInfoEx", null); + writer.WriteAttributeString("sec", "type", null, info.Format.ToLower()); - res.InnerText = info.Url; - - //// TODO: attribute needs SEC: - res.SetAttribute("type", "sec", info.Format.ToLower()); - container.AppendChild(res); + writer.WriteString(info.Url); + writer.WriteFullEndElement(); } else if (string.Equals(subtitleMode, "smi", StringComparison.OrdinalIgnoreCase)) { - var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NS_DIDL); - res.InnerText = info.Url; + writer.WriteAttributeString("protocolInfo", "http-get:*:smi/caption:*"); - res.SetAttribute("protocolInfo", "http-get:*:smi/caption:*"); - - container.AppendChild(res); + writer.WriteString(info.Url); + writer.WriteFullEndElement(); } else { - var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); - - res.InnerText = info.Url; - + writer.WriteStartElement(string.Empty, "res", NS_DIDL); var protocolInfo = string.Format("http-get:*:text/{0}:*", info.Format.ToLower()); - res.SetAttribute("protocolInfo", protocolInfo); + writer.WriteAttributeString("protocolInfo", protocolInfo); - container.AppendChild(res); + writer.WriteString(info.Url); + writer.WriteFullEndElement(); } return true; } - private void AddVideoResource(XmlElement container, IHasMediaSources video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo) + private void AddVideoResource(XmlWriter writer, IHasMediaSources video, string deviceId, Filter filter, string contentFeatures, StreamInfo streamInfo) { - var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NS_DIDL); var url = streamInfo.ToDlnaUrl(_serverAddress, _accessToken); - res.InnerText = url; - var mediaSource = streamInfo.MediaSource; if (mediaSource.RunTimeTicks.HasValue) { - res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); } if (filter.Contains("res@size")) @@ -272,7 +295,7 @@ namespace MediaBrowser.Dlna.Didl if (size.HasValue) { - res.SetAttribute("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); } } } @@ -286,25 +309,25 @@ namespace MediaBrowser.Dlna.Didl if (targetChannels.HasValue) { - res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); } if (filter.Contains("res@resolution")) { if (targetWidth.HasValue && targetHeight.HasValue) { - res.SetAttribute("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value)); + writer.WriteAttributeString("resolution", string.Format("{0}x{1}", targetWidth.Value, targetHeight.Value)); } } if (targetSampleRate.HasValue) { - res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); } if (totalBitrate.HasValue) { - res.SetAttribute("bitrate", totalBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); } var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, @@ -332,13 +355,15 @@ namespace MediaBrowser.Dlna.Didl ? MimeTypes.GetMimeType(filename) : mediaProfile.MimeType; - res.SetAttribute("protocolInfo", String.Format( + writer.WriteAttributeString("protocolInfo", String.Format( "http-get:*:{0}:{1}", mimeType, contentFeatures )); - container.AppendChild(res); + writer.WriteString(url); + + writer.WriteFullEndElement(); } private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context) @@ -368,11 +393,11 @@ namespace MediaBrowser.Dlna.Didl if (item.IndexNumber.HasValue) { - var number = item.IndexNumber.Value.ToString("00").ToString(CultureInfo.InvariantCulture); + var number = item.IndexNumber.Value.ToString("00", CultureInfo.InvariantCulture); if (episode.IndexNumberEnd.HasValue) { - number += "-" + episode.IndexNumberEnd.Value.ToString("00").ToString(CultureInfo.InvariantCulture); + number += "-" + episode.IndexNumberEnd.Value.ToString("00", CultureInfo.InvariantCulture); } return number + " - " + item.Name; @@ -382,32 +407,30 @@ namespace MediaBrowser.Dlna.Didl return item.Name; } - private void AddAudioResource(DlnaOptions options, XmlElement container, IHasMediaSources audio, string deviceId, Filter filter, StreamInfo streamInfo = null) + private void AddAudioResource(DlnaOptions options, XmlWriter writer, IHasMediaSources audio, string deviceId, Filter filter, StreamInfo streamInfo = null) { - var res = container.OwnerDocument.CreateElement(string.Empty, "res", NS_DIDL); + writer.WriteStartElement(string.Empty, "res", NS_DIDL); if (streamInfo == null) { var sources = _mediaSourceManager.GetStaticMediaSources(audio, true, _user).ToList(); streamInfo = new StreamBuilder(_mediaEncoder, GetStreamBuilderLogger(options)).BuildAudioItem(new AudioOptions - { - ItemId = GetClientId(audio), - MediaSources = sources, - Profile = _profile, - DeviceId = deviceId - }); + { + ItemId = GetClientId(audio), + MediaSources = sources, + Profile = _profile, + DeviceId = deviceId + }); } var url = streamInfo.ToDlnaUrl(_serverAddress, _accessToken); - res.InnerText = url; - var mediaSource = streamInfo.MediaSource; if (mediaSource.RunTimeTicks.HasValue) { - res.SetAttribute("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); + writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", _usCulture)); } if (filter.Contains("res@size")) @@ -418,7 +441,7 @@ namespace MediaBrowser.Dlna.Didl if (size.HasValue) { - res.SetAttribute("size", size.Value.ToString(_usCulture)); + writer.WriteAttributeString("size", size.Value.ToString(_usCulture)); } } } @@ -429,17 +452,17 @@ namespace MediaBrowser.Dlna.Didl if (targetChannels.HasValue) { - res.SetAttribute("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); + writer.WriteAttributeString("nrAudioChannels", targetChannels.Value.ToString(_usCulture)); } if (targetSampleRate.HasValue) { - res.SetAttribute("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); + writer.WriteAttributeString("sampleFrequency", targetSampleRate.Value.ToString(_usCulture)); } if (targetAudioBitrate.HasValue) { - res.SetAttribute("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); + writer.WriteAttributeString("bitrate", targetAudioBitrate.Value.ToString(_usCulture)); } var mediaProfile = _profile.GetAudioMediaProfile(streamInfo.Container, @@ -462,13 +485,15 @@ namespace MediaBrowser.Dlna.Didl streamInfo.RunTimeTicks, streamInfo.TranscodeSeekInfo); - res.SetAttribute("protocolInfo", String.Format( + writer.WriteAttributeString("protocolInfo", String.Format( "http-get:*:{0}:{1}", mimeType, contentFeatures )); - container.AppendChild(res); + writer.WriteString(url); + + writer.WriteFullEndElement(); } public static bool IsIdRoot(string id) @@ -486,47 +511,48 @@ namespace MediaBrowser.Dlna.Didl return false; } - public XmlElement GetFolderElement(XmlDocument doc, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) + public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null) { - var container = doc.CreateElement(string.Empty, "container", NS_DIDL); - container.SetAttribute("restricted", "0"); - container.SetAttribute("searchable", "1"); - container.SetAttribute("childCount", childCount.ToString(_usCulture)); + writer.WriteStartElement(string.Empty, "container", NS_DIDL); + + writer.WriteAttributeString("restricted", "0"); + writer.WriteAttributeString("searchable", "1"); + writer.WriteAttributeString("childCount", childCount.ToString(_usCulture)); var clientId = GetClientId(folder, stubType); if (string.Equals(requestedId, "0")) { - container.SetAttribute("id", "0"); - container.SetAttribute("parentID", "-1"); + writer.WriteAttributeString("id", "0"); + writer.WriteAttributeString("parentID", "-1"); } else { - container.SetAttribute("id", clientId); + writer.WriteAttributeString("id", clientId); if (context != null) { - container.SetAttribute("parentID", GetClientId(context, null)); + writer.WriteAttributeString("parentID", GetClientId(context, null)); } else { var parent = folder.DisplayParentId; if (!parent.HasValue) { - container.SetAttribute("parentID", "0"); + writer.WriteAttributeString("parentID", "0"); } else { - container.SetAttribute("parentID", GetClientId(parent.Value, null)); + writer.WriteAttributeString("parentID", GetClientId(parent.Value, null)); } } } - AddCommonFields(folder, stubType, null, container, filter); + AddGeneralProperties(folder, stubType, context, writer, filter); - AddCover(folder, context, stubType, container); + AddCover(folder, context, stubType, writer); - return container; + writer.WriteFullEndElement(); } //private void AddBookmarkInfo(BaseItem item, User user, XmlElement element) @@ -544,27 +570,22 @@ namespace MediaBrowser.Dlna.Didl /// <summary> /// Adds fields used by both items and folders /// </summary> - /// <param name="item">The item.</param> - /// <param name="itemStubType">Type of the item stub.</param> - /// <param name="context">The context.</param> - /// <param name="element">The element.</param> - /// <param name="filter">The filter.</param> - private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlElement element, Filter filter) + private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) { // Don't filter on dc:title because not all devices will include it in the filter // MediaMonkey for example won't display content without a title //if (filter.Contains("dc:title")) { - AddValue(element, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); + AddValue(writer, "dc", "title", GetDisplayName(item, itemStubType, context), NS_DC); } - element.AppendChild(CreateObjectClass(element.OwnerDocument, item, itemStubType)); + WriteObjectClass(writer, item, itemStubType); if (filter.Contains("dc:date")) { if (item.PremiereDate.HasValue) { - AddValue(element, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC); + AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o"), NS_DC); } } @@ -572,13 +593,13 @@ namespace MediaBrowser.Dlna.Didl { foreach (var genre in item.Genres) { - AddValue(element, "upnp", "genre", genre, NS_UPNP); + AddValue(writer, "upnp", "genre", genre, NS_UPNP); } } foreach (var studio in item.Studios) { - AddValue(element, "upnp", "publisher", studio, NS_UPNP); + AddValue(writer, "upnp", "publisher", studio, NS_UPNP); } if (filter.Contains("dc:description")) @@ -592,14 +613,14 @@ namespace MediaBrowser.Dlna.Didl if (!string.IsNullOrWhiteSpace(desc)) { - AddValue(element, "dc", "description", desc, NS_DC); + AddValue(writer, "dc", "description", desc, NS_DC); } } if (filter.Contains("upnp:longDescription")) { if (!string.IsNullOrWhiteSpace(item.Overview)) { - AddValue(element, "upnp", "longDescription", item.Overview, NS_UPNP); + AddValue(writer, "upnp", "longDescription", item.Overview, NS_UPNP); } } @@ -607,23 +628,23 @@ namespace MediaBrowser.Dlna.Didl { if (filter.Contains("dc:rating")) { - AddValue(element, "dc", "rating", item.OfficialRating, NS_DC); + AddValue(writer, "dc", "rating", item.OfficialRating, NS_DC); } if (filter.Contains("upnp:rating")) { - AddValue(element, "upnp", "rating", item.OfficialRating, NS_UPNP); + AddValue(writer, "upnp", "rating", item.OfficialRating, NS_UPNP); } } - AddPeople(item, element); + AddPeople(item, writer); } - private XmlElement CreateObjectClass(XmlDocument result, BaseItem item, StubType? stubType) + private void WriteObjectClass(XmlWriter writer, BaseItem item, StubType? stubType) { // More types here // http://oss.linn.co.uk/repos/Public/LibUpnpCil/DidlLite/UpnpAv/Test/TestDidlLite.cs - var objectClass = result.CreateElement("upnp", "class", NS_UPNP); + writer.WriteStartElement("upnp", "class", NS_UPNP); if (item.IsFolder || stubType.HasValue) { @@ -653,48 +674,48 @@ namespace MediaBrowser.Dlna.Didl } } - objectClass.InnerText = classType ?? "object.container.storageFolder"; + writer.WriteString(classType ?? "object.container.storageFolder"); } else if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase)) { - objectClass.InnerText = "object.item.audioItem.musicTrack"; + writer.WriteString("object.item.audioItem.musicTrack"); } else if (string.Equals(item.MediaType, MediaType.Photo, StringComparison.OrdinalIgnoreCase)) { - objectClass.InnerText = "object.item.imageItem.photo"; + writer.WriteString("object.item.imageItem.photo"); } else if (string.Equals(item.MediaType, MediaType.Video, StringComparison.OrdinalIgnoreCase)) { if (!_profile.RequiresPlainVideoItems && item is Movie) { - objectClass.InnerText = "object.item.videoItem.movie"; + writer.WriteString("object.item.videoItem.movie"); } else if (!_profile.RequiresPlainVideoItems && item is MusicVideo) { - objectClass.InnerText = "object.item.videoItem.musicVideoClip"; + writer.WriteString("object.item.videoItem.musicVideoClip"); } else { - objectClass.InnerText = "object.item.videoItem"; + writer.WriteString("object.item.videoItem"); } } else if (item is MusicGenre) { - objectClass.InnerText = _profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre"; + writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre.musicGenre"); } else if (item is Genre || item is GameGenre) { - objectClass.InnerText = _profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre"; + writer.WriteString(_profile.RequiresPlainFolders ? "object.container.storageFolder" : "object.container.genre"); } else { - objectClass.InnerText = "object.item"; + writer.WriteString("object.item"); } - return objectClass; + writer.WriteFullEndElement(); } - private void AddPeople(BaseItem item, XmlElement element) + private void AddPeople(BaseItem item, XmlWriter writer) { var types = new[] { @@ -718,7 +739,7 @@ namespace MediaBrowser.Dlna.Didl var type = types.FirstOrDefault(i => string.Equals(i, actor.Type, StringComparison.OrdinalIgnoreCase) || string.Equals(i, actor.Role, StringComparison.OrdinalIgnoreCase)) ?? PersonType.Actor; - AddValue(element, "upnp", type.ToLower(), actor.Name, NS_UPNP); + AddValue(writer, "upnp", type.ToLower(), actor.Name, NS_UPNP); index++; @@ -729,9 +750,9 @@ namespace MediaBrowser.Dlna.Didl } } - private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlElement element, Filter filter) + private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter) { - AddCommonFields(item, itemStubType, context, element, filter); + AddCommonFields(item, itemStubType, context, writer, filter); var audio = item as Audio; @@ -739,17 +760,17 @@ namespace MediaBrowser.Dlna.Didl { foreach (var artist in audio.Artists) { - AddValue(element, "upnp", "artist", artist, NS_UPNP); + AddValue(writer, "upnp", "artist", artist, NS_UPNP); } if (!string.IsNullOrEmpty(audio.Album)) { - AddValue(element, "upnp", "album", audio.Album, NS_UPNP); + AddValue(writer, "upnp", "album", audio.Album, NS_UPNP); } foreach (var artist in audio.AlbumArtists) { - AddAlbumArtist(element, artist); + AddAlbumArtist(writer, artist); } } @@ -759,12 +780,12 @@ namespace MediaBrowser.Dlna.Didl { foreach (var artist in album.AlbumArtists) { - AddAlbumArtist(element, artist); - AddValue(element, "upnp", "artist", artist, NS_UPNP); + AddAlbumArtist(writer, artist); + AddValue(writer, "upnp", "artist", artist, NS_UPNP); } foreach (var artist in album.Artists) { - AddValue(element, "upnp", "artist", artist, NS_UPNP); + AddValue(writer, "upnp", "artist", artist, NS_UPNP); } } @@ -774,37 +795,37 @@ namespace MediaBrowser.Dlna.Didl { foreach (var artist in musicVideo.Artists) { - AddValue(element, "upnp", "artist", artist, NS_UPNP); - AddAlbumArtist(element, artist); + AddValue(writer, "upnp", "artist", artist, NS_UPNP); + AddAlbumArtist(writer, artist); } if (!string.IsNullOrEmpty(musicVideo.Album)) { - AddValue(element, "upnp", "album", musicVideo.Album, NS_UPNP); + AddValue(writer, "upnp", "album", musicVideo.Album, NS_UPNP); } } if (item.IndexNumber.HasValue) { - AddValue(element, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "originalTrackNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); if (item is Episode) { - AddValue(element, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); + AddValue(writer, "upnp", "episodeNumber", item.IndexNumber.Value.ToString(_usCulture), NS_UPNP); } } } - private void AddAlbumArtist(XmlElement elem, string name) + private void AddAlbumArtist(XmlWriter writer, string name) { try { - var newNode = elem.OwnerDocument.CreateElement("upnp", "artist", NS_UPNP); - newNode.InnerText = name; + writer.WriteStartElement("upnp", "artist", NS_UPNP); + writer.WriteAttributeString("role", "AlbumArtist"); - newNode.SetAttribute("role", "AlbumArtist"); + writer.WriteString(name); - elem.AppendChild(newNode); + writer.WriteFullEndElement(); } catch (XmlException) { @@ -812,13 +833,11 @@ namespace MediaBrowser.Dlna.Didl } } - private void AddValue(XmlElement elem, string prefix, string name, string value, string namespaceUri) + private void AddValue(XmlWriter writer, string prefix, string name, string value, string namespaceUri) { try { - var date = elem.OwnerDocument.CreateElement(prefix, name, namespaceUri); - date.InnerText = value; - elem.AppendChild(date); + writer.WriteElementString(prefix, name, namespaceUri, value); } catch (XmlException) { @@ -826,11 +845,11 @@ namespace MediaBrowser.Dlna.Didl } } - private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlElement element) + private void AddCover(BaseItem item, BaseItem context, StubType? stubType, XmlWriter writer) { if (stubType.HasValue && stubType.Value == StubType.People) { - AddEmbeddedImageAsCover("people", element); + AddEmbeddedImageAsCover("people", writer); return; } @@ -860,8 +879,6 @@ namespace MediaBrowser.Dlna.Didl return; } - var result = element.OwnerDocument; - var playbackPercentage = 0; var unplayedCount = 0; @@ -891,18 +908,14 @@ namespace MediaBrowser.Dlna.Didl var albumartUrlInfo = GetImageUrl(imageInfo, _profile.MaxAlbumArtWidth, _profile.MaxAlbumArtHeight, playbackPercentage, unplayedCount, "jpg"); - var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP); - var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); - profile.InnerText = _profile.AlbumArtPn; - icon.SetAttributeNode(profile); - icon.InnerText = albumartUrlInfo.Url; - element.AppendChild(icon); + writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); + writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); + writer.WriteString(albumartUrlInfo.Url); + writer.WriteFullEndElement(); // TOOD: Remove these default values var iconUrlInfo = GetImageUrl(imageInfo, _profile.MaxIconWidth ?? 48, _profile.MaxIconHeight ?? 48, playbackPercentage, unplayedCount, "jpg"); - icon = result.CreateElement("upnp", "icon", NS_UPNP); - icon.InnerText = iconUrlInfo.Url; - element.AppendChild(icon); + writer.WriteElementString("upnp", "icon", NS_UPNP, iconUrlInfo.Url); if (!_profile.EnableAlbumArtInDidl) { @@ -916,36 +929,30 @@ namespace MediaBrowser.Dlna.Didl } } - AddImageResElement(item, element, 160, 160, playbackPercentage, unplayedCount, "jpg", "JPEG_TN"); + AddImageResElement(item, writer, 160, 160, playbackPercentage, unplayedCount, "jpg", "JPEG_TN"); if (!_profile.EnableSingleAlbumArtLimit) { - AddImageResElement(item, element, 4096, 4096, playbackPercentage, unplayedCount, "jpg", "JPEG_LRG"); - AddImageResElement(item, element, 1024, 768, playbackPercentage, unplayedCount, "jpg", "JPEG_MED"); - AddImageResElement(item, element, 640, 480, playbackPercentage, unplayedCount, "jpg", "JPEG_SM"); - AddImageResElement(item, element, 4096, 4096, playbackPercentage, unplayedCount, "png", "PNG_LRG"); - AddImageResElement(item, element, 160, 160, playbackPercentage, unplayedCount, "png", "PNG_TN"); + AddImageResElement(item, writer, 4096, 4096, playbackPercentage, unplayedCount, "jpg", "JPEG_LRG"); + AddImageResElement(item, writer, 1024, 768, playbackPercentage, unplayedCount, "jpg", "JPEG_MED"); + AddImageResElement(item, writer, 640, 480, playbackPercentage, unplayedCount, "jpg", "JPEG_SM"); + AddImageResElement(item, writer, 4096, 4096, playbackPercentage, unplayedCount, "png", "PNG_LRG"); + AddImageResElement(item, writer, 160, 160, playbackPercentage, unplayedCount, "png", "PNG_TN"); } } - private void AddEmbeddedImageAsCover(string name, XmlElement element) + private void AddEmbeddedImageAsCover(string name, XmlWriter writer) { - var result = element.OwnerDocument; + writer.WriteStartElement("upnp", "albumArtURI", NS_UPNP); + writer.WriteAttributeString("dlna", "profileID", NS_DLNA, _profile.AlbumArtPn); + writer.WriteString(_serverAddress + "/Dlna/icons/people480.jpg"); + writer.WriteFullEndElement(); - var icon = result.CreateElement("upnp", "albumArtURI", NS_UPNP); - var profile = result.CreateAttribute("dlna", "profileID", NS_DLNA); - profile.InnerText = _profile.AlbumArtPn; - icon.SetAttributeNode(profile); - icon.InnerText = _serverAddress + "/Dlna/icons/people480.jpg"; - element.AppendChild(icon); - - icon = result.CreateElement("upnp", "icon", NS_UPNP); - icon.InnerText = _serverAddress + "/Dlna/icons/people48.jpg"; - element.AppendChild(icon); + writer.WriteElementString("upnp", "icon", NS_UPNP, _serverAddress + "/Dlna/icons/people48.jpg"); } private void AddImageResElement(BaseItem item, - XmlElement element, + XmlWriter writer, int maxWidth, int maxHeight, int playbackPercentage, @@ -960,13 +967,9 @@ namespace MediaBrowser.Dlna.Didl return; } - var result = element.OwnerDocument; - var albumartUrlInfo = GetImageUrl(imageInfo, maxWidth, maxHeight, playbackPercentage, unplayedCount, format); - var res = result.CreateElement(string.Empty, "res", NS_DIDL); - - res.InnerText = albumartUrlInfo.Url; + writer.WriteStartElement(string.Empty, "res", NS_DIDL); var width = albumartUrlInfo.Width; var height = albumartUrlInfo.Height; @@ -974,7 +977,7 @@ namespace MediaBrowser.Dlna.Didl var contentFeatures = new ContentFeatureBuilder(_profile) .BuildImageHeader(format, width, height, imageInfo.IsDirectStream, org_Pn); - res.SetAttribute("protocolInfo", String.Format( + writer.WriteAttributeString("protocolInfo", String.Format( "http-get:*:{0}:{1}", MimeTypes.GetMimeType("file." + format), contentFeatures @@ -982,10 +985,12 @@ namespace MediaBrowser.Dlna.Didl if (width.HasValue && height.HasValue) { - res.SetAttribute("resolution", string.Format("{0}x{1}", width.Value, height.Value)); + writer.WriteAttributeString("resolution", string.Format("{0}x{1}", width.Value, height.Value)); } - element.AppendChild(res); + writer.WriteString(albumartUrlInfo.Url); + + writer.WriteFullEndElement(); } private ImageDownloadInfo GetImageInfo(BaseItem item) diff --git a/MediaBrowser.Dlna/Didl/Filter.cs b/Emby.Dlna/Didl/Filter.cs similarity index 96% rename from MediaBrowser.Dlna/Didl/Filter.cs rename to Emby.Dlna/Didl/Filter.cs index c980a2a2e9..5e9aeb530a 100644 --- a/MediaBrowser.Dlna/Didl/Filter.cs +++ b/Emby.Dlna/Didl/Filter.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace MediaBrowser.Dlna.Didl +namespace Emby.Dlna.Didl { public class Filter { diff --git a/Emby.Dlna/Didl/StringWriterWithEncoding.cs b/Emby.Dlna/Didl/StringWriterWithEncoding.cs new file mode 100644 index 0000000000..052d6610b1 --- /dev/null +++ b/Emby.Dlna/Didl/StringWriterWithEncoding.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Emby.Dlna.Didl +{ + public class StringWriterWithEncoding : StringWriter + { + private readonly Encoding _encoding; + + public StringWriterWithEncoding() + { + } + + public StringWriterWithEncoding(IFormatProvider formatProvider) + : base(formatProvider) + { + } + + public StringWriterWithEncoding(StringBuilder sb) + : base(sb) + { + } + + public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider) + : base(sb, formatProvider) + { + } + + + public StringWriterWithEncoding(Encoding encoding) + { + _encoding = encoding; + } + + public StringWriterWithEncoding(IFormatProvider formatProvider, Encoding encoding) + : base(formatProvider) + { + _encoding = encoding; + } + + public StringWriterWithEncoding(StringBuilder sb, Encoding encoding) + : base(sb) + { + _encoding = encoding; + } + + public StringWriterWithEncoding(StringBuilder sb, IFormatProvider formatProvider, Encoding encoding) + : base(sb, formatProvider) + { + _encoding = encoding; + } + + public override Encoding Encoding + { + get { return (null == _encoding) ? base.Encoding : _encoding; } + } + } +} diff --git a/MediaBrowser.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs similarity index 89% rename from MediaBrowser.Dlna/DlnaManager.cs rename to Emby.Dlna/DlnaManager.cs index aae157e7a8..4daaa50464 100644 --- a/MediaBrowser.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -4,8 +4,8 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Plugins; -using MediaBrowser.Dlna.Profiles; -using MediaBrowser.Dlna.Server; +using Emby.Dlna.Profiles; +using Emby.Dlna.Server; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Logging; @@ -16,9 +16,10 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; -using CommonIO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Reflection; -namespace MediaBrowser.Dlna +namespace Emby.Dlna { public class DlnaManager : IDlnaManager { @@ -28,6 +29,7 @@ namespace MediaBrowser.Dlna private readonly ILogger _logger; private readonly IJsonSerializer _jsonSerializer; private readonly IServerApplicationHost _appHost; + private readonly IAssemblyInfo _assemblyInfo; private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal); @@ -35,7 +37,7 @@ namespace MediaBrowser.Dlna IFileSystem fileSystem, IApplicationPaths appPaths, ILogger logger, - IJsonSerializer jsonSerializer, IServerApplicationHost appHost) + IJsonSerializer jsonSerializer, IServerApplicationHost appHost, IAssemblyInfo assemblyInfo) { _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; @@ -43,6 +45,7 @@ namespace MediaBrowser.Dlna _logger = logger; _jsonSerializer = jsonSerializer; _appHost = appHost; + _assemblyInfo = assemblyInfo; } public void InitProfiles() @@ -235,6 +238,12 @@ namespace MediaBrowser.Dlna private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header) { + // Handle invalid user setup + if (string.IsNullOrWhiteSpace(header.Name)) + { + return false; + } + string value; if (headers.TryGetValue(header.Name, out value)) @@ -277,19 +286,29 @@ namespace MediaBrowser.Dlna { try { - return _fileSystem.GetFiles(path) + var allFiles = _fileSystem.GetFiles(path) + .ToList(); + + var xmlFies = allFiles .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase)) - .Select(i => ParseProfileXmlFile(i.FullName, type)) + .ToList(); + + var parseFiles = new List<FileSystemMetadata>(); + + parseFiles.AddRange(xmlFies); + + return parseFiles + .Select(i => ParseProfileFile(i.FullName, type)) .Where(i => i != null) .ToList(); } - catch (DirectoryNotFoundException) + catch (IOException) { return new List<DeviceProfile>(); } } - private DeviceProfile ParseProfileXmlFile(string path, DeviceProfileType type) + private DeviceProfile ParseProfileFile(string path, DeviceProfileType type) { lock (_profiles) { @@ -301,7 +320,18 @@ namespace MediaBrowser.Dlna try { - var profile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); + DeviceProfile profile; + + if (string.Equals(Path.GetExtension(path), ".xml", StringComparison.OrdinalIgnoreCase)) + { + var tempProfile = (DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(DeviceProfile), path); + + profile = ReserializeProfile(tempProfile); + } + else + { + profile = (DeviceProfile)_jsonSerializer.DeserializeFromFile(typeof(DeviceProfile), path); + } profile.Id = path.ToLower().GetMD5().ToString("N"); profile.ProfileType = type; @@ -312,7 +342,7 @@ namespace MediaBrowser.Dlna } catch (Exception ex) { - _logger.ErrorException("Error parsing profile xml: {0}", ex, path); + _logger.ErrorException("Error parsing profile file: {0}", ex, path); return null; } @@ -328,7 +358,7 @@ namespace MediaBrowser.Dlna var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase)); - return ParseProfileXmlFile(info.Path, info.Info.Type); + return ParseProfileFile(info.Path, info.Info.Type); } private IEnumerable<InternalProfileInfo> GetProfileInfosInternal() @@ -348,21 +378,6 @@ namespace MediaBrowser.Dlna return GetProfileInfosInternal().Select(i => i.Info); } - private IEnumerable<InternalProfileInfo> GetProfileInfos(string path, DeviceProfileType type) - { - try - { - return _fileSystem.GetFiles(path) - .Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase)) - .Select(i => GetInternalProfileInfo(i, type)) - .ToList(); - } - catch (DirectoryNotFoundException) - { - return new List<InternalProfileInfo>(); - } - } - private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type) { return new InternalProfileInfo @@ -380,12 +395,11 @@ namespace MediaBrowser.Dlna private void ExtractSystemProfiles() { - var assembly = GetType().Assembly; var namespaceName = GetType().Namespace + ".Profiles.Xml."; var systemProfilesPath = SystemProfilesPath; - foreach (var name in assembly.GetManifestResourceNames() + foreach (var name in _assemblyInfo.GetManifestResourceNames(GetType()) .Where(i => i.StartsWith(namespaceName)) .ToList()) { @@ -393,15 +407,15 @@ namespace MediaBrowser.Dlna var path = Path.Combine(systemProfilesPath, filename); - using (var stream = assembly.GetManifestResourceStream(name)) + using (var stream = _assemblyInfo.GetManifestResourceStream(GetType(), name)) { - var fileInfo = new FileInfo(path); + var fileInfo = _fileSystem.GetFileInfo(path); if (!fileInfo.Exists || fileInfo.Length != stream.Length) { _fileSystem.CreateDirectory(systemProfilesPath); - using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var fileStream = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { stream.CopyTo(fileStream); } @@ -478,6 +492,11 @@ namespace MediaBrowser.Dlna { _profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile); } + SerializeToXml(profile, path); + } + + internal void SerializeToXml(DeviceProfile profile, string path) + { _xmlSerializer.SerializeToFile(profile, path); } @@ -521,10 +540,12 @@ namespace MediaBrowser.Dlna ? ImageFormat.Png : ImageFormat.Jpg; + var resource = GetType().Namespace + ".Images." + filename.ToLower(); + return new ImageStream { Format = format, - Stream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower()) + Stream = _assemblyInfo.GetManifestResourceStream(GetType(), resource) }; } } @@ -532,14 +553,16 @@ namespace MediaBrowser.Dlna class DlnaProfileEntryPoint : IServerEntryPoint { private readonly IApplicationPaths _appPaths; - private readonly IXmlSerializer _xmlSerializer; + private readonly IJsonSerializer _jsonSerializer; private readonly IFileSystem _fileSystem; + private readonly IXmlSerializer _xmlSerializer; - public DlnaProfileEntryPoint(IApplicationPaths appPaths, IXmlSerializer xmlSerializer, IFileSystem fileSystem) + public DlnaProfileEntryPoint(IApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer) { _appPaths = appPaths; - _xmlSerializer = xmlSerializer; _fileSystem = fileSystem; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; } public void Run() diff --git a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj similarity index 73% rename from MediaBrowser.Dlna/MediaBrowser.Dlna.csproj rename to Emby.Dlna/Emby.Dlna.csproj index ebffe6c575..c83aaecab2 100644 --- a/MediaBrowser.Dlna/MediaBrowser.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -1,20 +1,20 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <PropertyGroup> + <MinimumVisualStudioVersion>11.0</MinimumVisualStudioVersion> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{734098EB-6DC1-4DD0-A1CA-3140DCD2737C}</ProjectGuid> + <ProjectGuid>{805844AB-E92F-45E6-9D99-4F6D48D129A5}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>MediaBrowser.Dlna</RootNamespace> - <AssemblyName>MediaBrowser.Dlna</AssemblyName> + <RootNamespace>Emby.Dlna</RootNamespace> + <AssemblyName>Emby.Dlna</AssemblyName> + <DefaultLanguage>en-US</DefaultLanguage> <FileAlignment>512</FileAlignment> - <ProductVersion>10.0.0</ProductVersion> - <SchemaVersion>2.0</SchemaVersion> + <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <TargetFrameworkProfile>Profile7</TargetFrameworkProfile> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> - <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> - <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -24,62 +24,35 @@ <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> - <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <DebugType>none</DebugType> + <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\Release\</OutputPath> <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> - <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release Mono|AnyCPU' "> - <Optimize>false</Optimize> - <OutputPath>bin\Release Mono</OutputPath> - <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath> - </Reference> - <Reference Include="MoreLinq"> - <HintPath>..\packages\morelinq.1.4.0\lib\net35\MoreLinq.dll</HintPath> - </Reference> - <Reference Include="Patterns.Logging"> - <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath> - </Reference> - <Reference Include="Rssdp.NetFx40"> - <HintPath>..\ThirdParty\rssdp\Rssdp.NetFx40.dll</HintPath> - </Reference> - <Reference Include="Rssdp.Portable"> - <HintPath>..\ThirdParty\rssdp\Rssdp.Portable.dll</HintPath> - </Reference> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="System.Net.Http" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Xml" /> - </ItemGroup> - <ItemGroup> - <Compile Include="..\SharedVersion.cs"> - <Link>Properties\SharedVersion.cs</Link> - </Compile> - <Compile Include="Channels\DlnaChannelFactory.cs" /> + <Compile Include="Common\Argument.cs" /> + <Compile Include="Common\DeviceIcon.cs" /> + <Compile Include="Common\DeviceService.cs" /> + <Compile Include="Common\ServiceAction.cs" /> + <Compile Include="Common\StateVariable.cs" /> <Compile Include="ConfigurationExtension.cs" /> <Compile Include="ConnectionManager\ConnectionManager.cs" /> <Compile Include="ConnectionManager\ConnectionManagerXmlBuilder.cs" /> <Compile Include="ConnectionManager\ControlHandler.cs" /> <Compile Include="ConnectionManager\ServiceActionListBuilder.cs" /> + <Compile Include="ContentDirectory\ContentDirectory.cs" /> <Compile Include="ContentDirectory\ContentDirectoryBrowser.cs" /> + <Compile Include="ContentDirectory\ContentDirectoryXmlBuilder.cs" /> + <Compile Include="ContentDirectory\ControlHandler.cs" /> + <Compile Include="ContentDirectory\ServiceActionListBuilder.cs" /> + <Compile Include="Didl\DidlBuilder.cs" /> <Compile Include="Didl\Filter.cs" /> + <Compile Include="Didl\StringWriterWithEncoding.cs" /> <Compile Include="DlnaManager.cs" /> - <Compile Include="Common\Argument.cs" /> <Compile Include="Eventing\EventManager.cs" /> <Compile Include="Eventing\EventSubscription.cs" /> <Compile Include="Main\DlnaEntryPoint.cs" /> @@ -88,76 +61,74 @@ <Compile Include="MediaReceiverRegistrar\MediaReceiverRegistrarXmlBuilder.cs" /> <Compile Include="MediaReceiverRegistrar\ServiceActionListBuilder.cs" /> <Compile Include="PlayTo\CurrentIdEventArgs.cs" /> - <Compile Include="PlayTo\Device.cs"> - <SubType>Code</SubType> - </Compile> - <Compile Include="PlayTo\PlaybackStartEventArgs.cs" /> + <Compile Include="PlayTo\Device.cs" /> <Compile Include="PlayTo\DeviceInfo.cs" /> - <Compile Include="Common\DeviceService.cs" /> - <Compile Include="Didl\DidlBuilder.cs" /> - <Compile Include="PlayTo\PlayToController.cs" /> - <Compile Include="Profiles\BubbleUpnpProfile.cs" /> - <Compile Include="Profiles\DefaultProfile.cs" /> - <Compile Include="Profiles\DirectTvProfile.cs" /> - <Compile Include="Profiles\DishHopperJoeyProfile.cs" /> - <Compile Include="Profiles\KodiProfile.cs" /> - <Compile Include="Profiles\PopcornHourProfile.cs" /> - <Compile Include="Profiles\SonyBlurayPlayer2016.cs" /> - <Compile Include="Profiles\SonyBlurayPlayer2015.cs" /> - <Compile Include="Profiles\SonyBlurayPlayer2014.cs" /> - <Compile Include="Profiles\SonyBravia2014Profile.cs" /> - <Compile Include="Profiles\SonyPs4Profile.cs" /> - <Compile Include="Profiles\VlcProfile.cs" /> - <Compile Include="Ssdp\Extensions.cs" /> <Compile Include="PlayTo\PlaybackProgressEventArgs.cs" /> + <Compile Include="PlayTo\PlaybackStartEventArgs.cs" /> <Compile Include="PlayTo\PlaybackStoppedEventArgs.cs" /> - <Compile Include="PlayTo\PlaylistItem.cs"> - <SubType>Code</SubType> - </Compile> + <Compile Include="PlayTo\PlaylistItem.cs" /> <Compile Include="PlayTo\PlaylistItemFactory.cs" /> + <Compile Include="PlayTo\PlayToController.cs" /> <Compile Include="PlayTo\PlayToManager.cs" /> - <Compile Include="Common\ServiceAction.cs" /> - <Compile Include="PlayTo\TRANSPORTSTATE.cs" /> - <Compile Include="PlayTo\uParserObject.cs" /> - <Compile Include="Profiles\Foobar2000Profile.cs" /> - <Compile Include="Profiles\MediaMonkeyProfile.cs" /> - <Compile Include="ContentDirectory\ContentDirectory.cs" /> - <Compile Include="ContentDirectory\ControlHandler.cs" /> - <Compile Include="ContentDirectory\ServiceActionListBuilder.cs" /> - <Compile Include="ContentDirectory\ContentDirectoryXmlBuilder.cs" /> - <Compile Include="Service\BaseControlHandler.cs" /> - <Compile Include="Service\BaseService.cs" /> - <Compile Include="Service\ControlErrorHandler.cs" /> - <Compile Include="Service\ServiceXmlBuilder.cs" /> - <Compile Include="Server\DescriptionXmlBuilder.cs" /> - <Compile Include="Ssdp\DeviceDiscovery.cs" /> <Compile Include="PlayTo\SsdpHttpClient.cs" /> - <Compile Include="Common\StateVariable.cs" /> <Compile Include="PlayTo\TransportCommands.cs" /> + <Compile Include="PlayTo\TRANSPORTSTATE.cs" /> <Compile Include="PlayTo\TransportStateEventArgs.cs" /> <Compile Include="PlayTo\uBaseObject.cs" /> - <Compile Include="PlayTo\UpnpContainer.cs" /> - <Compile Include="Common\DeviceIcon.cs" /> <Compile Include="PlayTo\uParser.cs" /> + <Compile Include="PlayTo\uParserObject.cs" /> + <Compile Include="PlayTo\UpnpContainer.cs" /> <Compile Include="PlayTo\uPnpNamespaces.cs" /> + <Compile Include="Profiles\BubbleUpnpProfile.cs" /> + <Compile Include="Profiles\DefaultProfile.cs" /> <Compile Include="Profiles\DenonAvrProfile.cs" /> + <Compile Include="Profiles\DirectTvProfile.cs" /> + <Compile Include="Profiles\DishHopperJoeyProfile.cs" /> + <Compile Include="Profiles\Foobar2000Profile.cs" /> + <Compile Include="Profiles\KodiProfile.cs" /> <Compile Include="Profiles\LgTvProfile.cs" /> <Compile Include="Profiles\LinksysDMA2100Profile.cs" /> + <Compile Include="Profiles\MediaMonkeyProfile.cs" /> <Compile Include="Profiles\PanasonicVieraProfile.cs" /> + <Compile Include="Profiles\PopcornHourProfile.cs" /> <Compile Include="Profiles\SamsungSmartTvProfile.cs" /> <Compile Include="Profiles\SonyBlurayPlayer2013.cs" /> + <Compile Include="Profiles\SonyBlurayPlayer2014.cs" /> + <Compile Include="Profiles\SonyBlurayPlayer2015.cs" /> + <Compile Include="Profiles\SonyBlurayPlayer2016.cs" /> <Compile Include="Profiles\SonyBlurayPlayerProfile.cs" /> <Compile Include="Profiles\SonyBravia2010Profile.cs" /> <Compile Include="Profiles\SonyBravia2011Profile.cs" /> <Compile Include="Profiles\SonyBravia2012Profile.cs" /> <Compile Include="Profiles\SonyBravia2013Profile.cs" /> + <Compile Include="Profiles\SonyBravia2014Profile.cs" /> <Compile Include="Profiles\SonyPs3Profile.cs" /> + <Compile Include="Profiles\SonyPs4Profile.cs" /> + <Compile Include="Profiles\VlcProfile.cs" /> <Compile Include="Profiles\WdtvLiveProfile.cs" /> <Compile Include="Profiles\Xbox360Profile.cs" /> <Compile Include="Profiles\XboxOneProfile.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Server\Headers.cs" /> + <Compile Include="Server\DescriptionXmlBuilder.cs" /> <Compile Include="Server\UpnpDevice.cs" /> + <Compile Include="Service\BaseControlHandler.cs" /> + <Compile Include="Service\BaseService.cs" /> + <Compile Include="Service\ControlErrorHandler.cs" /> + <Compile Include="Service\ServiceXmlBuilder.cs" /> + <Compile Include="Ssdp\DeviceDiscovery.cs" /> + <Compile Include="Ssdp\Extensions.cs" /> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="Images\logo120.jpg" /> + <EmbeddedResource Include="Images\logo120.png" /> + <EmbeddedResource Include="Images\logo240.jpg" /> + <EmbeddedResource Include="Images\logo240.png" /> + <EmbeddedResource Include="Images\logo48.jpg" /> + <EmbeddedResource Include="Images\logo48.png" /> + <EmbeddedResource Include="Images\people48.jpg" /> + <EmbeddedResource Include="Images\people48.png" /> + <EmbeddedResource Include="Images\people480.jpg" /> + <EmbeddedResource Include="Images\people480.png" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> @@ -172,90 +143,44 @@ <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> <Name>MediaBrowser.Model</Name> </ProjectReference> + <ProjectReference Include="..\RSSDP\RSSDP.csproj"> + <Project>{21002819-c39a-4d3e-be83-2a276a77fb1f}</Project> + <Name>RSSDP</Name> + </ProjectReference> </ItemGroup> + <ItemGroup /> <ItemGroup> + <EmbeddedResource Include="Profiles\Xml\BubbleUPnp.xml" /> + <EmbeddedResource Include="Profiles\Xml\Default.xml" /> <EmbeddedResource Include="Profiles\Xml\Denon AVR.xml" /> + <EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" /> + <EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" /> <EmbeddedResource Include="Profiles\Xml\foobar2000.xml" /> + <EmbeddedResource Include="Profiles\Xml\Kodi.xml" /> <EmbeddedResource Include="Profiles\Xml\LG Smart TV.xml" /> <EmbeddedResource Include="Profiles\Xml\Linksys DMA2100.xml" /> + <EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" /> <EmbeddedResource Include="Profiles\Xml\Panasonic Viera.xml" /> + <EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" /> <EmbeddedResource Include="Profiles\Xml\Samsung Smart TV.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2013.xml" /> + <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" /> + <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" /> + <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282010%29.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282011%29.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282012%29.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml"> - <SubType>Designer</SubType> - </EmbeddedResource> + <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282013%29.xml" /> + <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml" /> <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 3.xml" /> + <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" /> + <EmbeddedResource Include="Profiles\Xml\Vlc.xml" /> <EmbeddedResource Include="Profiles\Xml\WDTV Live.xml" /> - <EmbeddedResource Include="Profiles\Xml\Xbox 360.xml"> - <SubType>Designer</SubType> - </EmbeddedResource> + <EmbeddedResource Include="Profiles\Xml\Xbox 360.xml" /> <EmbeddedResource Include="Profiles\Xml\Xbox One.xml" /> </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Default.xml"> - <SubType>Designer</SubType> - </EmbeddedResource> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Images\logo120.jpg" /> - <EmbeddedResource Include="Images\logo120.png" /> - <EmbeddedResource Include="Images\logo48.jpg" /> - <EmbeddedResource Include="Images\logo48.png" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\MediaMonkey.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Images\logo240.jpg" /> - <EmbeddedResource Include="Images\logo240.png" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\DirecTV HD-DVR.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Dish Hopper-Joey.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Popcorn Hour.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Images\people48.jpg" /> - <EmbeddedResource Include="Images\people48.png" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Images\people480.jpg" /> - <EmbeddedResource Include="Images\people480.png" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\BubbleUPnp.xml" /> - <EmbeddedResource Include="Profiles\Xml\Vlc.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Sony PlayStation 4.xml" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Kodi.xml"> - <SubType>Designer</SubType> - </EmbeddedResource> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Sony Bravia %282014%29.xml"> - <SubType>Designer</SubType> - </EmbeddedResource> - </ItemGroup> - <ItemGroup> - <None Include="packages.config" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2014.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2015.xml" /> - <EmbeddedResource Include="Profiles\Xml\Sony Blu-ray Player 2016.xml" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/MediaBrowser.Dlna/Eventing/EventManager.cs b/Emby.Dlna/Eventing/EventManager.cs similarity index 99% rename from MediaBrowser.Dlna/Eventing/EventManager.cs rename to Emby.Dlna/Eventing/EventManager.cs index 51c8d2d919..cf2c8d9956 100644 --- a/MediaBrowser.Dlna/Eventing/EventManager.cs +++ b/Emby.Dlna/Eventing/EventManager.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace MediaBrowser.Dlna.Eventing +namespace Emby.Dlna.Eventing { public class EventManager : IEventManager { diff --git a/MediaBrowser.Dlna/Eventing/EventSubscription.cs b/Emby.Dlna/Eventing/EventSubscription.cs similarity index 95% rename from MediaBrowser.Dlna/Eventing/EventSubscription.cs rename to Emby.Dlna/Eventing/EventSubscription.cs index bd4cbdd55d..adb042d6c4 100644 --- a/MediaBrowser.Dlna/Eventing/EventSubscription.cs +++ b/Emby.Dlna/Eventing/EventSubscription.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.Eventing +namespace Emby.Dlna.Eventing { public class EventSubscription { diff --git a/MediaBrowser.Dlna/Images/logo120.jpg b/Emby.Dlna/Images/logo120.jpg similarity index 100% rename from MediaBrowser.Dlna/Images/logo120.jpg rename to Emby.Dlna/Images/logo120.jpg diff --git a/MediaBrowser.Dlna/Images/logo120.png b/Emby.Dlna/Images/logo120.png similarity index 100% rename from MediaBrowser.Dlna/Images/logo120.png rename to Emby.Dlna/Images/logo120.png diff --git a/MediaBrowser.Dlna/Images/logo240.jpg b/Emby.Dlna/Images/logo240.jpg similarity index 100% rename from MediaBrowser.Dlna/Images/logo240.jpg rename to Emby.Dlna/Images/logo240.jpg diff --git a/MediaBrowser.Dlna/Images/logo240.png b/Emby.Dlna/Images/logo240.png similarity index 100% rename from MediaBrowser.Dlna/Images/logo240.png rename to Emby.Dlna/Images/logo240.png diff --git a/MediaBrowser.Dlna/Images/logo48.jpg b/Emby.Dlna/Images/logo48.jpg similarity index 100% rename from MediaBrowser.Dlna/Images/logo48.jpg rename to Emby.Dlna/Images/logo48.jpg diff --git a/MediaBrowser.Dlna/Images/logo48.png b/Emby.Dlna/Images/logo48.png similarity index 100% rename from MediaBrowser.Dlna/Images/logo48.png rename to Emby.Dlna/Images/logo48.png diff --git a/MediaBrowser.Dlna/Images/people48.jpg b/Emby.Dlna/Images/people48.jpg similarity index 100% rename from MediaBrowser.Dlna/Images/people48.jpg rename to Emby.Dlna/Images/people48.jpg diff --git a/MediaBrowser.Dlna/Images/people48.png b/Emby.Dlna/Images/people48.png similarity index 100% rename from MediaBrowser.Dlna/Images/people48.png rename to Emby.Dlna/Images/people48.png diff --git a/MediaBrowser.Dlna/Images/people480.jpg b/Emby.Dlna/Images/people480.jpg similarity index 100% rename from MediaBrowser.Dlna/Images/people480.jpg rename to Emby.Dlna/Images/people480.jpg diff --git a/MediaBrowser.Dlna/Images/people480.png b/Emby.Dlna/Images/people480.png similarity index 100% rename from MediaBrowser.Dlna/Images/people480.png rename to Emby.Dlna/Images/people480.png diff --git a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs b/Emby.Dlna/Main/DlnaEntryPoint.cs similarity index 82% rename from MediaBrowser.Dlna/Main/DlnaEntryPoint.cs rename to Emby.Dlna/Main/DlnaEntryPoint.cs index 277f80b478..16108522c3 100644 --- a/MediaBrowser.Dlna/Main/DlnaEntryPoint.cs +++ b/Emby.Dlna/Main/DlnaEntryPoint.cs @@ -6,11 +6,10 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; -using MediaBrowser.Dlna.PlayTo; -using MediaBrowser.Dlna.Ssdp; +using Emby.Dlna.PlayTo; +using Emby.Dlna.Ssdp; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; @@ -18,17 +17,21 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Threading; using Rssdp; using Rssdp.Infrastructure; -namespace MediaBrowser.Dlna.Main +namespace Emby.Dlna.Main { public class DlnaEntryPoint : IServerEntryPoint { private readonly IServerConfigurationManager _config; private readonly ILogger _logger; private readonly IServerApplicationHost _appHost; - private readonly INetworkManager _network; private PlayToManager _manager; private readonly ISessionManager _sessionManager; @@ -48,10 +51,16 @@ namespace MediaBrowser.Dlna.Main private bool _dlnaServerStarted; private SsdpDevicePublisher _Publisher; + private readonly ITimerFactory _timerFactory; + private readonly ISocketFactory _socketFactory; + private readonly IEnvironmentInfo _environmentInfo; + private readonly INetworkManager _networkManager; + + private ISsdpCommunicationsServer _communicationsServer; + public DlnaEntryPoint(IServerConfigurationManager config, ILogManager logManager, IServerApplicationHost appHost, - INetworkManager network, ISessionManager sessionManager, IHttpClient httpClient, ILibraryManager libraryManager, @@ -61,11 +70,10 @@ namespace MediaBrowser.Dlna.Main IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, - IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder) + IDeviceDiscovery deviceDiscovery, IMediaEncoder mediaEncoder, ISocketFactory socketFactory, ITimerFactory timerFactory, IEnvironmentInfo environmentInfo, INetworkManager networkManager) { _config = config; _appHost = appHost; - _network = network; _sessionManager = sessionManager; _httpClient = httpClient; _libraryManager = libraryManager; @@ -77,6 +85,10 @@ namespace MediaBrowser.Dlna.Main _mediaSourceManager = mediaSourceManager; _deviceDiscovery = deviceDiscovery; _mediaEncoder = mediaEncoder; + _socketFactory = socketFactory; + _timerFactory = timerFactory; + _environmentInfo = environmentInfo; + _networkManager = networkManager; _logger = logManager.GetLogger("Dlna"); } @@ -144,10 +156,20 @@ namespace MediaBrowser.Dlna.Main { try { - StartPublishing(); + if (_communicationsServer == null) + { + var enableMultiSocketBinding = _environmentInfo.OperatingSystem == OperatingSystem.Windows; + + _communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding) + { + IsShared = true + }; + } + + StartPublishing(_communicationsServer); _ssdpHandlerStarted = true; - StartDeviceDiscovery(); + StartDeviceDiscovery(_communicationsServer); } catch (Exception ex) { @@ -157,20 +179,20 @@ namespace MediaBrowser.Dlna.Main private void LogMessage(string msg) { - //_logger.Debug(msg); + _logger.Debug(msg); } - private void StartPublishing() + private void StartPublishing(ISsdpCommunicationsServer communicationsServer) { SsdpDevicePublisherBase.LogFunction = LogMessage; - _Publisher = new SsdpDevicePublisher(); + _Publisher = new SsdpDevicePublisher(communicationsServer, _timerFactory, _environmentInfo.OperatingSystemName, _environmentInfo.OperatingSystemVersion); } - private void StartDeviceDiscovery() + private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer) { try { - ((DeviceDiscovery)_deviceDiscovery).Start(); + ((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer); } catch (Exception ex) { @@ -232,6 +254,8 @@ namespace MediaBrowser.Dlna.Main var addresses = (await _appHost.GetLocalIpAddresses().ConfigureAwait(false)).ToList(); + var udn = CreateUuid(_appHost.SystemId); + foreach (var address in addresses) { //if (IPAddress.IsLoopback(address)) @@ -240,13 +264,9 @@ namespace MediaBrowser.Dlna.Main // continue; //} - var addressString = address.ToString(); - - var udn = CreateUuid(addressString); - var fullService = "urn:schemas-upnp-org:device:MediaServer:1"; - _logger.Info("Registering publisher for {0} on {1}", fullService, addressString); + _logger.Info("Registering publisher for {0} on {1}", fullService, address.ToString()); var descriptorUri = "/dlna/" + udn + "/description.xml"; var uri = new Uri(_appHost.GetLocalApiUrl(address) + descriptorUri); @@ -269,7 +289,7 @@ namespace MediaBrowser.Dlna.Main { "urn:schemas-upnp-org:service:ContentDirectory:1", "urn:schemas-upnp-org:service:ConnectionManager:1", - "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" + //"urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1" }; foreach (var subDevice in embeddedDevices) @@ -291,7 +311,12 @@ namespace MediaBrowser.Dlna.Main private string CreateUuid(string text) { - return text.GetMD5().ToString("N"); + Guid guid; + if (!Guid.TryParse(text, out guid)) + { + guid = text.GetMD5(); + } + return guid.ToString("N"); } private void SetProperies(SsdpDevice device, string fullDeviceType) @@ -327,7 +352,8 @@ namespace MediaBrowser.Dlna.Main _userDataManager, _localization, _mediaSourceManager, - _mediaEncoder); + _mediaEncoder, + _timerFactory); _manager.Start(); } @@ -362,6 +388,12 @@ namespace MediaBrowser.Dlna.Main DisposeDlnaServer(); DisposePlayToManager(); DisposeSsdpHandler(); + + if (_communicationsServer != null) + { + _communicationsServer.Dispose(); + _communicationsServer = null; + } } public void DisposeDlnaServer() diff --git a/MediaBrowser.Dlna/MediaReceiverRegistrar/ControlHandler.cs b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs similarity index 69% rename from MediaBrowser.Dlna/MediaReceiverRegistrar/ControlHandler.cs rename to Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs index d1f701711a..daf46b1061 100644 --- a/MediaBrowser.Dlna/MediaReceiverRegistrar/ControlHandler.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ControlHandler.cs @@ -1,20 +1,17 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Dlna.Server; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Server; +using Emby.Dlna.Service; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.MediaReceiverRegistrar +namespace Emby.Dlna.MediaReceiverRegistrar { public class ControlHandler : BaseControlHandler { - public ControlHandler(IServerConfigurationManager config, ILogger logger) : base(config, logger) - { - } - - protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, Headers methodParams) + protected override IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams) { if (string.Equals(methodName, "IsAuthorized", StringComparison.OrdinalIgnoreCase)) return HandleIsAuthorized(); @@ -26,7 +23,7 @@ namespace MediaBrowser.Dlna.MediaReceiverRegistrar private IEnumerable<KeyValuePair<string, string>> HandleIsAuthorized() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Result", "1" } }; @@ -34,10 +31,14 @@ namespace MediaBrowser.Dlna.MediaReceiverRegistrar private IEnumerable<KeyValuePair<string, string>> HandleIsValidated() { - return new Headers(true) + return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { { "Result", "1" } }; } + + public ControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(config, logger, xmlReaderSettingsFactory) + { + } } } diff --git a/MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs similarity index 70% rename from MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs rename to Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs index a3b2bcee0c..365354efd9 100644 --- a/MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrar.cs @@ -1,21 +1,24 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Service; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; +using MediaBrowser.Model.Xml; -namespace MediaBrowser.Dlna.MediaReceiverRegistrar +namespace Emby.Dlna.MediaReceiverRegistrar { public class MediaReceiverRegistrar : BaseService, IMediaReceiverRegistrar, IDisposable { private readonly IServerConfigurationManager _config; + protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; - public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config) + public MediaReceiverRegistrar(ILogger logger, IHttpClient httpClient, IServerConfigurationManager config, IXmlReaderSettingsFactory xmlReaderSettingsFactory) : base(logger, httpClient) { _config = config; + XmlReaderSettingsFactory = xmlReaderSettingsFactory; } public string GetServiceXml(IDictionary<string, string> headers) @@ -27,7 +30,7 @@ namespace MediaBrowser.Dlna.MediaReceiverRegistrar { return new ControlHandler( _config, - Logger) + Logger, XmlReaderSettingsFactory) .ProcessControlRequest(request); } diff --git a/MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs similarity index 94% rename from MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs rename to Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs index cb1fdcecbf..bc4bee7c92 100644 --- a/MediaBrowser.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/MediaReceiverRegistrarXmlBuilder.cs @@ -1,8 +1,8 @@ -using MediaBrowser.Dlna.Common; -using MediaBrowser.Dlna.Service; +using Emby.Dlna.Common; +using Emby.Dlna.Service; using System.Collections.Generic; -namespace MediaBrowser.Dlna.MediaReceiverRegistrar +namespace Emby.Dlna.MediaReceiverRegistrar { public class MediaReceiverRegistrarXmlBuilder { diff --git a/MediaBrowser.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs similarity index 97% rename from MediaBrowser.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs rename to Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs index 7e19805db3..691ba3061a 100644 --- a/MediaBrowser.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs +++ b/Emby.Dlna/MediaReceiverRegistrar/ServiceActionListBuilder.cs @@ -1,7 +1,7 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System.Collections.Generic; -namespace MediaBrowser.Dlna.MediaReceiverRegistrar +namespace Emby.Dlna.MediaReceiverRegistrar { public class ServiceActionListBuilder { diff --git a/MediaBrowser.Dlna/PlayTo/CurrentIdEventArgs.cs b/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs similarity index 77% rename from MediaBrowser.Dlna/PlayTo/CurrentIdEventArgs.cs rename to Emby.Dlna/PlayTo/CurrentIdEventArgs.cs index c34293e52b..99aa50bd91 100644 --- a/MediaBrowser.Dlna/PlayTo/CurrentIdEventArgs.cs +++ b/Emby.Dlna/PlayTo/CurrentIdEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class CurrentIdEventArgs : EventArgs { diff --git a/MediaBrowser.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs similarity index 93% rename from MediaBrowser.Dlna/PlayTo/Device.cs rename to Emby.Dlna/PlayTo/Device.cs index b656bc66ec..f6be479898 100644 --- a/MediaBrowser.Dlna/PlayTo/Device.cs +++ b/Emby.Dlna/PlayTo/Device.cs @@ -1,8 +1,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Dlna.Common; -using MediaBrowser.Dlna.Ssdp; +using Emby.Dlna.Common; +using Emby.Dlna.Ssdp; using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; using System; using System.Collections.Generic; using System.Globalization; @@ -12,17 +13,16 @@ using System.Security; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; +using Emby.Dlna.Server; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class Device : IDisposable { - const string ServiceAvtransportType = "urn:schemas-upnp-org:service:AVTransport:1"; - const string ServiceRenderingType = "urn:schemas-upnp-org:service:RenderingControl:1"; - #region Fields & Properties - private Timer _timer; + private ITimer _timer; public DeviceInfo Properties { get; set; } @@ -94,12 +94,15 @@ namespace MediaBrowser.Dlna.PlayTo public DateTime DateLastActivity { get; private set; } public Action OnDeviceUnavailable { get; set; } - public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config) + private readonly ITimerFactory _timerFactory; + + public Device(DeviceInfo deviceProperties, IHttpClient httpClient, ILogger logger, IServerConfigurationManager config, ITimerFactory timerFactory) { Properties = deviceProperties; _httpClient = httpClient; _logger = logger; _config = config; + _timerFactory = timerFactory; } private int GetPlaybackTimerIntervalMs() @@ -114,7 +117,7 @@ namespace MediaBrowser.Dlna.PlayTo public void Start() { - _timer = new Timer(TimerCallback, null, GetPlaybackTimerIntervalMs(), GetInactiveTimerIntervalMs()); + _timer = _timerFactory.Create(TimerCallback, null, GetPlaybackTimerIntervalMs(), GetInactiveTimerIntervalMs()); _timerActive = false; } @@ -248,13 +251,29 @@ namespace MediaBrowser.Dlna.PlayTo } } + private DeviceService GetServiceRenderingControl() + { + var services = Properties.Services; + + return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:RenderingControl:1", StringComparison.OrdinalIgnoreCase)) ?? + services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase)); + } + + private DeviceService GetAvTransportService() + { + var services = Properties.Services; + + return services.FirstOrDefault(s => string.Equals(s.ServiceType, "urn:schemas-upnp-org:service:AVTransport:1", StringComparison.OrdinalIgnoreCase)) ?? + services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:AVTransport", StringComparison.OrdinalIgnoreCase)); + } + private async Task<bool> SetMute(bool mute) { var command = RendererCommands.ServiceActions.FirstOrDefault(c => c.Name == "SetMute"); if (command == null) return false; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + var service = GetServiceRenderingControl(); if (service == null) { @@ -281,7 +300,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + var service = GetServiceRenderingControl(); if (service == null) { @@ -302,7 +321,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) { @@ -327,7 +346,7 @@ namespace MediaBrowser.Dlna.PlayTo {"CurrentURIMetaData", CreateDidlMeta(metaData)} }; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) { @@ -358,7 +377,7 @@ namespace MediaBrowser.Dlna.PlayTo if (string.IsNullOrEmpty(value)) return String.Empty; - return SecurityElement.Escape(value); + return DescriptionXmlBuilder.Escape(value); } public async Task SetPlay() @@ -367,7 +386,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) { @@ -384,7 +403,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.First(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); @@ -396,7 +415,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.First(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); await new SsdpHttpClient(_httpClient, _config).SendCommandAsync(Properties.BaseUrl, service, command.Name, AvCommands.BuildPost(command, service.ServiceType, 1)) .ConfigureAwait(false); @@ -469,7 +488,7 @@ namespace MediaBrowser.Dlna.PlayTo } } } - catch (WebException ex) + catch (HttpException ex) { if (_disposed) return; @@ -515,7 +534,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + var service = GetServiceRenderingControl(); if (service == null) { @@ -548,7 +567,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + var service = GetServiceRenderingControl(); if (service == null) { @@ -573,7 +592,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return null; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) return null; @@ -607,7 +626,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return null; - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) { @@ -638,7 +657,7 @@ namespace MediaBrowser.Dlna.PlayTo if (command == null) return new Tuple<bool, uBaseObject>(false, null); - var service = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var service = GetAvTransportService(); if (service == null) { @@ -691,7 +710,7 @@ namespace MediaBrowser.Dlna.PlayTo } XElement uPnpResponse; - + // Handle different variations sent back by devices try { @@ -774,28 +793,28 @@ namespace MediaBrowser.Dlna.PlayTo private async Task GetAVProtocolAsync() { - var avService = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceAvtransportType); + var avService = GetAvTransportService(); if (avService == null) return; string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); var httpClient = new SsdpHttpClient(_httpClient, _config); - var document = await httpClient.GetDataAsync(url); + var document = await httpClient.GetDataAsync(url).ConfigureAwait(false); AvCommands = TransportCommands.Create(document); } private async Task GetRenderingProtocolAsync() { - var avService = Properties.Services.FirstOrDefault(s => s.ServiceType == ServiceRenderingType); + var avService = GetServiceRenderingControl(); if (avService == null) return; string url = NormalizeUrl(Properties.BaseUrl, avService.ScpdUrl); var httpClient = new SsdpHttpClient(_httpClient, _config); - var document = await httpClient.GetDataAsync(url); + var document = await httpClient.GetDataAsync(url).ConfigureAwait(false); RendererCommands = TransportCommands.Create(document); } @@ -828,7 +847,7 @@ namespace MediaBrowser.Dlna.PlayTo set; } - public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger) + public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClient httpClient, IServerConfigurationManager config, ILogger logger, ITimerFactory timerFactory) { var ssdpHttpClient = new SsdpHttpClient(httpClient, config); @@ -893,8 +912,6 @@ namespace MediaBrowser.Dlna.PlayTo deviceProperties.Icon = CreateIcon(icon); } - var isRenderer = false; - foreach (var services in document.Descendants(uPnpNamespaces.ud.GetName("serviceList"))) { if (services == null) @@ -912,17 +929,13 @@ namespace MediaBrowser.Dlna.PlayTo if (service != null) { deviceProperties.Services.Add(service); - if (service.ServiceType == ServiceAvtransportType) - { - isRenderer = true; - } } } } - var device = new Device(deviceProperties, httpClient, logger, config); + var device = new Device(deviceProperties, httpClient, logger, config, timerFactory); - if (isRenderer) + if (device.GetAvTransportService() != null) { await device.GetRenderingProtocolAsync().ConfigureAwait(false); await device.GetAVProtocolAsync().ConfigureAwait(false); diff --git a/MediaBrowser.Dlna/PlayTo/DeviceInfo.cs b/Emby.Dlna/PlayTo/DeviceInfo.cs similarity index 96% rename from MediaBrowser.Dlna/PlayTo/DeviceInfo.cs rename to Emby.Dlna/PlayTo/DeviceInfo.cs index 24852466c2..d293bcea14 100644 --- a/MediaBrowser.Dlna/PlayTo/DeviceInfo.cs +++ b/Emby.Dlna/PlayTo/DeviceInfo.cs @@ -1,8 +1,8 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; using System.Collections.Generic; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class DeviceInfo { diff --git a/MediaBrowser.Dlna/PlayTo/PlayToController.cs b/Emby.Dlna/PlayTo/PlayToController.cs similarity index 99% rename from MediaBrowser.Dlna/PlayTo/PlayToController.cs rename to Emby.Dlna/PlayTo/PlayToController.cs index f6b24e6157..7dff8bda13 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToController.cs +++ b/Emby.Dlna/PlayTo/PlayToController.cs @@ -2,9 +2,8 @@ using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Session; -using MediaBrowser.Dlna.Didl; +using Emby.Dlna.Didl; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -20,8 +19,9 @@ using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Globalization; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlayToController : ISessionController, IDisposable { diff --git a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs b/Emby.Dlna/PlayTo/PlayToManager.cs similarity index 88% rename from MediaBrowser.Dlna/PlayTo/PlayToManager.cs rename to Emby.Dlna/PlayTo/PlayToManager.cs index 6d6986f017..a93f7da14e 100644 --- a/MediaBrowser.Dlna/PlayTo/PlayToManager.cs +++ b/Emby.Dlna/PlayTo/PlayToManager.cs @@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; @@ -14,9 +13,13 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { class PlayToManager : IDisposable { @@ -36,11 +39,12 @@ namespace MediaBrowser.Dlna.PlayTo private readonly IDeviceDiscovery _deviceDiscovery; private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; + private readonly ITimerFactory _timerFactory; private readonly List<string> _nonRendererUrls = new List<string>(); private DateTime _lastRendererClear; - public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) + public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, IServerConfigurationManager config, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, ITimerFactory timerFactory) { _logger = logger; _sessionManager = sessionManager; @@ -56,6 +60,7 @@ namespace MediaBrowser.Dlna.PlayTo _localization = localization; _mediaSourceManager = mediaSourceManager; _mediaEncoder = mediaEncoder; + _timerFactory = timerFactory; } public void Start() @@ -105,7 +110,7 @@ namespace MediaBrowser.Dlna.PlayTo var uri = info.Location; _logger.Debug("Attempting to create PlayToController from location {0}", location); - var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger).ConfigureAwait(false); + var device = await Device.CreateuPnpDeviceAsync(uri, _httpClient, _config, _logger, _timerFactory).ConfigureAwait(false); if (device.RendererCommands == null) { @@ -124,7 +129,16 @@ namespace MediaBrowser.Dlna.PlayTo if (controller == null) { - var serverAddress = await GetServerAddress(info.LocalEndPoint == null ? null : info.LocalEndPoint.Address).ConfigureAwait(false); + string serverAddress; + if (info.LocalIpAddress == null) + { + serverAddress = await GetServerAddress(null).ConfigureAwait(false); + } + else + { + serverAddress = await GetServerAddress(info.LocalIpAddress).ConfigureAwait(false); + } + string accessToken = null; sessionInfo.SessionController = controller = new PlayToController(sessionInfo, @@ -176,14 +190,14 @@ namespace MediaBrowser.Dlna.PlayTo } } - private Task<string> GetServerAddress(IPAddress localIp) + private Task<string> GetServerAddress(IpAddressInfo address) { - if (localIp == null) + if (address == null) { return _appHost.GetLocalApiUrl(); } - return Task.FromResult(_appHost.GetLocalApiUrl(localIp)); + return Task.FromResult(_appHost.GetLocalApiUrl(address)); } public void Dispose() diff --git a/MediaBrowser.Dlna/PlayTo/PlaybackProgressEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs similarity index 79% rename from MediaBrowser.Dlna/PlayTo/PlaybackProgressEventArgs.cs rename to Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs index 104697166c..b89f7a8645 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaybackProgressEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackProgressEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlaybackProgressEventArgs : EventArgs { diff --git a/MediaBrowser.Dlna/PlayTo/PlaybackStartEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs similarity index 79% rename from MediaBrowser.Dlna/PlayTo/PlaybackStartEventArgs.cs rename to Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs index 772eba55b1..17d2540a50 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaybackStartEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStartEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlaybackStartEventArgs : EventArgs { diff --git a/MediaBrowser.Dlna/PlayTo/PlaybackStoppedEventArgs.cs b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs similarity index 89% rename from MediaBrowser.Dlna/PlayTo/PlaybackStoppedEventArgs.cs rename to Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs index 5cf89a5bb1..847c33ff78 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaybackStoppedEventArgs.cs +++ b/Emby.Dlna/PlayTo/PlaybackStoppedEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlaybackStoppedEventArgs : EventArgs { diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs b/Emby.Dlna/PlayTo/PlaylistItem.cs similarity index 88% rename from MediaBrowser.Dlna/PlayTo/PlaylistItem.cs rename to Emby.Dlna/PlayTo/PlaylistItem.cs index e1f14bfa2c..b60e6a6fb6 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItem.cs +++ b/Emby.Dlna/PlayTo/PlaylistItem.cs @@ -1,6 +1,6 @@ using MediaBrowser.Model.Dlna; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlaylistItem { diff --git a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs similarity index 98% rename from MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs rename to Emby.Dlna/PlayTo/PlaylistItemFactory.cs index 317ec09699..3eb2bc1d5c 100644 --- a/MediaBrowser.Dlna/PlayTo/PlaylistItemFactory.cs +++ b/Emby.Dlna/PlayTo/PlaylistItemFactory.cs @@ -6,7 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class PlaylistItemFactory { diff --git a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs similarity index 98% rename from MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs rename to Emby.Dlna/PlayTo/SsdpHttpClient.cs index a1eb19d9aa..1aa671b8ff 100644 --- a/MediaBrowser.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -1,6 +1,6 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System; using System.Globalization; using System.IO; @@ -8,7 +8,7 @@ using System.Text; using System.Threading.Tasks; using System.Xml.Linq; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class SsdpHttpClient { diff --git a/MediaBrowser.Dlna/PlayTo/TRANSPORTSTATE.cs b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs similarity index 79% rename from MediaBrowser.Dlna/PlayTo/TRANSPORTSTATE.cs rename to Emby.Dlna/PlayTo/TRANSPORTSTATE.cs index d58c4413c0..93d306a178 100644 --- a/MediaBrowser.Dlna/PlayTo/TRANSPORTSTATE.cs +++ b/Emby.Dlna/PlayTo/TRANSPORTSTATE.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public enum TRANSPORTSTATE { diff --git a/MediaBrowser.Dlna/PlayTo/TransportCommands.cs b/Emby.Dlna/PlayTo/TransportCommands.cs similarity index 98% rename from MediaBrowser.Dlna/PlayTo/TransportCommands.cs rename to Emby.Dlna/PlayTo/TransportCommands.cs index c49767cfbb..d7d44573c7 100644 --- a/MediaBrowser.Dlna/PlayTo/TransportCommands.cs +++ b/Emby.Dlna/PlayTo/TransportCommands.cs @@ -1,11 +1,11 @@ using System; -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -using MediaBrowser.Dlna.Ssdp; +using Emby.Dlna.Ssdp; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class TransportCommands { diff --git a/MediaBrowser.Dlna/PlayTo/TransportStateEventArgs.cs b/Emby.Dlna/PlayTo/TransportStateEventArgs.cs similarity index 79% rename from MediaBrowser.Dlna/PlayTo/TransportStateEventArgs.cs rename to Emby.Dlna/PlayTo/TransportStateEventArgs.cs index 3e9aad8aef..c6a96f58c1 100644 --- a/MediaBrowser.Dlna/PlayTo/TransportStateEventArgs.cs +++ b/Emby.Dlna/PlayTo/TransportStateEventArgs.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class TransportStateEventArgs : EventArgs { diff --git a/MediaBrowser.Dlna/PlayTo/UpnpContainer.cs b/Emby.Dlna/PlayTo/UpnpContainer.cs similarity index 91% rename from MediaBrowser.Dlna/PlayTo/UpnpContainer.cs rename to Emby.Dlna/PlayTo/UpnpContainer.cs index e044d6b303..5bfc56bff9 100644 --- a/MediaBrowser.Dlna/PlayTo/UpnpContainer.cs +++ b/Emby.Dlna/PlayTo/UpnpContainer.cs @@ -1,8 +1,8 @@ using System; using System.Xml.Linq; -using MediaBrowser.Dlna.Ssdp; +using Emby.Dlna.Ssdp; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class UpnpContainer : uBaseObject { diff --git a/MediaBrowser.Dlna/PlayTo/uBaseObject.cs b/Emby.Dlna/PlayTo/uBaseObject.cs similarity index 69% rename from MediaBrowser.Dlna/PlayTo/uBaseObject.cs rename to Emby.Dlna/PlayTo/uBaseObject.cs index 7903941c85..1de46317eb 100644 --- a/MediaBrowser.Dlna/PlayTo/uBaseObject.cs +++ b/Emby.Dlna/PlayTo/uBaseObject.cs @@ -1,6 +1,6 @@ using System; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class uBaseObject { @@ -38,17 +38,17 @@ namespace MediaBrowser.Dlna.PlayTo { var classType = UpnpClass ?? string.Empty; - if (classType.IndexOf(Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1) + if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Audio, StringComparison.Ordinal) != -1) { - return Model.Entities.MediaType.Audio; + return MediaBrowser.Model.Entities.MediaType.Audio; } - if (classType.IndexOf(Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) + if (classType.IndexOf(MediaBrowser.Model.Entities.MediaType.Video, StringComparison.Ordinal) != -1) { - return Model.Entities.MediaType.Video; + return MediaBrowser.Model.Entities.MediaType.Video; } if (classType.IndexOf("image", StringComparison.Ordinal) != -1) { - return Model.Entities.MediaType.Photo; + return MediaBrowser.Model.Entities.MediaType.Photo; } return null; diff --git a/MediaBrowser.Dlna/PlayTo/uParser.cs b/Emby.Dlna/PlayTo/uParser.cs similarity index 97% rename from MediaBrowser.Dlna/PlayTo/uParser.cs rename to Emby.Dlna/PlayTo/uParser.cs index 838ddfc317..5caf83a9a6 100644 --- a/MediaBrowser.Dlna/PlayTo/uParser.cs +++ b/Emby.Dlna/PlayTo/uParser.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Xml.Linq; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class uParser { diff --git a/MediaBrowser.Dlna/PlayTo/uParserObject.cs b/Emby.Dlna/PlayTo/uParserObject.cs similarity index 77% rename from MediaBrowser.Dlna/PlayTo/uParserObject.cs rename to Emby.Dlna/PlayTo/uParserObject.cs index 265ef7f8d9..4e75adf1f1 100644 --- a/MediaBrowser.Dlna/PlayTo/uParserObject.cs +++ b/Emby.Dlna/PlayTo/uParserObject.cs @@ -1,6 +1,6 @@ using System.Xml.Linq; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class uParserObject { diff --git a/MediaBrowser.Dlna/PlayTo/uPnpNamespaces.cs b/Emby.Dlna/PlayTo/uPnpNamespaces.cs similarity index 98% rename from MediaBrowser.Dlna/PlayTo/uPnpNamespaces.cs rename to Emby.Dlna/PlayTo/uPnpNamespaces.cs index d44bdceaa8..81acb5e414 100644 --- a/MediaBrowser.Dlna/PlayTo/uPnpNamespaces.cs +++ b/Emby.Dlna/PlayTo/uPnpNamespaces.cs @@ -1,6 +1,6 @@ using System.Xml.Linq; -namespace MediaBrowser.Dlna.PlayTo +namespace Emby.Dlna.PlayTo { public class uPnpNamespaces { diff --git a/MediaBrowser.Dlna/Profiles/BubbleUpnpProfile.cs b/Emby.Dlna/Profiles/BubbleUpnpProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/BubbleUpnpProfile.cs rename to Emby.Dlna/Profiles/BubbleUpnpProfile.cs index 8f3ad82ba4..b551bff2ae 100644 --- a/MediaBrowser.Dlna/Profiles/BubbleUpnpProfile.cs +++ b/Emby.Dlna/Profiles/BubbleUpnpProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class BubbleUpnpProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/DefaultProfile.cs b/Emby.Dlna/Profiles/DefaultProfile.cs similarity index 98% rename from MediaBrowser.Dlna/Profiles/DefaultProfile.cs rename to Emby.Dlna/Profiles/DefaultProfile.cs index 48325d0d79..ff30f2e158 100644 --- a/MediaBrowser.Dlna/Profiles/DefaultProfile.cs +++ b/Emby.Dlna/Profiles/DefaultProfile.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class DefaultProfile : DeviceProfile @@ -30,8 +30,8 @@ namespace MediaBrowser.Dlna.Profiles MaxIconWidth = 48; MaxIconHeight = 48; - MaxStreamingBitrate = 20000000; - MaxStaticBitrate = 20000000; + MaxStreamingBitrate = 24000000; + MaxStaticBitrate = 24000000; MusicStreamingTranscodingBitrate = 192000; EnableAlbumArtInDidl = false; diff --git a/MediaBrowser.Dlna/Profiles/DenonAvrProfile.cs b/Emby.Dlna/Profiles/DenonAvrProfile.cs similarity index 95% rename from MediaBrowser.Dlna/Profiles/DenonAvrProfile.cs rename to Emby.Dlna/Profiles/DenonAvrProfile.cs index fb498c4ce4..eed2449891 100644 --- a/MediaBrowser.Dlna/Profiles/DenonAvrProfile.cs +++ b/Emby.Dlna/Profiles/DenonAvrProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class DenonAvrProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/DirectTvProfile.cs b/Emby.Dlna/Profiles/DirectTvProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/DirectTvProfile.cs rename to Emby.Dlna/Profiles/DirectTvProfile.cs index c2a007a31a..153d552045 100644 --- a/MediaBrowser.Dlna/Profiles/DirectTvProfile.cs +++ b/Emby.Dlna/Profiles/DirectTvProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class DirectTvProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/DishHopperJoeyProfile.cs b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/DishHopperJoeyProfile.cs rename to Emby.Dlna/Profiles/DishHopperJoeyProfile.cs index bd7b42d5d2..9f31377101 100644 --- a/MediaBrowser.Dlna/Profiles/DishHopperJoeyProfile.cs +++ b/Emby.Dlna/Profiles/DishHopperJoeyProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class DishHopperJoeyProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/Foobar2000Profile.cs b/Emby.Dlna/Profiles/Foobar2000Profile.cs similarity index 98% rename from MediaBrowser.Dlna/Profiles/Foobar2000Profile.cs rename to Emby.Dlna/Profiles/Foobar2000Profile.cs index 2c1919c00e..915c490484 100644 --- a/MediaBrowser.Dlna/Profiles/Foobar2000Profile.cs +++ b/Emby.Dlna/Profiles/Foobar2000Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class Foobar2000Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/KodiProfile.cs b/Emby.Dlna/Profiles/KodiProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/KodiProfile.cs rename to Emby.Dlna/Profiles/KodiProfile.cs index 5e1ac57608..dbcac6652c 100644 --- a/MediaBrowser.Dlna/Profiles/KodiProfile.cs +++ b/Emby.Dlna/Profiles/KodiProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class KodiProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/LgTvProfile.cs b/Emby.Dlna/Profiles/LgTvProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/LgTvProfile.cs rename to Emby.Dlna/Profiles/LgTvProfile.cs index c98dd04659..faaf63b314 100644 --- a/MediaBrowser.Dlna/Profiles/LgTvProfile.cs +++ b/Emby.Dlna/Profiles/LgTvProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class LgTvProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/LinksysDMA2100Profile.cs b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs similarity index 96% rename from MediaBrowser.Dlna/Profiles/LinksysDMA2100Profile.cs rename to Emby.Dlna/Profiles/LinksysDMA2100Profile.cs index 2488cf5423..4a4ecdc589 100644 --- a/MediaBrowser.Dlna/Profiles/LinksysDMA2100Profile.cs +++ b/Emby.Dlna/Profiles/LinksysDMA2100Profile.cs @@ -1,7 +1,7 @@ using System.Xml.Serialization; using MediaBrowser.Model.Dlna; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class LinksysDMA2100Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/MediaMonkeyProfile.cs b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs similarity index 98% rename from MediaBrowser.Dlna/Profiles/MediaMonkeyProfile.cs rename to Emby.Dlna/Profiles/MediaMonkeyProfile.cs index eef847852d..66bde1045b 100644 --- a/MediaBrowser.Dlna/Profiles/MediaMonkeyProfile.cs +++ b/Emby.Dlna/Profiles/MediaMonkeyProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class MediaMonkeyProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs rename to Emby.Dlna/Profiles/PanasonicVieraProfile.cs index 5edf3afbfe..f3d7f59512 100644 --- a/MediaBrowser.Dlna/Profiles/PanasonicVieraProfile.cs +++ b/Emby.Dlna/Profiles/PanasonicVieraProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class PanasonicVieraProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/PopcornHourProfile.cs b/Emby.Dlna/Profiles/PopcornHourProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/PopcornHourProfile.cs rename to Emby.Dlna/Profiles/PopcornHourProfile.cs index 0e1210afbb..0095c80a28 100644 --- a/MediaBrowser.Dlna/Profiles/PopcornHourProfile.cs +++ b/Emby.Dlna/Profiles/PopcornHourProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class PopcornHourProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs rename to Emby.Dlna/Profiles/SamsungSmartTvProfile.cs index aae520d6f0..5acdde327b 100644 --- a/MediaBrowser.Dlna/Profiles/SamsungSmartTvProfile.cs +++ b/Emby.Dlna/Profiles/SamsungSmartTvProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SamsungSmartTvProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013.cs rename to Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs index fefb961171..ac7f56b46f 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2013.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2013.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBlurayPlayer2013 : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2014.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2014.cs rename to Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs index 4f2ff3ad15..961ff30f28 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2014.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2014.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBlurayPlayer2014 : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2015.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2015.cs rename to Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs index 57cd5dad67..2573121b17 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2015.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2015.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBlurayPlayer2015 : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2016.cs b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2016.cs rename to Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs index f504820d14..1ffed3d62c 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayer2016.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayer2016.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBlurayPlayer2016 : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayerProfile.cs b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBlurayPlayerProfile.cs rename to Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs index f6cc036377..d1305d4246 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBlurayPlayerProfile.cs +++ b/Emby.Dlna/Profiles/SonyBlurayPlayerProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBlurayPlayerProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs rename to Emby.Dlna/Profiles/SonyBravia2010Profile.cs index a7f74b3697..5185503711 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2010Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2010Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBravia2010Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2011Profile.cs b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBravia2011Profile.cs rename to Emby.Dlna/Profiles/SonyBravia2011Profile.cs index fa258dd600..c21022aa3a 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2011Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2011Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBravia2011Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2012Profile.cs b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBravia2012Profile.cs rename to Emby.Dlna/Profiles/SonyBravia2012Profile.cs index a35cfc0dfa..1bbd40e914 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2012Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2012Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBravia2012Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2013Profile.cs b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBravia2013Profile.cs rename to Emby.Dlna/Profiles/SonyBravia2013Profile.cs index 16ff5dac5f..019bbafcb4 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2013Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2013Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBravia2013Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyBravia2014Profile.cs b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyBravia2014Profile.cs rename to Emby.Dlna/Profiles/SonyBravia2014Profile.cs index 02dbc88abe..910786b839 100644 --- a/MediaBrowser.Dlna/Profiles/SonyBravia2014Profile.cs +++ b/Emby.Dlna/Profiles/SonyBravia2014Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyBravia2014Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyPs3Profile.cs b/Emby.Dlna/Profiles/SonyPs3Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyPs3Profile.cs rename to Emby.Dlna/Profiles/SonyPs3Profile.cs index 6ad2b3fca2..001ef2bd8e 100644 --- a/MediaBrowser.Dlna/Profiles/SonyPs3Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs3Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyPs3Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/SonyPs4Profile.cs b/Emby.Dlna/Profiles/SonyPs4Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/SonyPs4Profile.cs rename to Emby.Dlna/Profiles/SonyPs4Profile.cs index dd974d252d..44649911dc 100644 --- a/MediaBrowser.Dlna/Profiles/SonyPs4Profile.cs +++ b/Emby.Dlna/Profiles/SonyPs4Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class SonyPs4Profile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/VlcProfile.cs b/Emby.Dlna/Profiles/VlcProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/VlcProfile.cs rename to Emby.Dlna/Profiles/VlcProfile.cs index 09d290f3ae..2cd0e5cae7 100644 --- a/MediaBrowser.Dlna/Profiles/VlcProfile.cs +++ b/Emby.Dlna/Profiles/VlcProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class VlcProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/WdtvLiveProfile.cs b/Emby.Dlna/Profiles/WdtvLiveProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/WdtvLiveProfile.cs rename to Emby.Dlna/Profiles/WdtvLiveProfile.cs index 5f9e30318f..e524816afe 100644 --- a/MediaBrowser.Dlna/Profiles/WdtvLiveProfile.cs +++ b/Emby.Dlna/Profiles/WdtvLiveProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class WdtvLiveProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/Xbox360Profile.cs b/Emby.Dlna/Profiles/Xbox360Profile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/Xbox360Profile.cs rename to Emby.Dlna/Profiles/Xbox360Profile.cs index 5a3e7b7c05..9f0130856f 100644 --- a/MediaBrowser.Dlna/Profiles/Xbox360Profile.cs +++ b/Emby.Dlna/Profiles/Xbox360Profile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { /// <summary> /// Good info on xbox 360 requirements: https://code.google.com/p/jems/wiki/XBox360Notes diff --git a/MediaBrowser.Dlna/Profiles/XboxOneProfile.cs b/Emby.Dlna/Profiles/XboxOneProfile.cs similarity index 99% rename from MediaBrowser.Dlna/Profiles/XboxOneProfile.cs rename to Emby.Dlna/Profiles/XboxOneProfile.cs index 367aa744b8..370534a676 100644 --- a/MediaBrowser.Dlna/Profiles/XboxOneProfile.cs +++ b/Emby.Dlna/Profiles/XboxOneProfile.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Dlna; using System.Xml.Serialization; -namespace MediaBrowser.Dlna.Profiles +namespace Emby.Dlna.Profiles { [XmlRoot("Profile")] public class XboxOneProfile : DefaultProfile diff --git a/MediaBrowser.Dlna/Profiles/Xml/BubbleUPnp.xml b/Emby.Dlna/Profiles/Xml/BubbleUPnp.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/BubbleUPnp.xml rename to Emby.Dlna/Profiles/Xml/BubbleUPnp.xml index 6ee4fe658e..25f981ee06 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/BubbleUPnp.xml +++ b/Emby.Dlna/Profiles/Xml/BubbleUPnp.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -40,9 +40,9 @@ <DirectPlayProfile container="" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Default.xml b/Emby.Dlna/Profiles/Xml/Default.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/Default.xml rename to Emby.Dlna/Profiles/Xml/Default.xml index c13224f37a..81209c1f68 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Default.xml +++ b/Emby.Dlna/Profiles/Xml/Default.xml @@ -16,8 +16,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -33,9 +33,9 @@ <DirectPlayProfile container="mp3,wma,aac,wav" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml b/Emby.Dlna/Profiles/Xml/Denon AVR.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml rename to Emby.Dlna/Profiles/Xml/Denon AVR.xml index ea6736d4c1..cdf09df0e9 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Denon AVR.xml +++ b/Emby.Dlna/Profiles/Xml/Denon AVR.xml @@ -21,8 +21,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -37,9 +37,9 @@ <DirectPlayProfile container="mp3,flac,m4a,wma" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/DirecTV HD-DVR.xml b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml similarity index 98% rename from MediaBrowser.Dlna/Profiles/Xml/DirecTV HD-DVR.xml rename to Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml index b5dde8d5c0..b8afed60e8 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/DirecTV HD-DVR.xml +++ b/Emby.Dlna/Profiles/Xml/DirecTV HD-DVR.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -39,8 +39,8 @@ <DirectPlayProfile container="jpeg,jpg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mpeg" type="Video" videoCodec="mpeg2video" audioCodec="mp2" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Dish Hopper-Joey.xml b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml similarity index 93% rename from MediaBrowser.Dlna/Profiles/Xml/Dish Hopper-Joey.xml rename to Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml index daf34b89b9..3ad2a01295 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Dish Hopper-Joey.xml +++ b/Emby.Dlna/Profiles/Xml/Dish Hopper-Joey.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -43,9 +43,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Kodi.xml b/Emby.Dlna/Profiles/Xml/Kodi.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Kodi.xml rename to Emby.Dlna/Profiles/Xml/Kodi.xml index f1083f5e24..58f9e8e235 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Kodi.xml +++ b/Emby.Dlna/Profiles/Xml/Kodi.xml @@ -23,7 +23,7 @@ <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> <MaxStreamingBitrate>100000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>1280000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -40,9 +40,9 @@ <DirectPlayProfile container="" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml rename to Emby.Dlna/Profiles/Xml/LG Smart TV.xml index 945383bdcd..3a185e7331 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/LG Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/LG Smart TV.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -42,9 +42,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml rename to Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml index 195863668f..5723f144bf 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Linksys DMA2100.xml +++ b/Emby.Dlna/Profiles/Xml/Linksys DMA2100.xml @@ -20,8 +20,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -37,9 +37,9 @@ <DirectPlayProfile container="avi,mp4,mkv,ts" type="Video" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml rename to Emby.Dlna/Profiles/Xml/MediaMonkey.xml index 9518fc4b02..929f6898e4 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/MediaMonkey.xml +++ b/Emby.Dlna/Profiles/Xml/MediaMonkey.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -43,9 +43,9 @@ <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml rename to Emby.Dlna/Profiles/Xml/Panasonic Viera.xml index d26346ff63..3e3049d302 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Panasonic Viera.xml +++ b/Emby.Dlna/Profiles/Xml/Panasonic Viera.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -50,9 +50,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Popcorn Hour.xml b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Popcorn Hour.xml rename to Emby.Dlna/Profiles/Xml/Popcorn Hour.xml index 68fea1733c..bc73d23716 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Popcorn Hour.xml +++ b/Emby.Dlna/Profiles/Xml/Popcorn Hour.xml @@ -16,8 +16,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -38,9 +38,9 @@ <DirectPlayProfile container="jpeg,gif,bmp,png" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mp4" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml rename to Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml index 1918c02976..31210fac14 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Samsung Smart TV.xml +++ b/Emby.Dlna/Profiles/Xml/Samsung Smart TV.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -50,9 +50,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="true" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="true" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml rename to Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml index f8b583b50f..2bf57d4260 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml +++ b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2013.xml @@ -26,8 +26,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -52,9 +52,9 @@ <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml rename to Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml index eaa37c6205..703edc35ae 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml +++ b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2014.xml @@ -26,8 +26,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -52,9 +52,9 @@ <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml rename to Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml index 368e892ff2..69f4b73c64 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml +++ b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2015.xml @@ -24,8 +24,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -50,9 +50,9 @@ <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml rename to Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml index 9ec096b7f4..a24cfab895 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml +++ b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player 2016.xml @@ -24,8 +24,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -50,9 +50,9 @@ <DirectPlayProfile container="jpeg,png,gif" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="mkv" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml rename to Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml index 08266d9438..7ce41dea0a 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Blu-ray Player.xml +++ b/Emby.Dlna/Profiles/Xml/Sony Blu-ray Player.xml @@ -24,8 +24,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -47,9 +47,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="mpeg2video" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="mpeg2video" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml similarity index 95% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml rename to Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml index e633f1a89d..600d76ea71 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2010).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2010).xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -45,9 +45,9 @@ <DirectPlayProfile container="mp3" audioCodec="mp3" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml rename to Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml index 07379c8d5c..232896c8a7 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2011).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2011).xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -48,9 +48,9 @@ <DirectPlayProfile container="asf" audioCodec="wmav2,wmapro,wmavoice" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml rename to Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml index 61870d7e78..5cf1c5eadb 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2012).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2012).xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -50,9 +50,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml rename to Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml index 21fc0ff563..f009d6f11b 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2013).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2013).xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -55,9 +55,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2014).xml b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2014).xml rename to Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml index bfa304b098..f996652a8c 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony Bravia (2014).xml +++ b/Emby.Dlna/Profiles/Xml/Sony Bravia (2014).xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -55,9 +55,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3" estimateContentLength="false" enableMpegtsM2TsMode="true" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml rename to Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml index 295f6fddb4..3608c343be 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 3.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 3.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -45,9 +45,9 @@ <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="ac3,aac,mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 4.xml b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 4.xml rename to Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml index 8f05e8eac4..498f9966fb 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Sony PlayStation 4.xml +++ b/Emby.Dlna/Profiles/Xml/Sony PlayStation 4.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -45,9 +45,9 @@ <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Vlc.xml b/Emby.Dlna/Profiles/Xml/Vlc.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/Vlc.xml rename to Emby.Dlna/Profiles/Xml/Vlc.xml index 8628e7f30b..3a5b7927bd 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Vlc.xml +++ b/Emby.Dlna/Profiles/Xml/Vlc.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -40,9 +40,9 @@ <DirectPlayProfile container="" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml b/Emby.Dlna/Profiles/Xml/WDTV Live.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml rename to Emby.Dlna/Profiles/Xml/WDTV Live.xml index ebd4eb9b58..bf3531a307 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/WDTV Live.xml +++ b/Emby.Dlna/Profiles/Xml/WDTV Live.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -51,9 +51,9 @@ <DirectPlayProfile container="jpeg,png,gif,bmp,tiff" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Photo"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml b/Emby.Dlna/Profiles/Xml/Xbox 360.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml rename to Emby.Dlna/Profiles/Xml/Xbox 360.xml index cd6968bd04..2f0fee8c18 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox 360.xml +++ b/Emby.Dlna/Profiles/Xml/Xbox 360.xml @@ -24,8 +24,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -46,9 +46,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="asf" type="Video" videoCodec="wmv2" audioCodec="wmav2" estimateContentLength="true" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="asf" type="Video" videoCodec="wmv2" audioCodec="wmav2" estimateContentLength="true" enableMpegtsM2TsMode="false" transcodeSeekInfo="Bytes" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Video" container="mp4,mov"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml b/Emby.Dlna/Profiles/Xml/Xbox One.xml similarity index 97% rename from MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml rename to Emby.Dlna/Profiles/Xml/Xbox One.xml index f442e3d66f..e47c308e43 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/Xbox One.xml +++ b/Emby.Dlna/Profiles/Xml/Xbox One.xml @@ -23,8 +23,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -46,9 +46,9 @@ <DirectPlayProfile container="jpeg" type="Photo" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" videoCodec="jpeg" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" videoCodec="jpeg" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles> <ContainerProfile type="Video" container="mp4,mov"> diff --git a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml b/Emby.Dlna/Profiles/Xml/foobar2000.xml similarity index 96% rename from MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml rename to Emby.Dlna/Profiles/Xml/foobar2000.xml index 53187db6dc..51f649e36a 100644 --- a/MediaBrowser.Dlna/Profiles/Xml/foobar2000.xml +++ b/Emby.Dlna/Profiles/Xml/foobar2000.xml @@ -22,8 +22,8 @@ <MaxAlbumArtHeight>480</MaxAlbumArtHeight> <MaxIconWidth>48</MaxIconWidth> <MaxIconHeight>48</MaxIconHeight> - <MaxStreamingBitrate>20000000</MaxStreamingBitrate> - <MaxStaticBitrate>20000000</MaxStaticBitrate> + <MaxStreamingBitrate>24000000</MaxStreamingBitrate> + <MaxStaticBitrate>24000000</MaxStaticBitrate> <MusicStreamingTranscodingBitrate>192000</MusicStreamingTranscodingBitrate> <MaxStaticMusicBitrate xsi:nil="true" /> <XDlnaDoc>DMS-1.50</XDlnaDoc> @@ -43,9 +43,9 @@ <DirectPlayProfile container="ogg" audioCodec="vorbis" type="Audio" /> </DirectPlayProfiles> <TranscodingProfiles> - <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> - <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" /> + <TranscodingProfile container="mp3" type="Audio" audioCodec="mp3" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="ts" type="Video" videoCodec="h264" audioCodec="aac" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> + <TranscodingProfile container="jpeg" type="Photo" estimateContentLength="false" enableMpegtsM2TsMode="false" transcodeSeekInfo="Auto" copyTimestamps="false" context="Streaming" enableSubtitlesInManifest="false" enableSplittingOnNonKeyFrames="false" /> </TranscodingProfiles> <ContainerProfiles /> <CodecProfiles /> diff --git a/Emby.Dlna/Properties/AssemblyInfo.cs b/Emby.Dlna/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..6f924f9e96 --- /dev/null +++ b/Emby.Dlna/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Resources; +using System.Reflection; +using System.Runtime.CompilerServices; +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("Emby.Dlna2")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Dlna2")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: NeutralResourcesLanguage("en")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs b/Emby.Dlna/Server/DescriptionXmlBuilder.cs similarity index 64% rename from MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs rename to Emby.Dlna/Server/DescriptionXmlBuilder.cs index 37006915c1..2a4a5792fa 100644 --- a/MediaBrowser.Dlna/Server/DescriptionXmlBuilder.cs +++ b/Emby.Dlna/Server/DescriptionXmlBuilder.cs @@ -1,4 +1,4 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Extensions; using System; @@ -8,7 +8,7 @@ using System.Linq; using System.Security; using System.Text; -namespace MediaBrowser.Dlna.Server +namespace Emby.Dlna.Server { public class DescriptionXmlBuilder { @@ -94,45 +94,118 @@ namespace MediaBrowser.Dlna.Server builder.Append("</device>"); } + private static readonly char[] s_escapeChars = new char[] + { + '<', + '>', + '"', + '\'', + '&' + }; + + private static readonly string[] s_escapeStringPairs = new string[] +{ + "<", + "<", + ">", + ">", + "\"", + """, + "'", + "'", + "&", + "&" +}; + + private static string GetEscapeSequence(char c) + { + int num = s_escapeStringPairs.Length; + for (int i = 0; i < num; i += 2) + { + string text = s_escapeStringPairs[i]; + string result = s_escapeStringPairs[i + 1]; + if (text[0] == c) + { + return result; + } + } + return c.ToString(); + } + + /// <summary>Replaces invalid XML characters in a string with their valid XML equivalent.</summary> + /// <returns>The input string with invalid characters replaced.</returns> + /// <param name="str">The string within which to escape invalid characters. </param> + public static string Escape(string str) + { + if (str == null) + { + return null; + } + StringBuilder stringBuilder = null; + int length = str.Length; + int num = 0; + while (true) + { + int num2 = str.IndexOfAny(s_escapeChars, num); + if (num2 == -1) + { + break; + } + if (stringBuilder == null) + { + stringBuilder = new StringBuilder(); + } + stringBuilder.Append(str, num, num2 - num); + stringBuilder.Append(GetEscapeSequence(str[num2])); + num = num2 + 1; + } + if (stringBuilder == null) + { + return str; + } + stringBuilder.Append(str, num, length - num); + return stringBuilder.ToString(); + } + private void AppendDeviceProperties(StringBuilder builder) { builder.Append("<deviceType>urn:schemas-upnp-org:device:MediaServer:1</deviceType>"); - builder.Append("<dlna:X_DLNACAP>" + SecurityElement.Escape(_profile.XDlnaCap ?? string.Empty) + "</dlna:X_DLNACAP>"); + builder.Append("<dlna:X_DLNACAP>" + Escape(_profile.XDlnaCap ?? string.Empty) + "</dlna:X_DLNACAP>"); builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">M-DMS-1.50</dlna:X_DLNADOC>"); - builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + SecurityElement.Escape(_profile.XDlnaDoc ?? string.Empty) + "</dlna:X_DLNADOC>"); + builder.Append("<dlna:X_DLNADOC xmlns:dlna=\"urn:schemas-dlna-org:device-1-0\">" + Escape(_profile.XDlnaDoc ?? string.Empty) + "</dlna:X_DLNADOC>"); - builder.Append("<friendlyName>" + SecurityElement.Escape(GetFriendlyName()) + "</friendlyName>"); - builder.Append("<manufacturer>" + SecurityElement.Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); - builder.Append("<manufacturerURL>" + SecurityElement.Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); + builder.Append("<friendlyName>" + Escape(GetFriendlyName()) + "</friendlyName>"); + builder.Append("<manufacturer>" + Escape(_profile.Manufacturer ?? string.Empty) + "</manufacturer>"); + builder.Append("<manufacturerURL>" + Escape(_profile.ManufacturerUrl ?? string.Empty) + "</manufacturerURL>"); - builder.Append("<modelDescription>" + SecurityElement.Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); - builder.Append("<modelName>" + SecurityElement.Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); + builder.Append("<modelDescription>" + Escape(_profile.ModelDescription ?? string.Empty) + "</modelDescription>"); + builder.Append("<modelName>" + Escape(_profile.ModelName ?? string.Empty) + "</modelName>"); - builder.Append("<modelNumber>" + SecurityElement.Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); - builder.Append("<modelURL>" + SecurityElement.Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); + builder.Append("<modelNumber>" + Escape(_profile.ModelNumber ?? string.Empty) + "</modelNumber>"); + builder.Append("<modelURL>" + Escape(_profile.ModelUrl ?? string.Empty) + "</modelURL>"); if (string.IsNullOrWhiteSpace(_profile.SerialNumber)) { - builder.Append("<serialNumber>" + SecurityElement.Escape(_serverId) + "</serialNumber>"); + builder.Append("<serialNumber>" + Escape(_serverId) + "</serialNumber>"); } else { - builder.Append("<serialNumber>" + SecurityElement.Escape(_profile.SerialNumber) + "</serialNumber>"); + builder.Append("<serialNumber>" + Escape(_profile.SerialNumber) + "</serialNumber>"); } - builder.Append("<UDN>uuid:" + SecurityElement.Escape(_serverUdn) + "</UDN>"); - builder.Append("<presentationURL>" + SecurityElement.Escape(_serverAddress) + "</presentationURL>"); + builder.Append("<UDN>uuid:" + Escape(_serverUdn) + "</UDN>"); + builder.Append("<presentationURL>" + Escape(_serverAddress) + "</presentationURL>"); if (!EnableAbsoluteUrls) { - //builder.Append("<URLBase>" + SecurityElement.Escape(_serverAddress) + "</URLBase>"); + //builder.Append("<URLBase>" + Escape(_serverAddress) + "</URLBase>"); } if (!string.IsNullOrWhiteSpace(_profile.SonyAggregationFlags)) { - builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + SecurityElement.Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); + builder.Append("<av:aggregationFlags xmlns:av=\"urn:schemas-sony-com:av\">" + Escape(_profile.SonyAggregationFlags) + "</av:aggregationFlags>"); } } @@ -143,7 +216,17 @@ namespace MediaBrowser.Dlna.Server return "Emby - " + _serverName; } - var characters = _serverName.Where(c => (char.IsLetterOrDigit(c) || c == '-')).ToArray(); + var characterList = new List<char>(); + + foreach (var c in _serverName) + { + if (char.IsLetterOrDigit(c) || c == '-') + { + characterList.Add(c); + } + } + + var characters = characterList.ToArray(); var serverName = new string(characters); @@ -160,10 +243,10 @@ namespace MediaBrowser.Dlna.Server { builder.Append("<icon>"); - builder.Append("<mimetype>" + SecurityElement.Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); - builder.Append("<width>" + SecurityElement.Escape(icon.Width.ToString(_usCulture)) + "</width>"); - builder.Append("<height>" + SecurityElement.Escape(icon.Height.ToString(_usCulture)) + "</height>"); - builder.Append("<depth>" + SecurityElement.Escape(icon.Depth ?? string.Empty) + "</depth>"); + builder.Append("<mimetype>" + Escape(icon.MimeType ?? string.Empty) + "</mimetype>"); + builder.Append("<width>" + Escape(icon.Width.ToString(_usCulture)) + "</width>"); + builder.Append("<height>" + Escape(icon.Height.ToString(_usCulture)) + "</height>"); + builder.Append("<depth>" + Escape(icon.Depth ?? string.Empty) + "</depth>"); builder.Append("<url>" + BuildUrl(icon.Url) + "</url>"); builder.Append("</icon>"); @@ -180,8 +263,8 @@ namespace MediaBrowser.Dlna.Server { builder.Append("<service>"); - builder.Append("<serviceType>" + SecurityElement.Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); - builder.Append("<serviceId>" + SecurityElement.Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); + builder.Append("<serviceType>" + Escape(service.ServiceType ?? string.Empty) + "</serviceType>"); + builder.Append("<serviceId>" + Escape(service.ServiceId ?? string.Empty) + "</serviceId>"); builder.Append("<SCPDURL>" + BuildUrl(service.ScpdUrl) + "</SCPDURL>"); builder.Append("<controlURL>" + BuildUrl(service.ControlUrl) + "</controlURL>"); builder.Append("<eventSubURL>" + BuildUrl(service.EventSubUrl) + "</eventSubURL>"); @@ -208,7 +291,7 @@ namespace MediaBrowser.Dlna.Server url = _serverAddress.TrimEnd('/') + url; } - return SecurityElement.Escape(url); + return Escape(url); } private IEnumerable<DeviceIcon> GetIcons() diff --git a/MediaBrowser.Dlna/Server/UpnpDevice.cs b/Emby.Dlna/Server/UpnpDevice.cs similarity index 86% rename from MediaBrowser.Dlna/Server/UpnpDevice.cs rename to Emby.Dlna/Server/UpnpDevice.cs index 355a35c012..46f3d1c833 100644 --- a/MediaBrowser.Dlna/Server/UpnpDevice.cs +++ b/Emby.Dlna/Server/UpnpDevice.cs @@ -1,7 +1,8 @@ using System; using System.Net; +using MediaBrowser.Model.Net; -namespace MediaBrowser.Dlna.Server +namespace Emby.Dlna.Server { public sealed class UpnpDevice { @@ -9,9 +10,9 @@ namespace MediaBrowser.Dlna.Server public readonly string Type; public readonly string USN; public readonly string Uuid; - public readonly IPAddress Address; + public readonly IpAddressInfo Address; - public UpnpDevice(string aUuid, string aType, Uri aDescriptor, IPAddress address) + public UpnpDevice(string aUuid, string aType, Uri aDescriptor, IpAddressInfo address) { Uuid = aUuid; Type = aType; diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs new file mode 100644 index 0000000000..3092589c12 --- /dev/null +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -0,0 +1,260 @@ +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Dlna; +using Emby.Dlna.Server; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; +using Emby.Dlna.Didl; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Model.Xml; + +namespace Emby.Dlna.Service +{ + public abstract class BaseControlHandler + { + private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; + + protected readonly IServerConfigurationManager Config; + protected readonly ILogger Logger; + protected readonly IXmlReaderSettingsFactory XmlReaderSettingsFactory; + + protected BaseControlHandler(IServerConfigurationManager config, ILogger logger, IXmlReaderSettingsFactory xmlReaderSettingsFactory) + { + Config = config; + Logger = logger; + XmlReaderSettingsFactory = xmlReaderSettingsFactory; + } + + public ControlResponse ProcessControlRequest(ControlRequest request) + { + try + { + var enableDebugLogging = Config.GetDlnaConfiguration().EnableDebugLog; + + if (enableDebugLogging) + { + LogRequest(request); + } + + var response = ProcessControlRequestInternal(request); + + if (enableDebugLogging) + { + LogResponse(response); + } + + return response; + } + catch (Exception ex) + { + Logger.ErrorException("Error processing control request", ex); + + return new ControlErrorHandler().GetResponse(ex); + } + } + + private ControlResponse ProcessControlRequestInternal(ControlRequest request) + { + ControlRequestInfo requestInfo = null; + + using (var streamReader = new StreamReader(request.InputXml)) + { + var readerSettings = XmlReaderSettingsFactory.Create(false); + + readerSettings.CheckCharacters = false; + readerSettings.IgnoreProcessingInstructions = true; + readerSettings.IgnoreComments = true; + + using (var reader = XmlReader.Create(streamReader, readerSettings)) + { + requestInfo = ParseRequest(reader); + } + } + + Logger.Debug("Received control request {0}", requestInfo.LocalName); + + var result = GetResult(requestInfo.LocalName, requestInfo.Headers); + + var settings = new XmlWriterSettings + { + Encoding = Encoding.UTF8, + CloseOutput = false + }; + + StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); + + using (XmlWriter writer = XmlWriter.Create(builder, settings)) + { + writer.WriteStartDocument(true); + + writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); + writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + + writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); + writer.WriteStartElement("u", requestInfo.LocalName + "Response", requestInfo.NamespaceURI); + foreach (var i in result) + { + writer.WriteStartElement(i.Key); + writer.WriteString(i.Value); + writer.WriteFullEndElement(); + } + writer.WriteFullEndElement(); + writer.WriteFullEndElement(); + + writer.WriteFullEndElement(); + writer.WriteEndDocument(); + } + + var xml = builder.ToString().Replace("xmlns:m=", "xmlns:u="); + + var controlResponse = new ControlResponse + { + Xml = xml, + IsSuccessful = true + }; + + //Logger.Debug(xml); + + controlResponse.Headers.Add("EXT", string.Empty); + + return controlResponse; + } + + private ControlRequestInfo ParseRequest(XmlReader reader) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + switch (reader.LocalName) + { + case "Body": + { + if (!reader.IsEmptyElement) + { + using (var subReader = reader.ReadSubtree()) + { + return ParseBodyTag(subReader); + } + } + else + { + reader.Read(); + } + break; + } + default: + { + reader.Skip(); + break; + } + } + } + else + { + reader.Read(); + } + } + + return new ControlRequestInfo(); + } + + private ControlRequestInfo ParseBodyTag(XmlReader reader) + { + var result = new ControlRequestInfo(); + + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + result.LocalName = reader.LocalName; + result.NamespaceURI = reader.NamespaceURI; + + if (!reader.IsEmptyElement) + { + using (var subReader = reader.ReadSubtree()) + { + ParseFirstBodyChild(subReader, result.Headers); + return result; + } + } + else + { + reader.Read(); + } + } + else + { + reader.Read(); + } + } + + return result; + } + + private void ParseFirstBodyChild(XmlReader reader, IDictionary<string,string> headers) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + if (reader.NodeType == XmlNodeType.Element) + { + // TODO: Should we be doing this here, or should it be handled earlier when decoding the request? + headers[reader.LocalName.RemoveDiacritics()] = reader.ReadElementContentAsString(); + } + else + { + reader.Read(); + } + } + } + + private class ControlRequestInfo + { + public string LocalName; + public string NamespaceURI; + public IDictionary<string, string> Headers = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase); + } + + protected abstract IEnumerable<KeyValuePair<string, string>> GetResult(string methodName, IDictionary<string, string> methodParams); + + private void LogRequest(ControlRequest request) + { + var builder = new StringBuilder(); + + var headers = string.Join(", ", request.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); + builder.AppendFormat("Headers: {0}", headers); + builder.AppendLine(); + //builder.Append(request.InputXml); + + Logger.LogMultiline("Control request", LogSeverity.Debug, builder); + } + + private void LogResponse(ControlResponse response) + { + var builder = new StringBuilder(); + + var headers = string.Join(", ", response.Headers.Select(i => string.Format("{0}={1}", i.Key, i.Value)).ToArray()); + builder.AppendFormat("Headers: {0}", headers); + builder.AppendLine(); + builder.Append(response.Xml); + + Logger.LogMultiline("Control response", LogSeverity.Debug, builder); + } + } +} diff --git a/MediaBrowser.Dlna/Service/BaseService.cs b/Emby.Dlna/Service/BaseService.cs similarity index 94% rename from MediaBrowser.Dlna/Service/BaseService.cs rename to Emby.Dlna/Service/BaseService.cs index aeea7b8f34..574d749588 100644 --- a/MediaBrowser.Dlna/Service/BaseService.cs +++ b/Emby.Dlna/Service/BaseService.cs @@ -1,9 +1,9 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Dlna; -using MediaBrowser.Dlna.Eventing; +using Emby.Dlna.Eventing; using MediaBrowser.Model.Logging; -namespace MediaBrowser.Dlna.Service +namespace Emby.Dlna.Service { public class BaseService : IEventManager { diff --git a/Emby.Dlna/Service/ControlErrorHandler.cs b/Emby.Dlna/Service/ControlErrorHandler.cs new file mode 100644 index 0000000000..a3cd77f0fb --- /dev/null +++ b/Emby.Dlna/Service/ControlErrorHandler.cs @@ -0,0 +1,55 @@ +using MediaBrowser.Controller.Dlna; +using System; +using System.IO; +using System.Text; +using System.Xml; +using Emby.Dlna.Didl; + +namespace Emby.Dlna.Service +{ + public class ControlErrorHandler + { + private const string NS_SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"; + + public ControlResponse GetResponse(Exception ex) + { + var settings = new XmlWriterSettings + { + Encoding = Encoding.UTF8, + CloseOutput = false + }; + + StringWriter builder = new StringWriterWithEncoding(Encoding.UTF8); + + using (XmlWriter writer = XmlWriter.Create(builder, settings)) + { + writer.WriteStartDocument(true); + + writer.WriteStartElement("SOAP-ENV", "Envelope", NS_SOAPENV); + writer.WriteAttributeString(string.Empty, "encodingStyle", NS_SOAPENV, "http://schemas.xmlsoap.org/soap/encoding/"); + + writer.WriteStartElement("SOAP-ENV", "Body", NS_SOAPENV); + writer.WriteStartElement("SOAP-ENV", "Fault", NS_SOAPENV); + + writer.WriteElementString("faultcode", "500"); + writer.WriteElementString("faultstring", ex.Message); + + writer.WriteStartElement("detail"); + writer.WriteRaw("<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\"><errorCode>401</errorCode><errorDescription>Invalid Action</errorDescription></UPnPError>"); + writer.WriteFullEndElement(); + + writer.WriteFullEndElement(); + writer.WriteFullEndElement(); + + writer.WriteFullEndElement(); + writer.WriteEndDocument(); + } + + return new ControlResponse + { + Xml = builder.ToString(), + IsSuccessful = false + }; + } + } +} diff --git a/MediaBrowser.Dlna/Service/ServiceXmlBuilder.cs b/Emby.Dlna/Service/ServiceXmlBuilder.cs similarity index 70% rename from MediaBrowser.Dlna/Service/ServiceXmlBuilder.cs rename to Emby.Dlna/Service/ServiceXmlBuilder.cs index fae604f85c..08eb804033 100644 --- a/MediaBrowser.Dlna/Service/ServiceXmlBuilder.cs +++ b/Emby.Dlna/Service/ServiceXmlBuilder.cs @@ -1,9 +1,10 @@ -using MediaBrowser.Dlna.Common; +using Emby.Dlna.Common; using System.Collections.Generic; using System.Security; using System.Text; +using Emby.Dlna.Server; -namespace MediaBrowser.Dlna.Service +namespace Emby.Dlna.Service { public class ServiceXmlBuilder { @@ -35,7 +36,7 @@ namespace MediaBrowser.Dlna.Service { builder.Append("<action>"); - builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>"); + builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); builder.Append("<argumentList>"); @@ -43,9 +44,9 @@ namespace MediaBrowser.Dlna.Service { builder.Append("<argument>"); - builder.Append("<name>" + SecurityElement.Escape(argument.Name ?? string.Empty) + "</name>"); - builder.Append("<direction>" + SecurityElement.Escape(argument.Direction ?? string.Empty) + "</direction>"); - builder.Append("<relatedStateVariable>" + SecurityElement.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); + builder.Append("<name>" + DescriptionXmlBuilder.Escape(argument.Name ?? string.Empty) + "</name>"); + builder.Append("<direction>" + DescriptionXmlBuilder.Escape(argument.Direction ?? string.Empty) + "</direction>"); + builder.Append("<relatedStateVariable>" + DescriptionXmlBuilder.Escape(argument.RelatedStateVariable ?? string.Empty) + "</relatedStateVariable>"); builder.Append("</argument>"); } @@ -68,15 +69,15 @@ namespace MediaBrowser.Dlna.Service builder.Append("<stateVariable sendEvents=\"" + sendEvents + "\">"); - builder.Append("<name>" + SecurityElement.Escape(item.Name ?? string.Empty) + "</name>"); - builder.Append("<dataType>" + SecurityElement.Escape(item.DataType ?? string.Empty) + "</dataType>"); + builder.Append("<name>" + DescriptionXmlBuilder.Escape(item.Name ?? string.Empty) + "</name>"); + builder.Append("<dataType>" + DescriptionXmlBuilder.Escape(item.DataType ?? string.Empty) + "</dataType>"); if (item.AllowedValues.Count > 0) { builder.Append("<allowedValueList>"); foreach (var allowedValue in item.AllowedValues) { - builder.Append("<allowedValue>" + SecurityElement.Escape(allowedValue) + "</allowedValue>"); + builder.Append("<allowedValue>" + DescriptionXmlBuilder.Escape(allowedValue) + "</allowedValue>"); } builder.Append("</allowedValueList>"); } diff --git a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs b/Emby.Dlna/Ssdp/DeviceDiscovery.cs similarity index 84% rename from MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs rename to Emby.Dlna/Ssdp/DeviceDiscovery.cs index c9bba526a5..1cd19d010c 100644 --- a/MediaBrowser.Dlna/Ssdp/DeviceDiscovery.cs +++ b/Emby.Dlna/Ssdp/DeviceDiscovery.cs @@ -7,14 +7,17 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Threading; using Rssdp; +using Rssdp.Infrastructure; -namespace MediaBrowser.Dlna.Ssdp +namespace Emby.Dlna.Ssdp { public class DeviceDiscovery : IDeviceDiscovery, IDisposable { @@ -27,20 +30,25 @@ namespace MediaBrowser.Dlna.Ssdp public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceDiscovered; public event EventHandler<GenericEventArgs<UpnpDeviceInfo>> DeviceLeft; - private SsdpDeviceLocator _DeviceLocator; + private SsdpDeviceLocator _deviceLocator; - public DeviceDiscovery(ILogger logger, IServerConfigurationManager config) + private readonly ITimerFactory _timerFactory; + private readonly ISocketFactory _socketFactory; + + public DeviceDiscovery(ILogger logger, IServerConfigurationManager config, ISocketFactory socketFactory, ITimerFactory timerFactory) { _tokenSource = new CancellationTokenSource(); _logger = logger; _config = config; + _socketFactory = socketFactory; + _timerFactory = timerFactory; } // Call this method from somewhere in your code to start the search. - public void BeginSearch() + public void Start(ISsdpCommunicationsServer communicationsServer) { - _DeviceLocator = new SsdpDeviceLocator(); + _deviceLocator = new SsdpDeviceLocator(communicationsServer, _timerFactory); // (Optional) Set the filter so we only see notifications for devices we care about // (can be any search target value i.e device type, uuid value etc - any value that appears in the @@ -48,8 +56,8 @@ namespace MediaBrowser.Dlna.Ssdp //_DeviceLocator.NotificationFilter = "upnp:rootdevice"; // Connect our event handler so we process devices as they are found - _DeviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; - _DeviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; + _deviceLocator.DeviceAvailable += deviceLocator_DeviceAvailable; + _deviceLocator.DeviceUnavailable += _DeviceLocator_DeviceUnavailable; // Perform a search so we don't have to wait for devices to broadcast notifications // again to get any results right away (notifications are broadcast periodically). @@ -65,9 +73,9 @@ namespace MediaBrowser.Dlna.Ssdp try { // Enable listening for notifications (optional) - _DeviceLocator.StartListeningForNotifications(); + _deviceLocator.StartListeningForNotifications(); - await _DeviceLocator.SearchAsync().ConfigureAwait(false); + await _deviceLocator.SearchAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -123,11 +131,6 @@ namespace MediaBrowser.Dlna.Ssdp EventHelper.FireEventIfNotNull(DeviceLeft, this, args, _logger); } - public void Start() - { - BeginSearch(); - } - public void Dispose() { if (!_disposed) diff --git a/MediaBrowser.Dlna/Ssdp/Extensions.cs b/Emby.Dlna/Ssdp/Extensions.cs similarity index 93% rename from MediaBrowser.Dlna/Ssdp/Extensions.cs rename to Emby.Dlna/Ssdp/Extensions.cs index 17ebcc7ead..611bf7e022 100644 --- a/MediaBrowser.Dlna/Ssdp/Extensions.cs +++ b/Emby.Dlna/Ssdp/Extensions.cs @@ -1,11 +1,10 @@ using System; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Threading.Tasks; using System.Xml.Linq; -namespace MediaBrowser.Dlna.Ssdp +namespace Emby.Dlna.Ssdp { public static class Extensions { diff --git a/Emby.Dlna/project.json b/Emby.Dlna/project.json new file mode 100644 index 0000000000..fbbe9eaf32 --- /dev/null +++ b/Emby.Dlna/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/Mono.Nat/Mono.Nat.csproj b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj similarity index 50% rename from Mono.Nat/Mono.Nat.csproj rename to Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj index 9c27814339..98e99c92b1 100644 --- a/Mono.Nat/Mono.Nat.csproj +++ b/Emby.Drawing.ImageMagick/Emby.Drawing.ImageMagick.csproj @@ -4,14 +4,13 @@ <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{D7453B88-2266-4805-B39B-2B5A2A33E1BA}</ProjectGuid> + <ProjectGuid>{6CFEE013-6E7C-432B-AC37-CABF0880C69A}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> - <RootNamespace>Mono.Nat</RootNamespace> - <AssemblyName>Mono.Nat</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + <RootNamespace>Emby.Drawing.ImageMagick</RootNamespace> + <AssemblyName>Emby.Drawing.ImageMagick</AssemblyName> + <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> - <TargetFrameworkProfile /> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -31,6 +30,10 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> + <Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> + <HintPath>..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll</HintPath> + <Private>True</Private> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Xml.Linq" /> @@ -41,49 +44,25 @@ <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> - <Compile Include="..\SharedVersion.cs"> - <Link>Properties\SharedVersion.cs</Link> - </Compile> - <Compile Include="AbstractNatDevice.cs" /> - <Compile Include="AsyncResults\AsyncResult.cs" /> - <Compile Include="Enums\MapState.cs" /> - <Compile Include="Enums\ProtocolType.cs" /> - <Compile Include="EventArgs\DeviceEventArgs.cs" /> - <Compile Include="Exceptions\MappingException.cs" /> - <Compile Include="IMapper.cs" /> - <Compile Include="INatDevice.cs" /> - <Compile Include="ISearcher.cs" /> - <Compile Include="Mapping.cs" /> - <Compile Include="NatProtocol.cs" /> - <Compile Include="NatUtility.cs" /> - <Compile Include="Pmp\AsyncResults\PortMapAsyncResult.cs" /> - <Compile Include="Pmp\Mappers\PmpMapper.cs" /> - <Compile Include="Pmp\Pmp.cs" /> - <Compile Include="Pmp\PmpConstants.cs" /> - <Compile Include="Pmp\PmpNatDevice.cs" /> - <Compile Include="Pmp\Searchers\PmpSearcher.cs" /> + <Compile Include="ImageHelpers.cs" /> + <Compile Include="ImageMagickEncoder.cs" /> + <Compile Include="PercentPlayedDrawer.cs" /> + <Compile Include="PlayedIndicatorDrawer.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="Upnp\AsyncResults\GetAllMappingsAsyncResult.cs" /> - <Compile Include="Upnp\AsyncResults\PortMapAsyncResult.cs" /> - <Compile Include="Upnp\Mappers\UpnpMapper.cs" /> - <Compile Include="Upnp\Messages\DiscoverDeviceMessage.cs" /> - <Compile Include="Upnp\Messages\ErrorMessage.cs" /> - <Compile Include="Upnp\Messages\GetServicesMessage.cs" /> - <Compile Include="Upnp\Messages\Requests\CreatePortMappingMessage.cs" /> - <Compile Include="Upnp\Messages\Requests\DeletePortMappingMessage.cs" /> - <Compile Include="Upnp\Messages\Requests\GetExternalIPAddressMessage.cs" /> - <Compile Include="Upnp\Messages\Requests\GetGenericPortMappingEntry.cs" /> - <Compile Include="Upnp\Messages\Requests\GetSpecificPortMappingEntryMessage.cs" /> - <Compile Include="Upnp\Messages\Responses\CreatePortMappingResponseMessage.cs" /> - <Compile Include="Upnp\Messages\Responses\DeletePortMappingResponseMessage.cs" /> - <Compile Include="Upnp\Messages\Responses\GetExternalIPAddressResponseMessage.cs" /> - <Compile Include="Upnp\Messages\Responses\GetGenericPortMappingEntryResponseMessage.cs" /> - <Compile Include="Upnp\Messages\UpnpMessage.cs" /> - <Compile Include="Upnp\Searchers\UpnpSearcher.cs" /> - <Compile Include="Upnp\Upnp.cs" /> - <Compile Include="Upnp\UpnpNatDevice.cs" /> + <Compile Include="StripCollageBuilder.cs" /> + <Compile Include="UnplayedCountIndicator.cs" /> </ItemGroup> <ItemGroup> + <EmbeddedResource Include="fonts\robotoregular.ttf" /> + </ItemGroup> + <ItemGroup> + <None Include="packages.config" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> + <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> + <Name>MediaBrowser.Common</Name> + </ProjectReference> <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> <Name>MediaBrowser.Controller</Name> diff --git a/Emby.Drawing.ImageMagick/ImageHelpers.cs b/Emby.Drawing.ImageMagick/ImageHelpers.cs new file mode 100644 index 0000000000..c623c21aa2 --- /dev/null +++ b/Emby.Drawing.ImageMagick/ImageHelpers.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Emby.Drawing.ImageMagick +{ + internal static class ImageHelpers + { + internal static List<string> ProjectPaths(List<string> paths, int count) + { + if (count <= 0) + { + throw new ArgumentOutOfRangeException("count"); + } + if (paths.Count == 0) + { + throw new ArgumentOutOfRangeException("paths"); + } + + var list = new List<string>(); + + AddToList(list, paths, count); + + return list.Take(count).ToList(); + } + + private static void AddToList(List<string> list, List<string> paths, int count) + { + while (list.Count < count) + { + foreach (var path in paths) + { + list.Add(path); + + if (list.Count >= count) + { + return; + } + } + } + } + } +} diff --git a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs similarity index 93% rename from Emby.Drawing/ImageMagick/ImageMagickEncoder.cs rename to Emby.Drawing.ImageMagick/ImageMagickEncoder.cs index 9820829ae6..39088c94b3 100644 --- a/Emby.Drawing/ImageMagick/ImageMagickEncoder.cs +++ b/Emby.Drawing.ImageMagick/ImageMagickEncoder.cs @@ -8,8 +8,7 @@ using MediaBrowser.Model.Logging; using System; using System.IO; using System.Linq; -using CommonIO; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick { @@ -17,17 +16,15 @@ namespace Emby.Drawing.ImageMagick { private readonly ILogger _logger; private readonly IApplicationPaths _appPaths; - private readonly IHttpClient _httpClient; + private readonly Func<IHttpClient> _httpClientFactory; private readonly IFileSystem _fileSystem; - private readonly IServerConfigurationManager _config; - public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config) + public ImageMagickEncoder(ILogger logger, IApplicationPaths appPaths, Func<IHttpClient> httpClientFactory, IFileSystem fileSystem) { _logger = logger; _appPaths = appPaths; - _httpClient = httpClient; + _httpClientFactory = httpClientFactory; _fileSystem = fileSystem; - _config = config; LogVersion(); } @@ -151,7 +148,7 @@ namespace Emby.Drawing.ImageMagick { using (var originalImage = new MagickWand(inputPath)) { - ScaleImage(originalImage, width, height); + ScaleImage(originalImage, width, height, options.Blur ?? 0); if (autoOrient) { @@ -173,7 +170,7 @@ namespace Emby.Drawing.ImageMagick { using (var originalImage = new MagickWand(inputPath)) { - ScaleImage(originalImage, width, height); + ScaleImage(originalImage, width, height, options.Blur ?? 0); if (autoOrient) { @@ -224,13 +221,13 @@ namespace Emby.Drawing.ImageMagick } } - private void ScaleImage(MagickWand wand, int width, int height) + private void ScaleImage(MagickWand wand, int width, int height, int blur) { - var highQuality = false; + var useResize = blur > 1; - if (highQuality) + if (useResize) { - wand.CurrentImage.ResizeImage(width, height); + wand.CurrentImage.ResizeImage(width, height, FilterTypes.GaussianFilter, blur); } else { @@ -258,7 +255,7 @@ namespace Emby.Drawing.ImageMagick { var currentImageSize = new ImageSize(imageWidth, imageHeight); - var task = new PlayedIndicatorDrawer(_appPaths, _httpClient, _fileSystem).DrawPlayedIndicator(wand, currentImageSize); + var task = new PlayedIndicatorDrawer(_appPaths, _httpClientFactory(), _fileSystem).DrawPlayedIndicator(wand, currentImageSize); Task.WaitAll(task); } else if (options.UnplayedCount.HasValue) diff --git a/Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs b/Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs similarity index 100% rename from Emby.Drawing/ImageMagick/PercentPlayedDrawer.cs rename to Emby.Drawing.ImageMagick/PercentPlayedDrawer.cs diff --git a/Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs b/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs similarity index 97% rename from Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs rename to Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs index d838410b9f..14fb0ddf15 100644 --- a/Emby.Drawing/ImageMagick/PlayedIndicatorDrawer.cs +++ b/Emby.Drawing.ImageMagick/PlayedIndicatorDrawer.cs @@ -5,7 +5,9 @@ using MediaBrowser.Model.Drawing; using System; using System.IO; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick { diff --git a/MediaBrowser.Model.net35/Properties/AssemblyInfo.cs b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs similarity index 64% rename from MediaBrowser.Model.net35/Properties/AssemblyInfo.cs rename to Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs index 838cccf662..1089607d67 100644 --- a/MediaBrowser.Model.net35/Properties/AssemblyInfo.cs +++ b/Emby.Drawing.ImageMagick/Properties/AssemblyInfo.cs @@ -1,15 +1,16 @@ using System.Reflection; +using System.Runtime.CompilerServices; 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("MediaBrowser.Model.net35")] +[assembly: AssemblyTitle("Emby.Drawing.ImageMagick")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MediaBrowser.Model.net35")] -[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyProduct("Emby.Drawing.ImageMagick")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -19,7 +20,7 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("801b3f80-cddc-4a3a-986b-3e7f0293da4b")] +[assembly: Guid("6cfee013-6e7c-432b-ac37-cabf0880c69a")] // Version information for an assembly consists of the following four values: // @@ -27,4 +28,9 @@ using System.Runtime.InteropServices; // Minor Version // Build Number // Revision -// \ No newline at end of file +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.Drawing/ImageMagick/StripCollageBuilder.cs b/Emby.Drawing.ImageMagick/StripCollageBuilder.cs similarity index 99% rename from Emby.Drawing/ImageMagick/StripCollageBuilder.cs rename to Emby.Drawing.ImageMagick/StripCollageBuilder.cs index 7bc144c11d..715ab46800 100644 --- a/Emby.Drawing/ImageMagick/StripCollageBuilder.cs +++ b/Emby.Drawing.ImageMagick/StripCollageBuilder.cs @@ -2,7 +2,9 @@ using MediaBrowser.Common.Configuration; using System; using System.Collections.Generic; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick { diff --git a/Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs b/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs similarity index 96% rename from Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs rename to Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs index 1d35b52c49..c531400992 100644 --- a/Emby.Drawing/ImageMagick/UnplayedCountIndicator.cs +++ b/Emby.Drawing.ImageMagick/UnplayedCountIndicator.cs @@ -2,7 +2,9 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Drawing; using System.Globalization; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; namespace Emby.Drawing.ImageMagick { diff --git a/Emby.Drawing.ImageMagick/packages.config b/Emby.Drawing.ImageMagick/packages.config new file mode 100644 index 0000000000..619310d28e --- /dev/null +++ b/Emby.Drawing.ImageMagick/packages.config @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<packages> + <package id="ImageMagickSharp" version="1.0.0.18" targetFramework="net452" /> +</packages> \ No newline at end of file diff --git a/Emby.Drawing/GDI/DynamicImageHelpers.cs b/Emby.Drawing.Net/DynamicImageHelpers.cs similarity index 96% rename from Emby.Drawing/GDI/DynamicImageHelpers.cs rename to Emby.Drawing.Net/DynamicImageHelpers.cs index 59340af8a9..1910f7840d 100644 --- a/Emby.Drawing/GDI/DynamicImageHelpers.cs +++ b/Emby.Drawing.Net/DynamicImageHelpers.cs @@ -3,9 +3,11 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public static class DynamicImageHelpers { diff --git a/Emby.Drawing.Net/Emby.Drawing.Net.csproj b/Emby.Drawing.Net/Emby.Drawing.Net.csproj new file mode 100644 index 0000000000..16e72a085b --- /dev/null +++ b/Emby.Drawing.Net/Emby.Drawing.Net.csproj @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{C97A239E-A96C-4D64-A844-CCF8CC30AECB}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Emby.Drawing.Net</RootNamespace> + <AssemblyName>Emby.Drawing.Net</AssemblyName> + <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> + <FileAlignment>512</FileAlignment> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="System" /> + <Reference Include="System.Core" /> + <Reference Include="System.Drawing" /> + <Reference Include="System.Xml.Linq" /> + <Reference Include="System.Data.DataSetExtensions" /> + <Reference Include="Microsoft.CSharp" /> + <Reference Include="System.Data" /> + <Reference Include="System.Net.Http" /> + <Reference Include="System.Xml" /> + </ItemGroup> + <ItemGroup> + <Compile Include="DynamicImageHelpers.cs" /> + <Compile Include="GDIImageEncoder.cs" /> + <Compile Include="ImageExtensions.cs" /> + <Compile Include="ImageHelpers.cs" /> + <Compile Include="PercentPlayedDrawer.cs" /> + <Compile Include="PlayedIndicatorDrawer.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="UnplayedCountIndicator.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> + <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> + <Name>MediaBrowser.Common</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> + <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> + <Name>MediaBrowser.Controller</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> + <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> + <Name>MediaBrowser.Model</Name> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <EmbeddedResource Include="empty.png" /> + </ItemGroup> + <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/Emby.Drawing/GDI/GDIImageEncoder.cs b/Emby.Drawing.Net/GDIImageEncoder.cs similarity index 96% rename from Emby.Drawing/GDI/GDIImageEncoder.cs rename to Emby.Drawing.Net/GDIImageEncoder.cs index afd16899dc..831a579792 100644 --- a/Emby.Drawing/GDI/GDIImageEncoder.cs +++ b/Emby.Drawing.Net/GDIImageEncoder.cs @@ -7,10 +7,12 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; using ImageFormat = MediaBrowser.Model.Drawing.ImageFormat; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class GDIImageEncoder : IImageEncoder { @@ -81,7 +83,7 @@ namespace Emby.Drawing.GDI { _fileSystem.CreateDirectory(Path.GetDirectoryName(outputPath)); - using (var outputStream = _fileSystem.GetFileStream(outputPath, FileMode.Create, FileAccess.Write, FileShare.Read, false)) + using (var outputStream = _fileSystem.GetFileStream(outputPath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false)) { croppedImage.Save(System.Drawing.Imaging.ImageFormat.Png, outputStream, 100); } @@ -136,7 +138,7 @@ namespace Emby.Drawing.GDI _fileSystem.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); // Save to the cache location - using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, false)) + using (var cacheFileStream = _fileSystem.GetFileStream(cacheFilePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, false)) { // Save to the memory stream thumbnail.Save(outputFormat, cacheFileStream, quality); diff --git a/Emby.Drawing/GDI/ImageExtensions.cs b/Emby.Drawing.Net/ImageExtensions.cs similarity index 99% rename from Emby.Drawing/GDI/ImageExtensions.cs rename to Emby.Drawing.Net/ImageExtensions.cs index 6af5a8688f..dec2613d0f 100644 --- a/Emby.Drawing/GDI/ImageExtensions.cs +++ b/Emby.Drawing.Net/ImageExtensions.cs @@ -4,7 +4,7 @@ using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public static class ImageExtensions { diff --git a/Emby.Drawing/ImageHelpers.cs b/Emby.Drawing.Net/ImageHelpers.cs similarity index 97% rename from Emby.Drawing/ImageHelpers.cs rename to Emby.Drawing.Net/ImageHelpers.cs index 90bde8b3bd..1afc47cd03 100644 --- a/Emby.Drawing/ImageHelpers.cs +++ b/Emby.Drawing.Net/ImageHelpers.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; -namespace Emby.Drawing +namespace Emby.Drawing.Net { internal static class ImageHelpers { diff --git a/Emby.Drawing/GDI/PercentPlayedDrawer.cs b/Emby.Drawing.Net/PercentPlayedDrawer.cs similarity index 97% rename from Emby.Drawing/GDI/PercentPlayedDrawer.cs rename to Emby.Drawing.Net/PercentPlayedDrawer.cs index c7afda60e4..fac15ba47a 100644 --- a/Emby.Drawing/GDI/PercentPlayedDrawer.cs +++ b/Emby.Drawing.Net/PercentPlayedDrawer.cs @@ -1,7 +1,7 @@ using System; using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class PercentPlayedDrawer { diff --git a/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs similarity index 97% rename from Emby.Drawing/GDI/PlayedIndicatorDrawer.cs rename to Emby.Drawing.Net/PlayedIndicatorDrawer.cs index 4428e4cbac..53683e6f45 100644 --- a/Emby.Drawing/GDI/PlayedIndicatorDrawer.cs +++ b/Emby.Drawing.Net/PlayedIndicatorDrawer.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class PlayedIndicatorDrawer { diff --git a/MediaBrowser.Dlna/Properties/AssemblyInfo.cs b/Emby.Drawing.Net/Properties/AssemblyInfo.cs similarity index 65% rename from MediaBrowser.Dlna/Properties/AssemblyInfo.cs rename to Emby.Drawing.Net/Properties/AssemblyInfo.cs index a8403e6a5e..321c3a297c 100644 --- a/MediaBrowser.Dlna/Properties/AssemblyInfo.cs +++ b/Emby.Drawing.Net/Properties/AssemblyInfo.cs @@ -1,15 +1,16 @@ using System.Reflection; +using System.Runtime.CompilerServices; 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("MediaBrowser.Dlna")] +[assembly: AssemblyTitle("Emby.Drawing.Net")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MediaBrowser.Dlna")] -[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyProduct("Emby.Drawing.Net")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -19,7 +20,7 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("c319ebfa-fd9d-42e4-ae74-a40039a6a688")] +[assembly: Guid("c97a239e-a96c-4d64-a844-ccf8cc30aecb")] // Version information for an assembly consists of the following four values: // @@ -27,4 +28,9 @@ using System.Runtime.InteropServices; // Minor Version // Build Number // Revision -// \ No newline at end of file +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Emby.Drawing/GDI/UnplayedCountIndicator.cs b/Emby.Drawing.Net/UnplayedCountIndicator.cs similarity index 98% rename from Emby.Drawing/GDI/UnplayedCountIndicator.cs rename to Emby.Drawing.Net/UnplayedCountIndicator.cs index 6420abb27f..a38abeb324 100644 --- a/Emby.Drawing/GDI/UnplayedCountIndicator.cs +++ b/Emby.Drawing.Net/UnplayedCountIndicator.cs @@ -1,6 +1,6 @@ using System.Drawing; -namespace Emby.Drawing.GDI +namespace Emby.Drawing.Net { public class UnplayedCountIndicator { diff --git a/Emby.Drawing/GDI/empty.png b/Emby.Drawing.Net/empty.png similarity index 100% rename from Emby.Drawing/GDI/empty.png rename to Emby.Drawing.Net/empty.png diff --git a/Emby.Drawing/Common/ImageHeader.cs b/Emby.Drawing/Common/ImageHeader.cs index b5c672a56d..c385779a1e 100644 --- a/Emby.Drawing/Common/ImageHeader.cs +++ b/Emby.Drawing/Common/ImageHeader.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; namespace Emby.Drawing.Common { @@ -46,7 +48,7 @@ namespace Emby.Drawing.Common /// <exception cref="ArgumentException">The image was of an unrecognised format.</exception> public static ImageSize GetDimensions(string path, ILogger logger, IFileSystem fileSystem) { - using (var fs = File.OpenRead(path)) + using (var fs = fileSystem.OpenRead(path)) { using (var binaryReader = new BinaryReader(fs)) { diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index aa7dc5a153..90418f6317 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -9,6 +9,8 @@ <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>Emby.Drawing</RootNamespace> <AssemblyName>Emby.Drawing</AssemblyName> + <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <TargetFrameworkProfile>Profile7</TargetFrameworkProfile> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> <SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir> @@ -31,30 +33,8 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> - <Reference Include="CommonIO, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\CommonIO.1.0.0.9\lib\net45\CommonIO.dll</HintPath> - </Reference> - <Reference Include="ImageMagickSharp, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL"> - <SpecificVersion>False</SpecificVersion> - <HintPath>..\packages\ImageMagickSharp.1.0.0.18\lib\net45\ImageMagickSharp.dll</HintPath> - </Reference> - <Reference Include="Patterns.Logging"> - <HintPath>..\packages\Patterns.Logging.1.0.0.2\lib\portable-net45+sl4+wp71+win8+wpa81\Patterns.Logging.dll</HintPath> - </Reference> - <Reference Include="policy.2.0.taglib-sharp"> - <HintPath>..\packages\taglib.2.1.0.0\lib\policy.2.0.taglib-sharp.dll</HintPath> - </Reference> - <Reference Include="System" /> - <Reference Include="System.Core" /> - <Reference Include="System.Drawing" /> - <Reference Include="System.Xml.Linq" /> - <Reference Include="System.Data.DataSetExtensions" /> - <Reference Include="Microsoft.CSharp" /> - <Reference Include="System.Data" /> - <Reference Include="System.Xml" /> - <Reference Include="taglib-sharp"> - <HintPath>..\packages\taglib.2.1.0.0\lib\taglib-sharp.dll</HintPath> + <Reference Include="TagLib.Portable"> + <HintPath>..\ThirdParty\taglib\TagLib.Portable.dll</HintPath> </Reference> </ItemGroup> <ItemGroup> @@ -62,25 +42,9 @@ <Link>Properties\SharedVersion.cs</Link> </Compile> <Compile Include="Common\ImageHeader.cs" /> - <Compile Include="GDI\DynamicImageHelpers.cs" /> - <Compile Include="GDI\GDIImageEncoder.cs" /> - <Compile Include="GDI\ImageExtensions.cs" /> - <Compile Include="GDI\PercentPlayedDrawer.cs" /> - <Compile Include="GDI\PlayedIndicatorDrawer.cs" /> - <Compile Include="GDI\UnplayedCountIndicator.cs" /> - <Compile Include="IImageEncoder.cs" /> - <Compile Include="ImageHelpers.cs" /> - <Compile Include="ImageMagick\ImageMagickEncoder.cs" /> - <Compile Include="ImageMagick\StripCollageBuilder.cs" /> <Compile Include="ImageProcessor.cs" /> - <Compile Include="ImageMagick\PercentPlayedDrawer.cs" /> - <Compile Include="ImageMagick\PlayedIndicatorDrawer.cs" /> <Compile Include="NullImageEncoder.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> - <Compile Include="ImageMagick\UnplayedCountIndicator.cs" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="ImageMagick\fonts\robotoregular.ttf" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> @@ -96,13 +60,8 @@ <Name>MediaBrowser.Model</Name> </ProjectReference> </ItemGroup> - <ItemGroup> - <None Include="packages.config" /> - </ItemGroup> - <ItemGroup> - <EmbeddedResource Include="GDI\empty.png" /> - </ItemGroup> - <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> + <ItemGroup /> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. <Target Name="BeforeBuild"> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index e9f8f81f31..a15f75c9ae 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -15,10 +15,14 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using Emby.Drawing.Common; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Net; +using MediaBrowser.Model.Threading; +using TagLib; namespace Emby.Drawing { @@ -61,7 +65,7 @@ namespace Emby.Drawing IFileSystem fileSystem, IJsonSerializer jsonSerializer, IImageEncoder imageEncoder, - int maxConcurrentImageProcesses, Func<ILibraryManager> libraryManager) + int maxConcurrentImageProcesses, Func<ILibraryManager> libraryManager, ITimerFactory timerFactory) { _logger = logger; _fileSystem = fileSystem; @@ -71,7 +75,7 @@ namespace Emby.Drawing _appPaths = appPaths; ImageEnhancers = new List<IImageEnhancer>(); - _saveImageSizeTimer = new Timer(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); + _saveImageSizeTimer = timerFactory.Create(SaveImageSizeCallback, null, Timeout.Infinite, Timeout.Infinite); Dictionary<Guid, ImageSize> sizeDictionary; @@ -85,7 +89,7 @@ namespace Emby.Drawing // No biggie sizeDictionary = new Dictionary<Guid, ImageSize>(); } - catch (DirectoryNotFoundException) + catch (IOException) { // No biggie sizeDictionary = new Dictionary<Guid, ImageSize>(); @@ -152,7 +156,7 @@ namespace Emby.Drawing { var file = await ProcessImage(options).ConfigureAwait(false); - using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileMode.Open, FileAccess.Read, FileShare.Read, true)) + using (var fileStream = _fileSystem.GetFileStream(file.Item1, FileOpenMode.Open, FileAccessMode.Read, FileShareMode.Read, true)) { await fileStream.CopyToAsync(toStream).ConfigureAwait(false); } @@ -232,7 +236,7 @@ namespace Emby.Drawing var quality = options.Quality; var outputFormat = GetOutputFormat(options.SupportedOutputFormats[0]); - var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.BackgroundColor, options.ForegroundLayer); + var cacheFilePath = GetCacheFilePath(originalImagePath, newSize, quality, dateModified, outputFormat, options.AddPlayedIndicator, options.PercentPlayed, options.UnplayedCount, options.Blur, options.BackgroundColor, options.ForegroundLayer); var imageProcessingLockTaken = false; @@ -282,7 +286,7 @@ namespace Emby.Drawing { try { - File.Copy(src, destination, true); + _fileSystem.CopyFile(src, destination, true); } catch { @@ -428,17 +432,12 @@ namespace Emby.Drawing return GetResult(croppedImagePath); } - var imageProcessingLockTaken = false; - try { _fileSystem.CreateDirectory(Path.GetDirectoryName(croppedImagePath)); var tmpPath = Path.ChangeExtension(Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N")), Path.GetExtension(croppedImagePath)); _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); - await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); - imageProcessingLockTaken = true; - _imageEncoder.CropWhiteSpace(originalImagePath, tmpPath); CopyFile(tmpPath, croppedImagePath); return GetResult(tmpPath); @@ -455,13 +454,6 @@ namespace Emby.Drawing return new Tuple<string, DateTime>(originalImagePath, dateModified); } - finally - { - if (imageProcessingLockTaken) - { - _imageProcessingSemaphore.Release(); - } - } } private Tuple<string, DateTime> GetResult(string path) @@ -477,7 +469,7 @@ namespace Emby.Drawing /// <summary> /// Gets the cache file path based on a set of parameters /// </summary> - private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, string backgroundColor, string foregroundLayer) + private string GetCacheFilePath(string originalPath, ImageSize outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) { var filename = originalPath; @@ -506,6 +498,11 @@ namespace Emby.Drawing filename += "p=" + unwatchedCount.Value; } + if (blur.HasValue) + { + filename += "blur=" + blur.Value; + } + if (!string.IsNullOrEmpty(backgroundColor)) { filename += "b=" + backgroundColor; @@ -576,7 +573,7 @@ namespace Emby.Drawing { try { - using (var file = TagLib.File.Create(path)) + using (var file = TagLib.File.Create(new StreamFileAbstraction(Path.GetFileName(path), _fileSystem.OpenRead(path), null))) { var image = file as TagLib.Image.File; @@ -596,7 +593,7 @@ namespace Emby.Drawing return ImageHeader.GetDimensions(path, _logger, _fileSystem); } - private readonly Timer _saveImageSizeTimer; + private readonly ITimer _saveImageSizeTimer; private const int SaveImageSizeTimeout = 5000; private readonly object _saveImageSizeLock = new object(); private void StartSaveImageSizeTimer() @@ -778,40 +775,38 @@ namespace Emby.Drawing // All enhanced images are saved as png to allow transparency var enhancedImagePath = GetCachePath(EnhancedImageCachePath, cacheGuid + ".png"); - var semaphore = GetLock(enhancedImagePath); - - await semaphore.WaitAsync().ConfigureAwait(false); - // Check again in case of contention if (_fileSystem.FileExists(enhancedImagePath)) { - semaphore.Release(); return enhancedImagePath; } - var imageProcessingLockTaken = false; + _fileSystem.CreateDirectory(Path.GetDirectoryName(enhancedImagePath)); + + var tmpPath = Path.Combine(_appPaths.TempDirectory, Path.ChangeExtension(Guid.NewGuid().ToString(), Path.GetExtension(enhancedImagePath))); + _fileSystem.CreateDirectory(Path.GetDirectoryName(tmpPath)); + + await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); try { - _fileSystem.CreateDirectory(Path.GetDirectoryName(enhancedImagePath)); + await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, tmpPath, item, imageType, imageIndex).ConfigureAwait(false); - await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); - - imageProcessingLockTaken = true; - - await ExecuteImageEnhancers(supportedEnhancers, originalImagePath, enhancedImagePath, item, imageType, imageIndex).ConfigureAwait(false); + try + { + _fileSystem.CopyFile(tmpPath, enhancedImagePath, true); + } + catch + { + + } } finally { - if (imageProcessingLockTaken) - { - _imageProcessingSemaphore.Release(); - } - - semaphore.Release(); + _imageProcessingSemaphore.Release(); } - return enhancedImagePath; + return tmpPath; } /// <summary> @@ -836,21 +831,6 @@ namespace Emby.Drawing } } - /// <summary> - /// The _semaphoreLocks - /// </summary> - private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = new ConcurrentDictionary<string, SemaphoreSlim>(); - - /// <summary> - /// Gets the lock. - /// </summary> - /// <param name="filename">The filename.</param> - /// <returns>System.Object.</returns> - private SemaphoreSlim GetLock(string filename) - { - return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); - } - /// <summary> /// Gets the cache path. /// </summary> @@ -917,20 +897,11 @@ namespace Emby.Drawing public async Task CreateImageCollage(ImageCollageOptions options) { - await _imageProcessingSemaphore.WaitAsync().ConfigureAwait(false); + _logger.Info("Creating image collage and saving to {0}", options.OutputPath); - try - { - _logger.Info("Creating image collage and saving to {0}", options.OutputPath); + _imageEncoder.CreateImageCollage(options); - _imageEncoder.CreateImageCollage(options); - - _logger.Info("Completed creation of image collage and saved to {0}", options.OutputPath); - } - finally - { - _imageProcessingSemaphore.Release(); - } + _logger.Info("Completed creation of image collage and saved to {0}", options.OutputPath); } public IEnumerable<IImageEnhancer> GetSupportedEnhancers(IHasImages item, ImageType imageType) diff --git a/Emby.Drawing/packages.config b/Emby.Drawing/packages.config deleted file mode 100644 index af298b3e10..0000000000 --- a/Emby.Drawing/packages.config +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<packages> - <package id="CommonIO" version="1.0.0.9" targetFramework="net45" /> - <package id="ImageMagickSharp" version="1.0.0.18" targetFramework="net45" /> - <package id="Patterns.Logging" version="1.0.0.2" targetFramework="net45" /> -</packages> \ No newline at end of file diff --git a/Emby.Drawing/project.json b/Emby.Drawing/project.json new file mode 100644 index 0000000000..fbbe9eaf32 --- /dev/null +++ b/Emby.Drawing/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj new file mode 100644 index 0000000000..21631aeb54 --- /dev/null +++ b/Emby.Photos/Emby.Photos.csproj @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> + <ProjectGuid>{89AB4548-770D-41FD-A891-8DAFF44F452C}</ProjectGuid> + <OutputType>Library</OutputType> + <AppDesignerFolder>Properties</AppDesignerFolder> + <RootNamespace>Emby.Photos</RootNamespace> + <AssemblyName>Emby.Photos</AssemblyName> + <FileAlignment>512</FileAlignment> + <ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> + <TargetFrameworkProfile>Profile7</TargetFrameworkProfile> + <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> + <DebugSymbols>true</DebugSymbols> + <DebugType>full</DebugType> + <Optimize>false</Optimize> + <OutputPath>bin\Debug\</OutputPath> + <DefineConstants>DEBUG;TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> + <DebugType>pdbonly</DebugType> + <Optimize>true</Optimize> + <OutputPath>bin\Release\</OutputPath> + <DefineConstants>TRACE</DefineConstants> + <ErrorReport>prompt</ErrorReport> + <WarningLevel>4</WarningLevel> + </PropertyGroup> + <ItemGroup> + <Reference Include="TagLib.Portable"> + <HintPath>..\ThirdParty\taglib\TagLib.Portable.dll</HintPath> + </Reference> + </ItemGroup> + <ItemGroup> + <Compile Include="..\SharedVersion.cs"> + <Link>Properties\SharedVersion.cs</Link> + </Compile> + <Compile Include="PhotoProvider.cs" /> + <Compile Include="Properties\AssemblyInfo.cs" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj"> + <Project>{9142eefa-7570-41e1-bfcc-468bb571af2f}</Project> + <Name>MediaBrowser.Common</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj"> + <Project>{17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2}</Project> + <Name>MediaBrowser.Controller</Name> + </ProjectReference> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj"> + <Project>{7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b}</Project> + <Name>MediaBrowser.Model</Name> + </ProjectReference> + </ItemGroup> + <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" /> + <!-- To modify your build process, add your task inside one of the targets below and uncomment it. + Other similar extension points exist, see Microsoft.Common.targets. + <Target Name="BeforeBuild"> + </Target> + <Target Name="AfterBuild"> + </Target> + --> +</Project> \ No newline at end of file diff --git a/Emby.Photos/Emby.Photos.nuget.targets b/Emby.Photos/Emby.Photos.nuget.targets new file mode 100644 index 0000000000..e69ce0e64f --- /dev/null +++ b/Emby.Photos/Emby.Photos.nuget.targets @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8" standalone="no"?> +<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <Target Name="EmitMSBuildWarning" BeforeTargets="Build"> + <Warning Text="Packages containing MSBuild targets and props files cannot be fully installed in projects targeting multiple frameworks. The MSBuild targets and props files have been ignored." /> + </Target> +</Project> \ No newline at end of file diff --git a/MediaBrowser.Providers/Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs similarity index 92% rename from MediaBrowser.Providers/Photos/PhotoProvider.cs rename to Emby.Photos/PhotoProvider.cs index c48c3d09be..006f29c2fa 100644 --- a/MediaBrowser.Providers/Photos/PhotoProvider.cs +++ b/Emby.Photos/PhotoProvider.cs @@ -1,26 +1,30 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using System; +using System; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; using TagLib; using TagLib.IFD; using TagLib.IFD.Entries; using TagLib.IFD.Tags; -namespace MediaBrowser.Providers.Photos +namespace Emby.Photos { public class PhotoProvider : ICustomMetadataProvider<Photo>, IHasItemChangeMonitor, IForcedProvider { private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; - public PhotoProvider(ILogger logger) + public PhotoProvider(ILogger logger, IFileSystem fileSystem) { _logger = logger; + _fileSystem = fileSystem; } public Task<ItemUpdateType> FetchAsync(Photo item, MetadataRefreshOptions options, CancellationToken cancellationToken) @@ -31,7 +35,7 @@ namespace MediaBrowser.Providers.Photos try { - using (var file = TagLib.File.Create(item.Path)) + using (var file = TagLib.File.Create(new StreamFileAbstraction(Path.GetFileName(item.Path), _fileSystem.OpenRead(item.Path), null))) { var image = file as TagLib.Image.File; @@ -114,7 +118,7 @@ namespace MediaBrowser.Providers.Photos } else { - Model.Drawing.ImageOrientation orientation; + MediaBrowser.Model.Drawing.ImageOrientation orientation; if (Enum.TryParse(image.ImageTag.Orientation.ToString(), true, out orientation)) { item.Orientation = orientation; diff --git a/MediaBrowser.Common.Implementations/Properties/AssemblyInfo.cs b/Emby.Photos/Properties/AssemblyInfo.cs similarity index 70% rename from MediaBrowser.Common.Implementations/Properties/AssemblyInfo.cs rename to Emby.Photos/Properties/AssemblyInfo.cs index 63a83e0034..49ac833457 100644 --- a/MediaBrowser.Common.Implementations/Properties/AssemblyInfo.cs +++ b/Emby.Photos/Properties/AssemblyInfo.cs @@ -1,15 +1,16 @@ using System.Reflection; +using System.Runtime.CompilerServices; 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("MediaBrowser.Common.Implementations")] +[assembly: AssemblyTitle("Emby.Photos")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("MediaBrowser.Common.Implementations")] -[assembly: AssemblyCopyright("Copyright © 2013")] +[assembly: AssemblyProduct("Emby.Photos")] +[assembly: AssemblyCopyright("Copyright © 2016")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] @@ -19,7 +20,7 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("fc7d85c6-0fe7-4db6-8158-54f7b18f17cd")] +[assembly: Guid("89ab4548-770d-41fd-a891-8daff44f452c")] // Version information for an assembly consists of the following four values: // @@ -28,3 +29,6 @@ using System.Runtime.InteropServices; // Build Number // Revision // +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] \ No newline at end of file diff --git a/Emby.Photos/project.json b/Emby.Photos/project.json new file mode 100644 index 0000000000..fbbe9eaf32 --- /dev/null +++ b/Emby.Photos/project.json @@ -0,0 +1,17 @@ +{ + "frameworks":{ + "netstandard1.6":{ + "dependencies":{ + "NETStandard.Library":"1.6.0", + } + }, + ".NETPortable,Version=v4.5,Profile=Profile7":{ + "buildOptions": { + "define": [ ] + }, + "frameworkAssemblies":{ + + } + } + } +} \ No newline at end of file diff --git a/Emby.Server.Core/ApplicationHost.cs b/Emby.Server.Core/ApplicationHost.cs new file mode 100644 index 0000000000..a0a7416e7b --- /dev/null +++ b/Emby.Server.Core/ApplicationHost.cs @@ -0,0 +1,1661 @@ +using MediaBrowser.Api; +using MediaBrowser.Common; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Events; +using MediaBrowser.Common.Extensions; +using Emby.Common.Implementations.ScheduledTasks; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Progress; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Connect; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Controller.Dlna; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Notifications; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Controller.Playlists; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Resolvers; +using MediaBrowser.Controller.Security; +using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Controller.Subtitles; +using MediaBrowser.Controller.Sync; +using MediaBrowser.Controller.TV; +using MediaBrowser.LocalMetadata.Savers; +using MediaBrowser.MediaEncoding.BdInfo; +using MediaBrowser.MediaEncoding.Encoder; +using MediaBrowser.MediaEncoding.Subtitles; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.MediaInfo; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Updates; +using MediaBrowser.Providers.Chapters; +using MediaBrowser.Providers.Manager; +using MediaBrowser.Providers.Subtitles; +using MediaBrowser.WebDashboard.Api; +using MediaBrowser.XbmcMetadata.Providers; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Emby.Common.Implementations; +using Emby.Common.Implementations.Archiving; +using Emby.Common.Implementations.Networking; +using Emby.Common.Implementations.Reflection; +using Emby.Common.Implementations.Serialization; +using Emby.Common.Implementations.TextEncoding; +using Emby.Common.Implementations.Xml; +using Emby.Photos; +using MediaBrowser.Model.IO; +using MediaBrowser.Api.Playback; +using MediaBrowser.Common.Plugins; +using MediaBrowser.Common.Security; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using Emby.Dlna; +using Emby.Dlna.ConnectionManager; +using Emby.Dlna.ContentDirectory; +using Emby.Dlna.Main; +using Emby.Dlna.MediaReceiverRegistrar; +using Emby.Dlna.Ssdp; +using Emby.Server.Core; +using Emby.Server.Implementations.Activity; +using Emby.Server.Core.Configuration; +using Emby.Server.Implementations.Devices; +using Emby.Server.Implementations.FFMpeg; +using Emby.Server.Core.IO; +using Emby.Server.Core.Localization; +using Emby.Server.Implementations.Migrations; +using Emby.Server.Implementations.Security; +using Emby.Server.Implementations.Social; +using Emby.Server.Implementations.Sync; +using Emby.Server.Implementations.Channels; +using Emby.Server.Implementations.Collections; +using Emby.Server.Implementations.Connect; +using Emby.Server.Implementations.Dto; +using Emby.Server.Implementations.EntryPoints; +using Emby.Server.Implementations.FileOrganization; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.HttpServer.Security; +using Emby.Server.Implementations.Library; +using Emby.Server.Implementations.LiveTv; +using Emby.Server.Implementations.Localization; +using Emby.Server.Implementations.MediaEncoder; +using Emby.Server.Implementations.Notifications; +using Emby.Server.Implementations.Data; +using Emby.Server.Implementations.Playlists; +using Emby.Server.Implementations; +using Emby.Server.Implementations.ServerManager; +using Emby.Server.Implementations.Session; +using Emby.Server.Implementations.Social; +using Emby.Server.Implementations.Sync; +using Emby.Server.Implementations.TV; +using Emby.Server.Implementations.Updates; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Dlna; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.News; +using MediaBrowser.Model.Reflection; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Social; +using MediaBrowser.Model.Text; +using MediaBrowser.Model.Xml; +using OpenSubtitlesHandler; +using ServiceStack; +using SocketHttpListener.Primitives; +using StringExtensions = MediaBrowser.Controller.Extensions.StringExtensions; +using Emby.Drawing; +using Emby.Server.Implementations.Migrations; +using MediaBrowser.Model.Diagnostics; +using Emby.Common.Implementations.Diagnostics; + +namespace Emby.Server.Core +{ + /// <summary> + /// Class CompositionRoot + /// </summary> + public abstract class ApplicationHost : BaseApplicationHost<ServerApplicationPaths>, IServerApplicationHost, IDependencyContainer + { + /// <summary> + /// Gets the server configuration manager. + /// </summary> + /// <value>The server configuration manager.</value> + public IServerConfigurationManager ServerConfigurationManager + { + get { return (IServerConfigurationManager)ConfigurationManager; } + } + + /// <summary> + /// Gets the configuration manager. + /// </summary> + /// <returns>IConfigurationManager.</returns> + protected override IConfigurationManager GetConfigurationManager() + { + return new ServerConfigurationManager(ApplicationPaths, LogManager, XmlSerializer, FileSystemManager); + } + + /// <summary> + /// Gets or sets the server manager. + /// </summary> + /// <value>The server manager.</value> + private IServerManager ServerManager { get; set; } + /// <summary> + /// Gets or sets the user manager. + /// </summary> + /// <value>The user manager.</value> + public IUserManager UserManager { get; set; } + /// <summary> + /// Gets or sets the library manager. + /// </summary> + /// <value>The library manager.</value> + internal ILibraryManager LibraryManager { get; set; } + /// <summary> + /// Gets or sets the directory watchers. + /// </summary> + /// <value>The directory watchers.</value> + private ILibraryMonitor LibraryMonitor { get; set; } + /// <summary> + /// Gets or sets the provider manager. + /// </summary> + /// <value>The provider manager.</value> + private IProviderManager ProviderManager { get; set; } + /// <summary> + /// Gets or sets the HTTP server. + /// </summary> + /// <value>The HTTP server.</value> + private IHttpServer HttpServer { get; set; } + private IDtoService DtoService { get; set; } + private IImageProcessor ImageProcessor { get; set; } + + /// <summary> + /// Gets or sets the media encoder. + /// </summary> + /// <value>The media encoder.</value> + private IMediaEncoder MediaEncoder { get; set; } + private ISubtitleEncoder SubtitleEncoder { get; set; } + + private IConnectManager ConnectManager { get; set; } + private ISessionManager SessionManager { get; set; } + + private ILiveTvManager LiveTvManager { get; set; } + + public ILocalizationManager LocalizationManager { get; set; } + + private IEncodingManager EncodingManager { get; set; } + private IChannelManager ChannelManager { get; set; } + private ISyncManager SyncManager { get; set; } + + /// <summary> + /// Gets or sets the user data repository. + /// </summary> + /// <value>The user data repository.</value> + private IUserDataManager UserDataManager { get; set; } + private IUserRepository UserRepository { get; set; } + internal IDisplayPreferencesRepository DisplayPreferencesRepository { get; set; } + internal IItemRepository ItemRepository { get; set; } + private INotificationsRepository NotificationsRepository { get; set; } + private IFileOrganizationRepository FileOrganizationRepository { get; set; } + + private INotificationManager NotificationManager { get; set; } + private ISubtitleManager SubtitleManager { get; set; } + private IChapterManager ChapterManager { get; set; } + private IDeviceManager DeviceManager { get; set; } + + internal IUserViewManager UserViewManager { get; set; } + + private IAuthenticationRepository AuthenticationRepository { get; set; } + private ISyncRepository SyncRepository { get; set; } + private ITVSeriesManager TVSeriesManager { get; set; } + private ICollectionManager CollectionManager { get; set; } + private IMediaSourceManager MediaSourceManager { get; set; } + private IPlaylistManager PlaylistManager { get; set; } + + /// <summary> + /// Gets or sets the installation manager. + /// </summary> + /// <value>The installation manager.</value> + protected IInstallationManager InstallationManager { get; private set; } + /// <summary> + /// Gets the security manager. + /// </summary> + /// <value>The security manager.</value> + protected ISecurityManager SecurityManager { get; private set; } + + /// <summary> + /// Gets or sets the zip client. + /// </summary> + /// <value>The zip client.</value> + protected IZipClient ZipClient { get; private set; } + + protected IAuthService AuthService { get; private set; } + + protected readonly StartupOptions StartupOptions; + private readonly string _releaseAssetFilename; + + internal IPowerManagement PowerManagement { get; private set; } + internal IImageEncoder ImageEncoder { get; private set; } + + private readonly Action<string, string> _certificateGenerator; + private readonly Func<string> _defaultUserNameFactory; + + /// <summary> + /// Initializes a new instance of the <see cref="ApplicationHost" /> class. + /// </summary> + public ApplicationHost(ServerApplicationPaths applicationPaths, + ILogManager logManager, + StartupOptions options, + IFileSystem fileSystem, + IPowerManagement powerManagement, + string releaseAssetFilename, + IEnvironmentInfo environmentInfo, + IImageEncoder imageEncoder, + ISystemEvents systemEvents, + IMemoryStreamFactory memoryStreamFactory, + INetworkManager networkManager, + Action<string, string> certificateGenerator, + Func<string> defaultUsernameFactory) + : base(applicationPaths, + logManager, + fileSystem, + environmentInfo, + systemEvents, + memoryStreamFactory, + networkManager) + { + StartupOptions = options; + _certificateGenerator = certificateGenerator; + _releaseAssetFilename = releaseAssetFilename; + _defaultUserNameFactory = defaultUsernameFactory; + PowerManagement = powerManagement; + + ImageEncoder = imageEncoder; + + SetBaseExceptionMessage(); + } + + private Version _version; + /// <summary> + /// Gets the current application version + /// </summary> + /// <value>The application version.</value> + public override Version ApplicationVersion + { + get + { + return _version ?? (_version = GetAssembly(GetType()).GetName().Version); + } + } + + public abstract bool SupportsRunningAsService { get; } + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get + { + return "Emby Server"; + } + } + + private Assembly GetAssembly(Type type) + { + return type.GetTypeInfo().Assembly; + } + + public abstract bool SupportsAutoRunAtStartup { get; } + + private void SetBaseExceptionMessage() + { + var builder = GetBaseExceptionMessage(ApplicationPaths); + + // Skip if plugins haven't been loaded yet + //if (Plugins != null) + //{ + // var pluginString = string.Join("|", Plugins.Select(i => i.Name + "-" + i.Version.ToString()).ToArray()); + // builder.Insert(0, string.Format("Plugins: {0}{1}", pluginString, Environment.NewLine)); + //} + + builder.Insert(0, string.Format("Version: {0}{1}", ApplicationVersion, Environment.NewLine)); + builder.Insert(0, "*** Error Report ***" + Environment.NewLine); + + LogManager.ExceptionMessagePrefix = builder.ToString(); + } + + /// <summary> + /// Runs the startup tasks. + /// </summary> + public override async Task RunStartupTasks() + { + await PerformPreInitMigrations().ConfigureAwait(false); + + await base.RunStartupTasks().ConfigureAwait(false); + + await MediaEncoder.Init().ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(MediaEncoder.EncoderPath)) + { + if (ServerConfigurationManager.Configuration.IsStartupWizardCompleted) + { + ServerConfigurationManager.Configuration.IsStartupWizardCompleted = false; + ServerConfigurationManager.SaveConfiguration(); + } + } + + Logger.Info("ServerId: {0}", SystemId); + Logger.Info("Core startup complete"); + HttpServer.GlobalResponse = null; + + PerformPostInitMigrations(); + Logger.Info("Post-init migrations complete"); + + foreach (var entryPoint in GetExports<IServerEntryPoint>().ToList()) + { + var name = entryPoint.GetType().FullName; + Logger.Info("Starting entry point {0}", name); + var now = DateTime.UtcNow; + try + { + entryPoint.Run(); + } + catch (Exception ex) + { + Logger.ErrorException("Error in {0}", ex, name); + } + Logger.Info("Entry point completed: {0}. Duration: {1} seconds", name, (DateTime.UtcNow - now).TotalSeconds.ToString(CultureInfo.InvariantCulture)); + } + Logger.Info("All entry points have started"); + + LogManager.RemoveConsoleOutput(); + } + + protected override IJsonSerializer CreateJsonSerializer() + { + try + { + // https://github.com/ServiceStack/ServiceStack/blob/master/tests/ServiceStack.WebHost.IntegrationTests/Web.config#L4 + Licensing.RegisterLicense("1001-e1JlZjoxMDAxLE5hbWU6VGVzdCBCdXNpbmVzcyxUeXBlOkJ1c2luZXNzLEhhc2g6UHVNTVRPclhvT2ZIbjQ5MG5LZE1mUTd5RUMzQnBucTFEbTE3TDczVEF4QUNMT1FhNXJMOWkzVjFGL2ZkVTE3Q2pDNENqTkQyUktRWmhvUVBhYTBiekJGUUZ3ZE5aZHFDYm9hL3lydGlwUHI5K1JsaTBYbzNsUC85cjVJNHE5QVhldDN6QkE4aTlvdldrdTgyTk1relY2eis2dFFqTThYN2lmc0JveHgycFdjPSxFeHBpcnk6MjAxMy0wMS0wMX0="); + } + catch + { + // Failing under mono + } + + var result = new JsonSerializer(FileSystemManager, LogManager.GetLogger("JsonSerializer")); + + ServiceStack.Text.JsConfig<LiveTvProgram>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<LiveTvChannel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<LiveTvVideoRecording>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<LiveTvAudioRecording>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Series>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Audio>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<MusicAlbum>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<MusicArtist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<MusicGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<MusicVideo>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Movie>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Playlist>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<AudioPodcast>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<AudioBook>.ExcludePropertyNames = new[] { "Artists", "AlbumArtists", "ChannelMediaSources", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Trailer>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<BoxSet>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Episode>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Season>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Book>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<CollectionFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Folder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Game>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<GameGenre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<GameSystem>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Genre>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Person>.ExcludePropertyNames = new[] { "PlaceOfBirth", "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Photo>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<PhotoAlbum>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Studio>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<UserRootFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<UserView>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Video>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Year>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<Channel>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + ServiceStack.Text.JsConfig<AggregateFolder>.ExcludePropertyNames = new[] { "ProviderIds", "ImageInfos", "ProductionLocations", "ThemeSongIds", "ThemeVideoIds", "TotalBitrate", "ShortOverview", "Taglines", "Keywords", "ExtraType" }; + + return result; + } + + public override Task Init(IProgress<double> progress) + { + HttpPort = ServerConfigurationManager.Configuration.HttpServerPortNumber; + HttpsPort = ServerConfigurationManager.Configuration.HttpsPortNumber; + + // Safeguard against invalid configuration + if (HttpPort == HttpsPort) + { + HttpPort = ServerConfiguration.DefaultHttpPort; + HttpsPort = ServerConfiguration.DefaultHttpsPort; + } + + return base.Init(progress); + } + + private async Task PerformPreInitMigrations() + { + var migrations = new List<IVersionMigration> + { + new UpdateLevelMigration(ServerConfigurationManager, this, HttpClient, JsonSerializer, _releaseAssetFilename, Logger) + }; + + foreach (var task in migrations) + { + try + { + await task.Run().ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error running migration", ex); + } + } + } + + private void PerformPostInitMigrations() + { + var migrations = new List<IVersionMigration> + { + new LibraryScanMigration(ServerConfigurationManager, TaskManager) + }; + + foreach (var task in migrations) + { + try + { + task.Run(); + } + catch (Exception ex) + { + Logger.ErrorException("Error running migration", ex); + } + } + } + + /// <summary> + /// Registers resources that classes will depend on + /// </summary> + protected override async Task RegisterResources(IProgress<double> progress) + { + await base.RegisterResources(progress).ConfigureAwait(false); + + RegisterSingleInstance(PowerManagement); + + SecurityManager = new PluginSecurityManager(this, HttpClient, JsonSerializer, ApplicationPaths, LogManager, FileSystemManager, CryptographyProvider); + RegisterSingleInstance(SecurityManager); + + InstallationManager = new InstallationManager(LogManager.GetLogger("InstallationManager"), this, ApplicationPaths, HttpClient, JsonSerializer, SecurityManager, ConfigurationManager, FileSystemManager, CryptographyProvider); + RegisterSingleInstance(InstallationManager); + + ZipClient = new ZipClient(FileSystemManager); + RegisterSingleInstance(ZipClient); + + RegisterSingleInstance<IHttpResultFactory>(new HttpResultFactory(LogManager, FileSystemManager, JsonSerializer, MemoryStreamFactory)); + + RegisterSingleInstance<IServerApplicationHost>(this); + RegisterSingleInstance<IServerApplicationPaths>(ApplicationPaths); + + RegisterSingleInstance(ServerConfigurationManager); + + IAssemblyInfo assemblyInfo = new AssemblyInfo(); + RegisterSingleInstance<IAssemblyInfo>(assemblyInfo); + + LocalizationManager = new LocalizationManager(ServerConfigurationManager, FileSystemManager, JsonSerializer, LogManager.GetLogger("LocalizationManager"), assemblyInfo, new TextLocalizer()); + StringExtensions.LocalizationManager = LocalizationManager; + RegisterSingleInstance(LocalizationManager); + + ITextEncoding textEncoding = new TextEncoding(FileSystemManager); + RegisterSingleInstance(textEncoding); + Utilities.EncodingHelper = textEncoding; + RegisterSingleInstance<IBlurayExaminer>(() => new BdInfoExaminer(FileSystemManager, textEncoding)); + + RegisterSingleInstance<IXmlReaderSettingsFactory>(new XmlReaderSettingsFactory()); + + UserDataManager = new UserDataManager(LogManager, ServerConfigurationManager); + RegisterSingleInstance(UserDataManager); + + UserRepository = GetUserRepository(); + // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it + RegisterSingleInstance(UserRepository); + + var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LogManager.GetLogger("SqliteDisplayPreferencesRepository"), JsonSerializer, ApplicationPaths, MemoryStreamFactory); + DisplayPreferencesRepository = displayPreferencesRepo; + RegisterSingleInstance(DisplayPreferencesRepository); + + var itemRepo = new SqliteItemRepository(ServerConfigurationManager, JsonSerializer, LogManager.GetLogger("SqliteItemRepository"), MemoryStreamFactory, assemblyInfo, FileSystemManager, EnvironmentInfo, TimerFactory); + ItemRepository = itemRepo; + RegisterSingleInstance(ItemRepository); + + FileOrganizationRepository = GetFileOrganizationRepository(); + RegisterSingleInstance(FileOrganizationRepository); + + AuthenticationRepository = await GetAuthenticationRepository().ConfigureAwait(false); + RegisterSingleInstance(AuthenticationRepository); + + SyncRepository = GetSyncRepository(); + RegisterSingleInstance(SyncRepository); + + UserManager = new UserManager(LogManager.GetLogger("UserManager"), ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, () => ConnectManager, this, JsonSerializer, FileSystemManager, CryptographyProvider, _defaultUserNameFactory()); + RegisterSingleInstance(UserManager); + + LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); + RegisterSingleInstance(LibraryManager); + + var musicManager = new MusicManager(LibraryManager); + RegisterSingleInstance<IMusicManager>(new MusicManager(LibraryManager)); + + LibraryMonitor = new LibraryMonitor(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, FileSystemManager, TimerFactory, SystemEvents, EnvironmentInfo); + RegisterSingleInstance(LibraryMonitor); + + ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager, ApplicationPaths, () => LibraryManager, JsonSerializer, MemoryStreamFactory); + RegisterSingleInstance(ProviderManager); + + RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager)); + + CertificatePath = GetCertificatePath(true); + Certificate = GetCertificate(CertificatePath); + + HttpServer = HttpServerFactory.CreateServer(this, LogManager, ServerConfigurationManager, NetworkManager, MemoryStreamFactory, "Emby", "web/index.html", textEncoding, SocketFactory, CryptographyProvider, JsonSerializer, XmlSerializer, EnvironmentInfo, Certificate, SupportsDualModeSockets); + HttpServer.GlobalResponse = LocalizationManager.GetLocalizedString("StartupEmbyServerIsLoading"); + RegisterSingleInstance(HttpServer, false); + progress.Report(10); + + ServerManager = new ServerManager(this, JsonSerializer, LogManager.GetLogger("ServerManager"), ServerConfigurationManager, MemoryStreamFactory, textEncoding); + RegisterSingleInstance(ServerManager); + + var innerProgress = new ActionableProgress<double>(); + innerProgress.RegisterAction(p => progress.Report((.75 * p) + 15)); + + ImageProcessor = GetImageProcessor(); + RegisterSingleInstance(ImageProcessor); + + TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); + RegisterSingleInstance(TVSeriesManager); + + SyncManager = new SyncManager(LibraryManager, SyncRepository, ImageProcessor, LogManager.GetLogger("SyncManager"), UserManager, () => DtoService, this, TVSeriesManager, () => MediaEncoder, FileSystemManager, () => SubtitleEncoder, ServerConfigurationManager, UserDataManager, () => MediaSourceManager, JsonSerializer, TaskManager, MemoryStreamFactory); + RegisterSingleInstance(SyncManager); + + DtoService = new DtoService(LogManager.GetLogger("DtoService"), LibraryManager, UserDataManager, ItemRepository, ImageProcessor, ServerConfigurationManager, FileSystemManager, ProviderManager, () => ChannelManager, SyncManager, this, () => DeviceManager, () => MediaSourceManager, () => LiveTvManager); + RegisterSingleInstance(DtoService); + + var encryptionManager = new EncryptionManager(); + RegisterSingleInstance<IEncryptionManager>(encryptionManager); + + ConnectManager = new ConnectManager(LogManager.GetLogger("ConnectManager"), ApplicationPaths, JsonSerializer, encryptionManager, HttpClient, this, ServerConfigurationManager, UserManager, ProviderManager, SecurityManager, FileSystemManager); + RegisterSingleInstance(ConnectManager); + + DeviceManager = new DeviceManager(new DeviceRepository(ApplicationPaths, JsonSerializer, LogManager.GetLogger("DeviceManager"), FileSystemManager), UserManager, FileSystemManager, LibraryMonitor, ServerConfigurationManager, LogManager.GetLogger("DeviceManager"), NetworkManager); + RegisterSingleInstance(DeviceManager); + + var newsService = new Emby.Server.Implementations.News.NewsService(ApplicationPaths, JsonSerializer); + RegisterSingleInstance<INewsService>(newsService); + + var fileOrganizationService = new FileOrganizationService(TaskManager, FileOrganizationRepository, LogManager.GetLogger("FileOrganizationService"), LibraryMonitor, LibraryManager, ServerConfigurationManager, FileSystemManager, ProviderManager); + RegisterSingleInstance<IFileOrganizationService>(fileOrganizationService); + + progress.Report(15); + + ChannelManager = new ChannelManager(UserManager, DtoService, LibraryManager, LogManager.GetLogger("ChannelManager"), ServerConfigurationManager, FileSystemManager, UserDataManager, JsonSerializer, LocalizationManager, HttpClient, ProviderManager); + RegisterSingleInstance(ChannelManager); + + MediaSourceManager = new MediaSourceManager(ItemRepository, UserManager, LibraryManager, LogManager.GetLogger("MediaSourceManager"), JsonSerializer, FileSystemManager, UserDataManager, TimerFactory); + RegisterSingleInstance(MediaSourceManager); + + SessionManager = new SessionManager(UserDataManager, LogManager.GetLogger("SessionManager"), LibraryManager, UserManager, musicManager, DtoService, ImageProcessor, JsonSerializer, this, HttpClient, AuthenticationRepository, DeviceManager, MediaSourceManager, TimerFactory); + RegisterSingleInstance(SessionManager); + + var dlnaManager = new DlnaManager(XmlSerializer, FileSystemManager, ApplicationPaths, LogManager.GetLogger("Dlna"), JsonSerializer, this, assemblyInfo); + RegisterSingleInstance<IDlnaManager>(dlnaManager); + + var connectionManager = new ConnectionManager(dlnaManager, ServerConfigurationManager, LogManager.GetLogger("UpnpConnectionManager"), HttpClient, new XmlReaderSettingsFactory()); + RegisterSingleInstance<IConnectionManager>(connectionManager); + + CollectionManager = new CollectionManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("CollectionManager"), ProviderManager); + RegisterSingleInstance(CollectionManager); + + PlaylistManager = new PlaylistManager(LibraryManager, FileSystemManager, LibraryMonitor, LogManager.GetLogger("PlaylistManager"), UserManager, ProviderManager); + RegisterSingleInstance<IPlaylistManager>(PlaylistManager); + + LiveTvManager = new LiveTvManager(this, ServerConfigurationManager, Logger, ItemRepository, ImageProcessor, UserDataManager, DtoService, UserManager, LibraryManager, TaskManager, LocalizationManager, JsonSerializer, ProviderManager, FileSystemManager, SecurityManager); + RegisterSingleInstance(LiveTvManager); + + UserViewManager = new UserViewManager(LibraryManager, LocalizationManager, UserManager, ChannelManager, LiveTvManager, ServerConfigurationManager); + RegisterSingleInstance(UserViewManager); + + var contentDirectory = new ContentDirectory(dlnaManager, UserDataManager, ImageProcessor, LibraryManager, ServerConfigurationManager, UserManager, LogManager.GetLogger("UpnpContentDirectory"), HttpClient, LocalizationManager, ChannelManager, MediaSourceManager, UserViewManager, () => MediaEncoder, new XmlReaderSettingsFactory()); + RegisterSingleInstance<IContentDirectory>(contentDirectory); + + var mediaRegistrar = new MediaReceiverRegistrar(LogManager.GetLogger("MediaReceiverRegistrar"), HttpClient, ServerConfigurationManager, new XmlReaderSettingsFactory()); + RegisterSingleInstance<IMediaReceiverRegistrar>(mediaRegistrar); + + NotificationManager = new NotificationManager(LogManager, UserManager, ServerConfigurationManager); + RegisterSingleInstance(NotificationManager); + + SubtitleManager = new SubtitleManager(LogManager.GetLogger("SubtitleManager"), FileSystemManager, LibraryMonitor, LibraryManager, MediaSourceManager); + RegisterSingleInstance(SubtitleManager); + + RegisterSingleInstance<IDeviceDiscovery>(new DeviceDiscovery(LogManager.GetLogger("IDeviceDiscovery"), ServerConfigurationManager, SocketFactory, TimerFactory)); + + ChapterManager = new ChapterManager(LibraryManager, LogManager.GetLogger("ChapterManager"), ServerConfigurationManager, ItemRepository); + RegisterSingleInstance(ChapterManager); + + await RegisterMediaEncoder(innerProgress).ConfigureAwait(false); + progress.Report(90); + + EncodingManager = new EncodingManager(FileSystemManager, Logger, MediaEncoder, ChapterManager, LibraryManager); + RegisterSingleInstance(EncodingManager); + + var sharingRepo = new SharingRepository(LogManager.GetLogger("SharingRepository"), ApplicationPaths); + sharingRepo.Initialize(); + // This is only needed for disposal purposes. If removing this, make sure to have the manager handle disposing it + RegisterSingleInstance<ISharingRepository>(sharingRepo); + RegisterSingleInstance<ISharingManager>(new SharingManager(sharingRepo, ServerConfigurationManager, LibraryManager, this)); + + var activityLogRepo = GetActivityLogRepository(); + RegisterSingleInstance(activityLogRepo); + RegisterSingleInstance<IActivityManager>(new ActivityManager(LogManager.GetLogger("ActivityManager"), activityLogRepo, UserManager)); + + var authContext = new AuthorizationContext(AuthenticationRepository, ConnectManager); + RegisterSingleInstance<IAuthorizationContext>(authContext); + RegisterSingleInstance<ISessionContext>(new SessionContext(UserManager, authContext, SessionManager)); + + AuthService = new AuthService(UserManager, authContext, ServerConfigurationManager, ConnectManager, SessionManager, DeviceManager); + RegisterSingleInstance<IAuthService>(AuthService); + + SubtitleEncoder = new SubtitleEncoder(LibraryManager, LogManager.GetLogger("SubtitleEncoder"), ApplicationPaths, FileSystemManager, MediaEncoder, JsonSerializer, HttpClient, MediaSourceManager, MemoryStreamFactory, ProcessFactory, textEncoding); + RegisterSingleInstance(SubtitleEncoder); + + displayPreferencesRepo.Initialize(); + + var userDataRepo = new SqliteUserDataRepository(LogManager.GetLogger("SqliteUserDataRepository"), ApplicationPaths, FileSystemManager); + + ((UserDataManager)UserDataManager).Repository = userDataRepo; + itemRepo.Initialize(userDataRepo); + ((LibraryManager)LibraryManager).ItemRepository = ItemRepository; + ConfigureNotificationsRepository(); + progress.Report(100); + + SetStaticProperties(); + + await ((UserManager)UserManager).Initialize().ConfigureAwait(false); + } + + protected abstract bool SupportsDualModeSockets { get; } + + private ICertificate GetCertificate(string certificateLocation) + { + if (string.IsNullOrWhiteSpace(certificateLocation)) + { + return null; + } + + try + { + if (!FileSystemManager.FileExists(certificateLocation)) + { + return null; + } + + X509Certificate2 localCert = new X509Certificate2(certificateLocation); + //localCert.PrivateKey = PrivateKey.CreateFromFile(pvk_file).RSA; + if (!localCert.HasPrivateKey) + { + //throw new FileNotFoundException("Secure requested, no private key included", certificateLocation); + return null; + } + + return new Certificate(localCert); + } + catch (Exception ex) + { + Logger.ErrorException("Error loading cert from {0}", ex, certificateLocation); + return null; + } + } + + private IImageProcessor GetImageProcessor() + { + var maxConcurrentImageProcesses = Math.Max(Environment.ProcessorCount, 4); + + if (StartupOptions.ContainsOption("-imagethreads")) + { + int.TryParse(StartupOptions.GetOption("-imagethreads"), NumberStyles.Any, CultureInfo.InvariantCulture, out maxConcurrentImageProcesses); + } + + return new ImageProcessor(LogManager.GetLogger("ImageProcessor"), ServerConfigurationManager.ApplicationPaths, FileSystemManager, JsonSerializer, ImageEncoder, maxConcurrentImageProcesses, () => LibraryManager, TimerFactory); + } + + protected abstract FFMpegInstallInfo GetFfmpegInstallInfo(); + + /// <summary> + /// Registers the media encoder. + /// </summary> + /// <returns>Task.</returns> + private async Task RegisterMediaEncoder(IProgress<double> progress) + { + string encoderPath = null; + string probePath = null; + + var info = await new FFMpegLoader(Logger, ApplicationPaths, HttpClient, ZipClient, FileSystemManager, GetFfmpegInstallInfo()) + .GetFFMpegInfo(StartupOptions, progress).ConfigureAwait(false); + + encoderPath = info.EncoderPath; + probePath = info.ProbePath; + var hasExternalEncoder = string.Equals(info.Version, "external", StringComparison.OrdinalIgnoreCase); + + var mediaEncoder = new MediaEncoder(LogManager.GetLogger("MediaEncoder"), + JsonSerializer, + encoderPath, + probePath, + hasExternalEncoder, + ServerConfigurationManager, + FileSystemManager, + LiveTvManager, + IsoManager, + LibraryManager, + ChannelManager, + SessionManager, + () => SubtitleEncoder, + () => MediaSourceManager, + HttpClient, + ZipClient, + MemoryStreamFactory, + ProcessFactory, + (Environment.ProcessorCount > 2 ? 14000 : 40000), + EnvironmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows, + EnvironmentInfo); + + MediaEncoder = mediaEncoder; + RegisterSingleInstance(MediaEncoder); + } + + /// <summary> + /// Gets the user repository. + /// </summary> + /// <returns>Task{IUserRepository}.</returns> + private IUserRepository GetUserRepository() + { + var repo = new SqliteUserRepository(LogManager.GetLogger("SqliteUserRepository"), ApplicationPaths, JsonSerializer, MemoryStreamFactory); + + repo.Initialize(); + + return repo; + } + + /// <summary> + /// Gets the file organization repository. + /// </summary> + /// <returns>Task{IUserRepository}.</returns> + private IFileOrganizationRepository GetFileOrganizationRepository() + { + var repo = new SqliteFileOrganizationRepository(LogManager.GetLogger("SqliteFileOrganizationRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + private async Task<IAuthenticationRepository> GetAuthenticationRepository() + { + var repo = new AuthenticationRepository(LogManager.GetLogger("AuthenticationRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + private IActivityRepository GetActivityLogRepository() + { + var repo = new ActivityRepository(LogManager.GetLogger("ActivityRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + private ISyncRepository GetSyncRepository() + { + var repo = new SyncRepository(LogManager.GetLogger("SyncRepository"), JsonSerializer, ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + return repo; + } + + /// <summary> + /// Configures the repositories. + /// </summary> + private void ConfigureNotificationsRepository() + { + var repo = new SqliteNotificationsRepository(LogManager.GetLogger("SqliteNotificationsRepository"), ServerConfigurationManager.ApplicationPaths); + + repo.Initialize(); + + NotificationsRepository = repo; + + RegisterSingleInstance(NotificationsRepository); + } + + /// <summary> + /// Dirty hacks + /// </summary> + private void SetStaticProperties() + { + // For now there's no real way to inject these properly + BaseItem.Logger = LogManager.GetLogger("BaseItem"); + BaseItem.ConfigurationManager = ServerConfigurationManager; + BaseItem.LibraryManager = LibraryManager; + BaseItem.ProviderManager = ProviderManager; + BaseItem.LocalizationManager = LocalizationManager; + BaseItem.ItemRepository = ItemRepository; + User.XmlSerializer = XmlSerializer; + User.UserManager = UserManager; + Folder.UserManager = UserManager; + BaseItem.FileSystem = FileSystemManager; + BaseItem.UserDataManager = UserDataManager; + BaseItem.ChannelManager = ChannelManager; + BaseItem.LiveTvManager = LiveTvManager; + Folder.UserViewManager = UserViewManager; + UserView.TVSeriesManager = TVSeriesManager; + UserView.PlaylistManager = PlaylistManager; + BaseItem.CollectionManager = CollectionManager; + BaseItem.MediaSourceManager = MediaSourceManager; + CollectionFolder.XmlSerializer = XmlSerializer; + BaseStreamingService.AppHost = this; + BaseStreamingService.HttpClient = HttpClient; + Utilities.CryptographyProvider = CryptographyProvider; + AuthenticatedAttribute.AuthService = AuthService; + } + + /// <summary> + /// Finds the parts. + /// </summary> + protected override void FindParts() + { + if (!ServerConfigurationManager.Configuration.IsPortAuthorized) + { + RegisterServerWithAdministratorAccess(); + ServerConfigurationManager.Configuration.IsPortAuthorized = true; + ConfigurationManager.SaveConfiguration(); + } + + RegisterModules(); + + base.FindParts(); + + HttpServer.Init(GetExports<IService>(false)); + + ServerManager.AddWebSocketListeners(GetExports<IWebSocketListener>(false)); + + StartServer(); + + LibraryManager.AddParts(GetExports<IResolverIgnoreRule>(), + GetExports<IVirtualFolderCreator>(), + GetExports<IItemResolver>(), + GetExports<IIntroProvider>(), + GetExports<IBaseItemComparer>(), + GetExports<ILibraryPostScanTask>()); + + ProviderManager.AddParts(GetExports<IImageProvider>(), + GetExports<IMetadataService>(), + GetExports<IMetadataProvider>(), + GetExports<IMetadataSaver>(), + GetExports<IExternalId>()); + + ImageProcessor.AddParts(GetExports<IImageEnhancer>()); + + LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); + + SubtitleManager.AddParts(GetExports<ISubtitleProvider>()); + + SessionManager.AddParts(GetExports<ISessionControllerFactory>()); + + ChannelManager.AddParts(GetExports<IChannel>()); + + MediaSourceManager.AddParts(GetExports<IMediaSourceProvider>()); + + NotificationManager.AddParts(GetExports<INotificationService>(), GetExports<INotificationTypeFactory>()); + SyncManager.AddParts(GetExports<ISyncProvider>()); + } + + private string CertificatePath { get; set; } + private ICertificate Certificate { get; set; } + + private IEnumerable<string> GetUrlPrefixes() + { + var hosts = new List<string>(); + + hosts.Add("+"); + + return hosts.SelectMany(i => + { + var prefixes = new List<string> + { + "http://"+i+":" + HttpPort + "/" + }; + + if (!string.IsNullOrWhiteSpace(CertificatePath)) + { + prefixes.Add("https://" + i + ":" + HttpsPort + "/"); + } + + return prefixes; + }); + } + + /// <summary> + /// Starts the server. + /// </summary> + private void StartServer() + { + try + { + ServerManager.Start(GetUrlPrefixes()); + return; + } + catch (Exception ex) + { + Logger.ErrorException("Error starting http server", ex); + + if (HttpPort == ServerConfiguration.DefaultHttpPort) + { + throw; + } + } + + HttpPort = ServerConfiguration.DefaultHttpPort; + + try + { + ServerManager.Start(GetUrlPrefixes()); + } + catch (Exception ex) + { + Logger.ErrorException("Error starting http server", ex); + + throw; + } + } + + private string GetCertificatePath(bool generateCertificate) + { + if (!string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.CertificatePath)) + { + // Custom cert + return ServerConfigurationManager.Configuration.CertificatePath; + } + + // Generate self-signed cert + var certHost = GetHostnameFromExternalDns(ServerConfigurationManager.Configuration.WanDdns); + var certPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.ProgramDataPath, "ssl", "cert_" + (certHost + "1").GetMD5().ToString("N") + ".pfx"); + + if (generateCertificate) + { + if (!FileSystemManager.FileExists(certPath)) + { + FileSystemManager.CreateDirectory(Path.GetDirectoryName(certPath)); + + try + { + _certificateGenerator(certPath, certHost); + } + catch (Exception ex) + { + Logger.ErrorException("Error creating ssl cert", ex); + return null; + } + } + } + + return certPath; + } + + /// <summary> + /// Called when [configuration updated]. + /// </summary> + /// <param name="sender">The sender.</param> + /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param> + protected override void OnConfigurationUpdated(object sender, EventArgs e) + { + base.OnConfigurationUpdated(sender, e); + + var requiresRestart = false; + + // Don't do anything if these haven't been set yet + if (HttpPort != 0 && HttpsPort != 0) + { + // Need to restart if ports have changed + if (ServerConfigurationManager.Configuration.HttpServerPortNumber != HttpPort || + ServerConfigurationManager.Configuration.HttpsPortNumber != HttpsPort) + { + if (ServerConfigurationManager.Configuration.IsPortAuthorized) + { + ServerConfigurationManager.Configuration.IsPortAuthorized = false; + ServerConfigurationManager.SaveConfiguration(); + + requiresRestart = true; + } + } + } + + if (!HttpServer.UrlPrefixes.SequenceEqual(GetUrlPrefixes(), StringComparer.OrdinalIgnoreCase)) + { + requiresRestart = true; + } + + if (!string.Equals(CertificatePath, GetCertificatePath(false), StringComparison.OrdinalIgnoreCase)) + { + requiresRestart = true; + } + + if (requiresRestart) + { + NotifyPendingRestart(); + } + } + + /// <summary> + /// Restarts this instance. + /// </summary> + public override async Task Restart() + { + if (!CanSelfRestart) + { + throw new PlatformNotSupportedException("The server is unable to self-restart. Please restart manually."); + } + + try + { + await SessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error sending server restart notification", ex); + } + + Logger.Info("Calling RestartInternal"); + + RestartInternal(); + } + + protected abstract void RestartInternal(); + + /// <summary> + /// Gets the composable part assemblies. + /// </summary> + /// <returns>IEnumerable{Assembly}.</returns> + protected override IEnumerable<Assembly> GetComposablePartAssemblies() + { + var list = GetPluginAssemblies() + .ToList(); + + // Gets all plugin assemblies by first reading all bytes of the .dll and calling Assembly.Load against that + // This will prevent the .dll file from getting locked, and allow us to replace it when needed + + // Include composable parts in the Api assembly + list.Add(GetAssembly(typeof(ApiEntryPoint))); + + // Include composable parts in the Dashboard assembly + list.Add(GetAssembly(typeof(DashboardService))); + + // Include composable parts in the Model assembly + list.Add(GetAssembly(typeof(SystemInfo))); + + // Include composable parts in the Common assembly + list.Add(GetAssembly(typeof(IApplicationHost))); + + // Include composable parts in the Controller assembly + list.Add(GetAssembly(typeof(IServerApplicationHost))); + + // Include composable parts in the Providers assembly + list.Add(GetAssembly(typeof(ProviderUtils))); + + // Include composable parts in the Photos assembly + list.Add(GetAssembly(typeof(PhotoProvider))); + + // Common implementations + list.Add(GetAssembly(typeof(TaskManager))); + + // Emby.Server implementations + list.Add(GetAssembly(typeof(InstallationManager))); + + // Emby.Server.Core + list.Add(GetAssembly(typeof(ServerApplicationPaths))); + + // MediaEncoding + list.Add(GetAssembly(typeof(MediaEncoder))); + + // Dlna + list.Add(GetAssembly(typeof(DlnaEntryPoint))); + + // Local metadata + list.Add(GetAssembly(typeof(BoxSetXmlSaver))); + + // Xbmc + list.Add(GetAssembly(typeof(ArtistNfoProvider))); + + list.AddRange(GetAssembliesWithPartsInternal()); + + // Include composable parts in the running assembly + list.Add(GetAssembly(typeof(ApplicationHost))); + + return list; + } + + protected abstract List<Assembly> GetAssembliesWithPartsInternal(); + + /// <summary> + /// Gets the plugin assemblies. + /// </summary> + /// <returns>IEnumerable{Assembly}.</returns> + private IEnumerable<Assembly> GetPluginAssemblies() + { + try + { + return Directory.EnumerateFiles(ApplicationPaths.PluginsPath, "*.dll", SearchOption.TopDirectoryOnly) + .Where(EnablePlugin) + .Select(LoadAssembly) + .Where(a => a != null) + .ToList(); + } + catch (DirectoryNotFoundException) + { + return new List<Assembly>(); + } + } + + private bool EnablePlugin(string path) + { + var filename = Path.GetFileName(path); + + var exclude = new[] + { + "mbplus.dll", + "mbintros.dll" + }; + + return !exclude.Contains(filename ?? string.Empty, StringComparer.OrdinalIgnoreCase); + } + + /// <summary> + /// Gets the system status. + /// </summary> + /// <returns>SystemInfo.</returns> + public async Task<SystemInfo> GetSystemInfo() + { + var localAddress = await GetLocalApiUrl().ConfigureAwait(false); + + return new SystemInfo + { + HasPendingRestart = HasPendingRestart, + Version = ApplicationVersion.ToString(), + WebSocketPortNumber = HttpPort, + FailedPluginAssemblies = FailedAssemblies.ToList(), + InProgressInstallations = InstallationManager.CurrentInstallations.Select(i => i.Item1).ToList(), + CompletedInstallations = InstallationManager.CompletedInstallations.ToList(), + Id = SystemId, + ProgramDataPath = ApplicationPaths.ProgramDataPath, + LogPath = ApplicationPaths.LogDirectoryPath, + ItemsByNamePath = ApplicationPaths.ItemsByNamePath, + InternalMetadataPath = ApplicationPaths.InternalMetadataPath, + CachePath = ApplicationPaths.CachePath, + MacAddress = GetMacAddress(), + HttpServerPortNumber = HttpPort, + SupportsHttps = SupportsHttps, + HttpsPortNumber = HttpsPort, + OperatingSystem = EnvironmentInfo.OperatingSystem.ToString(), + OperatingSystemDisplayName = OperatingSystemDisplayName, + CanSelfRestart = CanSelfRestart, + CanSelfUpdate = CanSelfUpdate, + WanAddress = ConnectManager.WanApiAddress, + HasUpdateAvailable = HasUpdateAvailable, + SupportsAutoRunAtStartup = SupportsAutoRunAtStartup, + TranscodingTempPath = ApplicationPaths.TranscodingTempPath, + IsRunningAsService = IsRunningAsService, + SupportsRunningAsService = SupportsRunningAsService, + ServerName = FriendlyName, + LocalAddress = localAddress, + SupportsLibraryMonitor = true, + EncoderLocationType = MediaEncoder.EncoderLocationType, + SystemArchitecture = EnvironmentInfo.SystemArchitecture, + SystemUpdateLevel = ConfigurationManager.CommonConfiguration.SystemUpdateLevel, + PackageName = StartupOptions.GetOption("-package") + }; + } + + public bool EnableHttps + { + get + { + return SupportsHttps && ServerConfigurationManager.Configuration.EnableHttps; + } + } + + public bool SupportsHttps + { + get { return Certificate != null; } + } + + public async Task<string> GetLocalApiUrl() + { + try + { + // Return the first matched address, if found, or the first known local address + var address = (await GetLocalIpAddresses().ConfigureAwait(false)).FirstOrDefault(i => !i.Equals(IpAddressInfo.Loopback) && !i.Equals(IpAddressInfo.IPv6Loopback)); + + if (address != null) + { + return GetLocalApiUrl(address); + } + + return null; + } + catch (Exception ex) + { + Logger.ErrorException("Error getting local Ip address information", ex); + } + + return null; + } + + public string GetLocalApiUrl(IpAddressInfo ipAddress) + { + if (ipAddress.AddressFamily == IpAddressFamily.InterNetworkV6) + { + return GetLocalApiUrl("[" + ipAddress.Address + "]"); + } + + return GetLocalApiUrl(ipAddress.Address); + } + + public string GetLocalApiUrl(string host) + { + return string.Format("http://{0}:{1}", + host, + HttpPort.ToString(CultureInfo.InvariantCulture)); + } + + public async Task<List<IpAddressInfo>> GetLocalIpAddresses() + { + var addresses = ServerConfigurationManager + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .Where(i => i != null) + .ToList(); + + if (addresses.Count == 0) + { + addresses.AddRange(NetworkManager.GetLocalIpAddresses()); + + var list = new List<IpAddressInfo>(); + + foreach (var address in addresses) + { + var valid = await IsIpAddressValidAsync(address).ConfigureAwait(false); + if (valid) + { + list.Add(address); + } + } + + addresses = list; + } + + return addresses; + } + + private IpAddressInfo NormalizeConfiguredLocalAddress(string address) + { + var index = address.Trim('/').IndexOf('/'); + + if (index != -1) + { + address = address.Substring(index + 1); + } + + IpAddressInfo result; + if (NetworkManager.TryParseIpAddress(address.Trim('/'), out result)) + { + return result; + } + return null; + } + + private readonly ConcurrentDictionary<string, bool> _validAddressResults = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase); + private DateTime _lastAddressCacheClear; + private async Task<bool> IsIpAddressValidAsync(IpAddressInfo address) + { + if (address.Equals(IpAddressInfo.Loopback) || + address.Equals(IpAddressInfo.IPv6Loopback)) + { + return true; + } + + var apiUrl = GetLocalApiUrl(address); + apiUrl += "/system/ping"; + + if ((DateTime.UtcNow - _lastAddressCacheClear).TotalMinutes >= 15) + { + _lastAddressCacheClear = DateTime.UtcNow; + _validAddressResults.Clear(); + } + + bool cachedResult; + if (_validAddressResults.TryGetValue(apiUrl, out cachedResult)) + { + return cachedResult; + } + + try + { + using (var response = await HttpClient.SendAsync(new HttpRequestOptions + { + Url = apiUrl, + LogErrorResponseBody = false, + LogErrors = false, + LogRequest = false, + TimeoutMs = 30000, + BufferContent = false + + }, "POST").ConfigureAwait(false)) + { + using (var reader = new StreamReader(response.Content)) + { + var result = reader.ReadToEnd(); + var valid = string.Equals(Name, result, StringComparison.OrdinalIgnoreCase); + + _validAddressResults.AddOrUpdate(apiUrl, valid, (k, v) => valid); + //Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, valid); + return valid; + } + } + } + catch + { + //Logger.Debug("Ping test result to {0}. Success: {1}", apiUrl, false); + + _validAddressResults.AddOrUpdate(apiUrl, false, (k, v) => false); + return false; + } + } + + public string FriendlyName + { + get + { + return string.IsNullOrWhiteSpace(ServerConfigurationManager.Configuration.ServerName) + ? Environment.MachineName + : ServerConfigurationManager.Configuration.ServerName; + } + } + + public int HttpPort { get; private set; } + + public int HttpsPort { get; private set; } + + /// <summary> + /// Gets the mac address. + /// </summary> + /// <returns>System.String.</returns> + private string GetMacAddress() + { + try + { + return NetworkManager.GetMacAddress(); + } + catch (Exception ex) + { + Logger.ErrorException("Error getting mac address", ex); + return null; + } + } + + /// <summary> + /// Shuts down. + /// </summary> + public override async Task Shutdown() + { + try + { + await SessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Logger.ErrorException("Error sending server shutdown notification", ex); + } + + ShutdownInternal(); + } + + protected abstract void ShutdownInternal(); + + /// <summary> + /// Registers the server with administrator access. + /// </summary> + private void RegisterServerWithAdministratorAccess() + { + Logger.Info("Requesting administrative access to authorize http server"); + + try + { + AuthorizeServer(); + } + catch (Exception ex) + { + Logger.ErrorException("Error authorizing server", ex); + } + } + + protected abstract void AuthorizeServer(); + + public event EventHandler HasUpdateAvailableChanged; + + private bool _hasUpdateAvailable; + public bool HasUpdateAvailable + { + get { return _hasUpdateAvailable; } + set + { + var fireEvent = value && !_hasUpdateAvailable; + + _hasUpdateAvailable = value; + + if (fireEvent) + { + EventHelper.FireEventIfNotNull(HasUpdateAvailableChanged, this, EventArgs.Empty, Logger); + } + } + } + + /// <summary> + /// Checks for update. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task{CheckForUpdateResult}.</returns> + public override async Task<CheckForUpdateResult> CheckForApplicationUpdate(CancellationToken cancellationToken, IProgress<double> progress) + { + var cacheLength = TimeSpan.FromHours(3); + var updateLevel = ConfigurationManager.CommonConfiguration.SystemUpdateLevel; + + if (updateLevel == PackageVersionClass.Beta) + { + cacheLength = TimeSpan.FromHours(1); + } + else if (updateLevel == PackageVersionClass.Dev) + { + cacheLength = TimeSpan.FromMinutes(5); + } + + var result = await new GithubUpdater(HttpClient, JsonSerializer).CheckForUpdateResult("MediaBrowser", "Emby", ApplicationVersion, updateLevel, _releaseAssetFilename, + "MBServer", "Mbserver.zip", cacheLength, cancellationToken).ConfigureAwait(false); + + HasUpdateAvailable = result.IsUpdateAvailable; + + return result; + } + + /// <summary> + /// Updates the application. + /// </summary> + /// <param name="package">The package that contains the update</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + public override async Task UpdateApplication(PackageVersionInfo package, CancellationToken cancellationToken, IProgress<double> progress) + { + await InstallationManager.InstallPackage(package, false, progress, cancellationToken).ConfigureAwait(false); + + HasUpdateAvailable = false; + + OnApplicationUpdated(package); + } + + /// <summary> + /// Configures the automatic run at startup. + /// </summary> + /// <param name="autorun">if set to <c>true</c> [autorun].</param> + protected override void ConfigureAutoRunAtStartup(bool autorun) + { + if (SupportsAutoRunAtStartup) + { + ConfigureAutoRunInternal(autorun); + } + } + + protected abstract void ConfigureAutoRunInternal(bool autorun); + + /// <summary> + /// This returns localhost in the case of no external dns, and the hostname if the + /// dns is prefixed with a valid Uri prefix. + /// </summary> + /// <param name="externalDns">The external dns prefix to get the hostname of.</param> + /// <returns>The hostname in <paramref name="externalDns"/></returns> + private static string GetHostnameFromExternalDns(string externalDns) + { + if (string.IsNullOrWhiteSpace(externalDns)) + { + return "localhost"; + } + + try + { + return new Uri(externalDns).Host; + } + catch + { + return externalDns; + } + } + + public void LaunchUrl(string url) + { + if (EnvironmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows) + { + throw new NotImplementedException(); + } + + var process = ProcessFactory.Create(new ProcessOptions + { + FileName = url, + EnableRaisingEvents = true, + UseShellExecute = true, + ErrorDialog = false + }); + + process.Exited += ProcessExited; + + try + { + process.Start(); + } + catch (Exception ex) + { + Console.WriteLine("Error launching url: {0}", url); + Logger.ErrorException("Error launching url: {0}", ex, url); + + throw; + } + } + + private static void ProcessExited(object sender, EventArgs e) + { + ((IProcess)sender).Dispose(); + } + + public void EnableLoopback(string appName) + { + EnableLoopbackInternal(appName); + } + + protected abstract void EnableLoopbackInternal(string appName); + + private void RegisterModules() + { + var moduleTypes = GetExportTypes<IDependencyModule>(); + + foreach (var type in moduleTypes) + { + try + { + var instance = Activator.CreateInstance(type) as IDependencyModule; + if (instance != null) + instance.BindDependencies(this); + } + catch (Exception ex) + { + Logger.ErrorException("Error setting up dependency bindings for " + type.Name, ex); + } + } + } + + void IDependencyContainer.RegisterSingleInstance<T>(T obj, bool manageLifetime) + { + RegisterSingleInstance(obj, manageLifetime); + } + + void IDependencyContainer.RegisterSingleInstance<T>(Func<T> func) + { + RegisterSingleInstance(func); + } + + void IDependencyContainer.Register(Type typeInterface, Type typeImplementation) + { + Container.Register(typeInterface, typeImplementation); + } + + } +} diff --git a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Core/Configuration/ServerConfigurationManager.cs similarity index 98% rename from MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs rename to Emby.Server.Core/Configuration/ServerConfigurationManager.cs index e8669bbc2c..f98c096ca3 100644 --- a/MediaBrowser.Server.Implementations/Configuration/ServerConfigurationManager.cs +++ b/Emby.Server.Core/Configuration/ServerConfigurationManager.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Emby.Common.Implementations.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; -using MediaBrowser.Common.Implementations.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -10,14 +13,11 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Events; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; -using System; -using System.IO; -using System.Linq; -using CommonIO; -namespace MediaBrowser.Server.Implementations.Configuration +namespace Emby.Server.Core.Configuration { /// <summary> /// Class ServerConfigurationManager diff --git a/Emby.Server.Core/Emby.Server.Core.xproj b/Emby.Server.Core/Emby.Server.Core.xproj new file mode 100644 index 0000000000..00f7664bd6 --- /dev/null +++ b/Emby.Server.Core/Emby.Server.Core.xproj @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion> + <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> + </PropertyGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" /> + <PropertyGroup Label="Globals"> + <ProjectGuid>65aa7d67-8059-40cd-91f1-16d02687226c</ProjectGuid> + <RootNamespace>Emby.Server.Core</RootNamespace> + <BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath> + <OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath> + <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion> + </PropertyGroup> + <PropertyGroup> + <SchemaVersion>2.0</SchemaVersion> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\ServiceStack\ServiceStack.csproj" /> + <ProjectReference Include="..\Emby.Drawing\Emby.Drawing.csproj" /> + <ProjectReference Include="..\Emby.Photos\Emby.Photos.csproj" /> + <ProjectReference Include="..\MediaBrowser.Api\MediaBrowser.Api.csproj" /> + <ProjectReference Include="..\MediaBrowser.XbmcMetadata\MediaBrowser.XbmcMetadata.csproj" /> + <ProjectReference Include="..\MediaBrowser.LocalMetadata\MediaBrowser.LocalMetadata.csproj" /> + <ProjectReference Include="..\MediaBrowser.WebDashboard\MediaBrowser.WebDashboard.csproj" /> + <ProjectReference Include="..\MediaBrowser.MediaEncoding\MediaBrowser.MediaEncoding.csproj" /> + <ProjectReference Include="..\Emby.Dlna\Emby.Dlna.csproj" /> + <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> + <ProjectReference Include="..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + <ProjectReference Include="..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + <ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + </ItemGroup> + <Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" /> +</Project> \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs similarity index 84% rename from MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs rename to Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs index 3274231ee7..b75de2ff4f 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ExternalPortForwarding.cs +++ b/Emby.Server.Core/EntryPoints/ExternalPortForwarding.cs @@ -1,34 +1,39 @@ -using MediaBrowser.Controller; -using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; -using MediaBrowser.Controller.Plugins; -using MediaBrowser.Model.Logging; -using Mono.Nat; -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Net; -using MediaBrowser.Common.Threading; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Events; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Threading; +using Mono.Nat; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Core.EntryPoints { public class ExternalPortForwarding : IServerEntryPoint { private readonly IServerApplicationHost _appHost; private readonly ILogger _logger; + private readonly IHttpClient _httpClient; private readonly IServerConfigurationManager _config; private readonly IDeviceDiscovery _deviceDiscovery; - private PeriodicTimer _timer; + private ITimer _timer; private bool _isStarted; + private readonly ITimerFactory _timerFactory; - public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery) + public ExternalPortForwarding(ILogManager logmanager, IServerApplicationHost appHost, IServerConfigurationManager config, IDeviceDiscovery deviceDiscovery, IHttpClient httpClient, ITimerFactory timerFactory) { _logger = logmanager.GetLogger("PortMapper"); _appHost = appHost; _config = config; _deviceDiscovery = deviceDiscovery; + _httpClient = httpClient; + _timerFactory = timerFactory; } private string _lastConfigIdentifier; @@ -63,6 +68,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints public void Run() { NatUtility.Logger = _logger; + NatUtility.HttpClient = _httpClient; if (_config.Configuration.EnableUPnP) { @@ -87,12 +93,9 @@ namespace MediaBrowser.Server.Implementations.EntryPoints NatUtility.DeviceLost += NatUtility_DeviceLost; - // it is hard to say what one should do when an unhandled exception is raised - // because there isn't anything one can do about it. Probably save a log or ignored it. - NatUtility.UnhandledException += NatUtility_UnhandledException; NatUtility.StartDiscovery(); - _timer = new PeriodicTimer(ClearCreatedRules, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + _timer = _timerFactory.Create(ClearCreatedRules, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); _deviceDiscovery.DeviceDiscovered += _deviceDiscovery_DeviceDiscovered; @@ -136,7 +139,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _usnsHandled.Add(identifier); } - _logger.Debug("Calling Nat.Handle on " + identifier); + _logger.Debug("Found NAT device: " + identifier); IPAddress address; if (IPAddress.TryParse(info.Location.Host, out address)) @@ -150,16 +153,23 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { var localAddressString = await _appHost.GetLocalApiUrl().ConfigureAwait(false); - if (!IPAddress.TryParse(localAddressString, out localAddress)) + Uri uri; + if (Uri.TryCreate(localAddressString, UriKind.Absolute, out uri)) { - return; + localAddressString = uri.Host; + + if (!IPAddress.TryParse(localAddressString, out localAddress)) + { + return; + } } } - catch + catch (Exception ex) { return; } + _logger.Debug("Calling Nat.Handle on " + identifier); NatUtility.Handle(localAddress, info, endpoint, NatProtocol.Upnp); } } @@ -173,21 +183,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints } } - void NatUtility_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - var ex = e.ExceptionObject as Exception; - - if (ex == null) - { - //_logger.Error("Unidentified error reported by Mono.Nat"); - } - else - { - // Seeing some blank exceptions coming through here - //_logger.ErrorException("Error reported by Mono.Nat: ", ex); - } - } - void NatUtility_DeviceFound(object sender, DeviceEventArgs e) { try @@ -229,13 +224,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints } } - private void CreatePortMap(INatDevice device, int privatePort, int publicPort) + private async void CreatePortMap(INatDevice device, int privatePort, int publicPort) { _logger.Debug("Creating port map on port {0}", privatePort); - device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) + + try { - Description = _appHost.Name - }); + await device.CreatePortMap(new Mapping(Protocol.Tcp, privatePort, publicPort) + { + Description = _appHost.Name + + }).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.Error("Error creating port map: " + ex.Message); + } } // As I said before, this method will be never invoked. You can remove it. @@ -268,7 +272,6 @@ namespace MediaBrowser.Server.Implementations.EntryPoints NatUtility.StopDiscovery(); NatUtility.DeviceFound -= NatUtility_DeviceFound; NatUtility.DeviceLost -= NatUtility_DeviceLost; - NatUtility.UnhandledException -= NatUtility_UnhandledException; } // Statements in try-block will no fail because StopDiscovery is a one-line // method that was no chances to fail. diff --git a/Emby.Server.Core/HttpServerFactory.cs b/Emby.Server.Core/HttpServerFactory.cs new file mode 100644 index 0000000000..8ec5fb0269 --- /dev/null +++ b/Emby.Server.Core/HttpServerFactory.cs @@ -0,0 +1,109 @@ +using System; +using System.IO; +using System.Net.Security; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Emby.Common.Implementations.Net; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using ServiceStack.Text.Jsv; +using SocketHttpListener.Primitives; + +namespace Emby.Server.Core +{ + /// <summary> + /// Class ServerFactory + /// </summary> + public static class HttpServerFactory + { + /// <summary> + /// Creates the server. + /// </summary> + /// <returns>IHttpServer.</returns> + public static IHttpServer CreateServer(IServerApplicationHost applicationHost, + ILogManager logManager, + IServerConfigurationManager config, + INetworkManager networkmanager, + IMemoryStreamFactory streamProvider, + string serverName, + string defaultRedirectpath, + ITextEncoding textEncoding, + ISocketFactory socketFactory, + ICryptoProvider cryptoProvider, + IJsonSerializer json, + IXmlSerializer xml, + IEnvironmentInfo environment, + ICertificate certificate, + bool enableDualModeSockets) + { + var logger = logManager.GetLogger("HttpServer"); + + return new HttpListenerHost(applicationHost, + logger, + config, + serverName, + defaultRedirectpath, + networkmanager, + streamProvider, + textEncoding, + socketFactory, + cryptoProvider, + json, + xml, + environment, + certificate, + new StreamFactory(), + GetParseFn, + enableDualModeSockets); + } + + private static Func<string, object> GetParseFn(Type propertyType) + { + return s => JsvReader.GetParseFn(propertyType)(s); + } + } + + public class StreamFactory : IStreamFactory + { + public Stream CreateNetworkStream(ISocket socket, bool ownsSocket) + { + var netSocket = (NetSocket)socket; + + return new NetworkStream(netSocket.Socket, ownsSocket); + } + + public Task AuthenticateSslStreamAsServer(Stream stream, ICertificate certificate) + { + var sslStream = (SslStream)stream; + var cert = (Certificate)certificate; + + return sslStream.AuthenticateAsServerAsync(cert.X509Certificate); + } + + public Stream CreateSslStream(Stream innerStream, bool leaveInnerStreamOpen) + { + return new SslStream(innerStream, leaveInnerStreamOpen); + } + } + + public class Certificate : ICertificate + { + public Certificate(X509Certificate x509Certificate) + { + X509Certificate = x509Certificate; + } + + public X509Certificate X509Certificate { get; private set; } + } +} diff --git a/Emby.Server.Core/IO/LibraryMonitor.cs b/Emby.Server.Core/IO/LibraryMonitor.cs new file mode 100644 index 0000000000..6ed096f441 --- /dev/null +++ b/Emby.Server.Core/IO/LibraryMonitor.cs @@ -0,0 +1,631 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Threading; +using Emby.Server.Implementations.IO; + +namespace Emby.Server.Core.IO +{ + public class LibraryMonitor : ILibraryMonitor + { + /// <summary> + /// The file system watchers + /// </summary> + private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase); + /// <summary> + /// The affected paths + /// </summary> + private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>(); + + /// <summary> + /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. + /// </summary> + private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// Any file name ending in any of these will be ignored by the watchers + /// </summary> + private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string> + { + "small.jpg", + "albumart.jpg", + + // WMC temp recording directories that will constantly be written to + "TempRec", + "TempSBE" + }; + + private readonly IReadOnlyList<string> _alwaysIgnoreSubstrings = new List<string> + { + // Synology + "eaDir", + "#recycle", + ".wd_tv", + ".actors" + }; + + private readonly IReadOnlyList<string> _alwaysIgnoreExtensions = new List<string> + { + // thumbs.db + ".db", + + // bts sync files + ".bts", + ".sync" + }; + + /// <summary> + /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. + /// </summary> + /// <param name="path">The path.</param> + private void TemporarilyIgnore(string path) + { + _tempIgnoredPaths[path] = path; + } + + public void ReportFileSystemChangeBeginning(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + TemporarilyIgnore(path); + } + + public bool IsPathLocked(string path) + { + var lockedPaths = _tempIgnoredPaths.Keys.ToList(); + return lockedPaths.Any(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase) || _fileSystem.ContainsSubPath(i, path)); + } + + public async void ReportFileSystemChangeComplete(string path, bool refreshPath) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to. + // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds + // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata + await Task.Delay(45000).ConfigureAwait(false); + + string val; + _tempIgnoredPaths.TryRemove(path, out val); + + if (refreshPath) + { + try + { + ReportFileSystemChanged(path); + } + catch (Exception ex) + { + Logger.ErrorException("Error in ReportFileSystemChanged for {0}", ex, path); + } + } + } + + /// <summary> + /// Gets or sets the logger. + /// </summary> + /// <value>The logger.</value> + private ILogger Logger { get; set; } + + /// <summary> + /// Gets or sets the task manager. + /// </summary> + /// <value>The task manager.</value> + private ITaskManager TaskManager { get; set; } + + private ILibraryManager LibraryManager { get; set; } + private IServerConfigurationManager ConfigurationManager { get; set; } + + private readonly IFileSystem _fileSystem; + private readonly ITimerFactory _timerFactory; + private readonly IEnvironmentInfo _environmentInfo; + + /// <summary> + /// Initializes a new instance of the <see cref="LibraryMonitor" /> class. + /// </summary> + public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, ITimerFactory timerFactory, ISystemEvents systemEvents, IEnvironmentInfo environmentInfo) + { + if (taskManager == null) + { + throw new ArgumentNullException("taskManager"); + } + + LibraryManager = libraryManager; + TaskManager = taskManager; + Logger = logManager.GetLogger(GetType().Name); + ConfigurationManager = configurationManager; + _fileSystem = fileSystem; + _timerFactory = timerFactory; + _environmentInfo = environmentInfo; + + systemEvents.Resume += _systemEvents_Resume; + } + + private void _systemEvents_Resume(object sender, EventArgs e) + { + Restart(); + } + + private void Restart() + { + Stop(); + Start(); + } + + private bool IsLibraryMonitorEnabaled(BaseItem item) + { + if (item is BasePluginFolder) + { + return false; + } + + var options = LibraryManager.GetLibraryOptions(item); + + if (options != null) + { + return options.EnableRealtimeMonitor; + } + + return false; + } + + public void Start() + { + LibraryManager.ItemAdded += LibraryManager_ItemAdded; + LibraryManager.ItemRemoved += LibraryManager_ItemRemoved; + + var pathsToWatch = new List<string> { }; + + var paths = LibraryManager + .RootFolder + .Children + .Where(IsLibraryMonitorEnabaled) + .OfType<Folder>() + .SelectMany(f => f.PhysicalLocations) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) + .ToList(); + + foreach (var path in paths) + { + if (!ContainsParentFolder(pathsToWatch, path)) + { + pathsToWatch.Add(path); + } + } + + foreach (var path in pathsToWatch) + { + StartWatchingPath(path); + } + } + + private void StartWatching(BaseItem item) + { + if (IsLibraryMonitorEnabaled(item)) + { + StartWatchingPath(item.Path); + } + } + + /// <summary> + /// Handles the ItemRemoved event of the LibraryManager control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> + void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e) + { + if (e.Item.GetParent() is AggregateFolder) + { + StopWatchingPath(e.Item.Path); + } + } + + /// <summary> + /// Handles the ItemAdded event of the LibraryManager control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param> + void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e) + { + if (e.Item.GetParent() is AggregateFolder) + { + StartWatching(e.Item); + } + } + + /// <summary> + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// </summary> + /// <param name="lst">The LST.</param> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">path</exception> + private static bool ContainsParentFolder(IEnumerable<string> lst, string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentNullException("path"); + } + + path = path.TrimEnd(Path.DirectorySeparatorChar); + + return lst.Any(str => + { + //this should be a little quicker than examining each actual parent folder... + var compare = str.TrimEnd(Path.DirectorySeparatorChar); + + return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar); + }); + } + + /// <summary> + /// Starts the watching path. + /// </summary> + /// <param name="path">The path.</param> + private void StartWatchingPath(string path) + { + // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel + Task.Run(() => + { + try + { + var newWatcher = new FileSystemWatcher(path, "*") + { + IncludeSubdirectories = true + }; + + if (_environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows) + { + newWatcher.InternalBufferSize = 32767; + } + + newWatcher.NotifyFilter = NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastWrite | + NotifyFilters.Size | + NotifyFilters.Attributes; + + newWatcher.Created += watcher_Changed; + newWatcher.Deleted += watcher_Changed; + newWatcher.Renamed += watcher_Changed; + newWatcher.Changed += watcher_Changed; + + newWatcher.Error += watcher_Error; + + if (_fileSystemWatchers.TryAdd(path, newWatcher)) + { + newWatcher.EnableRaisingEvents = true; + Logger.Info("Watching directory " + path); + } + else + { + Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path); + newWatcher.Dispose(); + } + + } + catch (Exception ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + }); + } + + /// <summary> + /// Stops the watching path. + /// </summary> + /// <param name="path">The path.</param> + private void StopWatchingPath(string path) + { + FileSystemWatcher watcher; + + if (_fileSystemWatchers.TryGetValue(path, out watcher)) + { + DisposeWatcher(watcher); + } + } + + /// <summary> + /// Disposes the watcher. + /// </summary> + /// <param name="watcher">The watcher.</param> + private void DisposeWatcher(FileSystemWatcher watcher) + { + try + { + using (watcher) + { + Logger.Info("Stopping directory watching for path {0}", watcher.Path); + + watcher.EnableRaisingEvents = false; + } + } + catch + { + + } + finally + { + RemoveWatcherFromList(watcher); + } + } + + /// <summary> + /// Removes the watcher from list. + /// </summary> + /// <param name="watcher">The watcher.</param> + private void RemoveWatcherFromList(FileSystemWatcher watcher) + { + FileSystemWatcher removed; + + _fileSystemWatchers.TryRemove(watcher.Path, out removed); + } + + /// <summary> + /// Handles the Error event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param> + void watcher_Error(object sender, ErrorEventArgs e) + { + var ex = e.GetException(); + var dw = (FileSystemWatcher)sender; + + Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex); + + DisposeWatcher(dw); + } + + /// <summary> + /// Handles the Changed event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param> + void watcher_Changed(object sender, FileSystemEventArgs e) + { + try + { + Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath); + + var path = e.FullPath; + + // For deletes, use the parent path + if (e.ChangeType == WatcherChangeTypes.Deleted) + { + var parentPath = Path.GetDirectoryName(path); + + if (!string.IsNullOrWhiteSpace(parentPath)) + { + path = parentPath; + } + } + + ReportFileSystemChanged(path); + } + catch (Exception ex) + { + Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath); + } + } + + public void ReportFileSystemChanged(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + var filename = Path.GetFileName(path); + + var monitorPath = !string.IsNullOrEmpty(filename) && + !_alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase) && + !_alwaysIgnoreExtensions.Contains(Path.GetExtension(path) ?? string.Empty, StringComparer.OrdinalIgnoreCase) && + _alwaysIgnoreSubstrings.All(i => path.IndexOf(i, StringComparison.OrdinalIgnoreCase) == -1); + + // Ignore certain files + var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList(); + + // If the parent of an ignored path has a change event, ignore that too + if (tempIgnorePaths.Any(i => + { + if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + if (_fileSystem.ContainsSubPath(i, path)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + + // Go up a level + var parent = Path.GetDirectoryName(i); + if (!string.IsNullOrEmpty(parent)) + { + if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase)) + { + Logger.Debug("Ignoring change to {0}", path); + return true; + } + } + + return false; + + })) + { + monitorPath = false; + } + + if (monitorPath) + { + // Avoid implicitly captured closure + CreateRefresher(path); + } + } + + private void CreateRefresher(string path) + { + var parentPath = Path.GetDirectoryName(path); + + lock (_activeRefreshers) + { + var refreshers = _activeRefreshers.ToList(); + foreach (var refresher in refreshers) + { + // Path is already being refreshed + if (string.Equals(path, refresher.Path, StringComparison.Ordinal)) + { + refresher.RestartTimer(); + return; + } + + // Parent folder is already being refreshed + if (_fileSystem.ContainsSubPath(refresher.Path, path)) + { + refresher.AddPath(path); + return; + } + + // New path is a parent + if (_fileSystem.ContainsSubPath(path, refresher.Path)) + { + refresher.ResetPath(path, null); + return; + } + + // They are siblings. Rebase the refresher to the parent folder. + if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal)) + { + refresher.ResetPath(parentPath, path); + return; + } + } + + var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger, _timerFactory, _environmentInfo); + newRefresher.Completed += NewRefresher_Completed; + _activeRefreshers.Add(newRefresher); + } + } + + private void NewRefresher_Completed(object sender, EventArgs e) + { + var refresher = (FileRefresher)sender; + DisposeRefresher(refresher); + } + + /// <summary> + /// Stops this instance. + /// </summary> + public void Stop() + { + LibraryManager.ItemAdded -= LibraryManager_ItemAdded; + LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved; + + foreach (var watcher in _fileSystemWatchers.Values.ToList()) + { + watcher.Created -= watcher_Changed; + watcher.Deleted -= watcher_Changed; + watcher.Renamed -= watcher_Changed; + watcher.Changed -= watcher_Changed; + + try + { + watcher.EnableRaisingEvents = false; + } + catch (InvalidOperationException) + { + // Seeing this under mono on linux sometimes + // Collection was modified; enumeration operation may not execute. + } + + watcher.Dispose(); + } + + _fileSystemWatchers.Clear(); + DisposeRefreshers(); + } + + private void DisposeRefresher(FileRefresher refresher) + { + lock (_activeRefreshers) + { + refresher.Dispose(); + _activeRefreshers.Remove(refresher); + } + } + + private void DisposeRefreshers() + { + lock (_activeRefreshers) + { + foreach (var refresher in _activeRefreshers.ToList()) + { + refresher.Dispose(); + } + _activeRefreshers.Clear(); + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + Stop(); + } + } + } + + public class LibraryMonitorStartup : IServerEntryPoint + { + private readonly ILibraryMonitor _monitor; + + public LibraryMonitorStartup(ILibraryMonitor monitor) + { + _monitor = monitor; + } + + public void Run() + { + _monitor.Start(); + } + + public void Dispose() + { + } + } +} diff --git a/Emby.Server.Core/Localization/TextLocalizer.cs b/Emby.Server.Core/Localization/TextLocalizer.cs new file mode 100644 index 0000000000..6690c62638 --- /dev/null +++ b/Emby.Server.Core/Localization/TextLocalizer.cs @@ -0,0 +1,56 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Emby.Server.Implementations.Localization; + +namespace Emby.Server.Core.Localization +{ + public class TextLocalizer : ITextLocalizer + { + public string RemoveDiacritics(string text) + { + if (text == null) + { + throw new ArgumentNullException("text"); + } + + var chars = Normalize(text, NormalizationForm.FormD) + .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark); + + return Normalize(String.Concat(chars), NormalizationForm.FormC); + } + + private static string Normalize(string text, NormalizationForm form, bool stripStringOnFailure = true) + { + if (stripStringOnFailure) + { + try + { + return text.Normalize(form); + } + catch (ArgumentException) + { + // will throw if input contains invalid unicode chars + // https://mnaoumov.wordpress.com/2014/06/14/stripping-invalid-characters-from-utf-16-strings/ + text = StripInvalidUnicodeCharacters(text); + return Normalize(text, form, false); + } + } + + return text.Normalize(form); + } + + private static string StripInvalidUnicodeCharacters(string str) + { + var invalidCharactersRegex = new Regex("([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])"); + return invalidCharactersRegex.Replace(str, ""); + } + + public string NormalizeFormKD(string text) + { + return text.Normalize(NormalizationForm.FormKD); + } + } +} diff --git a/Emby.Server.Core/Properties/AssemblyInfo.cs b/Emby.Server.Core/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..69df3d58db --- /dev/null +++ b/Emby.Server.Core/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +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: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Emby.Server.Core")] +[assembly: AssemblyTrademark("")] + +// 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)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("65aa7d67-8059-40cd-91f1-16d02687226c")] diff --git a/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs b/Emby.Server.Core/ServerApplicationPaths.cs similarity index 96% rename from MediaBrowser.Server.Implementations/ServerApplicationPaths.cs rename to Emby.Server.Core/ServerApplicationPaths.cs index 237d49fdae..dc80b773c5 100644 --- a/MediaBrowser.Server.Implementations/ServerApplicationPaths.cs +++ b/Emby.Server.Core/ServerApplicationPaths.cs @@ -1,8 +1,8 @@ -using MediaBrowser.Common.Implementations; +using System.IO; +using Emby.Common.Implementations; using MediaBrowser.Controller; -using System.IO; -namespace MediaBrowser.Server.Implementations +namespace Emby.Server.Core { /// <summary> /// Extends BaseApplicationPaths to add paths that are only applicable on the server @@ -12,8 +12,8 @@ namespace MediaBrowser.Server.Implementations /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths" /> class. /// </summary> - public ServerApplicationPaths(string programDataPath, string applicationPath, string applicationResourcesPath) - : base(programDataPath, applicationPath) + public ServerApplicationPaths(string programDataPath, string appFolderPath, string applicationResourcesPath) + : base(programDataPath, appFolderPath) { ApplicationResourcesPath = applicationResourcesPath; } diff --git a/MediaBrowser.Server.Startup.Common/UnhandledExceptionWriter.cs b/Emby.Server.Core/UnhandledExceptionWriter.cs similarity index 92% rename from MediaBrowser.Server.Startup.Common/UnhandledExceptionWriter.cs rename to Emby.Server.Core/UnhandledExceptionWriter.cs index 804533b9d1..5147be9e7c 100644 --- a/MediaBrowser.Server.Startup.Common/UnhandledExceptionWriter.cs +++ b/Emby.Server.Core/UnhandledExceptionWriter.cs @@ -1,10 +1,9 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Implementations.Logging; using MediaBrowser.Model.Logging; using System; using System.IO; -namespace MediaBrowser.Server.Startup.Common +namespace Emby.Server.Core { public class UnhandledExceptionWriter { diff --git a/Emby.Server.Core/project.json b/Emby.Server.Core/project.json new file mode 100644 index 0000000000..e987da6aa9 --- /dev/null +++ b/Emby.Server.Core/project.json @@ -0,0 +1,131 @@ +{ + "version": "1.0.0-*", + + "dependencies": { + + }, + + "frameworks": { + "net46": { + "frameworkAssemblies": { + "System.Runtime": "4.0.0" + }, + "dependencies": { + "MediaBrowser.Model": { + "target": "project" + }, + "MediaBrowser.Common": { + "target": "project" + }, + "MediaBrowser.Controller": { + "target": "project" + }, + "Emby.Common.Implementations": { + "target": "project" + }, + "Mono.Nat": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" + }, + "MediaBrowser.Server.Implementations": { + "target": "project" + }, + "Emby.Dlna": { + "target": "project" + }, + "Emby.Photos": { + "target": "project" + }, + "MediaBrowser.Api": { + "target": "project" + }, + "MediaBrowser.MediaEncoding": { + "target": "project" + }, + "MediaBrowser.XbmcMetadata": { + "target": "project" + }, + "MediaBrowser.LocalMetadata": { + "target": "project" + }, + "MediaBrowser.WebDashboard": { + "target": "project" + }, + "Emby.Drawing": { + "target": "project" + }, + "ServiceStack": { + "target": "project" + }, + "SocketHttpListener.Portable": { + "target": "project" + } + } + }, + "netstandard1.6": { + "imports": "dnxcore50", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.AppDomain": "2.0.11", + "System.Globalization.Extensions": "4.3.0", + "System.IO.FileSystem.Watcher": "4.3.0", + "System.Net.Security": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "MediaBrowser.Model": { + "target": "project" + }, + "MediaBrowser.Common": { + "target": "project" + }, + "MediaBrowser.Controller": { + "target": "project" + }, + "Emby.Common.Implementations": { + "target": "project" + }, + "Mono.Nat": { + "target": "project" + }, + "Emby.Server.Implementations": { + "target": "project" + }, + "MediaBrowser.Server.Implementations": { + "target": "project" + }, + "Emby.Dlna": { + "target": "project" + }, + "Emby.Photos": { + "target": "project" + }, + "MediaBrowser.Api": { + "target": "project" + }, + "MediaBrowser.MediaEncoding": { + "target": "project" + }, + "MediaBrowser.XbmcMetadata": { + "target": "project" + }, + "MediaBrowser.LocalMetadata": { + "target": "project" + }, + "MediaBrowser.WebDashboard": { + "target": "project" + }, + "Emby.Drawing": { + "target": "project" + }, + "SocketHttpListener.Portable": { + "target": "project" + }, + "ServiceStack": { + "target": "project" + } + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs similarity index 96% rename from MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs rename to Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index a36583a412..11fd3a872e 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -1,14 +1,10 @@ using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Implementations.Logging; using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; -using MediaBrowser.Controller.Activity; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Subtitles; @@ -21,8 +17,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using MediaBrowser.Model.Globalization; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.Activity { public class ActivityLogEntryPoint : IServerEntryPoint { @@ -126,8 +123,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints return; } - var themeMedia = item as IThemeMedia; - if (themeMedia != null && themeMedia.IsThemeMedia) + if (item.IsThemeMedia) { // Don't report theme song or local trailer playback return; @@ -159,8 +155,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints return; } - var themeMedia = item as IThemeMedia; - if (themeMedia != null && themeMedia.IsThemeMedia) + if (item.IsThemeMedia) { // Don't report theme song or local trailer playback return; @@ -401,8 +396,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { var task = e.Argument; - var activityTask = task.ScheduledTask as IScheduledTaskActivityLog; - if (activityTask != null && !activityTask.IsActivityLogged) + var activityTask = task.ScheduledTask as IConfigurableScheduledTask; + if (activityTask != null && !activityTask.IsLogged) { return; } @@ -419,8 +414,8 @@ namespace MediaBrowser.Server.Implementations.EntryPoints var result = e.Result; var task = e.Task; - var activityTask = task.ScheduledTask as IScheduledTaskActivityLog; - if (activityTask != null && !activityTask.IsActivityLogged) + var activityTask = task.ScheduledTask as IConfigurableScheduledTask; + if (activityTask != null && !activityTask.IsLogged) { return; } diff --git a/MediaBrowser.Server.Implementations/Activity/ActivityManager.cs b/Emby.Server.Implementations/Activity/ActivityManager.cs similarity index 94% rename from MediaBrowser.Server.Implementations/Activity/ActivityManager.cs rename to Emby.Server.Implementations/Activity/ActivityManager.cs index 0904c92f1c..b6095f0820 100644 --- a/MediaBrowser.Server.Implementations/Activity/ActivityManager.cs +++ b/Emby.Server.Implementations/Activity/ActivityManager.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Events; -using MediaBrowser.Controller.Activity; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Activity; using MediaBrowser.Model.Events; @@ -9,7 +8,7 @@ using System; using System.Linq; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Activity +namespace Emby.Server.Implementations.Activity { public class ActivityManager : IActivityManager { diff --git a/Emby.Server.Implementations/Activity/ActivityRepository.cs b/Emby.Server.Implementations/Activity/ActivityRepository.cs new file mode 100644 index 0000000000..bf88358465 --- /dev/null +++ b/Emby.Server.Implementations/Activity/ActivityRepository.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Emby.Server.Implementations.Data; +using MediaBrowser.Controller; +using MediaBrowser.Model.Activity; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Activity +{ + public class ActivityRepository : BaseSqliteRepository, IActivityRepository + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public ActivityRepository(ILogger logger, IServerApplicationPaths appPaths) + : base(logger) + { + DbFilePath = Path.Combine(appPaths.DataPath, "activitylog.db"); + } + + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + string[] queries = { + "create table if not exists ActivityLogEntries (Id GUID PRIMARY KEY, Name TEXT, Overview TEXT, ShortOverview TEXT, Type TEXT, ItemId TEXT, UserId TEXT, DateCreated DATETIME, LogSeverity TEXT)", + "create index if not exists idx_ActivityLogEntries on ActivityLogEntries(Id)" + }; + + connection.RunQueries(queries); + } + } + + private const string BaseActivitySelectText = "select Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity from ActivityLogEntries"; + + public Task Create(ActivityLogEntry entry) + { + return Update(entry); + } + + public async Task Update(ActivityLogEntry entry) + { + if (entry == null) + { + throw new ArgumentNullException("entry"); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("replace into ActivityLogEntries (Id, Name, Overview, ShortOverview, Type, ItemId, UserId, DateCreated, LogSeverity) values (@Id, @Name, @Overview, @ShortOverview, @Type, @ItemId, @UserId, @DateCreated, @LogSeverity)")) + { + statement.TryBind("@Id", entry.Id.ToGuidParamValue()); + statement.TryBind("@Name", entry.Name); + + statement.TryBind("@Overview", entry.Overview); + statement.TryBind("@ShortOverview", entry.ShortOverview); + statement.TryBind("@Type", entry.Type); + statement.TryBind("@ItemId", entry.ItemId); + statement.TryBind("@UserId", entry.UserId); + statement.TryBind("@DateCreated", entry.Date.ToDateTimeParamValue()); + statement.TryBind("@LogSeverity", entry.Severity.ToString()); + + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + + public QueryResult<ActivityLogEntry> GetActivityLogEntries(DateTime? minDate, int? startIndex, int? limit) + { + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + var commandText = BaseActivitySelectText; + var whereClauses = new List<string>(); + + if (minDate.HasValue) + { + whereClauses.Add("DateCreated>=@DateCreated"); + } + + var whereTextWithoutPaging = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + if (startIndex.HasValue && startIndex.Value > 0) + { + var pagingWhereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + whereClauses.Add(string.Format("Id NOT IN (SELECT Id FROM ActivityLogEntries {0} ORDER BY DateCreated DESC LIMIT {1})", + pagingWhereText, + startIndex.Value.ToString(_usCulture))); + } + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + commandText += " ORDER BY DateCreated DESC"; + + if (limit.HasValue) + { + commandText += " LIMIT " + limit.Value.ToString(_usCulture); + } + + var statementTexts = new List<string>(); + statementTexts.Add(commandText); + statementTexts.Add("select count (Id) from ActivityLogEntries" + whereTextWithoutPaging); + + return connection.RunInTransaction(db => + { + var list = new List<ActivityLogEntry>(); + var result = new QueryResult<ActivityLogEntry>(); + + var statements = PrepareAllSafe(db, statementTexts).ToList(); + + using (var statement = statements[0]) + { + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetEntry(row)); + } + } + + using (var statement = statements[1]) + { + if (minDate.HasValue) + { + statement.TryBind("@DateCreated", minDate.Value.ToDateTimeParamValue()); + } + + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } + + result.Items = list.ToArray(); + return result; + + }, ReadTransactionMode); + } + } + } + + private ActivityLogEntry GetEntry(IReadOnlyList<IResultSetValue> reader) + { + var index = 0; + + var info = new ActivityLogEntry + { + Id = reader[index].ReadGuid().ToString("N") + }; + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.Name = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.Overview = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.ShortOverview = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.Type = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.ItemId = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.UserId = reader[index].ToString(); + } + + index++; + info.Date = reader[index].ReadDateTime(); + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + info.Severity = (LogSeverity)Enum.Parse(typeof(LogSeverity), reader[index].ToString(), true); + } + + return info; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Branding/BrandingConfigurationFactory.cs b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs similarity index 90% rename from MediaBrowser.Server.Implementations/Branding/BrandingConfigurationFactory.cs rename to Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs index d6cd3424b1..a29f55f168 100644 --- a/MediaBrowser.Server.Implementations/Branding/BrandingConfigurationFactory.cs +++ b/Emby.Server.Implementations/Branding/BrandingConfigurationFactory.cs @@ -2,7 +2,7 @@ using MediaBrowser.Model.Branding; using System.Collections.Generic; -namespace MediaBrowser.Server.Implementations.Branding +namespace Emby.Server.Implementations.Branding { public class BrandingConfigurationFactory : IConfigurationFactory { diff --git a/MediaBrowser.Server.Startup.Common/Browser/BrowserLauncher.cs b/Emby.Server.Implementations/Browser/BrowserLauncher.cs similarity index 90% rename from MediaBrowser.Server.Startup.Common/Browser/BrowserLauncher.cs rename to Emby.Server.Implementations/Browser/BrowserLauncher.cs index 1a0e2d9731..05cde91e22 100644 --- a/MediaBrowser.Server.Startup.Common/Browser/BrowserLauncher.cs +++ b/Emby.Server.Implementations/Browser/BrowserLauncher.cs @@ -1,7 +1,7 @@ using MediaBrowser.Controller; using System; -namespace MediaBrowser.Server.Startup.Common.Browser +namespace Emby.Server.Implementations.Browser { /// <summary> /// Class BrowserLauncher @@ -65,10 +65,8 @@ namespace MediaBrowser.Server.Startup.Common.Browser { } - catch (Exception ex) + catch (Exception) { - Console.WriteLine("Error launching url: " + url); - Console.WriteLine(ex.Message); } } } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelConfigurations.cs b/Emby.Server.Implementations/Channels/ChannelConfigurations.cs similarity index 93% rename from MediaBrowser.Server.Implementations/Channels/ChannelConfigurations.cs rename to Emby.Server.Implementations/Channels/ChannelConfigurations.cs index 9dfb0404e0..ef0973e7f4 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelConfigurations.cs +++ b/Emby.Server.Implementations/Channels/ChannelConfigurations.cs @@ -2,7 +2,7 @@ using MediaBrowser.Model.Configuration; using System.Collections.Generic; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { public static class ChannelConfigurationExtension { diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs similarity index 96% rename from MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs rename to Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs index fae78b9bc0..98011ddd48 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs @@ -7,7 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { public class ChannelDynamicMediaSourceProvider : IMediaSourceProvider { diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs similarity index 96% rename from MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs rename to Emby.Server.Implementations/Channels/ChannelImageProvider.cs index c98f71ce2a..f892b1e620 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/Emby.Server.Implementations/Channels/ChannelImageProvider.cs @@ -7,7 +7,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { public class ChannelImageProvider : IDynamicImageProvider, IHasItemChangeMonitor { diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs similarity index 95% rename from MediaBrowser.Server.Implementations/Channels/ChannelManager.cs rename to Emby.Server.Implementations/Channels/ChannelManager.cs index 1369efae1b..0df916ded3 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -5,7 +5,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Localization; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; @@ -24,12 +23,15 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Globalization; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { public class ChannelManager : IChannelManager { @@ -250,6 +252,42 @@ namespace MediaBrowser.Server.Implementations.Channels return item; } + private List<ChannelMediaInfo> GetSavedMediaSources(BaseItem item) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasources.json"); + + try + { + return _jsonSerializer.DeserializeFromFile<List<ChannelMediaInfo>>(path) ?? new List<ChannelMediaInfo>(); + } + catch + { + return new List<ChannelMediaInfo>(); + } + } + + private void SaveMediaSources(BaseItem item, List<ChannelMediaInfo> mediaSources) + { + var path = Path.Combine(item.GetInternalMetadataPath(), "channelmediasources.json"); + + if (mediaSources == null || mediaSources.Count == 0) + { + try + { + _fileSystem.DeleteFile(path); + } + catch + { + + } + return; + } + + _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); + + _jsonSerializer.SerializeToFile(mediaSources, path); + } + public async Task<IEnumerable<MediaSourceInfo>> GetStaticMediaSources(BaseItem item, bool includeCachedVersions, CancellationToken cancellationToken) { IEnumerable<ChannelMediaInfo> results = new List<ChannelMediaInfo>(); @@ -261,7 +299,7 @@ namespace MediaBrowser.Server.Implementations.Channels var audio = item as Audio; if (audio != null) { - results = audio.ChannelMediaSources ?? new List<ChannelMediaInfo>(); + results = audio.ChannelMediaSources ?? GetSavedMediaSources(audio); } var sources = SortMediaInfoResults(results) @@ -288,7 +326,7 @@ namespace MediaBrowser.Server.Implementations.Channels if (requiresCallback != null) { - results = await GetChannelItemMediaSourcesInternal(requiresCallback, item.ExternalId, cancellationToken) + results = await GetChannelItemMediaSourcesInternal(requiresCallback, GetItemExternalId(item), cancellationToken) .ConfigureAwait(false); } else @@ -372,7 +410,7 @@ namespace MediaBrowser.Server.Implementations.Channels } } } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -743,7 +781,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -763,7 +801,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -905,7 +943,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -925,7 +963,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -1037,6 +1075,18 @@ namespace MediaBrowser.Server.Implementations.Channels return result; } + private string GetItemExternalId(BaseItem item) + { + var externalId = item.ExternalId; + + if (string.IsNullOrWhiteSpace(externalId)) + { + externalId = item.GetProviderId("ProviderExternalId"); + } + + return externalId; + } + private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private async Task<ChannelItemResult> GetChannelItems(IChannel channel, User user, @@ -1058,7 +1108,11 @@ namespace MediaBrowser.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + if (cachedResult != null) + { + return cachedResult; + } } } } @@ -1066,7 +1120,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -1081,7 +1135,11 @@ namespace MediaBrowser.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - return _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + var cachedResult = _jsonSerializer.DeserializeFromFile<ChannelItemResult>(cachePath); + if (cachedResult != null) + { + return cachedResult; + } } } } @@ -1089,7 +1147,7 @@ namespace MediaBrowser.Server.Implementations.Channels { } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -1107,11 +1165,16 @@ namespace MediaBrowser.Server.Implementations.Channels { var categoryItem = _libraryManager.GetItemById(new Guid(folderId)); - query.FolderId = categoryItem.ExternalId; + query.FolderId = GetItemExternalId(categoryItem); } var result = await channel.GetChannelItems(query, cancellationToken).ConfigureAwait(false); + if (result == null) + { + throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); + } + if (!startIndex.HasValue && !limit.HasValue) { CacheResponse(result, cachePath); @@ -1383,7 +1446,6 @@ namespace MediaBrowser.Server.Implementations.Channels if (channelAudioItem != null) { channelAudioItem.ExtraType = info.ExtraType; - channelAudioItem.ChannelMediaSources = info.MediaSources; var mediaSource = info.MediaSources.FirstOrDefault(); item.Path = mediaSource == null ? null : mediaSource.Path; @@ -1424,6 +1486,8 @@ namespace MediaBrowser.Server.Implementations.Channels await item.UpdateToRepository(ItemUpdateType.None, cancellationToken).ConfigureAwait(false); } + SaveMediaSources(item, info.MediaSources); + return item; } diff --git a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs similarity index 99% rename from MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs rename to Emby.Server.Implementations/Channels/ChannelPostScanTask.cs index b25c9c8180..aef06bd1d6 100644 --- a/MediaBrowser.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs @@ -4,14 +4,14 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Logging; -using MoreLinq; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { public class ChannelPostScanTask { diff --git a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs similarity index 72% rename from MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs rename to Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index 5ac3da3db6..d5ec86445d 100644 --- a/MediaBrowser.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -1,14 +1,14 @@ -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; using System.Threading.Tasks; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.Channels +namespace Emby.Server.Implementations.Channels { - class RefreshChannelsScheduledTask : IScheduledTask, IConfigurableScheduledTask + class RefreshChannelsScheduledTask : IScheduledTask { private readonly IChannelManager _channelManager; private readonly IUserManager _userManager; @@ -48,14 +48,23 @@ namespace MediaBrowser.Server.Implementations.Channels .ConfigureAwait(false); } - public IEnumerable<ITaskTrigger> GetDefaultTriggers() + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new ITaskTrigger[] - { - new IntervalTrigger{ Interval = TimeSpan.FromHours(24)} + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; } + public string Key + { + get { return "RefreshInternetChannels"; } + } + public bool IsHidden { get { return false; } diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs similarity index 94% rename from MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs rename to Emby.Server.Implementations/Collections/CollectionImageProvider.cs index 25393d30f6..b82d4e44e0 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionImageProvider.cs +++ b/Emby.Server.Implementations/Collections/CollectionImageProvider.cs @@ -6,14 +6,14 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -using MediaBrowser.Server.Implementations.Photos; -using MoreLinq; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using CommonIO; +using Emby.Server.Implementations.Images; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Server.Implementations.Collections +namespace Emby.Server.Implementations.Collections { public class CollectionImageProvider : BaseDynamicImageProvider<BoxSet> { diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs similarity index 99% rename from MediaBrowser.Server.Implementations/Collections/CollectionManager.cs rename to Emby.Server.Implementations/Collections/CollectionManager.cs index cb2bd645dd..d0bd76c354 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -11,9 +11,9 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Server.Implementations.Collections +namespace Emby.Server.Implementations.Collections { public class CollectionManager : ICollectionManager { diff --git a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs b/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs similarity index 84% rename from MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs rename to Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs index 50bb6c5592..4ff33e6451 100644 --- a/MediaBrowser.Server.Implementations/Collections/CollectionsDynamicFolder.cs +++ b/Emby.Server.Implementations/Collections/CollectionsDynamicFolder.cs @@ -1,10 +1,12 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Entities; using System.IO; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Collections; +using MediaBrowser.Controller.IO; -namespace MediaBrowser.Server.Implementations.Collections +namespace Emby.Server.Implementations.Collections { public class CollectionsDynamicFolder : IVirtualFolderCreator { diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectData.cs b/Emby.Server.Implementations/Connect/ConnectData.cs similarity index 95% rename from MediaBrowser.Server.Implementations/Connect/ConnectData.cs rename to Emby.Server.Implementations/Connect/ConnectData.cs index 5ec0bea22c..41b89ce52b 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectData.cs +++ b/Emby.Server.Implementations/Connect/ConnectData.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace MediaBrowser.Server.Implementations.Connect +namespace Emby.Server.Implementations.Connect { public class ConnectData { diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs b/Emby.Server.Implementations/Connect/ConnectEntryPoint.cs similarity index 84% rename from MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs rename to Emby.Server.Implementations/Connect/ConnectEntryPoint.cs index f9eff3c92f..170ef07f31 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectEntryPoint.cs +++ b/Emby.Server.Implementations/Connect/ConnectEntryPoint.cs @@ -7,18 +7,16 @@ using MediaBrowser.Model.Logging; using MediaBrowser.Model.Net; using System; using System.IO; -using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading.Tasks; -using CommonIO; -using MediaBrowser.Common.Threading; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.Connect +namespace Emby.Server.Implementations.Connect { public class ConnectEntryPoint : IServerEntryPoint { - private PeriodicTimer _timer; + private ITimer _timer; private readonly IHttpClient _httpClient; private readonly IApplicationPaths _appPaths; private readonly ILogger _logger; @@ -27,8 +25,9 @@ namespace MediaBrowser.Server.Implementations.Connect private readonly INetworkManager _networkManager; private readonly IApplicationHost _appHost; private readonly IFileSystem _fileSystem; + private readonly ITimerFactory _timerFactory; - public ConnectEntryPoint(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, INetworkManager networkManager, IConnectManager connectManager, IApplicationHost appHost, IFileSystem fileSystem) + public ConnectEntryPoint(IHttpClient httpClient, IApplicationPaths appPaths, ILogger logger, INetworkManager networkManager, IConnectManager connectManager, IApplicationHost appHost, IFileSystem fileSystem, ITimerFactory timerFactory) { _httpClient = httpClient; _appPaths = appPaths; @@ -37,13 +36,14 @@ namespace MediaBrowser.Server.Implementations.Connect _connectManager = connectManager; _appHost = appHost; _fileSystem = fileSystem; + _timerFactory = timerFactory; } public void Run() { LoadCachedAddress(); - _timer = new PeriodicTimer(TimerCallback, null, TimeSpan.FromSeconds(5), TimeSpan.FromHours(1)); + _timer = _timerFactory.Create(TimerCallback, null, TimeSpan.FromSeconds(5), TimeSpan.FromHours(1)); ((ConnectManager)_connectManager).Start(); } @@ -55,7 +55,7 @@ namespace MediaBrowser.Server.Implementations.Connect private async void TimerCallback(object state) { - IPAddress validIpAddress = null; + IpAddressInfo validIpAddress = null; foreach (var ipLookupUrl in _ipLookups) { @@ -64,7 +64,7 @@ namespace MediaBrowser.Server.Implementations.Connect validIpAddress = await GetIpAddress(ipLookupUrl).ConfigureAwait(false); // Try to find the ipv4 address, if present - if (validIpAddress.AddressFamily == AddressFamily.InterNetwork) + if (validIpAddress.AddressFamily != IpAddressFamily.InterNetworkV6) { break; } @@ -77,9 +77,9 @@ namespace MediaBrowser.Server.Implementations.Connect _logger.ErrorException("Error getting connection info", ex); } } - + // If this produced an ipv6 address, try again - if (validIpAddress != null && validIpAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (validIpAddress != null && validIpAddress.AddressFamily == IpAddressFamily.InterNetworkV6) { foreach (var ipLookupUrl in _ipLookups) { @@ -88,7 +88,7 @@ namespace MediaBrowser.Server.Implementations.Connect var newAddress = await GetIpAddress(ipLookupUrl, true).ConfigureAwait(false); // Try to find the ipv4 address, if present - if (newAddress.AddressFamily == AddressFamily.InterNetwork) + if (newAddress.AddressFamily != IpAddressFamily.InterNetworkV6) { validIpAddress = newAddress; break; @@ -111,7 +111,7 @@ namespace MediaBrowser.Server.Implementations.Connect } } - private async Task<IPAddress> GetIpAddress(string lookupUrl, bool preferIpv4 = false) + private async Task<IpAddressInfo> GetIpAddress(string lookupUrl, bool preferIpv4 = false) { // Sometimes whatismyipaddress might fail, but it won't do us any good having users raise alarms over it. var logErrors = false; @@ -136,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.Connect { var addressString = await reader.ReadToEndAsync().ConfigureAwait(false); - return IPAddress.Parse(addressString); + return _networkManager.ParseIpAddress(addressString); } } } @@ -146,7 +146,7 @@ namespace MediaBrowser.Server.Implementations.Connect get { return Path.Combine(_appPaths.DataPath, "wan.txt"); } } - private void CacheAddress(IPAddress address) + private void CacheAddress(IpAddressInfo address) { var path = CacheFilePath; @@ -170,9 +170,9 @@ namespace MediaBrowser.Server.Implementations.Connect try { var endpoint = _fileSystem.ReadAllText(path, Encoding.UTF8); - IPAddress ipAddress; + IpAddressInfo ipAddress; - if (IPAddress.TryParse(endpoint, out ipAddress)) + if (_networkManager.TryParseIpAddress(endpoint, out ipAddress)) { ((ConnectManager)_connectManager).OnWanAddressResolved(ipAddress); } diff --git a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs b/Emby.Server.Implementations/Connect/ConnectManager.cs similarity index 98% rename from MediaBrowser.Server.Implementations/Connect/ConnectManager.cs rename to Emby.Server.Implementations/Connect/ConnectManager.cs index d7c1b0da07..b7faaa9013 100644 --- a/MediaBrowser.Server.Implementations/Connect/ConnectManager.cs +++ b/Emby.Server.Implementations/Connect/ConnectManager.cs @@ -19,14 +19,13 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; -using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Common.Extensions; -namespace MediaBrowser.Server.Implementations.Connect +namespace Emby.Server.Implementations.Connect { public class ConnectManager : IConnectManager { @@ -55,7 +54,7 @@ namespace MediaBrowser.Server.Implementations.Connect get { return _data.AccessKey; } } - private IPAddress DiscoveredWanIpAddress { get; set; } + private IpAddressInfo DiscoveredWanIpAddress { get; set; } public string WanIpAddress { @@ -75,7 +74,7 @@ namespace MediaBrowser.Server.Implementations.Connect if (string.IsNullOrWhiteSpace(address) && DiscoveredWanIpAddress != null) { - if (DiscoveredWanIpAddress.AddressFamily == AddressFamily.InterNetworkV6) + if (DiscoveredWanIpAddress.AddressFamily == IpAddressFamily.InterNetworkV6) { address = "[" + DiscoveredWanIpAddress + "]"; } @@ -146,7 +145,7 @@ namespace MediaBrowser.Server.Implementations.Connect _config.ConfigurationUpdated += _config_ConfigurationUpdated; } - internal void OnWanAddressResolved(IPAddress address) + internal void OnWanAddressResolved(IpAddressInfo address) { DiscoveredWanIpAddress = address; @@ -818,7 +817,6 @@ namespace MediaBrowser.Server.Implementations.Connect } } - private readonly SemaphoreSlim _connectImageSemaphore = new SemaphoreSlim(5, 5); private async Task RefreshAuthorizations(List<ServerUserAuthorizationResponse> list, bool refreshImages) { var users = _userManager.Users.ToList(); @@ -993,7 +991,7 @@ namespace MediaBrowser.Server.Implementations.Connect if (changed) { - await _providerManager.SaveImage(user, imageUrl, _connectImageSemaphore, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false); + await _providerManager.SaveImage(user, imageUrl, null, ImageType.Primary, null, CancellationToken.None).ConfigureAwait(false); await user.RefreshMetadata(new MetadataRefreshOptions(_fileSystem) { diff --git a/MediaBrowser.Server.Implementations/Connect/Responses.cs b/Emby.Server.Implementations/Connect/Responses.cs similarity index 98% rename from MediaBrowser.Server.Implementations/Connect/Responses.cs rename to Emby.Server.Implementations/Connect/Responses.cs index f865278294..87cb6cdf93 100644 --- a/MediaBrowser.Server.Implementations/Connect/Responses.cs +++ b/Emby.Server.Implementations/Connect/Responses.cs @@ -1,7 +1,7 @@ using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Connect; -namespace MediaBrowser.Server.Implementations.Connect +namespace Emby.Server.Implementations.Connect { public class ServerRegistrationResponse { diff --git a/MediaBrowser.Server.Implementations/Connect/Validator.cs b/Emby.Server.Implementations/Connect/Validator.cs similarity index 94% rename from MediaBrowser.Server.Implementations/Connect/Validator.cs rename to Emby.Server.Implementations/Connect/Validator.cs index 8cdfc4a6b3..5c94fa71c7 100644 --- a/MediaBrowser.Server.Implementations/Connect/Validator.cs +++ b/Emby.Server.Implementations/Connect/Validator.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace MediaBrowser.Server.Implementations.Connect +namespace Emby.Server.Implementations.Connect { public static class Validator { diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs new file mode 100644 index 0000000000..64a0d889eb --- /dev/null +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -0,0 +1,401 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Logging; +using SQLitePCL.pretty; +using System.Linq; +using SQLitePCL; + +namespace Emby.Server.Implementations.Data +{ + public abstract class BaseSqliteRepository : IDisposable + { + protected string DbFilePath { get; set; } + protected ReaderWriterLockSlim WriteLock; + + protected ILogger Logger { get; private set; } + + protected BaseSqliteRepository(ILogger logger) + { + Logger = logger; + + WriteLock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); + } + + protected TransactionMode TransactionMode + { + get { return TransactionMode.Deferred; } + } + + protected TransactionMode ReadTransactionMode + { + get { return TransactionMode.Deferred; } + } + + internal static int ThreadSafeMode { get; set; } + + static BaseSqliteRepository() + { + SQLite3.EnableSharedCache = false; + + int rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MEMSTATUS, 0); + //CheckOk(rc); + + rc = raw.sqlite3_config(raw.SQLITE_CONFIG_MULTITHREAD, 1); + //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SINGLETHREAD, 1); + //rc = raw.sqlite3_config(raw.SQLITE_CONFIG_SERIALIZED, 1); + //CheckOk(rc); + + rc = raw.sqlite3_enable_shared_cache(1); + + ThreadSafeMode = raw.sqlite3_threadsafe(); + } + + private static bool _versionLogged; + + private string _defaultWal; + protected ManagedConnection _connection; + + protected virtual bool EnableSingleConnection + { + get { return true; } + } + + protected ManagedConnection CreateConnection(bool isReadOnly = false) + { + if (_connection != null) + { + return _connection; + } + + lock (WriteLock) + { + if (!_versionLogged) + { + _versionLogged = true; + Logger.Info("Sqlite version: " + SQLite3.Version); + Logger.Info("Sqlite compiler options: " + string.Join(",", SQLite3.CompilerOptions.ToArray())); + } + + ConnectionFlags connectionFlags; + + if (isReadOnly) + { + //Logger.Info("Opening read connection"); + //connectionFlags = ConnectionFlags.ReadOnly; + connectionFlags = ConnectionFlags.Create; + connectionFlags |= ConnectionFlags.ReadWrite; + } + else + { + //Logger.Info("Opening write connection"); + connectionFlags = ConnectionFlags.Create; + connectionFlags |= ConnectionFlags.ReadWrite; + } + + if (EnableSingleConnection) + { + connectionFlags |= ConnectionFlags.PrivateCache; + } + else + { + connectionFlags |= ConnectionFlags.SharedCached; + } + + connectionFlags |= ConnectionFlags.NoMutex; + + var db = SQLite3.Open(DbFilePath, connectionFlags, null); + + if (string.IsNullOrWhiteSpace(_defaultWal)) + { + _defaultWal = db.Query("PRAGMA journal_mode").SelectScalarString().First(); + + Logger.Info("Default journal_mode for {0} is {1}", DbFilePath, _defaultWal); + } + + var queries = new List<string> + { + //"PRAGMA cache size=-10000" + //"PRAGMA read_uncommitted = true", + "PRAGMA synchronous=Normal" + }; + + if (CacheSize.HasValue) + { + queries.Add("PRAGMA cache_size=-" + CacheSize.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (EnableTempStoreMemory) + { + queries.Add("PRAGMA temp_store = memory"); + } + + //var cacheSize = CacheSize; + //if (cacheSize.HasValue) + //{ + + //} + + ////foreach (var query in queries) + ////{ + //// db.Execute(query); + ////} + + //Logger.Info("synchronous: " + db.Query("PRAGMA synchronous").SelectScalarString().First()); + //Logger.Info("temp_store: " + db.Query("PRAGMA temp_store").SelectScalarString().First()); + + /*if (!string.Equals(_defaultWal, "wal", StringComparison.OrdinalIgnoreCase)) + { + queries.Add("PRAGMA journal_mode=WAL"); + + using (WriteLock.Write()) + { + db.ExecuteAll(string.Join(";", queries.ToArray())); + } + } + else*/ + foreach (var query in queries) + { + db.Execute(query); + } + + _connection = new ManagedConnection(db, false); + + return _connection; + } + } + + public IStatement PrepareStatement(ManagedConnection connection, string sql) + { + return connection.PrepareStatement(sql); + } + + public IStatement PrepareStatementSafe(ManagedConnection connection, string sql) + { + return connection.PrepareStatement(sql); + } + + public IStatement PrepareStatement(IDatabaseConnection connection, string sql) + { + return connection.PrepareStatement(sql); + } + + public IStatement PrepareStatementSafe(IDatabaseConnection connection, string sql) + { + return connection.PrepareStatement(sql); + } + + public List<IStatement> PrepareAll(IDatabaseConnection connection, IEnumerable<string> sql) + { + return PrepareAllSafe(connection, sql); + } + + public List<IStatement> PrepareAllSafe(IDatabaseConnection connection, IEnumerable<string> sql) + { + return sql.Select(connection.PrepareStatement).ToList(); + } + + protected void RunDefaultInitialization(ManagedConnection db) + { + var queries = new List<string> + { + "PRAGMA journal_mode=WAL", + "PRAGMA page_size=4096", + "PRAGMA synchronous=Normal" + }; + + if (EnableTempStoreMemory) + { + queries.AddRange(new List<string> + { + "pragma default_temp_store = memory", + "pragma temp_store = memory" + }); + } + + db.ExecuteAll(string.Join(";", queries.ToArray())); + Logger.Info("PRAGMA synchronous=" + db.Query("PRAGMA synchronous").SelectScalarString().First()); + } + + protected virtual bool EnableTempStoreMemory + { + get + { + return false; + } + } + + protected virtual int? CacheSize + { + get + { + return null; + } + } + + internal static void CheckOk(int rc) + { + string msg = ""; + + if (raw.SQLITE_OK != rc) + { + throw CreateException((ErrorCode)rc, msg); + } + } + + internal static Exception CreateException(ErrorCode rc, string msg) + { + var exp = new Exception(msg); + + return exp; + } + + private bool _disposed; + protected void CheckDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().Name + " has been disposed and cannot be accessed."); + } + } + + public void Dispose() + { + _disposed = true; + Dispose(true); + GC.SuppressFinalize(this); + } + + private readonly object _disposeLock = new object(); + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + try + { + lock (_disposeLock) + { + using (WriteLock.Write()) + { + if (_connection != null) + { + using (_connection) + { + + } + _connection = null; + } + + CloseConnection(); + } + } + } + catch (Exception ex) + { + Logger.ErrorException("Error disposing database", ex); + } + } + } + + protected virtual void CloseConnection() + { + + } + + protected List<string> GetColumnNames(IDatabaseConnection connection, string table) + { + var list = new List<string>(); + + foreach (var row in connection.Query("PRAGMA table_info(" + table + ")")) + { + if (row[1].SQLiteType != SQLiteType.Null) + { + var name = row[1].ToString(); + + list.Add(name); + } + } + + return list; + } + + protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames) + { + if (existingColumnNames.Contains(columnName, StringComparer.OrdinalIgnoreCase)) + { + return; + } + + connection.Execute("alter table " + table + " add column " + columnName + " " + type + " NULL"); + } + } + + public static class ReaderWriterLockSlimExtensions + { + private sealed class ReadLockToken : IDisposable + { + private ReaderWriterLockSlim _sync; + public ReadLockToken(ReaderWriterLockSlim sync) + { + _sync = sync; + sync.EnterReadLock(); + } + public void Dispose() + { + if (_sync != null) + { + _sync.ExitReadLock(); + _sync = null; + } + } + } + private sealed class WriteLockToken : IDisposable + { + private ReaderWriterLockSlim _sync; + public WriteLockToken(ReaderWriterLockSlim sync) + { + _sync = sync; + sync.EnterWriteLock(); + } + public void Dispose() + { + if (_sync != null) + { + _sync.ExitWriteLock(); + _sync = null; + } + } + } + + public class DummyToken : IDisposable + { + public void Dispose() + { + } + } + + public static IDisposable Read(this ReaderWriterLockSlim obj) + { + //if (BaseSqliteRepository.ThreadSafeMode > 0) + //{ + // return new DummyToken(); + //} + return new WriteLockToken(obj); + } + public static IDisposable Write(this ReaderWriterLockSlim obj) + { + //if (BaseSqliteRepository.ThreadSafeMode > 0) + //{ + // return new DummyToken(); + //} + return new WriteLockToken(obj); + } + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs similarity index 54% rename from MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs rename to Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index c1394ee1c3..5bc3a625f1 100644 --- a/MediaBrowser.Server.Implementations/Persistence/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -1,6 +1,4 @@ using MediaBrowser.Common.Progress; -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; @@ -8,44 +6,29 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Localization; -using MediaBrowser.Controller.Net; -using MediaBrowser.Server.Implementations.ScheduledTasks; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.Persistence +namespace Emby.Server.Implementations.Data { public class CleanDatabaseScheduledTask : IScheduledTask { private readonly ILibraryManager _libraryManager; private readonly IItemRepository _itemRepo; private readonly ILogger _logger; - private readonly IServerConfigurationManager _config; private readonly IFileSystem _fileSystem; - private readonly IHttpServer _httpServer; - private readonly ILocalizationManager _localization; - private readonly ITaskManager _taskManager; - public const int MigrationVersion = 23; - public static bool EnableUnavailableMessage = false; - - public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IServerConfigurationManager config, IFileSystem fileSystem, IHttpServer httpServer, ILocalizationManager localization, ITaskManager taskManager) + public CleanDatabaseScheduledTask(ILibraryManager libraryManager, IItemRepository itemRepo, ILogger logger, IFileSystem fileSystem) { _libraryManager = libraryManager; _itemRepo = itemRepo; _logger = logger; - _config = config; _fileSystem = fileSystem; - _httpServer = httpServer; - _localization = localization; - _taskManager = taskManager; } public string Name @@ -65,8 +48,6 @@ namespace MediaBrowser.Server.Implementations.Persistence public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) { - OnProgress(0); - // Ensure these objects are lazy loaded. // Without this there is a deadlock that will need to be investigated var rootChildren = _libraryManager.RootFolder.Children.ToList(); @@ -75,19 +56,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var innerProgress = new ActionableProgress<double>(); innerProgress.RegisterAction(p => { - double newPercentCommplete = .4 * p; - OnProgress(newPercentCommplete); - - progress.Report(newPercentCommplete); - }); - - await UpdateToLatestSchema(cancellationToken, innerProgress).ConfigureAwait(false); - - innerProgress = new ActionableProgress<double>(); - innerProgress.RegisterAction(p => - { - double newPercentCommplete = 40 + .05 * p; - OnProgress(newPercentCommplete); + double newPercentCommplete = .45 * p; progress.Report(newPercentCommplete); }); await CleanDeadItems(cancellationToken, innerProgress).ConfigureAwait(false); @@ -97,122 +66,12 @@ namespace MediaBrowser.Server.Implementations.Persistence innerProgress.RegisterAction(p => { double newPercentCommplete = 45 + .55 * p; - OnProgress(newPercentCommplete); progress.Report(newPercentCommplete); }); await CleanDeletedItems(cancellationToken, innerProgress).ConfigureAwait(false); progress.Report(100); await _itemRepo.UpdateInheritedValues(cancellationToken).ConfigureAwait(false); - - if (_config.Configuration.MigrationVersion < MigrationVersion) - { - _config.Configuration.MigrationVersion = MigrationVersion; - _config.SaveConfiguration(); - } - - if (_config.Configuration.SchemaVersion < SqliteItemRepository.LatestSchemaVersion) - { - _config.Configuration.SchemaVersion = SqliteItemRepository.LatestSchemaVersion; - _config.SaveConfiguration(); - } - - if (EnableUnavailableMessage) - { - EnableUnavailableMessage = false; - _httpServer.GlobalResponse = null; - _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>(); - } - - _taskManager.SuspendTriggers = false; - } - - private void OnProgress(double newPercentCommplete) - { - if (EnableUnavailableMessage) - { - var html = "<!doctype html><html><head><title>Emby"; - var text = _localization.GetLocalizedString("DbUpgradeMessage"); - html += string.Format(text, newPercentCommplete.ToString("N2", CultureInfo.InvariantCulture)); - - html += ""; - html += ""; - - _httpServer.GlobalResponse = html; - } - } - - private async Task UpdateToLatestSchema(CancellationToken cancellationToken, IProgress progress) - { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery - { - IsCurrentSchema = false, - ExcludeItemTypes = new[] { typeof(LiveTvProgram).Name } - }); - - var numComplete = 0; - var numItems = itemIds.Count; - - _logger.Debug("Upgrading schema for {0} items", numItems); - - var list = new List(); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (itemId != Guid.Empty) - { - // Somehow some invalid data got into the db. It probably predates the boundary checking - var item = _libraryManager.GetItemById(itemId); - - if (item != null) - { - list.Add(item); - } - } - - if (list.Count >= 1000) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - - list.Clear(); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); - } - - if (list.Count > 0) - { - try - { - await _itemRepo.SaveItems(list, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - _logger.ErrorException("Error saving item", ex); - } - } - - progress.Report(100); } private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) @@ -276,9 +135,9 @@ namespace MediaBrowser.Server.Implementations.Persistence }); var numComplete = 0; - var numItems = result.Items.Length; + var numItems = result.Count; - foreach (var item in result.Items) + foreach (var item in result) { cancellationToken.ThrowIfCancellationRequested(); @@ -337,12 +196,22 @@ namespace MediaBrowser.Server.Implementations.Persistence } } - public IEnumerable GetDefaultTriggers() + /// + /// Creates the triggers that define when the task will run + /// + /// IEnumerable{BaseTaskTrigger}. + public IEnumerable GetDefaultTriggers() { - return new ITaskTrigger[] - { - new IntervalTrigger{ Interval = TimeSpan.FromHours(24)} + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} }; } + + public string Key + { + get { return "CleanDatabase"; } + } } } \ No newline at end of file diff --git a/Emby.Server.Implementations/Data/ManagedConnection.cs b/Emby.Server.Implementations/Data/ManagedConnection.cs new file mode 100644 index 0000000000..91a2dfdf62 --- /dev/null +++ b/Emby.Server.Implementations/Data/ManagedConnection.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public class ManagedConnection : IDisposable + { + private SQLiteDatabaseConnection db; + private readonly bool _closeOnDispose; + + public ManagedConnection(SQLiteDatabaseConnection db, bool closeOnDispose) + { + this.db = db; + _closeOnDispose = closeOnDispose; + } + + public IStatement PrepareStatement(string sql) + { + return db.PrepareStatement(sql); + } + + public IEnumerable PrepareAll(string sql) + { + return db.PrepareAll(sql); + } + + public void ExecuteAll(string sql) + { + db.ExecuteAll(sql); + } + + public void Execute(string sql, params object[] values) + { + db.Execute(sql, values); + } + + public void RunQueries(string[] sql) + { + db.RunQueries(sql); + } + + public void RunInTransaction(Action action, TransactionMode mode) + { + db.RunInTransaction(action, mode); + } + + public T RunInTransaction(Func action, TransactionMode mode) + { + return db.RunInTransaction(action, mode); + } + + public IEnumerable> Query(string sql) + { + return db.Query(sql); + } + + public IEnumerable> Query(string sql, params object[] values) + { + return db.Query(sql, values); + } + + public void Close() + { + using (db) + { + + } + } + + public void Dispose() + { + if (_closeOnDispose) + { + Close(); + } + } + } +} diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs new file mode 100644 index 0000000000..f3d84315e9 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteDisplayPreferencesRepository + /// + public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository + { + private readonly IMemoryStreamFactory _memoryStreamProvider; + + public SqliteDisplayPreferencesRepository(ILogger logger, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IMemoryStreamFactory memoryStreamProvider) + : base(logger) + { + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// The _json serializer + /// + private readonly IJsonSerializer _jsonSerializer; + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + string[] queries = { + + "create table if not exists userdisplaypreferences (id GUID, userId GUID, client text, data BLOB)", + "create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)" + }; + + connection.RunQueries(queries); + } + } + + /// + /// Save the display preferences associated with an item in the repo + /// + /// The display preferences. + /// The user id. + /// The client. + /// The cancellation token. + /// Task. + /// item + public async Task SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + if (string.IsNullOrWhiteSpace(displayPreferences.Id)) + { + throw new ArgumentNullException("displayPreferences.Id"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + SaveDisplayPreferences(displayPreferences, userId, client, db); + }, TransactionMode); + } + } + } + + private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection) + { + var serialized = _jsonSerializer.SerializeToBytes(displayPreferences, _memoryStreamProvider); + + using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)")) + { + statement.TryBind("@id", displayPreferences.Id.ToGuidParamValue()); + statement.TryBind("@userId", userId.ToGuidParamValue()); + statement.TryBind("@client", client); + statement.TryBind("@data", serialized); + + statement.MoveNext(); + } + } + + /// + /// Save all display preferences associated with a user in the repo + /// + /// The display preferences. + /// The user id. + /// The cancellation token. + /// Task. + /// item + public async Task SaveAllDisplayPreferences(IEnumerable displayPreferences, Guid userId, CancellationToken cancellationToken) + { + if (displayPreferences == null) + { + throw new ArgumentNullException("displayPreferences"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + foreach (var displayPreference in displayPreferences) + { + SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db); + } + }, TransactionMode); + } + } + } + + /// + /// Gets the display preferences. + /// + /// The display preferences id. + /// The user id. + /// The client. + /// Task{DisplayPreferences}. + /// item + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client) + { + if (string.IsNullOrWhiteSpace(displayPreferencesId)) + { + throw new ArgumentNullException("displayPreferencesId"); + } + + var guidId = displayPreferencesId.GetMD5(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client")) + { + statement.TryBind("@id", guidId.ToGuidParamValue()); + statement.TryBind("@userId", userId.ToGuidParamValue()); + statement.TryBind("@client", client); + + foreach (var row in statement.ExecuteQuery()) + { + return Get(row); + } + } + + return new DisplayPreferences + { + Id = guidId.ToString("N") + }; + } + } + } + + /// + /// Gets all display preferences for the given user. + /// + /// The user id. + /// Task{DisplayPreferences}. + /// item + public IEnumerable GetAllDisplayPreferences(Guid userId) + { + var list = new List(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId")) + { + statement.TryBind("@userId", userId.ToGuidParamValue()); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(Get(row)); + } + } + } + } + + return list; + } + + private DisplayPreferences Get(IReadOnlyList row) + { + using (var stream = _memoryStreamProvider.CreateNew(row[0].ToBlob())) + { + stream.Position = 0; + return _jsonSerializer.DeserializeFromStream(stream); + } + } + + public Task SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken) + { + return SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken); + } + + public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client) + { + return GetDisplayPreferences(displayPreferencesId, new Guid(userId), client); + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs new file mode 100644 index 0000000000..d6ad0ba8ab --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -0,0 +1,394 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Serialization; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public static class SqliteExtensions + { + public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries) + { + if (queries == null) + { + throw new ArgumentNullException("queries"); + } + + connection.RunInTransaction(conn => + { + //foreach (var query in queries) + //{ + // conn.Execute(query); + //} + conn.ExecuteAll(string.Join(";", queries)); + }); + } + + public static byte[] ToGuidParamValue(this string str) + { + return ToGuidParamValue(new Guid(str)); + } + + public static byte[] ToGuidParamValue(this Guid guid) + { + return guid.ToByteArray(); + } + + public static Guid ReadGuid(this IResultSetValue result) + { + return new Guid(result.ToBlob()); + } + + public static string ToDateTimeParamValue(this DateTime dateValue) + { + var kind = DateTimeKind.Utc; + + return (dateValue.Kind == DateTimeKind.Unspecified) + ? DateTime.SpecifyKind(dateValue, kind).ToString( + GetDateTimeKindFormat(kind), + CultureInfo.InvariantCulture) + : dateValue.ToString( + GetDateTimeKindFormat(dateValue.Kind), + CultureInfo.InvariantCulture); + } + + private static string GetDateTimeKindFormat( + DateTimeKind kind) + { + return (kind == DateTimeKind.Utc) ? _datetimeFormatUtc : _datetimeFormatLocal; + } + + /// + /// An array of ISO-8601 DateTime formats that we support parsing. + /// + private static string[] _datetimeFormats = new string[] { + "THHmmssK", + "THHmmK", + "HH:mm:ss.FFFFFFFK", + "HH:mm:ssK", + "HH:mmK", + "yyyy-MM-dd HH:mm:ss.FFFFFFFK", /* NOTE: UTC default (5). */ + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mmK", + "yyyy-MM-ddTHH:mm:ss.FFFFFFFK", + "yyyy-MM-ddTHH:mmK", + "yyyy-MM-ddTHH:mm:ssK", + "yyyyMMddHHmmssK", + "yyyyMMddHHmmK", + "yyyyMMddTHHmmssFFFFFFFK", + "THHmmss", + "THHmm", + "HH:mm:ss.FFFFFFF", + "HH:mm:ss", + "HH:mm", + "yyyy-MM-dd HH:mm:ss.FFFFFFF", /* NOTE: Non-UTC default (19). */ + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy-MM-ddTHH:mm:ss.FFFFFFF", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyyMMddHHmmss", + "yyyyMMddHHmm", + "yyyyMMddTHHmmssFFFFFFF", + "yyyy-MM-dd", + "yyyyMMdd", + "yy-MM-dd" + }; + + private static string _datetimeFormatUtc = _datetimeFormats[5]; + private static string _datetimeFormatLocal = _datetimeFormats[19]; + + public static DateTime ReadDateTime(this IResultSetValue result) + { + var dateText = result.ToString(); + + return DateTime.ParseExact( + dateText, _datetimeFormats, + DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.None).ToUniversalTime(); + } + + /// + /// Serializes to bytes. + /// + /// System.Byte[][]. + /// obj + public static byte[] SerializeToBytes(this IJsonSerializer json, object obj, IMemoryStreamFactory streamProvider) + { + if (obj == null) + { + throw new ArgumentNullException("obj"); + } + + using (var stream = streamProvider.CreateNew()) + { + json.SerializeToStream(obj, stream); + return stream.ToArray(); + } + } + + public static void Attach(ManagedConnection db, string path, string alias) + { + var commandText = string.Format("attach @path as {0};", alias); + + using (var statement = db.PrepareStatement(commandText)) + { + statement.TryBind("@path", path); + statement.MoveNext(); + } + } + + public static bool IsDBNull(this IReadOnlyList result, int index) + { + return result[index].SQLiteType == SQLiteType.Null; + } + + public static string GetString(this IReadOnlyList result, int index) + { + return result[index].ToString(); + } + + public static bool GetBoolean(this IReadOnlyList result, int index) + { + return result[index].ToBool(); + } + + public static int GetInt32(this IReadOnlyList result, int index) + { + return result[index].ToInt(); + } + + public static long GetInt64(this IReadOnlyList result, int index) + { + return result[index].ToInt64(); + } + + public static float GetFloat(this IReadOnlyList result, int index) + { + return result[index].ToFloat(); + } + + public static Guid GetGuid(this IReadOnlyList result, int index) + { + return result[index].ReadGuid(); + } + + private static void CheckName(string name) + { +#if DEBUG + //if (!name.IndexOf("@", StringComparison.OrdinalIgnoreCase) != 0) + { + throw new Exception("Invalid param name: " + name); + } +#endif + } + + public static void TryBind(this IStatement statement, string name, double value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, string value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + if (value == null) + { + bindParam.BindNull(); + } + else + { + bindParam.Bind(value); + } + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, bool value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, float value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, int value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, Guid value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value.ToGuidParamValue()); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, DateTime value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value.ToDateTimeParamValue()); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, long value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, byte[] value) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.Bind(value); + } + else + { + CheckName(name); + } + } + + public static void TryBindNull(this IStatement statement, string name) + { + IBindParameter bindParam; + if (statement.BindParameters.TryGetValue(name, out bindParam)) + { + bindParam.BindNull(); + } + else + { + CheckName(name); + } + } + + public static void TryBind(this IStatement statement, string name, DateTime? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, Guid? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, int? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, float? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static void TryBind(this IStatement statement, string name, bool? value) + { + if (value.HasValue) + { + TryBind(statement, name, value.Value); + } + else + { + TryBindNull(statement, name); + } + } + + public static IEnumerable> ExecuteQuery( + this IStatement This) + { + while (This.MoveNext()) + { + yield return This.Current; + } + } + } +} diff --git a/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs b/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs new file mode 100644 index 0000000000..9fbe8669d1 --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteFileOrganizationRepository.cs @@ -0,0 +1,284 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.FileOrganization; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public class SqliteFileOrganizationRepository : BaseSqliteRepository, IFileOrganizationRepository, IDisposable + { + private readonly CultureInfo _usCulture = new CultureInfo("en-US"); + + public SqliteFileOrganizationRepository(ILogger logger, IServerApplicationPaths appPaths) : base(logger) + { + DbFilePath = Path.Combine(appPaths.DataPath, "fileorganization.db"); + } + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + string[] queries = { + + "create table if not exists FileOrganizerResults (ResultId GUID PRIMARY KEY, OriginalPath TEXT, TargetPath TEXT, FileLength INT, OrganizationDate datetime, Status TEXT, OrganizationType TEXT, StatusMessage TEXT, ExtractedName TEXT, ExtractedYear int null, ExtractedSeasonNumber int null, ExtractedEpisodeNumber int null, ExtractedEndingEpisodeNumber, DuplicatePaths TEXT int null)", + "create index if not exists idx_FileOrganizerResults on FileOrganizerResults(ResultId)" + }; + + connection.RunQueries(queries); + } + } + + public async Task SaveResult(FileOrganizationResult result, CancellationToken cancellationToken) + { + if (result == null) + { + throw new ArgumentNullException("result"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + var commandText = "replace into FileOrganizerResults (ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths) values (@ResultId, @OriginalPath, @TargetPath, @FileLength, @OrganizationDate, @Status, @OrganizationType, @StatusMessage, @ExtractedName, @ExtractedYear, @ExtractedSeasonNumber, @ExtractedEpisodeNumber, @ExtractedEndingEpisodeNumber, @DuplicatePaths)"; + + using (var statement = db.PrepareStatement(commandText)) + { + statement.TryBind("@ResultId", result.Id.ToGuidParamValue()); + statement.TryBind("@OriginalPath", result.OriginalPath); + + statement.TryBind("@TargetPath", result.TargetPath); + statement.TryBind("@FileLength", result.FileSize); + statement.TryBind("@OrganizationDate", result.Date.ToDateTimeParamValue()); + statement.TryBind("@Status", result.Status.ToString()); + statement.TryBind("@OrganizationType", result.Type.ToString()); + statement.TryBind("@StatusMessage", result.StatusMessage); + statement.TryBind("@ExtractedName", result.ExtractedName); + statement.TryBind("@ExtractedYear", result.ExtractedYear); + statement.TryBind("@ExtractedSeasonNumber", result.ExtractedSeasonNumber); + statement.TryBind("@ExtractedEpisodeNumber", result.ExtractedEpisodeNumber); + statement.TryBind("@ExtractedEndingEpisodeNumber", result.ExtractedEndingEpisodeNumber); + statement.TryBind("@DuplicatePaths", string.Join("|", result.DuplicatePaths.ToArray())); + + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + + public async Task Delete(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("delete from FileOrganizerResults where ResultId = @ResultId")) + { + statement.TryBind("@ResultId", id.ToGuidParamValue()); + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + + public async Task DeleteAll() + { + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + var commandText = "delete from FileOrganizerResults"; + + db.Execute(commandText); + }, TransactionMode); + } + } + } + + public QueryResult GetResults(FileOrganizationResultQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + var commandText = "SELECT ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults"; + + if (query.StartIndex.HasValue && query.StartIndex.Value > 0) + { + commandText += string.Format(" WHERE ResultId NOT IN (SELECT ResultId FROM FileOrganizerResults ORDER BY OrganizationDate desc LIMIT {0})", + query.StartIndex.Value.ToString(_usCulture)); + } + + commandText += " ORDER BY OrganizationDate desc"; + + if (query.Limit.HasValue) + { + commandText += " LIMIT " + query.Limit.Value.ToString(_usCulture); + } + + var list = new List(); + + using (var statement = connection.PrepareStatement(commandText)) + { + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetResult(row)); + } + } + + int count; + using (var statement = connection.PrepareStatement("select count (ResultId) from FileOrganizerResults")) + { + count = statement.ExecuteQuery().SelectScalarInt().First(); + } + + return new QueryResult() + { + Items = list.ToArray(), + TotalRecordCount = count + }; + } + } + } + + public FileOrganizationResult GetResult(string id) + { + if (string.IsNullOrEmpty(id)) + { + throw new ArgumentNullException("id"); + } + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select ResultId, OriginalPath, TargetPath, FileLength, OrganizationDate, Status, OrganizationType, StatusMessage, ExtractedName, ExtractedYear, ExtractedSeasonNumber, ExtractedEpisodeNumber, ExtractedEndingEpisodeNumber, DuplicatePaths from FileOrganizerResults where ResultId=@ResultId")) + { + statement.TryBind("@ResultId", id.ToGuidParamValue()); + + foreach (var row in statement.ExecuteQuery()) + { + return GetResult(row); + } + } + + return null; + } + } + } + + public FileOrganizationResult GetResult(IReadOnlyList reader) + { + var index = 0; + + var result = new FileOrganizationResult + { + Id = reader[0].ReadGuid().ToString("N") + }; + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.OriginalPath = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.TargetPath = reader[index].ToString(); + } + + index++; + result.FileSize = reader[index].ToInt64(); + + index++; + result.Date = reader[index].ReadDateTime(); + + index++; + result.Status = (FileSortingStatus)Enum.Parse(typeof(FileSortingStatus), reader[index].ToString(), true); + + index++; + result.Type = (FileOrganizerType)Enum.Parse(typeof(FileOrganizerType), reader[index].ToString(), true); + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.StatusMessage = reader[index].ToString(); + } + + result.OriginalFileName = Path.GetFileName(result.OriginalPath); + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.ExtractedName = reader[index].ToString(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.ExtractedYear = reader[index].ToInt(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.ExtractedSeasonNumber = reader[index].ToInt(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.ExtractedEpisodeNumber = reader[index].ToInt(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.ExtractedEndingEpisodeNumber = reader[index].ToInt(); + } + + index++; + if (reader[index].SQLiteType != SQLiteType.Null) + { + result.DuplicatePaths = reader[index].ToString().Split('|').Where(i => !string.IsNullOrEmpty(i)).ToList(); + } + + return result; + } + } +} diff --git a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs similarity index 52% rename from MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs rename to Emby.Server.Implementations/Data/SqliteItemRepository.cs index f33b18389a..1517029050 100644 --- a/MediaBrowser.Server.Implementations/Persistence/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -1,16 +1,5 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Controller.Entities.Audio; -using MediaBrowser.Controller.Entities.Movies; -using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Querying; -using MediaBrowser.Model.Serialization; using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.IO; using System.Linq; @@ -18,27 +7,39 @@ using System.Runtime.Serialization; using System.Text; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Extensions; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Model.Dto; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Querying; +using MediaBrowser.Model.Serialization; using MediaBrowser.Server.Implementations.Devices; using MediaBrowser.Server.Implementations.Playlists; +using MediaBrowser.Model.Reflection; +using SQLitePCL.pretty; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.Persistence +namespace Emby.Server.Implementations.Data { /// /// Class SQLiteItemRepository /// public class SqliteItemRepository : BaseSqliteRepository, IItemRepository { - private IDbConnection _connection; - - private readonly TypeMapper _typeMapper = new TypeMapper(); + private readonly TypeMapper _typeMapper; /// /// Gets the name of the repository @@ -63,49 +64,19 @@ namespace MediaBrowser.Server.Implementations.Persistence /// private readonly IServerConfigurationManager _config; - /// - /// The _save item command - /// - private IDbCommand _saveItemCommand; - private readonly string _criticReviewsPath; - private IDbCommand _deleteItemCommand; - - private IDbCommand _deletePeopleCommand; - private IDbCommand _savePersonCommand; - - private IDbCommand _deleteChaptersCommand; - private IDbCommand _saveChapterCommand; - - private IDbCommand _deleteStreamsCommand; - private IDbCommand _saveStreamCommand; - - private IDbCommand _deleteAncestorsCommand; - private IDbCommand _saveAncestorCommand; - - private IDbCommand _deleteUserDataKeysCommand; - private IDbCommand _saveUserDataKeysCommand; - - private IDbCommand _deleteItemValuesCommand; - private IDbCommand _saveItemValuesCommand; - - private IDbCommand _deleteProviderIdsCommand; - private IDbCommand _saveProviderIdsCommand; - - private IDbCommand _deleteImagesCommand; - private IDbCommand _saveImagesCommand; - - private IDbCommand _updateInheritedTagsCommand; - - public const int LatestSchemaVersion = 109; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IMemoryStreamFactory _memoryStreamProvider; + private readonly IFileSystem _fileSystem; + private readonly IEnvironmentInfo _environmentInfo; + private readonly ITimerFactory _timerFactory; + private ITimer _shrinkMemoryTimer; /// /// Initializes a new instance of the class. /// - public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogManager logManager, IDbConnector connector, IMemoryStreamProvider memoryStreamProvider) - : base(logManager, connector) + public SqliteItemRepository(IServerConfigurationManager config, IJsonSerializer jsonSerializer, ILogger logger, IMemoryStreamFactory memoryStreamProvider, IAssemblyInfo assemblyInfo, IFileSystem fileSystem, IEnvironmentInfo environmentInfo, ITimerFactory timerFactory) + : base(logger) { if (config == null) { @@ -119,6 +90,10 @@ namespace MediaBrowser.Server.Implementations.Persistence _config = config; _jsonSerializer = jsonSerializer; _memoryStreamProvider = memoryStreamProvider; + _fileSystem = fileSystem; + _environmentInfo = environmentInfo; + _timerFactory = timerFactory; + _typeMapper = new TypeMapper(assemblyInfo); _criticReviewsPath = Path.Combine(_config.ApplicationPaths.DataPath, "critic-reviews"); DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db"); @@ -126,25 +101,31 @@ namespace MediaBrowser.Server.Implementations.Persistence private const string ChaptersTableName = "Chapters2"; - protected override async Task CreateConnection(bool isReadOnly = false) + protected override int? CacheSize { - var cacheSize = _config.Configuration.SqliteCacheSize; - if (cacheSize <= 0) + get { - cacheSize = Math.Min(Environment.ProcessorCount * 50000, 100000); + return 20000; } + } - var connection = await DbConnector.Connect(DbFilePath, false, false, 0 - cacheSize).ConfigureAwait(false); - - connection.RunQueries(new[] + protected override bool EnableTempStoreMemory + { + get { - "pragma temp_store = memory", - "pragma default_temp_store = memory", - "PRAGMA locking_mode=EXCLUSIVE" + return true; + } + } - }, Logger); + protected override void CloseConnection() + { + base.CloseConnection(); - return connection; + if (_shrinkMemoryTimer != null) + { + _shrinkMemoryTimer.Dispose(); + _shrinkMemoryTimer = null; + } } /// @@ -153,30 +134,24 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Task. public async Task Initialize(SqliteUserDataRepository userDataRepo) { - _connection = await CreateConnection(false).ConfigureAwait(false); + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); - var createMediaStreamsTableCommand - = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; + var createMediaStreamsTableCommand + = "create table if not exists mediastreams (ItemId GUID, StreamIndex INT, StreamType TEXT, Codec TEXT, Language TEXT, ChannelLayout TEXT, Profile TEXT, AspectRatio TEXT, Path TEXT, IsInterlaced BIT, BitRate INT NULL, Channels INT NULL, SampleRate INT NULL, IsDefault BIT, IsForced BIT, IsExternal BIT, Height INT NULL, Width INT NULL, AverageFrameRate FLOAT NULL, RealFrameRate FLOAT NULL, Level FLOAT NULL, PixelFormat TEXT, BitDepth INT NULL, IsAnamorphic BIT NULL, RefFrames INT NULL, CodecTag TEXT NULL, Comment TEXT NULL, NalLengthSize TEXT NULL, IsAvc BIT NULL, Title TEXT NULL, TimeBase TEXT NULL, CodecTimeBase TEXT NULL, PRIMARY KEY (ItemId, StreamIndex))"; - string[] queries = { + string[] queries = { + "PRAGMA locking_mode=NORMAL", - "create table if not exists TypedBaseItems (guid GUID primary key, type TEXT, data BLOB, ParentId GUID, Path TEXT)", + "create table if not exists TypedBaseItems (guid GUID primary key NOT NULL, type TEXT NOT NULL, data BLOB NULL, ParentId GUID NULL, Path TEXT NULL)", "create table if not exists AncestorIds (ItemId GUID, AncestorId GUID, AncestorIdText TEXT, PRIMARY KEY (ItemId, AncestorId))", "create index if not exists idx_AncestorIds1 on AncestorIds(AncestorId)", "create index if not exists idx_AncestorIds2 on AncestorIds(AncestorIdText)", - "create table if not exists UserDataKeys (ItemId GUID, UserDataKey TEXT Priority INT, PRIMARY KEY (ItemId, UserDataKey))", - "create table if not exists ItemValues (ItemId GUID, Type INT, Value TEXT, CleanValue TEXT)", - "create table if not exists ProviderIds (ItemId GUID, Name TEXT, Value TEXT, PRIMARY KEY (ItemId, Name))", - // covering index - "create index if not exists Idx_ProviderIds1 on ProviderIds(ItemId,Name,Value)", - - "create table if not exists Images (ItemId GUID NOT NULL, Path TEXT NOT NULL, ImageType INT NOT NULL, DateModified DATETIME, IsPlaceHolder BIT NOT NULL, SortOrder INT)", - "create index if not exists idx_Images on Images(ItemId)", - "create table if not exists People (ItemId GUID, Name TEXT NOT NULL, Role TEXT, PersonType TEXT, SortOrder int, ListOrder int)", "drop index if exists idxPeopleItemId", @@ -189,111 +164,138 @@ namespace MediaBrowser.Server.Implementations.Persistence "create index if not exists idx_mediastreams1 on mediastreams(ItemId)", + "pragma shrink_memory" + }; - _connection.RunQueries(queries, Logger); + connection.RunQueries(queries); - _connection.AddColumn(Logger, "AncestorIds", "AncestorIdText", "Text"); + connection.RunInTransaction(db => + { + var existingColumnNames = GetColumnNames(db, "AncestorIds"); + AddColumn(db, "AncestorIds", "AncestorIdText", "Text", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "Path", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "StartDate", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "EndDate", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "ChannelId", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsMovie", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsSports", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsKids", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "CommunityRating", "Float"); - _connection.AddColumn(Logger, "TypedBaseItems", "CustomRating", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IndexNumber", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsLocked", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "Name", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRating", "Text"); + existingColumnNames = GetColumnNames(db, "TypedBaseItems"); - _connection.AddColumn(Logger, "TypedBaseItems", "MediaType", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Overview", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ParentIndexNumber", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "PremiereDate", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "ProductionYear", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "ParentId", "GUID"); - _connection.AddColumn(Logger, "TypedBaseItems", "Genres", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "SchemaVersion", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "SortName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "RunTimeTicks", "BIGINT"); + AddColumn(db, "TypedBaseItems", "Path", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "StartDate", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "EndDate", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ChannelId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsMovie", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsSports", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsKids", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "CommunityRating", "Float", existingColumnNames); + AddColumn(db, "TypedBaseItems", "CustomRating", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IndexNumber", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsLocked", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Name", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "OfficialRating", "Text", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "OfficialRatingDescription", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "HomePageUrl", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "VoteCount", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "DisplayMediaType", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "DateCreated", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "DateModified", "DATETIME"); + AddColumn(db, "TypedBaseItems", "MediaType", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Overview", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ParentIndexNumber", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "PremiereDate", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ProductionYear", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ParentId", "GUID", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Genres", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SortName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "RunTimeTicks", "BIGINT", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "ForcedSortName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsOffline", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "LocationType", "Text"); + AddColumn(db, "TypedBaseItems", "OfficialRatingDescription", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "HomePageUrl", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "VoteCount", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "DisplayMediaType", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "DateCreated", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "DateModified", "DATETIME", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "IsSeries", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsLive", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsNews", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsPremiere", "BIT"); + AddColumn(db, "TypedBaseItems", "ForcedSortName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "LocationType", "Text", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "EpisodeTitle", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsRepeat", "BIT"); + AddColumn(db, "TypedBaseItems", "IsSeries", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsLive", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsNews", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsPremiere", "BIT", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataLanguage", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "PreferredMetadataCountryCode", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsHD", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "ExternalEtag", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "DateLastRefreshed", "DATETIME"); + AddColumn(db, "TypedBaseItems", "EpisodeTitle", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsRepeat", "BIT", existingColumnNames); - _connection.AddColumn(Logger, "TypedBaseItems", "DateLastSaved", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsInMixedFolder", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "LockedFields", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Studios", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Audio", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ExternalServiceId", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Tags", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsFolder", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "InheritedParentalRatingValue", "INT"); - _connection.AddColumn(Logger, "TypedBaseItems", "UnratedType", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "TopParentId", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsItemByName", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "SourceType", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "TrailerTypes", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "CriticRating", "Float"); - _connection.AddColumn(Logger, "TypedBaseItems", "CriticRatingSummary", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "InheritedTags", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "CleanName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "PresentationUniqueKey", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "SlugName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "OriginalTitle", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "PrimaryVersionId", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "DateLastMediaAdded", "DATETIME"); - _connection.AddColumn(Logger, "TypedBaseItems", "Album", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "IsVirtualItem", "BIT"); - _connection.AddColumn(Logger, "TypedBaseItems", "SeriesName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "UserDataKey", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "SeasonName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "SeasonId", "GUID"); - _connection.AddColumn(Logger, "TypedBaseItems", "SeriesId", "GUID"); - _connection.AddColumn(Logger, "TypedBaseItems", "SeriesSortName", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ExternalSeriesId", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ShortOverview", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Tagline", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Keywords", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ProviderIds", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "Images", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ProductionLocations", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ThemeSongIds", "Text"); - _connection.AddColumn(Logger, "TypedBaseItems", "ThemeVideoIds", "Text"); + AddColumn(db, "TypedBaseItems", "PreferredMetadataLanguage", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "PreferredMetadataCountryCode", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsHD", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExternalEtag", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "DateLastRefreshed", "DATETIME", existingColumnNames); - _connection.AddColumn(Logger, "UserDataKeys", "Priority", "INT"); - _connection.AddColumn(Logger, "ItemValues", "CleanValue", "Text"); + AddColumn(db, "TypedBaseItems", "DateLastSaved", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsInMixedFolder", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "LockedFields", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Studios", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Audio", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExternalServiceId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Tags", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsFolder", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "InheritedParentalRatingValue", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "UnratedType", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "TopParentId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsItemByName", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SourceType", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "TrailerTypes", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "CriticRating", "Float", existingColumnNames); + AddColumn(db, "TypedBaseItems", "CriticRatingSummary", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "InheritedTags", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "CleanName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "PresentationUniqueKey", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SlugName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "OriginalTitle", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "PrimaryVersionId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Album", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeriesName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeasonName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeasonId", "GUID", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeriesId", "GUID", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeriesSortName", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExternalSeriesId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ShortOverview", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Tagline", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Keywords", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ProviderIds", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Images", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ProductionLocations", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ThemeSongIds", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ThemeVideoIds", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "TotalBitrate", "INT", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExtraType", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "Artists", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "AlbumArtists", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "ExternalId", "Text", existingColumnNames); + AddColumn(db, "TypedBaseItems", "SeriesPresentationUniqueKey", "Text", existingColumnNames); - _connection.AddColumn(Logger, ChaptersTableName, "ImageDateModified", "DATETIME"); + existingColumnNames = GetColumnNames(db, "ItemValues"); + AddColumn(db, "ItemValues", "CleanValue", "Text", existingColumnNames); - string[] postQueries = + existingColumnNames = GetColumnNames(db, ChaptersTableName); + AddColumn(db, ChaptersTableName, "ImageDateModified", "DATETIME", existingColumnNames); - { + existingColumnNames = GetColumnNames(db, "MediaStreams"); + AddColumn(db, "MediaStreams", "IsAvc", "BIT", existingColumnNames); + AddColumn(db, "MediaStreams", "TimeBase", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "CodecTimeBase", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "Title", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "NalLengthSize", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "Comment", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "CodecTag", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "PixelFormat", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "BitDepth", "INT", existingColumnNames); + AddColumn(db, "MediaStreams", "RefFrames", "INT", existingColumnNames); + AddColumn(db, "MediaStreams", "KeyFrames", "TEXT", existingColumnNames); + AddColumn(db, "MediaStreams", "IsAnamorphic", "BIT", existingColumnNames); + }, TransactionMode); + + string[] postQueries = + + { // obsolete "drop index if exists idx_TypedBaseItems", "drop index if exists idx_mediastreams", @@ -313,6 +315,15 @@ namespace MediaBrowser.Server.Implementations.Persistence "drop index if exists idx_ItemValues3", "drop index if exists idx_ItemValues4", "drop index if exists idx_ItemValues5", + "drop index if exists idx_UserDataKeys3", + "drop table if exists UserDataKeys", + "drop table if exists ProviderIds", + "drop index if exists Idx_ProviderIds1", + "drop table if exists Images", + "drop index if exists idx_Images", + "drop index if exists idx_TypeSeriesPresentationUniqueKey", + "drop index if exists idx_SeriesPresentationUniqueKey", + "drop index if exists idx_TypeSeriesPresentationUniqueKey2", "create index if not exists idx_PathTypedBaseItems on TypedBaseItems(Path)", "create index if not exists idx_ParentIdTypedBaseItems on TypedBaseItems(ParentId)", @@ -325,6 +336,13 @@ namespace MediaBrowser.Server.Implementations.Persistence // covering index "create index if not exists idx_TopParentIdGuid on TypedBaseItems(TopParentId,Guid)", + // series + "create index if not exists idx_TypeSeriesPresentationUniqueKey1 on TypedBaseItems(Type,SeriesPresentationUniqueKey,PresentationUniqueKey,SortName)", + + // series counts + // seriesdateplayed sort order + "create index if not exists idx_TypeSeriesPresentationUniqueKey3 on TypedBaseItems(SeriesPresentationUniqueKey,Type,IsFolder,IsVirtualItem)", + // live tv programs "create index if not exists idx_TypeTopParentIdStartDate on TypedBaseItems(Type,TopParentId,StartDate)", @@ -344,21 +362,40 @@ namespace MediaBrowser.Server.Implementations.Persistence // items by name "create index if not exists idx_ItemValues6 on ItemValues(ItemId,Type,CleanValue)", - "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)", - - // covering index - "create index if not exists idx_UserDataKeys3 on UserDataKeys(ItemId,Priority,UserDataKey)" + "create index if not exists idx_ItemValues7 on ItemValues(Type,CleanValue,ItemId)" }; - _connection.RunQueries(postQueries, Logger); + connection.RunQueries(postQueries); - PrepareStatements(); + //await Vacuum(_connection).ConfigureAwait(false); + } - new MediaStreamColumns(_connection, Logger).AddColumns(); + userDataRepo.Initialize(WriteLock, _connection); - DataExtensions.Attach(_connection, Path.Combine(_config.ApplicationPaths.DataPath, "userdata_v2.db"), "UserDataDb"); - await userDataRepo.Initialize(_connection, WriteLock).ConfigureAwait(false); - //await Vacuum(_connection).ConfigureAwait(false); + _shrinkMemoryTimer = _timerFactory.Create(OnShrinkMemoryTimerCallback, null, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15)); + } + + private void OnShrinkMemoryTimerCallback(object state) + { + try + { + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunQueries(new string[] + { + "pragma shrink_memory" + }); + } + } + + GC.Collect(); + } + catch (Exception ex) + { + Logger.ErrorException("Error running shrink memory", ex); + } } private readonly string[] _retriveItemColumns = @@ -367,7 +404,6 @@ namespace MediaBrowser.Server.Implementations.Persistence "data", "StartDate", "EndDate", - "IsOffline", "ChannelId", "IsMovie", "IsSports", @@ -437,7 +473,13 @@ namespace MediaBrowser.Server.Implementations.Persistence "Images", "ProductionLocations", "ThemeSongIds", - "ThemeVideoIds" + "ThemeVideoIds", + "TotalBitrate", + "ExtraType", + "Artists", + "AlbumArtists", + "ExternalId", + "SeriesPresentationUniqueKey" }; private readonly string[] _mediaStreamSaveColumns = @@ -476,10 +518,7 @@ namespace MediaBrowser.Server.Implementations.Persistence "CodecTimeBase" }; - /// - /// Prepares the statements. - /// - private void PrepareStatements() + private string GetSaveItemCommandText() { var saveColumns = new List { @@ -513,7 +552,6 @@ namespace MediaBrowser.Server.Implementations.Persistence "ParentId", "Genres", "InheritedParentalRatingValue", - "SchemaVersion", "SortName", "RunTimeTicks", "OfficialRatingDescription", @@ -523,7 +561,6 @@ namespace MediaBrowser.Server.Implementations.Persistence "DateCreated", "DateModified", "ForcedSortName", - "IsOffline", "LocationType", "PreferredMetadataLanguage", "PreferredMetadataCountryCode", @@ -568,135 +605,27 @@ namespace MediaBrowser.Server.Implementations.Persistence "Images", "ProductionLocations", "ThemeSongIds", - "ThemeVideoIds" + "ThemeVideoIds", + "TotalBitrate", + "ExtraType", + "Artists", + "AlbumArtists", + "ExternalId", + "SeriesPresentationUniqueKey" }; - _saveItemCommand = _connection.CreateCommand(); - _saveItemCommand.CommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; - for (var i = 1; i <= saveColumns.Count; i++) + var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns.ToArray()) + ") values ("; + + for (var i = 0; i < saveColumns.Count; i++) { - if (i > 1) + if (i > 0) { - _saveItemCommand.CommandText += ","; + saveItemCommandCommandText += ","; } - _saveItemCommand.CommandText += "@" + i.ToString(CultureInfo.InvariantCulture); - - _saveItemCommand.Parameters.Add(_saveItemCommand, "@" + i.ToString(CultureInfo.InvariantCulture)); + saveItemCommandCommandText += "@" + saveColumns[i]; } - _saveItemCommand.CommandText += ")"; - - _deleteItemCommand = _connection.CreateCommand(); - _deleteItemCommand.CommandText = "delete from TypedBaseItems where guid=@Id"; - _deleteItemCommand.Parameters.Add(_deleteItemCommand, "@Id"); - - // People - _deletePeopleCommand = _connection.CreateCommand(); - _deletePeopleCommand.CommandText = "delete from People where ItemId=@Id"; - _deletePeopleCommand.Parameters.Add(_deletePeopleCommand, "@Id"); - - _savePersonCommand = _connection.CreateCommand(); - _savePersonCommand.CommandText = "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values (@ItemId, @Name, @Role, @PersonType, @SortOrder, @ListOrder)"; - _savePersonCommand.Parameters.Add(_savePersonCommand, "@ItemId"); - _savePersonCommand.Parameters.Add(_savePersonCommand, "@Name"); - _savePersonCommand.Parameters.Add(_savePersonCommand, "@Role"); - _savePersonCommand.Parameters.Add(_savePersonCommand, "@PersonType"); - _savePersonCommand.Parameters.Add(_savePersonCommand, "@SortOrder"); - _savePersonCommand.Parameters.Add(_savePersonCommand, "@ListOrder"); - - // Ancestors - _deleteAncestorsCommand = _connection.CreateCommand(); - _deleteAncestorsCommand.CommandText = "delete from AncestorIds where ItemId=@Id"; - _deleteAncestorsCommand.Parameters.Add(_deleteAncestorsCommand, "@Id"); - - _saveAncestorCommand = _connection.CreateCommand(); - _saveAncestorCommand.CommandText = "insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values (@ItemId, @AncestorId, @AncestorIdText)"; - _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@ItemId"); - _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorId"); - _saveAncestorCommand.Parameters.Add(_saveAncestorCommand, "@AncestorIdText"); - - // Chapters - _deleteChaptersCommand = _connection.CreateCommand(); - _deleteChaptersCommand.CommandText = "delete from " + ChaptersTableName + " where ItemId=@ItemId"; - _deleteChaptersCommand.Parameters.Add(_deleteChaptersCommand, "@ItemId"); - - _saveChapterCommand = _connection.CreateCommand(); - _saveChapterCommand.CommandText = "replace into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath)"; - - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ItemId"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ChapterIndex"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@StartPositionTicks"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@Name"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImagePath"); - _saveChapterCommand.Parameters.Add(_saveChapterCommand, "@ImageDateModified"); - - // MediaStreams - _deleteStreamsCommand = _connection.CreateCommand(); - _deleteStreamsCommand.CommandText = "delete from mediastreams where ItemId=@ItemId"; - _deleteStreamsCommand.Parameters.Add(_deleteStreamsCommand, "@ItemId"); - - _saveStreamCommand = _connection.CreateCommand(); - - _saveStreamCommand.CommandText = string.Format("replace into mediastreams ({0}) values ({1})", - string.Join(",", _mediaStreamSaveColumns), - string.Join(",", _mediaStreamSaveColumns.Select(i => "@" + i).ToArray())); - - foreach (var col in _mediaStreamSaveColumns) - { - _saveStreamCommand.Parameters.Add(_saveStreamCommand, "@" + col); - } - - _updateInheritedTagsCommand = _connection.CreateCommand(); - _updateInheritedTagsCommand.CommandText = "Update TypedBaseItems set InheritedTags=@InheritedTags where Guid=@Guid"; - _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@Guid"); - _updateInheritedTagsCommand.Parameters.Add(_updateInheritedTagsCommand, "@InheritedTags"); - - // user data - _deleteUserDataKeysCommand = _connection.CreateCommand(); - _deleteUserDataKeysCommand.CommandText = "delete from UserDataKeys where ItemId=@Id"; - _deleteUserDataKeysCommand.Parameters.Add(_deleteUserDataKeysCommand, "@Id"); - - _saveUserDataKeysCommand = _connection.CreateCommand(); - _saveUserDataKeysCommand.CommandText = "insert into UserDataKeys (ItemId, UserDataKey, Priority) values (@ItemId, @UserDataKey, @Priority)"; - _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@ItemId"); - _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@UserDataKey"); - _saveUserDataKeysCommand.Parameters.Add(_saveUserDataKeysCommand, "@Priority"); - - // item values - _deleteItemValuesCommand = _connection.CreateCommand(); - _deleteItemValuesCommand.CommandText = "delete from ItemValues where ItemId=@Id"; - _deleteItemValuesCommand.Parameters.Add(_deleteItemValuesCommand, "@Id"); - - _saveItemValuesCommand = _connection.CreateCommand(); - _saveItemValuesCommand.CommandText = "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)"; - _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@ItemId"); - _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Type"); - _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@Value"); - _saveItemValuesCommand.Parameters.Add(_saveItemValuesCommand, "@CleanValue"); - - // provider ids - _deleteProviderIdsCommand = _connection.CreateCommand(); - _deleteProviderIdsCommand.CommandText = "delete from ProviderIds where ItemId=@Id"; - _deleteProviderIdsCommand.Parameters.Add(_deleteProviderIdsCommand, "@Id"); - - _saveProviderIdsCommand = _connection.CreateCommand(); - _saveProviderIdsCommand.CommandText = "insert into ProviderIds (ItemId, Name, Value) values (@ItemId, @Name, @Value)"; - _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@ItemId"); - _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Name"); - _saveProviderIdsCommand.Parameters.Add(_saveProviderIdsCommand, "@Value"); - - // images - _deleteImagesCommand = _connection.CreateCommand(); - _deleteImagesCommand.CommandText = "delete from Images where ItemId=@Id"; - _deleteImagesCommand.Parameters.Add(_deleteImagesCommand, "@Id"); - - _saveImagesCommand = _connection.CreateCommand(); - _saveImagesCommand.CommandText = "insert into Images (ItemId, ImageType, Path, DateModified, IsPlaceHolder, SortOrder) values (@ItemId, @ImageType, @Path, @DateModified, @IsPlaceHolder, @SortOrder)"; - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ItemId"); - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@ImageType"); - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@Path"); - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@DateModified"); - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@IsPlaceHolder"); - _saveImagesCommand.Parameters.Add(_saveImagesCommand, "@SortOrder"); + saveItemCommandCommandText += ")"; + return saveItemCommandCommandText; } /// @@ -713,7 +642,7 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("item"); } - return SaveItems(new[] { item }, cancellationToken); + return SaveItems(new List { item }, cancellationToken); } /// @@ -727,7 +656,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// or /// cancellationToken /// - public async Task SaveItems(IEnumerable items, CancellationToken cancellationToken) + public async Task SaveItems(List items, CancellationToken cancellationToken) { if (items == null) { @@ -738,378 +667,461 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + var tuples = new List, BaseItem, string>>(); + foreach (var item in items) { - transaction = _connection.BeginTransaction(); + var ancestorIds = item.SupportsAncestors ? + item.GetAncestorIds().Distinct().ToList() : + null; - foreach (var item in items) - { - cancellationToken.ThrowIfCancellationRequested(); + var topParent = item.GetTopParent(); - var index = 0; + var userdataKey = item.GetUserDataKeys().FirstOrDefault(); - _saveItemCommand.GetParameter(index++).Value = item.Id; - _saveItemCommand.GetParameter(index++).Value = item.GetType().FullName; - - if (TypeRequiresDeserialization(item.GetType())) - { - _saveItemCommand.GetParameter(index++).Value = _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.Path; - - var hasStartDate = item as IHasStartDate; - if (hasStartDate != null) - { - _saveItemCommand.GetParameter(index++).Value = hasStartDate.StartDate; - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.EndDate; - _saveItemCommand.GetParameter(index++).Value = item.ChannelId; - - var hasProgramAttributes = item as IHasProgramAttributes; - if (hasProgramAttributes != null) - { - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsKids; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsMovie; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSports; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsSeries; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsLive; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsNews; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsPremiere; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.EpisodeTitle; - _saveItemCommand.GetParameter(index++).Value = hasProgramAttributes.IsRepeat; - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.CommunityRating; - _saveItemCommand.GetParameter(index++).Value = item.CustomRating; - - _saveItemCommand.GetParameter(index++).Value = item.IndexNumber; - _saveItemCommand.GetParameter(index++).Value = item.IsLocked; - - _saveItemCommand.GetParameter(index++).Value = item.Name; - _saveItemCommand.GetParameter(index++).Value = item.OfficialRating; - - _saveItemCommand.GetParameter(index++).Value = item.MediaType; - _saveItemCommand.GetParameter(index++).Value = item.Overview; - _saveItemCommand.GetParameter(index++).Value = item.ParentIndexNumber; - _saveItemCommand.GetParameter(index++).Value = item.PremiereDate; - _saveItemCommand.GetParameter(index++).Value = item.ProductionYear; - - if (item.ParentId == Guid.Empty) - { - _saveItemCommand.GetParameter(index++).Value = null; - } - else - { - _saveItemCommand.GetParameter(index++).Value = item.ParentId; - } - - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Genres.ToArray()); - _saveItemCommand.GetParameter(index++).Value = item.GetInheritedParentalRatingValue() ?? 0; - - _saveItemCommand.GetParameter(index++).Value = LatestSchemaVersion; - _saveItemCommand.GetParameter(index++).Value = item.SortName; - _saveItemCommand.GetParameter(index++).Value = item.RunTimeTicks; - - _saveItemCommand.GetParameter(index++).Value = item.OfficialRatingDescription; - _saveItemCommand.GetParameter(index++).Value = item.HomePageUrl; - _saveItemCommand.GetParameter(index++).Value = item.VoteCount; - _saveItemCommand.GetParameter(index++).Value = item.DisplayMediaType; - _saveItemCommand.GetParameter(index++).Value = item.DateCreated; - _saveItemCommand.GetParameter(index++).Value = item.DateModified; - - _saveItemCommand.GetParameter(index++).Value = item.ForcedSortName; - _saveItemCommand.GetParameter(index++).Value = item.IsOffline; - _saveItemCommand.GetParameter(index++).Value = item.LocationType.ToString(); - - _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataLanguage; - _saveItemCommand.GetParameter(index++).Value = item.PreferredMetadataCountryCode; - _saveItemCommand.GetParameter(index++).Value = item.IsHD; - _saveItemCommand.GetParameter(index++).Value = item.ExternalEtag; - - if (item.DateLastRefreshed == default(DateTime)) - { - _saveItemCommand.GetParameter(index++).Value = null; - } - else - { - _saveItemCommand.GetParameter(index++).Value = item.DateLastRefreshed; - } - - if (item.DateLastSaved == default(DateTime)) - { - _saveItemCommand.GetParameter(index++).Value = null; - } - else - { - _saveItemCommand.GetParameter(index++).Value = item.DateLastSaved; - } - - _saveItemCommand.GetParameter(index++).Value = item.IsInMixedFolder; - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray()); - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Studios.ToArray()); - - if (item.Audio.HasValue) - { - _saveItemCommand.GetParameter(index++).Value = item.Audio.Value.ToString(); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.ServiceName; - - if (item.Tags.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Tags.ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.IsFolder; - - _saveItemCommand.GetParameter(index++).Value = item.GetBlockUnratedType().ToString(); - - var topParent = item.GetTopParent(); - if (topParent != null) - { - //Logger.Debug("Item {0} has top parent {1}", item.Id, topParent.Id); - _saveItemCommand.GetParameter(index++).Value = topParent.Id.ToString("N"); - } - else - { - //Logger.Debug("Item {0} has null top parent", item.Id); - _saveItemCommand.GetParameter(index++).Value = null; - } - - var isByName = false; - var byName = item as IItemByName; - if (byName != null) - { - var dualAccess = item as IHasDualAccess; - isByName = dualAccess == null || dualAccess.IsAccessedByName; - } - _saveItemCommand.GetParameter(index++).Value = isByName; - - _saveItemCommand.GetParameter(index++).Value = item.SourceType.ToString(); - - var trailer = item as Trailer; - if (trailer != null && trailer.TrailerTypes.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", trailer.TrailerTypes.Select(i => i.ToString()).ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.CriticRating; - _saveItemCommand.GetParameter(index++).Value = item.CriticRatingSummary; - - var inheritedTags = item.GetInheritedTags(); - if (inheritedTags.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", inheritedTags.ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - if (string.IsNullOrWhiteSpace(item.Name)) - { - _saveItemCommand.GetParameter(index++).Value = null; - } - else - { - _saveItemCommand.GetParameter(index++).Value = GetCleanValue(item.Name); - } - - _saveItemCommand.GetParameter(index++).Value = item.GetPresentationUniqueKey(); - _saveItemCommand.GetParameter(index++).Value = item.SlugName; - _saveItemCommand.GetParameter(index++).Value = item.OriginalTitle; - - var video = item as Video; - if (video != null) - { - _saveItemCommand.GetParameter(index++).Value = video.PrimaryVersionId; - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - var folder = item as Folder; - if (folder != null && folder.DateLastMediaAdded.HasValue) - { - _saveItemCommand.GetParameter(index++).Value = folder.DateLastMediaAdded.Value; - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.Album; - - _saveItemCommand.GetParameter(index++).Value = item.IsVirtualItem; - - var hasSeries = item as IHasSeries; - if (hasSeries != null) - { - _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesName(); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.GetUserDataKeys().FirstOrDefault(); - - var episode = item as Episode; - if (episode != null) - { - _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonName(); - _saveItemCommand.GetParameter(index++).Value = episode.FindSeasonId(); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - } - - if (hasSeries != null) - { - _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesId(); - _saveItemCommand.GetParameter(index++).Value = hasSeries.FindSeriesSortName(); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = item.ExternalSeriesId; - _saveItemCommand.GetParameter(index++).Value = item.ShortOverview; - _saveItemCommand.GetParameter(index++).Value = item.Tagline; - - if (item.Keywords.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.Keywords.ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.GetParameter(index++).Value = SerializeProviderIds(item); - _saveItemCommand.GetParameter(index++).Value = SerializeImages(item); - - if (item.ProductionLocations.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ProductionLocations.ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - if (item.ThemeSongIds.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeSongIds.Select(i => i.ToString("N")).ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - if (item.ThemeVideoIds.Count > 0) - { - _saveItemCommand.GetParameter(index++).Value = string.Join("|", item.ThemeVideoIds.Select(i => i.ToString("N")).ToArray()); - } - else - { - _saveItemCommand.GetParameter(index++).Value = null; - } - - _saveItemCommand.Transaction = transaction; - - _saveItemCommand.ExecuteNonQuery(); - - if (item.SupportsAncestors) - { - UpdateAncestors(item.Id, item.GetAncestorIds().Distinct().ToList(), transaction); - } - - UpdateUserDataKeys(item.Id, item.GetUserDataKeys().Distinct(StringComparer.OrdinalIgnoreCase).ToList(), transaction); - UpdateImages(item.Id, item.ImageInfos, transaction); - UpdateProviderIds(item.Id, item.ProviderIds, transaction); - UpdateItemValues(item.Id, GetItemValuesToSave(item), transaction); - } - - transaction.Commit(); + tuples.Add(new Tuple, BaseItem, string>(item, ancestorIds, topParent, userdataKey)); } - catch (OperationCanceledException) + + using (WriteLock.Write()) { - if (transaction != null) + using (var connection = CreateConnection()) { - transaction.Rollback(); + connection.RunInTransaction(db => + { + SaveItemsInTranscation(db, tuples); + }, TransactionMode); } - - throw; } - catch (Exception e) + } + + private void SaveItemsInTranscation(IDatabaseConnection db, List, BaseItem, string>> tuples) + { + var requiresReset = false; + + var statements = PrepareAllSafe(db, new string[] { - Logger.ErrorException("Failed to save items:", e); + GetSaveItemCommandText(), + "delete from AncestorIds where ItemId=@ItemId", + "insert into AncestorIds (ItemId, AncestorId, AncestorIdText) values (@ItemId, @AncestorId, @AncestorIdText)" + }).ToList(); - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally + using (var saveItemStatement = statements[0]) { - if (transaction != null) + using (var deleteAncestorsStatement = statements[1]) { - transaction.Dispose(); - } + using (var updateAncestorsStatement = statements[2]) + { + foreach (var tuple in tuples) + { + if (requiresReset) + { + saveItemStatement.Reset(); + } - WriteLock.Release(); + var item = tuple.Item1; + var topParent = tuple.Item3; + var userDataKey = tuple.Item4; + + SaveItem(item, topParent, userDataKey, saveItemStatement); + //Logger.Debug(_saveItemCommand.CommandText); + + if (item.SupportsAncestors) + { + UpdateAncestors(item.Id, tuple.Item2, db, deleteAncestorsStatement, updateAncestorsStatement); + } + + UpdateItemValues(item.Id, GetItemValuesToSave(item), db); + + requiresReset = true; + } + } + } } } + private void SaveItem(BaseItem item, BaseItem topParent, string userDataKey, IStatement saveItemStatement) + { + saveItemStatement.TryBind("@guid", item.Id); + saveItemStatement.TryBind("@type", item.GetType().FullName); + + if (TypeRequiresDeserialization(item.GetType())) + { + saveItemStatement.TryBind("@data", _jsonSerializer.SerializeToBytes(item, _memoryStreamProvider)); + } + else + { + saveItemStatement.TryBindNull("@data"); + } + + saveItemStatement.TryBind("@Path", item.Path); + + var hasStartDate = item as IHasStartDate; + if (hasStartDate != null) + { + saveItemStatement.TryBind("@StartDate", hasStartDate.StartDate); + } + else + { + saveItemStatement.TryBindNull("@StartDate"); + } + + if (item.EndDate.HasValue) + { + saveItemStatement.TryBind("@EndDate", item.EndDate.Value); + } + else + { + saveItemStatement.TryBindNull("@EndDate"); + } + + saveItemStatement.TryBind("@ChannelId", item.ChannelId); + + var hasProgramAttributes = item as IHasProgramAttributes; + if (hasProgramAttributes != null) + { + saveItemStatement.TryBind("@IsKids", hasProgramAttributes.IsKids); + saveItemStatement.TryBind("@IsMovie", hasProgramAttributes.IsMovie); + saveItemStatement.TryBind("@IsSports", hasProgramAttributes.IsSports); + saveItemStatement.TryBind("@IsSeries", hasProgramAttributes.IsSeries); + saveItemStatement.TryBind("@IsLive", hasProgramAttributes.IsLive); + saveItemStatement.TryBind("@IsNews", hasProgramAttributes.IsNews); + saveItemStatement.TryBind("@IsPremiere", hasProgramAttributes.IsPremiere); + saveItemStatement.TryBind("@EpisodeTitle", hasProgramAttributes.EpisodeTitle); + saveItemStatement.TryBind("@IsRepeat", hasProgramAttributes.IsRepeat); + } + else + { + saveItemStatement.TryBindNull("@IsKids"); + saveItemStatement.TryBindNull("@IsMovie"); + saveItemStatement.TryBindNull("@IsSports"); + saveItemStatement.TryBindNull("@IsSeries"); + saveItemStatement.TryBindNull("@IsLive"); + saveItemStatement.TryBindNull("@IsNews"); + saveItemStatement.TryBindNull("@IsPremiere"); + saveItemStatement.TryBindNull("@EpisodeTitle"); + saveItemStatement.TryBindNull("@IsRepeat"); + } + + saveItemStatement.TryBind("@CommunityRating", item.CommunityRating); + saveItemStatement.TryBind("@CustomRating", item.CustomRating); + saveItemStatement.TryBind("@IndexNumber", item.IndexNumber); + saveItemStatement.TryBind("@IsLocked", item.IsLocked); + saveItemStatement.TryBind("@Name", item.Name); + saveItemStatement.TryBind("@OfficialRating", item.OfficialRating); + saveItemStatement.TryBind("@MediaType", item.MediaType); + saveItemStatement.TryBind("@Overview", item.Overview); + saveItemStatement.TryBind("@ParentIndexNumber", item.ParentIndexNumber); + saveItemStatement.TryBind("@PremiereDate", item.PremiereDate); + saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); + + if (item.ParentId == Guid.Empty) + { + saveItemStatement.TryBindNull("@ParentId"); + } + else + { + saveItemStatement.TryBind("@ParentId", item.ParentId); + } + + if (item.Genres.Count > 0) + { + saveItemStatement.TryBind("@Genres", string.Join("|", item.Genres.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@Genres"); + } + + saveItemStatement.TryBind("@InheritedParentalRatingValue", item.InheritedParentalRatingValue); + + saveItemStatement.TryBind("@SortName", item.SortName); + saveItemStatement.TryBind("@RunTimeTicks", item.RunTimeTicks); + + saveItemStatement.TryBind("@OfficialRatingDescription", item.OfficialRatingDescription); + saveItemStatement.TryBind("@HomePageUrl", item.HomePageUrl); + saveItemStatement.TryBind("@VoteCount", item.VoteCount); + saveItemStatement.TryBind("@DisplayMediaType", item.DisplayMediaType); + saveItemStatement.TryBind("@DateCreated", item.DateCreated); + saveItemStatement.TryBind("@DateModified", item.DateModified); + + saveItemStatement.TryBind("@ForcedSortName", item.ForcedSortName); + saveItemStatement.TryBind("@LocationType", item.LocationType.ToString()); + + saveItemStatement.TryBind("@PreferredMetadataLanguage", item.PreferredMetadataLanguage); + saveItemStatement.TryBind("@PreferredMetadataCountryCode", item.PreferredMetadataCountryCode); + saveItemStatement.TryBind("@IsHD", item.IsHD); + saveItemStatement.TryBind("@ExternalEtag", item.ExternalEtag); + + if (item.DateLastRefreshed != default(DateTime)) + { + saveItemStatement.TryBind("@DateLastRefreshed", item.DateLastRefreshed); + } + else + { + saveItemStatement.TryBindNull("@DateLastRefreshed"); + } + + if (item.DateLastSaved != default(DateTime)) + { + saveItemStatement.TryBind("@DateLastSaved", item.DateLastSaved); + } + else + { + saveItemStatement.TryBindNull("@DateLastSaved"); + } + + saveItemStatement.TryBind("@IsInMixedFolder", item.IsInMixedFolder); + + if (item.LockedFields.Count > 0) + { + saveItemStatement.TryBind("@LockedFields", string.Join("|", item.LockedFields.Select(i => i.ToString()).ToArray())); + } + else + { + saveItemStatement.TryBindNull("@LockedFields"); + } + + if (item.Studios.Count > 0) + { + saveItemStatement.TryBind("@Studios", string.Join("|", item.Studios.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@Studios"); + } + + if (item.Audio.HasValue) + { + saveItemStatement.TryBind("@Audio", item.Audio.Value.ToString()); + } + else + { + saveItemStatement.TryBindNull("@Audio"); + } + + saveItemStatement.TryBind("@ExternalServiceId", item.ServiceName); + + if (item.Tags.Count > 0) + { + saveItemStatement.TryBind("@Tags", string.Join("|", item.Tags.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@Tags"); + } + + saveItemStatement.TryBind("@IsFolder", item.IsFolder); + + saveItemStatement.TryBind("@UnratedType", item.GetBlockUnratedType().ToString()); + + if (topParent != null) + { + //Logger.Debug("Item {0} has top parent {1}", item.Id, topParent.Id); + saveItemStatement.TryBind("@TopParentId", topParent.Id.ToString("N")); + } + else + { + //Logger.Debug("Item {0} has null top parent", item.Id); + saveItemStatement.TryBindNull("@TopParentId"); + } + + var isByName = false; + var byName = item as IItemByName; + if (byName != null) + { + var dualAccess = item as IHasDualAccess; + isByName = dualAccess == null || dualAccess.IsAccessedByName; + } + saveItemStatement.TryBind("@IsItemByName", isByName); + saveItemStatement.TryBind("@SourceType", item.SourceType.ToString()); + + var trailer = item as Trailer; + if (trailer != null && trailer.TrailerTypes.Count > 0) + { + saveItemStatement.TryBind("@TrailerTypes", string.Join("|", trailer.TrailerTypes.Select(i => i.ToString()).ToArray())); + } + else + { + saveItemStatement.TryBindNull("@TrailerTypes"); + } + + saveItemStatement.TryBind("@CriticRating", item.CriticRating); + saveItemStatement.TryBind("@CriticRatingSummary", item.CriticRatingSummary); + + var inheritedTags = item.InheritedTags; + if (inheritedTags.Count > 0) + { + saveItemStatement.TryBind("@InheritedTags", string.Join("|", inheritedTags.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@InheritedTags"); + } + + if (string.IsNullOrWhiteSpace(item.Name)) + { + saveItemStatement.TryBindNull("@CleanName"); + } + else + { + saveItemStatement.TryBind("@CleanName", GetCleanValue(item.Name)); + } + + saveItemStatement.TryBind("@PresentationUniqueKey", item.PresentationUniqueKey); + saveItemStatement.TryBind("@SlugName", item.SlugName); + saveItemStatement.TryBind("@OriginalTitle", item.OriginalTitle); + + var video = item as Video; + if (video != null) + { + saveItemStatement.TryBind("@PrimaryVersionId", video.PrimaryVersionId); + } + else + { + saveItemStatement.TryBindNull("@PrimaryVersionId"); + } + + var folder = item as Folder; + if (folder != null && folder.DateLastMediaAdded.HasValue) + { + saveItemStatement.TryBind("@DateLastMediaAdded", folder.DateLastMediaAdded.Value); + } + else + { + saveItemStatement.TryBindNull("@DateLastMediaAdded"); + } + + saveItemStatement.TryBind("@Album", item.Album); + saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem); + + var hasSeries = item as IHasSeries; + if (hasSeries != null) + { + saveItemStatement.TryBind("@SeriesName", hasSeries.SeriesName); + } + else + { + saveItemStatement.TryBindNull("@SeriesName"); + } + + if (string.IsNullOrWhiteSpace(userDataKey)) + { + saveItemStatement.TryBindNull("@UserDataKey"); + } + else + { + saveItemStatement.TryBind("@UserDataKey", userDataKey); + } + + var episode = item as Episode; + if (episode != null) + { + saveItemStatement.TryBind("@SeasonName", episode.SeasonName); + saveItemStatement.TryBind("@SeasonId", episode.SeasonId); + } + else + { + saveItemStatement.TryBindNull("@SeasonName"); + saveItemStatement.TryBindNull("@SeasonId"); + } + + if (hasSeries != null) + { + saveItemStatement.TryBind("@SeriesId", hasSeries.SeriesId); + saveItemStatement.TryBind("@SeriesSortName", hasSeries.SeriesSortName); + saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); + } + else + { + saveItemStatement.TryBindNull("@SeriesId"); + saveItemStatement.TryBindNull("@SeriesSortName"); + saveItemStatement.TryBindNull("@SeriesPresentationUniqueKey"); + } + + saveItemStatement.TryBind("@ExternalSeriesId", item.ExternalSeriesId); + saveItemStatement.TryBind("@ShortOverview", item.ShortOverview); + saveItemStatement.TryBind("@Tagline", item.Tagline); + + if (item.Keywords.Count > 0) + { + saveItemStatement.TryBind("@Keywords", string.Join("|", item.Keywords.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@Keywords"); + } + + saveItemStatement.TryBind("@ProviderIds", SerializeProviderIds(item)); + saveItemStatement.TryBind("@Images", SerializeImages(item)); + + if (item.ProductionLocations.Count > 0) + { + saveItemStatement.TryBind("@ProductionLocations", string.Join("|", item.ProductionLocations.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@ProductionLocations"); + } + + if (item.ThemeSongIds.Count > 0) + { + saveItemStatement.TryBind("@ThemeSongIds", string.Join("|", item.ThemeSongIds.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@ThemeSongIds"); + } + + if (item.ThemeVideoIds.Count > 0) + { + saveItemStatement.TryBind("@ThemeVideoIds", string.Join("|", item.ThemeVideoIds.ToArray())); + } + else + { + saveItemStatement.TryBindNull("@ThemeVideoIds"); + } + + saveItemStatement.TryBind("@TotalBitrate", item.TotalBitrate); + if (item.ExtraType.HasValue) + { + saveItemStatement.TryBind("@ExtraType", item.ExtraType.Value.ToString()); + } + else + { + saveItemStatement.TryBindNull("@ExtraType"); + } + + string artists = null; + var hasArtists = item as IHasArtist; + if (hasArtists != null) + { + if (hasArtists.Artists.Count > 0) + { + artists = string.Join("|", hasArtists.Artists.ToArray()); + } + } + saveItemStatement.TryBind("@Artists", artists); + + string albumArtists = null; + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null) + { + if (hasAlbumArtists.AlbumArtists.Count > 0) + { + albumArtists = string.Join("|", hasAlbumArtists.AlbumArtists.ToArray()); + } + } + saveItemStatement.TryBind("@AlbumArtists", albumArtists); + saveItemStatement.TryBind("@ExternalId", item.ExternalId); + + saveItemStatement.MoveNext(); + } + private string SerializeProviderIds(BaseItem item) { - var ids = item.ProviderIds.ToList(); + // Ideally we shouldn't need this IsNullOrWhiteSpace check but we're seeing some cases of bad data slip through + var ids = item.ProviderIds + .Where(i => !string.IsNullOrWhiteSpace(i.Value)) + .ToList(); if (ids.Count == 0) { @@ -1137,7 +1149,10 @@ namespace MediaBrowser.Server.Implementations.Persistence { var idParts = part.Split('='); - item.SetProviderId(idParts[0], idParts[1]); + if (idParts.Length == 2) + { + item.SetProviderId(idParts[0], idParts[1]); + } } } @@ -1150,7 +1165,9 @@ namespace MediaBrowser.Server.Implementations.Persistence return null; } - return string.Join("|", images.Select(ToValueString).ToArray()); + var imageStrings = images.Where(i => !string.IsNullOrWhiteSpace(i.Path)).Select(ToValueString).ToArray(); + + return string.Join("|", imageStrings); } private void DeserializeImages(string value, BaseItem item) @@ -1169,7 +1186,12 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var part in parts) { - item.ImageInfos.Add(ItemImageInfoFromValueString(part)); + var image = ItemImageInfoFromValueString(part); + + if (image != null) + { + item.ImageInfos.Add(image); + } } } @@ -1177,7 +1199,14 @@ namespace MediaBrowser.Server.Implementations.Persistence { var delimeter = "*"; - return (image.Path ?? string.Empty) + + var path = image.Path; + + if (path == null) + { + path = string.Empty; + } + + return path + delimeter + image.DateModified.Ticks.ToString(CultureInfo.InvariantCulture) + delimeter + @@ -1190,6 +1219,11 @@ namespace MediaBrowser.Server.Implementations.Persistence { var parts = value.Split(new[] { '*' }, StringSplitOptions.None); + if (parts.Length != 4) + { + return null; + } + var image = new ItemImageInfo(); image.Path = parts[0]; @@ -1215,24 +1249,27 @@ namespace MediaBrowser.Server.Implementations.Persistence } CheckDisposed(); - - using (var cmd = _connection.CreateCommand()) + //Logger.Info("Retrieving item {0}", id.ToString("N")); + using (WriteLock.Read()) { - cmd.CommandText = "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"; - cmd.Parameters.Add(cmd, "@guid", DbType.Guid).Value = id; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + using (var connection = CreateConnection(true)) { - if (reader.Read()) + using (var statement = PrepareStatementSafe(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid")) { - return GetItem(reader); + statement.TryBind("@guid", id); + + foreach (var row in statement.ExecuteQuery()) + { + return GetItem(row); + } } + + return null; } - return null; } } - private BaseItem GetItem(IDataReader reader) + private BaseItem GetItem(IReadOnlyList reader) { return GetItem(reader, new InternalItemsQuery()); } @@ -1313,11 +1350,34 @@ namespace MediaBrowser.Server.Implementations.Persistence return false; } } + if (_config.Configuration.SkipDeserializationForAudio) + { + if (type == typeof(Audio)) + { + return false; + } + if (type == typeof(LiveTvAudioRecording)) + { + return false; + } + if (type == typeof(AudioPodcast)) + { + return false; + } + if (type == typeof(AudioBook)) + { + return false; + } + if (type == typeof(MusicAlbum)) + { + return false; + } + } return true; } - private BaseItem GetItem(IDataReader reader, InternalItemsQuery query) + private BaseItem GetItem(IReadOnlyList reader, InternalItemsQuery query) { var typeString = reader.GetString(0); @@ -1334,8 +1394,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (TypeRequiresDeserialization(type)) { - using (var stream = reader.GetMemoryStream(1, _memoryStreamProvider)) + using (var stream = _memoryStreamProvider.CreateNew(reader[1].ToBlob())) { + stream.Position = 0; + try { item = _jsonSerializer.DeserializeFromStream(stream, type) as BaseItem; @@ -1368,75 +1430,83 @@ namespace MediaBrowser.Server.Implementations.Persistence var hasStartDate = item as IHasStartDate; if (hasStartDate != null) { - hasStartDate.StartDate = reader.GetDateTime(2).ToUniversalTime(); + hasStartDate.StartDate = reader[2].ReadDateTime(); } } if (!reader.IsDBNull(3)) { - item.EndDate = reader.GetDateTime(3).ToUniversalTime(); + item.EndDate = reader[3].ReadDateTime(); } if (!reader.IsDBNull(4)) { - item.IsOffline = reader.GetBoolean(4); + item.ChannelId = reader.GetString(4); } - if (!reader.IsDBNull(5)) - { - item.ChannelId = reader.GetString(5); - } + var index = 5; var hasProgramAttributes = item as IHasProgramAttributes; if (hasProgramAttributes != null) { - if (!reader.IsDBNull(6)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsMovie = reader.GetBoolean(6); + hasProgramAttributes.IsMovie = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(7)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsSports = reader.GetBoolean(7); + hasProgramAttributes.IsSports = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(8)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsKids = reader.GetBoolean(8); + hasProgramAttributes.IsKids = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(9)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsSeries = reader.GetBoolean(9); + hasProgramAttributes.IsSeries = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(10)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsLive = reader.GetBoolean(10); + hasProgramAttributes.IsLive = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(11)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsNews = reader.GetBoolean(11); + hasProgramAttributes.IsNews = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(12)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsPremiere = reader.GetBoolean(12); + hasProgramAttributes.IsPremiere = reader.GetBoolean(index); } + index++; - if (!reader.IsDBNull(13)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.EpisodeTitle = reader.GetString(13); + hasProgramAttributes.EpisodeTitle = reader.GetString(index); } + index++; - if (!reader.IsDBNull(14)) + if (!reader.IsDBNull(index)) { - hasProgramAttributes.IsRepeat = reader.GetBoolean(14); + hasProgramAttributes.IsRepeat = reader.GetBoolean(index); } + index++; + } + else + { + index += 9; } - - var index = 15; if (!reader.IsDBNull(index)) { @@ -1494,7 +1564,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(index)) { - item.DateLastRefreshed = reader.GetDateTime(index).ToUniversalTime(); + item.DateLastRefreshed = reader[index].ReadDateTime(); } index++; @@ -1512,7 +1582,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(index)) { - item.PremiereDate = reader.GetDateTime(index).ToUniversalTime(); + item.PremiereDate = reader[index].ReadDateTime(); } index++; @@ -1598,14 +1668,14 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (!reader.IsDBNull(index)) { - item.DateCreated = reader.GetDateTime(index).ToUniversalTime(); + item.DateCreated = reader[index].ReadDateTime(); } index++; } if (!reader.IsDBNull(index)) { - item.DateModified = reader.GetDateTime(index).ToUniversalTime(); + item.DateModified = reader[index].ReadDateTime(); } index++; @@ -1649,7 +1719,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(index)) { - item.DateLastSaved = reader.GetDateTime(index).ToUniversalTime(); + item.DateLastSaved = reader[index].ReadDateTime(); } index++; @@ -1720,7 +1790,7 @@ namespace MediaBrowser.Server.Implementations.Persistence var folder = item as Folder; if (folder != null && !reader.IsDBNull(index)) { - folder.DateLastMediaAdded = reader.GetDateTime(index).ToUniversalTime(); + folder.DateLastMediaAdded = reader[index].ReadDateTime(); } index++; } @@ -1892,6 +1962,47 @@ namespace MediaBrowser.Server.Implementations.Persistence index++; } + if (!reader.IsDBNull(index)) + { + item.TotalBitrate = reader.GetInt32(index); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExtraType = (ExtraType)Enum.Parse(typeof(ExtraType), reader.GetString(index), true); + } + index++; + + var hasArtists = item as IHasArtist; + if (hasArtists != null && !reader.IsDBNull(index)) + { + hasArtists.Artists = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + var hasAlbumArtists = item as IHasAlbumArtist; + if (hasAlbumArtists != null && !reader.IsDBNull(index)) + { + hasAlbumArtists.AlbumArtists = reader.GetString(index).Split('|').Where(i => !string.IsNullOrWhiteSpace(i)).ToList(); + } + index++; + + if (!reader.IsDBNull(index)) + { + item.ExternalId = reader.GetString(index); + } + index++; + + if (hasSeries != null) + { + if (!reader.IsDBNull(index)) + { + hasSeries.SeriesPresentationUniqueKey = reader.GetString(index); + } + } + index++; + if (string.IsNullOrWhiteSpace(item.Tagline)) { var movie = item as Movie; @@ -1926,11 +2037,11 @@ namespace MediaBrowser.Server.Implementations.Persistence return _jsonSerializer.DeserializeFromFile>(path); } - catch (DirectoryNotFoundException) + catch (FileNotFoundException) { return new List(); } - catch (FileNotFoundException) + catch (IOException) { return new List(); } @@ -1945,7 +2056,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Task. public Task SaveCriticReviews(Guid itemId, IEnumerable criticReviews) { - Directory.CreateDirectory(_criticReviewsPath); + _fileSystem.CreateDirectory(_criticReviewsPath); var path = Path.Combine(_criticReviewsPath, itemId + ".json"); @@ -1967,24 +2078,26 @@ namespace MediaBrowser.Server.Implementations.Persistence { throw new ArgumentNullException("id"); } - var list = new List(); - using (var cmd = _connection.CreateCommand()) + using (WriteLock.Read()) { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc"; - - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (var connection = CreateConnection(true)) { - while (reader.Read()) + var list = new List(); + + using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId order by ChapterIndex asc")) { - list.Add(GetChapter(reader)); + statement.TryBind("@ItemId", id); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetChapter(row)); + } } + + return list; } } - - return list; } /// @@ -2002,22 +2115,23 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("id"); } - using (var cmd = _connection.CreateCommand()) + using (WriteLock.Read()) { - cmd.CommandText = "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex"; - - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = id; - cmd.Parameters.Add(cmd, "@ChapterIndex", DbType.Int32).Value = index; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult | CommandBehavior.SingleRow)) + using (var connection = CreateConnection(true)) { - if (reader.Read()) + using (var statement = PrepareStatementSafe(connection, "select StartPositionTicks,Name,ImagePath,ImageDateModified from " + ChaptersTableName + " where ItemId = @ItemId and ChapterIndex=@ChapterIndex")) { - return GetChapter(reader); + statement.TryBind("@ItemId", id); + statement.TryBind("@ChapterIndex", index); + + foreach (var row in statement.ExecuteQuery()) + { + return GetChapter(row); + } } } - return null; } + return null; } /// @@ -2025,7 +2139,7 @@ namespace MediaBrowser.Server.Implementations.Persistence /// /// The reader. /// ChapterInfo. - private ChapterInfo GetChapter(IDataReader reader) + private ChapterInfo GetChapter(IReadOnlyList reader) { var chapter = new ChapterInfo { @@ -2044,7 +2158,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!reader.IsDBNull(3)) { - chapter.ImageDateModified = reader.GetDateTime(3).ToUniversalTime(); + chapter.ImageDateModified = reader[3].ReadDateTime(); } return chapter; @@ -2080,85 +2194,40 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); + var index = 0; - IDbTransaction transaction = null; - - try + using (WriteLock.Write()) { - transaction = _connection.BeginTransaction(); - - // First delete chapters - _deleteChaptersCommand.GetParameter(0).Value = id; - - _deleteChaptersCommand.Transaction = transaction; - - _deleteChaptersCommand.ExecuteNonQuery(); - - var index = 0; - - foreach (var chapter in chapters) + using (var connection = CreateConnection()) { - cancellationToken.ThrowIfCancellationRequested(); + connection.RunInTransaction(db => + { + // First delete chapters + db.Execute("delete from " + ChaptersTableName + " where ItemId=@ItemId", id.ToGuidParamValue()); - _saveChapterCommand.GetParameter(0).Value = id; - _saveChapterCommand.GetParameter(1).Value = index; - _saveChapterCommand.GetParameter(2).Value = chapter.StartPositionTicks; - _saveChapterCommand.GetParameter(3).Value = chapter.Name; - _saveChapterCommand.GetParameter(4).Value = chapter.ImagePath; - _saveChapterCommand.GetParameter(5).Value = chapter.ImageDateModified; + using (var saveChapterStatement = PrepareStatement(db, "replace into " + ChaptersTableName + " (ItemId, ChapterIndex, StartPositionTicks, Name, ImagePath, ImageDateModified) values (@ItemId, @ChapterIndex, @StartPositionTicks, @Name, @ImagePath, @ImageDateModified)")) + { + foreach (var chapter in chapters) + { + if (index > 0) + { + saveChapterStatement.Reset(); + } - _saveChapterCommand.Transaction = transaction; + saveChapterStatement.TryBind("@ItemId", id.ToGuidParamValue()); + saveChapterStatement.TryBind("@ChapterIndex", index); + saveChapterStatement.TryBind("@StartPositionTicks", chapter.StartPositionTicks); + saveChapterStatement.TryBind("@Name", chapter.Name); + saveChapterStatement.TryBind("@ImagePath", chapter.ImagePath); + saveChapterStatement.TryBind("@ImageDateModified", chapter.ImageDateModified); - _saveChapterCommand.ExecuteNonQuery(); + saveChapterStatement.MoveNext(); - index++; + index++; + } + } + }, TransactionMode); } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save chapters:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); - } - } - - protected override void CloseConnection() - { - if (_connection != null) - { - if (_connection.IsOpen()) - { - _connection.Close(); - } - - _connection.Dispose(); - _connection = null; } } @@ -2171,7 +2240,7 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.SimilarTo != null && query.User != null) { - return true; + //return true; } var sortingFields = query.SortBy.ToList(); @@ -2197,6 +2266,10 @@ namespace MediaBrowser.Server.Implementations.Persistence { return true; } + if (sortingFields.Contains(ItemSortBy.SeriesDatePlayed, StringComparer.OrdinalIgnoreCase)) + { + return true; + } if (query.IsFavoriteOrLiked.HasValue) { @@ -2252,7 +2325,7 @@ namespace MediaBrowser.Server.Implementations.Persistence return new[] { field.ToString() }; } - private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns, IDbCommand cmd) + private string[] GetFinalColumnsToSelect(InternalItemsQuery query, string[] startColumns) { var list = startColumns.ToList(); @@ -2274,13 +2347,13 @@ namespace MediaBrowser.Server.Implementations.Persistence if (EnableJoinUserData(query)) { - list.Add("UserDataDb.UserData.UserId"); - list.Add("UserDataDb.UserData.lastPlayedDate"); - list.Add("UserDataDb.UserData.playbackPositionTicks"); - list.Add("UserDataDb.UserData.playcount"); - list.Add("UserDataDb.UserData.isFavorite"); - list.Add("UserDataDb.UserData.played"); - list.Add("UserDataDb.UserData.rating"); + list.Add("UserData.UserId"); + list.Add("UserData.lastPlayedDate"); + list.Add("UserData.playbackPositionTicks"); + list.Add("UserData.playcount"); + list.Add("UserData.isFavorite"); + list.Add("UserData.played"); + list.Add("UserData.rating"); } if (query.SimilarTo != null) @@ -2296,15 +2369,10 @@ namespace MediaBrowser.Server.Implementations.Persistence builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 10 Then 2 Else 0 End )"); builder.Append("+(Select Case When Abs(COALESCE(ProductionYear, 0) - @ItemProductionYear) < 5 Then 2 Else 0 End )"); - //// genres - builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=2 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=2)) * 10)"); + //// genres, tags + builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type in (2,3,4,5) and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and Type in (2,3,4,5))) * 10)"); - //// tags - builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=4 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=4)) * 10)"); - - builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=5 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=5)) * 10)"); - - builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=3 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)"); + //builder.Append("+ ((Select count(CleanValue) from ItemValues where ItemId=Guid and Type=3 and CleanValue in (select CleanValue from itemvalues where ItemId=@SimilarItemId and type=3)) * 3)"); //builder.Append("+ ((Select count(Name) from People where ItemId=Guid and Name in (select Name from People where ItemId=@SimilarItemId)) * 3)"); @@ -2313,9 +2381,6 @@ namespace MediaBrowser.Server.Implementations.Persistence builder.Append(") as SimilarityScore"); list.Add(builder.ToString()); - cmd.Parameters.Add(cmd, "@ItemOfficialRating", DbType.String).Value = item.OfficialRating; - cmd.Parameters.Add(cmd, "@ItemProductionYear", DbType.Int32).Value = item.ProductionYear ?? 0; - cmd.Parameters.Add(cmd, "@SimilarItemId", DbType.Guid).Value = item.Id; var excludeIds = query.ExcludeItemIds.ToList(); excludeIds.Add(item.Id.ToString("N")); @@ -2327,6 +2392,20 @@ namespace MediaBrowser.Server.Implementations.Persistence return list.ToArray(); } + private void BindSimilarParams(InternalItemsQuery query, IStatement statement) + { + var item = query.SimilarTo; + + if (item == null) + { + return; + } + + statement.TryBind("@ItemOfficialRating", item.OfficialRating); + statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); + statement.TryBind("@SimilarItemId", item.Id); + } + private string GetJoinUserDataText(InternalItemsQuery query) { if (!EnableJoinUserData(query)) @@ -2334,12 +2413,7 @@ namespace MediaBrowser.Server.Implementations.Persistence return string.Empty; } - if (_config.Configuration.SchemaVersion >= 96) - { - return " left join UserDataDb.UserData on UserDataKey=UserDataDb.UserData.Key And (UserId=@UserId)"; - } - - return " left join UserDataDb.UserData on (select UserDataKey from UserDataKeys where ItemId=Guid order by Priority LIMIT 1)=UserDataDb.UserData.Key And (UserId=@UserId)"; + return " left join UserData on UserDataKey=UserData.Key And (UserId=@UserId)"; } private string GetGroupBy(InternalItemsQuery query) @@ -2364,6 +2438,63 @@ namespace MediaBrowser.Server.Implementations.Persistence return " from TypedBaseItems " + alias; } + public int GetCount(InternalItemsQuery query) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + CheckDisposed(); + + //Logger.Info("GetItemList: " + _environmentInfo.StackTrace); + + var now = DateTime.UtcNow; + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) + { + query.Limit = query.Limit.Value + 4; + } + + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new [] { "count(distinct PresentationUniqueKey)" })) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + //commandText += GetGroupBy(query); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = PrepareStatementSafe(connection, commandText)) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + var count = statement.ExecuteQuery().SelectScalarInt().First(); + LogQueryTime("GetCount", commandText, now); + return count; + } + } + + } + } + public List GetItemList(InternalItemsQuery query) { if (query == null) @@ -2373,9 +2504,9 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - var now = DateTime.UtcNow; + //Logger.Info("GetItemList: " + _environmentInfo.StackTrace); - var list = new List(); + var now = DateTime.UtcNow; // Hack for right now since we currently don't support filtering out these duplicates within a query if (query.Limit.HasValue && query.EnableGroupByMetadataKey) @@ -2383,79 +2514,93 @@ namespace MediaBrowser.Server.Implementations.Persistence query.Limit = query.Limit.Value + 4; } - using (var cmd = _connection.CreateCommand()) + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns)) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + commandText += GetGroupBy(query); + + commandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); - cmd.CommandText += GetJoinUserDataText(query); + var offset = query.StartIndex ?? 0; - if (EnableJoinUserData(query)) + if (query.Limit.HasValue || offset > 0) { - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - var whereClauses = GetWhereClauses(query, cmd); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (offset > 0) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); } + } - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - LogQueryTime("GetItemList", cmd, now); - - while (reader.Read()) + return connection.RunInTransaction(db => { - var item = GetItem(reader, query); - if (item != null) + var list = new List(); + + using (var statement = PrepareStatementSafe(db, commandText)) { - list.Add(item); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query); + if (item != null) + { + list.Add(item); + } + } } - } + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.EnableGroupByMetadataKey) + { + var limit = query.Limit ?? int.MaxValue; + limit -= 4; + var newList = new List(); + + foreach (var item in list) + { + AddItem(newList, item); + + if (newList.Count >= limit) + { + break; + } + } + + list = newList; + } + + LogQueryTime("GetItemList", commandText, now); + + return list; + + }, ReadTransactionMode); } } - - // Hack for right now since we currently don't support filtering out these duplicates within a query - if (query.EnableGroupByMetadataKey) - { - var limit = query.Limit ?? int.MaxValue; - limit -= 4; - var newList = new List(); - - foreach (var item in list) - { - AddItem(newList, item); - - if (newList.Count >= limit) - { - break; - } - } - - list = newList; - } - - return list; } private void AddItem(List items, BaseItem newItem) @@ -2486,21 +2631,21 @@ namespace MediaBrowser.Server.Implementations.Persistence items.Add(newItem); } - private void LogQueryTime(string methodName, IDbCommand cmd, DateTime startDate) + private void LogQueryTime(string methodName, string commandText, DateTime startDate) { var elapsed = (DateTime.UtcNow - startDate).TotalMilliseconds; var slowThreshold = 1000; #if DEBUG - slowThreshold = 50; + slowThreshold = 2; #endif if (elapsed >= slowThreshold) { Logger.Debug("{2} query time (slow): {0}ms. Query: {1}", Convert.ToInt32(elapsed), - cmd.CommandText, + commandText, methodName); } else @@ -2523,115 +2668,142 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { - var list = GetItemList(query); + var returnList = GetItemList(query); return new QueryResult { - Items = list.ToArray(), - TotalRecordCount = list.Count + Items = returnList.ToArray(), + TotalRecordCount = returnList.Count }; } + //Logger.Info("GetItems: " + _environmentInfo.StackTrace); var now = DateTime.UtcNow; - using (var cmd = _connection.CreateCommand()) + var list = new List(); + + // Hack for right now since we currently don't support filtering out these duplicates within a query + if (query.Limit.HasValue && query.EnableGroupByMetadataKey) { - cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns, cmd)) + GetFromText(); - cmd.CommandText += GetJoinUserDataText(query); + query.Limit = query.Limit.Value + 4; + } - if (EnableJoinUserData(query)) + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, _retriveItemColumns)) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var whereTextWithoutPaging = whereText; + + commandText += whereText; + + commandText += GetGroupBy(query); + + commandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) { - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - var whereClauses = GetWhereClauses(query, cmd); - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (offset > 0) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); } + } - cmd.CommandText += ";"; + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - if (isReturningZeroItems) - { - cmd.CommandText = ""; - } + var statementTexts = new List(); + if (!isReturningZeroItems) + { + statementTexts.Add(commandText); + } + if (query.EnableTotalRecordCount) + { + commandText = string.Empty; if (EnableGroupByPresentationUniqueKey(query)) { - cmd.CommandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); + commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); } else { - cmd.CommandText += " select count (guid)" + GetFromText(); + commandText += " select count (guid)" + GetFromText(); } - cmd.CommandText += GetJoinUserDataText(query); - cmd.CommandText += whereTextWithoutPaging; + commandText += GetJoinUserDataText(query); + commandText += whereTextWithoutPaging; + statementTexts.Add(commandText); + } - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - LogQueryTime("GetItems", cmd, now); + return connection.RunInTransaction(db => + { + var result = new QueryResult(); + var statements = PrepareAllSafe(db, statementTexts) + .ToList(); - if (isReturningZeroItems) - { - if (reader.Read()) + if (!isReturningZeroItems) { - count = reader.GetInt32(0); - } - } - else - { - while (reader.Read()) - { - var item = GetItem(reader, query); - if (item != null) + using (var statement = statements[0]) { - list.Add(item); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row, query); + if (item != null) + { + list.Add(item); + } + } } } - if (reader.NextResult() && reader.Read()) + if (query.EnableTotalRecordCount) { - count = reader.GetInt32(0); - } - } - } + using (var statement = statements[statements.Count - 1]) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } + } + + LogQueryTime("GetItems", commandText, now); + + result.Items = list.ToArray(); + return result; + + }, ReadTransactionMode); + } } } @@ -2743,7 +2915,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } if (string.Equals(name, ItemSortBy.SeriesDatePlayed, StringComparison.OrdinalIgnoreCase)) { - return new Tuple("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where B.Guid in (Select ItemId from AncestorIds where AncestorId in (select guid from typedbaseitems c where C.Type = 'MediaBrowser.Controller.Entities.TV.Series' And C.Guid in (Select AncestorId from AncestorIds where ItemId=A.Guid))))", false); + return new Tuple("(Select MAX(LastPlayedDate) from TypedBaseItems B" + GetJoinUserDataText(query) + " where Played=1 and B.SeriesPresentationUniqueKey=A.PresentationUniqueKey)", false); } return new Tuple(name, false); @@ -2757,63 +2929,72 @@ namespace MediaBrowser.Server.Implementations.Persistence } CheckDisposed(); + //Logger.Info("GetItemIdsList: " + _environmentInfo.StackTrace); var now = DateTime.UtcNow; - using (var cmd = _connection.CreateCommand()) + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" })) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + commandText += GetGroupBy(query); + + commandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); - cmd.CommandText += GetJoinUserDataText(query); + var offset = query.StartIndex ?? 0; - if (EnableJoinUserData(query)) + if (query.Limit.HasValue || offset > 0) { - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - var whereClauses = GetWhereClauses(query, cmd); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (offset > 0) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); } + } - var list = new List(); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - LogQueryTime("GetItemIdsList", cmd, now); + var list = new List(); - while (reader.Read()) + using (var statement = PrepareStatementSafe(connection, commandText)) { - list.Add(reader.GetGuid(0)); - } - } + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - return list; + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row[0].ReadGuid()); + } + } + + LogQueryTime("GetItemList", commandText, now); + + return list; + } } } - public QueryResult> GetItemIdsWithPath(InternalItemsQuery query) + public List> GetItemIdsWithPath(InternalItemsQuery query) { if (query == null) { @@ -2822,73 +3003,70 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - using (var cmd = _connection.CreateCommand()) + var now = DateTime.UtcNow; + + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid", "path" })) + GetFromText(); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + commandText += whereText; + + commandText += GetGroupBy(query); + + commandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText = "select guid,path from TypedBaseItems"; + var offset = query.StartIndex ?? 0; - var whereClauses = GetWhereClauses(query, cmd); - - var whereTextWithoutPaging = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (query.Limit.HasValue || offset > 0) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - cmd.CommandText += "; select count (guid) from TypedBaseItems" + whereTextWithoutPaging; - - var list = new List>(); - var count = 0; - - Logger.Debug(cmd.CommandText); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + if (offset > 0) { - while (reader.Read()) - { - var id = reader.GetGuid(0); - string path = null; + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); + } + } - if (!reader.IsDBNull(1)) + var list = new List>(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = PrepareStatementSafe(connection, commandText)) + { + if (EnableJoinUserData(query)) { - path = reader.GetString(1); + statement.TryBind("@UserId", query.User.Id); } - list.Add(new Tuple(id, path)); - } - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); + // Running this again will bind the params + GetWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) + { + var id = row.GetGuid(0); + string path = null; + + if (!row.IsDBNull(1)) + { + path = row.GetString(1); + } + list.Add(new Tuple(id, path)); + } } } - return new QueryResult>() - { - Items = list.ToArray(), - TotalRecordCount = count - }; + LogQueryTime("GetItemIdsWithPath", commandText, now); + + return list; } } @@ -2903,92 +3081,136 @@ namespace MediaBrowser.Server.Implementations.Persistence if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { - var list = GetItemIdsList(query); + var returnList = GetItemIdsList(query); return new QueryResult { - Items = list.ToArray(), - TotalRecordCount = list.Count + Items = returnList.ToArray(), + TotalRecordCount = returnList.Count }; } + //Logger.Info("GetItemIds: " + _environmentInfo.StackTrace); var now = DateTime.UtcNow; - using (var cmd = _connection.CreateCommand()) + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" })) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var whereClauses = GetWhereClauses(query, null); + + var whereText = whereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", whereClauses.ToArray()); + + var whereTextWithoutPaging = whereText; + + commandText += whereText; + + commandText += GetGroupBy(query); + + commandText += GetOrderByText(query); + + if (query.Limit.HasValue || query.StartIndex.HasValue) { - cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, new[] { "guid" }, cmd)) + GetFromText(); + var offset = query.StartIndex ?? 0; - var whereClauses = GetWhereClauses(query, cmd); - cmd.CommandText += GetJoinUserDataText(query); - - if (EnableJoinUserData(query)) + if (query.Limit.HasValue || offset > 0) { - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - cmd.CommandText += whereText; - - cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) + if (offset > 0) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); } + } + + var list = new List(); + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; + + var statementTexts = new List(); + if (!isReturningZeroItems) + { + statementTexts.Add(commandText); + } + if (query.EnableTotalRecordCount) + { + commandText = string.Empty; if (EnableGroupByPresentationUniqueKey(query)) { - cmd.CommandText += "; select count (distinct PresentationUniqueKey)" + GetFromText(); + commandText += " select count (distinct PresentationUniqueKey)" + GetFromText(); } else { - cmd.CommandText += "; select count (guid)" + GetFromText(); + commandText += " select count (guid)" + GetFromText(); } - cmd.CommandText += GetJoinUserDataText(query); - cmd.CommandText += whereText; + commandText += GetJoinUserDataText(query); + commandText += whereTextWithoutPaging; + statementTexts.Add(commandText); + } - var list = new List(); - var count = 0; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess)) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - LogQueryTime("GetItemIds", cmd, now); - - while (reader.Read()) + return connection.RunInTransaction(db => { - list.Add(reader.GetGuid(0)); - } + var result = new QueryResult(); - if (reader.NextResult() && reader.Read()) - { - count = reader.GetInt32(0); - } + var statements = PrepareAllSafe(db, statementTexts) + .ToList(); + + if (!isReturningZeroItems) + { + using (var statement = statements[0]) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row[0].ReadGuid()); + } + } + } + + if (query.EnableTotalRecordCount) + { + using (var statement = statements[statements.Count - 1]) + { + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + BindSimilarParams(query, statement); + + // Running this again will bind the params + GetWhereClauses(query, statement); + + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + } + } + + LogQueryTime("GetItemIds", commandText, now); + + result.Items = list.ToArray(); + return result; + + }, ReadTransactionMode); } - - return new QueryResult() - { - Items = list.ToArray(), - TotalRecordCount = count - }; } } - private List GetWhereClauses(InternalItemsQuery query, IDbCommand cmd, string paramSuffix = "") + private List GetWhereClauses(InternalItemsQuery query, IStatement statement, string paramSuffix = "") { var whereClauses = new List(); @@ -2996,32 +3218,21 @@ namespace MediaBrowser.Server.Implementations.Persistence { //whereClauses.Add("(UserId is null or UserId=@UserId)"); } - if (query.IsCurrentSchema.HasValue) - { - if (query.IsCurrentSchema.Value) - { - whereClauses.Add("(SchemaVersion not null AND SchemaVersion=@SchemaVersion)"); - } - else - { - whereClauses.Add("(SchemaVersion is null or SchemaVersion<>@SchemaVersion)"); - } - cmd.Parameters.Add(cmd, "@SchemaVersion", DbType.Int32).Value = LatestSchemaVersion; - } if (query.IsHD.HasValue) { whereClauses.Add("IsHD=@IsHD"); - cmd.Parameters.Add(cmd, "@IsHD", DbType.Boolean).Value = query.IsHD; + if (statement != null) + { + statement.TryBind("@IsHD", query.IsHD); + } } if (query.IsLocked.HasValue) { whereClauses.Add("IsLocked=@IsLocked"); - cmd.Parameters.Add(cmd, "@IsLocked", DbType.Boolean).Value = query.IsLocked; - } - if (query.IsOffline.HasValue) - { - whereClauses.Add("IsOffline=@IsOffline"); - cmd.Parameters.Add(cmd, "@IsOffline", DbType.Boolean).Value = query.IsOffline; + if (statement != null) + { + statement.TryBind("@IsLocked", query.IsLocked); + } } var exclusiveProgramAttribtues = !(query.IsMovie ?? true) || @@ -3047,33 +3258,51 @@ namespace MediaBrowser.Server.Implementations.Persistence if (alternateTypes.Count == 0) { whereClauses.Add("IsMovie=@IsMovie"); - cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + if (statement != null) + { + statement.TryBind("@IsMovie", query.IsMovie); + } } else { whereClauses.Add("(IsMovie is null OR IsMovie=@IsMovie)"); - cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = query.IsMovie; + if (statement != null) + { + statement.TryBind("@IsMovie", query.IsMovie); + } } } if (query.IsSeries.HasValue) { whereClauses.Add("IsSeries=@IsSeries"); - cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = query.IsSeries; + if (statement != null) + { + statement.TryBind("@IsSeries", query.IsSeries); + } } if (query.IsNews.HasValue) { whereClauses.Add("IsNews=@IsNews"); - cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = query.IsNews; + if (statement != null) + { + statement.TryBind("@IsNews", query.IsNews); + } } if (query.IsKids.HasValue) { whereClauses.Add("IsKids=@IsKids"); - cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = query.IsKids; + if (statement != null) + { + statement.TryBind("@IsKids", query.IsKids); + } } if (query.IsSports.HasValue) { whereClauses.Add("IsSports=@IsSports"); - cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = query.IsSports; + if (statement != null) + { + statement.TryBind("@IsSports", query.IsSports); + } } } else @@ -3100,27 +3329,42 @@ namespace MediaBrowser.Server.Implementations.Persistence programAttribtues.Add("(IsMovie is null OR IsMovie=@IsMovie)"); } - cmd.Parameters.Add(cmd, "@IsMovie", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsMovie", true); + } } if (query.IsSports ?? false) { programAttribtues.Add("IsSports=@IsSports"); - cmd.Parameters.Add(cmd, "@IsSports", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsSports", query.IsSports); + } } if (query.IsNews ?? false) { programAttribtues.Add("IsNews=@IsNews"); - cmd.Parameters.Add(cmd, "@IsNews", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsNews", query.IsNews); + } } if (query.IsSeries ?? false) { programAttribtues.Add("IsSeries=@IsSeries"); - cmd.Parameters.Add(cmd, "@IsSeries", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsSeries", query.IsSeries); + } } if (query.IsKids ?? false) { programAttribtues.Add("IsKids=@IsKids"); - cmd.Parameters.Add(cmd, "@IsKids", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsKids", query.IsKids); + } } if (programAttribtues.Count > 0) { @@ -3136,14 +3380,20 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.IsFolder.HasValue) { whereClauses.Add("IsFolder=@IsFolder"); - cmd.Parameters.Add(cmd, "@IsFolder", DbType.Boolean).Value = query.IsFolder; + if (statement != null) + { + statement.TryBind("@IsFolder", query.IsFolder); + } } var includeTypes = query.IncludeItemTypes.SelectMany(MapIncludeItemTypes).ToArray(); if (includeTypes.Length == 1) { whereClauses.Add("type=@type" + paramSuffix); - cmd.Parameters.Add(cmd, "@type" + paramSuffix, DbType.String).Value = includeTypes[0]; + if (statement != null) + { + statement.TryBind("@type" + paramSuffix, includeTypes[0]); + } } else if (includeTypes.Length > 1) { @@ -3155,7 +3405,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (excludeTypes.Length == 1) { whereClauses.Add("type<>@type"); - cmd.Parameters.Add(cmd, "@type", DbType.String).Value = excludeTypes[0]; + if (statement != null) + { + statement.TryBind("@type", excludeTypes[0]); + } } else if (excludeTypes.Length > 1) { @@ -3166,7 +3419,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.ChannelIds.Length == 1) { whereClauses.Add("ChannelId=@ChannelId"); - cmd.Parameters.Add(cmd, "@ChannelId", DbType.String).Value = query.ChannelIds[0]; + if (statement != null) + { + statement.TryBind("@ChannelId", query.ChannelIds[0]); + } } if (query.ChannelIds.Length > 1) { @@ -3177,43 +3433,64 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.ParentId.HasValue) { whereClauses.Add("ParentId=@ParentId"); - cmd.Parameters.Add(cmd, "@ParentId", DbType.Guid).Value = query.ParentId.Value; + if (statement != null) + { + statement.TryBind("@ParentId", query.ParentId.Value); + } } if (!string.IsNullOrWhiteSpace(query.Path)) { whereClauses.Add("Path=@Path"); - cmd.Parameters.Add(cmd, "@Path", DbType.String).Value = query.Path; + if (statement != null) + { + statement.TryBind("@Path", query.Path); + } } if (!string.IsNullOrWhiteSpace(query.PresentationUniqueKey)) { whereClauses.Add("PresentationUniqueKey=@PresentationUniqueKey"); - cmd.Parameters.Add(cmd, "@PresentationUniqueKey", DbType.String).Value = query.PresentationUniqueKey; + if (statement != null) + { + statement.TryBind("@PresentationUniqueKey", query.PresentationUniqueKey); + } } if (query.MinCommunityRating.HasValue) { whereClauses.Add("CommunityRating>=@MinCommunityRating"); - cmd.Parameters.Add(cmd, "@MinCommunityRating", DbType.Double).Value = query.MinCommunityRating.Value; + if (statement != null) + { + statement.TryBind("@MinCommunityRating", query.MinCommunityRating.Value); + } } if (query.MinIndexNumber.HasValue) { whereClauses.Add("IndexNumber>=@MinIndexNumber"); - cmd.Parameters.Add(cmd, "@MinIndexNumber", DbType.Int32).Value = query.MinIndexNumber.Value; + if (statement != null) + { + statement.TryBind("@MinIndexNumber", query.MinIndexNumber.Value); + } } if (query.MinDateCreated.HasValue) { whereClauses.Add("DateCreated>=@MinDateCreated"); - cmd.Parameters.Add(cmd, "@MinDateCreated", DbType.DateTime).Value = query.MinDateCreated.Value; + if (statement != null) + { + statement.TryBind("@MinDateCreated", query.MinDateCreated.Value); + } } if (query.MinDateLastSaved.HasValue) { whereClauses.Add("DateLastSaved>=@MinDateLastSaved"); - cmd.Parameters.Add(cmd, "@MinDateLastSaved", DbType.DateTime).Value = query.MinDateLastSaved.Value; + if (statement != null) + { + statement.TryBind("@MinDateLastSaved", query.MinDateLastSaved.Value); + } } //if (query.MinPlayers.HasValue) @@ -3231,57 +3508,87 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.IndexNumber.HasValue) { whereClauses.Add("IndexNumber=@IndexNumber"); - cmd.Parameters.Add(cmd, "@IndexNumber", DbType.Int32).Value = query.IndexNumber.Value; + if (statement != null) + { + statement.TryBind("@IndexNumber", query.IndexNumber.Value); + } } if (query.ParentIndexNumber.HasValue) { whereClauses.Add("ParentIndexNumber=@ParentIndexNumber"); - cmd.Parameters.Add(cmd, "@ParentIndexNumber", DbType.Int32).Value = query.ParentIndexNumber.Value; + if (statement != null) + { + statement.TryBind("@ParentIndexNumber", query.ParentIndexNumber.Value); + } } if (query.ParentIndexNumberNotEquals.HasValue) { whereClauses.Add("(ParentIndexNumber<>@ParentIndexNumberNotEquals or ParentIndexNumber is null)"); - cmd.Parameters.Add(cmd, "@ParentIndexNumberNotEquals", DbType.Int32).Value = query.ParentIndexNumberNotEquals.Value; + if (statement != null) + { + statement.TryBind("@ParentIndexNumberNotEquals", query.ParentIndexNumberNotEquals.Value); + } } if (query.MinEndDate.HasValue) { whereClauses.Add("EndDate>=@MinEndDate"); - cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = query.MinEndDate.Value; + if (statement != null) + { + statement.TryBind("@MinEndDate", query.MinEndDate.Value); + } } if (query.MaxEndDate.HasValue) { whereClauses.Add("EndDate<=@MaxEndDate"); - cmd.Parameters.Add(cmd, "@MaxEndDate", DbType.Date).Value = query.MaxEndDate.Value; + if (statement != null) + { + statement.TryBind("@MaxEndDate", query.MaxEndDate.Value); + } } if (query.MinStartDate.HasValue) { whereClauses.Add("StartDate>=@MinStartDate"); - cmd.Parameters.Add(cmd, "@MinStartDate", DbType.Date).Value = query.MinStartDate.Value; + if (statement != null) + { + statement.TryBind("@MinStartDate", query.MinStartDate.Value); + } } if (query.MaxStartDate.HasValue) { whereClauses.Add("StartDate<=@MaxStartDate"); - cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = query.MaxStartDate.Value; + if (statement != null) + { + statement.TryBind("@MaxStartDate", query.MaxStartDate.Value); + } } if (query.MinPremiereDate.HasValue) { whereClauses.Add("PremiereDate>=@MinPremiereDate"); - cmd.Parameters.Add(cmd, "@MinPremiereDate", DbType.Date).Value = query.MinPremiereDate.Value; + if (statement != null) + { + statement.TryBind("@MinPremiereDate", query.MinPremiereDate.Value); + } } if (query.MaxPremiereDate.HasValue) { whereClauses.Add("PremiereDate<=@MaxPremiereDate"); - cmd.Parameters.Add(cmd, "@MaxPremiereDate", DbType.Date).Value = query.MaxPremiereDate.Value; + if (statement != null) + { + statement.TryBind("@MaxPremiereDate", query.MaxPremiereDate.Value); + } } if (query.SourceTypes.Length == 1) { whereClauses.Add("SourceType=@SourceType"); - cmd.Parameters.Add(cmd, "@SourceType", DbType.String).Value = query.SourceTypes[0]; + if (statement != null) + { + statement.TryBind("@SourceType", query.SourceTypes[0].ToString()); + } } else if (query.SourceTypes.Length > 1) { @@ -3291,8 +3598,11 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.ExcludeSourceTypes.Length == 1) { - whereClauses.Add("SourceType<>@SourceType"); - cmd.Parameters.Add(cmd, "@SourceType", DbType.String).Value = query.SourceTypes[0]; + whereClauses.Add("SourceType<>@ExcludeSourceTypes"); + if (statement != null) + { + statement.TryBind("@ExcludeSourceTypes", query.ExcludeSourceTypes[0].ToString()); + } } else if (query.ExcludeSourceTypes.Length > 1) { @@ -3307,7 +3617,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var type in query.TrailerTypes) { clauses.Add("TrailerTypes like @TrailerTypes" + index); - cmd.Parameters.Add(cmd, "@TrailerTypes" + index, DbType.String).Value = "%" + type + "%"; + if (statement != null) + { + statement.TryBind("@TrailerTypes" + index, "%" + type + "%"); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3319,87 +3632,143 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.IsAiring.Value) { whereClauses.Add("StartDate<=@MaxStartDate"); - cmd.Parameters.Add(cmd, "@MaxStartDate", DbType.Date).Value = DateTime.UtcNow; + if (statement != null) + { + statement.TryBind("@MaxStartDate", DateTime.UtcNow); + } whereClauses.Add("EndDate>=@MinEndDate"); - cmd.Parameters.Add(cmd, "@MinEndDate", DbType.Date).Value = DateTime.UtcNow; + if (statement != null) + { + statement.TryBind("@MinEndDate", DateTime.UtcNow); + } } else { whereClauses.Add("(StartDate>@IsAiringDate OR EndDate < @IsAiringDate)"); - cmd.Parameters.Add(cmd, "@IsAiringDate", DbType.Date).Value = DateTime.UtcNow; + if (statement != null) + { + statement.TryBind("@IsAiringDate", DateTime.UtcNow); + } } } if (query.PersonIds.Length > 0) { - // Todo: improve without having to do this - query.Person = query.PersonIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).FirstOrDefault(); + // TODO: Should this query with CleanName ? + + var clauses = new List(); + var index = 0; + foreach (var personId in query.PersonIds) + { + var paramName = "@PersonId" + index; + + clauses.Add("(select Name from TypedBaseItems where guid=" + paramName + ") in (select Name from People where ItemId=Guid)"); + if (statement != null) + { + statement.TryBind(paramName, personId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (!string.IsNullOrWhiteSpace(query.Person)) { whereClauses.Add("Guid in (select ItemId from People where Name=@PersonName)"); - cmd.Parameters.Add(cmd, "@PersonName", DbType.String).Value = query.Person; + if (statement != null) + { + statement.TryBind("@PersonName", query.Person); + } } if (!string.IsNullOrWhiteSpace(query.SlugName)) { whereClauses.Add("SlugName=@SlugName"); - cmd.Parameters.Add(cmd, "@SlugName", DbType.String).Value = query.SlugName; + if (statement != null) + { + statement.TryBind("@SlugName", query.SlugName); + } } if (!string.IsNullOrWhiteSpace(query.MinSortName)) { whereClauses.Add("SortName>=@MinSortName"); - cmd.Parameters.Add(cmd, "@MinSortName", DbType.String).Value = query.MinSortName; + if (statement != null) + { + statement.TryBind("@MinSortName", query.MinSortName); + } } if (!string.IsNullOrWhiteSpace(query.ExternalSeriesId)) { whereClauses.Add("ExternalSeriesId=@ExternalSeriesId"); - cmd.Parameters.Add(cmd, "@ExternalSeriesId", DbType.String).Value = query.ExternalSeriesId; + if (statement != null) + { + statement.TryBind("@ExternalSeriesId", query.ExternalSeriesId); + } + } + + if (!string.IsNullOrWhiteSpace(query.ExternalId)) + { + whereClauses.Add("ExternalId=@ExternalId"); + if (statement != null) + { + statement.TryBind("@ExternalId", query.ExternalId); + } } if (!string.IsNullOrWhiteSpace(query.Name)) { whereClauses.Add("CleanName=@Name"); - cmd.Parameters.Add(cmd, "@Name", DbType.String).Value = GetCleanValue(query.Name); + + if (statement != null) + { + statement.TryBind("@Name", GetCleanValue(query.Name)); + } } if (!string.IsNullOrWhiteSpace(query.NameContains)) { whereClauses.Add("CleanName like @NameContains"); - cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + GetCleanValue(query.NameContains) + "%"; + if (statement != null) + { + statement.TryBind("@NameContains", "%" + GetCleanValue(query.NameContains) + "%"); + } } if (!string.IsNullOrWhiteSpace(query.NameStartsWith)) { whereClauses.Add("SortName like @NameStartsWith"); - cmd.Parameters.Add(cmd, "@NameStartsWith", DbType.String).Value = query.NameStartsWith + "%"; + if (statement != null) + { + statement.TryBind("@NameStartsWith", query.NameStartsWith + "%"); + } } if (!string.IsNullOrWhiteSpace(query.NameStartsWithOrGreater)) { whereClauses.Add("SortName >= @NameStartsWithOrGreater"); // lowercase this because SortName is stored as lowercase - cmd.Parameters.Add(cmd, "@NameStartsWithOrGreater", DbType.String).Value = query.NameStartsWithOrGreater.ToLower(); + if (statement != null) + { + statement.TryBind("@NameStartsWithOrGreater", query.NameStartsWithOrGreater.ToLower()); + } } if (!string.IsNullOrWhiteSpace(query.NameLessThan)) { whereClauses.Add("SortName < @NameLessThan"); // lowercase this because SortName is stored as lowercase - cmd.Parameters.Add(cmd, "@NameLessThan", DbType.String).Value = query.NameLessThan.ToLower(); + if (statement != null) + { + statement.TryBind("@NameLessThan", query.NameLessThan.ToLower()); + } } - if (query.ImageTypes.Length > 0 && _config.Configuration.SchemaVersion >= 87) + if (query.ImageTypes.Length > 0) { - var requiredImageIndex = 0; - foreach (var requiredImage in query.ImageTypes) { - var paramName = "@RequiredImageType" + requiredImageIndex; - whereClauses.Add("(select path from images where ItemId=Guid and ImageType=" + paramName + " limit 1) not null"); - cmd.Parameters.Add(cmd, paramName, DbType.Int32).Value = (int)requiredImage; - requiredImageIndex++; + whereClauses.Add("Images like '%" + requiredImage + "%'"); } } @@ -3408,12 +3777,18 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.IsLiked.Value) { whereClauses.Add("rating>=@UserRating"); - cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + if (statement != null) + { + statement.TryBind("@UserRating", UserItemData.MinLikeValue); + } } else { whereClauses.Add("(rating is null or rating<@UserRating)"); - cmd.Parameters.Add(cmd, "@UserRating", DbType.Double).Value = UserItemData.MinLikeValue; + if (statement != null) + { + statement.TryBind("@UserRating", UserItemData.MinLikeValue); + } } } @@ -3427,7 +3802,10 @@ namespace MediaBrowser.Server.Implementations.Persistence { whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavoriteOrLiked)"); } - cmd.Parameters.Add(cmd, "@IsFavoriteOrLiked", DbType.Boolean).Value = query.IsFavoriteOrLiked.Value; + if (statement != null) + { + statement.TryBind("@IsFavoriteOrLiked", query.IsFavoriteOrLiked.Value); + } } if (query.IsFavorite.HasValue) @@ -3440,7 +3818,10 @@ namespace MediaBrowser.Server.Implementations.Persistence { whereClauses.Add("(IsFavorite is null or IsFavorite=@IsFavorite)"); } - cmd.Parameters.Add(cmd, "@IsFavorite", DbType.Boolean).Value = query.IsFavorite.Value; + if (statement != null) + { + statement.TryBind("@IsFavorite", query.IsFavorite.Value); + } } if (EnableJoinUserData(query)) @@ -3455,7 +3836,10 @@ namespace MediaBrowser.Server.Implementations.Persistence { whereClauses.Add("(played is null or played=@IsPlayed)"); } - cmd.Parameters.Add(cmd, "@IsPlayed", DbType.Boolean).Value = query.IsPlayed.Value; + if (statement != null) + { + statement.TryBind("@IsPlayed", query.IsPlayed.Value); + } } } @@ -3478,7 +3862,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var artist in query.ArtistNames) { clauses.Add("@ArtistName" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); - cmd.Parameters.Add(cmd, "@ArtistName" + index, DbType.String).Value = GetCleanValue(artist); + if (statement != null) + { + statement.TryBind("@ArtistName" + index, GetCleanValue(artist)); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3491,22 +3878,36 @@ namespace MediaBrowser.Server.Implementations.Persistence var index = 0; foreach (var artistId in query.ExcludeArtistIds) { - var artistItem = RetrieveItem(new Guid(artistId)); - if (artistItem != null) + var paramName = "@ExcludeArtistId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type<=1)"); + if (statement != null) { - clauses.Add("@ExcludeArtistName" + index + " not in (select CleanValue from itemvalues where ItemId=Guid and Type <= 1)"); - cmd.Parameters.Add(cmd, "@ExcludeArtistName" + index, DbType.String).Value = GetCleanValue(artistItem.Name); - index++; + statement.TryBind(paramName, artistId.ToGuidParamValue()); } + index++; } - var clause = "(" + string.Join(" AND ", clauses.ToArray()) + ")"; + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; whereClauses.Add(clause); } if (query.GenreIds.Length > 0) { - // Todo: improve without having to do this - query.Genres = query.GenreIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + var clauses = new List(); + var index = 0; + foreach (var genreId in query.GenreIds) + { + var paramName = "@GenreId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); + if (statement != null) + { + statement.TryBind(paramName, genreId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (query.Genres.Length > 0) @@ -3516,7 +3917,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var item in query.Genres) { clauses.Add("@Genre" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=2)"); - cmd.Parameters.Add(cmd, "@Genre" + index, DbType.String).Value = GetCleanValue(item); + if (statement != null) + { + statement.TryBind("@Genre" + index, GetCleanValue(item)); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3530,7 +3934,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var item in query.Tags) { clauses.Add("@Tag" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=4)"); - cmd.Parameters.Add(cmd, "@Tag" + index, DbType.String).Value = GetCleanValue(item); + if (statement != null) + { + statement.TryBind("@Tag" + index, GetCleanValue(item)); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3539,8 +3946,21 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.StudioIds.Length > 0) { - // Todo: improve without having to do this - query.Studios = query.StudioIds.Select(i => RetrieveItem(new Guid(i))).Where(i => i != null).Select(i => i.Name).ToArray(); + var clauses = new List(); + var index = 0; + foreach (var studioId in query.StudioIds) + { + var paramName = "@StudioId" + index; + + clauses.Add("(select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); + if (statement != null) + { + statement.TryBind(paramName, studioId.ToGuidParamValue()); + } + index++; + } + var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; + whereClauses.Add(clause); } if (query.Studios.Length > 0) @@ -3550,7 +3970,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var item in query.Studios) { clauses.Add("@Studio" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=3)"); - cmd.Parameters.Add(cmd, "@Studio" + index, DbType.String).Value = GetCleanValue(item); + if (statement != null) + { + statement.TryBind("@Studio" + index, GetCleanValue(item)); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3564,7 +3987,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var item in query.Keywords) { clauses.Add("@Keyword" + index + " in (select CleanValue from itemvalues where ItemId=Guid and Type=5)"); - cmd.Parameters.Add(cmd, "@Keyword" + index, DbType.String).Value = GetCleanValue(item); + if (statement != null) + { + statement.TryBind("@Keyword" + index, GetCleanValue(item)); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3578,7 +4004,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var item in query.OfficialRatings) { clauses.Add("OfficialRating=@OfficialRating" + index); - cmd.Parameters.Add(cmd, "@OfficialRating" + index, DbType.String).Value = item; + if (statement != null) + { + statement.TryBind("@OfficialRating" + index, item); + } index++; } var clause = "(" + string.Join(" OR ", clauses.ToArray()) + ")"; @@ -3588,13 +4017,19 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.MinParentalRating.HasValue) { whereClauses.Add("InheritedParentalRatingValue<=@MinParentalRating"); - cmd.Parameters.Add(cmd, "@MinParentalRating", DbType.Int32).Value = query.MinParentalRating.Value; + if (statement != null) + { + statement.TryBind("@MinParentalRating", query.MinParentalRating.Value); + } } if (query.MaxParentalRating.HasValue) { whereClauses.Add("InheritedParentalRatingValue<=@MaxParentalRating"); - cmd.Parameters.Add(cmd, "@MaxParentalRating", DbType.Int32).Value = query.MaxParentalRating.Value; + if (statement != null) + { + statement.TryBind("@MaxParentalRating", query.MaxParentalRating.Value); + } } if (query.HasParentalRating.HasValue) @@ -3632,7 +4067,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.Years.Length == 1) { whereClauses.Add("ProductionYear=@Years"); - cmd.Parameters.Add(cmd, "@Years", DbType.Int32).Value = query.Years[0].ToString(); + if (statement != null) + { + statement.TryBind("@Years", query.Years[0].ToString()); + } } else if (query.Years.Length > 1) { @@ -3650,7 +4088,10 @@ namespace MediaBrowser.Server.Implementations.Persistence else { whereClauses.Add("LocationType=@LocationType"); - cmd.Parameters.Add(cmd, "@LocationType", DbType.String).Value = query.LocationTypes[0].ToString(); + if (statement != null) + { + statement.TryBind("@LocationType", query.LocationTypes[0].ToString()); + } } } else if (query.LocationTypes.Length > 1) @@ -3668,7 +4109,10 @@ namespace MediaBrowser.Server.Implementations.Persistence else { whereClauses.Add("LocationType<>@ExcludeLocationTypes"); - cmd.Parameters.Add(cmd, "@ExcludeLocationTypes", DbType.String).Value = query.ExcludeLocationTypes[0].ToString(); + if (statement != null) + { + statement.TryBind("@ExcludeLocationTypes", query.ExcludeLocationTypes[0].ToString()); + } } } else if (query.ExcludeLocationTypes.Length > 1) @@ -3679,14 +4123,10 @@ namespace MediaBrowser.Server.Implementations.Persistence } if (query.IsVirtualItem.HasValue) { - if (_config.Configuration.SchemaVersion >= 90) + whereClauses.Add("IsVirtualItem=@IsVirtualItem"); + if (statement != null) { - whereClauses.Add("IsVirtualItem=@IsVirtualItem"); - cmd.Parameters.Add(cmd, "@IsVirtualItem", DbType.Boolean).Value = query.IsVirtualItem.Value; - } - else if (!query.IsVirtualItem.Value) - { - whereClauses.Add("LocationType<>'Virtual'"); + statement.TryBind("@IsVirtualItem", query.IsVirtualItem.Value); } } if (query.IsSpecialSeason.HasValue) @@ -3711,7 +4151,7 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("PremiereDate < DATETIME('now')"); } } - if (query.IsMissing.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsMissing.HasValue) { if (query.IsMissing.Value) { @@ -3722,7 +4162,7 @@ namespace MediaBrowser.Server.Implementations.Persistence whereClauses.Add("(IsVirtualItem=0 OR PremiereDate >= DATETIME('now'))"); } } - if (query.IsVirtualUnaired.HasValue && _config.Configuration.SchemaVersion >= 90) + if (query.IsVirtualUnaired.HasValue) { if (query.IsVirtualUnaired.Value) { @@ -3736,7 +4176,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.MediaTypes.Length == 1) { whereClauses.Add("MediaType=@MediaTypes"); - cmd.Parameters.Add(cmd, "@MediaTypes", DbType.String).Value = query.MediaTypes[0]; + if (statement != null) + { + statement.TryBind("@MediaTypes", query.MediaTypes[0]); + } } if (query.MediaTypes.Length > 1) { @@ -3752,7 +4195,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var id in query.ItemIds) { includeIds.Add("Guid = @IncludeId" + index); - cmd.Parameters.Add(cmd, "@IncludeId" + index, DbType.Guid).Value = new Guid(id); + if (statement != null) + { + statement.TryBind("@IncludeId" + index, new Guid(id)); + } index++; } @@ -3766,7 +4212,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var id in query.ExcludeItemIds) { excludeIds.Add("Guid <> @ExcludeId" + index); - cmd.Parameters.Add(cmd, "@ExcludeId" + index, DbType.Guid).Value = new Guid(id); + if (statement != null) + { + statement.TryBind("@ExcludeId" + index, new Guid(id)); + } index++; } @@ -3786,9 +4235,15 @@ namespace MediaBrowser.Server.Implementations.Persistence } var paramName = "@ExcludeProviderId" + index; - excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); - cmd.Parameters.Add(cmd, paramName, DbType.String).Value = pair.Value; + //excludeIds.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = '" + pair.Key + "'), '') <> " + paramName + ")"); + excludeIds.Add("ProviderIds not like " + paramName); + if (statement != null) + { + statement.TryBind(paramName, "%" + pair.Key + "=" + pair.Value + "%"); + } index++; + + break; } whereClauses.Add(string.Join(" AND ", excludeIds.ToArray())); @@ -3796,20 +4251,17 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.HasImdbId.HasValue) { - var fn = query.HasImdbId.Value ? "<>" : "="; - whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Imdb'), '') " + fn + " '')"); + whereClauses.Add("ProviderIds like '%imdb=%'"); } if (query.HasTmdbId.HasValue) { - var fn = query.HasTmdbId.Value ? "<>" : "="; - whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tmdb'), '') " + fn + " '')"); + whereClauses.Add("ProviderIds like '%tmdb=%'"); } if (query.HasTvdbId.HasValue) { - var fn = query.HasTvdbId.Value ? "<>" : "="; - whereClauses.Add("(COALESCE((select value from ProviderIds where ItemId=Guid and Name = 'Tvdb'), '') " + fn + " '')"); + whereClauses.Add("ProviderIds like '%tvdb=%'"); } if (query.AlbumNames.Length > 0) @@ -3824,7 +4276,11 @@ namespace MediaBrowser.Server.Implementations.Persistence clause += " OR "; } clause += "Album=@AlbumName" + index; - cmd.Parameters.Add(cmd, "@AlbumName" + index, DbType.String).Value = name; + + if (statement != null) + { + statement.TryBind("@AlbumName" + index, name); + } index++; } @@ -3862,13 +4318,19 @@ namespace MediaBrowser.Server.Implementations.Persistence if (enableItemsByName) { whereClauses.Add("(TopParentId=@TopParentId or IsItemByName=@IsItemByName)"); - cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsItemByName", true); + } } else { whereClauses.Add("(TopParentId=@TopParentId)"); } - cmd.Parameters.Add(cmd, "@TopParentId", DbType.String).Value = query.TopParentIds[0]; + if (statement != null) + { + statement.TryBind("@TopParentId", query.TopParentIds[0]); + } } if (query.TopParentIds.Length > 1) { @@ -3877,7 +4339,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (enableItemsByName) { whereClauses.Add("(IsItemByName=@IsItemByName or TopParentId in (" + val + "))"); - cmd.Parameters.Add(cmd, "@IsItemByName", DbType.Boolean).Value = true; + if (statement != null) + { + statement.TryBind("@IsItemByName", true); + } } else { @@ -3888,7 +4353,11 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.AncestorIds.Length == 1) { whereClauses.Add("Guid in (select itemId from AncestorIds where AncestorId=@AncestorId)"); - cmd.Parameters.Add(cmd, "@AncestorId", DbType.Guid).Value = new Guid(query.AncestorIds[0]); + + if (statement != null) + { + statement.TryBind("@AncestorId", new Guid(query.AncestorIds[0])); + } } if (query.AncestorIds.Length > 1) { @@ -3899,13 +4368,29 @@ namespace MediaBrowser.Server.Implementations.Persistence { var inClause = "select guid from TypedBaseItems where PresentationUniqueKey=@AncestorWithPresentationUniqueKey"; whereClauses.Add(string.Format("Guid in (select itemId from AncestorIds where AncestorId in ({0}))", inClause)); - cmd.Parameters.Add(cmd, "@AncestorWithPresentationUniqueKey", DbType.String).Value = query.AncestorWithPresentationUniqueKey; + if (statement != null) + { + statement.TryBind("@AncestorWithPresentationUniqueKey", query.AncestorWithPresentationUniqueKey); + } + } + + if (!string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey)) + { + whereClauses.Add("SeriesPresentationUniqueKey=@SeriesPresentationUniqueKey"); + + if (statement != null) + { + statement.TryBind("@SeriesPresentationUniqueKey", query.SeriesPresentationUniqueKey); + } } if (query.BlockUnratedItems.Length == 1) { whereClauses.Add("(InheritedParentalRatingValue > 0 or UnratedType <> @UnratedType)"); - cmd.Parameters.Add(cmd, "@UnratedType", DbType.String).Value = query.BlockUnratedItems[0].ToString(); + if (statement != null) + { + statement.TryBind("@UnratedType", query.BlockUnratedItems[0].ToString()); + } } if (query.BlockUnratedItems.Length > 1) { @@ -3917,7 +4402,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var excludeTag in query.ExcludeTags) { whereClauses.Add("(Tags is null OR Tags not like @excludeTag" + excludeTagIndex + ")"); - cmd.Parameters.Add(cmd, "@excludeTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + if (statement != null) + { + statement.TryBind("@excludeTag" + excludeTagIndex, "%" + excludeTag + "%"); + } excludeTagIndex++; } @@ -3925,7 +4413,10 @@ namespace MediaBrowser.Server.Implementations.Persistence foreach (var excludeTag in query.ExcludeInheritedTags) { whereClauses.Add("(InheritedTags is null OR InheritedTags not like @excludeInheritedTag" + excludeTagIndex + ")"); - cmd.Parameters.Add(cmd, "@excludeInheritedTag" + excludeTagIndex, DbType.String).Value = "%" + excludeTag + "%"; + if (statement != null) + { + statement.TryBind("@excludeInheritedTag" + excludeTagIndex, "%" + excludeTag + "%"); + } excludeTagIndex++; } @@ -3995,6 +4486,7 @@ namespace MediaBrowser.Server.Implementations.Persistence typeof(Movie), typeof(Playlist), typeof(AudioPodcast), + typeof(AudioBook), typeof(Trailer), typeof(BoxSet), typeof(Episode), @@ -4028,76 +4520,41 @@ namespace MediaBrowser.Server.Implementations.Persistence { var newValues = new List>(); - using (var cmd = _connection.CreateCommand()) - { - cmd.CommandText = "select Guid,InheritedTags,(select group_concat(Tags, '|') from TypedBaseItems where (guid=outer.guid) OR (guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid))) as NewInheritedTags from typedbaseitems as Outer where NewInheritedTags <> InheritedTags"; + var commandText = "select Guid,InheritedTags,(select group_concat(Tags, '|') from TypedBaseItems where (guid=outer.guid) OR (guid in (Select AncestorId from AncestorIds where ItemId=Outer.guid))) as NewInheritedTags from typedbaseitems as Outer where NewInheritedTags <> InheritedTags"; - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) { - while (reader.Read()) + foreach (var row in connection.Query(commandText)) { - var id = reader.GetGuid(0); - string value = reader.IsDBNull(2) ? null : reader.GetString(2); + var id = row.GetGuid(0); + string value = row.IsDBNull(2) ? null : row.GetString(2); newValues.Add(new Tuple(id, value)); } + + Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); + if (newValues.Count == 0) + { + return; + } + + // write lock here + using (var statement = PrepareStatement(connection, "Update TypedBaseItems set InheritedTags=@InheritedTags where Guid=@Guid")) + { + foreach (var item in newValues) + { + var paramList = new List(); + + paramList.Add(item.Item1); + paramList.Add(item.Item2); + + statement.Execute(paramList.ToArray()); + } + } } } - - Logger.Debug("UpdateInheritedTags - {0} rows", newValues.Count); - if (newValues.Count == 0) - { - return; - } - - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try - { - transaction = _connection.BeginTransaction(); - - foreach (var item in newValues) - { - _updateInheritedTagsCommand.GetParameter(0).Value = item.Item1; - _updateInheritedTagsCommand.GetParameter(1).Value = item.Item2; - - _updateInheritedTagsCommand.Transaction = transaction; - _updateInheritedTagsCommand.ExecuteNonQuery(); - } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Error running query:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); - } } private static Dictionary GetTypeMapDictionary() @@ -4139,89 +4596,41 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (WriteLock.Write()) { - transaction = _connection.BeginTransaction(); - - // Delete people - _deletePeopleCommand.GetParameter(0).Value = id; - _deletePeopleCommand.Transaction = transaction; - _deletePeopleCommand.ExecuteNonQuery(); - - // Delete chapters - _deleteChaptersCommand.GetParameter(0).Value = id; - _deleteChaptersCommand.Transaction = transaction; - _deleteChaptersCommand.ExecuteNonQuery(); - - // Delete media streams - _deleteStreamsCommand.GetParameter(0).Value = id; - _deleteStreamsCommand.Transaction = transaction; - _deleteStreamsCommand.ExecuteNonQuery(); - - // Delete ancestors - _deleteAncestorsCommand.GetParameter(0).Value = id; - _deleteAncestorsCommand.Transaction = transaction; - _deleteAncestorsCommand.ExecuteNonQuery(); - - // Delete user data keys - _deleteUserDataKeysCommand.GetParameter(0).Value = id; - _deleteUserDataKeysCommand.Transaction = transaction; - _deleteUserDataKeysCommand.ExecuteNonQuery(); - - // Delete item values - _deleteItemValuesCommand.GetParameter(0).Value = id; - _deleteItemValuesCommand.Transaction = transaction; - _deleteItemValuesCommand.ExecuteNonQuery(); - - // Delete provider ids - _deleteProviderIdsCommand.GetParameter(0).Value = id; - _deleteProviderIdsCommand.Transaction = transaction; - _deleteProviderIdsCommand.ExecuteNonQuery(); - - // Delete images - _deleteImagesCommand.GetParameter(0).Value = id; - _deleteImagesCommand.Transaction = transaction; - _deleteImagesCommand.ExecuteNonQuery(); - - // Delete the item - _deleteItemCommand.GetParameter(0).Value = id; - _deleteItemCommand.Transaction = transaction; - _deleteItemCommand.ExecuteNonQuery(); - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) + using (var connection = CreateConnection()) { - transaction.Rollback(); - } + connection.RunInTransaction(db => + { + // Delete people + ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", id.ToGuidParamValue()); - throw; + // Delete chapters + ExecuteWithSingleParam(db, "delete from " + ChaptersTableName + " where ItemId=@Id", id.ToGuidParamValue()); + + // Delete media streams + ExecuteWithSingleParam(db, "delete from mediastreams where ItemId=@Id", id.ToGuidParamValue()); + + // Delete ancestors + ExecuteWithSingleParam(db, "delete from AncestorIds where ItemId=@Id", id.ToGuidParamValue()); + + // Delete item values + ExecuteWithSingleParam(db, "delete from ItemValues where ItemId=@Id", id.ToGuidParamValue()); + + // Delete the item + ExecuteWithSingleParam(db, "delete from TypedBaseItems where guid=@Id", id.ToGuidParamValue()); + }, TransactionMode); + } } - catch (Exception e) + } + + private void ExecuteWithSingleParam(IDatabaseConnection db, string query, byte[] value) + { + using (var statement = PrepareStatement(db, query)) { - Logger.ErrorException("Failed to save children:", e); + statement.TryBind("@Id", value); - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); + statement.MoveNext(); } } @@ -4234,30 +4643,34 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - using (var cmd = _connection.CreateCommand()) + var commandText = "select Distinct Name from People"; + + var whereClauses = GetPeopleWhereClauses(query, null); + + if (whereClauses.Count > 0) { - cmd.CommandText = "select Distinct Name from People"; + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } - var whereClauses = GetPeopleWhereClauses(query, cmd); + commandText += " order by ListOrder"; - if (whereClauses.Count > 0) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } - - cmd.CommandText += " order by ListOrder"; - - var list = new List(); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) + var list = new List(); + using (var statement = PrepareStatementSafe(connection, commandText)) { - list.Add(reader.GetString(0)); - } - } + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); - return list; + foreach (var row in statement.ExecuteQuery()) + { + list.Add(row.GetString(0)); + } + } + return list; + } } } @@ -4270,51 +4683,66 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - using (var cmd = _connection.CreateCommand()) + var commandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; + + var whereClauses = GetPeopleWhereClauses(query, null); + + if (whereClauses.Count > 0) { - cmd.CommandText = "select ItemId, Name, Role, PersonType, SortOrder from People"; + commandText += " where " + string.Join(" AND ", whereClauses.ToArray()); + } - var whereClauses = GetPeopleWhereClauses(query, cmd); + commandText += " order by ListOrder"; - if (whereClauses.Count > 0) + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - cmd.CommandText += " where " + string.Join(" AND ", whereClauses.ToArray()); - } + var list = new List(); - cmd.CommandText += " order by ListOrder"; - - var list = new List(); - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) + using (var statement = PrepareStatementSafe(connection, commandText)) { - list.Add(GetPerson(reader)); - } - } + // Run this again to bind the params + GetPeopleWhereClauses(query, statement); - return list; + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetPerson(row)); + } + } + + return list; + } } } - private List GetPeopleWhereClauses(InternalPeopleQuery query, IDbCommand cmd) + private List GetPeopleWhereClauses(InternalPeopleQuery query, IStatement statement) { var whereClauses = new List(); if (query.ItemId != Guid.Empty) { whereClauses.Add("ItemId=@ItemId"); - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; + if (statement != null) + { + statement.TryBind("@ItemId", query.ItemId.ToGuidParamValue()); + } } if (query.AppearsInItemId != Guid.Empty) { whereClauses.Add("Name in (Select Name from People where ItemId=@AppearsInItemId)"); - cmd.Parameters.Add(cmd, "@AppearsInItemId", DbType.Guid).Value = query.AppearsInItemId; + if (statement != null) + { + statement.TryBind("@AppearsInItemId", query.AppearsInItemId.ToGuidParamValue()); + } } if (query.PersonTypes.Count == 1) { whereClauses.Add("PersonType=@PersonType"); - cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.PersonTypes[0]; + if (statement != null) + { + statement.TryBind("@PersonType", query.PersonTypes[0]); + } } if (query.PersonTypes.Count > 1) { @@ -4325,7 +4753,10 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.ExcludePersonTypes.Count == 1) { whereClauses.Add("PersonType<>@PersonType"); - cmd.Parameters.Add(cmd, "@PersonType", DbType.String).Value = query.ExcludePersonTypes[0]; + if (statement != null) + { + statement.TryBind("@PersonType", query.ExcludePersonTypes[0]); + } } if (query.ExcludePersonTypes.Count > 1) { @@ -4336,23 +4767,32 @@ namespace MediaBrowser.Server.Implementations.Persistence if (query.MaxListOrder.HasValue) { whereClauses.Add("ListOrder<=@MaxListOrder"); - cmd.Parameters.Add(cmd, "@MaxListOrder", DbType.Int32).Value = query.MaxListOrder.Value; + if (statement != null) + { + statement.TryBind("@MaxListOrder", query.MaxListOrder.Value); + } } if (!string.IsNullOrWhiteSpace(query.NameContains)) { whereClauses.Add("Name like @NameContains"); - cmd.Parameters.Add(cmd, "@NameContains", DbType.String).Value = "%" + query.NameContains + "%"; + if (statement != null) + { + statement.TryBind("@NameContains", "%" + query.NameContains + "%"); + } } if (query.SourceTypes.Length == 1) { whereClauses.Add("(select sourcetype from typedbaseitems where guid=ItemId) = @SourceTypes"); - cmd.Parameters.Add(cmd, "@SourceTypes", DbType.String).Value = query.SourceTypes[0].ToString(); + if (statement != null) + { + statement.TryBind("@SourceTypes", query.SourceTypes[0].ToString()); + } } return whereClauses; } - private void UpdateAncestors(Guid itemId, List ancestorIds, IDbTransaction transaction) + private void UpdateAncestors(Guid itemId, List ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement, IStatement updateAncestorsStatement) { if (itemId == Guid.Empty) { @@ -4367,20 +4807,17 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); // First delete - _deleteAncestorsCommand.GetParameter(0).Value = itemId; - _deleteAncestorsCommand.Transaction = transaction; - - _deleteAncestorsCommand.ExecuteNonQuery(); + deleteAncestorsStatement.Reset(); + deleteAncestorsStatement.TryBind("@ItemId", itemId.ToGuidParamValue()); + deleteAncestorsStatement.MoveNext(); foreach (var ancestorId in ancestorIds) { - _saveAncestorCommand.GetParameter(0).Value = itemId; - _saveAncestorCommand.GetParameter(1).Value = ancestorId; - _saveAncestorCommand.GetParameter(2).Value = ancestorId.ToString("N"); - - _saveAncestorCommand.Transaction = transaction; - - _saveAncestorCommand.ExecuteNonQuery(); + updateAncestorsStatement.Reset(); + updateAncestorsStatement.TryBind("@ItemId", itemId.ToGuidParamValue()); + updateAncestorsStatement.TryBind("@AncestorId", ancestorId.ToGuidParamValue()); + updateAncestorsStatement.TryBind("@AncestorIdText", ancestorId.ToString("N")); + updateAncestorsStatement.MoveNext(); } } @@ -4457,43 +4894,43 @@ namespace MediaBrowser.Server.Implementations.Persistence ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); - var list = new List(); + var commandText = "Select Value From ItemValues where " + typeClause; - using (var cmd = _connection.CreateCommand()) + if (withItemTypes.Count > 0) { - cmd.CommandText = "Select Value From ItemValues where " + typeClause; - - if (withItemTypes.Count > 0) - { - var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'").ToArray()); - cmd.CommandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; - } - if (excludeItemTypes.Count > 0) - { - var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'").ToArray()); - cmd.CommandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; - } - - cmd.CommandText += " Group By CleanValue"; - - var commandBehavior = CommandBehavior.SequentialAccess | CommandBehavior.SingleResult; - - using (var reader = cmd.ExecuteReader(commandBehavior)) - { - LogQueryTime("GetItemValueNames", cmd, now); - - while (reader.Read()) - { - if (!reader.IsDBNull(0)) - { - list.Add(reader.GetString(0)); - } - } - } - + var typeString = string.Join(",", withItemTypes.Select(i => "'" + i + "'").ToArray()); + commandText += " AND ItemId In (select guid from typedbaseitems where type in (" + typeString + "))"; + } + if (excludeItemTypes.Count > 0) + { + var typeString = string.Join(",", excludeItemTypes.Select(i => "'" + i + "'").ToArray()); + commandText += " AND ItemId not In (select guid from typedbaseitems where type in (" + typeString + "))"; } - return list; + commandText += " Group By CleanValue"; + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + var list = new List(); + + using (var statement = PrepareStatementSafe(connection, commandText)) + { + foreach (var row in statement.ExecuteQuery()) + { + if (!row.IsDBNull(0)) + { + list.Add(row.GetString(0)); + } + } + } + + LogQueryTime("GetItemValueNames", commandText, now); + + return list; + } + } } private QueryResult> GetItemValues(InternalItemsQuery query, int[] itemValueTypes, string returnType) @@ -4509,6 +4946,7 @@ namespace MediaBrowser.Server.Implementations.Persistence } CheckDisposed(); + //Logger.Info("GetItemValues: " + _environmentInfo.StackTrace); var now = DateTime.UtcNow; @@ -4516,50 +4954,17 @@ namespace MediaBrowser.Server.Implementations.Persistence ("Type=" + itemValueTypes[0].ToString(CultureInfo.InvariantCulture)) : ("Type in (" + string.Join(",", itemValueTypes.Select(i => i.ToString(CultureInfo.InvariantCulture)).ToArray()) + ")"); - using (var cmd = _connection.CreateCommand()) + InternalItemsQuery typeSubQuery = null; + + var itemCountColumns = new List>(); + + var typesToCount = query.IncludeItemTypes.ToList(); + + if (typesToCount.Count > 0) { - var itemCountColumns = new List>(); + var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); - var typesToCount = query.IncludeItemTypes.ToList(); - - if (typesToCount.Count > 0) - { - var itemCountColumnQuery = "select group_concat(type, '|')" + GetFromText("B"); - - var typeSubQuery = new InternalItemsQuery(query.User) - { - ExcludeItemTypes = query.ExcludeItemTypes, - IncludeItemTypes = query.IncludeItemTypes, - MediaTypes = query.MediaTypes, - AncestorIds = query.AncestorIds, - ExcludeItemIds = query.ExcludeItemIds, - ItemIds = query.ItemIds, - TopParentIds = query.TopParentIds, - ParentId = query.ParentId, - IsPlayed = query.IsPlayed - }; - var whereClauses = GetWhereClauses(typeSubQuery, cmd, "itemTypes"); - - whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); - - var typeWhereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses.ToArray()); - - itemCountColumnQuery += typeWhereText; - - //itemCountColumnQuery += ")"; - - itemCountColumns.Add(new Tuple("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); - } - - var columns = _retriveItemColumns.ToList(); - columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); - - cmd.CommandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray(), cmd)) + GetFromText(); - cmd.CommandText += GetJoinUserDataText(query); - - var innerQuery = new InternalItemsQuery(query.User) + typeSubQuery = new InternalItemsQuery(query.User) { ExcludeItemTypes = query.ExcludeItemTypes, IncludeItemTypes = query.IncludeItemTypes, @@ -4571,152 +4976,206 @@ namespace MediaBrowser.Server.Implementations.Persistence ParentId = query.ParentId, IsPlayed = query.IsPlayed }; + var whereClauses = GetWhereClauses(typeSubQuery, null, "itemTypes"); - var innerWhereClauses = GetWhereClauses(innerQuery, cmd); + whereClauses.Add("guid in (select ItemId from ItemValues where ItemValues.CleanValue=A.CleanName AND " + typeClause + ")"); - var innerWhereText = innerWhereClauses.Count == 0 ? + var typeWhereText = whereClauses.Count == 0 ? string.Empty : - " where " + string.Join(" AND ", innerWhereClauses.ToArray()); + " where " + string.Join(" AND ", whereClauses.ToArray()); - var whereText = " where Type=@SelectType"; + itemCountColumnQuery += typeWhereText; - if (typesToCount.Count == 0) + //itemCountColumnQuery += ")"; + + itemCountColumns.Add(new Tuple("itemTypes", "(" + itemCountColumnQuery + ") as itemTypes")); + } + + var columns = _retriveItemColumns.ToList(); + columns.AddRange(itemCountColumns.Select(i => i.Item2).ToArray()); + + var commandText = "select " + string.Join(",", GetFinalColumnsToSelect(query, columns.ToArray())) + GetFromText(); + commandText += GetJoinUserDataText(query); + + var innerQuery = new InternalItemsQuery(query.User) + { + ExcludeItemTypes = query.ExcludeItemTypes, + IncludeItemTypes = query.IncludeItemTypes, + MediaTypes = query.MediaTypes, + AncestorIds = query.AncestorIds, + ExcludeItemIds = query.ExcludeItemIds, + ItemIds = query.ItemIds, + TopParentIds = query.TopParentIds, + ParentId = query.ParentId, + IsPlayed = query.IsPlayed + }; + + var innerWhereClauses = GetWhereClauses(innerQuery, null); + + var innerWhereText = innerWhereClauses.Count == 0 ? + string.Empty : + " where " + string.Join(" AND ", innerWhereClauses.ToArray()); + + var whereText = " where Type=@SelectType"; + + if (typesToCount.Count == 0) + { + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + else + { + //whereText += " And itemTypes not null"; + whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + } + + var outerQuery = new InternalItemsQuery(query.User) + { + IsFavorite = query.IsFavorite, + IsFavoriteOrLiked = query.IsFavoriteOrLiked, + IsLiked = query.IsLiked, + IsLocked = query.IsLocked, + NameLessThan = query.NameLessThan, + NameStartsWith = query.NameStartsWith, + NameStartsWithOrGreater = query.NameStartsWithOrGreater, + AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, + Tags = query.Tags, + OfficialRatings = query.OfficialRatings, + GenreIds = query.GenreIds, + Genres = query.Genres, + Years = query.Years + }; + + var outerWhereClauses = GetWhereClauses(outerQuery, null); + + whereText += outerWhereClauses.Count == 0 ? + string.Empty : + " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); + //cmd.CommandText += GetGroupBy(query); + + commandText += whereText; + commandText += " group by PresentationUniqueKey"; + + commandText += " order by SortName"; + + if (query.Limit.HasValue || query.StartIndex.HasValue) + { + var offset = query.StartIndex ?? 0; + + if (query.Limit.HasValue || offset > 0) { - whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; - } - else - { - //whereText += " And itemTypes not null"; - whereText += " And CleanName In (Select CleanValue from ItemValues where " + typeClause + " AND ItemId in (select guid from TypedBaseItems" + innerWhereText + "))"; + commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); } - var outerQuery = new InternalItemsQuery(query.User) + if (offset > 0) { - IsFavorite = query.IsFavorite, - IsFavoriteOrLiked = query.IsFavoriteOrLiked, - IsLiked = query.IsLiked, - IsLocked = query.IsLocked, - NameLessThan = query.NameLessThan, - NameStartsWith = query.NameStartsWith, - NameStartsWithOrGreater = query.NameStartsWithOrGreater, - AlbumArtistStartsWithOrGreater = query.AlbumArtistStartsWithOrGreater, - Tags = query.Tags, - OfficialRatings = query.OfficialRatings, - GenreIds = query.GenreIds, - Genres = query.Genres, - Years = query.Years - }; - - var outerWhereClauses = GetWhereClauses(outerQuery, cmd); - - whereText += outerWhereClauses.Count == 0 ? - string.Empty : - " AND " + string.Join(" AND ", outerWhereClauses.ToArray()); - //cmd.CommandText += GetGroupBy(query); - - cmd.CommandText += whereText; - cmd.CommandText += " group by PresentationUniqueKey"; - - cmd.Parameters.Add(cmd, "@SelectType", DbType.String).Value = returnType; - - if (EnableJoinUserData(query)) - { - cmd.Parameters.Add(cmd, "@UserId", DbType.Guid).Value = query.User.Id; + commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); } + } - cmd.CommandText += " order by SortName"; + var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - if (query.Limit.HasValue || query.StartIndex.HasValue) + var statementTexts = new List(); + if (!isReturningZeroItems) + { + statementTexts.Add(commandText); + } + if (query.EnableTotalRecordCount) + { + var countText = "select count (distinct PresentationUniqueKey)" + GetFromText(); + + countText += GetJoinUserDataText(query); + countText += whereText; + statementTexts.Add(countText); + } + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) + return connection.RunInTransaction(db => { - cmd.CommandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } + var list = new List>(); + var result = new QueryResult>(); - if (offset > 0) - { - cmd.CommandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } - } + var statements = PrepareAllSafe(db, statementTexts) + .ToList(); - cmd.CommandText += ";"; - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - if (isReturningZeroItems) - { - cmd.CommandText = ""; - } - - if (query.EnableTotalRecordCount) - { - cmd.CommandText += "select count (distinct PresentationUniqueKey)" + GetFromText(); - - cmd.CommandText += GetJoinUserDataText(query); - cmd.CommandText += whereText; - } - else - { - cmd.CommandText = cmd.CommandText.TrimEnd(';'); - } - - var list = new List>(); - var count = 0; - - var commandBehavior = isReturningZeroItems || !query.EnableTotalRecordCount - ? (CommandBehavior.SequentialAccess | CommandBehavior.SingleResult) - : CommandBehavior.SequentialAccess; - - //Logger.Debug("GetItemValues: " + cmd.CommandText); - - using (var reader = cmd.ExecuteReader(commandBehavior)) - { - LogQueryTime("GetItemValues", cmd, now); - - if (isReturningZeroItems) - { - if (reader.Read()) + if (!isReturningZeroItems) { - count = reader.GetInt32(0); - } - } - else - { - while (reader.Read()) - { - var item = GetItem(reader); - if (item != null) + using (var statement = statements[0]) { - var countStartColumn = columns.Count - 1; + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } - list.Add(new Tuple(item, GetItemCounts(reader, countStartColumn, typesToCount))); + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null, "itemTypes"); + } + BindSimilarParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + foreach (var row in statement.ExecuteQuery()) + { + var item = GetItem(row); + if (item != null) + { + var countStartColumn = columns.Count - 1; + + list.Add(new Tuple(item, GetItemCounts(row, countStartColumn, typesToCount))); + } + } + + LogQueryTime("GetItemValues", commandText, now); } } - if (reader.NextResult() && reader.Read()) + if (query.EnableTotalRecordCount) { - count = reader.GetInt32(0); + commandText = "select count (distinct PresentationUniqueKey)" + GetFromText(); + + commandText += GetJoinUserDataText(query); + commandText += whereText; + + using (var statement = statements[statements.Count - 1]) + { + statement.TryBind("@SelectType", returnType); + if (EnableJoinUserData(query)) + { + statement.TryBind("@UserId", query.User.Id); + } + + if (typeSubQuery != null) + { + GetWhereClauses(typeSubQuery, null, "itemTypes"); + } + BindSimilarParams(query, statement); + GetWhereClauses(innerQuery, statement); + GetWhereClauses(outerQuery, statement); + + result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); + + LogQueryTime("GetItemValues", commandText, now); + } } - } + + if (result.TotalRecordCount == 0) + { + result.TotalRecordCount = list.Count; + } + result.Items = list.ToArray(); + + return result; + + }, ReadTransactionMode); } - - if (count == 0) - { - count = list.Count; - } - - return new QueryResult> - { - Items = list.ToArray(), - TotalRecordCount = count - }; - } } - private ItemCounts GetItemCounts(IDataReader reader, int countStartColumn, List typesToCount) + private ItemCounts GetItemCounts(IReadOnlyList reader, int countStartColumn, List typesToCount) { var counts = new ItemCounts(); @@ -4802,97 +5261,7 @@ namespace MediaBrowser.Server.Implementations.Persistence return list; } - private void UpdateImages(Guid itemId, List images, IDbTransaction transaction) - { - if (itemId == Guid.Empty) - { - throw new ArgumentNullException("itemId"); - } - - if (images == null) - { - throw new ArgumentNullException("images"); - } - - CheckDisposed(); - - // First delete - _deleteImagesCommand.GetParameter(0).Value = itemId; - _deleteImagesCommand.Transaction = transaction; - - _deleteImagesCommand.ExecuteNonQuery(); - - var index = 0; - foreach (var image in images) - { - if (string.IsNullOrWhiteSpace(image.Path)) - { - // Invalid - continue; - } - - _saveImagesCommand.GetParameter(0).Value = itemId; - _saveImagesCommand.GetParameter(1).Value = image.Type; - _saveImagesCommand.GetParameter(2).Value = image.Path; - - if (image.DateModified == default(DateTime)) - { - _saveImagesCommand.GetParameter(3).Value = null; - } - else - { - _saveImagesCommand.GetParameter(3).Value = image.DateModified; - } - - _saveImagesCommand.GetParameter(4).Value = image.IsPlaceholder; - _saveImagesCommand.GetParameter(5).Value = index; - - _saveImagesCommand.Transaction = transaction; - - _saveImagesCommand.ExecuteNonQuery(); - index++; - } - } - - private void UpdateProviderIds(Guid itemId, Dictionary values, IDbTransaction transaction) - { - if (itemId == Guid.Empty) - { - throw new ArgumentNullException("itemId"); - } - - if (values == null) - { - throw new ArgumentNullException("values"); - } - - // Just in case there might be case-insensitive duplicates, strip them out now - var newValues = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var pair in values) - { - newValues[pair.Key] = pair.Value; - } - - CheckDisposed(); - - // First delete - _deleteProviderIdsCommand.GetParameter(0).Value = itemId; - _deleteProviderIdsCommand.Transaction = transaction; - - _deleteProviderIdsCommand.ExecuteNonQuery(); - - foreach (var pair in newValues) - { - _saveProviderIdsCommand.GetParameter(0).Value = itemId; - _saveProviderIdsCommand.GetParameter(1).Value = pair.Key; - _saveProviderIdsCommand.GetParameter(2).Value = pair.Value; - _saveProviderIdsCommand.Transaction = transaction; - - _saveProviderIdsCommand.ExecuteNonQuery(); - } - } - - private void UpdateItemValues(Guid itemId, List> values, IDbTransaction transaction) + private void UpdateItemValues(Guid itemId, List> values, IDatabaseConnection db) { if (itemId == Guid.Empty) { @@ -4907,60 +5276,29 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); // First delete - _deleteItemValuesCommand.GetParameter(0).Value = itemId; - _deleteItemValuesCommand.Transaction = transaction; + db.Execute("delete from ItemValues where ItemId=@Id", itemId.ToGuidParamValue()); - _deleteItemValuesCommand.ExecuteNonQuery(); - - foreach (var pair in values) + using (var statement = PrepareStatement(db, "insert into ItemValues (ItemId, Type, Value, CleanValue) values (@ItemId, @Type, @Value, @CleanValue)")) { - _saveItemValuesCommand.GetParameter(0).Value = itemId; - _saveItemValuesCommand.GetParameter(1).Value = pair.Item1; - _saveItemValuesCommand.GetParameter(2).Value = pair.Item2; - if (pair.Item2 == null) + foreach (var pair in values) { - _saveItemValuesCommand.GetParameter(3).Value = null; + statement.Reset(); + + statement.TryBind("@ItemId", itemId.ToGuidParamValue()); + statement.TryBind("@Type", pair.Item1); + statement.TryBind("@Value", pair.Item2); + + if (pair.Item2 == null) + { + statement.TryBindNull("@CleanValue"); + } + else + { + statement.TryBind("@CleanValue", GetCleanValue(pair.Item2)); + } + + statement.MoveNext(); } - else - { - _saveItemValuesCommand.GetParameter(3).Value = GetCleanValue(pair.Item2); - } - _saveItemValuesCommand.Transaction = transaction; - - _saveItemValuesCommand.ExecuteNonQuery(); - } - } - - private void UpdateUserDataKeys(Guid itemId, List keys, IDbTransaction transaction) - { - if (itemId == Guid.Empty) - { - throw new ArgumentNullException("itemId"); - } - - if (keys == null) - { - throw new ArgumentNullException("keys"); - } - - CheckDisposed(); - - // First delete - _deleteUserDataKeysCommand.GetParameter(0).Value = itemId; - _deleteUserDataKeysCommand.Transaction = transaction; - - _deleteUserDataKeysCommand.ExecuteNonQuery(); - var index = 0; - - foreach (var key in keys) - { - _saveUserDataKeysCommand.GetParameter(0).Value = itemId; - _saveUserDataKeysCommand.GetParameter(1).Value = key; - _saveUserDataKeysCommand.GetParameter(2).Value = index; - index++; - _saveUserDataKeysCommand.Transaction = transaction; - - _saveUserDataKeysCommand.ExecuteNonQuery(); } } @@ -4978,75 +5316,42 @@ namespace MediaBrowser.Server.Implementations.Persistence CheckDisposed(); - var cancellationToken = CancellationToken.None; - - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (WriteLock.Write()) { - transaction = _connection.BeginTransaction(); - - // First delete - _deletePeopleCommand.GetParameter(0).Value = itemId; - _deletePeopleCommand.Transaction = transaction; - - _deletePeopleCommand.ExecuteNonQuery(); - - var listIndex = 0; - - foreach (var person in people) + using (var connection = CreateConnection()) { - cancellationToken.ThrowIfCancellationRequested(); + // First delete + // "delete from People where ItemId=?" + connection.Execute("delete from People where ItemId=?", itemId.ToGuidParamValue()); - _savePersonCommand.GetParameter(0).Value = itemId; - _savePersonCommand.GetParameter(1).Value = person.Name; - _savePersonCommand.GetParameter(2).Value = person.Role; - _savePersonCommand.GetParameter(3).Value = person.Type; - _savePersonCommand.GetParameter(4).Value = person.SortOrder; - _savePersonCommand.GetParameter(5).Value = listIndex; + var listIndex = 0; - _savePersonCommand.Transaction = transaction; + using (var statement = PrepareStatement(connection, + "insert into People (ItemId, Name, Role, PersonType, SortOrder, ListOrder) values (@ItemId, @Name, @Role, @PersonType, @SortOrder, @ListOrder)")) + { + foreach (var person in people) + { + if (listIndex > 0) + { + statement.Reset(); + } - _savePersonCommand.ExecuteNonQuery(); - listIndex++; + statement.TryBind("@ItemId", itemId.ToGuidParamValue()); + statement.TryBind("@Name", person.Name); + statement.TryBind("@Role", person.Role); + statement.TryBind("@PersonType", person.Type); + statement.TryBind("@SortOrder", person.SortOrder); + statement.TryBind("@ListOrder", listIndex); + + statement.MoveNext(); + listIndex++; + } + } } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save people:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); } } - private PersonInfo GetPerson(IDataReader reader) + private PersonInfo GetPerson(IReadOnlyList reader) { var item = new PersonInfo(); @@ -5080,41 +5385,51 @@ namespace MediaBrowser.Server.Implementations.Persistence throw new ArgumentNullException("query"); } - var list = new List(); + var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; - using (var cmd = _connection.CreateCommand()) + cmdText += " ItemId=@ItemId"; + + if (query.Type.HasValue) { - var cmdText = "select " + string.Join(",", _mediaStreamSaveColumns) + " from mediastreams where"; - - cmdText += " ItemId=@ItemId"; - cmd.Parameters.Add(cmd, "@ItemId", DbType.Guid).Value = query.ItemId; - - if (query.Type.HasValue) - { - cmdText += " AND StreamType=@StreamType"; - cmd.Parameters.Add(cmd, "@StreamType", DbType.String).Value = query.Type.Value.ToString(); - } - - if (query.Index.HasValue) - { - cmdText += " AND StreamIndex=@StreamIndex"; - cmd.Parameters.Add(cmd, "@StreamIndex", DbType.Int32).Value = query.Index.Value; - } - - cmdText += " order by StreamIndex ASC"; - - cmd.CommandText = cmdText; - - using (var reader = cmd.ExecuteReader(CommandBehavior.SequentialAccess | CommandBehavior.SingleResult)) - { - while (reader.Read()) - { - list.Add(GetMediaStream(reader)); - } - } + cmdText += " AND StreamType=@StreamType"; } - return list; + if (query.Index.HasValue) + { + cmdText += " AND StreamIndex=@StreamIndex"; + } + + cmdText += " order by StreamIndex ASC"; + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + var list = new List(); + + using (var statement = PrepareStatementSafe(connection, cmdText)) + { + statement.TryBind("@ItemId", query.ItemId.ToGuidParamValue()); + + if (query.Type.HasValue) + { + statement.TryBind("@StreamType", query.Type.Value.ToString()); + } + + if (query.Index.HasValue) + { + statement.TryBind("@StreamIndex", query.Index.Value); + } + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(GetMediaStream(row)); + } + } + + return list; + } + } } public async Task SaveMediaStreams(Guid id, List streams, CancellationToken cancellationToken) @@ -5133,100 +5448,63 @@ namespace MediaBrowser.Server.Implementations.Persistence cancellationToken.ThrowIfCancellationRequested(); - await WriteLock.WaitAsync(cancellationToken).ConfigureAwait(false); - - IDbTransaction transaction = null; - - try + using (WriteLock.Write()) { - transaction = _connection.BeginTransaction(); - - // First delete chapters - _deleteStreamsCommand.GetParameter(0).Value = id; - - _deleteStreamsCommand.Transaction = transaction; - - _deleteStreamsCommand.ExecuteNonQuery(); - - foreach (var stream in streams) + using (var connection = CreateConnection()) { - cancellationToken.ThrowIfCancellationRequested(); + // First delete chapters + connection.Execute("delete from mediastreams where ItemId=@ItemId", id.ToGuidParamValue()); - var index = 0; + using (var statement = PrepareStatement(connection, string.Format("replace into mediastreams ({0}) values ({1})", + string.Join(",", _mediaStreamSaveColumns), + string.Join(",", _mediaStreamSaveColumns.Select(i => "@" + i).ToArray())))) + { + foreach (var stream in streams) + { + var paramList = new List(); - _saveStreamCommand.GetParameter(index++).Value = id; - _saveStreamCommand.GetParameter(index++).Value = stream.Index; - _saveStreamCommand.GetParameter(index++).Value = stream.Type.ToString(); - _saveStreamCommand.GetParameter(index++).Value = stream.Codec; - _saveStreamCommand.GetParameter(index++).Value = stream.Language; - _saveStreamCommand.GetParameter(index++).Value = stream.ChannelLayout; - _saveStreamCommand.GetParameter(index++).Value = stream.Profile; - _saveStreamCommand.GetParameter(index++).Value = stream.AspectRatio; - _saveStreamCommand.GetParameter(index++).Value = stream.Path; + paramList.Add(id.ToGuidParamValue()); + paramList.Add(stream.Index); + paramList.Add(stream.Type.ToString()); + paramList.Add(stream.Codec); + paramList.Add(stream.Language); + paramList.Add(stream.ChannelLayout); + paramList.Add(stream.Profile); + paramList.Add(stream.AspectRatio); + paramList.Add(stream.Path); - _saveStreamCommand.GetParameter(index++).Value = stream.IsInterlaced; + paramList.Add(stream.IsInterlaced); + paramList.Add(stream.BitRate); + paramList.Add(stream.Channels); + paramList.Add(stream.SampleRate); - _saveStreamCommand.GetParameter(index++).Value = stream.BitRate; - _saveStreamCommand.GetParameter(index++).Value = stream.Channels; - _saveStreamCommand.GetParameter(index++).Value = stream.SampleRate; + paramList.Add(stream.IsDefault); + paramList.Add(stream.IsForced); + paramList.Add(stream.IsExternal); - _saveStreamCommand.GetParameter(index++).Value = stream.IsDefault; - _saveStreamCommand.GetParameter(index++).Value = stream.IsForced; - _saveStreamCommand.GetParameter(index++).Value = stream.IsExternal; + paramList.Add(stream.Width); + paramList.Add(stream.Height); + paramList.Add(stream.AverageFrameRate); + paramList.Add(stream.RealFrameRate); + paramList.Add(stream.Level); + paramList.Add(stream.PixelFormat); + paramList.Add(stream.BitDepth); + paramList.Add(stream.IsExternal); + paramList.Add(stream.RefFrames); - _saveStreamCommand.GetParameter(index++).Value = stream.Width; - _saveStreamCommand.GetParameter(index++).Value = stream.Height; - _saveStreamCommand.GetParameter(index++).Value = stream.AverageFrameRate; - _saveStreamCommand.GetParameter(index++).Value = stream.RealFrameRate; - _saveStreamCommand.GetParameter(index++).Value = stream.Level; - _saveStreamCommand.GetParameter(index++).Value = stream.PixelFormat; - _saveStreamCommand.GetParameter(index++).Value = stream.BitDepth; - _saveStreamCommand.GetParameter(index++).Value = stream.IsAnamorphic; - _saveStreamCommand.GetParameter(index++).Value = stream.RefFrames; + paramList.Add(stream.CodecTag); + paramList.Add(stream.Comment); + paramList.Add(stream.NalLengthSize); + paramList.Add(stream.IsAVC); + paramList.Add(stream.Title); - _saveStreamCommand.GetParameter(index++).Value = stream.CodecTag; - _saveStreamCommand.GetParameter(index++).Value = stream.Comment; - _saveStreamCommand.GetParameter(index++).Value = stream.NalLengthSize; - _saveStreamCommand.GetParameter(index++).Value = stream.IsAVC; - _saveStreamCommand.GetParameter(index++).Value = stream.Title; + paramList.Add(stream.TimeBase); + paramList.Add(stream.CodecTimeBase); - _saveStreamCommand.GetParameter(index++).Value = stream.TimeBase; - _saveStreamCommand.GetParameter(index++).Value = stream.CodecTimeBase; - - _saveStreamCommand.Transaction = transaction; - _saveStreamCommand.ExecuteNonQuery(); + statement.Execute(paramList.ToArray()); + } + } } - - transaction.Commit(); - } - catch (OperationCanceledException) - { - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - catch (Exception e) - { - Logger.ErrorException("Failed to save media streams:", e); - - if (transaction != null) - { - transaction.Rollback(); - } - - throw; - } - finally - { - if (transaction != null) - { - transaction.Dispose(); - } - - WriteLock.Release(); } } @@ -5235,58 +5513,58 @@ namespace MediaBrowser.Server.Implementations.Persistence /// /// The reader. /// ChapterInfo. - private MediaStream GetMediaStream(IDataReader reader) + private MediaStream GetMediaStream(IReadOnlyList reader) { var item = new MediaStream { - Index = reader.GetInt32(1) + Index = reader[1].ToInt() }; - item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader.GetString(2), true); + item.Type = (MediaStreamType)Enum.Parse(typeof(MediaStreamType), reader[2].ToString(), true); - if (!reader.IsDBNull(3)) + if (reader[3].SQLiteType != SQLiteType.Null) { - item.Codec = reader.GetString(3); + item.Codec = reader[3].ToString(); } - if (!reader.IsDBNull(4)) + if (reader[4].SQLiteType != SQLiteType.Null) { - item.Language = reader.GetString(4); + item.Language = reader[4].ToString(); } - if (!reader.IsDBNull(5)) + if (reader[5].SQLiteType != SQLiteType.Null) { - item.ChannelLayout = reader.GetString(5); + item.ChannelLayout = reader[5].ToString(); } - if (!reader.IsDBNull(6)) + if (reader[6].SQLiteType != SQLiteType.Null) { - item.Profile = reader.GetString(6); + item.Profile = reader[6].ToString(); } - if (!reader.IsDBNull(7)) + if (reader[7].SQLiteType != SQLiteType.Null) { - item.AspectRatio = reader.GetString(7); + item.AspectRatio = reader[7].ToString(); } - if (!reader.IsDBNull(8)) + if (reader[8].SQLiteType != SQLiteType.Null) { - item.Path = reader.GetString(8); + item.Path = reader[8].ToString(); } item.IsInterlaced = reader.GetBoolean(9); - if (!reader.IsDBNull(10)) + if (reader[10].SQLiteType != SQLiteType.Null) { item.BitRate = reader.GetInt32(10); } - if (!reader.IsDBNull(11)) + if (reader[11].SQLiteType != SQLiteType.Null) { item.Channels = reader.GetInt32(11); } - if (!reader.IsDBNull(12)) + if (reader[12].SQLiteType != SQLiteType.Null) { item.SampleRate = reader.GetInt32(12); } @@ -5295,84 +5573,84 @@ namespace MediaBrowser.Server.Implementations.Persistence item.IsForced = reader.GetBoolean(14); item.IsExternal = reader.GetBoolean(15); - if (!reader.IsDBNull(16)) + if (reader[16].SQLiteType != SQLiteType.Null) { item.Width = reader.GetInt32(16); } - if (!reader.IsDBNull(17)) + if (reader[17].SQLiteType != SQLiteType.Null) { item.Height = reader.GetInt32(17); } - if (!reader.IsDBNull(18)) + if (reader[18].SQLiteType != SQLiteType.Null) { item.AverageFrameRate = reader.GetFloat(18); } - if (!reader.IsDBNull(19)) + if (reader[19].SQLiteType != SQLiteType.Null) { item.RealFrameRate = reader.GetFloat(19); } - if (!reader.IsDBNull(20)) + if (reader[20].SQLiteType != SQLiteType.Null) { item.Level = reader.GetFloat(20); } - if (!reader.IsDBNull(21)) + if (reader[21].SQLiteType != SQLiteType.Null) { - item.PixelFormat = reader.GetString(21); + item.PixelFormat = reader[21].ToString(); } - if (!reader.IsDBNull(22)) + if (reader[22].SQLiteType != SQLiteType.Null) { item.BitDepth = reader.GetInt32(22); } - if (!reader.IsDBNull(23)) + if (reader[23].SQLiteType != SQLiteType.Null) { item.IsAnamorphic = reader.GetBoolean(23); } - if (!reader.IsDBNull(24)) + if (reader[24].SQLiteType != SQLiteType.Null) { item.RefFrames = reader.GetInt32(24); } - if (!reader.IsDBNull(25)) + if (reader[25].SQLiteType != SQLiteType.Null) { item.CodecTag = reader.GetString(25); } - if (!reader.IsDBNull(26)) + if (reader[26].SQLiteType != SQLiteType.Null) { item.Comment = reader.GetString(26); } - if (!reader.IsDBNull(27)) + if (reader[27].SQLiteType != SQLiteType.Null) { item.NalLengthSize = reader.GetString(27); } - if (!reader.IsDBNull(28)) + if (reader[28].SQLiteType != SQLiteType.Null) { - item.IsAVC = reader.GetBoolean(28); + item.IsAVC = reader[28].ToBool(); } - if (!reader.IsDBNull(29)) + if (reader[29].SQLiteType != SQLiteType.Null) { - item.Title = reader.GetString(29); + item.Title = reader[29].ToString(); } - if (!reader.IsDBNull(30)) + if (reader[30].SQLiteType != SQLiteType.Null) { - item.TimeBase = reader.GetString(30); + item.TimeBase = reader[30].ToString(); } - if (!reader.IsDBNull(31)) + if (reader[31].SQLiteType != SQLiteType.Null) { - item.CodecTimeBase = reader.GetString(31); + item.CodecTimeBase = reader[31].ToString(); } return item; diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs new file mode 100644 index 0000000000..2e39b038ae --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -0,0 +1,422 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + public class SqliteUserDataRepository : BaseSqliteRepository, IUserDataRepository + { + private readonly string _importFile; + private readonly IFileSystem _fileSystem; + + public SqliteUserDataRepository(ILogger logger, IApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) + { + _fileSystem = fileSystem; + DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); + _importFile = Path.Combine(appPaths.DataPath, "userdata_v2.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize(ReaderWriterLockSlim writeLock, ManagedConnection managedConnection) + { + _connection = managedConnection; + + WriteLock.Dispose(); + WriteLock = writeLock; + + using (var connection = CreateConnection()) + { + string[] queries = { + + "create table if not exists userdata (key nvarchar, userId GUID, rating float null, played bit, playCount int, isFavorite bit, playbackPositionTicks bigint, lastPlayedDate datetime null)", + + "create table if not exists DataSettings (IsUserDataImported bit)", + + "drop index if exists idx_userdata", + "drop index if exists idx_userdata1", + "drop index if exists idx_userdata2", + "drop index if exists userdataindex1", + + "create unique index if not exists userdataindex on userdata (key, userId)", + "create index if not exists userdataindex2 on userdata (key, userId, played)", + "create index if not exists userdataindex3 on userdata (key, userId, playbackPositionTicks)", + "create index if not exists userdataindex4 on userdata (key, userId, isFavorite)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries); + + connection.RunInTransaction(db => + { + var existingColumnNames = GetColumnNames(db, "userdata"); + + AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames); + AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames); + }, TransactionMode); + + ImportUserDataIfNeeded(connection); + } + } + + protected override bool EnableTempStoreMemory + { + get + { + return true; + } + } + + private void ImportUserDataIfNeeded(ManagedConnection connection) + { + if (!_fileSystem.FileExists(_importFile)) + { + return; + } + + var fileToImport = _importFile; + var isImported = connection.Query("select IsUserDataImported from DataSettings").SelectScalarBool().FirstOrDefault(); + + if (isImported) + { + return; + } + + ImportUserData(connection, fileToImport); + + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("replace into DataSettings (IsUserDataImported) values (@IsUserDataImported)")) + { + statement.TryBind("@IsUserDataImported", true); + statement.MoveNext(); + } + }, TransactionMode); + } + + private void ImportUserData(ManagedConnection connection, string file) + { + SqliteExtensions.Attach(connection, file, "UserDataBackup"); + + var columns = "key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex"; + + connection.RunInTransaction(db => + { + db.Execute("REPLACE INTO userdata(" + columns + ") SELECT " + columns + " FROM UserDataBackup.userdata;"); + }, TransactionMode); + } + + /// + /// Saves the user data. + /// + /// The user id. + /// The key. + /// The user data. + /// The cancellation token. + /// Task. + /// userData + /// or + /// cancellationToken + /// or + /// userId + /// or + /// userDataId + public Task SaveUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key"); + } + + return PersistUserData(userId, key, userData, cancellationToken); + } + + public Task SaveAllUserData(Guid userId, IEnumerable userData, CancellationToken cancellationToken) + { + if (userData == null) + { + throw new ArgumentNullException("userData"); + } + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + return PersistAllUserData(userId, userData.ToList(), cancellationToken); + } + + /// + /// Persists the user data. + /// + /// The user id. + /// The key. + /// The user data. + /// The cancellation token. + /// Task. + public async Task PersistUserData(Guid userId, string key, UserItemData userData, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + SaveUserData(db, userId, key, userData); + }, TransactionMode); + } + } + } + + private void SaveUserData(IDatabaseConnection db, Guid userId, string key, UserItemData userData) + { + using (var statement = db.PrepareStatement("replace into userdata (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)")) + { + statement.TryBind("@userId", userId.ToGuidParamValue()); + statement.TryBind("@key", key); + + if (userData.Rating.HasValue) + { + statement.TryBind("@rating", userData.Rating.Value); + } + else + { + statement.TryBindNull("@rating"); + } + + statement.TryBind("@played", userData.Played); + statement.TryBind("@playCount", userData.PlayCount); + statement.TryBind("@isFavorite", userData.IsFavorite); + statement.TryBind("@playbackPositionTicks", userData.PlaybackPositionTicks); + + if (userData.LastPlayedDate.HasValue) + { + statement.TryBind("@lastPlayedDate", userData.LastPlayedDate.Value.ToDateTimeParamValue()); + } + else + { + statement.TryBindNull("@lastPlayedDate"); + } + + if (userData.AudioStreamIndex.HasValue) + { + statement.TryBind("@AudioStreamIndex", userData.AudioStreamIndex.Value); + } + else + { + statement.TryBindNull("@AudioStreamIndex"); + } + + if (userData.SubtitleStreamIndex.HasValue) + { + statement.TryBind("@SubtitleStreamIndex", userData.SubtitleStreamIndex.Value); + } + else + { + statement.TryBindNull("@SubtitleStreamIndex"); + } + + statement.MoveNext(); + } + } + + /// + /// Persist all user data for the specified user + /// + private async Task PersistAllUserData(Guid userId, List userDataList, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + foreach (var userItemData in userDataList) + { + SaveUserData(db, userId, userItemData.Key, userItemData); + } + }, TransactionMode); + } + } + } + + /// + /// Gets the user data. + /// + /// The user id. + /// The key. + /// Task{UserItemData}. + /// + /// userId + /// or + /// key + /// + public UserItemData GetUserData(Guid userId, string key) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException("key"); + } + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where key =@Key and userId=@UserId")) + { + statement.TryBind("@UserId", userId.ToGuidParamValue()); + statement.TryBind("@Key", key); + + foreach (var row in statement.ExecuteQuery()) + { + return ReadRow(row); + } + } + + return null; + } + } + } + + public UserItemData GetUserData(Guid userId, List keys) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + if (keys == null) + { + throw new ArgumentNullException("keys"); + } + + if (keys.Count == 0) + { + return null; + } + + return GetUserData(userId, keys[0]); + } + + /// + /// Return all user-data associated with the given user + /// + /// + /// + public IEnumerable GetAllUserData(Guid userId) + { + if (userId == Guid.Empty) + { + throw new ArgumentNullException("userId"); + } + + var list = new List(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection()) + { + using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from userdata where userId=@UserId")) + { + statement.TryBind("@UserId", userId.ToGuidParamValue()); + + foreach (var row in statement.ExecuteQuery()) + { + list.Add(ReadRow(row)); + } + } + } + } + + return list; + } + + /// + /// Read a row from the specified reader into the provided userData object + /// + /// + private UserItemData ReadRow(IReadOnlyList reader) + { + var userData = new UserItemData(); + + userData.Key = reader[0].ToString(); + userData.UserId = reader[1].ReadGuid(); + + if (reader[2].SQLiteType != SQLiteType.Null) + { + userData.Rating = reader[2].ToDouble(); + } + + userData.Played = reader[3].ToBool(); + userData.PlayCount = reader[4].ToInt(); + userData.IsFavorite = reader[5].ToBool(); + userData.PlaybackPositionTicks = reader[6].ToInt64(); + + if (reader[7].SQLiteType != SQLiteType.Null) + { + userData.LastPlayedDate = reader[7].ReadDateTime(); + } + + if (reader[8].SQLiteType != SQLiteType.Null) + { + userData.AudioStreamIndex = reader[8].ToInt(); + } + + if (reader[9].SQLiteType != SQLiteType.Null) + { + userData.SubtitleStreamIndex = reader[9].ToInt(); + } + + return userData; + } + + protected override void Dispose(bool dispose) + { + // handled by library database + } + + protected override void CloseConnection() + { + // handled by library database + } + } +} \ No newline at end of file diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs new file mode 100644 index 0000000000..b2b917e5eb --- /dev/null +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using SQLitePCL.pretty; + +namespace Emby.Server.Implementations.Data +{ + /// + /// Class SQLiteUserRepository + /// + public class SqliteUserRepository : BaseSqliteRepository, IUserRepository + { + private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamFactory _memoryStreamProvider; + + public SqliteUserRepository(ILogger logger, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamProvider) + : base(logger) + { + _jsonSerializer = jsonSerializer; + _memoryStreamProvider = memoryStreamProvider; + + DbFilePath = Path.Combine(appPaths.DataPath, "users.db"); + } + + /// + /// Gets the name of the repository + /// + /// The name. + public string Name + { + get + { + return "SQLite"; + } + } + + /// + /// Opens the connection to the database + /// + /// Task. + public void Initialize() + { + using (var connection = CreateConnection()) + { + RunDefaultInitialization(connection); + + string[] queries = { + + "create table if not exists users (guid GUID primary key, data BLOB)", + "create index if not exists idx_users on users(guid)", + "create table if not exists schema_version (table_name primary key, version)", + + "pragma shrink_memory" + }; + + connection.RunQueries(queries); + } + } + + /// + /// Save a user in the repo + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task SaveUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + var serialized = _jsonSerializer.SerializeToBytes(user, _memoryStreamProvider); + + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("replace into users (guid, data) values (@guid, @data)")) + { + statement.TryBind("@guid", user.Id.ToGuidParamValue()); + statement.TryBind("@data", serialized); + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + + /// + /// Retrieve all users from the database + /// + /// IEnumerable{User}. + public IEnumerable RetrieveAllUsers() + { + var list = new List(); + + using (WriteLock.Read()) + { + using (var connection = CreateConnection(true)) + { + foreach (var row in connection.Query("select guid,data from users")) + { + var id = row[0].ReadGuid(); + + using (var stream = _memoryStreamProvider.CreateNew(row[1].ToBlob())) + { + stream.Position = 0; + var user = _jsonSerializer.DeserializeFromStream(stream); + user.Id = id; + list.Add(user); + } + } + } + } + + return list; + } + + /// + /// Deletes the user. + /// + /// The user. + /// The cancellation token. + /// Task. + /// user + public async Task DeleteUser(User user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + using (WriteLock.Write()) + { + using (var connection = CreateConnection()) + { + connection.RunInTransaction(db => + { + using (var statement = db.PrepareStatement("delete from users where guid=@id")) + { + statement.TryBind("@id", user.Id.ToGuidParamValue()); + statement.MoveNext(); + } + }, TransactionMode); + } + } + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/Persistence/TypeMapper.cs b/Emby.Server.Implementations/Data/TypeMapper.cs similarity index 70% rename from MediaBrowser.Server.Implementations/Persistence/TypeMapper.cs rename to Emby.Server.Implementations/Data/TypeMapper.cs index 2de02d8171..f4b37749e7 100644 --- a/MediaBrowser.Server.Implementations/Persistence/TypeMapper.cs +++ b/Emby.Server.Implementations/Data/TypeMapper.cs @@ -1,19 +1,27 @@ using System; using System.Collections.Concurrent; +using MediaBrowser.Model.Reflection; using System.Linq; -namespace MediaBrowser.Server.Implementations.Persistence +namespace Emby.Server.Implementations.Data { /// /// Class TypeMapper /// public class TypeMapper { + private readonly IAssemblyInfo _assemblyInfo; + /// /// This holds all the types in the running assemblies so that we can de-serialize properly when we don't have strong types /// private readonly ConcurrentDictionary _typeMap = new ConcurrentDictionary(); + public TypeMapper(IAssemblyInfo assemblyInfo) + { + _assemblyInfo = assemblyInfo; + } + /// /// Gets the type. /// @@ -24,7 +32,7 @@ namespace MediaBrowser.Server.Implementations.Persistence { if (string.IsNullOrEmpty(typeName)) { - throw new ArgumentNullException(); + throw new ArgumentNullException("typeName"); } return _typeMap.GetOrAdd(typeName, LookupType); @@ -37,11 +45,10 @@ namespace MediaBrowser.Server.Implementations.Persistence /// Type. private Type LookupType(string typeName) { - return AppDomain - .CurrentDomain - .GetAssemblies() - .Select(a => a.GetType(typeName, false)) - .FirstOrDefault(t => t != null); + return _assemblyInfo + .GetCurrentAssemblies() + .Select(a => a.GetType(typeName)) + .FirstOrDefault(t => t != null); } } } diff --git a/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs b/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs new file mode 100644 index 0000000000..e2d5d0272f --- /dev/null +++ b/Emby.Server.Implementations/Devices/CameraUploadsDynamicFolder.cs @@ -0,0 +1,41 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.Entities; +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Server.Implementations.Devices; + +namespace Emby.Server.Implementations.Devices +{ + public class CameraUploadsDynamicFolder : IVirtualFolderCreator + { + private readonly IApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; + + public CameraUploadsDynamicFolder(IApplicationPaths appPaths, IFileSystem fileSystem) + { + _appPaths = appPaths; + _fileSystem = fileSystem; + } + + public BasePluginFolder GetFolder() + { + var path = Path.Combine(_appPaths.DataPath, "camerauploads"); + + _fileSystem.CreateDirectory(path); + + return new CameraUploadsFolder + { + Path = path + }; + } + } + +} diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs similarity index 96% rename from MediaBrowser.Server.Implementations/Devices/DeviceManager.cs rename to Emby.Server.Implementations/Devices/DeviceManager.cs index c3db9140cf..88c0ea2035 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -16,10 +16,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.IO; -namespace MediaBrowser.Server.Implementations.Devices +namespace Emby.Server.Implementations.Devices { public class DeviceManager : IDeviceManager { @@ -99,13 +101,6 @@ namespace MediaBrowser.Server.Implementations.Devices { IEnumerable devices = _repo.GetDevices().OrderByDescending(i => i.DateLastModified); - if (query.SupportsContentUploading.HasValue) - { - var val = query.SupportsContentUploading.Value; - - devices = devices.Where(i => GetCapabilities(i.Id).SupportsContentUploading == val); - } - if (query.SupportsSync.HasValue) { var val = query.SupportsSync.Value; @@ -167,7 +162,7 @@ namespace MediaBrowser.Server.Implementations.Devices try { - using (var fs = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) + using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read)) { await stream.CopyToAsync(fs).ConfigureAwait(false); } diff --git a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs b/Emby.Server.Implementations/Devices/DeviceRepository.cs similarity index 97% rename from MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs rename to Emby.Server.Implementations/Devices/DeviceRepository.cs index 6e67af82b4..f739765b34 100644 --- a/MediaBrowser.Server.Implementations/Devices/DeviceRepository.cs +++ b/Emby.Server.Implementations/Devices/DeviceRepository.cs @@ -1,18 +1,18 @@ -using MediaBrowser.Common.Configuration; -using MediaBrowser.Common.Extensions; -using MediaBrowser.Controller.Devices; -using MediaBrowser.Model.Devices; -using MediaBrowser.Model.Logging; -using MediaBrowser.Model.Serialization; -using MediaBrowser.Model.Session; -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Devices; +using MediaBrowser.Model.Devices; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Session; -namespace MediaBrowser.Server.Implementations.Devices +namespace Emby.Server.Implementations.Devices { public class DeviceRepository : IDeviceRepository { @@ -147,7 +147,7 @@ namespace MediaBrowser.Server.Implementations.Devices { _fileSystem.DeleteDirectory(path, true); } - catch (DirectoryNotFoundException) + catch (IOException) { } diff --git a/MediaBrowser.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs similarity index 91% rename from MediaBrowser.Server.Implementations/Dto/DtoService.cs rename to Emby.Server.Implementations/Dto/DtoService.cs index a0f7aa999e..8e6c1263d6 100644 --- a/MediaBrowser.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -19,15 +19,17 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Sync; -using MoreLinq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; -namespace MediaBrowser.Server.Implementations.Dto +namespace Emby.Server.Implementations.Dto { public class DtoService : IDtoService { @@ -457,12 +459,21 @@ namespace MediaBrowser.Server.Implementations.Dto if (dtoOptions.EnableUserData) { - dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user).ConfigureAwait(false); + dto.UserData = await _userDataRepository.GetUserDataDto(item, dto, user, dtoOptions.Fields).ConfigureAwait(false); } if (!dto.ChildCount.HasValue && item.SourceType == SourceType.Library) { - dto.ChildCount = GetChildCount(folder, user); + // For these types we can try to optimize and assume these values will be equal + if (item is MusicAlbum || item is Season) + { + dto.ChildCount = dto.RecursiveItemCount; + } + + if (dtoOptions.Fields.Contains(ItemFields.ChildCount)) + { + dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user); + } } if (fields.Contains(ItemFields.CumulativeRunTimeTicks)) @@ -480,7 +491,7 @@ namespace MediaBrowser.Server.Implementations.Dto { if (dtoOptions.EnableUserData) { - dto.UserData = _userDataRepository.GetUserDataDto(item, user).Result; + dto.UserData = await _userDataRepository.GetUserDataDto(item, user).ConfigureAwait(false); } } @@ -1149,28 +1160,29 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.Artists = hasArtist.Artists; - var artistItems = _libraryManager.GetArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); + //var artistItems = _libraryManager.GetArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); - dto.ArtistItems = artistItems.Items - .Select(i => - { - var artist = i.Item1; - return new NameIdPair - { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + //dto.ArtistItems = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); // Include artists that are not in the database yet, e.g., just added via metadata editor - var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + //var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList(); + dto.ArtistItems = new List(); dto.ArtistItems.AddRange(hasArtist.Artists - .Except(foundArtists, new DistinctNameComparer()) + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { // This should not be necessary but we're seeing some cases of it @@ -1199,23 +1211,48 @@ namespace MediaBrowser.Server.Implementations.Dto { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); - var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery - { - EnableTotalRecordCount = false, - ItemIds = new[] { item.Id.ToString("N") } - }); + //var artistItems = _libraryManager.GetAlbumArtists(new InternalItemsQuery + //{ + // EnableTotalRecordCount = false, + // ItemIds = new[] { item.Id.ToString("N") } + //}); - dto.AlbumArtists = artistItems.Items + //dto.AlbumArtists = artistItems.Items + // .Select(i => + // { + // var artist = i.Item1; + // return new NameIdPair + // { + // Name = artist.Name, + // Id = artist.Id.ToString("N") + // }; + // }) + // .ToList(); + + dto.AlbumArtists = new List(); + dto.AlbumArtists.AddRange(hasAlbumArtist.AlbumArtists + //.Except(foundArtists, new DistinctNameComparer()) .Select(i => { - var artist = i.Item1; - return new NameIdPair + // This should not be necessary but we're seeing some cases of it + if (string.IsNullOrWhiteSpace(i)) { - Name = artist.Name, - Id = artist.Id.ToString("N") - }; - }) - .ToList(); + return null; + } + + var artist = _libraryManager.GetArtist(i); + if (artist != null) + { + return new NameIdPair + { + Name = artist.Name, + Id = artist.Id.ToString("N") + }; + } + + return null; + + }).Where(i => i != null)); } // Add video info @@ -1349,6 +1386,27 @@ namespace MediaBrowser.Server.Implementations.Dto if (episodeSeries != null) { dto.SeriesStudio = episodeSeries.Studios.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(dto.SeriesStudio)) + { + try + { + var studio = _libraryManager.GetStudio(dto.SeriesStudio); + + if (studio != null) + { + dto.SeriesStudioInfo = new StudioDto + { + Name = dto.SeriesStudio, + Id = studio.Id.ToString("N"), + PrimaryImageTag = GetImageCacheTag(studio, ImageType.Primary) + }; + } + } + catch (Exception ex) + { + + } + } } } } @@ -1446,13 +1504,35 @@ namespace MediaBrowser.Server.Implementations.Dto } } + private BaseItem GetImageDisplayParent(BaseItem item) + { + var musicAlbum = item as MusicAlbum; + if (musicAlbum != null) + { + var artist = musicAlbum.MusicArtist; + if (artist != null) + { + return artist; + } + } + return item.GetParent(); + } + private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner) { + if (!item.SupportsInheritedParentImages) + { + return; + } + var logoLimit = options.GetImageLimit(ImageType.Logo); var artLimit = options.GetImageLimit(ImageType.Art); var thumbLimit = options.GetImageLimit(ImageType.Thumb); var backdropLimit = options.GetImageLimit(ImageType.Backdrop); + // For now. Emby apps are not using this + artLimit = 0; + if (logoLimit == 0 && artLimit == 0 && thumbLimit == 0 && backdropLimit == 0) { return; @@ -1462,7 +1542,7 @@ namespace MediaBrowser.Server.Implementations.Dto var isFirst = true; while (((!dto.HasLogo && logoLimit > 0) || (!dto.HasArtImage && artLimit > 0) || (!dto.HasThumb && thumbLimit > 0) || parent is Series) && - (parent = parent ?? (isFirst ? item.GetParent() ?? owner : parent)) != null) + (parent = parent ?? (isFirst ? GetImageDisplayParent(item) ?? owner : parent)) != null) { if (parent == null) { @@ -1513,7 +1593,13 @@ namespace MediaBrowser.Server.Implementations.Dto } isFirst = false; - parent = parent.GetParent(); + + if (!parent.SupportsInheritedParentImages) + { + break; + } + + parent = GetImageDisplayParent(parent); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj new file mode 100644 index 0000000000..d773fbbf73 --- /dev/null +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -0,0 +1,435 @@ + + + + + Debug + AnyCPU + {E383961B-9356-4D5D-8233-9A1079D03055} + Library + Properties + Emby.Server.Implementations + Emby.Server.Implementations + 512 + {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Profile7 + v4.5 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {9142eefa-7570-41e1-bfcc-468bb571af2f} + MediaBrowser.Common + + + {17e1f4e6-8abd-4fe5-9ecf-43d4b6087ba2} + MediaBrowser.Controller + + + {7eeeb4bb-f3e8-48fc-b4c5-70f0fff8329b} + MediaBrowser.Model + + + {442b5058-dcaf-4263-bb6a-f21e31120a1b} + MediaBrowser.Providers + + + {2e781478-814d-4a48-9d80-bff206441a65} + MediaBrowser.Server.Implementations + + + {680a1709-25eb-4d52-a87f-ee03ffd94baa} + ServiceStack + + + {4f26d5d8-a7b0-42b3-ba42-7cb7d245934e} + SocketHttpListener.Portable + + + ..\packages\Emby.XmlTv.1.0.3\lib\portable-net45+win8\Emby.XmlTv.dll + True + + + ..\packages\MediaBrowser.Naming.1.0.3\lib\portable-net45+win8\MediaBrowser.Naming.dll + True + + + ..\packages\SQLitePCL.pretty.1.1.0\lib\portable-net45+netcore45+wpa81+wp8\SQLitePCL.pretty.dll + True + + + ..\packages\SQLitePCLRaw.core.1.1.1\lib\portable-net45+netcore45+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\SQLitePCLRaw.core.dll + True + + + ..\packages\UniversalDetector.1.0.1\lib\portable-net45+sl4+wp71+win8+wpa81\UniversalDetector.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs similarity index 89% rename from MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs rename to Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs index d5f265ddad..38708648fe 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/AutomaticRestartEntryPoint.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller; +using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; @@ -11,8 +10,9 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { public class AutomaticRestartEntryPoint : IServerEntryPoint { @@ -22,10 +22,11 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly ISessionManager _sessionManager; private readonly IServerConfigurationManager _config; private readonly ILiveTvManager _liveTvManager; + private readonly ITimerFactory _timerFactory; - private Timer _timer; + private ITimer _timer; - public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config, ILiveTvManager liveTvManager) + public AutomaticRestartEntryPoint(IServerApplicationHost appHost, ILogger logger, ITaskManager iTaskManager, ISessionManager sessionManager, IServerConfigurationManager config, ILiveTvManager liveTvManager, ITimerFactory timerFactory) { _appHost = appHost; _logger = logger; @@ -33,6 +34,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _sessionManager = sessionManager; _config = config; _liveTvManager = liveTvManager; + _timerFactory = timerFactory; } public void Run() @@ -49,7 +51,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints if (_appHost.HasPendingRestart) { - _timer = new Timer(TimerCallback, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); + _timer = _timerFactory.Create(TimerCallback, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); } } diff --git a/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs b/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs new file mode 100644 index 0000000000..8ae85e3909 --- /dev/null +++ b/Emby.Server.Implementations/EntryPoints/KeepServerAwake.cs @@ -0,0 +1,65 @@ +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Controller.Session; +using MediaBrowser.Model.Logging; +using System; +using System.Linq; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Threading; + +namespace Emby.Server.Implementations.EntryPoints +{ + public class KeepServerAwake : IServerEntryPoint + { + private readonly ISessionManager _sessionManager; + private readonly ILogger _logger; + private ITimer _timer; + private readonly IServerApplicationHost _appHost; + private readonly ITimerFactory _timerFactory; + private readonly IPowerManagement _powerManagement; + + public KeepServerAwake(ISessionManager sessionManager, ILogger logger, IServerApplicationHost appHost, ITimerFactory timerFactory, IPowerManagement powerManagement) + { + _sessionManager = sessionManager; + _logger = logger; + _appHost = appHost; + _timerFactory = timerFactory; + _powerManagement = powerManagement; + } + + public void Run() + { + _timer = _timerFactory.Create(OnTimerCallback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); + } + + private void OnTimerCallback(object state) + { + var now = DateTime.UtcNow; + + try + { + if (_sessionManager.Sessions.Any(i => (now - i.LastActivityDate).TotalMinutes < 15)) + { + _powerManagement.PreventSystemStandby(); + } + else + { + _powerManagement.AllowSystemStandby(); + } + } + catch (Exception ex) + { + _logger.ErrorException("Error resetting system standby timer", ex); + } + } + + public void Dispose() + { + if (_timer != null) + { + _timer.Dispose(); + _timer = null; + } + } + } +} diff --git a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs similarity index 94% rename from MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs rename to Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index afc4e9702e..91142f9284 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -4,14 +4,15 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; -using MoreLinq; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { public class LibraryChangedNotifier : IServerEntryPoint { @@ -23,6 +24,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly ISessionManager _sessionManager; private readonly IUserManager _userManager; private readonly ILogger _logger; + private readonly ITimerFactory _timerFactory; /// /// The _library changed sync lock @@ -40,19 +42,20 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// Gets or sets the library update timer. /// /// The library update timer. - private Timer LibraryUpdateTimer { get; set; } + private ITimer LibraryUpdateTimer { get; set; } /// /// The library update duration /// private const int LibraryUpdateDuration = 5000; - public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger) + public LibraryChangedNotifier(ILibraryManager libraryManager, ISessionManager sessionManager, IUserManager userManager, ILogger logger, ITimerFactory timerFactory) { _libraryManager = libraryManager; _sessionManager = sessionManager; _userManager = userManager; _logger = logger; + _timerFactory = timerFactory; } public void Run() @@ -79,7 +82,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { if (LibraryUpdateTimer == null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, + LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else @@ -112,7 +115,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { if (LibraryUpdateTimer == null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, + LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else @@ -140,7 +143,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { if (LibraryUpdateTimer == null) { - LibraryUpdateTimer = new Timer(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, + LibraryUpdateTimer = _timerFactory.Create(LibraryUpdateTimerCallback, null, LibraryUpdateDuration, Timeout.Infinite); } else diff --git a/MediaBrowser.Server.Implementations/EntryPoints/LoadRegistrations.cs b/Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs similarity index 81% rename from MediaBrowser.Server.Implementations/EntryPoints/LoadRegistrations.cs rename to Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs index f41d81137f..0203b51928 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/LoadRegistrations.cs +++ b/Emby.Server.Implementations/EntryPoints/LoadRegistrations.cs @@ -3,9 +3,9 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; using System; using System.Threading.Tasks; -using MediaBrowser.Common.Threading; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class LoadRegistrations @@ -22,16 +22,18 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// private readonly ILogger _logger; - private PeriodicTimer _timer; + private ITimer _timer; + private readonly ITimerFactory _timerFactory; /// /// Initializes a new instance of the class. /// /// The security manager. /// The log manager. - public LoadRegistrations(ISecurityManager securityManager, ILogManager logManager) + public LoadRegistrations(ISecurityManager securityManager, ILogManager logManager, ITimerFactory timerFactory) { _securityManager = securityManager; + _timerFactory = timerFactory; _logger = logManager.GetLogger("Registration Loader"); } @@ -41,7 +43,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// public void Run() { - _timer = new PeriodicTimer(s => LoadAllRegistrations(), null, TimeSpan.FromMilliseconds(100), TimeSpan.FromHours(12)); + _timer = _timerFactory.Create(s => LoadAllRegistrations(), null, TimeSpan.FromMilliseconds(100), TimeSpan.FromHours(12)); } private async Task LoadAllRegistrations() diff --git a/MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs similarity index 89% rename from MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs rename to Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs index 414fda400b..b674fc39bb 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs @@ -7,7 +7,7 @@ using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Logging; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { public class RecordingNotifier : IServerEntryPoint { @@ -32,22 +32,22 @@ namespace MediaBrowser.Server.Implementations.EntryPoints _liveTvManager.SeriesTimerCreated += _liveTvManager_SeriesTimerCreated; } - private void _liveTvManager_SeriesTimerCreated(object sender, Model.Events.GenericEventArgs e) + private void _liveTvManager_SeriesTimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs e) { SendMessage("SeriesTimerCreated", e.Argument); } - private void _liveTvManager_TimerCreated(object sender, Model.Events.GenericEventArgs e) + private void _liveTvManager_TimerCreated(object sender, MediaBrowser.Model.Events.GenericEventArgs e) { SendMessage("TimerCreated", e.Argument); } - private void _liveTvManager_SeriesTimerCancelled(object sender, Model.Events.GenericEventArgs e) + private void _liveTvManager_SeriesTimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs e) { SendMessage("SeriesTimerCancelled", e.Argument); } - private void _liveTvManager_TimerCancelled(object sender, Model.Events.GenericEventArgs e) + private void _liveTvManager_TimerCancelled(object sender, MediaBrowser.Model.Events.GenericEventArgs e) { SendMessage("TimerCancelled", e.Argument); } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs similarity index 95% rename from MediaBrowser.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs rename to Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs index a0b7ff515c..77de849a17 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs +++ b/Emby.Server.Implementations/EntryPoints/RefreshUsersMetadata.cs @@ -2,7 +2,7 @@ using MediaBrowser.Controller.Plugins; using System.Threading; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class RefreshUsersMetadata diff --git a/MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs similarity index 98% rename from MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs rename to Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs index 3ea8417f86..4d640bc95d 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/ServerEventNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/ServerEventNotifier.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; @@ -13,8 +12,9 @@ using MediaBrowser.Model.Sync; using System; using System.Collections.Generic; using System.Threading; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class WebSocketEvents diff --git a/MediaBrowser.Server.Startup.Common/EntryPoints/StartupWizard.cs b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs similarity index 91% rename from MediaBrowser.Server.Startup.Common/EntryPoints/StartupWizard.cs rename to Emby.Server.Implementations/EntryPoints/StartupWizard.cs index f9d173c595..424153f220 100644 --- a/MediaBrowser.Server.Startup.Common/EntryPoints/StartupWizard.cs +++ b/Emby.Server.Implementations/EntryPoints/StartupWizard.cs @@ -1,9 +1,9 @@ -using MediaBrowser.Controller; +using Emby.Server.Implementations.Browser; +using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Startup.Common.Browser; -namespace MediaBrowser.Server.Startup.Common.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class StartupWizard diff --git a/Emby.Server.Implementations/EntryPoints/SystemEvents.cs b/Emby.Server.Implementations/EntryPoints/SystemEvents.cs new file mode 100644 index 0000000000..021ae47ec3 --- /dev/null +++ b/Emby.Server.Implementations/EntryPoints/SystemEvents.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using MediaBrowser.Model.System; +using MediaBrowser.Controller.Plugins; +using MediaBrowser.Common; + +namespace Emby.Server.Implementations.EntryPoints +{ + public class SystemEvents : IServerEntryPoint + { + private readonly ISystemEvents _systemEvents; + private readonly IApplicationHost _appHost; + + public SystemEvents(ISystemEvents systemEvents, IApplicationHost appHost) + { + _systemEvents = systemEvents; + _appHost = appHost; + } + + public void Run() + { + _systemEvents.SessionLogoff += _systemEvents_SessionLogoff; + _systemEvents.SystemShutdown += _systemEvents_SystemShutdown; + } + + private void _systemEvents_SessionLogoff(object sender, EventArgs e) + { + if (!_appHost.IsRunningAsService) + { + _appHost.Shutdown(); + } + } + + private void _systemEvents_SystemShutdown(object sender, EventArgs e) + { + _appHost.Shutdown(); + } + + public void Dispose() + { + _systemEvents.SystemShutdown -= _systemEvents_SystemShutdown; + } + } +} diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs similarity index 71% rename from MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs rename to Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 386c16513b..df5a7c985d 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -1,12 +1,12 @@ -using MediaBrowser.Common.Net; +using System; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; -using MediaBrowser.Server.Implementations.Udp; -using System.Net.Sockets; +using Emby.Server.Implementations.Udp; +using MediaBrowser.Model.Net; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class UdpServerEntryPoint @@ -23,10 +23,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// The _logger /// private readonly ILogger _logger; - /// - /// The _network manager - /// - private readonly INetworkManager _networkManager; + private readonly ISocketFactory _socketFactory; private readonly IServerApplicationHost _appHost; private readonly IJsonSerializer _json; @@ -35,16 +32,12 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// /// Initializes a new instance of the class. /// - /// The logger. - /// The network manager. - /// The application host. - /// The json. - public UdpServerEntryPoint(ILogger logger, INetworkManager networkManager, IServerApplicationHost appHost, IJsonSerializer json) + public UdpServerEntryPoint(ILogger logger, IServerApplicationHost appHost, IJsonSerializer json, ISocketFactory socketFactory) { _logger = logger; - _networkManager = networkManager; _appHost = appHost; _json = json; + _socketFactory = socketFactory; } /// @@ -52,7 +45,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints /// public void Run() { - var udpServer = new UdpServer(_logger, _networkManager, _appHost, _json); + var udpServer = new UdpServer(_logger, _appHost, _json, _socketFactory); try { @@ -60,7 +53,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints UdpServer = udpServer; } - catch (SocketException ex) + catch (Exception ex) { _logger.ErrorException("Failed to start UDP Server", ex); } diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs similarity index 98% rename from MediaBrowser.Server.Implementations/EntryPoints/UsageEntryPoint.cs rename to Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs index d14bd43689..1b897ca299 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UsageEntryPoint.cs @@ -12,7 +12,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.Configuration; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { /// /// Class UsageEntryPoint diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs b/Emby.Server.Implementations/EntryPoints/UsageReporter.cs similarity index 98% rename from MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs rename to Emby.Server.Implementations/EntryPoints/UsageReporter.cs index e445300e4d..be848acb77 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UsageReporter.cs +++ b/Emby.Server.Implementations/EntryPoints/UsageReporter.cs @@ -10,7 +10,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.Logging; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { public class UsageReporter { diff --git a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs similarity index 92% rename from MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs rename to Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs index 2bb0101330..b934101809 100644 --- a/MediaBrowser.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs @@ -5,14 +5,15 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Session; -using MoreLinq; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.EntryPoints +namespace Emby.Server.Implementations.EntryPoints { class UserDataChangeNotifier : IServerEntryPoint { @@ -22,17 +23,19 @@ namespace MediaBrowser.Server.Implementations.EntryPoints private readonly IUserManager _userManager; private readonly object _syncLock = new object(); - private Timer UpdateTimer { get; set; } + private ITimer UpdateTimer { get; set; } + private readonly ITimerFactory _timerFactory; private const int UpdateDuration = 500; private readonly Dictionary> _changedItems = new Dictionary>(); - public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager) + public UserDataChangeNotifier(IUserDataManager userDataManager, ISessionManager sessionManager, ILogger logger, IUserManager userManager, ITimerFactory timerFactory) { _userDataManager = userDataManager; _sessionManager = sessionManager; _logger = logger; _userManager = userManager; + _timerFactory = timerFactory; } public void Run() @@ -51,7 +54,7 @@ namespace MediaBrowser.Server.Implementations.EntryPoints { if (UpdateTimer == null) { - UpdateTimer = new Timer(UpdateTimerCallback, null, UpdateDuration, + UpdateTimer = _timerFactory.Create(UpdateTimerCallback, null, UpdateDuration, Timeout.Infinite); } else diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs similarity index 91% rename from MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs rename to Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs index d33b12a9a7..e725d22f51 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInfo.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegInfo.cs @@ -1,4 +1,4 @@ -namespace MediaBrowser.Server.Startup.Common.FFMpeg +namespace Emby.Server.Implementations.FFMpeg { /// /// Class FFMpegInfo diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs similarity index 90% rename from MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs rename to Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs index a2a44f805c..1d769acec1 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegInstallInfo.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegInstallInfo.cs @@ -1,5 +1,5 @@  -namespace MediaBrowser.Server.Startup.Common.FFMpeg +namespace Emby.Server.Implementations.FFMpeg { public class FFMpegInstallInfo { diff --git a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs similarity index 86% rename from MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs rename to Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs index 68e2a49275..2becebb3d3 100644 --- a/MediaBrowser.Server.Startup.Common/FFMpeg/FFMpegLoader.cs +++ b/Emby.Server.Implementations/FFMpeg/FFMpegLoader.cs @@ -2,16 +2,16 @@ using MediaBrowser.Common.Net; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; -using Mono.Unix.Native; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using Emby.Server.Implementations; +using Emby.Server.Implementations.FFMpeg; -namespace MediaBrowser.Server.Startup.Common.FFMpeg +namespace Emby.Server.Implementations.FFMpeg { public class FFMpegLoader { @@ -20,21 +20,19 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg private readonly ILogger _logger; private readonly IZipClient _zipClient; private readonly IFileSystem _fileSystem; - private readonly NativeEnvironment _environment; private readonly FFMpegInstallInfo _ffmpegInstallInfo; - public FFMpegLoader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem, NativeEnvironment environment, FFMpegInstallInfo ffmpegInstallInfo) + public FFMpegLoader(ILogger logger, IApplicationPaths appPaths, IHttpClient httpClient, IZipClient zipClient, IFileSystem fileSystem, FFMpegInstallInfo ffmpegInstallInfo) { _logger = logger; _appPaths = appPaths; _httpClient = httpClient; _zipClient = zipClient; _fileSystem = fileSystem; - _environment = environment; _ffmpegInstallInfo = ffmpegInstallInfo; } - public async Task GetFFMpegInfo(NativeEnvironment environment, StartupOptions options, IProgress progress) + public async Task GetFFMpegInfo(StartupOptions options, IProgress progress) { var customffMpegPath = options.GetOption("-ffmpeg"); var customffProbePath = options.GetOption("-ffprobe"); @@ -121,11 +119,11 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg { var encoderFilename = Path.GetFileName(info.EncoderPath); var probeFilename = Path.GetFileName(info.ProbePath); - - foreach (var directory in Directory.EnumerateDirectories(rootEncoderPath, "*", SearchOption.TopDirectoryOnly) + + foreach (var directory in _fileSystem.GetDirectoryPaths(rootEncoderPath) .ToList()) { - var allFiles = Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories).ToList(); + var allFiles = _fileSystem.GetFilePaths(directory, true).ToList(); var encoder = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), encoderFilename, StringComparison.OrdinalIgnoreCase)); var probe = allFiles.FirstOrDefault(i => string.Equals(Path.GetFileName(i), probeFilename, StringComparison.OrdinalIgnoreCase)); @@ -184,7 +182,7 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg { ExtractArchive(downloadinfo, tempFile, tempFolder); - var files = Directory.EnumerateFiles(tempFolder, "*", SearchOption.AllDirectories) + var files = _fileSystem.GetFilePaths(tempFolder, true) .ToList(); foreach (var file in files.Where(i => @@ -209,13 +207,7 @@ namespace MediaBrowser.Server.Startup.Common.FFMpeg private void SetFilePermissions(string path) { - // Linux: File permission to 666, and user's execute bit - if (_environment.OperatingSystem == OperatingSystem.Bsd || _environment.OperatingSystem == OperatingSystem.Linux || _environment.OperatingSystem == OperatingSystem.Osx) - { - _logger.Info("Syscall.chmod {0} FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH", path); - - Syscall.chmod(path, FilePermissions.DEFFILEMODE | FilePermissions.S_IRWXU | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH); - } + _fileSystem.SetExecutable(path); } private void ExtractArchive(FFMpegInstallInfo downloadinfo, string archivePath, string targetPath) diff --git a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs similarity index 96% rename from MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs rename to Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs index 5e01666a9a..5bb21d02ac 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/EpisodeFileOrganizer.cs @@ -7,8 +7,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Implementations.Library; -using MediaBrowser.Server.Implementations.Logging; using System; using System.Collections.Generic; using System.Globalization; @@ -16,9 +14,15 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using Emby.Server.Implementations.Library; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Naming.TV; +using EpisodeInfo = MediaBrowser.Controller.Providers.EpisodeInfo; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { public class EpisodeFileOrganizer { @@ -53,7 +57,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization OriginalPath = path, OriginalFileName = Path.GetFileName(path), Type = FileOrganizerType.Episode, - FileSize = new FileInfo(path).Length + FileSize = _fileSystem.GetFileInfo(path).Length }; try @@ -66,10 +70,10 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } var namingOptions = ((LibraryManager)_libraryManager).GetNamingOptions(); - var resolver = new Naming.TV.EpisodeResolver(namingOptions, new PatternsLogger()); + var resolver = new EpisodeResolver(namingOptions, new NullLogger()); var episodeInfo = resolver.Resolve(path, false) ?? - new Naming.TV.EpisodeInfo(); + new MediaBrowser.Naming.TV.EpisodeInfo(); var seriesName = episodeInfo.SeriesName; @@ -503,7 +507,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization episodePaths.AddRange(filesOfOtherExtensions); } - catch (DirectoryNotFoundException) + catch (IOException) { // No big deal. Maybe the season folder doesn't already exist. } @@ -515,6 +519,12 @@ namespace MediaBrowser.Server.Implementations.FileOrganization private void PerformFileSorting(TvFileOrganizationOptions options, FileOrganizationResult result) { + // We should probably handle this earlier so that we never even make it this far + if (string.Equals(result.OriginalPath, result.TargetPath, StringComparison.OrdinalIgnoreCase)) + { + return; + } + _libraryMonitor.ReportFileSystemChangeBeginning(result.TargetPath); _fileSystem.CreateDirectory(Path.GetDirectoryName(result.TargetPath)); @@ -573,7 +583,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization result.ExtractedName = nameWithoutYear; result.ExtractedYear = yearInName; - var series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery + var series = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Series).Name }, Recursive = true @@ -591,7 +601,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization if (info != null) { - series = _libraryManager.GetItemList(new Controller.Entities.InternalItemsQuery + series = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = new[] { typeof(Series).Name }, Recursive = true, @@ -806,8 +816,8 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { try { - var sourceFileInfo = new FileInfo(sourcePath); - var destinationFileInfo = new FileInfo(newPath); + var sourceFileInfo = _fileSystem.GetFileInfo(sourcePath); + var destinationFileInfo = _fileSystem.GetFileInfo(newPath); if (sourceFileInfo.Length == destinationFileInfo.Length) { @@ -818,7 +828,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization { return false; } - catch (DirectoryNotFoundException) + catch (IOException) { return false; } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs b/Emby.Server.Implementations/FileOrganization/Extensions.cs similarity index 94% rename from MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs rename to Emby.Server.Implementations/FileOrganization/Extensions.cs index c560152dbe..506bc0327e 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/Extensions.cs +++ b/Emby.Server.Implementations/FileOrganization/Extensions.cs @@ -2,7 +2,7 @@ using MediaBrowser.Model.FileOrganization; using System.Collections.Generic; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { public static class ConfigurationExtension { diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs similarity index 95% rename from MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs rename to Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs index 5c3814f669..2a01765472 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs +++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationNotifier.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.FileOrganization; +using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; @@ -7,8 +6,9 @@ using MediaBrowser.Model.FileOrganization; using MediaBrowser.Model.Logging; using System; using System.Threading; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { /// /// Class SessionInfoWebSocketListener diff --git a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs similarity index 98% rename from MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs rename to Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs index a42eba6cae..4094e6b9b8 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/FileOrganizationService.cs +++ b/Emby.Server.Implementations/FileOrganization/FileOrganizationService.cs @@ -1,5 +1,4 @@ using MediaBrowser.Common.Extensions; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Library; @@ -14,12 +13,15 @@ using System.Collections.Concurrent; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Events; using MediaBrowser.Common.Events; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { public class FileOrganizationService : IFileOrganizationService { diff --git a/MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs b/Emby.Server.Implementations/FileOrganization/NameUtils.cs similarity index 77% rename from MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs rename to Emby.Server.Implementations/FileOrganization/NameUtils.cs index 624133d4fb..eb22ca4ea8 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/NameUtils.cs +++ b/Emby.Server.Implementations/FileOrganization/NameUtils.cs @@ -2,10 +2,9 @@ using MediaBrowser.Controller.Entities; using System; using System.Globalization; -using System.Linq; -using System.Text; +using MediaBrowser.Controller.Extensions; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { public static class NameUtils { @@ -54,7 +53,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization private static string GetComparableName(string name) { - name = RemoveDiacritics(name); + name = name.RemoveDiacritics(); name = " " + name + " "; @@ -78,19 +77,5 @@ namespace MediaBrowser.Server.Implementations.FileOrganization return name.Trim(); } - - /// - /// Removes the diacritics. - /// - /// The text. - /// System.String. - private static string RemoveDiacritics(string text) - { - return String.Concat( - text.Normalize(NormalizationForm.FormD) - .Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != - UnicodeCategory.NonSpacingMark) - ).Normalize(NormalizationForm.FormC); - } } } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs similarity index 78% rename from MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs rename to Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs index de98b83ef2..5be7ba7ada 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs +++ b/Emby.Server.Implementations/FileOrganization/OrganizerScheduledTask.cs @@ -1,5 +1,4 @@ -using MediaBrowser.Common.ScheduledTasks; -using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.FileOrganization; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -9,11 +8,14 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { - public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask, IScheduledTaskActivityLog, IHasKey + public class OrganizerScheduledTask : IScheduledTask, IConfigurableScheduledTask { private readonly ILibraryMonitor _libraryMonitor; private readonly ILibraryManager _libraryManager; @@ -63,12 +65,17 @@ namespace MediaBrowser.Server.Implementations.FileOrganization } } - public IEnumerable GetDefaultTriggers() + /// + /// Creates the triggers that define when the task will run + /// + /// IEnumerable{BaseTaskTrigger}. + public IEnumerable GetDefaultTriggers() { - return new ITaskTrigger[] - { - new IntervalTrigger{ Interval = TimeSpan.FromMinutes(5)} - }; + return new[] { + + // Every so often + new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromMinutes(5).Ticks} + }; } public bool IsHidden @@ -81,7 +88,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization get { return GetAutoOrganizeOptions().TvOptions.IsEnabled; } } - public bool IsActivityLogged + public bool IsLogged { get { return false; } } diff --git a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs similarity index 97% rename from MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs rename to Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs index 4f42d8a20d..2850c3a610 100644 --- a/MediaBrowser.Server.Implementations/FileOrganization/TvFolderOrganizer.cs +++ b/Emby.Server.Implementations/FileOrganization/TvFolderOrganizer.cs @@ -10,9 +10,11 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Server.Implementations.FileOrganization +namespace Emby.Server.Implementations.FileOrganization { public class TvFolderOrganizer { @@ -189,7 +191,7 @@ namespace MediaBrowser.Server.Implementations.FileOrganization _fileSystem.DeleteDirectory(path, false); } catch (UnauthorizedAccessException) { } - catch (DirectoryNotFoundException) { } + catch (IOException) { } } } catch (UnauthorizedAccessException) { } diff --git a/MediaBrowser.Server.Implementations/HttpServer/GetSwaggerResource.cs b/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs similarity index 79% rename from MediaBrowser.Server.Implementations/HttpServer/GetSwaggerResource.cs rename to Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs index 36a257f632..819ede1ab6 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/GetSwaggerResource.cs +++ b/Emby.Server.Implementations/HttpServer/GetSwaggerResource.cs @@ -1,6 +1,6 @@ -using ServiceStack; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { /// /// Class GetDashboardResource diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs new file mode 100644 index 0000000000..0e1f5a5517 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -0,0 +1,729 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.Logging; +using ServiceStack; +using ServiceStack.Host; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.HttpServer.SocketSharp; +using MediaBrowser.Common.Net; +using MediaBrowser.Common.Security; +using MediaBrowser.Controller; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Serialization; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Text; +using SocketHttpListener.Net; +using SocketHttpListener.Primitives; + +namespace Emby.Server.Implementations.HttpServer +{ + public class HttpListenerHost : ServiceStackHost, IHttpServer + { + private string DefaultRedirectPath { get; set; } + + private readonly ILogger _logger; + public IEnumerable UrlPrefixes { get; private set; } + + private readonly List _restServices = new List(); + + private IHttpListener _listener; + + public event EventHandler WebSocketConnected; + public event EventHandler WebSocketConnecting; + + private readonly IServerConfigurationManager _config; + private readonly INetworkManager _networkManager; + private readonly IMemoryStreamFactory _memoryStreamProvider; + + private readonly IServerApplicationHost _appHost; + + private readonly ITextEncoding _textEncoding; + private readonly ISocketFactory _socketFactory; + private readonly ICryptoProvider _cryptoProvider; + + private readonly IJsonSerializer _jsonSerializer; + private readonly IXmlSerializer _xmlSerializer; + private readonly ICertificate _certificate; + private readonly IEnvironmentInfo _environment; + private readonly IStreamFactory _streamFactory; + private readonly Func> _funcParseFn; + private readonly bool _enableDualModeSockets; + + public HttpListenerHost(IServerApplicationHost applicationHost, + ILogger logger, + IServerConfigurationManager config, + string serviceName, + string defaultRedirectPath, INetworkManager networkManager, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IJsonSerializer jsonSerializer, IXmlSerializer xmlSerializer, IEnvironmentInfo environment, ICertificate certificate, IStreamFactory streamFactory, Func> funcParseFn, bool enableDualModeSockets) + : base(serviceName) + { + _appHost = applicationHost; + DefaultRedirectPath = defaultRedirectPath; + _networkManager = networkManager; + _memoryStreamProvider = memoryStreamProvider; + _textEncoding = textEncoding; + _socketFactory = socketFactory; + _cryptoProvider = cryptoProvider; + _jsonSerializer = jsonSerializer; + _xmlSerializer = xmlSerializer; + _environment = environment; + _certificate = certificate; + _streamFactory = streamFactory; + _funcParseFn = funcParseFn; + _enableDualModeSockets = enableDualModeSockets; + _config = config; + + _logger = logger; + } + + public string GlobalResponse { get; set; } + + readonly Dictionary _mapExceptionToStatusCode = new Dictionary + { + {typeof (InvalidOperationException), 500}, + {typeof (NotImplementedException), 500}, + {typeof (ResourceNotFoundException), 404}, + {typeof (FileNotFoundException), 404}, + //{typeof (DirectoryNotFoundException), 404}, + {typeof (SecurityException), 401}, + {typeof (PaymentRequiredException), 402}, + {typeof (UnauthorizedAccessException), 500}, + {typeof (PlatformNotSupportedException), 500}, + {typeof (NotSupportedException), 500} + }; + + public override void Configure() + { + var requestFilters = _appHost.GetExports().ToList(); + foreach (var filter in requestFilters) + { + GlobalRequestFilters.Add(filter.Filter); + } + + GlobalResponseFilters.Add(new ResponseFilter(_logger).FilterResponse); + } + + protected override ILogger Logger + { + get + { + return _logger; + } + } + + public override T Resolve() + { + return _appHost.Resolve(); + } + + public override T TryResolve() + { + return _appHost.TryResolve(); + } + + public override object CreateInstance(Type type) + { + return _appHost.CreateInstance(type); + } + + protected override ServiceController CreateServiceController() + { + var types = _restServices.Select(r => r.GetType()).ToArray(); + + return new ServiceController(() => types); + } + + public override ServiceStackHost Start(string listeningAtUrlBase) + { + StartListener(); + return this; + } + + /// + /// Starts the Web Service + /// + private void StartListener() + { + WebSocketSharpRequest.HandlerFactoryPath = GetHandlerPathIfAny(UrlPrefixes.First()); + + _listener = GetListener(); + + _listener.WebSocketConnected = OnWebSocketConnected; + _listener.WebSocketConnecting = OnWebSocketConnecting; + _listener.ErrorHandler = ErrorHandler; + _listener.RequestHandler = RequestHandler; + + _listener.Start(UrlPrefixes); + } + + public static string GetHandlerPathIfAny(string listenerUrl) + { + if (listenerUrl == null) return null; + var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase); + if (pos == -1) return null; + var startHostUrl = listenerUrl.Substring(pos + "://".Length); + var endPos = startHostUrl.IndexOf('/'); + if (endPos == -1) return null; + var endHostUrl = startHostUrl.Substring(endPos + 1); + return string.IsNullOrEmpty(endHostUrl) ? null : endHostUrl.TrimEnd('/'); + } + + private IHttpListener GetListener() + { + return new WebSocketSharpListener(_logger, + _certificate, + _memoryStreamProvider, + _textEncoding, + _networkManager, + _socketFactory, + _cryptoProvider, + _streamFactory, + _enableDualModeSockets, + GetRequest); + } + + private IHttpRequest GetRequest(HttpListenerContext httpContext) + { + var operationName = httpContext.Request.GetOperationName(); + + var req = new WebSocketSharpRequest(httpContext, operationName, _logger, _memoryStreamProvider); + + return req; + } + + private void OnWebSocketConnecting(WebSocketConnectingEventArgs args) + { + if (_disposed) + { + return; + } + + if (WebSocketConnecting != null) + { + WebSocketConnecting(this, args); + } + } + + private void OnWebSocketConnected(WebSocketConnectEventArgs args) + { + if (_disposed) + { + return; + } + + if (WebSocketConnected != null) + { + WebSocketConnected(this, args); + } + } + + private void ErrorHandler(Exception ex, IRequest httpReq) + { + try + { + _logger.ErrorException("Error processing request", ex); + + var httpRes = httpReq.Response; + + if (httpRes.IsClosed) + { + return; + } + + int statusCode; + if (!_mapExceptionToStatusCode.TryGetValue(ex.GetType(), out statusCode)) + { + statusCode = 500; + } + httpRes.StatusCode = statusCode; + + httpRes.ContentType = "text/html"; + Write(httpRes, ex.Message); + } + catch + { + //_logger.ErrorException("Error this.ProcessRequest(context)(Exception while writing error to the response)", errorEx); + } + } + + /// + /// Shut down the Web Service + /// + public void Stop() + { + if (_listener != null) + { + _listener.Stop(); + } + } + + private readonly Dictionary _skipLogExtensions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {".js", 0}, + {".css", 0}, + {".woff", 0}, + {".woff2", 0}, + {".ttf", 0}, + {".html", 0} + }; + + private bool EnableLogging(string url, string localPath) + { + var extension = GetExtension(url); + + if (string.IsNullOrWhiteSpace(extension) || !_skipLogExtensions.ContainsKey(extension)) + { + if (string.IsNullOrWhiteSpace(localPath) || localPath.IndexOf("system/ping", StringComparison.OrdinalIgnoreCase) == -1) + { + return true; + } + } + + return false; + } + + private string GetExtension(string url) + { + var parts = url.Split(new[] { '?' }, 2); + + return Path.GetExtension(parts[0]); + } + + public static string RemoveQueryStringByKey(string url, string key) + { + var uri = new Uri(url); + + // this gets all the query string key value pairs as a collection + var newQueryString = MyHttpUtility.ParseQueryString(uri.Query); + + var originalCount = newQueryString.Count; + + if (originalCount == 0) + { + return url; + } + + // this removes the key if exists + newQueryString.Remove(key); + + if (originalCount == newQueryString.Count) + { + return url; + } + + // this gets the page path from root without QueryString + string pagePathWithoutQueryString = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0]; + + return newQueryString.Count > 0 + ? String.Format("{0}?{1}", pagePathWithoutQueryString, newQueryString) + : pagePathWithoutQueryString; + } + + private string GetUrlToLog(string url) + { + url = RemoveQueryStringByKey(url, "api_key"); + + return url; + } + + private string NormalizeConfiguredLocalAddress(string address) + { + var index = address.Trim('/').IndexOf('/'); + + if (index != -1) + { + address = address.Substring(index + 1); + } + + return address.Trim('/'); + } + + private bool ValidateHost(Uri url) + { + var hosts = _config + .Configuration + .LocalNetworkAddresses + .Select(NormalizeConfiguredLocalAddress) + .ToList(); + + if (hosts.Count == 0) + { + return true; + } + + var host = url.Host ?? string.Empty; + + _logger.Debug("Validating host {0}", host); + + if (_networkManager.IsInPrivateAddressSpace(host)) + { + hosts.Add("localhost"); + hosts.Add("127.0.0.1"); + + return hosts.Any(i => host.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1); + } + + return true; + } + + /// + /// Overridable method that can be used to implement a custom hnandler + /// + /// The HTTP req. + /// The URL. + /// Task. + protected async Task RequestHandler(IHttpRequest httpReq, Uri url) + { + var date = DateTime.Now; + var httpRes = httpReq.Response; + bool enableLog = false; + string urlToLog = null; + string remoteIp = null; + + try + { + if (_disposed) + { + httpRes.StatusCode = 503; + httpRes.ContentType = "text/plain"; + Write(httpRes, "Server shutting down"); + return; + } + + if (!ValidateHost(url)) + { + httpRes.StatusCode = 400; + httpRes.ContentType = "text/plain"; + Write(httpRes, "Invalid host"); + return; + } + + if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) + { + httpRes.StatusCode = 200; + httpRes.AddHeader("Access-Control-Allow-Origin", "*"); + httpRes.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + httpRes.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + httpRes.ContentType = "text/plain"; + Write(httpRes, string.Empty); + return; + } + + var operationName = httpReq.OperationName; + var localPath = url.LocalPath; + + var urlString = url.OriginalString; + enableLog = EnableLogging(urlString, localPath); + urlToLog = urlString; + + if (enableLog) + { + urlToLog = GetUrlToLog(urlString); + remoteIp = httpReq.RemoteIp; + + LoggerUtils.LogRequest(_logger, urlToLog, httpReq.HttpMethod, httpReq.UserAgent); + } + + if (string.Equals(localPath, "/emby/", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/emby", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "emby/" + DefaultRedirectPath); + return; + } + + if (string.Equals(localPath, "/mediabrowser/", StringComparison.OrdinalIgnoreCase) || + string.Equals(localPath, "/mediabrowser", StringComparison.OrdinalIgnoreCase) || + localPath.IndexOf("mediabrowser/web", StringComparison.OrdinalIgnoreCase) != -1) + { + httpRes.StatusCode = 200; + httpRes.ContentType = "text/html"; + var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) + .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) + { + Write(httpRes, + "EmbyPlease update your Emby bookmark to " + newUrl + ""); + return; + } + } + + if (localPath.IndexOf("dashboard/", StringComparison.OrdinalIgnoreCase) != -1 && + localPath.IndexOf("web/dashboard", StringComparison.OrdinalIgnoreCase) == -1) + { + httpRes.StatusCode = 200; + httpRes.ContentType = "text/html"; + var newUrl = urlString.Replace("mediabrowser", "emby", StringComparison.OrdinalIgnoreCase) + .Replace("/dashboard/", "/web/", StringComparison.OrdinalIgnoreCase); + + if (!string.Equals(newUrl, urlString, StringComparison.OrdinalIgnoreCase)) + { + Write(httpRes, + "EmbyPlease update your Emby bookmark to " + newUrl + ""); + return; + } + } + + if (string.Equals(localPath, "/web", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/web/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "../" + DefaultRedirectPath); + return; + } + if (string.Equals(localPath, "/", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, DefaultRedirectPath); + return; + } + if (string.IsNullOrEmpty(localPath)) + { + RedirectToUrl(httpRes, "/" + DefaultRedirectPath); + return; + } + + if (string.Equals(localPath, "/emby/pin", StringComparison.OrdinalIgnoreCase)) + { + RedirectToUrl(httpRes, "web/pin.html"); + return; + } + + if (!string.IsNullOrWhiteSpace(GlobalResponse)) + { + httpRes.StatusCode = 503; + httpRes.ContentType = "text/html"; + Write(httpRes, GlobalResponse); + return; + } + + var handler = HttpHandlerFactory.GetHandler(httpReq, _logger); + + if (handler != null) + { + await handler.ProcessRequestAsync(httpReq, httpRes, operationName).ConfigureAwait(false); + } + else + { + ErrorHandler(new FileNotFoundException(), httpReq); + } + } + catch (Exception ex) + { + ErrorHandler(ex, httpReq); + } + finally + { + httpRes.Close(); + + if (enableLog) + { + var statusCode = httpRes.StatusCode; + + var duration = DateTime.Now - date; + + LoggerUtils.LogResponse(_logger, statusCode, urlToLog, remoteIp, duration); + } + } + } + + private void Write(IResponse response, string text) + { + var bOutput = Encoding.UTF8.GetBytes(text); + response.SetContentLength(bOutput.Length); + + var outputStream = response.OutputStream; + outputStream.Write(bOutput, 0, bOutput.Length); + } + + public static void RedirectToUrl(IResponse httpRes, string url) + { + httpRes.StatusCode = 302; + httpRes.AddHeader("Location", url); + } + + + /// + /// Adds the rest handlers. + /// + /// The services. + public void Init(IEnumerable services) + { + _restServices.AddRange(services); + + ServiceController = CreateServiceController(); + + _logger.Info("Calling ServiceStack AppHost.Init"); + + base.Init(); + } + + public override RouteAttribute[] GetRouteAttributes(Type requestType) + { + var routes = base.GetRouteAttributes(requestType).ToList(); + var clone = routes.ToList(); + + foreach (var route in clone) + { + routes.Add(new RouteAttribute(NormalizeEmbyRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + + routes.Add(new RouteAttribute(NormalizeRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + + routes.Add(new RouteAttribute(DoubleNormalizeEmbyRoutePath(route.Path), route.Verbs) + { + Notes = route.Notes, + Priority = route.Priority, + Summary = route.Summary + }); + } + + return routes.ToArray(); + } + + public override object GetTaskResult(Task task, string requestName) + { + try + { + var taskObject = task as Task; + if (taskObject != null) + { + return taskObject.Result; + } + + task.Wait(); + + var type = task.GetType().GetTypeInfo(); + if (!type.IsGenericType) + { + return null; + } + + Logger.Warn("Getting task result from " + requestName + " using reflection. For better performance have your api return Task"); + return type.GetDeclaredProperty("Result").GetValue(task); + } + catch (TypeAccessException) + { + return null; //return null for void Task's + } + } + + public override Func GetParseFn(Type propertyType) + { + return _funcParseFn(propertyType); + } + + public override void SerializeToJson(object o, Stream stream) + { + _jsonSerializer.SerializeToStream(o, stream); + } + + public override void SerializeToXml(object o, Stream stream) + { + _xmlSerializer.SerializeToStream(o, stream); + } + + public override object DeserializeXml(Type type, Stream stream) + { + return _xmlSerializer.DeserializeFromStream(type, stream); + } + + public override object DeserializeJson(Type type, Stream stream) + { + return _jsonSerializer.DeserializeFromStream(stream, type); + } + + private string NormalizeEmbyRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/emby" + path; + } + + return "emby/" + path; + } + + private string DoubleNormalizeEmbyRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/emby/emby" + path; + } + + return "emby/emby/" + path; + } + + private string NormalizeRoutePath(string path) + { + if (path.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return "/mediabrowser" + path; + } + + return "mediabrowser/" + path; + } + + private bool _disposed; + private readonly object _disposeLock = new object(); + protected virtual void Dispose(bool disposing) + { + if (_disposed) return; + base.Dispose(); + + lock (_disposeLock) + { + if (_disposed) return; + + if (disposing) + { + Stop(); + } + + //release unmanaged resources here... + _disposed = true; + } + } + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public void StartServer(IEnumerable urlPrefixes) + { + UrlPrefixes = urlPrefixes.ToList(); + Start(UrlPrefixes.First()); + } + } +} \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs similarity index 72% rename from MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs rename to Emby.Server.Implementations/HttpServer/HttpResultFactory.cs index 10d6f74938..995dc7b7b0 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/HttpResultFactory.cs +++ b/Emby.Server.Implementations/HttpServer/HttpResultFactory.cs @@ -2,19 +2,26 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; -using ServiceStack; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Net; +using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; -using CommonIO; +using System.Xml; +using Emby.Server.Implementations.HttpServer; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; +using ServiceStack; +using ServiceStack.Host; +using IRequest = MediaBrowser.Model.Services.IRequest; using MimeTypes = MediaBrowser.Model.Net.MimeTypes; +using StreamWriter = Emby.Server.Implementations.HttpServer.StreamWriter; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { /// /// Class HttpResultFactory @@ -27,17 +34,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer private readonly ILogger _logger; private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; + private readonly IMemoryStreamFactory _memoryStreamFactory; /// /// Initializes a new instance of the class. /// - /// The log manager. - /// The file system. - /// The json serializer. - public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer) + public HttpResultFactory(ILogManager logManager, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IMemoryStreamFactory memoryStreamFactory) { _fileSystem = fileSystem; _jsonSerializer = jsonSerializer; + _memoryStreamFactory = memoryStreamFactory; _logger = logManager.GetLogger("HttpResultFactory"); } @@ -50,19 +56,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// System.Object. public object GetResult(object content, string contentType, IDictionary responseHeaders = null) { - return GetHttpResult(content, contentType, responseHeaders); + return GetHttpResult(content, contentType, true, responseHeaders); } /// /// Gets the HTTP result. /// - /// The content. - /// Type of the content. - /// The response headers. - /// IHasOptions. - private IHasOptions GetHttpResult(object content, string contentType, IDictionary responseHeaders = null) + private IHasHeaders GetHttpResult(object content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null) { - IHasOptions result; + IHasHeaders result; var stream = content as Stream; @@ -89,7 +91,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } else { - result = new HttpResult(content, contentType); + result = new HttpResult(content, contentType, HttpStatusCode.OK); } } } @@ -98,7 +100,12 @@ namespace MediaBrowser.Server.Implementations.HttpServer responseHeaders = new Dictionary(); } - responseHeaders["Expires"] = "-1"; + string expires; + if (addCachePrevention && !responseHeaders.TryGetValue("Expires", out expires)) + { + responseHeaders["Expires"] = "-1"; + } + AddResponseHeaders(result, responseHeaders); return result; @@ -127,7 +134,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer throw new ArgumentNullException("result"); } - var optimizedResult = requestContext.ToOptimizedResult(result); + var optimizedResult = ToOptimizedResult(requestContext, result); if (responseHeaders == null) { @@ -140,16 +147,116 @@ namespace MediaBrowser.Server.Implementations.HttpServer } // Apply headers - var hasOptions = optimizedResult as IHasOptions; + var hasHeaders = optimizedResult as IHasHeaders; - if (hasOptions != null) + if (hasHeaders != null) { - AddResponseHeaders(hasOptions, responseHeaders); + AddResponseHeaders(hasHeaders, responseHeaders); } return optimizedResult; } - + + public static string GetCompressionType(IRequest request) + { + var acceptEncoding = request.Headers["Accept-Encoding"]; + + if (!string.IsNullOrWhiteSpace(acceptEncoding)) + { + if (acceptEncoding.Contains("deflate")) + return "deflate"; + + if (acceptEncoding.Contains("gzip")) + return "gzip"; + } + + return null; + } + + /// + /// Returns the optimized result for the IRequestContext. + /// Does not use or store results in any cache. + /// + /// + /// + /// + public object ToOptimizedResult(IRequest request, T dto) + { + var compressionType = GetCompressionType(request); + if (compressionType == null) + { + var contentType = request.ResponseContentType; + + switch (GetRealContentType(contentType)) + { + case "application/xml": + case "text/xml": + case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml + return SerializeToXmlString(dto); + + case "application/json": + case "text/json": + return _jsonSerializer.SerializeToString(dto); + } + } + + // Do not use the memoryStreamFactory here, they don't place nice with compression + using (var ms = new MemoryStream()) + { + using (var compressionStream = GetCompressionStream(ms, compressionType)) + { + ContentTypes.Instance.SerializeToStream(request, dto, compressionStream); + compressionStream.Dispose(); + + var compressedBytes = ms.ToArray(); + + var httpResult = new StreamWriter(compressedBytes, request.ResponseContentType, _logger); + + //httpResult.Headers["Content-Length"] = compressedBytes.Length.ToString(UsCulture); + httpResult.Headers["Content-Encoding"] = compressionType; + + return httpResult; + } + } + } + + private static Stream GetCompressionStream(Stream outputStream, string compressionType) + { + if (compressionType == "deflate") + return new DeflateStream(outputStream, CompressionMode.Compress, true); + if (compressionType == "gzip") + return new GZipStream(outputStream, CompressionMode.Compress, true); + + throw new NotSupportedException(compressionType); + } + + public static string GetRealContentType(string contentType) + { + return contentType == null + ? null + : contentType.Split(';')[0].ToLower().Trim(); + } + + private string SerializeToXmlString(object from) + { + using (var ms = new MemoryStream()) + { + var xwSettings = new XmlWriterSettings(); + xwSettings.Encoding = new UTF8Encoding(false); + xwSettings.OmitXmlDeclaration = false; + + using (var xw = XmlWriter.Create(ms, xwSettings)) + { + var serializer = new DataContractSerializer(from.GetType()); + serializer.WriteObject(xw, from); + xw.Flush(); + ms.Seek(0, SeekOrigin.Begin); + var reader = new StreamReader(ms); + return reader.ReadToEnd(); + } + } + } + /// /// Gets the optimized result using cache. /// @@ -237,31 +344,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer result = factoryFn(); // Apply caching headers - var hasOptions = result as IHasOptions; + var hasHeaders = result as IHasHeaders; - if (hasOptions != null) + if (hasHeaders != null) { - AddResponseHeaders(hasOptions, responseHeaders); - return hasOptions; + AddResponseHeaders(hasHeaders, responseHeaders); + return hasHeaders; } - IHasOptions httpResult; - - var stream = result as Stream; - - if (stream != null) - { - httpResult = new StreamWriter(stream, contentType, _logger); - } - else - { - // Otherwise wrap into an HttpResult - httpResult = new HttpResult(result, contentType ?? "text/html", HttpStatusCode.NotModified); - } - - AddResponseHeaders(httpResult, responseHeaders); - - return httpResult; + return GetHttpResult(result, contentType, false, responseHeaders); } /// @@ -298,7 +389,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer public Task GetStaticFileResult(IRequest requestContext, string path, - FileShare fileShare = FileShare.Read) + FileShareMode fileShare = FileShareMode.Read) { if (string.IsNullOrEmpty(path)) { @@ -323,7 +414,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer throw new ArgumentNullException("path"); } - if (fileShare != FileShare.Read && fileShare != FileShare.ReadWrite) + if (fileShare != FileShareMode.Read && fileShare != FileShareMode.ReadWrite) { throw new ArgumentException("FileShare must be either Read or ReadWrite"); } @@ -352,9 +443,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// The path. /// The file share. /// Stream. - private Stream GetFileStream(string path, FileShare fileShare) + private Stream GetFileStream(string path, FileShareMode fileShare) { - return _fileSystem.GetFileStream(path, FileMode.Open, FileAccess.Read, fileShare); + return _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShare); } public Task GetStaticResult(IRequest requestContext, @@ -404,10 +495,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer } var compress = ShouldCompressResponse(requestContext, contentType); - var hasOptions = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); - AddResponseHeaders(hasOptions, options.ResponseHeaders); + var hasHeaders = await GetStaticResult(requestContext, options, compress).ConfigureAwait(false); + AddResponseHeaders(hasHeaders, options.ResponseHeaders); - return hasOptions; + return hasHeaders; } /// @@ -419,7 +510,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer private bool ShouldCompressResponse(IRequest requestContext, string contentType) { // It will take some work to support compression with byte range requests - if (!string.IsNullOrEmpty(requestContext.GetHeader("Range"))) + if (!string.IsNullOrEmpty(requestContext.Headers.Get("Range"))) { return false; } @@ -461,18 +552,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - private async Task GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress) + private async Task GetStaticResult(IRequest requestContext, StaticResultOptions options, bool compress) { var isHeadRequest = options.IsHeadRequest; var factoryFn = options.ContentFactory; var contentType = options.ContentType; var responseHeaders = options.ResponseHeaders; - var requestedCompressionType = requestContext.GetCompressionType(); + var requestedCompressionType = GetCompressionType(requestContext); if (!compress || string.IsNullOrEmpty(requestedCompressionType)) { - var rangeHeader = requestContext.GetHeader("Range"); + var rangeHeader = requestContext.Headers.Get("Range"); var stream = await factoryFn().ConfigureAwait(false); @@ -490,7 +581,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer { stream.Dispose(); - return GetHttpResult(new byte[] { }, contentType); + return GetHttpResult(new byte[] { }, contentType, true); } return new StreamWriter(stream, contentType, _logger) @@ -510,16 +601,64 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - var contents = content.Compress(requestedCompressionType); + var contents = Compress(content, requestedCompressionType); responseHeaders["Content-Length"] = contents.Length.ToString(UsCulture); + responseHeaders["Content-Encoding"] = requestedCompressionType; if (isHeadRequest) { - return GetHttpResult(new byte[] { }, contentType); + return GetHttpResult(new byte[] { }, contentType, true); } - return new CompressedResult(contents, requestedCompressionType, contentType); + return GetHttpResult(contents, contentType, true, responseHeaders); + } + + private byte[] Compress(string text, string compressionType) + { + if (compressionType == "deflate") + return Deflate(text); + + if (compressionType == "gzip") + return GZip(text); + + throw new NotSupportedException(compressionType); + } + + private byte[] Deflate(string text) + { + return Deflate(Encoding.UTF8.GetBytes(text)); + } + + private byte[] Deflate(byte[] bytes) + { + // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream + // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream + using (var ms = new MemoryStream()) + using (var zipStream = new DeflateStream(ms, CompressionMode.Compress)) + { + zipStream.Write(bytes, 0, bytes.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } + } + + private byte[] GZip(string text) + { + return GZip(Encoding.UTF8.GetBytes(text)); + } + + private byte[] GZip(byte[] buffer) + { + using (var ms = new MemoryStream()) + using (var zipStream = new GZipStream(ms, CompressionMode.Compress)) + { + zipStream.Write(buffer, 0, buffer.Length); + zipStream.Dispose(); + + return ms.ToArray(); + } } /// @@ -596,9 +735,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// true if [is not modified] [the specified cache key]; otherwise, false. private bool IsNotModified(IRequest requestContext, Guid? cacheKey, DateTime? lastDateModified, TimeSpan? cacheDuration) { - var isNotModified = true; + //var isNotModified = true; - var ifModifiedSinceHeader = requestContext.GetHeader("If-Modified-Since"); + var ifModifiedSinceHeader = requestContext.Headers.Get("If-Modified-Since"); if (!string.IsNullOrEmpty(ifModifiedSinceHeader)) { @@ -606,18 +745,23 @@ namespace MediaBrowser.Server.Implementations.HttpServer if (DateTime.TryParse(ifModifiedSinceHeader, out ifModifiedSince)) { - isNotModified = IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified); + if (IsNotModified(ifModifiedSince.ToUniversalTime(), cacheDuration, lastDateModified)) + { + return true; + } } } - var ifNoneMatchHeader = requestContext.GetHeader("If-None-Match"); + var ifNoneMatchHeader = requestContext.Headers.Get("If-None-Match"); // Validate If-None-Match - if (isNotModified && (cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) + if ((cacheKey.HasValue || !string.IsNullOrEmpty(ifNoneMatchHeader))) { Guid ifNoneMatch; - if (Guid.TryParse(ifNoneMatchHeader ?? string.Empty, out ifNoneMatch)) + ifNoneMatchHeader = (ifNoneMatchHeader ?? string.Empty).Trim('\"'); + + if (Guid.TryParse(ifNoneMatchHeader, out ifNoneMatch)) { if (cacheKey.HasValue && cacheKey.Value == ifNoneMatch) { @@ -673,19 +817,14 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// /// Adds the response headers. /// - /// The has options. + /// The has options. /// The response headers. - private void AddResponseHeaders(IHasOptions hasOptions, IEnumerable> responseHeaders) + private void AddResponseHeaders(IHasHeaders hasHeaders, IEnumerable> responseHeaders) { foreach (var item in responseHeaders) { - hasOptions.Options[item.Key] = item.Value; + hasHeaders.Headers[item.Key] = item.Value; } } - - public object GetAsyncStreamWriter(IAsyncStreamSource streamSource) - { - return new AsyncStreamWriter(streamSource); - } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs b/Emby.Server.Implementations/HttpServer/IHttpListener.cs similarity index 94% rename from MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs rename to Emby.Server.Implementations/HttpServer/IHttpListener.cs index dc315601f2..9f96a8e49d 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/IHttpListener.cs +++ b/Emby.Server.Implementations/HttpServer/IHttpListener.cs @@ -1,10 +1,10 @@ using MediaBrowser.Controller.Net; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.Threading.Tasks; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { public interface IHttpListener : IDisposable { diff --git a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs similarity index 96% rename from MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs rename to Emby.Server.Implementations/HttpServer/LoggerUtils.cs index bfbb228edf..8fc92a09a7 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/LoggerUtils.cs +++ b/Emby.Server.Implementations/HttpServer/LoggerUtils.cs @@ -3,7 +3,7 @@ using System; using System.Globalization; using SocketHttpListener.Net; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { public static class LoggerUtils { diff --git a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs similarity index 72% rename from MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs rename to Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs index 4b94095f52..e88994bec0 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/RangeRequestWriter.cs +++ b/Emby.Server.Implementations/HttpServer/RangeRequestWriter.cs @@ -1,16 +1,16 @@ using MediaBrowser.Model.Logging; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Threading; using System.Threading.Tasks; -using ServiceStack; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { - public class RangeRequestWriter : IStreamWriter, IAsyncStreamWriter, IHttpResult + public class RangeRequestWriter : IAsyncStreamWriter, IHttpResult { /// /// Gets or sets the source stream. @@ -40,27 +40,17 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// private static readonly CultureInfo UsCulture = new CultureInfo("en-US"); - public Func ResultScope { get; set; } public List Cookies { get; private set; } /// /// Additional HTTP Headers /// /// The headers. - public Dictionary Headers + public IDictionary Headers { get { return _options; } } - /// - /// Gets the options. - /// - /// The options. - public IDictionary Options - { - get { return Headers; } - } - /// /// Initializes a new instance of the class. /// @@ -81,8 +71,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer this._logger = logger; ContentType = contentType; - Options["Content-Type"] = contentType; - Options["Accept-Ranges"] = "bytes"; + Headers["Content-Type"] = contentType; + Headers["Accept-Ranges"] = "bytes"; StatusCode = HttpStatusCode.PartialContent; Cookies = new List(); @@ -112,8 +102,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer RangeLength = 1 + RangeEnd - RangeStart; // Content-Length is the length of what we're serving, not the original content - Options["Content-Length"] = RangeLength.ToString(UsCulture); - Options["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); + Headers["Content-Length"] = RangeLength.ToString(UsCulture); + Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", RangeStart, RangeEnd, TotalContentLength); if (RangeStart > 0) { @@ -164,62 +154,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - /// - /// Writes to. - /// - /// The response stream. - public void WriteTo(Stream responseStream) - { - try - { - // Headers only - if (IsHeadRequest) - { - return; - } - - using (var source = SourceStream) - { - // If the requested range is "0-", we can optimize by just doing a stream copy - if (RangeEnd >= TotalContentLength - 1) - { - source.CopyTo(responseStream, BufferSize); - } - else - { - CopyToInternal(source, responseStream, RangeLength); - } - } - } - finally - { - if (OnComplete != null) - { - OnComplete(); - } - } - } - - private void CopyToInternal(Stream source, Stream destination, long copyLength) - { - var array = new byte[BufferSize]; - int count; - while ((count = source.Read(array, 0, array.Length)) != 0) - { - var bytesToCopy = Math.Min(count, copyLength); - - destination.Write(array, 0, Convert.ToInt32(bytesToCopy)); - - copyLength -= bytesToCopy; - - if (copyLength <= 0) - { - break; - } - } - } - - public async Task WriteToAsync(Stream responseStream) + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { try { @@ -276,8 +211,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer public object Response { get; set; } - public IContentTypeWriter ResponseFilter { get; set; } - public int Status { get; set; } public HttpStatusCode StatusCode @@ -287,7 +220,5 @@ namespace MediaBrowser.Server.Implementations.HttpServer } public string StatusDescription { get; set; } - - public int PaddingLength { get; set; } } } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs similarity index 68% rename from MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs rename to Emby.Server.Implementations/HttpServer/ResponseFilter.cs index ee05702f4c..6d9d7d921d 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/ResponseFilter.cs +++ b/Emby.Server.Implementations/HttpServer/ResponseFilter.cs @@ -1,12 +1,11 @@ using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Implementations.HttpServer.SocketSharp; -using ServiceStack.Web; using System; using System.Globalization; -using System.Net; using System.Text; +using Emby.Server.Implementations.HttpServer.SocketSharp; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { public class ResponseFilter { @@ -28,6 +27,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer { // Try to prevent compatibility view res.AddHeader("X-UA-Compatible", "IE=Edge"); + res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Range, X-MediaBrowser-Token, X-Emby-Authorization"); + res.AddHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS"); + res.AddHeader("Access-Control-Allow-Origin", "*"); var exception = dto as Exception; @@ -46,21 +48,21 @@ namespace MediaBrowser.Server.Implementations.HttpServer var vary = "Accept-Encoding"; - var hasOptions = dto as IHasOptions; + var hasHeaders = dto as IHasHeaders; var sharpResponse = res as WebSocketSharpResponse; - if (hasOptions != null) + if (hasHeaders != null) { - if (!hasOptions.Options.ContainsKey("Server")) + if (!hasHeaders.Headers.ContainsKey("Server")) { - hasOptions.Options["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50"; - //hasOptions.Options["Server"] = "Mono-HTTPAPI/1.1"; + hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1, UPnP/1.0 DLNADOC/1.50"; + //hasHeaders.Headers["Server"] = "Mono-HTTPAPI/1.1"; } // Content length has to be explicitly set on on HttpListenerResponse or it won't be happy string contentLength; - if (hasOptions.Options.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength)) + if (hasHeaders.Headers.TryGetValue("Content-Length", out contentLength) && !string.IsNullOrEmpty(contentLength)) { var length = long.Parse(contentLength, UsCulture); @@ -68,15 +70,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer { res.SetContentLength(length); - var listenerResponse = res.OriginalResponse as HttpListenerResponse; + //var listenerResponse = res.OriginalResponse as HttpListenerResponse; - if (listenerResponse != null) - { - // Disable chunked encoding. Technically this is only needed when using Content-Range, but - // anytime we know the content length there's no need for it - listenerResponse.SendChunked = false; - return; - } + //if (listenerResponse != null) + //{ + // // Disable chunked encoding. Technically this is only needed when using Content-Range, but + // // anytime we know the content length there's no need for it + // listenerResponse.SendChunked = false; + // return; + //} if (sharpResponse != null) { @@ -85,13 +87,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer } } - string hasOptionsVary; - if (hasOptions.Options.TryGetValue("Vary", out hasOptionsVary)) + string hasHeadersVary; + if (hasHeaders.Headers.TryGetValue("Vary", out hasHeadersVary)) { - vary = hasOptionsVary; + vary = hasHeadersVary; } - hasOptions.Options["Vary"] = vary; + hasHeaders.Headers["Vary"] = vary; } //res.KeepAlive = false; diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs similarity index 99% rename from MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs rename to Emby.Server.Implementations/HttpServer/Security/AuthService.cs index d8f7d889c3..4d00c9b195 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -10,7 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; -namespace MediaBrowser.Server.Implementations.HttpServer.Security +namespace Emby.Server.Implementations.HttpServer.Security { public class AuthService : IAuthService { diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs similarity index 97% rename from MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs rename to Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs index bc3e7b163b..ec3dfeb609 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/AuthorizationContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthorizationContext.cs @@ -1,12 +1,12 @@ using MediaBrowser.Controller.Connect; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.Linq; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer.Security +namespace Emby.Server.Implementations.HttpServer.Security { public class AuthorizationContext : IAuthorizationContext { @@ -21,7 +21,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security public AuthorizationInfo GetAuthorizationInfo(object requestContext) { - var req = new ServiceStackServiceRequest((IRequest)requestContext); + var req = new ServiceRequest((IRequest)requestContext); return GetAuthorizationInfo(req); } diff --git a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs similarity index 90% rename from MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs rename to Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index a498d32fac..33dd4e2d77 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -3,10 +3,10 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; -using ServiceStack.Web; using System.Threading.Tasks; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer.Security +namespace Emby.Server.Implementations.HttpServer.Security { public class SessionContext : ISessionContext { @@ -47,7 +47,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security public Task GetSession(object requestContext) { - var req = new ServiceStackServiceRequest((IRequest)requestContext); + var req = new ServiceRequest((IRequest)requestContext); return GetSession(req); } @@ -60,7 +60,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.Security public Task GetUser(object requestContext) { - var req = new ServiceStackServiceRequest((IRequest)requestContext); + var req = new ServiceRequest((IRequest)requestContext); return GetUser(req); } } diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs new file mode 100644 index 0000000000..07a338f198 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/Extensions.cs @@ -0,0 +1,12 @@ +using SocketHttpListener.Net; + +namespace Emby.Server.Implementations.HttpServer.SocketSharp +{ + public static class Extensions + { + public static string GetOperationName(this HttpListenerRequest request) + { + return request.Url.Segments[request.Url.Segments.Length - 1]; + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs similarity index 97% rename from MediaBrowser.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs rename to Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs index 3ef48d13a0..4fbe0ed946 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/HttpUtility.cs @@ -1,32 +1,13 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Specialized; using System.Text; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +namespace Emby.Server.Implementations.HttpServer.SocketSharp { public static class MyHttpUtility { - sealed class HttpQSCollection : NameValueCollection - { - public override string ToString() - { - int count = Count; - if (count == 0) - return ""; - StringBuilder sb = new StringBuilder(); - string[] keys = AllKeys; - for (int i = 0; i < count; i++) - { - sb.AppendFormat("{0}={1}&", keys[i], this[keys[i]]); - } - if (sb.Length > 0) - sb.Length--; - return sb.ToString(); - } - } - // Must be sorted static readonly long[] entities = new long[] { (long)'A' << 56 | (long)'E' << 48 | (long)'l' << 40 | (long)'i' << 32 | (long)'g' << 24, @@ -606,7 +587,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp byte[] buf = bytes.ToArray(); bytes = null; - return e.GetString(buf); + return e.GetString(buf, 0, buf.Length); } @@ -857,28 +838,28 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return output.ToString(); } - public static NameValueCollection ParseQueryString(string query) + public static QueryParamCollection ParseQueryString(string query) { return ParseQueryString(query, Encoding.UTF8); } - public static NameValueCollection ParseQueryString(string query, Encoding encoding) + public static QueryParamCollection ParseQueryString(string query, Encoding encoding) { if (query == null) throw new ArgumentNullException("query"); if (encoding == null) throw new ArgumentNullException("encoding"); if (query.Length == 0 || (query.Length == 1 && query[0] == '?')) - return new NameValueCollection(); + return new QueryParamCollection(); if (query[0] == '?') query = query.Substring(1); - NameValueCollection result = new HttpQSCollection(); + QueryParamCollection result = new QueryParamCollection(); ParseQueryString(query, encoding, result); return result; } - internal static void ParseQueryString(string query, Encoding encoding, NameValueCollection result) + internal static void ParseQueryString(string query, Encoding encoding, QueryParamCollection result) { if (query.Length == 0) return; diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs similarity index 87% rename from MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs rename to Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs index d20dd7ec08..ec14c32c8f 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/RequestMono.cs @@ -1,14 +1,14 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; +using System.Net; using System.Text; using System.Threading.Tasks; -using System.Web; -using ServiceStack; -using ServiceStack.Web; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +namespace Emby.Server.Implementations.HttpServer.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { @@ -69,7 +69,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp input.Position = e.Start; input.Read(copy, 0, (int)e.Length); - form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy)); + form.Add(e.Name, (e.Encoding ?? ContentEncoding).GetString(copy, 0, copy.Length)); } else { @@ -77,20 +77,20 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp // We use a substream, as in 2.x we will support large uploads streamed to disk, // HttpPostedFile sub = new HttpPostedFile(e.Filename, e.ContentType, input, e.Start, e.Length); - files.AddFile(e.Name, sub); + files[e.Name] = sub; } } } } - public NameValueCollection Form + public QueryParamCollection Form { get { if (form == null) { form = new WebROCollection(); - files = new HttpFileCollection(); + files = new Dictionary(); if (IsContentType("multipart/form-data", true)) { @@ -128,7 +128,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return string.IsNullOrEmpty(request.Headers[HttpHeaders.Accept]) ? null : request.Headers[HttpHeaders.Accept]; + return string.IsNullOrEmpty(request.Headers["Accept"]) ? null : request.Headers["Accept"]; } } @@ -136,7 +136,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return string.IsNullOrEmpty(request.Headers[HttpHeaders.Authorization]) ? null : request.Headers[HttpHeaders.Authorization]; + return string.IsNullOrEmpty(request.Headers["Authorization"]) ? null : request.Headers["Authorization"]; } } @@ -152,17 +152,18 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp string msg = String.Format("A potentially dangerous Request.{0} value was " + "detected from the client ({1}={2}).", name, key, v); - throw new HttpRequestValidationException(msg); + throw new Exception(msg); } - static void ValidateNameValueCollection(string name, NameValueCollection coll) + static void ValidateNameValueCollection(string name, QueryParamCollection coll) { if (coll == null) return; - foreach (string key in coll.Keys) + foreach (var pair in coll) { - string val = coll[key]; + var key = pair.Name; + var val = pair.Value; if (val != null && val.Length > 0 && IsInvalidString(val)) ThrowValidationException(name, key, val); } @@ -224,7 +225,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp if (starts_with) return StrUtils.StartsWith(ContentType, ct, true); - return String.Compare(ContentType, ct, true, Helpers.InvariantCulture) == 0; + return string.Equals(ContentType, ct, StringComparison.OrdinalIgnoreCase); } async Task LoadWwwForm() @@ -277,9 +278,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp void AddRawKeyValue(StringBuilder key, StringBuilder value) { - string decodedKey = HttpUtility.UrlDecode(key.ToString(), ContentEncoding); + string decodedKey = WebUtility.UrlDecode(key.ToString()); form.Add(decodedKey, - HttpUtility.UrlDecode(value.ToString(), ContentEncoding)); + WebUtility.UrlDecode(value.ToString())); key.Length = 0; value.Length = 0; @@ -287,68 +288,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp WebROCollection form; - HttpFileCollection files; + Dictionary files; - public sealed class HttpFileCollection : NameObjectCollectionBase - { - internal HttpFileCollection() - { - } - - internal void AddFile(string name, HttpPostedFile file) - { - BaseAdd(name, file); - } - - public void CopyTo(Array dest, int index) - { - /* XXX this is kind of gross and inefficient - * since it makes a copy of the superclass's - * list */ - object[] values = BaseGetAllValues(); - values.CopyTo(dest, index); - } - - public string GetKey(int index) - { - return BaseGetKey(index); - } - - public HttpPostedFile Get(int index) - { - return (HttpPostedFile)BaseGet(index); - } - - public HttpPostedFile Get(string key) - { - return (HttpPostedFile)BaseGet(key); - } - - public HttpPostedFile this[string key] - { - get - { - return Get(key); - } - } - - public HttpPostedFile this[int index] - { - get - { - return Get(index); - } - } - - public string[] AllKeys - { - get - { - return BaseGetAllKeys(); - } - } - } - class WebROCollection : NameValueCollection + class WebROCollection : QueryParamCollection { bool got_id; int id; @@ -369,28 +311,29 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } public void Protect() { - IsReadOnly = true; + //IsReadOnly = true; } public void Unprotect() { - IsReadOnly = false; + //IsReadOnly = false; } public override string ToString() { StringBuilder result = new StringBuilder(); - foreach (string key in AllKeys) + foreach (var pair in this) { if (result.Length > 0) result.Append('&'); + var key = pair.Name; if (key != null && key.Length > 0) { result.Append(key); result.Append('='); } - result.Append(Get(key)); + result.Append(pair.Value); } return result.ToString(); @@ -588,29 +531,15 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp internal sealed class StrUtils { - StrUtils() { } - - public static bool StartsWith(string str1, string str2) - { - return StartsWith(str1, str2, false); - } - public static bool StartsWith(string str1, string str2, bool ignore_case) { - int l2 = str2.Length; - if (l2 == 0) - return true; - - int l1 = str1.Length; - if (l2 > l1) + if (string.IsNullOrWhiteSpace(str1)) + { return false; + } - return 0 == String.Compare(str1, 0, str2, 0, l2, ignore_case, Helpers.InvariantCulture); - } - - public static bool EndsWith(string str1, string str2) - { - return EndsWith(str1, str2, false); + var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return str1.IndexOf(str2, comparison) == 0; } public static bool EndsWith(string str1, string str2, bool ignore_case) @@ -623,7 +552,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp if (l2 > l1) return false; - return 0 == String.Compare(str1, l1 - l2, str2, 0, l2, ignore_case, Helpers.InvariantCulture); + var comparison = ignore_case ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return str1.IndexOf(str2, comparison) == str1.Length - str2.Length - 1; } } @@ -741,7 +671,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp for (int i = temp.Length - 1; i >= 0; i--) source[i] = (byte)temp[i]; - return encoding.GetString(source); + return encoding.GetString(source, 0, source.Length); } bool ReadBoundary() diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs similarity index 86% rename from MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs rename to Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs index d363c4de6c..9823a2ff50 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/SharpWebSocket.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; using WebSocketState = MediaBrowser.Model.Net.WebSocketState; -namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +namespace Emby.Server.Implementations.HttpServer.SocketSharp { public class SharpWebSocket : IWebSocket { @@ -25,12 +25,6 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - /// - /// Initializes a new instance of the class. - /// - /// The socket. - /// The logger. - /// socket public SharpWebSocket(SocketHttpListener.WebSocket socket, ILogger logger) { if (socket == null) @@ -108,11 +102,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp /// Task. public Task SendAsync(byte[] bytes, bool endOfMessage, CancellationToken cancellationToken) { - var completionSource = new TaskCompletionSource(); - - WebSocket.SendAsync(bytes, res => completionSource.TrySetResult(true)); - - return completionSource.Task; + return WebSocket.SendAsync(bytes); } /// @@ -124,11 +114,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp /// Task. public Task SendAsync(string text, bool endOfMessage, CancellationToken cancellationToken) { - var completionSource = new TaskCompletionSource(); - - WebSocket.SendAsync(text, res => completionSource.TrySetResult(true)); - - return completionSource.Task; + return WebSocket.SendAsync(text); } /// diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs similarity index 60% rename from MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs rename to Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs index b090c97c6e..4606d0e316 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpListener.cs @@ -1,35 +1,50 @@ -using System.Collections.Specialized; -using MediaBrowser.Controller.Net; +using MediaBrowser.Controller.Net; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Implementations.Logging; -using ServiceStack; -using ServiceStack.Web; using SocketHttpListener.Net; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using MediaBrowser.Common.IO; +using MediaBrowser.Common.Net; +using MediaBrowser.Model.Cryptography; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Net; +using MediaBrowser.Model.Services; +using MediaBrowser.Model.Text; +using SocketHttpListener.Primitives; -namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +namespace Emby.Server.Implementations.HttpServer.SocketSharp { public class WebSocketSharpListener : IHttpListener { private HttpListener _listener; private readonly ILogger _logger; - private readonly string _certificatePath; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly ICertificate _certificate; + private readonly IMemoryStreamFactory _memoryStreamProvider; + private readonly ITextEncoding _textEncoding; + private readonly INetworkManager _networkManager; + private readonly ISocketFactory _socketFactory; + private readonly ICryptoProvider _cryptoProvider; + private readonly IStreamFactory _streamFactory; + private readonly Func _httpRequestFactory; + private readonly bool _enableDualMode; - public WebSocketSharpListener(ILogger logger, string certificatePath, IMemoryStreamProvider memoryStreamProvider) + public WebSocketSharpListener(ILogger logger, ICertificate certificate, IMemoryStreamFactory memoryStreamProvider, ITextEncoding textEncoding, INetworkManager networkManager, ISocketFactory socketFactory, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, bool enableDualMode, Func httpRequestFactory) { _logger = logger; - _certificatePath = certificatePath; + _certificate = certificate; _memoryStreamProvider = memoryStreamProvider; + _textEncoding = textEncoding; + _networkManager = networkManager; + _socketFactory = socketFactory; + _cryptoProvider = cryptoProvider; + _streamFactory = streamFactory; + _enableDualMode = enableDualMode; + _httpRequestFactory = httpRequestFactory; } public Action ErrorHandler { get; set; } - public Func RequestHandler { get; set; } public Action WebSocketConnecting { get; set; } @@ -39,7 +54,14 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public void Start(IEnumerable urlPrefixes) { if (_listener == null) - _listener = new HttpListener(new PatternsLogger(_logger), _certificatePath); + _listener = new HttpListener(_logger, _cryptoProvider, _streamFactory, _socketFactory, _networkManager, _textEncoding, _memoryStreamProvider); + + _listener.EnableDualMode = _enableDualMode; + + if (_certificate != null) + { + _listener.LoadCert(_certificate); + } foreach (var prefix in urlPrefixes) { @@ -54,44 +76,36 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp private void ProcessContext(HttpListenerContext context) { - Task.Factory.StartNew(() => InitTask(context)); + //Task.Factory.StartNew(() => InitTask(context), TaskCreationOptions.DenyChildAttach | TaskCreationOptions.PreferFairness); + Task.Run(() => InitTask(context)); } - private void InitTask(HttpListenerContext context) + private Task InitTask(HttpListenerContext context) { + IHttpRequest httpReq = null; + var request = context.Request; + try { - var task = this.ProcessRequestAsync(context); - task.ContinueWith(x => HandleError(x.Exception, context), TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent); + if (request.IsWebSocketRequest) + { + LoggerUtils.LogRequest(_logger, request); - //if (task.Status == TaskStatus.Created) - //{ - // task.RunSynchronously(); - //} + ProcessWebSocketRequest(context); + return Task.FromResult(true); + } + + httpReq = GetRequest(context); } catch (Exception ex) { - HandleError(ex, context); - } - } + _logger.ErrorException("Error processing request", ex); - private Task ProcessRequestAsync(HttpListenerContext context) - { - var request = context.Request; - - if (request.IsWebSocketRequest) - { - LoggerUtils.LogRequest(_logger, request); - - ProcessWebSocketRequest(context); + httpReq = httpReq ?? GetRequest(context); + ErrorHandler(ex, httpReq); return Task.FromResult(true); } - if (string.IsNullOrEmpty(context.Request.RawUrl)) - return ((object)null).AsTaskResult(); - - var httpReq = GetRequest(context); - return RequestHandler(httpReq, request.Url); } @@ -101,12 +115,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { var endpoint = ctx.Request.RemoteEndPoint.ToString(); var url = ctx.Request.RawUrl; - var queryString = new NameValueCollection(ctx.Request.QueryString ?? new NameValueCollection()); var connectingArgs = new WebSocketConnectingEventArgs { Url = url, - QueryString = queryString, + QueryString = ctx.Request.QueryString, Endpoint = endpoint }; @@ -126,7 +139,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp WebSocketConnected(new WebSocketConnectEventArgs { Url = url, - QueryString = queryString, + QueryString = ctx.Request.QueryString, WebSocket = new SharpWebSocket(webSocketContext.WebSocket, _logger), Endpoint = endpoint }); @@ -149,22 +162,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp private IHttpRequest GetRequest(HttpListenerContext httpContext) { - var operationName = httpContext.Request.GetOperationName(); - - var req = new WebSocketSharpRequest(httpContext, operationName, RequestAttributes.None, _logger, _memoryStreamProvider); - req.RequestAttributes = req.GetAttributes(); - - return req; - } - - private void HandleError(Exception ex, HttpListenerContext context) - { - var httpReq = GetRequest(context); - - if (ErrorHandler != null) - { - ErrorHandler(ex, httpReq); - } + return _httpRequestFactory(httpContext); } public void Stop() @@ -205,4 +203,5 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } } } + } \ No newline at end of file diff --git a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs similarity index 54% rename from MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs rename to Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs index b5c8d01075..b3fcde7456 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpRequest.cs @@ -1,33 +1,33 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Text; -using Funq; -using MediaBrowser.Common.IO; +using Emby.Server.Implementations.HttpServer; +using Emby.Server.Implementations.HttpServer.SocketSharp; +using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; -using ServiceStack; -using ServiceStack.Host; -using ServiceStack.Web; +using MediaBrowser.Model.Services; using SocketHttpListener.Net; +using IHttpFile = MediaBrowser.Model.Services.IHttpFile; +using IHttpRequest = MediaBrowser.Model.Services.IHttpRequest; +using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; +using IResponse = MediaBrowser.Model.Services.IResponse; -namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp +namespace Emby.Server.Implementations.HttpServer.SocketSharp { public partial class WebSocketSharpRequest : IHttpRequest { - public Container Container { get; set; } private readonly HttpListenerRequest request; private readonly IHttpResponse response; - private readonly IMemoryStreamProvider _memoryStreamProvider; + private readonly IMemoryStreamFactory _memoryStreamProvider; - public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, RequestAttributes requestAttributes, ILogger logger, IMemoryStreamProvider memoryStreamProvider) + public WebSocketSharpRequest(HttpListenerContext httpContext, string operationName, ILogger logger, IMemoryStreamFactory memoryStreamProvider) { this.OperationName = operationName; - this.RequestAttributes = requestAttributes; _memoryStreamProvider = memoryStreamProvider; this.request = httpContext.Request; this.response = new WebSocketSharpResponse(logger, httpContext.Response, this); - - this.RequestPreferences = new RequestPreferences(this); } public HttpListenerRequest HttpRequest @@ -50,40 +50,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp get { return response; } } - public RequestAttributes RequestAttributes { get; set; } - - public IRequestPreferences RequestPreferences { get; private set; } - - public T TryResolve() - { - if (typeof(T) == typeof(IHttpRequest)) - throw new Exception("You don't need to use IHttpRequest.TryResolve to resolve itself"); - - if (typeof(T) == typeof(IHttpResponse)) - throw new Exception("Resolve IHttpResponse with 'Response' property instead of IHttpRequest.TryResolve"); - - return Container == null - ? HostContext.TryResolve() - : Container.TryResolve(); - } - public string OperationName { get; set; } public object Dto { get; set; } - public string GetRawBody() - { - if (bufferedStream != null) - { - return bufferedStream.ToArray().FromUtf8Bytes(); - } - - using (var reader = new StreamReader(InputStream)) - { - return reader.ReadToEnd(); - } - } - public string RawUrl { get { return request.RawUrl; } @@ -103,7 +73,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return String.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedFor]) ? null : request.Headers[HttpHeaders.XForwardedFor]; + return String.IsNullOrEmpty(request.Headers["X-Forwarded-For"]) ? null : request.Headers["X-Forwarded-For"]; } } @@ -111,7 +81,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedPort]) ? (int?)null : int.Parse(request.Headers[HttpHeaders.XForwardedPort]); + return string.IsNullOrEmpty(request.Headers["X-Forwarded-Port"]) ? (int?)null : int.Parse(request.Headers["X-Forwarded-Port"]); } } @@ -119,7 +89,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return string.IsNullOrEmpty(request.Headers[HttpHeaders.XForwardedProtocol]) ? null : request.Headers[HttpHeaders.XForwardedProtocol]; + return string.IsNullOrEmpty(request.Headers["X-Forwarded-Proto"]) ? null : request.Headers["X-Forwarded-Proto"]; } } @@ -127,7 +97,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { get { - return String.IsNullOrEmpty(request.Headers[HttpHeaders.XRealIp]) ? null : request.Headers[HttpHeaders.XRealIp]; + return String.IsNullOrEmpty(request.Headers["X-Real-IP"]) ? null : request.Headers["X-Real-IP"]; } } @@ -139,7 +109,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return remoteIp ?? (remoteIp = (CheckBadChars(XForwardedFor)) ?? (NormalizeIp(CheckBadChars(XRealIp)) ?? - (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.Address.ToString()) : null))); + (request.RemoteEndPoint != null ? NormalizeIp(request.RemoteEndPoint.IpAddress.ToString()) : null))); } } @@ -258,7 +228,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp get { return responseContentType - ?? (responseContentType = this.GetResponseContentType()); + ?? (responseContentType = GetResponseContentType(this)); } set { @@ -267,8 +237,120 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } } + public const string FormUrlEncoded = "application/x-www-form-urlencoded"; + public const string MultiPartFormData = "multipart/form-data"; + private static string GetResponseContentType(IRequest httpReq) + { + var specifiedContentType = GetQueryStringContentType(httpReq); + if (!string.IsNullOrEmpty(specifiedContentType)) return specifiedContentType; + + var serverDefaultContentType = "application/json"; + + var acceptContentTypes = httpReq.AcceptTypes; + var defaultContentType = httpReq.ContentType; + if (HasAnyOfContentTypes(httpReq, FormUrlEncoded, MultiPartFormData)) + { + defaultContentType = serverDefaultContentType; + } + + var preferredContentTypes = new string[] {}; + + var acceptsAnything = false; + var hasDefaultContentType = !string.IsNullOrEmpty(defaultContentType); + if (acceptContentTypes != null) + { + var hasPreferredContentTypes = new bool[preferredContentTypes.Length]; + foreach (var acceptsType in acceptContentTypes) + { + var contentType = HttpResultFactory.GetRealContentType(acceptsType); + acceptsAnything = acceptsAnything || contentType == "*/*"; + + for (var i = 0; i < preferredContentTypes.Length; i++) + { + if (hasPreferredContentTypes[i]) continue; + var preferredContentType = preferredContentTypes[i]; + hasPreferredContentTypes[i] = contentType.StartsWith(preferredContentType); + + //Prefer Request.ContentType if it is also a preferredContentType + if (hasPreferredContentTypes[i] && preferredContentType == defaultContentType) + return preferredContentType; + } + } + + for (var i = 0; i < preferredContentTypes.Length; i++) + { + if (hasPreferredContentTypes[i]) return preferredContentTypes[i]; + } + + if (acceptsAnything) + { + if (hasDefaultContentType) + return defaultContentType; + if (serverDefaultContentType != null) + return serverDefaultContentType; + } + } + + if (acceptContentTypes == null && httpReq.ContentType == Soap11) + { + return Soap11; + } + + //We could also send a '406 Not Acceptable', but this is allowed also + return serverDefaultContentType; + } + + public const string Soap11 = "text/xml; charset=utf-8"; + + public static bool HasAnyOfContentTypes(IRequest request, params string[] contentTypes) + { + if (contentTypes == null || request.ContentType == null) return false; + foreach (var contentType in contentTypes) + { + if (IsContentType(request, contentType)) return true; + } + return false; + } + + public static bool IsContentType(IRequest request, string contentType) + { + return request.ContentType.StartsWith(contentType, StringComparison.OrdinalIgnoreCase); + } + + public const string Xml = "application/xml"; + private static string GetQueryStringContentType(IRequest httpReq) + { + var format = httpReq.QueryString["format"]; + if (format == null) + { + const int formatMaxLength = 4; + var pi = httpReq.PathInfo; + if (pi == null || pi.Length <= formatMaxLength) return null; + if (pi[0] == '/') pi = pi.Substring(1); + format = LeftPart(pi, '/'); + if (format.Length > formatMaxLength) return null; + } + + format = LeftPart(format, '.').ToLower(); + if (format.Contains("json")) return "application/json"; + if (format.Contains("xml")) return Xml; + + return null; + } + + public static string LeftPart(string strVal, char needle) + { + if (strVal == null) return null; + var pos = strVal.IndexOf(needle); + return pos == -1 + ? strVal + : strVal.Substring(0, pos); + } + public bool HasExplicitResponseContentType { get; private set; } + public static string HandlerFactoryPath; + private string pathInfo; public string PathInfo { @@ -276,13 +358,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp { if (this.pathInfo == null) { - var mode = HostContext.Config.HandlerFactoryPath; + var mode = HandlerFactoryPath; var pos = request.RawUrl.IndexOf("?"); if (pos != -1) { var path = request.RawUrl.Substring(0, pos); - this.pathInfo = HttpRequestExtensions.GetPathInfo( + this.pathInfo = GetPathInfo( path, mode, mode ?? ""); @@ -292,13 +374,62 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp this.pathInfo = request.RawUrl; } - this.pathInfo = this.pathInfo.UrlDecode(); + this.pathInfo = WebUtility.UrlDecode(pathInfo); this.pathInfo = NormalizePathInfo(pathInfo, mode); } return this.pathInfo; } } + private static string GetPathInfo(string fullPath, string mode, string appPath) + { + var pathInfo = ResolvePathInfoFromMappedPath(fullPath, mode); + if (!string.IsNullOrEmpty(pathInfo)) return pathInfo; + + //Wildcard mode relies on this to work out the handlerPath + pathInfo = ResolvePathInfoFromMappedPath(fullPath, appPath); + if (!string.IsNullOrEmpty(pathInfo)) return pathInfo; + + return fullPath; + } + + + + private static string ResolvePathInfoFromMappedPath(string fullPath, string mappedPathRoot) + { + if (mappedPathRoot == null) return null; + + var sbPathInfo = new StringBuilder(); + var fullPathParts = fullPath.Split('/'); + var mappedPathRootParts = mappedPathRoot.Split('/'); + var fullPathIndexOffset = mappedPathRootParts.Length - 1; + var pathRootFound = false; + + for (var fullPathIndex = 0; fullPathIndex < fullPathParts.Length; fullPathIndex++) + { + if (pathRootFound) + { + sbPathInfo.Append("/" + fullPathParts[fullPathIndex]); + } + else if (fullPathIndex - fullPathIndexOffset >= 0) + { + pathRootFound = true; + for (var mappedPathRootIndex = 0; mappedPathRootIndex < mappedPathRootParts.Length; mappedPathRootIndex++) + { + if (!string.Equals(fullPathParts[fullPathIndex - fullPathIndexOffset + mappedPathRootIndex], mappedPathRootParts[mappedPathRootIndex], StringComparison.OrdinalIgnoreCase)) + { + pathRootFound = false; + break; + } + } + } + } + if (!pathRootFound) return null; + + var path = sbPathInfo.ToString(); + return path.Length > 1 ? path.TrimEnd('/') : "/"; + } + private Dictionary cookies; public IDictionary Cookies { @@ -307,9 +438,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp if (cookies == null) { cookies = new Dictionary(); - for (var i = 0; i < this.request.Cookies.Count; i++) + foreach (var cookie in this.request.Cookies) { - var httpCookie = this.request.Cookies[i]; + var httpCookie = (Cookie) cookie; cookies[httpCookie.Name] = new System.Net.Cookie(httpCookie.Name, httpCookie.Value, httpCookie.Path, httpCookie.Domain); } } @@ -323,22 +454,21 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp get { return request.UserAgent; } } - private NameValueCollectionWrapper headers; - public INameValueCollection Headers + public QueryParamCollection Headers { - get { return headers ?? (headers = new NameValueCollectionWrapper(request.Headers)); } + get { return request.Headers; } } - private NameValueCollectionWrapper queryString; - public INameValueCollection QueryString + private QueryParamCollection queryString; + public QueryParamCollection QueryString { - get { return queryString ?? (queryString = new NameValueCollectionWrapper(MyHttpUtility.ParseQueryString(request.Url.Query))); } + get { return queryString ?? (queryString = MyHttpUtility.ParseQueryString(request.Url.Query)); } } - private NameValueCollectionWrapper formData; - public INameValueCollection FormData + private QueryParamCollection formData; + public QueryParamCollection FormData { - get { return formData ?? (formData = new NameValueCollectionWrapper(this.Form)); } + get { return formData ?? (formData = this.Form); } } public bool IsLocal @@ -352,8 +482,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp get { return httpMethod - ?? (httpMethod = Param(HttpHeaders.XHttpMethodOverride) - ?? request.HttpMethod); + ?? (httpMethod = request.HttpMethod); } } @@ -400,21 +529,9 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp } } - public bool UseBufferedStream - { - get { return bufferedStream != null; } - set - { - bufferedStream = value - ? bufferedStream ?? _memoryStreamProvider.CreateNew(request.InputStream.ReadFully()) - : null; - } - } - - private MemoryStream bufferedStream; public Stream InputStream { - get { return bufferedStream ?? request.InputStream; } + get { return request.InputStream; } } public long ContentLength @@ -433,10 +550,10 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return httpFiles = new IHttpFile[0]; httpFiles = new IHttpFile[files.Count]; - for (var i = 0; i < files.Count; i++) + var i = 0; + foreach (var pair in files) { - var reqFile = files[i]; - + var reqFile = pair.Value; httpFiles[i] = new HttpFile { ContentType = reqFile.ContentType, @@ -444,25 +561,25 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp FileName = reqFile.FileName, InputStream = reqFile.InputStream, }; + i++; } } return httpFiles; } } - static Stream GetSubStream(Stream stream, IMemoryStreamProvider streamProvider) + static Stream GetSubStream(Stream stream, IMemoryStreamFactory streamProvider) { if (stream is MemoryStream) { var other = (MemoryStream)stream; - try + + byte[] buffer; + if (streamProvider.TryGetBuffer(other, out buffer)) { - return new MemoryStream(other.GetBuffer(), 0, (int)other.Length, false, true); - } - catch (UnauthorizedAccessException) - { - return new MemoryStream(other.ToArray(), 0, (int)other.Length, false, true); + return streamProvider.CreateNew(buffer); } + return streamProvider.CreateNew(other.ToArray()); } return stream; @@ -471,7 +588,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public static string GetHandlerPathIfAny(string listenerUrl) { if (listenerUrl == null) return null; - var pos = listenerUrl.IndexOf("://", StringComparison.InvariantCultureIgnoreCase); + var pos = listenerUrl.IndexOf("://", StringComparison.OrdinalIgnoreCase); if (pos == -1) return null; var startHostUrl = listenerUrl.Substring(pos + "://".Length); var endPos = startHostUrl.IndexOf('/'); @@ -483,7 +600,7 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp public static string NormalizePathInfo(string pathInfo, string handlerPath) { if (handlerPath != null && pathInfo.TrimStart('/').StartsWith( - handlerPath, StringComparison.InvariantCultureIgnoreCase)) + handlerPath, StringComparison.OrdinalIgnoreCase)) { return pathInfo.TrimStart('/').Substring(handlerPath.Length); } @@ -491,4 +608,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer.SocketSharp return pathInfo; } } + + public class HttpFile : IHttpFile + { + public string Name { get; set; } + public string FileName { get; set; } + public long ContentLength { get; set; } + public string ContentType { get; set; } + public Stream InputStream { get; set; } + } } diff --git a/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs new file mode 100644 index 0000000000..36f7954116 --- /dev/null +++ b/Emby.Server.Implementations/HttpServer/SocketSharp/WebSocketSharpResponse.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using MediaBrowser.Model.Logging; +using SocketHttpListener.Net; +using HttpListenerResponse = SocketHttpListener.Net.HttpListenerResponse; +using IHttpResponse = MediaBrowser.Model.Services.IHttpResponse; +using IRequest = MediaBrowser.Model.Services.IRequest; + +namespace Emby.Server.Implementations.HttpServer.SocketSharp +{ + public class WebSocketSharpResponse : IHttpResponse + { + private readonly ILogger _logger; + private readonly HttpListenerResponse _response; + + public WebSocketSharpResponse(ILogger logger, HttpListenerResponse response, IRequest request) + { + _logger = logger; + this._response = response; + Items = new Dictionary(); + Request = request; + } + + public IRequest Request { get; private set; } + public bool UseBufferedStream { get; set; } + public Dictionary Items { get; private set; } + public object OriginalResponse + { + get { return _response; } + } + + public int StatusCode + { + get { return this._response.StatusCode; } + set { this._response.StatusCode = value; } + } + + public string StatusDescription + { + get { return this._response.StatusDescription; } + set { this._response.StatusDescription = value; } + } + + public string ContentType + { + get { return _response.ContentType; } + set { _response.ContentType = value; } + } + + //public ICookies Cookies { get; set; } + + public void AddHeader(string name, string value) + { + if (string.Equals(name, "Content-Type", StringComparison.OrdinalIgnoreCase)) + { + ContentType = value; + return; + } + + _response.AddHeader(name, value); + } + + public string GetHeader(string name) + { + return _response.Headers[name]; + } + + public void Redirect(string url) + { + _response.Redirect(url); + } + + public Stream OutputStream + { + get { return _response.OutputStream; } + } + + public void Close() + { + if (!this.IsClosed) + { + this.IsClosed = true; + + try + { + CloseOutputStream(this._response); + } + catch (Exception ex) + { + _logger.ErrorException("Error closing HttpListener output stream", ex); + } + } + } + + public void CloseOutputStream(HttpListenerResponse response) + { + try + { + var outputStream = response.OutputStream; + + // This is needed with compression + if (outputStream is ResponseStream) + { + //if (!string.IsNullOrWhiteSpace(GetHeader("Content-Encoding"))) + { + outputStream.Flush(); + } + + outputStream.Dispose(); + } + response.Close(); + } + catch (Exception ex) + { + _logger.ErrorException("Error in HttpListenerResponseWrapper: " + ex.Message, ex); + } + } + + public bool IsClosed + { + get; + private set; + } + + public void SetContentLength(long contentLength) + { + //you can happily set the Content-Length header in Asp.Net + //but HttpListener will complain if you do - you have to set ContentLength64 on the response. + //workaround: HttpListener throws "The parameter is incorrect" exceptions when we try to set the Content-Length header + _response.ContentLength64 = contentLength; + } + + public void SetCookie(Cookie cookie) + { + var cookieStr = AsHeaderValue(cookie); + _response.Headers.Add("Set-Cookie", cookieStr); + } + + public static string AsHeaderValue(Cookie cookie) + { + var defaultExpires = DateTime.MinValue; + + var path = cookie.Expires == defaultExpires + ? "/" + : cookie.Path ?? "/"; + + var sb = new StringBuilder(); + + sb.Append($"{cookie.Name}={cookie.Value};path={path}"); + + if (cookie.Expires != defaultExpires) + { + sb.Append($";expires={cookie.Expires:R}"); + } + + if (!string.IsNullOrEmpty(cookie.Domain)) + { + sb.Append($";domain={cookie.Domain}"); + } + //else if (restrictAllCookiesToDomain != null) + //{ + // sb.Append($";domain={restrictAllCookiesToDomain}"); + //} + + if (cookie.Secure) + { + sb.Append(";Secure"); + } + if (cookie.HttpOnly) + { + sb.Append(";HttpOnly"); + } + + return sb.ToString(); + } + + + public bool SendChunked + { + get { return _response.SendChunked; } + set { _response.SendChunked = value; } + } + + public bool KeepAlive { get; set; } + + public void ClearCookies() + { + } + } +} diff --git a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs b/Emby.Server.Implementations/HttpServer/StreamWriter.cs similarity index 60% rename from MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs rename to Emby.Server.Implementations/HttpServer/StreamWriter.cs index 5f122fb96f..33378949c3 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/StreamWriter.cs +++ b/Emby.Server.Implementations/HttpServer/StreamWriter.cs @@ -1,19 +1,19 @@ using MediaBrowser.Model.Logging; -using ServiceStack.Web; using System; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.IO; -using ServiceStack; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { /// /// Class StreamWriter /// - public class StreamWriter : IStreamWriter, IAsyncStreamWriter, IHasOptions + public class StreamWriter : IAsyncStreamWriter, IHasHeaders { private ILogger Logger { get; set; } @@ -25,6 +25,8 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// The source stream. private Stream SourceStream { get; set; } + private byte[] SourceBytes { get; set; } + /// /// The _options /// @@ -33,14 +35,13 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// Gets the options. /// /// The options. - public IDictionary Options + public IDictionary Headers { get { return _options; } } public Action OnComplete { get; set; } public Action OnError { get; set; } - private readonly byte[] _bytes; /// /// Initializes a new instance of the class. @@ -58,11 +59,11 @@ namespace MediaBrowser.Server.Implementations.HttpServer SourceStream = source; Logger = logger; - Options["Content-Type"] = contentType; + Headers["Content-Type"] = contentType; if (source.CanSeek) { - Options["Content-Length"] = source.Length.ToString(UsCulture); + Headers["Content-Length"] = source.Length.ToString(UsCulture); } } @@ -73,76 +74,35 @@ namespace MediaBrowser.Server.Implementations.HttpServer /// Type of the content. /// The logger. public StreamWriter(byte[] source, string contentType, ILogger logger) - : this(new MemoryStream(source), contentType, logger) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException("contentType"); } - _bytes = source; + SourceBytes = source; Logger = logger; - Options["Content-Type"] = contentType; + Headers["Content-Type"] = contentType; - Options["Content-Length"] = source.Length.ToString(UsCulture); + Headers["Content-Length"] = source.Length.ToString(UsCulture); } - private const int BufferSize = 81920; - - /// - /// Writes to. - /// - /// The response stream. - public void WriteTo(Stream responseStream) + public async Task WriteToAsync(Stream responseStream, CancellationToken cancellationToken) { try { - if (_bytes != null) + var bytes = SourceBytes; + + if (bytes != null) { - responseStream.Write(_bytes, 0, _bytes.Length); + await responseStream.WriteAsync(bytes, 0, bytes.Length).ConfigureAwait(false); } else { using (var src = SourceStream) { - src.CopyTo(responseStream, BufferSize); - } - } - } - catch (Exception ex) - { - Logger.ErrorException("Error streaming data", ex); - - if (OnError != null) - { - OnError(); - } - - throw; - } - finally - { - if (OnComplete != null) - { - OnComplete(); - } - } - } - - public async Task WriteToAsync(Stream responseStream) - { - try - { - if (_bytes != null) - { - await responseStream.WriteAsync(_bytes, 0, _bytes.Length); - } - else - { - using (var src = SourceStream) - { - await src.CopyToAsync(responseStream, BufferSize).ConfigureAwait(false); + await src.CopyToAsync(responseStream).ConfigureAwait(false); } } } diff --git a/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs b/Emby.Server.Implementations/HttpServer/SwaggerService.cs similarity index 60% rename from MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs rename to Emby.Server.Implementations/HttpServer/SwaggerService.cs index d91f316d6d..d41946645b 100644 --- a/MediaBrowser.Server.Implementations/HttpServer/SwaggerService.cs +++ b/Emby.Server.Implementations/HttpServer/SwaggerService.cs @@ -1,17 +1,21 @@ using MediaBrowser.Controller; using MediaBrowser.Controller.Net; -using ServiceStack.Web; using System.IO; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Services; -namespace MediaBrowser.Server.Implementations.HttpServer +namespace Emby.Server.Implementations.HttpServer { - public class SwaggerService : IHasResultFactory, IRestfulService + public class SwaggerService : IService, IRequiresRequest { private readonly IServerApplicationPaths _appPaths; + private readonly IFileSystem _fileSystem; - public SwaggerService(IServerApplicationPaths appPaths) + public SwaggerService(IServerApplicationPaths appPaths, IFileSystem fileSystem, IHttpResultFactory resultFactory) { _appPaths = appPaths; + _fileSystem = fileSystem; + _resultFactory = resultFactory; } /// @@ -23,16 +27,16 @@ namespace MediaBrowser.Server.Implementations.HttpServer { var swaggerDirectory = Path.Combine(_appPaths.ApplicationResourcesPath, "swagger-ui"); - var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', Path.DirectorySeparatorChar)); + var requestedFile = Path.Combine(swaggerDirectory, request.ResourceName.Replace('/', _fileSystem.DirectorySeparatorChar)); - return ResultFactory.GetStaticFileResult(Request, requestedFile).Result; + return _resultFactory.GetStaticFileResult(Request, requestedFile).Result; } /// /// Gets or sets the result factory. /// /// The result factory. - public IHttpResultFactory ResultFactory { get; set; } + private readonly IHttpResultFactory _resultFactory; /// /// Gets or sets the request context. diff --git a/MediaBrowser.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs similarity index 86% rename from MediaBrowser.Server.Implementations/IO/FileRefresher.cs rename to Emby.Server.Implementations/IO/FileRefresher.cs index c2c776c2bb..39033249fd 100644 --- a/MediaBrowser.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -4,17 +4,20 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Model.IO; using MediaBrowser.Common.Events; -using MediaBrowser.Common.ScheduledTasks; +using MediaBrowser.Common.IO; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Logging; -using MediaBrowser.Server.Implementations.ScheduledTasks; -using MoreLinq; +using MediaBrowser.Model.System; +using MediaBrowser.Model.Tasks; +using MediaBrowser.Model.Threading; -namespace MediaBrowser.Server.Implementations.IO +namespace Emby.Server.Implementations.IO { public class FileRefresher : IDisposable { @@ -24,13 +27,15 @@ namespace MediaBrowser.Server.Implementations.IO private IServerConfigurationManager ConfigurationManager { get; set; } private readonly IFileSystem _fileSystem; private readonly List _affectedPaths = new List(); - private Timer _timer; + private ITimer _timer; + private readonly ITimerFactory _timerFactory; private readonly object _timerLock = new object(); public string Path { get; private set; } public event EventHandler Completed; + private readonly IEnvironmentInfo _environmentInfo; - public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger) + public FileRefresher(string path, IFileSystem fileSystem, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ITaskManager taskManager, ILogger logger, ITimerFactory timerFactory, IEnvironmentInfo environmentInfo) { logger.Debug("New file refresher created for {0}", path); Path = path; @@ -40,6 +45,8 @@ namespace MediaBrowser.Server.Implementations.IO LibraryManager = libraryManager; TaskManager = taskManager; Logger = logger; + _timerFactory = timerFactory; + _environmentInfo = environmentInfo; AddPath(path); } @@ -86,7 +93,7 @@ namespace MediaBrowser.Server.Implementations.IO if (_timer == null) { - _timer = new Timer(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); + _timer = _timerFactory.Create(OnTimerCallback, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1)); } else { @@ -161,7 +168,7 @@ namespace MediaBrowser.Server.Implementations.IO // If the root folder changed, run the library task so the user can see it if (itemsToRefresh.Any(i => i is AggregateFolder)) { - TaskManager.CancelIfRunningAndQueue(); + LibraryManager.ValidateMediaLibrary(new Progress(), CancellationToken.None); return; } @@ -222,7 +229,7 @@ namespace MediaBrowser.Server.Implementations.IO private bool IsFileLocked(string path) { - if (Environment.OSVersion.Platform != PlatformID.Win32NT) + if (_environmentInfo.OperatingSystem != OperatingSystem.Windows) { // Causing lockups on linux return false; @@ -236,7 +243,7 @@ namespace MediaBrowser.Server.Implementations.IO || data.IsDirectory // Opening a writable stream will fail with readonly files - || data.Attributes.HasFlag(FileAttributes.ReadOnly)) + || data.IsReadOnly) { return false; } @@ -255,22 +262,22 @@ namespace MediaBrowser.Server.Implementations.IO // But if the server only has readonly access, this is going to cause this entire algorithm to fail // So we'll take a best guess about our access level var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta - ? FileAccess.ReadWrite - : FileAccess.Read; + ? FileAccessMode.ReadWrite + : FileAccessMode.Read; try { - using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite)) + using (_fileSystem.GetFileStream(path, FileOpenMode.Open, requestedFileAccess, FileShareMode.ReadWrite)) { //file is not locked return false; } } - catch (DirectoryNotFoundException) - { - // File may have been deleted - return false; - } + //catch (DirectoryNotFoundException) + //{ + // // File may have been deleted + // return false; + //} catch (FileNotFoundException) { // File may have been deleted diff --git a/MediaBrowser.Server.Startup.Common/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs similarity index 86% rename from MediaBrowser.Server.Startup.Common/MbLinkShortcutHandler.cs rename to Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs index 14588b427a..0b1391ae02 100644 --- a/MediaBrowser.Server.Startup.Common/MbLinkShortcutHandler.cs +++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs @@ -1,8 +1,10 @@ using System; using System.IO; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Server.Startup.Common +namespace Emby.Server.Implementations.IO { public class MbLinkShortcutHandler : IShortcutHandler { @@ -47,7 +49,7 @@ namespace MediaBrowser.Server.Startup.Common throw new ArgumentNullException("targetPath"); } - File.WriteAllText(shortcutPath, targetPath); + _fileSystem.WriteAllText(shortcutPath, targetPath); } } } diff --git a/MediaBrowser.Controller/IO/ThrottledStream.cs b/Emby.Server.Implementations/IO/ThrottledStream.cs similarity index 98% rename from MediaBrowser.Controller/IO/ThrottledStream.cs rename to Emby.Server.Implementations/IO/ThrottledStream.cs index 1df00b45a2..81760b6397 100644 --- a/MediaBrowser.Controller/IO/ThrottledStream.cs +++ b/Emby.Server.Implementations/IO/ThrottledStream.cs @@ -3,7 +3,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -namespace MediaBrowser.Controller.IO +namespace Emby.Server.Implementations.IO { /// /// Class for streaming data with throttling support. @@ -326,9 +326,10 @@ namespace MediaBrowser.Controller.IO try { // The time to sleep is more then a millisecond, so sleep. - Thread.Sleep(toSleep); + var task = Task.Delay(toSleep); + Task.WaitAll(task); } - catch (ThreadAbortException) + catch { // Eatup ThreadAbortException. } diff --git a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs similarity index 95% rename from MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs rename to Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 22d7ba3bec..7a36691dff 100644 --- a/MediaBrowser.Server.Implementations/Photos/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -12,11 +12,14 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Net; -namespace MediaBrowser.Server.Implementations.Photos +namespace Emby.Server.Implementations.Images { public abstract class BaseDynamicImageProvider : IHasItemChangeMonitor, IForcedProvider, ICustomMetadataProvider, IHasOrder where T : IHasMetadata @@ -144,7 +147,14 @@ namespace MediaBrowser.Server.Implementations.Photos return ItemUpdateType.None; } - await ProviderManager.SaveImage(item, outputPath, "image/png", imageType, null, false, cancellationToken).ConfigureAwait(false); + var mimeType = MimeTypes.GetMimeType(outputPath); + + if (string.Equals(mimeType, "application/octet-stream", StringComparison.OrdinalIgnoreCase)) + { + mimeType = "image/png"; + } + + await ProviderManager.SaveImage(item, outputPath, mimeType, imageType, null, false, cancellationToken).ConfigureAwait(false); return ItemUpdateType.ImageUpdate; } @@ -351,7 +361,7 @@ namespace MediaBrowser.Server.Implementations.Photos var ext = Path.GetExtension(image); var outputPath = Path.ChangeExtension(outputPathWithoutExtension, ext); - File.Copy(image, outputPath); + FileSystem.CopyFile(image, outputPath, true); return outputPath; } diff --git a/Emby.Server.Implementations/Intros/DefaultIntroProvider.cs b/Emby.Server.Implementations/Intros/DefaultIntroProvider.cs new file mode 100644 index 0000000000..180f6aba7f --- /dev/null +++ b/Emby.Server.Implementations/Intros/DefaultIntroProvider.cs @@ -0,0 +1,384 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Security; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Configuration; +using MediaBrowser.Model.Entities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Extensions; +using MediaBrowser.Model.Globalization; + +namespace Emby.Server.Implementations.Intros +{ + public class DefaultIntroProvider : IIntroProvider + { + private readonly ISecurityManager _security; + private readonly ILocalizationManager _localization; + private readonly IConfigurationManager _serverConfig; + private readonly ILibraryManager _libraryManager; + private readonly IFileSystem _fileSystem; + private readonly IMediaSourceManager _mediaSourceManager; + + public DefaultIntroProvider(ISecurityManager security, ILocalizationManager localization, IConfigurationManager serverConfig, ILibraryManager libraryManager, IFileSystem fileSystem, IMediaSourceManager mediaSourceManager) + { + _security = security; + _localization = localization; + _serverConfig = serverConfig; + _libraryManager = libraryManager; + _fileSystem = fileSystem; + _mediaSourceManager = mediaSourceManager; + } + + public async Task> GetIntros(BaseItem item, User user) + { + var config = GetOptions(); + + if (item is Movie) + { + if (!config.EnableIntrosForMovies) + { + return new List(); + } + } + else if (item is Episode) + { + if (!config.EnableIntrosForEpisodes) + { + return new List(); + } + } + else + { + return new List(); + } + + var ratingLevel = string.IsNullOrWhiteSpace(item.OfficialRating) + ? null + : _localization.GetRatingLevel(item.OfficialRating); + + var candidates = new List(); + + var trailerTypes = new List(); + var sourceTypes = new List(); + + if (config.EnableIntrosFromMoviesInLibrary) + { + trailerTypes.Add(TrailerType.LocalTrailer); + sourceTypes.Add(SourceType.Library); + } + + if (IsSupporter) + { + if (config.EnableIntrosFromUpcomingTrailers) + { + trailerTypes.Add(TrailerType.ComingSoonToTheaters); + sourceTypes.Clear(); + } + if (config.EnableIntrosFromUpcomingDvdMovies) + { + trailerTypes.Add(TrailerType.ComingSoonToDvd); + sourceTypes.Clear(); + } + if (config.EnableIntrosFromUpcomingStreamingMovies) + { + trailerTypes.Add(TrailerType.ComingSoonToStreaming); + sourceTypes.Clear(); + } + if (config.EnableIntrosFromSimilarMovies) + { + trailerTypes.Add(TrailerType.Archive); + sourceTypes.Clear(); + } + } + + if (trailerTypes.Count > 0) + { + var trailerResult = _libraryManager.GetItemList(new InternalItemsQuery + { + IncludeItemTypes = new[] { typeof(Trailer).Name }, + TrailerTypes = trailerTypes.ToArray(), + SimilarTo = item, + IsPlayed = config.EnableIntrosForWatchedContent ? (bool?)null : false, + MaxParentalRating = config.EnableIntrosParentalControl ? ratingLevel : null, + BlockUnratedItems = config.EnableIntrosParentalControl ? new[] { UnratedItem.Trailer } : new UnratedItem[] { }, + + // Account for duplicates by imdb id, since the database doesn't support this yet + Limit = config.TrailerLimit * 2, + SourceTypes = sourceTypes.ToArray() + + }).Where(i => string.IsNullOrWhiteSpace(i.GetProviderId(MetadataProviders.Imdb)) || !string.Equals(i.GetProviderId(MetadataProviders.Imdb), item.GetProviderId(MetadataProviders.Imdb), StringComparison.OrdinalIgnoreCase)).Take(config.TrailerLimit); + + candidates.AddRange(trailerResult.Select(i => new ItemWithTrailer + { + Item = i, + Type = i.SourceType == SourceType.Channel ? ItemWithTrailerType.ChannelTrailer : ItemWithTrailerType.ItemWithTrailer, + LibraryManager = _libraryManager + })); + } + + return GetResult(item, candidates, config); + } + + private IEnumerable GetResult(BaseItem item, IEnumerable candidates, CinemaModeConfiguration config) + { + var customIntros = !string.IsNullOrWhiteSpace(config.CustomIntroPath) ? + GetCustomIntros(config) : + new List(); + + var mediaInfoIntros = !string.IsNullOrWhiteSpace(config.MediaInfoIntroPath) ? + GetMediaInfoIntros(config, item) : + new List(); + + // Avoid implicitly captured closure + return candidates.Select(i => i.IntroInfo) + .Concat(customIntros.Take(1)) + .Concat(mediaInfoIntros); + } + + private CinemaModeConfiguration GetOptions() + { + return _serverConfig.GetConfiguration("cinemamode"); + } + + private List GetCustomIntros(CinemaModeConfiguration options) + { + try + { + return GetCustomIntroFiles(options, true, false) + .OrderBy(i => Guid.NewGuid()) + .Select(i => new IntroInfo + { + Path = i + + }).ToList(); + } + catch (IOException) + { + return new List(); + } + } + + private IEnumerable GetMediaInfoIntros(CinemaModeConfiguration options, BaseItem item) + { + try + { + var hasMediaSources = item as IHasMediaSources; + + if (hasMediaSources == null) + { + return new List(); + } + + var mediaSource = _mediaSourceManager.GetStaticMediaSources(hasMediaSources, false) + .FirstOrDefault(); + + if (mediaSource == null) + { + return new List(); + } + + var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video); + var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio); + + var allIntros = GetCustomIntroFiles(options, false, true) + .OrderBy(i => Guid.NewGuid()) + .Select(i => new IntroInfo + { + Path = i + + }).ToList(); + + var returnResult = new List(); + + if (videoStream != null) + { + returnResult.AddRange(GetMediaInfoIntrosByVideoStream(allIntros, videoStream).Take(1)); + } + + if (audioStream != null) + { + returnResult.AddRange(GetMediaInfoIntrosByAudioStream(allIntros, audioStream).Take(1)); + } + + returnResult.AddRange(GetMediaInfoIntrosByTags(allIntros, item.Tags).Take(1)); + + return returnResult.DistinctBy(i => i.Path, StringComparer.OrdinalIgnoreCase); + } + catch (IOException) + { + return new List(); + } + } + + private IEnumerable GetMediaInfoIntrosByVideoStream(List allIntros, MediaStream stream) + { + var codec = stream.Codec; + + if (string.IsNullOrWhiteSpace(codec)) + { + return new List(); + } + + return allIntros + .Where(i => IsMatch(i.Path, codec)) + .OrderBy(i => Guid.NewGuid()); + } + + private IEnumerable GetMediaInfoIntrosByAudioStream(List allIntros, MediaStream stream) + { + var codec = stream.Codec; + + if (string.IsNullOrWhiteSpace(codec)) + { + return new List(); + } + + return allIntros + .Where(i => IsAudioMatch(i.Path, stream)) + .OrderBy(i => Guid.NewGuid()); + } + + private IEnumerable GetMediaInfoIntrosByTags(List allIntros, List tags) + { + return allIntros + .Where(i => tags.Any(t => IsMatch(i.Path, t))) + .OrderBy(i => Guid.NewGuid()); + } + + private bool IsMatch(string file, string attribute) + { + var filename = Path.GetFileNameWithoutExtension(file) ?? string.Empty; + filename = Normalize(filename); + + if (string.IsNullOrWhiteSpace(filename)) + { + return false; + } + + attribute = Normalize(attribute); + if (string.IsNullOrWhiteSpace(attribute)) + { + return false; + } + + return string.Equals(filename, attribute, StringComparison.OrdinalIgnoreCase); + } + + private string Normalize(string value) + { + return value; + } + + private bool IsAudioMatch(string path, MediaStream stream) + { + if (!string.IsNullOrWhiteSpace(stream.Codec)) + { + if (IsMatch(path, stream.Codec)) + { + return true; + } + } + if (!string.IsNullOrWhiteSpace(stream.Profile)) + { + if (IsMatch(path, stream.Profile)) + { + return true; + } + } + + return false; + } + + private IEnumerable GetCustomIntroFiles(CinemaModeConfiguration options, bool enableCustomIntros, bool enableMediaInfoIntros) + { + var list = new List(); + + if (enableCustomIntros && !string.IsNullOrWhiteSpace(options.CustomIntroPath)) + { + list.AddRange(_fileSystem.GetFilePaths(options.CustomIntroPath, true) + .Where(_libraryManager.IsVideoFile)); + } + + if (enableMediaInfoIntros && !string.IsNullOrWhiteSpace(options.MediaInfoIntroPath)) + { + list.AddRange(_fileSystem.GetFilePaths(options.MediaInfoIntroPath, true) + .Where(_libraryManager.IsVideoFile)); + } + + return list.Distinct(StringComparer.OrdinalIgnoreCase); + } + + public IEnumerable GetAllIntroFiles() + { + return GetCustomIntroFiles(GetOptions(), true, true); + } + + private bool IsSupporter + { + get { return _security.IsMBSupporter; } + } + + public string Name + { + get { return "Default"; } + } + + internal class ItemWithTrailer + { + internal BaseItem Item; + internal ItemWithTrailerType Type; + internal ILibraryManager LibraryManager; + + public IntroInfo IntroInfo + { + get + { + var id = Item.Id; + + if (Type == ItemWithTrailerType.ItemWithTrailer) + { + var hasTrailers = Item as IHasTrailers; + + if (hasTrailers != null) + { + id = hasTrailers.LocalTrailerIds.FirstOrDefault(); + } + } + return new IntroInfo + { + ItemId = id + }; + } + } + } + + internal enum ItemWithTrailerType + { + ChannelTrailer, + ItemWithTrailer + } + } + + public class CinemaModeConfigurationFactory : IConfigurationFactory + { + public IEnumerable GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + ConfigurationType = typeof(CinemaModeConfiguration), + Key = "cinemamode" + } + }; + } + } + +} diff --git a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs similarity index 96% rename from MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs rename to Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs index b550d1dda2..2e69cd2efa 100644 --- a/MediaBrowser.Server.Implementations/Library/CoreResolutionIgnoreRule.cs +++ b/Emby.Server.Implementations/Library/CoreResolutionIgnoreRule.cs @@ -6,9 +6,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using CommonIO; +using MediaBrowser.Common.IO; +using MediaBrowser.Controller.IO; +using MediaBrowser.Model.IO; -namespace MediaBrowser.Server.Implementations.Library +namespace Emby.Server.Implementations.Library { /// /// Provides the core resolver ignore rules @@ -53,7 +55,7 @@ namespace MediaBrowser.Server.Implementations.Library public bool ShouldIgnore(FileSystemMetadata fileInfo, BaseItem parent) { var filename = fileInfo.Name; - var isHidden = (fileInfo.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden; + var isHidden = fileInfo.IsHidden; var path = fileInfo.FullName; // Handle mac .DS_Store diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs similarity index 93% rename from MediaBrowser.Server.Implementations/Library/LibraryManager.cs rename to Emby.Server.Implementations/Library/LibraryManager.cs index 64abcc0440..5bf53fcb43 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,7 +1,5 @@ -using Interfaces.IO; -using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; -using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -20,9 +18,6 @@ using MediaBrowser.Naming.Audio; using MediaBrowser.Naming.Common; using MediaBrowser.Naming.TV; using MediaBrowser.Naming.Video; -using MediaBrowser.Server.Implementations.Library.Validators; -using MediaBrowser.Server.Implementations.Logging; -using MediaBrowser.Server.Implementations.ScheduledTasks; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -32,20 +27,23 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using CommonIO; +using Emby.Server.Implementations.Library.Resolvers; +using Emby.Server.Implementations.Library.Validators; +using Emby.Server.Implementations.ScheduledTasks; +using MediaBrowser.Model.IO; using MediaBrowser.Controller.Channels; using MediaBrowser.Model.Channels; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.Library; using MediaBrowser.Model.Net; -using MediaBrowser.Server.Implementations.Library.Resolvers; -using MoreLinq; using SortOrder = MediaBrowser.Model.Entities.SortOrder; using VideoResolver = MediaBrowser.Naming.Video.VideoResolver; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.IO; +using MediaBrowser.Model.Tasks; -namespace MediaBrowser.Server.Implementations.Library +namespace Emby.Server.Implementations.Library { /// /// Class LibraryManager @@ -341,28 +339,23 @@ namespace MediaBrowser.Server.Implementations.Library { throw new ArgumentNullException("item"); } - RegisterItem(item.Id, item); - } - - private void RegisterItem(Guid id, BaseItem item) - { if (item is IItemByName) { - if (!(item is MusicArtist)) + if (!(item is MusicArtist) && !(item is Studio)) { return; } } - if (item.IsFolder) + else if (item.IsFolder) { - if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder)) - { - if (item.SourceType != SourceType.Library) - { - return; - } - } + //if (!(item is ICollectionFolder) && !(item is UserView) && !(item is Channel) && !(item is AggregateFolder)) + //{ + // if (item.SourceType != SourceType.Library) + // { + // return; + // } + //} } else { @@ -372,7 +365,7 @@ namespace MediaBrowser.Server.Implementations.Library } } - LibraryItemsCache.AddOrUpdate(id, item, delegate { return item; }); + LibraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); } public async Task DeleteItem(BaseItem item, DeleteOptions options) @@ -404,7 +397,7 @@ namespace MediaBrowser.Server.Implementations.Library { _fileSystem.DeleteDirectory(metadataPath, true); } - catch (DirectoryNotFoundException) + catch (IOException) { } @@ -825,6 +818,30 @@ namespace MediaBrowser.Server.Implementations.Library return _userRootFolder; } + public Guid? FindIdByPath(string path, bool? isFolder) + { + // If this returns multiple items it could be tricky figuring out which one is correct. + // In most cases, the newest one will be and the others obsolete but not yet cleaned up + + var query = new InternalItemsQuery + { + Path = path, + IsFolder = isFolder, + SortBy = new[] { ItemSortBy.DateCreated }, + SortOrder = SortOrder.Descending, + Limit = 1 + }; + + var id = GetItemIds(query); + + if (id.Count == 0) + { + return null; + } + + return id[0]; + } + public BaseItem FindByPath(string path, bool? isFolder) { // If this returns multiple items it could be tricky figuring out which one is correct. @@ -1064,6 +1081,12 @@ namespace MediaBrowser.Server.Implementations.Library try { await PerformLibraryValidation(progress, cancellationToken).ConfigureAwait(false); + + if (!ConfigurationManager.Configuration.EnableSeriesPresentationUniqueKey) + { + ConfigurationManager.Configuration.EnableSeriesPresentationUniqueKey = true; + ConfigurationManager.SaveConfiguration(); + } } finally { @@ -1076,6 +1099,11 @@ namespace MediaBrowser.Server.Implementations.Library { _logger.Info("Validating media library"); + // Ensure these objects are lazy loaded. + // Without this there is a deadlock that will need to be investigated + var rootChildren = RootFolder.Children.ToList(); + rootChildren = GetUserRootFolder().Children.ToList(); + await RootFolder.RefreshMetadata(cancellationToken).ConfigureAwait(false); progress.Report(.5); @@ -1085,16 +1113,21 @@ namespace MediaBrowser.Server.Implementations.Library progress.Report(1); - var userRoot = GetUserRootFolder(); + await GetUserRootFolder().RefreshMetadata(cancellationToken).ConfigureAwait(false); - await userRoot.RefreshMetadata(cancellationToken).ConfigureAwait(false); - - await userRoot.ValidateChildren(new Progress(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); + await GetUserRootFolder().ValidateChildren(new Progress(), cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: false).ConfigureAwait(false); progress.Report(2); + // Quickly scan CollectionFolders for changes + foreach (var folder in GetUserRootFolder().Children.OfType().ToList()) + { + await folder.RefreshMetadata(cancellationToken).ConfigureAwait(false); + } + progress.Report(3); + var innerProgress = new ActionableProgress(); - innerProgress.RegisterAction(pct => progress.Report(2 + pct * .73)); + innerProgress.RegisterAction(pct => progress.Report(3 + pct * .72)); // Now validate the entire media library await RootFolder.ValidateChildren(innerProgress, cancellationToken, new MetadataRefreshOptions(_fileSystem), recursive: true).ConfigureAwait(false); @@ -1190,7 +1223,8 @@ namespace MediaBrowser.Server.Implementations.Library { Name = Path.GetFileName(dir), - Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) + Locations = _fileSystem.GetFilePaths(dir, false) + .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Select(_fileSystem.ResolveShortcut) .OrderBy(i => i) .ToList(), @@ -1262,7 +1296,6 @@ namespace MediaBrowser.Server.Implementations.Library if (parent != null) { SetTopParentIdsOrAncestors(query, new List { parent }); - query.ParentId = null; } } @@ -1274,10 +1307,27 @@ namespace MediaBrowser.Server.Implementations.Library return ItemRepository.GetItemList(query); } - public IEnumerable GetItemList(InternalItemsQuery query, IEnumerable parentIds) + public int GetCount(InternalItemsQuery query) { - var parents = parentIds.Select(i => GetItemById(new Guid(i))).Where(i => i != null).ToList(); + if (query.Recursive && query.ParentId.HasValue) + { + var parent = GetItemById(query.ParentId.Value); + if (parent != null) + { + SetTopParentIdsOrAncestors(query, new List { parent }); + } + } + if (query.User != null) + { + AddUserToQuery(query, query.User); + } + + return ItemRepository.GetCount(query); + } + + public IEnumerable GetItemList(InternalItemsQuery query, List parents) + { SetTopParentIdsOrAncestors(query, parents); if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) @@ -1407,8 +1457,14 @@ namespace MediaBrowser.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); query.AncestorIds = new string[] { }; + + // Prevent searching in all libraries due to empty filter + if (query.TopParentIds.Length == 0) + { + query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + } } } @@ -1431,7 +1487,6 @@ namespace MediaBrowser.Server.Implementations.Library if (parent != null) { SetTopParentIdsOrAncestors(query, new List { parent }); - query.ParentId = null; } } @@ -1466,13 +1521,27 @@ namespace MediaBrowser.Server.Implementations.Library })) { // Optimize by querying against top level views - query.TopParentIds = parents.SelectMany(i => GetTopParentsForQuery(i, query.User)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).Select(i => i.ToString("N")).ToArray(); + + // Prevent searching in all libraries due to empty filter + if (query.TopParentIds.Length == 0) + { + query.TopParentIds = new[] { Guid.NewGuid().ToString("N") }; + } } else { // We need to be able to query from any arbitrary ancestor up the tree query.AncestorIds = parents.SelectMany(i => i.GetIdsForAncestorQuery()).Select(i => i.ToString("N")).ToArray(); + + // Prevent searching in all libraries due to empty filter + if (query.AncestorIds.Length == 0) + { + query.AncestorIds = new[] { Guid.NewGuid().ToString("N") }; + } } + + query.ParentId = null; } private void AddUserToQuery(InternalItemsQuery query, User user) @@ -1481,8 +1550,9 @@ namespace MediaBrowser.Server.Implementations.Library !query.ParentId.HasValue && query.ChannelIds.Length == 0 && query.TopParentIds.Length == 0 && - string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) - && query.ItemIds.Length == 0) + string.IsNullOrWhiteSpace(query.AncestorWithPresentationUniqueKey) && + string.IsNullOrWhiteSpace(query.SeriesPresentationUniqueKey) && + query.ItemIds.Length == 0) { var userViews = _userviewManager().GetUserViews(new UserViewQuery { @@ -1491,11 +1561,11 @@ namespace MediaBrowser.Server.Implementations.Library }, CancellationToken.None).Result.ToList(); - query.TopParentIds = userViews.SelectMany(i => GetTopParentsForQuery(i, user)).Select(i => i.Id.ToString("N")).ToArray(); + query.TopParentIds = userViews.SelectMany(i => GetTopParentIdsForQuery(i, user)).Select(i => i.ToString("N")).ToArray(); } } - private IEnumerable GetTopParentsForQuery(BaseItem item, User user) + private IEnumerable GetTopParentIdsForQuery(BaseItem item, User user) { var view = item as UserView; @@ -1503,7 +1573,7 @@ namespace MediaBrowser.Server.Implementations.Library { if (string.Equals(view.ViewType, CollectionType.LiveTv)) { - return new[] { view }; + return new[] { view.Id }; } if (string.Equals(view.ViewType, CollectionType.Channels)) { @@ -1513,7 +1583,7 @@ namespace MediaBrowser.Server.Implementations.Library }, CancellationToken.None).Result; - return channelResult.Items; + return channelResult.Items.Select(i => i.Id); } // Translate view into folders @@ -1522,45 +1592,45 @@ namespace MediaBrowser.Server.Implementations.Library var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } if (view.ParentId != Guid.Empty) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) { - return GetTopParentsForQuery(displayParent, user); + return GetTopParentIdsForQuery(displayParent, user); } - return new BaseItem[] { }; + return new Guid[] { }; } // Handle grouping - if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)) + if (user != null && !string.IsNullOrWhiteSpace(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0) { return user.RootFolder .GetChildren(user, true) .OfType() .Where(i => string.IsNullOrWhiteSpace(i.CollectionType) || string.Equals(i.CollectionType, view.ViewType, StringComparison.OrdinalIgnoreCase)) .Where(i => user.IsFolderGrouped(i.Id)) - .SelectMany(i => GetTopParentsForQuery(i, user)); + .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return new BaseItem[] { }; + return new Guid[] { }; } var collectionFolder = item as CollectionFolder; if (collectionFolder != null) { - return collectionFolder.GetPhysicalParents(); + return collectionFolder.PhysicalFolderIds; } var topParent = item.GetTopParent(); if (topParent != null) { - return new[] { topParent }; + return new[] { topParent.Id }; } - return new BaseItem[] { }; + return new Guid[] { }; } /// @@ -2266,7 +2336,7 @@ namespace MediaBrowser.Server.Implementations.Library public bool IsVideoFile(string path, LibraryOptions libraryOptions) { - var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new PatternsLogger()); + var resolver = new VideoResolver(GetNamingOptions(libraryOptions), new NullLogger()); return resolver.IsVideoFile(path); } @@ -2294,7 +2364,7 @@ namespace MediaBrowser.Server.Implementations.Library public bool FillMissingEpisodeNumbersFromPath(Episode episode) { var resolver = new EpisodeResolver(GetNamingOptions(), - new PatternsLogger()); + new NullLogger()); var isFolder = episode.VideoType == VideoType.BluRay || episode.VideoType == VideoType.Dvd || episode.VideoType == VideoType.HdDvd; @@ -2303,11 +2373,11 @@ namespace MediaBrowser.Server.Implementations.Library var episodeInfo = locationType == LocationType.FileSystem || locationType == LocationType.Offline ? resolver.Resolve(episode.Path, isFolder) : - new Naming.TV.EpisodeInfo(); + new MediaBrowser.Naming.TV.EpisodeInfo(); if (episodeInfo == null) { - episodeInfo = new Naming.TV.EpisodeInfo(); + episodeInfo = new MediaBrowser.Naming.TV.EpisodeInfo(); } var changed = false; @@ -2440,7 +2510,7 @@ namespace MediaBrowser.Server.Implementations.Library public ItemLookupInfo ParseName(string name) { - var resolver = new VideoResolver(GetNamingOptions(), new PatternsLogger()); + var resolver = new VideoResolver(GetNamingOptions(), new NullLogger()); var result = resolver.CleanDateTime(name); var cleanName = resolver.CleanString(result.Name); @@ -2459,14 +2529,9 @@ namespace MediaBrowser.Server.Implementations.Library .SelectMany(i => _fileSystem.GetFiles(i.FullName, false)) .ToList(); - var videoListResolver = new VideoListResolver(GetNamingOptions(), new PatternsLogger()); + var videoListResolver = new VideoListResolver(GetNamingOptions(), new NullLogger()); - var videos = videoListResolver.Resolve(fileSystemChildren.Select(i => new FileMetadata - { - Id = i.FullName, - IsFolder = (i.Attributes & FileAttributes.Directory) == FileAttributes.Directory - - }).ToList()); + var videos = videoListResolver.Resolve(fileSystemChildren); var currentVideo = videos.FirstOrDefault(i => string.Equals(owner.Path, i.Files.First().Path, StringComparison.OrdinalIgnoreCase)); @@ -2501,21 +2566,18 @@ namespace MediaBrowser.Server.Implementations.Library }).OrderBy(i => i.Path).ToList(); } + private static readonly string[] ExtrasSubfolderNames = new[] { "extras", "specials", "shorts", "scenes", "featurettes", "behind the scenes", "deleted scenes" }; + public IEnumerable