Browse Source

fix bug with smart collection progress (#1118)

pull/1119/head
Jason Dove 4 years ago committed by GitHub
parent
commit
4c75e638a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 3
      ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs
  3. 2
      ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs
  4. 164
      ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs
  5. 158
      ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs
  6. 21
      ErsatzTV.Core/Scheduling/PlayoutBuilder.cs
  7. 2
      ErsatzTV.Infrastructure/Search/SearchIndex.cs
  8. 2
      ErsatzTV.Scanner/ErsatzTV.Scanner.csproj
  9. 8
      ErsatzTV.sln

1
CHANGELOG.md

@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Fixed
- Fix schedule editor crashing due to bad music video artist data
- Fix bug where playouts would not maintain smart collection progress on schedules that use multiple smart collections
### Changed
- Always use software pipeline for error display

3
ErsatzTV.Application/Playouts/Commands/BuildPlayoutHandler.cs

@ -105,8 +105,11 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro @@ -105,8 +105,11 @@ public class BuildPlayoutHandler : IRequestHandler<BuildPlayout, Either<BaseErro
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(psa => psa.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)

2
ErsatzTV.Core.Tests/Fakes/FakeMediaCollectionRepository.cs

@ -15,7 +15,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository @@ -15,7 +15,7 @@ public class FakeMediaCollectionRepository : IMediaCollectionRepository
public Task<List<MediaItem>> GetItems(int id) => _data[id].ToList().AsTask();
public Task<List<MediaItem>> GetMultiCollectionItems(int id) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => throw new NotSupportedException();
public Task<List<MediaItem>> GetSmartCollectionItems(int id) => _data[id].ToList().AsTask();
public Task<List<CollectionWithItems>> GetMultiCollectionCollections(int id) =>
throw new NotSupportedException();

164
ErsatzTV.Core.Tests/Scheduling/PlayoutBuilderTests.cs

@ -2380,6 +2380,91 @@ public class PlayoutBuilderTests @@ -2380,6 +2380,91 @@ public class PlayoutBuilderTests
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
firstSeedValue.Should().Be(secondSeedValue);
}
[Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed()
{
var mediaItems = new List<MediaItem>
{
TestMovie(1, TimeSpan.FromHours(1), DateTime.Today),
TestMovie(2, TimeSpan.FromHours(1), DateTime.Today.AddHours(1)),
TestMovie(3, TimeSpan.FromHours(1), DateTime.Today.AddHours(3))
};
(PlayoutBuilder builder, Playout playout) =
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle);
DateTimeOffset start = HoursAfterMidnight(0);
DateTimeOffset finish = start + TimeSpan.FromHours(6);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
result.Items.Count.Should().Be(6);
result.ProgramScheduleAnchors.Count.Should().Be(2);
PlayoutProgramScheduleAnchor primaryAnchor = result.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
primaryAnchor.EnumeratorState.Seed.Should().BeGreaterThan(0);
primaryAnchor.EnumeratorState.Index.Should().Be(0);
int firstSeedValue = primaryAnchor.EnumeratorState.Seed;
DateTimeOffset start2 = HoursAfterMidnight(0);
DateTimeOffset finish2 = start2 + TimeSpan.FromHours(6);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
primaryAnchor = result2.ProgramScheduleAnchors.First(a => a.SmartCollectionId == 1);
int secondSeedValue = primaryAnchor.EnumeratorState.Seed;
firstSeedValue.Should().Be(secondSeedValue);
primaryAnchor.EnumeratorState.Index.Should().Be(0);
}
[Test]
public async Task ShuffleFlood_MultipleSmartCollections_Should_MaintainRandomSeed_MultipleDays()
{
var mediaItems = new List<MediaItem>();
for (int i = 1; i <= 100; i++)
{
mediaItems.Add(TestMovie(i, TimeSpan.FromMinutes(55), DateTime.Today.AddHours(i)));
}
(PlayoutBuilder builder, Playout playout) =
TestDataFloodForSmartCollectionItems(mediaItems, PlaybackOrder.Shuffle);
DateTimeOffset start = HoursAfterMidnight(0).AddSeconds(5);
DateTimeOffset finish = start + TimeSpan.FromDays(2);
Playout result = await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
result.Items.Count.Should().Be(53);
result.ProgramScheduleAnchors.Count.Should().Be(4);
result.ProgramScheduleAnchors.All(x => x.AnchorDate is not null).Should().BeTrue();
PlayoutProgramScheduleAnchor lastCheckpoint = result.ProgramScheduleAnchors
.Filter(psa => psa.SmartCollectionId == 1)
.OrderByDescending(a => a.AnchorDate ?? DateTime.MinValue)
.First();
lastCheckpoint.EnumeratorState.Seed.Should().BeGreaterThan(0);
lastCheckpoint.EnumeratorState.Index.Should().Be(53);
int firstSeedValue = lastCheckpoint.EnumeratorState.Seed;
for (var i = 1; i < 20; i++)
{
DateTimeOffset start2 = start.AddHours(i);
DateTimeOffset finish2 = start2 + TimeSpan.FromDays(2);
Playout result2 = await builder.Build(result, PlayoutBuildMode.Continue, start2, finish2);
PlayoutProgramScheduleAnchor continueAnchor =
result2.ProgramScheduleAnchors
.Filter(psa => psa.SmartCollectionId == 1)
.First(x => x.AnchorDate is null);
int secondSeedValue = continueAnchor.EnumeratorState.Seed;
// the continue anchor should have the same seed as the most recent (last) checkpoint from the first run
firstSeedValue.Should().Be(secondSeedValue);
}
}
[Test]
public async Task FloodContent_Should_FloodWithFixedStartTime_FromAnchor()
@ -2727,10 +2812,34 @@ public class PlayoutBuilderTests @@ -2727,10 +2812,34 @@ public class PlayoutBuilderTests
{
Id = 1,
Index = 1,
CollectionType = ProgramScheduleItemCollectionType.Collection,
Collection = mediaCollection,
CollectionId = mediaCollection.Id,
StartTime = null,
PlaybackOrder = playbackOrder
PlaybackOrder = playbackOrder,
};
private static ProgramScheduleItem Flood(
SmartCollection smartCollection,
SmartCollection fillerCollection,
PlaybackOrder playbackOrder) =>
new ProgramScheduleItemFlood
{
Id = 1,
Index = 1,
CollectionType = ProgramScheduleItemCollectionType.SmartCollection,
SmartCollection = smartCollection,
SmartCollectionId = smartCollection.Id,
StartTime = null,
PlaybackOrder = playbackOrder,
FallbackFiller = new FillerPreset
{
Id = 1,
CollectionType = ProgramScheduleItemCollectionType.SmartCollection,
SmartCollection = fillerCollection,
SmartCollectionId = fillerCollection.Id,
FillerKind = FillerKind.Fallback
}
};
private static Movie TestMovie(int id, TimeSpan duration, DateTime aired) =>
@ -2791,6 +2900,59 @@ public class PlayoutBuilderTests @@ -2791,6 +2900,59 @@ public class PlayoutBuilderTests
return new TestData(builder, playout);
}
private TestData TestDataFloodForSmartCollectionItems(
List<MediaItem> mediaItems,
PlaybackOrder playbackOrder,
Mock<IConfigElementRepository> configMock = null)
{
var mediaCollection = new SmartCollection
{
Id = 1,
Query = "asdf"
};
var fillerCollection = new SmartCollection
{
Id = 2,
Query = "ghjk"
};
Mock<IConfigElementRepository> configRepo = configMock ?? new Mock<IConfigElementRepository>();
var collectionRepo = new FakeMediaCollectionRepository(
Map(
(mediaCollection.Id, mediaItems),
(fillerCollection.Id, mediaItems.Take(1).ToList())
)
);
var televisionRepo = new FakeTelevisionRepository();
var artistRepo = new Mock<IArtistRepository>();
var factory = new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>();
var localFileSystem = new Mock<ILocalFileSystem>();
var builder = new PlayoutBuilder(
configRepo.Object,
collectionRepo,
televisionRepo,
artistRepo.Object,
factory.Object,
localFileSystem.Object,
_logger);
var items = new List<ProgramScheduleItem> { Flood(mediaCollection, fillerCollection, playbackOrder) };
var playout = new Playout
{
Id = 1,
ProgramSchedule = new ProgramSchedule { Items = items },
Channel = new Channel(Guid.Empty) { Id = 1, Name = "Test Channel" },
Items = new List<PlayoutItem>(),
ProgramScheduleAnchors = new List<PlayoutProgramScheduleAnchor>(),
ProgramScheduleAlternates = new List<ProgramScheduleAlternate>()
};
return new TestData(builder, playout);
}
private record TestData(PlayoutBuilder Builder, Playout Playout);
}

