You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
629 lines
22 KiB
629 lines
22 KiB
using MediaBrowser.Common.Configuration;
|
|
using MediaBrowser.Common.Extensions;
|
|
using MediaBrowser.Controller;
|
|
using MediaBrowser.Controller.Dlna;
|
|
using MediaBrowser.Controller.Drawing;
|
|
using MediaBrowser.Controller.Plugins;
|
|
using MediaBrowser.Dlna.Profiles;
|
|
using MediaBrowser.Dlna.Server;
|
|
using MediaBrowser.Model.Dlna;
|
|
using MediaBrowser.Model.Drawing;
|
|
using MediaBrowser.Model.Logging;
|
|
using MediaBrowser.Model.Serialization;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using CommonIO;
|
|
|
|
namespace MediaBrowser.Dlna
|
|
{
|
|
public class DlnaManager : IDlnaManager
|
|
{
|
|
private readonly IApplicationPaths _appPaths;
|
|
private readonly IXmlSerializer _xmlSerializer;
|
|
private readonly IFileSystem _fileSystem;
|
|
private readonly ILogger _logger;
|
|
private readonly IJsonSerializer _jsonSerializer;
|
|
private readonly IServerApplicationHost _appHost;
|
|
|
|
private readonly Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>> _profiles = new Dictionary<string, Tuple<InternalProfileInfo, DeviceProfile>>(StringComparer.Ordinal);
|
|
|
|
public DlnaManager(IXmlSerializer xmlSerializer,
|
|
IFileSystem fileSystem,
|
|
IApplicationPaths appPaths,
|
|
ILogger logger,
|
|
IJsonSerializer jsonSerializer, IServerApplicationHost appHost)
|
|
{
|
|
_xmlSerializer = xmlSerializer;
|
|
_fileSystem = fileSystem;
|
|
_appPaths = appPaths;
|
|
_logger = logger;
|
|
_jsonSerializer = jsonSerializer;
|
|
_appHost = appHost;
|
|
}
|
|
|
|
public void InitProfiles()
|
|
{
|
|
try
|
|
{
|
|
ExtractSystemProfiles();
|
|
LoadProfiles();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.ErrorException("Error extracting DLNA profiles.", ex);
|
|
}
|
|
}
|
|
|
|
private void LoadProfiles()
|
|
{
|
|
var list = GetProfiles(UserProfilesPath, DeviceProfileType.User)
|
|
.OrderBy(i => i.Name)
|
|
.ToList();
|
|
|
|
list.AddRange(GetProfiles(SystemProfilesPath, DeviceProfileType.System)
|
|
.OrderBy(i => i.Name));
|
|
}
|
|
|
|
public IEnumerable<DeviceProfile> GetProfiles()
|
|
{
|
|
lock (_profiles)
|
|
{
|
|
var list = _profiles.Values.ToList();
|
|
return list
|
|
.OrderBy(i => i.Item1.Info.Type == DeviceProfileType.User ? 0 : 1)
|
|
.ThenBy(i => i.Item1.Info.Name)
|
|
.Select(i => i.Item2)
|
|
.ToList();
|
|
}
|
|
|
|
}
|
|
|
|
public DeviceProfile GetDefaultProfile()
|
|
{
|
|
return new DefaultProfile();
|
|
}
|
|
|
|
public DeviceProfile GetProfile(DeviceIdentification deviceInfo)
|
|
{
|
|
if (deviceInfo == null)
|
|
{
|
|
throw new ArgumentNullException("deviceInfo");
|
|
}
|
|
|
|
var profile = GetProfiles()
|
|
.FirstOrDefault(i => i.Identification != null && IsMatch(deviceInfo, i.Identification));
|
|
|
|
if (profile != null)
|
|
{
|
|
_logger.Debug("Found matching device profile: {0}", profile.Name);
|
|
}
|
|
else
|
|
{
|
|
_logger.Debug("No matching device profile found. The default will need to be used.");
|
|
LogUnmatchedProfile(deviceInfo);
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
|
|
private void LogUnmatchedProfile(DeviceIdentification profile)
|
|
{
|
|
var builder = new StringBuilder();
|
|
|
|
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
|
|
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
|
|
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
|
|
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
|
|
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
|
|
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
|
|
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
|
|
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
|
|
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
|
|
|
|
_logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, builder);
|
|
}
|
|
|
|
private bool IsMatch(DeviceIdentification deviceInfo, DeviceIdentification profileInfo)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.DeviceDescription))
|
|
{
|
|
if (deviceInfo.DeviceDescription == null || !IsRegexMatch(deviceInfo.DeviceDescription, profileInfo.DeviceDescription))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.FriendlyName))
|
|
{
|
|
if (deviceInfo.FriendlyName == null || !IsRegexMatch(deviceInfo.FriendlyName, profileInfo.FriendlyName))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.Manufacturer))
|
|
{
|
|
if (deviceInfo.Manufacturer == null || !IsRegexMatch(deviceInfo.Manufacturer, profileInfo.Manufacturer))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.ManufacturerUrl))
|
|
{
|
|
if (deviceInfo.ManufacturerUrl == null || !IsRegexMatch(deviceInfo.ManufacturerUrl, profileInfo.ManufacturerUrl))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.ModelDescription))
|
|
{
|
|
if (deviceInfo.ModelDescription == null || !IsRegexMatch(deviceInfo.ModelDescription, profileInfo.ModelDescription))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.ModelName))
|
|
{
|
|
if (deviceInfo.ModelName == null || !IsRegexMatch(deviceInfo.ModelName, profileInfo.ModelName))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.ModelNumber))
|
|
{
|
|
if (deviceInfo.ModelNumber == null || !IsRegexMatch(deviceInfo.ModelNumber, profileInfo.ModelNumber))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.ModelUrl))
|
|
{
|
|
if (deviceInfo.ModelUrl == null || !IsRegexMatch(deviceInfo.ModelUrl, profileInfo.ModelUrl))
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(profileInfo.SerialNumber))
|
|
{
|
|
if (deviceInfo.SerialNumber == null || !IsRegexMatch(deviceInfo.SerialNumber, profileInfo.SerialNumber))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool IsRegexMatch(string input, string pattern)
|
|
{
|
|
try
|
|
{
|
|
return Regex.IsMatch(input, pattern);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
_logger.ErrorException("Error evaluating regex pattern {0}", ex, pattern);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public DeviceProfile GetProfile(IDictionary<string, string> headers)
|
|
{
|
|
if (headers == null)
|
|
{
|
|
throw new ArgumentNullException("headers");
|
|
}
|
|
|
|
// Convert to case insensitive
|
|
headers = new Dictionary<string, string>(headers, StringComparer.OrdinalIgnoreCase);
|
|
|
|
var profile = GetProfiles().FirstOrDefault(i => i.Identification != null && IsMatch(headers, i.Identification));
|
|
|
|
if (profile != null)
|
|
{
|
|
_logger.Debug("Found matching device profile: {0}", profile.Name);
|
|
}
|
|
else
|
|
{
|
|
var msg = new StringBuilder();
|
|
foreach (var header in headers)
|
|
{
|
|
msg.AppendLine(header.Key + ": " + header.Value);
|
|
}
|
|
_logger.LogMultiline("No matching device profile found. The default will need to be used.", LogSeverity.Info, msg);
|
|
}
|
|
|
|
return profile;
|
|
}
|
|
|
|
private bool IsMatch(IDictionary<string, string> headers, DeviceIdentification profileInfo)
|
|
{
|
|
return profileInfo.Headers.Any(i => IsMatch(headers, i));
|
|
}
|
|
|
|
private bool IsMatch(IDictionary<string, string> headers, HttpHeaderInfo header)
|
|
{
|
|
string value;
|
|
|
|
if (headers.TryGetValue(header.Name, out value))
|
|
{
|
|
switch (header.Match)
|
|
{
|
|
case HeaderMatchType.Equals:
|
|
return string.Equals(value, header.Value, StringComparison.OrdinalIgnoreCase);
|
|
case HeaderMatchType.Substring:
|
|
var isMatch = value.IndexOf(header.Value, StringComparison.OrdinalIgnoreCase) != -1;
|
|
//_logger.Debug("IsMatch-Substring value: {0} testValue: {1} isMatch: {2}", value, header.Value, isMatch);
|
|
return isMatch;
|
|
case HeaderMatchType.Regex:
|
|
return Regex.IsMatch(value, header.Value, RegexOptions.IgnoreCase);
|
|
default:
|
|
throw new ArgumentException("Unrecognized HeaderMatchType");
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private string UserProfilesPath
|
|
{
|
|
get
|
|
{
|
|
return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "user");
|
|
}
|
|
}
|
|
|
|
private string SystemProfilesPath
|
|
{
|
|
get
|
|
{
|
|
return Path.Combine(_appPaths.ConfigurationDirectoryPath, "dlna", "system");
|
|
}
|
|
}
|
|
|
|
private IEnumerable<DeviceProfile> GetProfiles(string path, DeviceProfileType type)
|
|
{
|
|
try
|
|
{
|
|
var allFiles = _fileSystem.GetFiles(path)
|
|
.ToList();
|
|
|
|
var xmlFies = allFiles
|
|
.Where(i => string.Equals(i.Extension, ".xml", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
var jsonFiles = allFiles
|
|
.Where(i => string.Equals(i.Extension, ".json", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
var jsonFileNames = jsonFiles
|
|
.Select(i => Path.GetFileNameWithoutExtension(i.Name))
|
|
.ToList();
|
|
|
|
var parseFiles = jsonFiles.ToList();
|
|
|
|
parseFiles.AddRange(xmlFies.Where(i => !jsonFileNames.Contains(Path.GetFileNameWithoutExtension(i.Name), StringComparer.Ordinal)));
|
|
|
|
return parseFiles
|
|
.Select(i => ParseProfileFile(i.FullName, type))
|
|
.Where(i => i != null)
|
|
.ToList();
|
|
}
|
|
catch (DirectoryNotFoundException)
|
|
{
|
|
return new List<DeviceProfile>();
|
|
}
|
|
}
|
|
|
|
private DeviceProfile ParseProfileFile(string path, DeviceProfileType type)
|
|
{
|
|
lock (_profiles)
|
|
{
|
|
Tuple<InternalProfileInfo, DeviceProfile> profileTuple;
|
|
if (_profiles.TryGetValue(path, out profileTuple))
|
|
{
|
|
return profileTuple.Item2;
|
|
}
|
|
|
|
try
|
|
{
|
|
DeviceProfile profile;
|
|
|
|
if (string.Equals(Path.GetExtension(path), ".xml", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var tempProfile = (MediaBrowser.Dlna.ProfileSerialization.DeviceProfile)_xmlSerializer.DeserializeFromFile(typeof(MediaBrowser.Dlna.ProfileSerialization.DeviceProfile), path);
|
|
|
|
var json = _jsonSerializer.SerializeToString(tempProfile);
|
|
profile = (DeviceProfile)_jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
|
}
|
|
else
|
|
{
|
|
profile = (DeviceProfile)_jsonSerializer.DeserializeFromFile(typeof(DeviceProfile), path);
|
|
}
|
|
|
|
profile.Id = path.ToLower().GetMD5().ToString("N");
|
|
profile.ProfileType = type;
|
|
|
|
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
|
|
|
return profile;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.ErrorException("Error parsing profile file: {0}", ex, path);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public DeviceProfile GetProfile(string id)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(id))
|
|
{
|
|
throw new ArgumentNullException("id");
|
|
}
|
|
|
|
var info = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, id, StringComparison.OrdinalIgnoreCase));
|
|
|
|
return ParseProfileFile(info.Path, info.Info.Type);
|
|
}
|
|
|
|
private IEnumerable<InternalProfileInfo> GetProfileInfosInternal()
|
|
{
|
|
lock (_profiles)
|
|
{
|
|
var list = _profiles.Values.ToList();
|
|
return list
|
|
.Select(i => i.Item1)
|
|
.OrderBy(i => i.Info.Type == DeviceProfileType.User ? 0 : 1)
|
|
.ThenBy(i => i.Info.Name);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<DeviceProfileInfo> GetProfileInfos()
|
|
{
|
|
return GetProfileInfosInternal().Select(i => i.Info);
|
|
}
|
|
|
|
private InternalProfileInfo GetInternalProfileInfo(FileSystemMetadata file, DeviceProfileType type)
|
|
{
|
|
return new InternalProfileInfo
|
|
{
|
|
Path = file.FullName,
|
|
|
|
Info = new DeviceProfileInfo
|
|
{
|
|
Id = file.FullName.ToLower().GetMD5().ToString("N"),
|
|
Name = _fileSystem.GetFileNameWithoutExtension(file),
|
|
Type = type
|
|
}
|
|
};
|
|
}
|
|
|
|
private void ExtractSystemProfiles()
|
|
{
|
|
var assembly = GetType().Assembly;
|
|
var namespaceName = GetType().Namespace + ".Profiles.Json.";
|
|
|
|
var systemProfilesPath = SystemProfilesPath;
|
|
|
|
foreach (var name in assembly.GetManifestResourceNames()
|
|
.Where(i => i.StartsWith(namespaceName))
|
|
.ToList())
|
|
{
|
|
var filename = Path.GetFileName(name).Substring(namespaceName.Length);
|
|
|
|
var path = Path.Combine(systemProfilesPath, filename);
|
|
|
|
using (var stream = assembly.GetManifestResourceStream(name))
|
|
{
|
|
var fileInfo = new FileInfo(path);
|
|
|
|
if (!fileInfo.Exists || fileInfo.Length != stream.Length)
|
|
{
|
|
_fileSystem.CreateDirectory(systemProfilesPath);
|
|
|
|
using (var fileStream = _fileSystem.GetFileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
|
|
{
|
|
stream.CopyTo(fileStream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Not necessary, but just to make it easy to find
|
|
_fileSystem.CreateDirectory(UserProfilesPath);
|
|
}
|
|
|
|
public void DeleteProfile(string id)
|
|
{
|
|
var info = GetProfileInfosInternal().First(i => string.Equals(id, i.Info.Id, StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (info.Info.Type == DeviceProfileType.System)
|
|
{
|
|
throw new ArgumentException("System profiles cannot be deleted.");
|
|
}
|
|
|
|
_fileSystem.DeleteFile(info.Path);
|
|
|
|
lock (_profiles)
|
|
{
|
|
_profiles.Remove(info.Path);
|
|
}
|
|
}
|
|
|
|
public void CreateProfile(DeviceProfile profile)
|
|
{
|
|
profile = ReserializeProfile(profile);
|
|
|
|
if (string.IsNullOrWhiteSpace(profile.Name))
|
|
{
|
|
throw new ArgumentException("Profile is missing Name");
|
|
}
|
|
|
|
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".json";
|
|
var path = Path.Combine(UserProfilesPath, newFilename);
|
|
|
|
SaveProfile(profile, path, DeviceProfileType.User);
|
|
}
|
|
|
|
public void UpdateProfile(DeviceProfile profile)
|
|
{
|
|
profile = ReserializeProfile(profile);
|
|
|
|
if (string.IsNullOrWhiteSpace(profile.Id))
|
|
{
|
|
throw new ArgumentException("Profile is missing Id");
|
|
}
|
|
if (string.IsNullOrWhiteSpace(profile.Name))
|
|
{
|
|
throw new ArgumentException("Profile is missing Name");
|
|
}
|
|
|
|
var current = GetProfileInfosInternal().First(i => string.Equals(i.Info.Id, profile.Id, StringComparison.OrdinalIgnoreCase));
|
|
|
|
var newFilename = _fileSystem.GetValidFilename(profile.Name) + ".json";
|
|
var path = Path.Combine(UserProfilesPath, newFilename);
|
|
|
|
if (!string.Equals(path, current.Path, StringComparison.Ordinal) &&
|
|
current.Info.Type != DeviceProfileType.System)
|
|
{
|
|
_fileSystem.DeleteFile(current.Path);
|
|
}
|
|
|
|
SaveProfile(profile, path, DeviceProfileType.User);
|
|
}
|
|
|
|
private void SaveProfile(DeviceProfile profile, string path, DeviceProfileType type)
|
|
{
|
|
lock (_profiles)
|
|
{
|
|
_profiles[path] = new Tuple<InternalProfileInfo, DeviceProfile>(GetInternalProfileInfo(_fileSystem.GetFileInfo(path), type), profile);
|
|
}
|
|
SerializeToJson(profile, path);
|
|
}
|
|
|
|
internal void SerializeToJson(DeviceProfile profile, string path)
|
|
{
|
|
_jsonSerializer.SerializeToFile(profile, path);
|
|
|
|
try
|
|
{
|
|
File.Delete(Path.ChangeExtension(path, ".xml"));
|
|
}
|
|
catch
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recreates the object using serialization, to ensure it's not a subclass.
|
|
/// If it's a subclass it may not serlialize properly to xml (different root element tag name)
|
|
/// </summary>
|
|
/// <param name="profile"></param>
|
|
/// <returns></returns>
|
|
private DeviceProfile ReserializeProfile(DeviceProfile profile)
|
|
{
|
|
if (profile.GetType() == typeof(DeviceProfile))
|
|
{
|
|
return profile;
|
|
}
|
|
|
|
var json = _jsonSerializer.SerializeToString(profile);
|
|
|
|
return _jsonSerializer.DeserializeFromString<DeviceProfile>(json);
|
|
}
|
|
|
|
class InternalProfileInfo
|
|
{
|
|
internal DeviceProfileInfo Info { get; set; }
|
|
internal string Path { get; set; }
|
|
}
|
|
|
|
public string GetServerDescriptionXml(IDictionary<string, string> headers, string serverUuId, string serverAddress)
|
|
{
|
|
var profile = GetProfile(headers) ??
|
|
GetDefaultProfile();
|
|
|
|
var serverId = _appHost.SystemId;
|
|
|
|
return new DescriptionXmlBuilder(profile, serverUuId, serverAddress, _appHost.FriendlyName, serverId).GetXml();
|
|
}
|
|
|
|
public ImageStream GetIcon(string filename)
|
|
{
|
|
var format = filename.EndsWith(".png", StringComparison.OrdinalIgnoreCase)
|
|
? ImageFormat.Png
|
|
: ImageFormat.Jpg;
|
|
|
|
return new ImageStream
|
|
{
|
|
Format = format,
|
|
Stream = GetType().Assembly.GetManifestResourceStream("MediaBrowser.Dlna.Images." + filename.ToLower())
|
|
};
|
|
}
|
|
}
|
|
|
|
class DlnaProfileEntryPoint : IServerEntryPoint
|
|
{
|
|
private readonly IApplicationPaths _appPaths;
|
|
private readonly IJsonSerializer _jsonSerializer;
|
|
private readonly IFileSystem _fileSystem;
|
|
|
|
public DlnaProfileEntryPoint(IApplicationPaths appPaths, IFileSystem fileSystem, IJsonSerializer jsonSerializer)
|
|
{
|
|
_appPaths = appPaths;
|
|
_fileSystem = fileSystem;
|
|
_jsonSerializer = jsonSerializer;
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
//DumpProfiles();
|
|
}
|
|
|
|
private void DumpProfiles()
|
|
{
|
|
var list = new List<DeviceProfile>
|
|
{
|
|
new SamsungSmartTvProfile(),
|
|
new Xbox360Profile(),
|
|
new XboxOneProfile(),
|
|
new SonyPs3Profile(),
|
|
new SonyPs4Profile(),
|
|
new SonyBravia2010Profile(),
|
|
new SonyBravia2011Profile(),
|
|
new SonyBravia2012Profile(),
|
|
new SonyBravia2013Profile(),
|
|
new SonyBravia2014Profile(),
|
|
new SonyBlurayPlayer2013(),
|
|
new SonyBlurayPlayer2014(),
|
|
new SonyBlurayPlayer2015(),
|
|
new SonyBlurayPlayer2016(),
|
|
new SonyBlurayPlayerProfile(),
|
|
new PanasonicVieraProfile(),
|
|
new WdtvLiveProfile(),
|
|
new DenonAvrProfile(),
|
|
new LinksysDMA2100Profile(),
|
|
new LgTvProfile(),
|
|
new Foobar2000Profile(),
|
|
new MediaMonkeyProfile(),
|
|
//new Windows81Profile(),
|
|
//new WindowsMediaCenterProfile(),
|
|
//new WindowsPhoneProfile(),
|
|
new DirectTvProfile(),
|
|
new DishHopperJoeyProfile(),
|
|
new DefaultProfile(),
|
|
new PopcornHourProfile(),
|
|
new VlcProfile(),
|
|
new BubbleUpnpProfile(),
|
|
new KodiProfile(),
|
|
};
|
|
|
|
foreach (var item in list)
|
|
{
|
|
var path = Path.Combine(_appPaths.ProgramDataPath, _fileSystem.GetValidFilename(item.Name) + ".json");
|
|
|
|
_jsonSerializer.SerializeToFile(item, path);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
} |