commit
5e879484e9
|
@ -10,22 +10,27 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using System.Globalization;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace Emby.Drawing.Skia
|
||||
{
|
||||
public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private static IApplicationPaths _appPaths;
|
||||
private readonly Func<IHttpClient> _httpClientFactory;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private static ILocalizationManager _localizationManager;
|
||||
|
||||
public SkiaEncoder(ILogger logger, IApplicationPaths appPaths, Func<IHttpClient> httpClientFactory, IFileSystem fileSystem)
|
||||
public SkiaEncoder(ILogger logger, IApplicationPaths appPaths, Func<IHttpClient> httpClientFactory, IFileSystem fileSystem, ILocalizationManager localizationManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_appPaths = appPaths;
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_fileSystem = fileSystem;
|
||||
_localizationManager = localizationManager;
|
||||
|
||||
LogVersion();
|
||||
}
|
||||
|
@ -190,14 +195,53 @@ namespace Emby.Drawing.Skia
|
|||
}
|
||||
}
|
||||
|
||||
private static string[] TransparentImageTypes = new string[] { ".png", ".gif", ".webp" };
|
||||
internal static SKBitmap Decode(string path, bool forceCleanBitmap, out SKCodecOrigin origin)
|
||||
private static bool HasDiacritics(string text)
|
||||
{
|
||||
return !String.Equals(text, text.RemoveDiacritics(), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool RequiresSpecialCharacterHack(string path)
|
||||
{
|
||||
if (_localizationManager.HasUnicodeCategory(path, UnicodeCategory.OtherLetter))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (HasDiacritics(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path, IFileSystem fileSystem)
|
||||
{
|
||||
if (!RequiresSpecialCharacterHack(path))
|
||||
{
|
||||
return path;
|
||||
}
|
||||
|
||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path) ?? string.Empty);
|
||||
|
||||
fileSystem.CopyFile(path, tempPath, true);
|
||||
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
private static string[] TransparentImageTypes = new string[] { ".png", ".gif", ".webp" };
|
||||
internal static SKBitmap Decode(string path, bool forceCleanBitmap, IFileSystem fileSystem, out SKCodecOrigin origin)
|
||||
{
|
||||
if (!fileSystem.FileExists(path))
|
||||
{
|
||||
throw new FileNotFoundException("File not found", path);
|
||||
}
|
||||
|
||||
var requiresTransparencyHack = TransparentImageTypes.Contains(Path.GetExtension(path) ?? string.Empty);
|
||||
|
||||
if (requiresTransparencyHack || forceCleanBitmap)
|
||||
{
|
||||
using (var stream = new SKFileStream(path))
|
||||
using (var stream = new SKFileStream(NormalizePath(path, fileSystem)))
|
||||
{
|
||||
using (var codec = SKCodec.Create(stream))
|
||||
{
|
||||
|
@ -227,11 +271,11 @@ namespace Emby.Drawing.Skia
|
|||
}
|
||||
}
|
||||
|
||||
var resultBitmap = SKBitmap.Decode(path);
|
||||
var resultBitmap = SKBitmap.Decode(NormalizePath(path, fileSystem));
|
||||
|
||||
if (resultBitmap == null)
|
||||
{
|
||||
return Decode(path, true, out origin);
|
||||
return Decode(path, true, fileSystem, out origin);
|
||||
}
|
||||
|
||||
// If we have to resize these they often end up distorted
|
||||
|
@ -239,7 +283,7 @@ namespace Emby.Drawing.Skia
|
|||
{
|
||||
using (resultBitmap)
|
||||
{
|
||||
return Decode(path, true, out origin);
|
||||
return Decode(path, true, fileSystem, out origin);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,13 +295,13 @@ namespace Emby.Drawing.Skia
|
|||
{
|
||||
if (cropWhitespace)
|
||||
{
|
||||
using (var bitmap = Decode(path, forceAnalyzeBitmap, out origin))
|
||||
using (var bitmap = Decode(path, forceAnalyzeBitmap, _fileSystem, out origin))
|
||||
{
|
||||
return CropWhiteSpace(bitmap);
|
||||
}
|
||||
}
|
||||
|
||||
return Decode(path, forceAnalyzeBitmap, out origin);
|
||||
return Decode(path, forceAnalyzeBitmap, _fileSystem, out origin);
|
||||
}
|
||||
|
||||
private SKBitmap GetBitmap(string path, bool cropWhitespace, bool autoOrient)
|
||||
|
|
|
@ -83,7 +83,7 @@ namespace Emby.Drawing.Skia
|
|||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
SKCodecOrigin origin;
|
||||
using (var currentBitmap = SkiaEncoder.Decode(paths[imageIndex], false, out origin))
|
||||
using (var currentBitmap = SkiaEncoder.Decode(paths[imageIndex], false, _fileSystem, out origin))
|
||||
{
|
||||
// resize to the same aspect as the original
|
||||
int iWidth = (int)Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
|
||||
|
@ -165,7 +165,7 @@ namespace Emby.Drawing.Skia
|
|||
for (var y = 0; y < 2; y++)
|
||||
{
|
||||
SKCodecOrigin origin;
|
||||
using (var currentBitmap = SkiaEncoder.Decode(paths[imageIndex], false, out origin))
|
||||
using (var currentBitmap = SkiaEncoder.Decode(paths[imageIndex], false, _fileSystem, out origin))
|
||||
{
|
||||
using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType))
|
||||
{
|
||||
|
|
|
@ -195,52 +195,49 @@ namespace Emby.Server.Implementations.Configuration
|
|||
}
|
||||
}
|
||||
|
||||
public void DisableMetadataService(string service)
|
||||
public bool SetOptimalValues()
|
||||
{
|
||||
DisableMetadataService(typeof(Movie), Configuration, service);
|
||||
DisableMetadataService(typeof(Episode), Configuration, service);
|
||||
DisableMetadataService(typeof(Series), Configuration, service);
|
||||
DisableMetadataService(typeof(Season), Configuration, service);
|
||||
DisableMetadataService(typeof(MusicArtist), Configuration, service);
|
||||
DisableMetadataService(typeof(MusicAlbum), Configuration, service);
|
||||
DisableMetadataService(typeof(MusicVideo), Configuration, service);
|
||||
DisableMetadataService(typeof(Video), Configuration, service);
|
||||
var config = Configuration;
|
||||
|
||||
var changed = false;
|
||||
|
||||
if (!config.EnableCaseSensitiveItemIds)
|
||||
{
|
||||
config.EnableCaseSensitiveItemIds = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
private void DisableMetadataService(Type type, ServerConfiguration config, string service)
|
||||
if (!config.SkipDeserializationForBasicTypes)
|
||||
{
|
||||
var options = GetMetadataOptions(type, config);
|
||||
|
||||
if (!options.DisabledMetadataSavers.Contains(service, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var list = options.DisabledMetadataSavers.ToList();
|
||||
|
||||
list.Add(service);
|
||||
|
||||
options.DisabledMetadataSavers = list.ToArray(list.Count);
|
||||
}
|
||||
config.SkipDeserializationForBasicTypes = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
private MetadataOptions GetMetadataOptions(Type type, ServerConfiguration config)
|
||||
if (!config.EnableSimpleArtistDetection)
|
||||
{
|
||||
var options = config.MetadataOptions
|
||||
.FirstOrDefault(i => string.Equals(i.ItemType, type.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (options == null)
|
||||
{
|
||||
var list = config.MetadataOptions.ToList();
|
||||
|
||||
options = new MetadataOptions
|
||||
{
|
||||
ItemType = type.Name
|
||||
};
|
||||
|
||||
list.Add(options);
|
||||
|
||||
config.MetadataOptions = list.ToArray(list.Count);
|
||||
config.EnableSimpleArtistDetection = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
if (!config.EnableNormalizedItemByNameIds)
|
||||
{
|
||||
config.EnableNormalizedItemByNameIds = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.DisableLiveTvChannelUserDataName)
|
||||
{
|
||||
config.DisableLiveTvChannelUserDataName = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!config.EnableNewOmdbSupport)
|
||||
{
|
||||
config.EnableNewOmdbSupport = true;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5298,7 +5298,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
|
|||
OfficialRatings = query.OfficialRatings,
|
||||
GenreIds = query.GenreIds,
|
||||
Genres = query.Genres,
|
||||
Years = query.Years
|
||||
Years = query.Years,
|
||||
NameContains = query.NameContains
|
||||
};
|
||||
|
||||
var outerWhereClauses = GetWhereClauses(outerQuery, null);
|
||||
|
|
|
@ -160,7 +160,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
|
|||
ProductionYear = video.Year,
|
||||
Name = parseName ?
|
||||
video.Name :
|
||||
Path.GetFileName(video.Files[0].Path),
|
||||
Path.GetFileNameWithoutExtension(video.Files[0].Path),
|
||||
AdditionalParts = video.Files.Skip(1).Select(i => i.Path).ToArray(),
|
||||
LocalAlternateVersions = video.AlternateVersions.Select(i => i.Path).ToArray()
|
||||
};
|
||||
|
|
|
@ -99,8 +99,6 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
var terms = GetWords(searchTerm);
|
||||
|
||||
var hints = new List<Tuple<BaseItem, string, int>>();
|
||||
|
||||
var excludeItemTypes = query.ExcludeItemTypes.ToList();
|
||||
var includeItemTypes = (query.IncludeItemTypes ?? new string[] { }).ToList();
|
||||
|
||||
|
@ -161,8 +159,15 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
AddIfMissing(excludeItemTypes, typeof(CollectionFolder).Name);
|
||||
AddIfMissing(excludeItemTypes, typeof(Folder).Name);
|
||||
var mediaTypes = query.MediaTypes.ToList();
|
||||
|
||||
var mediaItems = _libraryManager.GetItemList(new InternalItemsQuery(user)
|
||||
if (includeItemTypes.Count > 0)
|
||||
{
|
||||
excludeItemTypes.Clear();
|
||||
mediaTypes.Clear();
|
||||
}
|
||||
|
||||
var searchQuery = new InternalItemsQuery(user)
|
||||
{
|
||||
NameContains = searchTerm,
|
||||
ExcludeItemTypes = excludeItemTypes.ToArray(excludeItemTypes.Count),
|
||||
|
@ -178,7 +183,7 @@ namespace Emby.Server.Implementations.Library
|
|||
IsNews = query.IsNews,
|
||||
IsSeries = query.IsSeries,
|
||||
IsSports = query.IsSports,
|
||||
MediaTypes = query.MediaTypes,
|
||||
MediaTypes = mediaTypes.ToArray(),
|
||||
|
||||
DtoOptions = new DtoOptions
|
||||
{
|
||||
|
@ -189,17 +194,33 @@ namespace Emby.Server.Implementations.Library
|
|||
ItemFields.ChannelInfo
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Add search hints based on item name
|
||||
hints.AddRange(mediaItems.Select(item =>
|
||||
List<BaseItem> mediaItems;
|
||||
|
||||
if (searchQuery.IncludeItemTypes.Length == 1 && string.Equals(searchQuery.IncludeItemTypes[0], "MusicArtist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (searchQuery.ParentId.HasValue)
|
||||
{
|
||||
searchQuery.AncestorIds = new string[] { searchQuery.ParentId.Value.ToString("N") };
|
||||
}
|
||||
searchQuery.ParentId = null;
|
||||
searchQuery.IncludeItemsByName = true;
|
||||
searchQuery.IncludeItemTypes = new string[] { };
|
||||
mediaItems = _libraryManager.GetArtists(searchQuery).Items.Select(i => i.Item1).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaItems = _libraryManager.GetItemList(searchQuery);
|
||||
}
|
||||
|
||||
var returnValue = mediaItems.Select(item =>
|
||||
{
|
||||
var index = GetIndex(item.Name, searchTerm, terms);
|
||||
|
||||
return new Tuple<BaseItem, string, int>(item, index.Item1, index.Item2);
|
||||
}));
|
||||
|
||||
var returnValue = hints.Where(i => i.Item3 >= 0).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).Select(i => new SearchHintInfo
|
||||
}).OrderBy(i => i.Item3).ThenBy(i => i.Item1.SortName).Select(i => new SearchHintInfo
|
||||
{
|
||||
Item = i.Item1,
|
||||
MatchedTerm = i.Item2
|
||||
|
|
|
@ -308,6 +308,19 @@ namespace Emby.Server.Implementations.Localization
|
|||
return value == null ? (int?)null : value.Value;
|
||||
}
|
||||
|
||||
public bool HasUnicodeCategory(string value, UnicodeCategory category)
|
||||
{
|
||||
foreach (var chr in value)
|
||||
{
|
||||
if (char.GetUnicodeCategory(chr) == category)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public string GetLocalizedString(string phrase)
|
||||
{
|
||||
return GetLocalizedString(phrase, _configurationManager.Configuration.UICulture);
|
||||
|
|
|
@ -134,8 +134,6 @@ namespace MediaBrowser.Api
|
|||
|
||||
public void Post(AutoSetMetadataOptions request)
|
||||
{
|
||||
_configurationManager.DisableMetadataService("Emby Xml");
|
||||
_configurationManager.SaveConfiguration();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace MediaBrowser.Api
|
|||
public void Post(ReportStartupWizardComplete request)
|
||||
{
|
||||
_config.Configuration.IsStartupWizardCompleted = true;
|
||||
SetWizardFinishValues(_config.Configuration);
|
||||
_config.SetOptimalValues();
|
||||
_config.SaveConfiguration();
|
||||
}
|
||||
|
||||
|
@ -87,16 +87,6 @@ namespace MediaBrowser.Api
|
|||
return result;
|
||||
}
|
||||
|
||||
private void SetWizardFinishValues(ServerConfiguration config)
|
||||
{
|
||||
config.EnableCaseSensitiveItemIds = true;
|
||||
config.SkipDeserializationForBasicTypes = true;
|
||||
config.EnableSimpleArtistDetection = true;
|
||||
config.EnableNormalizedItemByNameIds = true;
|
||||
config.DisableLiveTvChannelUserDataName = true;
|
||||
config.EnableNewOmdbSupport = true;
|
||||
}
|
||||
|
||||
public void Post(UpdateStartupConfiguration request)
|
||||
{
|
||||
_config.Configuration.UICulture = request.UICulture;
|
||||
|
|
|
@ -20,10 +20,6 @@ namespace MediaBrowser.Controller.Configuration
|
|||
/// <value>The configuration.</value>
|
||||
ServerConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the preferred metadata service.
|
||||
/// </summary>
|
||||
/// <param name="service">The service.</param>
|
||||
void DisableMetadataService(string service);
|
||||
bool SetOptimalValues();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
using System;
|
||||
|
||||
namespace MediaBrowser.Model.Dlna
|
||||
{
|
||||
public class PlaybackException : Exception
|
||||
{
|
||||
public PlaybackErrorCode ErrorCode { get; set;}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.Model.Globalization
|
||||
{
|
||||
|
@ -54,5 +55,7 @@ namespace MediaBrowser.Model.Globalization
|
|||
string RemoveDiacritics(string text);
|
||||
|
||||
string NormalizeFormKD(string text);
|
||||
|
||||
bool HasUnicodeCategory(string value, UnicodeCategory category);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,7 +98,6 @@
|
|||
<Compile Include="Dlna\ResponseProfile.cs" />
|
||||
<Compile Include="Dlna\StreamInfoSorter.cs" />
|
||||
<Compile Include="Dlna\PlaybackErrorCode.cs" />
|
||||
<Compile Include="Dlna\PlaybackException.cs" />
|
||||
<Compile Include="Dlna\ResolutionConfiguration.cs" />
|
||||
<Compile Include="Dlna\ResolutionNormalizer.cs" />
|
||||
<Compile Include="Dlna\ResolutionOptions.cs" />
|
||||
|
|
|
@ -163,7 +163,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||
|
||||
private void FetchShortcutInfo(Video video)
|
||||
{
|
||||
video.ShortcutPath = _fileSystem.ReadAllText(video.Path);
|
||||
video.ShortcutPath = _fileSystem.ReadAllText(video.Path)
|
||||
.Replace("\t", string.Empty)
|
||||
.Replace("\r", string.Empty)
|
||||
.Replace("\n", string.Empty)
|
||||
.Trim();
|
||||
}
|
||||
|
||||
public Task<ItemUpdateType> FetchAudioInfo<T>(T item, CancellationToken cancellationToken)
|
||||
|
|
|
@ -9,6 +9,7 @@ using MediaBrowser.Model.IO;
|
|||
using MediaBrowser.Model.Logging;
|
||||
using Emby.Drawing.Skia;
|
||||
using MediaBrowser.Model.System;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Server.Startup.Common
|
||||
{
|
||||
|
@ -20,13 +21,14 @@ namespace MediaBrowser.Server.Startup.Common
|
|||
StartupOptions startupOptions,
|
||||
Func<IHttpClient> httpClient,
|
||||
IApplicationPaths appPaths,
|
||||
IEnvironmentInfo environment)
|
||||
IEnvironmentInfo environment,
|
||||
ILocalizationManager localizationManager)
|
||||
{
|
||||
if (!startupOptions.ContainsOption("-enablegdi"))
|
||||
{
|
||||
try
|
||||
{
|
||||
return new SkiaEncoder(logManager.GetLogger("Skia"), appPaths, httpClient, fileSystem);
|
||||
return new SkiaEncoder(logManager.GetLogger("Skia"), appPaths, httpClient, fileSystem, localizationManager);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
@ -127,7 +127,7 @@ namespace MediaBrowser.Server.Mono
|
|||
|
||||
Task.WaitAll(task);
|
||||
|
||||
appHost.ImageProcessor.ImageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo);
|
||||
appHost.ImageProcessor.ImageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => appHost.HttpClient, appPaths, environmentInfo, appHost.LocalizationManager);
|
||||
|
||||
Console.WriteLine("Running startup tasks");
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ using MediaBrowser.Common.Net;
|
|||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Logging;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
|
||||
namespace MediaBrowser.Server.Startup.Common
|
||||
{
|
||||
|
@ -17,11 +18,12 @@ namespace MediaBrowser.Server.Startup.Common
|
|||
IFileSystem fileSystem,
|
||||
StartupOptions startupOptions,
|
||||
Func<IHttpClient> httpClient,
|
||||
IApplicationPaths appPaths)
|
||||
IApplicationPaths appPaths,
|
||||
ILocalizationManager localizationManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new SkiaEncoder(logManager.GetLogger("Skia"), appPaths, httpClient, fileSystem);
|
||||
return new SkiaEncoder(logManager.GetLogger("Skia"), appPaths, httpClient, fileSystem, localizationManager);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
|
@ -304,6 +304,19 @@ namespace MediaBrowser.ServerApplication
|
|||
}
|
||||
}
|
||||
|
||||
private static string UpdatePackageFileName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Environment.Is64BitOperatingSystem)
|
||||
{
|
||||
return "embyserver-win-x64-{version}.zip";
|
||||
}
|
||||
|
||||
return "embyserver-win-x86-{version}.zip";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the application.
|
||||
/// </summary>
|
||||
|
@ -324,7 +337,7 @@ namespace MediaBrowser.ServerApplication
|
|||
options,
|
||||
fileSystem,
|
||||
new PowerManagement(),
|
||||
"emby.windows.zip",
|
||||
UpdatePackageFileName,
|
||||
environmentInfo,
|
||||
new NullImageEncoder(),
|
||||
new SystemEvents(logManager.GetLogger("SystemEvents")),
|
||||
|
@ -355,7 +368,7 @@ namespace MediaBrowser.ServerApplication
|
|||
}
|
||||
|
||||
// set image encoder here
|
||||
appHost.ImageProcessor.ImageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => appHost.HttpClient, appPaths);
|
||||
appHost.ImageProcessor.ImageEncoder = ImageEncoderHelper.GetImageEncoder(_logger, logManager, fileSystem, options, () => appHost.HttpClient, appPaths, appHost.LocalizationManager);
|
||||
|
||||
task = task.ContinueWith(new Action<Task>(a => appHost.RunStartupTasks()), TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.AttachedToParent);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user