158
ErsatzTV.Core.Tests/Scheduling/ScheduleIntegrationTests.cs

@ -3,12 +3,17 @@ using Dapper; @@ -3,12 +3,17 @@ using Dapper;
using Destructurama;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Repositories.Caching;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Data.Repositories.Caching;
using ErsatzTV.Infrastructure.Extensions;
using ErsatzTV.Infrastructure.Search;
using LanguageExt.UnsafeValueAccess;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@ -28,7 +33,7 @@ public class ScheduleIntegrationTests @@ -28,7 +33,7 @@ public class ScheduleIntegrationTests
public ScheduleIntegrationTests()
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning)
.WriteTo.Console()
@ -37,7 +42,123 @@ public class ScheduleIntegrationTests @@ -37,7 +42,123 @@ public class ScheduleIntegrationTests
}
[Test]
public async Task Test()
public async Task TestExistingData()
{
const string DB_FILE_NAME = "/tmp/whatever.sqlite3";
const int PLAYOUT_ID = 39;
var start = new DateTimeOffset(2023, 1, 18, 11, 0, 0, TimeSpan.FromHours(-5));
DateTimeOffset finish = start.AddDays(2);
IServiceCollection services = new ServiceCollection()
.AddLogging();
var connectionString = $"Data Source={DB_FILE_NAME};foreign keys=true;";
services.AddDbContext<TvContext>(
options => options.UseSqlite(
connectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}),
ServiceLifetime.Scoped,
ServiceLifetime.Singleton);
services.AddDbContextFactory<TvContext>(
options => options.UseSqlite(
connectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure");
}));
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler());
services.AddSingleton((Func<IServiceProvider, ILoggerFactory>)(_ => new SerilogLoggerFactory()));
services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<ICachingSearchRepository, CachingSearchRepository>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddSingleton<ISearchIndex, SearchIndex>();
services.AddSingleton(_ => new Mock<IClient>().Object);
ServiceProvider provider = services.BuildServiceProvider();
IDbContextFactory<TvContext> factory = provider.GetRequiredService<IDbContextFactory<TvContext>>();
ILogger<ScheduleIntegrationTests> logger = provider.GetRequiredService<ILogger<ScheduleIntegrationTests>>();
logger.LogInformation("Database is at {File}", DB_FILE_NAME);
await using TvContext dbContext = await factory.CreateDbContextAsync(CancellationToken.None);
await dbContext.Database.MigrateAsync(CancellationToken.None);
await DbInitializer.Initialize(dbContext, CancellationToken.None);
ISearchIndex searchIndex = provider.GetRequiredService<ISearchIndex>();
await searchIndex.Initialize(
new LocalFileSystem(
provider.GetRequiredService<IClient>(),
provider.GetRequiredService<ILogger<LocalFileSystem>>()),
provider.GetRequiredService<IConfigElementRepository>());
await searchIndex.Rebuild(
provider.GetRequiredService<ICachingSearchRepository>(),
provider.GetRequiredService<IFallbackMetadataProvider>());
var builder = new PlayoutBuilder(
new ConfigElementRepository(factory),
new MediaCollectionRepository(new Mock<IClient>().Object, searchIndex, factory),
new TelevisionRepository(factory),
new ArtistRepository(factory),
new Mock<IMultiEpisodeShuffleCollectionEnumeratorFactory>().Object,
new Mock<ILocalFileSystem>().Object,
provider.GetRequiredService<ILogger<PlayoutBuilder>>());
{
await using TvContext context = await factory.CreateDbContextAsync();
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
await builder.Build(playout, PlayoutBuildMode.Reset, start, finish);
await context.SaveChangesAsync();
}
for (var i = 1; i <= (24 * 1); i++)
{
await using TvContext context = await factory.CreateDbContextAsync();
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
await context.SaveChangesAsync();
}
for (var i = 25; i <= 26; i++)
{
await using TvContext context = await factory.CreateDbContextAsync();
Option<Playout> maybePlayout = await GetPlayout(context, PLAYOUT_ID);
Playout playout = maybePlayout.ValueUnsafe();
await builder.Build(playout, PlayoutBuildMode.Continue, start.AddHours(i), finish.AddHours(i));
await context.SaveChangesAsync();
}
}
[Test]
public async Task TestMockData()
{
string dbFileName = Path.GetTempFileName() + ".sqlite3";
@ -221,8 +342,41 @@ public class ScheduleIntegrationTests @@ -221,8 +342,41 @@ public class ScheduleIntegrationTests
return await dbContext.Playouts
.Include(p => p.Channel)
.Include(p => p.Items)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MediaItem)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PreRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.MidRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.PostRollFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.TailFiller)
.Include(p => p.ProgramScheduleAlternates)
.ThenInclude(a => a.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.FallbackFiller)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.EnumeratorState)
.Include(p => p.ProgramScheduleAnchors)
.ThenInclude(a => a.MediaItem)
.Include(p => p.ProgramSchedule)
.ThenInclude(ps => ps.Items)
.ThenInclude(psi => psi.Collection)

