mirror of
https://github.com/jellyfin/jellyfin.git
synced 2024-09-06 22:07:13 -04:00
Support anime series ordering with core providers
This commit is contained in:
parent
918d5b990a
commit
5c3e6a48d7
@ -187,6 +187,7 @@
|
|||||||
<Compile Include="Providers\MetadataRefreshOptions.cs" />
|
<Compile Include="Providers\MetadataRefreshOptions.cs" />
|
||||||
<Compile Include="Providers\NameParser.cs" />
|
<Compile Include="Providers\NameParser.cs" />
|
||||||
<Compile Include="Providers\MetadataStatus.cs" />
|
<Compile Include="Providers\MetadataStatus.cs" />
|
||||||
|
<Compile Include="Providers\ISeriesOrderManager.cs" />
|
||||||
<Compile Include="Session\ISessionManager.cs" />
|
<Compile Include="Session\ISessionManager.cs" />
|
||||||
<Compile Include="Drawing\ImageExtensions.cs" />
|
<Compile Include="Drawing\ImageExtensions.cs" />
|
||||||
<Compile Include="Entities\AggregateFolder.cs" />
|
<Compile Include="Entities\AggregateFolder.cs" />
|
||||||
|
26
MediaBrowser.Controller/Providers/ISeriesOrderManager.cs
Normal file
26
MediaBrowser.Controller/Providers/ISeriesOrderManager.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Providers
|
||||||
|
{
|
||||||
|
public interface ISeriesOrderProvider
|
||||||
|
{
|
||||||
|
string OrderType { get; }
|
||||||
|
Task<int?> FindSeriesIndex(string seriesName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SeriesOrderTypes
|
||||||
|
{
|
||||||
|
public const string Anime = "Anime";
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ISeriesOrderManager
|
||||||
|
{
|
||||||
|
Task<int?> FindSeriesIndex(string orderType, string seriesName);
|
||||||
|
void AddParts(IEnumerable<ISeriesOrderProvider> orderProviders);
|
||||||
|
}
|
||||||
|
}
|
36
MediaBrowser.Providers/Manager/SeriesOrderManager.cs
Normal file
36
MediaBrowser.Providers/Manager/SeriesOrderManager.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using MediaBrowser.Common;
|
||||||
|
using MediaBrowser.Controller.Providers;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Providers.Manager
|
||||||
|
{
|
||||||
|
public class SeriesOrderManager : ISeriesOrderManager
|
||||||
|
{
|
||||||
|
private Dictionary<string, ISeriesOrderProvider[]> _providers;
|
||||||
|
|
||||||
|
public void AddParts(IEnumerable<ISeriesOrderProvider> orderProviders)
|
||||||
|
{
|
||||||
|
_providers = orderProviders
|
||||||
|
.GroupBy(p => p.OrderType)
|
||||||
|
.ToDictionary(g => g.Key, g => g.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int?> FindSeriesIndex(string orderType, string seriesName)
|
||||||
|
{
|
||||||
|
ISeriesOrderProvider[] providers;
|
||||||
|
if (!_providers.TryGetValue(orderType, out providers))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
foreach (ISeriesOrderProvider provider in providers)
|
||||||
|
{
|
||||||
|
int? index = await provider.FindSeriesIndex(seriesName);
|
||||||
|
if (index != null)
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -103,6 +103,7 @@
|
|||||||
<Compile Include="Games\GameXmlParser.cs" />
|
<Compile Include="Games\GameXmlParser.cs" />
|
||||||
<Compile Include="Games\GameXmlProvider.cs" />
|
<Compile Include="Games\GameXmlProvider.cs" />
|
||||||
<Compile Include="Games\GameSystemXmlProvider.cs" />
|
<Compile Include="Games\GameSystemXmlProvider.cs" />
|
||||||
|
<Compile Include="Manager\SeriesOrderManager.cs" />
|
||||||
<Compile Include="MediaInfo\FFProbeAudioInfo.cs" />
|
<Compile Include="MediaInfo\FFProbeAudioInfo.cs" />
|
||||||
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
|
<Compile Include="MediaInfo\FFProbeHelpers.cs" />
|
||||||
<Compile Include="MediaInfo\FFProbeProvider.cs" />
|
<Compile Include="MediaInfo\FFProbeProvider.cs" />
|
||||||
|
@ -77,7 +77,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AddImages(list, season.IndexNumber.Value, xmlPath, cancellationToken);
|
int seasonNumber = AdjustForSeriesOffset(series, season.IndexNumber.Value);
|
||||||
|
AddImages(list, seasonNumber, xmlPath, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@ -115,6 +116,15 @@ namespace MediaBrowser.Providers.TV
|
|||||||
.ThenByDescending(i => i.VoteCount ?? 0);
|
.ThenByDescending(i => i.VoteCount ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int AdjustForSeriesOffset(Series series, int seasonNumber)
|
||||||
|
{
|
||||||
|
var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds);
|
||||||
|
if (offset != null)
|
||||||
|
return (int)(seasonNumber + offset);
|
||||||
|
|
||||||
|
return seasonNumber;
|
||||||
|
}
|
||||||
|
|
||||||
private void AddImages(List<RemoteImageInfo> list, int seasonNumber, string xmlPath, CancellationToken cancellationToken)
|
private void AddImages(List<RemoteImageInfo> list, int seasonNumber, string xmlPath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
|
using (var streamReader = new StreamReader(xmlPath, Encoding.UTF8))
|
||||||
|
@ -62,8 +62,9 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
// Process images
|
// Process images
|
||||||
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
|
var seriesDataPath = TvdbSeriesProvider.GetSeriesDataPath(_config.ApplicationPaths, seriesId);
|
||||||
|
var indexOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds) ?? 0;
|
||||||
|
|
||||||
var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath);
|
var files = TvdbEpisodeProvider.Current.GetEpisodeXmlFiles(episode.ParentIndexNumber + indexOffset, episode.IndexNumber, episode.IndexNumberEnd, seriesDataPath);
|
||||||
|
|
||||||
var result = files.Select(i => GetImageInfo(i, cancellationToken))
|
var result = files.Select(i => GetImageInfo(i, cancellationToken))
|
||||||
.Where(i => i != null);
|
.Where(i => i != null);
|
||||||
|
@ -52,7 +52,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var item = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken);
|
var item = FetchEpisodeData(searchInfo, seriesDataPath, searchInfo.SeriesProviderIds, cancellationToken);
|
||||||
|
|
||||||
if (item != null)
|
if (item != null)
|
||||||
{
|
{
|
||||||
@ -96,7 +96,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
result.Item = FetchEpisodeData(searchInfo, seriesDataPath, cancellationToken);
|
result.Item = FetchEpisodeData(searchInfo, seriesDataPath, searchInfo.SeriesProviderIds, cancellationToken);
|
||||||
result.HasMetadata = result.Item != null;
|
result.HasMetadata = result.Item != null;
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
@ -213,7 +213,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
/// <param name="seriesDataPath">The series data path.</param>
|
/// <param name="seriesDataPath">The series data path.</param>
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
/// <returns>Task{System.Boolean}.</returns>
|
/// <returns>Task{System.Boolean}.</returns>
|
||||||
private Episode FetchEpisodeData(EpisodeInfo id, string seriesDataPath, CancellationToken cancellationToken)
|
private Episode FetchEpisodeData(EpisodeInfo id, string seriesDataPath, Dictionary<string, string> seriesProviderIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (id.IndexNumber == null)
|
if (id.IndexNumber == null)
|
||||||
{
|
{
|
||||||
@ -221,7 +221,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
var episodeNumber = id.IndexNumber.Value;
|
var episodeNumber = id.IndexNumber.Value;
|
||||||
var seasonNumber = id.ParentIndexNumber;
|
var seasonOffset = TvdbSeriesProvider.GetSeriesOffset(seriesProviderIds) ?? 0;
|
||||||
|
var seasonNumber = id.ParentIndexNumber + seasonOffset;
|
||||||
|
|
||||||
if (seasonNumber == null)
|
if (seasonNumber == null)
|
||||||
{
|
{
|
||||||
|
@ -22,8 +22,9 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
|
public class TvdbSeasonImageProvider : IRemoteImageProvider, IHasOrder, IHasChangeMonitor
|
||||||
{
|
{
|
||||||
|
private static readonly CultureInfo UsCulture = new CultureInfo("en-US");
|
||||||
|
|
||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
|
||||||
@ -77,7 +78,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return GetImages(path, item.GetPreferredMetadataLanguage(), season.IndexNumber.Value, cancellationToken);
|
int seasonNumber = AdjustForSeriesOffset(series, season.IndexNumber.Value);
|
||||||
|
return GetImages(path, item.GetPreferredMetadataLanguage(), seasonNumber, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@ -88,7 +90,16 @@ namespace MediaBrowser.Providers.TV
|
|||||||
return new RemoteImageInfo[] { };
|
return new RemoteImageInfo[] { };
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, int seasonNumber, CancellationToken cancellationToken)
|
private int AdjustForSeriesOffset(Series series, int seasonNumber)
|
||||||
|
{
|
||||||
|
var offset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds);
|
||||||
|
if (offset != null)
|
||||||
|
return (int) (seasonNumber + offset);
|
||||||
|
|
||||||
|
return seasonNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEnumerable<RemoteImageInfo> GetImages(string xmlPath, string preferredLanguage, int seasonNumber, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var settings = new XmlReaderSettings
|
var settings = new XmlReaderSettings
|
||||||
{
|
{
|
||||||
@ -159,7 +170,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddImage(XmlReader reader, List<RemoteImageInfo> images, int seasonNumber)
|
private static void AddImage(XmlReader reader, List<RemoteImageInfo> images, int seasonNumber)
|
||||||
{
|
{
|
||||||
reader.MoveToContent();
|
reader.MoveToContent();
|
||||||
|
|
||||||
@ -186,7 +197,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
double rval;
|
double rval;
|
||||||
|
|
||||||
if (double.TryParse(val, NumberStyles.Any, _usCulture, out rval))
|
if (double.TryParse(val, NumberStyles.Any, UsCulture, out rval))
|
||||||
{
|
{
|
||||||
rating = rval;
|
rating = rval;
|
||||||
}
|
}
|
||||||
@ -200,7 +211,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
int rval;
|
int rval;
|
||||||
|
|
||||||
if (int.TryParse(val, NumberStyles.Integer, _usCulture, out rval))
|
if (int.TryParse(val, NumberStyles.Integer, UsCulture, out rval))
|
||||||
{
|
{
|
||||||
voteCount = rval;
|
voteCount = rval;
|
||||||
}
|
}
|
||||||
@ -237,12 +248,12 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
int rval;
|
int rval;
|
||||||
|
|
||||||
if (int.TryParse(resolutionParts[0], NumberStyles.Integer, _usCulture, out rval))
|
if (int.TryParse(resolutionParts[0], NumberStyles.Integer, UsCulture, out rval))
|
||||||
{
|
{
|
||||||
width = rval;
|
width = rval;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (int.TryParse(resolutionParts[1], NumberStyles.Integer, _usCulture, out rval))
|
if (int.TryParse(resolutionParts[1], NumberStyles.Integer, UsCulture, out rval))
|
||||||
{
|
{
|
||||||
height = rval;
|
height = rval;
|
||||||
}
|
}
|
||||||
@ -285,7 +296,7 @@ namespace MediaBrowser.Providers.TV
|
|||||||
CommunityRating = rating,
|
CommunityRating = rating,
|
||||||
VoteCount = voteCount,
|
VoteCount = voteCount,
|
||||||
Url = TVUtils.BannerUrl + url,
|
Url = TVUtils.BannerUrl + url,
|
||||||
ProviderName = Name,
|
ProviderName = ProviderName,
|
||||||
Language = language,
|
Language = language,
|
||||||
Width = width,
|
Width = width,
|
||||||
Height = height
|
Height = height
|
||||||
|
@ -77,6 +77,10 @@ namespace MediaBrowser.Providers.TV
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var seriesOffset = TvdbSeriesProvider.GetSeriesOffset(series.ProviderIds);
|
||||||
|
if (seriesOffset != null)
|
||||||
|
return TvdbSeasonImageProvider.GetImages(path, language, seriesOffset.Value + 1, cancellationToken);
|
||||||
|
|
||||||
return GetImages(path, language, cancellationToken);
|
return GetImages(path, language, cancellationToken);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
|
@ -25,6 +25,8 @@ namespace MediaBrowser.Providers.TV
|
|||||||
{
|
{
|
||||||
public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
|
public class TvdbSeriesProvider : IRemoteMetadataProvider<Series, SeriesInfo>, IHasOrder
|
||||||
{
|
{
|
||||||
|
internal const string TvdbSeriesOffset = "TvdbSeriesOffset";
|
||||||
|
|
||||||
internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
|
internal readonly SemaphoreSlim TvDbResourcePool = new SemaphoreSlim(2, 2);
|
||||||
internal static TvdbSeriesProvider Current { get; private set; }
|
internal static TvdbSeriesProvider Current { get; private set; }
|
||||||
private readonly IZipClient _zipClient;
|
private readonly IZipClient _zipClient;
|
||||||
@ -33,14 +35,16 @@ namespace MediaBrowser.Providers.TV
|
|||||||
private readonly IServerConfigurationManager _config;
|
private readonly IServerConfigurationManager _config;
|
||||||
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
private readonly CultureInfo _usCulture = new CultureInfo("en-US");
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly ISeriesOrderManager _seriesOrder;
|
||||||
|
|
||||||
public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger)
|
public TvdbSeriesProvider(IZipClient zipClient, IHttpClient httpClient, IFileSystem fileSystem, IServerConfigurationManager config, ILogger logger, ISeriesOrderManager seriesOrder)
|
||||||
{
|
{
|
||||||
_zipClient = zipClient;
|
_zipClient = zipClient;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_fileSystem = fileSystem;
|
_fileSystem = fileSystem;
|
||||||
_config = config;
|
_config = config;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_seriesOrder = seriesOrder;
|
||||||
Current = this;
|
Current = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,11 +96,35 @@ namespace MediaBrowser.Providers.TV
|
|||||||
result.HasMetadata = true;
|
result.HasMetadata = true;
|
||||||
|
|
||||||
FetchSeriesData(result.Item, seriesId, cancellationToken);
|
FetchSeriesData(result.Item, seriesId, cancellationToken);
|
||||||
|
await FindAnimeSeriesIndex(result.Item, itemId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task FindAnimeSeriesIndex(Series series, SeriesInfo info)
|
||||||
|
{
|
||||||
|
var index = await _seriesOrder.FindSeriesIndex(SeriesOrderTypes.Anime, series.Name);
|
||||||
|
if (index == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var offset = info.AnimeSeriesIndex - index;
|
||||||
|
series.SetProviderId(TvdbSeriesOffset, offset.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int? GetSeriesOffset(Dictionary<string, string> seriesProviderIds)
|
||||||
|
{
|
||||||
|
string offsetString;
|
||||||
|
if (!seriesProviderIds.TryGetValue(TvdbSeriesOffset, out offsetString))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
int offset;
|
||||||
|
if (int.TryParse(offsetString, out offset))
|
||||||
|
return offset;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fetches the series data.
|
/// Fetches the series data.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -157,6 +157,7 @@ namespace MediaBrowser.ServerApplication
|
|||||||
private IHttpServer HttpServer { get; set; }
|
private IHttpServer HttpServer { get; set; }
|
||||||
private IDtoService DtoService { get; set; }
|
private IDtoService DtoService { get; set; }
|
||||||
private IImageProcessor ImageProcessor { get; set; }
|
private IImageProcessor ImageProcessor { get; set; }
|
||||||
|
private ISeriesOrderManager SeriesOrderManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the media encoder.
|
/// Gets or sets the media encoder.
|
||||||
@ -453,6 +454,9 @@ namespace MediaBrowser.ServerApplication
|
|||||||
ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager);
|
ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, LibraryMonitor, LogManager, FileSystemManager);
|
||||||
RegisterSingleInstance(ProviderManager);
|
RegisterSingleInstance(ProviderManager);
|
||||||
|
|
||||||
|
SeriesOrderManager = new SeriesOrderManager();
|
||||||
|
RegisterSingleInstance(SeriesOrderManager);
|
||||||
|
|
||||||
RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager));
|
RegisterSingleInstance<ISearchEngine>(() => new SearchEngine(LogManager, LibraryManager, UserManager));
|
||||||
|
|
||||||
SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager);
|
SessionManager = new SessionManager(UserDataManager, ServerConfigurationManager, Logger, UserRepository, LibraryManager, UserManager);
|
||||||
@ -680,6 +684,8 @@ namespace MediaBrowser.ServerApplication
|
|||||||
GetExports<IImageSaver>(),
|
GetExports<IImageSaver>(),
|
||||||
GetExports<IExternalId>());
|
GetExports<IExternalId>());
|
||||||
|
|
||||||
|
SeriesOrderManager.AddParts(GetExports<ISeriesOrderProvider>());
|
||||||
|
|
||||||
ImageProcessor.AddParts(GetExports<IImageEnhancer>());
|
ImageProcessor.AddParts(GetExports<IImageEnhancer>());
|
||||||
|
|
||||||
LiveTvManager.AddParts(GetExports<ILiveTvService>());
|
LiveTvManager.AddParts(GetExports<ILiveTvService>());
|
||||||
|
@ -251,7 +251,4 @@ Global
|
|||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(Performance) = preSolution
|
|
||||||
HasPerformanceSessions = true
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
Loading…
Reference in New Issue
Block a user