fixes #232 - '/' in artist causes issues.
This commit is contained in:
parent
57d7e9fccc
commit
5c873d3ed1
|
@ -1,5 +1,8 @@
|
|||
using MediaBrowser.Common.Net;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Logging;
|
||||
|
@ -88,6 +91,123 @@ namespace MediaBrowser.Api
|
|||
{
|
||||
return ResultFactory.GetStaticFileResult(RequestContext, path);
|
||||
}
|
||||
|
||||
private readonly char[] _dashReplaceChars = new[] { '?', '/' };
|
||||
private const char SlugChar = '-';
|
||||
|
||||
protected Task<Artist> GetArtist(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
return libraryManager.GetArtist(DeSlugArtistName(name, libraryManager));
|
||||
}
|
||||
|
||||
protected Task<Studio> GetStudio(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
return libraryManager.GetStudio(DeSlugStudioName(name, libraryManager));
|
||||
}
|
||||
|
||||
protected Task<Genre> GetGenre(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
return libraryManager.GetGenre(DeSlugGenreName(name, libraryManager));
|
||||
}
|
||||
|
||||
protected Task<Person> GetPerson(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
return libraryManager.GetPerson(DeSlugPersonName(name, libraryManager));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deslugs an artist name by finding the correct entry in the library
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="libraryManager"></param>
|
||||
/// <returns></returns>
|
||||
protected string DeSlugArtistName(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
if (name.IndexOf(SlugChar) == -1)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.RecursiveChildren
|
||||
.OfType<Audio>()
|
||||
.SelectMany(i => new[] { i.Artist, i.AlbumArtist })
|
||||
.Where(i => !string.IsNullOrEmpty(i))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
||||
return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}) ?? name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deslugs a genre name by finding the correct entry in the library
|
||||
/// </summary>
|
||||
protected string DeSlugGenreName(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
if (name.IndexOf(SlugChar) == -1)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.RecursiveChildren
|
||||
.SelectMany(i => i.Genres)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
||||
return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}) ?? name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deslugs a studio name by finding the correct entry in the library
|
||||
/// </summary>
|
||||
protected string DeSlugStudioName(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
if (name.IndexOf(SlugChar) == -1)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.RecursiveChildren
|
||||
.SelectMany(i => i.Studios)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
||||
return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}) ?? name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deslugs a person name by finding the correct entry in the library
|
||||
/// </summary>
|
||||
protected string DeSlugPersonName(string name, ILibraryManager libraryManager)
|
||||
{
|
||||
if (name.IndexOf(SlugChar) == -1)
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return libraryManager.RootFolder.RecursiveChildren
|
||||
.SelectMany(i => i.People)
|
||||
.Select(i => i.Name)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.FirstOrDefault(i =>
|
||||
{
|
||||
i = _dashReplaceChars.Aggregate(i, (current, c) => current.Replace(c, SlugChar));
|
||||
|
||||
return string.Equals(i, name, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
}) ?? name;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -464,7 +464,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetStudioImage request)
|
||||
{
|
||||
var item = _libraryManager.GetStudio(request.Name).Result;
|
||||
var item = GetStudio(request.Name, _libraryManager).Result;
|
||||
|
||||
return GetImage(request, item);
|
||||
}
|
||||
|
@ -476,7 +476,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetPersonImage request)
|
||||
{
|
||||
var item = _libraryManager.GetPerson(request.Name).Result;
|
||||
var item = GetPerson(request.Name, _libraryManager).Result;
|
||||
|
||||
return GetImage(request, item);
|
||||
}
|
||||
|
@ -488,7 +488,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetArtistImage request)
|
||||
{
|
||||
var item = _libraryManager.GetArtist(request.Name).Result;
|
||||
var item = GetArtist(request.Name, _libraryManager).Result;
|
||||
|
||||
return GetImage(request, item);
|
||||
}
|
||||
|
@ -500,7 +500,7 @@ namespace MediaBrowser.Api.Images
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetGenreImage request)
|
||||
{
|
||||
var item = _libraryManager.GetGenre(request.Name).Result;
|
||||
var item = GetGenre(request.Name, _libraryManager).Result;
|
||||
|
||||
return GetImage(request, item);
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>Task{BaseItemDto}.</returns>
|
||||
private async Task<BaseItemDto> GetItem(GetArtist request)
|
||||
{
|
||||
var item = await LibraryManager.GetArtist(request.Name).ConfigureAwait(false);
|
||||
var item = await GetArtist(request.Name, LibraryManager).ConfigureAwait(false);
|
||||
|
||||
// Get everything
|
||||
var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
|
||||
|
@ -127,7 +127,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetArtistsItemCounts request)
|
||||
{
|
||||
var items = GetItems(request.UserId).OfType<Audio>().Where(i => i.HasArtist(request.Name)).ToList();
|
||||
var name = DeSlugArtistName(request.Name, LibraryManager);
|
||||
|
||||
var items = GetItems(request.UserId).OfType<Audio>().Where(i => i.HasArtist(name)).ToList();
|
||||
|
||||
var counts = new ItemByNameCounts
|
||||
{
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>Task{BaseItemDto}.</returns>
|
||||
private async Task<BaseItemDto> GetItem(GetGenre request)
|
||||
{
|
||||
var item = await LibraryManager.GetGenre(request.Name).ConfigureAwait(false);
|
||||
var item = await GetGenre(request.Name, LibraryManager).ConfigureAwait(false);
|
||||
|
||||
// Get everything
|
||||
var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
|
||||
|
@ -156,7 +156,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetGenreItemCounts request)
|
||||
{
|
||||
var items = GetItems(request.UserId).Where(i => i.Genres != null && i.Genres.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
var name = DeSlugGenreName(request.Name, LibraryManager);
|
||||
|
||||
var items = GetItems(request.UserId).Where(i => i.Genres != null && i.Genres.Contains(name, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
var counts = new ItemByNameCounts
|
||||
{
|
||||
|
|
|
@ -212,19 +212,19 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
if (string.Equals(type, "Persons"))
|
||||
{
|
||||
item = await LibraryManager.GetPerson(name).ConfigureAwait(false);
|
||||
item = await GetPerson(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Artists"))
|
||||
{
|
||||
item = await LibraryManager.GetArtist(name).ConfigureAwait(false);
|
||||
item = await GetArtist(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Genres"))
|
||||
{
|
||||
item = await LibraryManager.GetGenre(name).ConfigureAwait(false);
|
||||
item = await GetGenre(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Studios"))
|
||||
{
|
||||
item = await LibraryManager.GetStudio(name).ConfigureAwait(false);
|
||||
item = await GetStudio(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -256,19 +256,19 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
|
||||
if (string.Equals(type, "Persons"))
|
||||
{
|
||||
item = await LibraryManager.GetPerson(name).ConfigureAwait(false);
|
||||
item = await GetPerson(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Artists"))
|
||||
{
|
||||
item = await LibraryManager.GetArtist(name).ConfigureAwait(false);
|
||||
item = await GetArtist(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Genres"))
|
||||
{
|
||||
item = await LibraryManager.GetGenre(name).ConfigureAwait(false);
|
||||
item = await GetGenre(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else if (string.Equals(type, "Studios"))
|
||||
{
|
||||
item = await LibraryManager.GetStudio(name).ConfigureAwait(false);
|
||||
item = await GetStudio(name, LibraryManager).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -109,7 +109,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>Task{BaseItemDto}.</returns>
|
||||
private async Task<BaseItemDto> GetItem(GetPerson request)
|
||||
{
|
||||
var item = await LibraryManager.GetPerson(request.Name).ConfigureAwait(false);
|
||||
var item = await GetPerson(request.Name, LibraryManager).ConfigureAwait(false);
|
||||
|
||||
// Get everything
|
||||
var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
|
||||
|
@ -145,7 +145,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetPersonItemCounts request)
|
||||
{
|
||||
var items = GetItems(request.UserId).Where(i => i.People != null && i.People.Any(p => string.Equals(p.Name, request.Name, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
var name = DeSlugPersonName(request.Name, LibraryManager);
|
||||
|
||||
var items = GetItems(request.UserId).Where(i => i.People != null && i.People.Any(p => string.Equals(p.Name, name, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||
|
||||
var counts = new ItemByNameCounts
|
||||
{
|
||||
|
|
|
@ -94,7 +94,7 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>Task{BaseItemDto}.</returns>
|
||||
private async Task<BaseItemDto> GetItem(GetStudio request)
|
||||
{
|
||||
var item = await LibraryManager.GetStudio(request.Name).ConfigureAwait(false);
|
||||
var item = await GetStudio(request.Name, LibraryManager).ConfigureAwait(false);
|
||||
|
||||
// Get everything
|
||||
var fields = Enum.GetNames(typeof(ItemFields)).Select(i => (ItemFields)Enum.Parse(typeof(ItemFields), i, true));
|
||||
|
@ -118,7 +118,9 @@ namespace MediaBrowser.Api.UserLibrary
|
|||
/// <returns>System.Object.</returns>
|
||||
public object Get(GetStudioItemCounts request)
|
||||
{
|
||||
var items = GetItems(request.UserId).Where(i => i.Studios != null && i.Studios.Contains(request.Name, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
var name = DeSlugStudioName(request.Name, LibraryManager);
|
||||
|
||||
var items = GetItems(request.UserId).Where(i => i.Studios != null && i.Studios.Contains(name, StringComparer.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
var counts = new ItemByNameCounts
|
||||
{
|
||||
|
|
|
@ -8,7 +8,6 @@ using MediaBrowser.Model.Serialization;
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
@ -29,33 +28,6 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
|
||||
protected readonly IJsonSerializer JsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the FF probe cache.
|
||||
/// </summary>
|
||||
/// <value>The FF probe cache.</value>
|
||||
protected FileSystemRepository FFProbeCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
FFProbeCache = new FileSystemRepository(Path.Combine(ConfigurationManager.ApplicationPaths.CachePath, CacheDirectoryName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The name of the cache directory.</value>
|
||||
protected virtual string CacheDirectoryName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "ffmpeg-video-info";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the priority.
|
||||
/// </summary>
|
||||
|
@ -86,7 +58,7 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
{
|
||||
OnPreFetch(myItem, isoMount);
|
||||
|
||||
var result = await GetMediaInfo(item, isoMount, item.DateModified, FFProbeCache, cancellationToken).ConfigureAwait(false);
|
||||
var result = await GetMediaInfo(item, isoMount, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
@ -123,39 +95,15 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="isoMount">The iso mount.</param>
|
||||
/// <param name="lastDateModified">The last date modified.</param>
|
||||
/// <param name="cache">The cache.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{MediaInfoResult}.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">inputPath
|
||||
/// or
|
||||
/// cache</exception>
|
||||
private async Task<MediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, DateTime lastDateModified, FileSystemRepository cache, CancellationToken cancellationToken)
|
||||
private async Task<MediaInfoResult> GetMediaInfo(BaseItem item, IIsoMount isoMount, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cache == null)
|
||||
{
|
||||
throw new ArgumentNullException("cache");
|
||||
}
|
||||
|
||||
// Put the ffmpeg version into the cache name so that it's unique per-version
|
||||
// We don't want to try and deserialize data based on an old version, which could potentially fail
|
||||
var resourceName = item.Id + "_" + lastDateModified.Ticks + "_" + MediaEncoder.Version;
|
||||
|
||||
// Forumulate the cache file path
|
||||
var cacheFilePath = cache.GetResourcePath(resourceName, ".js");
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Avoid File.Exists by just trying to deserialize
|
||||
try
|
||||
{
|
||||
return JsonSerializer.DeserializeFromFile<MediaInfoResult>(cacheFilePath);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
// Cache file doesn't exist
|
||||
}
|
||||
|
||||
var type = InputType.AudioFile;
|
||||
var inputPath = isoMount == null ? new[] { item.Path } : new[] { isoMount.MountedPath };
|
||||
|
||||
|
@ -166,11 +114,7 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
inputPath = MediaEncoderHelpers.GetInputArgument(video, isoMount, out type);
|
||||
}
|
||||
|
||||
var info = await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
JsonSerializer.SerializeToFile(info, cacheFilePath);
|
||||
|
||||
return info;
|
||||
return await MediaEncoder.GetMediaInfo(inputPath, type, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -23,18 +23,6 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the cache directory.
|
||||
/// </summary>
|
||||
/// <value>The name of the cache directory.</value>
|
||||
protected override string CacheDirectoryName
|
||||
{
|
||||
get
|
||||
{
|
||||
return "ffmpeg-audio-info";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the specified audio.
|
||||
/// </summary>
|
||||
|
|
|
@ -35,16 +35,8 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
|
||||
_blurayExaminer = blurayExaminer;
|
||||
_isoManager = isoManager;
|
||||
|
||||
BdInfoCache = new FileSystemRepository(Path.Combine(ConfigurationManager.ApplicationPaths.CachePath, "bdinfo"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bd info cache.
|
||||
/// </summary>
|
||||
/// <value>The bd info cache.</value>
|
||||
private FileSystemRepository BdInfoCache { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bluray examiner.
|
||||
/// </summary>
|
||||
|
@ -231,7 +223,7 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
if (video.VideoType == VideoType.BluRay || (video.IsoType.HasValue && video.IsoType.Value == IsoType.BluRay))
|
||||
{
|
||||
var inputPath = isoMount != null ? isoMount.MountedPath : video.Path;
|
||||
FetchBdInfo(video, inputPath, BdInfoCache, cancellationToken);
|
||||
FetchBdInfo(video, inputPath, cancellationToken);
|
||||
}
|
||||
|
||||
AddExternalSubtitles(video);
|
||||
|
@ -334,29 +326,12 @@ namespace MediaBrowser.Controller.Providers.MediaInfo
|
|||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="bdInfoCache">The bd info cache.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
private void FetchBdInfo(BaseItem item, string inputPath, FileSystemRepository bdInfoCache, CancellationToken cancellationToken)
|
||||
private void FetchBdInfo(BaseItem item, string inputPath, CancellationToken cancellationToken)
|
||||
{
|
||||
var video = (Video)item;
|
||||
|
||||
// Get the path to the cache file
|
||||
var cacheName = item.Id + "_" + item.DateModified.Ticks;
|
||||
|
||||
var cacheFile = bdInfoCache.GetResourcePath(cacheName, ".js");
|
||||
|
||||
BlurayDiscInfo result;
|
||||
|
||||
try
|
||||
{
|
||||
result = JsonSerializer.DeserializeFromFile<BlurayDiscInfo>(cacheFile);
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
result = GetBDInfo(inputPath);
|
||||
|
||||
JsonSerializer.SerializeToFile(result, cacheFile);
|
||||
}
|
||||
var result = GetBDInfo(inputPath);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
|
|
|
@ -97,6 +97,10 @@ MediaBrowser.ApiClient = function ($, navigator, JSON, WebSocket, setTimeout) {
|
|||
|
||||
self.encodeName = function (name) {
|
||||
|
||||
name = name.split('/').join('-');
|
||||
|
||||
name = name.split('?').join('-');
|
||||
|
||||
var val = $.param({ name: name });
|
||||
return val.substring(val.indexOf('=') + 1).replace("'", '%27');
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.105" targetFramework="net45" />
|
||||
<package id="MediaBrowser.ApiClient.Javascript" version="3.0.108" targetFramework="net45" />
|
||||
<package id="ServiceStack.Common" version="3.9.44" targetFramework="net45" />
|
||||
<package id="ServiceStack.Text" version="3.9.44" targetFramework="net45" />
|
||||
</packages>
|
Loading…
Reference in New Issue
Block a user