AA.Compare Project to Match not runToDoCollectionFirst
Removed Layered AppSettings with Nested Objects at First Level
This commit is contained in:
50
Compare/AA.Compare.csproj
Normal file
50
Compare/AA.Compare.csproj
Normal file
@ -0,0 +1,50 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<UserSecretsId>770b6ae3-266e-4d5f-970a-173709b064de</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>Phares.View.by.Distance.Compare</PackageId>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<Version>9.0.100.0</Version>
|
||||
<Authors>Mike Phares</Authors>
|
||||
<Company>Phares</Company>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<IsWindows Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true'">true</IsWindows>
|
||||
<IsOSX Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">true</IsOSX>
|
||||
<IsLinux Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">true</IsLinux>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsWindows)'=='true'">
|
||||
<DefineConstants>Windows</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsOSX)'=='true'">
|
||||
<DefineConstants>OSX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsLinux)'=='true'">
|
||||
<DefineConstants>Linux</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'browser-wasm'">
|
||||
<SupportedPlatform Include="browser" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="runtime.win-x64.Microsoft.DotNet.ILCompiler" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageReference Include="ShellProgressBar" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Face\AA.Face.csproj" />
|
||||
<ProjectReference Include="..\Distance\AA.Distance.csproj" />
|
||||
<ProjectReference Include="..\Metadata\AA.Metadata.csproj" />
|
||||
<ProjectReference Include="..\People\AA.People.csproj" />
|
||||
<ProjectReference Include="..\Shared\AA.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
168
Compare/Compare.cs
Normal file
168
Compare/Compare.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ShellProgressBar;
|
||||
using System.Collections.ObjectModel;
|
||||
using View_by_Distance.Compare.Models;
|
||||
using View_by_Distance.Distance.Models.Stateless.Methods;
|
||||
using View_by_Distance.Face.Models.Stateless.Methods;
|
||||
using View_by_Distance.Metadata.Models;
|
||||
using View_by_Distance.People.Models.Stateless.Methods;
|
||||
using View_by_Distance.Shared.Models;
|
||||
using View_by_Distance.Shared.Models.Stateless.Methods;
|
||||
|
||||
namespace View_by_Distance.Compare;
|
||||
|
||||
public partial class Compare : ICompare, IDisposable
|
||||
{
|
||||
|
||||
private ProgressBar? _ProgressBar;
|
||||
private readonly ProgressBarOptions _ProgressBarOptions;
|
||||
|
||||
public Compare(List<string> args, ILogger<Program>? logger, AppSettings appSettings, bool isSilent, IConsole console)
|
||||
{
|
||||
if (isSilent)
|
||||
{ }
|
||||
if (args is null)
|
||||
throw new NullReferenceException(nameof(args));
|
||||
if (console is null)
|
||||
throw new NullReferenceException(nameof(console));
|
||||
ICompare compare = this;
|
||||
long ticks = DateTime.Now.Ticks;
|
||||
_ProgressBarOptions = new() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true };
|
||||
CompareWork(logger, appSettings, compare, ticks);
|
||||
}
|
||||
|
||||
void ICompare.Tick() =>
|
||||
_ProgressBar?.Tick();
|
||||
|
||||
void ICompare.ConstructProgressBar(int maxTicks, string message)
|
||||
{
|
||||
_ProgressBar?.Dispose();
|
||||
_ProgressBar = new(maxTicks, message, _ProgressBarOptions);
|
||||
}
|
||||
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
_ProgressBar?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private static bool GetRunToDoCollectionFirst(AppSettings appSettings, long ticks)
|
||||
{
|
||||
bool result = appSettings.DistanceSettings.SaveSortingWithoutPerson;
|
||||
if (!result)
|
||||
result = !IId.IsOffsetDeterministicHashCode(appSettings.MetadataSettings);
|
||||
if (!result)
|
||||
{
|
||||
string[] directories;
|
||||
directories = Directory.GetDirectories(appSettings.ResultSettings.RootDirectory, "*", SearchOption.TopDirectoryOnly);
|
||||
if (directories.Length == 0)
|
||||
result = true;
|
||||
else
|
||||
{
|
||||
string seasonDirectory;
|
||||
DirectoryInfo directoryInfo;
|
||||
DateTime dateTime = new(ticks);
|
||||
string rootDirectory = appSettings.ResultSettings.RootDirectory;
|
||||
(int season, string seasonName) = IDate.GetSeason(dateTime.DayOfYear);
|
||||
string eDistanceContentDirectory = IResult.GetResultsDateGroupDirectory(appSettings.ResultSettings, nameof(E_Distance), appSettings.ResultSettings.ResultContent);
|
||||
FileSystemInfo fileSystemInfo = new DirectoryInfo(eDistanceContentDirectory);
|
||||
string[] checkDirectories =
|
||||
[
|
||||
Path.Combine(rootDirectory, "Ancestry"),
|
||||
Path.Combine(rootDirectory, "Facebook"),
|
||||
Path.Combine(rootDirectory, "LinkedIn")
|
||||
];
|
||||
foreach (string checkDirectory in checkDirectories)
|
||||
{
|
||||
if (checkDirectory == rootDirectory)
|
||||
seasonDirectory = Path.Combine(checkDirectory, $"{dateTime.Year}.{season} {seasonName}");
|
||||
else
|
||||
seasonDirectory = Path.Combine(checkDirectory, $"{dateTime.Year}.{season} {seasonName} {Path.GetFileName(checkDirectory)}");
|
||||
if (!Directory.Exists(seasonDirectory))
|
||||
_ = Directory.CreateDirectory(seasonDirectory);
|
||||
if (result)
|
||||
continue;
|
||||
directories = Directory.GetDirectories(checkDirectory, "*", SearchOption.TopDirectoryOnly);
|
||||
foreach (string directory in directories)
|
||||
{
|
||||
directoryInfo = new(directory);
|
||||
if (directoryInfo.LastWriteTime > fileSystemInfo.LastWriteTime)
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result)
|
||||
result = true;
|
||||
if (!result)
|
||||
result = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ReadOnlyCollections GetReadOnlyCollections(AppSettings appSettings)
|
||||
{
|
||||
ReadOnlyCollections result;
|
||||
ReadOnlyCollection<PersonContainer> personContainers = IPeople.GetPersonContainers(appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.PeopleSettings, appSettings.CompareSettings);
|
||||
ReadOnlyCollection<long> personKeys = IPeople.GetPersonKeys(personContainers);
|
||||
result = IPeople.GetReadOnlyCollections(appSettings.ResultSettings, appSettings.PeopleSettings, appSettings.DistanceSettings, appSettings.CompareSettings, personContainers, personKeys);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ReadOnlyCollection<ExifDirectory> GetMappedExifDirectoryWithEncoding(AppSettings appSettings, ICompare compare, long ticks, ReadOnlyCollections readOnlyCollections)
|
||||
{
|
||||
ReadOnlyCollection<ExifDirectory> results;
|
||||
ReadOnlyCollection<ExifDirectory> exifDirectories = IDistance.GetMapped(appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.PeopleSettings, appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, readOnlyCollections);
|
||||
if (exifDirectories.Count == 0 && !appSettings.DistanceSettings.SaveSortingWithoutPerson)
|
||||
throw new NotSupportedException($"Switch {nameof(appSettings.DistanceSettings.SaveSortingWithoutPerson)}!");
|
||||
results = IDistance.GetMappedExifDirectoryWithEncoding(compare, ticks, exifDirectories);
|
||||
if (results.Count == 0 && !appSettings.DistanceSettings.SaveSortingWithoutPerson)
|
||||
throw new NotSupportedException($"Switch {nameof(appSettings.DistanceSettings.SaveSortingWithoutPerson)}!");
|
||||
return results;
|
||||
}
|
||||
|
||||
private void CompareWork(ILogger<Program>? logger, AppSettings appSettings, ICompare compare, long ticks)
|
||||
{
|
||||
const int updated = 0;
|
||||
DistanceLimits? distanceLimits;
|
||||
logger?.LogInformation("{Ticks}", ticks);
|
||||
ReadOnlyCollection<LocationContainer> matrix;
|
||||
ReadOnlyCollection<SaveContainer> saveContainers;
|
||||
ReadOnlyCollection<ExifDirectory> exifDirectories;
|
||||
ReadOnlyCollection<LocationContainer> preFiltered;
|
||||
ReadOnlyCollection<LocationContainer> postFiltered;
|
||||
ReadOnlyDictionary<string, LocationContainer> onlyOne;
|
||||
bool runToDoCollectionFirst = GetRunToDoCollectionFirst(appSettings, ticks);
|
||||
ReadOnlyCollections readOnlyCollections = GetReadOnlyCollections(appSettings);
|
||||
ReadOnlyCollection<ExifDirectory> mappedExifDirectoryWithEncoding = GetMappedExifDirectoryWithEncoding(appSettings, compare, ticks, readOnlyCollections);
|
||||
ReadOnlyDictionary<int, ReadOnlyDictionary<int, FilePath>> keyValuePairs = IDistance.Extract(appSettings.CompareSettings, mappedExifDirectoryWithEncoding);
|
||||
foreach (string outputResolution in appSettings.CompareSettings.OutputResolutions)
|
||||
{
|
||||
if (runToDoCollectionFirst || outputResolution.Any(char.IsNumber))
|
||||
continue;
|
||||
_ProgressBar?.Dispose();
|
||||
logger?.LogInformation("{outputResolution}", outputResolution);
|
||||
exifDirectories = IFace.GetExifDirectories(appSettings.ResultSettings, appSettings.MetadataSettings, appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, outputResolution);
|
||||
preFiltered = IDistance.GetPreFilterLocationContainer(appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, readOnlyCollections, keyValuePairs, exifDirectories);
|
||||
if (preFiltered.Count == 0)
|
||||
continue;
|
||||
distanceLimits = new(appSettings.DistanceSettings);
|
||||
postFiltered = IDistance.GetPostFilterLocationContainer(preFiltered, distanceLimits);
|
||||
if (postFiltered.Count == 0)
|
||||
continue;
|
||||
matrix = IDistance.GetMatrixLocationContainers(appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, mappedExifDirectoryWithEncoding, distanceLimits, postFiltered);
|
||||
if (matrix.Count == 0)
|
||||
continue;
|
||||
onlyOne = IDistance.GetOnlyOne(appSettings.DistanceSettings, matrix);
|
||||
if (onlyOne.Count == 0)
|
||||
continue;
|
||||
saveContainers = IDistance.GetSaveContainers(appSettings.ResultSettings, appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, outputResolution, onlyOne);
|
||||
if (saveContainers.Count == 0)
|
||||
continue;
|
||||
IDistance.SaveContainers(appSettings.DistanceSettings, appSettings.CompareSettings, compare, ticks, updated, saveContainers);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
78
Compare/Models/AppSettings.cs
Normal file
78
Compare/Models/AppSettings.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using View_by_Distance.Shared.Models;
|
||||
|
||||
namespace View_by_Distance.Compare.Models;
|
||||
|
||||
public record AppSettings(ResultSettings ResultSettings,
|
||||
MetadataSettings MetadataSettings,
|
||||
PeopleSettings PeopleSettings,
|
||||
DistanceSettings DistanceSettings,
|
||||
CompareSettings CompareSettings)
|
||||
{
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string result = JsonSerializer.Serialize(this, AppSettingsSourceGenerationContext.Default.AppSettings);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Verify(AppSettings appSettings)
|
||||
{
|
||||
if (appSettings.DistanceSettings.RangeDaysDeltaTolerance.Length != 3)
|
||||
throw new NullReferenceException(nameof(appSettings.DistanceSettings.RangeDaysDeltaTolerance));
|
||||
if (appSettings.DistanceSettings.RangeDistanceTolerance.Length != 3)
|
||||
throw new NullReferenceException(nameof(appSettings.DistanceSettings.RangeDistanceTolerance));
|
||||
if (appSettings.DistanceSettings.RangeFaceAreaTolerance.Length != 3)
|
||||
throw new NullReferenceException(nameof(appSettings.DistanceSettings.RangeFaceAreaTolerance));
|
||||
if (appSettings.DistanceSettings.RangeFaceConfidence.Length != 3)
|
||||
throw new NullReferenceException(nameof(appSettings.DistanceSettings.RangeFaceConfidence));
|
||||
_ = DateTime.Now.AddDays(-appSettings.DistanceSettings.RangeDaysDeltaTolerance[1]);
|
||||
if (appSettings.DistanceSettings.SaveSortingWithoutPerson && appSettings.PeopleSettings.JLinks.Length > 0)
|
||||
throw new Exception("Settings has SaveSortingWithoutPerson and JLinks!");
|
||||
if (appSettings.DistanceSettings.SaveSortingWithoutPerson && !string.IsNullOrEmpty(appSettings.DistanceSettings.FocusModel))
|
||||
throw new Exception("Settings has SaveSortingWithoutPerson and FocusModel!");
|
||||
if (appSettings.DistanceSettings.SaveSortingWithoutPerson && !string.IsNullOrEmpty(appSettings.DistanceSettings.FocusDirectory))
|
||||
throw new Exception("Settings has SaveSortingWithoutPerson and FocusDirectory!");
|
||||
if (appSettings.CompareSettings.MaxDegreeOfParallelism > Environment.ProcessorCount)
|
||||
throw new Exception("MaxDegreeOfParallelism must be =< Environment.ProcessorCount!");
|
||||
if (!string.IsNullOrEmpty(appSettings.DistanceSettings.FocusDirectory) && appSettings.DistanceSettings.FocusDirectory.Length != 2)
|
||||
throw new NotSupportedException($"{nameof(appSettings.DistanceSettings.FocusDirectory)} currently only works with output directory! Example 00.");
|
||||
}
|
||||
|
||||
public static AppSettings Get(IConfigurationRoot configurationRoot)
|
||||
{
|
||||
AppSettings result;
|
||||
#pragma warning disable IL3050, IL2026
|
||||
ResultSettings? resultSettings = configurationRoot.GetSection(nameof(ResultSettings)).Get<ResultSettings>();
|
||||
MetadataSettings? metadataSettings = configurationRoot.GetSection(nameof(MetadataSettings)).Get<MetadataSettings>();
|
||||
PeopleSettings? peopleSettings = configurationRoot.GetSection(nameof(PeopleSettings)).Get<PeopleSettings>();
|
||||
DistanceSettings? distanceSettings = configurationRoot.GetSection(nameof(DistanceSettings)).Get<DistanceSettings>();
|
||||
CompareSettings? compareSettings = configurationRoot.GetSection(nameof(CompareSettings)).Get<CompareSettings>();
|
||||
#pragma warning restore IL3050, IL2026
|
||||
if (resultSettings is null || metadataSettings is null || peopleSettings is null || distanceSettings is null || compareSettings?.Company is null)
|
||||
{
|
||||
List<string> paths = [];
|
||||
foreach (IConfigurationProvider configurationProvider in configurationRoot.Providers)
|
||||
{
|
||||
if (configurationProvider is not Microsoft.Extensions.Configuration.Json.JsonConfigurationProvider jsonConfigurationProvider)
|
||||
continue;
|
||||
if (jsonConfigurationProvider.Source.FileProvider is not Microsoft.Extensions.FileProviders.PhysicalFileProvider physicalFileProvider)
|
||||
continue;
|
||||
paths.Add(physicalFileProvider.Root);
|
||||
}
|
||||
throw new NotSupportedException($"Not found!{Environment.NewLine}{string.Join(Environment.NewLine, paths.Distinct())}");
|
||||
}
|
||||
result = new(resultSettings, metadataSettings, peopleSettings, distanceSettings, compareSettings);
|
||||
Verify(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(AppSettings))]
|
||||
internal partial class AppSettingsSourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
30
Compare/Models/CompareSettings.cs
Normal file
30
Compare/Models/CompareSettings.cs
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace View_by_Distance.Compare.Models;
|
||||
|
||||
public record CompareSettings(string Company,
|
||||
string FacesFileNameExtension,
|
||||
string FacesHiddenFileNameExtension,
|
||||
string FacesPartsFileNameExtension,
|
||||
string[] IgnoreExtensions,
|
||||
int MaxDegreeOfParallelism,
|
||||
string[] OutputResolutions,
|
||||
string[] ValidImageFormatExtensions,
|
||||
string[] ValidVideoFormatExtensions) : Shared.Models.Properties.ICompareSettings
|
||||
{
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string result = JsonSerializer.Serialize(this, CompareSettingsSourceGenerationContext.Default.CompareSettings);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(CompareSettings))]
|
||||
internal partial class CompareSettingsSourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
32
Compare/Models/Identifier.cs
Normal file
32
Compare/Models/Identifier.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace View_by_Distance.Compare.Models;
|
||||
|
||||
internal sealed record Identifier(string[] DirectoryNames,
|
||||
bool? HasDateTimeOriginal,
|
||||
int Id,
|
||||
long Length,
|
||||
string PaddedId,
|
||||
long Ticks)
|
||||
{
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string result = JsonSerializer.Serialize(this, IdentifierSourceGenerationContext.Default.Identifier);
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(Identifier))]
|
||||
internal partial class IdentifierSourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(Identifier[]))]
|
||||
internal partial class IdentifierCollectionSourceGenerationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
53
Compare/Program.cs
Normal file
53
Compare/Program.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using View_by_Distance.Compare.Models;
|
||||
|
||||
namespace View_by_Distance.Compare;
|
||||
|
||||
public class Program
|
||||
{
|
||||
|
||||
public static void Secondary(ILogger<Program> logger, List<string> args)
|
||||
{
|
||||
IConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
|
||||
.AddEnvironmentVariables()
|
||||
.AddUserSecrets<Program>();
|
||||
IConfigurationRoot configurationRoot = configurationBuilder.Build();
|
||||
AppSettings appSettings = AppSettings.Get(configurationRoot);
|
||||
int silentIndex = args.IndexOf("s");
|
||||
if (silentIndex > -1)
|
||||
args.RemoveAt(silentIndex);
|
||||
try
|
||||
{
|
||||
if (args is null)
|
||||
throw new Exception("args is null!");
|
||||
Shared.Models.Console console = new();
|
||||
_ = new Compare(args, logger, appSettings, silentIndex > -1, console);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger?.LogError(ex, "Error!");
|
||||
}
|
||||
if (silentIndex > -1)
|
||||
logger?.LogInformation("Done. Bye");
|
||||
else
|
||||
{
|
||||
logger?.LogInformation("Done. Press 'Enter' to end");
|
||||
_ = Console.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
#pragma warning disable IL3050
|
||||
ILogger<Program>? logger = Host.CreateDefaultBuilder(args).Build().Services.GetRequiredService<ILogger<Program>>();
|
||||
#pragma warning restore IL3050
|
||||
if (args is not null)
|
||||
Secondary(logger, args.ToList());
|
||||
else
|
||||
Secondary(logger, []);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user