Xem is used now

pull/4/head
Mark McDowall 11 years ago
parent 236316c402
commit f2d030ef27

@ -128,7 +128,7 @@ namespace NzbDrone.Core.Test.MediaFileTests
imported.Add(new ImportDecision(localEpisode)); imported.Add(new ImportDecision(localEpisode));
Mocker.GetMock<IMakeImportDecision>() Mocker.GetMock<IMakeImportDecision>()
.Setup(s => s.GetImportDecisions(It.IsAny<IEnumerable<String>>(), It.IsAny<Series>())) .Setup(s => s.GetImportDecisions(It.IsAny<IEnumerable<String>>(), It.IsAny<Series>(), true))
.Returns(imported); .Returns(imported);
Mocker.GetMock<IImportApprovedEpisodes>() Mocker.GetMock<IImportApprovedEpisodes>()

@ -64,7 +64,8 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
_series = new Series(); _series = new Series();
_localEpisode = new LocalEpisode { Series = _series, Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" }; _localEpisode = new LocalEpisode { Series = _series, Path = @"C:\Test\Unsorted\The.Office.S03E115.DVDRip.XviD-OSiTV.avi" };
Mocker.GetMock<IParsingService>().Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>())) Mocker.GetMock<IParsingService>()
.Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()))
.Returns(_localEpisode); .Returns(_localEpisode);
@ -84,7 +85,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
Subject.GetImportDecisions(_videoFiles, new Series()); Subject.GetImportDecisions(_videoFiles, new Series(), false);
_fail1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once()); _fail1.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
_fail2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once()); _fail2.Verify(c => c.IsSatisfiedBy(_localEpisode), Times.Once());
@ -99,7 +100,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_fail1); GivenSpecifications(_fail1);
var result = Subject.GetImportDecisions(_videoFiles, new Series()); var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
result.Single().Approved.Should().BeFalse(); result.Single().Approved.Should().BeFalse();
} }
@ -109,7 +110,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_pass1, _fail1, _pass2, _pass3); GivenSpecifications(_pass1, _fail1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series()); var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
result.Single().Approved.Should().BeFalse(); result.Single().Approved.Should().BeFalse();
} }
@ -119,7 +120,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_pass1, _pass2, _pass3); GivenSpecifications(_pass1, _pass2, _pass3);
var result = Subject.GetImportDecisions(_videoFiles, new Series()); var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
result.Single().Approved.Should().BeTrue(); result.Single().Approved.Should().BeTrue();
} }
@ -129,7 +130,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3); GivenSpecifications(_pass1, _pass2, _pass3, _fail1, _fail2, _fail3);
var result = Subject.GetImportDecisions(_videoFiles, new Series()); var result = Subject.GetImportDecisions(_videoFiles, new Series(), false);
result.Single().Rejections.Should().HaveCount(3); result.Single().Rejections.Should().HaveCount(3);
} }
@ -138,7 +139,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
{ {
GivenSpecifications(_pass1); GivenSpecifications(_pass1);
Mocker.GetMock<IParsingService>().Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>())) Mocker.GetMock<IParsingService>().Setup(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>(), It.IsAny<Boolean>()))
.Throws<TestException>(); .Throws<TestException>();
_videoFiles = new List<String> _videoFiles = new List<String>
@ -153,7 +154,7 @@ namespace NzbDrone.Core.Test.MediaFileTests.EpisodeImportTests
.Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<int>())) .Setup(c => c.FilterExistingFiles(_videoFiles, It.IsAny<int>()))
.Returns(_videoFiles); .Returns(_videoFiles);
Subject.GetImportDecisions(_videoFiles, new Series()); Subject.GetImportDecisions(_videoFiles, new Series(), false);
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Verify(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>()), Times.Exactly(_videoFiles.Count)); .Verify(c => c.GetEpisodes(It.IsAny<String>(), It.IsAny<Series>()), Times.Exactly(_videoFiles.Count));