21
ErsatzTV.Core/Scheduling/PlayoutBuilder.cs

@ -668,6 +668,8 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -668,6 +668,8 @@ public class PlayoutBuilder : IPlayoutBuilder
a => a.CollectionType == collectionKey.CollectionType
&& a.CollectionId == collectionKey.CollectionId
&& a.MediaItemId == collectionKey.MediaItemId
&& a.SmartCollectionId == collectionKey.SmartCollectionId
&& a.MultiCollectionId == collectionKey.MultiCollectionId
&& a.AnchorDate is null);
var maybeEnumeratorState = collectionEnumerators.ToDictionary(e => e.Key, e => e.Value.State);
@ -727,15 +729,18 @@ public class PlayoutBuilder : IPlayoutBuilder @@ -727,15 +729,18 @@ public class PlayoutBuilder : IPlayoutBuilder
&& a.SmartCollectionId == collectionKey.SmartCollectionId
&& a.MediaItemId == collectionKey.MediaItemId);
// foreach (PlayoutProgramScheduleAnchor anchor in maybeAnchor)
// {
// _logger.LogDebug("Selecting anchor {@Anchor}", anchor);
// }
CollectionEnumeratorState state = null;
foreach (PlayoutProgramScheduleAnchor anchor in maybeAnchor)
{
// _logger.LogDebug("Selecting anchor {@Anchor}", anchor);
anchor.EnumeratorState ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
state = anchor.EnumeratorState;
}
CollectionEnumeratorState state = maybeAnchor.Match(
anchor => anchor.EnumeratorState ??
(anchor.EnumeratorState = new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 }),
() => new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 });
state ??= new CollectionEnumeratorState { Seed = Random.Next(), Index = 0 };
int collectionId = collectionKey.CollectionId ?? 0;

2
ErsatzTV.Infrastructure/Search/SearchIndex.cs

@ -184,7 +184,7 @@ public sealed class SearchIndex : ISearchIndex @@ -184,7 +184,7 @@ public sealed class SearchIndex : ISearchIndex
{ "searchField", searchField }
};
client.Breadcrumbs.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
client?.Breadcrumbs?.Leave("SearchIndex.Search", BreadcrumbType.State, metadata);
if (string.IsNullOrWhiteSpace(searchQuery.Replace("*", string.Empty).Replace("?", string.Empty)) ||
_writer.MaxDoc == 0)

2
ErsatzTV.Scanner/ErsatzTV.Scanner.csproj

@ -5,6 +5,8 @@ @@ -5,6 +5,8 @@
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Debug No Sync</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<ItemGroup>

8
ErsatzTV.sln

@ -33,8 +33,8 @@ Global @@ -33,8 +33,8 @@ Global
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Release|Any CPU.Build.0 = Release|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug No Sync|Any CPU.Build.0 = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{E83551AD-27E4-46E5-AD06-5B0DF797B8FF}.Debug|Any CPU.Build.0 = Debug No Sync|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C56FC23D-B863-401E-8E7C-E92BC307AFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -77,12 +77,12 @@ Global @@ -77,12 +77,12 @@ Global
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Release|Any CPU.Build.0 = Release|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
{591FB3F4-4DD8-441B-B7C8-F2A42BF69992}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Release|Any CPU.Build.0 = Release|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.ActiveCfg = Debug|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug No Sync|Any CPU.Build.0 = Debug|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.ActiveCfg = Debug No Sync|Any CPU
{5664D574-2B8B-41C1-B091-8D3E887AE24E}.Debug|Any CPU.Build.0 = Debug No Sync|Any CPU
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EF80455-953D-4696-831D-E8CBCA82B0EF}.Release|Any CPU.ActiveCfg = Release|Any CPU

Loading…
Cancel
Save