@ -56,23 +56,31 @@ namespace NzbDrone.Core.IndexerSearch
return SearchDaily(episode.SeriesId, episode.AirDate.Value.Date); return SearchDaily(episode.SeriesId, episode.AirDate.Value.Date);
} }
return SearchSingle(episode.SeriesId, episode.SeasonNumber, episode.EpisodeNumber); return SearchSingle(series, episode);
} }
private List<DownloadDecision> SearchSingle(int seriesId, int seasonNumber, int episodeNumber) private List<DownloadDecision> SearchSingle(Series series, Episode episode)
{ {
var searchSpec = Get<SingleEpisodeSearchCriteria>(seriesId, seasonNumber); var searchSpec = Get<SingleEpisodeSearchCriteria>(series.Id, episode.SeasonNumber);
if (_seriesService.GetSeries(seriesId).UseSceneNumbering) if (series.UseSceneNumbering)
{ {
var episode = _episodeService.GetEpisode(seriesId, seasonNumber, episodeNumber); if (episode.SceneSeasonNumber > 0 && episode.SceneEpisodeNumber > 0)
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber; {
searchSpec.SeasonNumber = episode.SceneSeasonNumber; searchSpec.EpisodeNumber = episode.SceneEpisodeNumber;
searchSpec.SeasonNumber = episode.SceneSeasonNumber;
}
else
{
searchSpec.EpisodeNumber = episode.EpisodeNumber;
searchSpec.SeasonNumber = episode.SeasonNumber;
}
} }
else else
{ {
searchSpec.EpisodeNumber = episodeNumber; searchSpec.EpisodeNumber = episode.EpisodeNumber;
searchSpec.SeasonNumber = seasonNumber; searchSpec.SeasonNumber = episode.SeasonNumber;
} }
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec); return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);

@ -40,7 +40,7 @@ namespace NzbDrone.Core.Jobs
var defaultTasks = new[] var defaultTasks = new[]
{ {
new ScheduledTask{ Interval = 15, TypeName = typeof(RssSyncCommand).FullName}, new ScheduledTask{ Interval = 15, TypeName = typeof(RssSyncCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(UpdateXemMappings).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(UpdateXemMappingsCommand).FullName},
new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName},
new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName},
new ScheduledTask{ Interval = 5, TypeName = typeof(ApplicationUpdateCommand).FullName}, new ScheduledTask{ Interval = 5, TypeName = typeof(ApplicationUpdateCommand).FullName},

@ -52,7 +52,7 @@ namespace NzbDrone.Core.MediaFiles
var mediaFileList = GetVideoFiles(series.Path); var mediaFileList = GetVideoFiles(series.Path);
var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series); var decisions = _importDecisionMaker.GetImportDecisions(mediaFileList, series, false);
_importApprovedEpisodes.Import(decisions); _importApprovedEpisodes.Import(decisions);
} }

@ -135,7 +135,7 @@ namespace NzbDrone.Core.MediaFiles
private List<ImportDecision> ProcessFiles(IEnumerable<string> videoFiles, Series series) private List<ImportDecision> ProcessFiles(IEnumerable<string> videoFiles, Series series)
{ {
var decisions = _importDecisionMaker.GetImportDecisions(videoFiles, series); var decisions = _importDecisionMaker.GetImportDecisions(videoFiles, series, true);
return _importApprovedEpisodes.Import(decisions, true); return _importApprovedEpisodes.Import(decisions, true);
} }

@ -14,7 +14,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
{ {
public interface IMakeImportDecision public interface IMakeImportDecision
{ {
List<ImportDecision> GetImportDecisions(IEnumerable<String> videoFiles, Series series); List<ImportDecision> GetImportDecisions(IEnumerable<String> videoFiles, Series series, bool sceneSource);
} }
public class ImportDecisionMaker : IMakeImportDecision public class ImportDecisionMaker : IMakeImportDecision
@ -39,16 +39,16 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
_logger = logger; _logger = logger;
} }
public List<ImportDecision> GetImportDecisions(IEnumerable<String> videoFiles, Series series) public List<ImportDecision> GetImportDecisions(IEnumerable<string> videoFiles, Series series, bool sceneSource)
{ {
var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series.Id); var newFiles = _mediaFileService.FilterExistingFiles(videoFiles.ToList(), series.Id);
_logger.Debug("Analysing {0}/{1} files.", newFiles.Count, videoFiles.Count()); _logger.Debug("Analysing {0}/{1} files.", newFiles.Count, videoFiles.Count());
return GetDecisions(newFiles, series).ToList(); return GetDecisions(newFiles, series, sceneSource).ToList();
} }
private IEnumerable<ImportDecision> GetDecisions(IEnumerable<String> videoFiles, Series series) private IEnumerable<ImportDecision> GetDecisions(IEnumerable<String> videoFiles, Series series, bool sceneSource)
{ {
foreach (var file in videoFiles) foreach (var file in videoFiles)
{ {
@ -56,7 +56,7 @@ namespace NzbDrone.Core.MediaFiles.EpisodeImport
try try
{ {
var parsedEpisode = _parsingService.GetEpisodes(file, series); var parsedEpisode = _parsingService.GetEpisodes(file, series, sceneSource);
if (parsedEpisode != null) if (parsedEpisode != null)
{ {

@ -382,7 +382,7 @@
<Compile Include="Parser\Model\ReportInfo.cs" /> <Compile Include="Parser\Model\ReportInfo.cs" />
<Compile Include="Parser\Parser.cs" /> <Compile Include="Parser\Parser.cs" />
<Compile Include="Parser\ParsingService.cs" /> <Compile Include="Parser\ParsingService.cs" />
<Compile Include="Providers\UpdateXemMappings.cs" /> <Compile Include="Providers\UpdateXemMappingsCommand.cs" />
<Compile Include="Qualities\QualityProfileInUseException.cs" /> <Compile Include="Qualities\QualityProfileInUseException.cs" />
<Compile Include="Qualities\QualitySizeRepository.cs" /> <Compile Include="Qualities\QualitySizeRepository.cs" />
<Compile Include="Qualities\QualityProfileRepository.cs" /> <Compile Include="Qualities\QualityProfileRepository.cs" />

@ -14,7 +14,6 @@ namespace NzbDrone.Core.Parser.Model
public Language Language { get; set; } public Language Language { get; set; }
public bool FullSeason { get; set; } public bool FullSeason { get; set; }
public bool SceneSource { get; set; }
public override string ToString() public override string ToString()
{ {

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Parser
public interface IParsingService public interface IParsingService
{ {
LocalEpisode GetEpisodes(string filename, Series series); LocalEpisode GetEpisodes(string filename, Series series);
LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource);
Series GetSeries(string title); Series GetSeries(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo);
} }
@ -27,6 +28,11 @@ namespace NzbDrone.Core.Parser
} }
public LocalEpisode GetEpisodes(string filename, Series series) public LocalEpisode GetEpisodes(string filename, Series series)
{
return GetEpisodes(filename, series, false);
}
public LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource)
{ {
var parsedEpisodeInfo = Parser.ParsePath(filename); var parsedEpisodeInfo = Parser.ParsePath(filename);
@ -35,7 +41,7 @@ namespace NzbDrone.Core.Parser
return null; return null;
} }
var episodes = GetEpisodes(parsedEpisodeInfo, series); var episodes = GetEpisodes(parsedEpisodeInfo, series, sceneSource);
if (!episodes.Any()) if (!episodes.Any())
{ {
@ -43,13 +49,13 @@ namespace NzbDrone.Core.Parser
} }
return new LocalEpisode return new LocalEpisode
{ {
Series = series, Series = series,
Quality = parsedEpisodeInfo.Quality, Quality = parsedEpisodeInfo.Quality,
Episodes = episodes, Episodes = episodes,
Path = filename, Path = filename,
ParsedEpisodeInfo = parsedEpisodeInfo ParsedEpisodeInfo = parsedEpisodeInfo
}; };
} }
public Series GetSeries(string title) public Series GetSeries(string title)
@ -82,12 +88,12 @@ namespace NzbDrone.Core.Parser
} }
remoteEpisode.Series = series; remoteEpisode.Series = series;
remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series); remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series, true);
return remoteEpisode; return remoteEpisode;
} }
private List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series) private List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource)
{ {
var result = new List<Episode>(); var result = new List<Episode>();
@ -98,7 +104,6 @@ namespace NzbDrone.Core.Parser
_logger.Warn("Found daily-style episode for non-daily series: {0}.", series); _logger.Warn("Found daily-style episode for non-daily series: {0}.", series);
return new List<Episode>(); return new List<Episode>();
} }
//TODO: this will fail since parsed date will be local, and stored date will be UTC //TODO: this will fail since parsed date will be local, and stored date will be UTC
//which means they will probably end up on different dates //which means they will probably end up on different dates
@ -119,33 +124,29 @@ namespace NzbDrone.Core.Parser
{ {
Episode episodeInfo = null; Episode episodeInfo = null;
if (series.UseSceneNumbering && parsedEpisodeInfo.SceneSource) if (series.UseSceneNumbering && sceneSource)
{ {
episodeInfo = _episodeService.FindEpisode(series.Id, parsedEpisodeInfo.SeasonNumber, episodeNumber, true); episodeInfo = _episodeService.FindEpisode(series.Id, parsedEpisodeInfo.SeasonNumber, episodeNumber, true);
if (episodeInfo != null)
{
_logger.Info("Using Scene to TVDB Mapping for: {0} - Scene: {1}x{2:00} - TVDB: {3}x{4:00}",
series.Title,
episodeInfo.SceneSeasonNumber,
episodeInfo.SceneEpisodeNumber,
episodeInfo.SeasonNumber,
episodeInfo.EpisodeNumber);
}
} }
if (episodeInfo == null) if (episodeInfo == null)
{ {
episodeInfo = _episodeService.GetEpisode(series.Id, parsedEpisodeInfo.SeasonNumber, episodeNumber); episodeInfo = _episodeService.GetEpisode(series.Id, parsedEpisodeInfo.SeasonNumber, episodeNumber);
if (episodeInfo == null && parsedEpisodeInfo.AirDate != null)
{
episodeInfo = _episodeService.FindEpisode(series.Id, parsedEpisodeInfo.AirDate.Value);
}
} }
if (episodeInfo != null) if (episodeInfo != null)
{ {
result.Add(episodeInfo); result.Add(episodeInfo);
if (series.UseSceneNumbering)
{
_logger.Info("Using Scene to TVDB Mapping for: {0} - Scene: {1}x{2:00} - TVDB: {3}x{4:00}",
series.Title,
episodeInfo.SceneSeasonNumber,
episodeInfo.SceneEpisodeNumber,
episodeInfo.SeasonNumber,
episodeInfo.EpisodeNumber);
}
} }
else else
{ {

@ -1,8 +0,0 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Providers
{
public class UpdateXemMappings : ICommand
{
}
}

@ -0,0 +1,14 @@
using NzbDrone.Common.Messaging;
namespace NzbDrone.Core.Providers
{
public class UpdateXemMappingsCommand : ICommand
{
public int? SeriesId { get; private set; }
public UpdateXemMappingsCommand(int? seriesId)
{
SeriesId = seriesId;
}
}
}

@ -4,32 +4,41 @@ using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Messaging; using NzbDrone.Common.Messaging;
using NzbDrone.Core.Tv; using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
namespace NzbDrone.Core.Providers namespace NzbDrone.Core.Providers
{ {
public class XemProvider : IExecute<UpdateXemMappings> public interface IXemProvider
{
void UpdateMappings();
void UpdateMappings(int seriesId);
void PerformUpdate(Series series);
}
public class XemProvider : IXemProvider, IExecute<UpdateXemMappingsCommand>, IHandle<SeriesUpdatedEvent>
{ {
private readonly IEpisodeService _episodeService; private readonly IEpisodeService _episodeService;
private readonly XemCommunicationProvider _xemCommunicationProvider; private readonly XemCommunicationProvider _xemCommunicationProvider;
private readonly ISeriesRepository _seriesRepository; private readonly ISeriesService _seriesService;
private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
public XemProvider(IEpisodeService episodeService, XemCommunicationProvider xemCommunicationProvider, ISeriesRepository seriesRepository) public XemProvider(IEpisodeService episodeService, XemCommunicationProvider xemCommunicationProvider, ISeriesService seriesService)
{ {
if (seriesService == null) throw new ArgumentNullException("seriesService");
_episodeService = episodeService; _episodeService = episodeService;
_xemCommunicationProvider = xemCommunicationProvider; _xemCommunicationProvider = xemCommunicationProvider;
_seriesRepository = seriesRepository; _seriesService = seriesService;
} }
public virtual void UpdateMappings() public void UpdateMappings()
{ {
_logger.Trace("Starting scene numbering update"); _logger.Trace("Starting scene numbering update");
try try
{ {
var ids = _xemCommunicationProvider.GetXemSeriesIds(); var ids = _xemCommunicationProvider.GetXemSeriesIds();
var series = _seriesRepository.All(); var series = _seriesService.GetAllSeries();
var wantedSeries = series.Where(s => ids.Contains(s.Id)).ToList(); var wantedSeries = series.Where(s => ids.Contains(s.TvdbId)).ToList();
foreach (var ser in wantedSeries) foreach (var ser in wantedSeries)
{ {
@ -46,34 +55,34 @@ namespace NzbDrone.Core.Providers
} }
} }
public virtual void UpdateMappings(int seriesId) public void UpdateMappings(int seriesId)
{ {
var xemIds = _xemCommunicationProvider.GetXemSeriesIds(); var series = _seriesService.GetSeries(seriesId);
if (!xemIds.Contains(seriesId)) if (series == null)
{ {
_logger.Trace("Xem doesn't have a mapping for this series: {0}", seriesId); _logger.Trace("Series could not be found: {0}", seriesId);
return; return;
} }
var series = _seriesRepository.Get(seriesId); var xemIds = _xemCommunicationProvider.GetXemSeriesIds();
if (series == null) if (!xemIds.Contains(series.TvdbId))
{ {
_logger.Trace("Series could not be found: {0}", seriesId); _logger.Trace("Xem doesn't have a mapping for this series: {0}", series.TvdbId);
return; return;
} }
PerformUpdate(series); PerformUpdate(series);
} }
public virtual void PerformUpdate(Series series) public void PerformUpdate(Series series)
{ {
_logger.Trace("Updating scene numbering mapping for: {0}", series); _logger.Trace("Updating scene numbering mapping for: {0}", series);
try try
{ {
var episodesToUpdate = new List<Episode>(); var episodesToUpdate = new List<Episode>();
var mappings = _xemCommunicationProvider.GetSceneTvdbMappings(series.Id); var mappings = _xemCommunicationProvider.GetSceneTvdbMappings(series.TvdbId);
if (mappings == null) if (mappings == null)
{ {
@ -106,7 +115,7 @@ namespace NzbDrone.Core.Providers
_logger.Trace("Setting UseSceneMapping for {0}", series); _logger.Trace("Setting UseSceneMapping for {0}", series);
series.UseSceneNumbering = true; series.UseSceneNumbering = true;
_seriesRepository.Update(series); _seriesService.UpdateSeries(series);
} }
catch (Exception ex) catch (Exception ex)
@ -115,9 +124,21 @@ namespace NzbDrone.Core.Providers
} }
} }
public void Execute(UpdateXemMappings message) public void Execute(UpdateXemMappingsCommand message)
{
if (message.SeriesId.HasValue)
{
UpdateMappings(message.SeriesId.Value);
}
else
{
UpdateMappings();
}
}
public void Handle(SeriesUpdatedEvent message)
{ {
UpdateMappings(); PerformUpdate(message.Series);
} }
} }
} }

@ -2,7 +2,6 @@
<FileVersion>1</FileVersion> <FileVersion>1</FileVersion>
<AutoEnableOnStartup>False</AutoEnableOnStartup> <AutoEnableOnStartup>False</AutoEnableOnStartup>
<AllowParallelTestExecution>true</AllowParallelTestExecution> <AllowParallelTestExecution>true</AllowParallelTestExecution>
<AllowTestsToRunInParallelWithThemselves>true</AllowTestsToRunInParallelWithThemselves>
<FrameworkUtilisationTypeForNUnit>UseDynamicAnalysis</FrameworkUtilisationTypeForNUnit> <FrameworkUtilisationTypeForNUnit>UseDynamicAnalysis</FrameworkUtilisationTypeForNUnit>
<FrameworkUtilisationTypeForGallio>Disabled</FrameworkUtilisationTypeForGallio> <FrameworkUtilisationTypeForGallio>Disabled</FrameworkUtilisationTypeForGallio>
<FrameworkUtilisationTypeForMSpec>Disabled</FrameworkUtilisationTypeForMSpec> <FrameworkUtilisationTypeForMSpec>Disabled</FrameworkUtilisationTypeForMSpec>

Loading…
Cancel
Save