Something went wrong. Try again.
Something went wrong. Try again.
14 kB · 299 lines
C#
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300using Mediatracker.Administration.Jobs;using Mediatracker.Administration.Settings;using Mediatracker.AtProto.Repo;using Mediatracker.AtProto.Tap;using Mediatracker.Tracking.Account;using Mediatracker.Tracking.Calendar;using Mediatracker.Tracking.EntryMigration;using Mediatracker.Tracking.FeedDirectory;using Mediatracker.Tracking.Feeds;using Mediatracker.Tracking.Identifiers;using Mediatracker.Tracking.Library;using Mediatracker.Tracking.Lists;using Mediatracker.Tracking.Movies;using Mediatracker.Tracking.RecordIndex;using Mediatracker.Tracking.RestoreJobs;using Mediatracker.Tracking.TitleMapping;using Mediatracker.Tracking.TvShows;using Microsoft.EntityFrameworkCore;using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using Microsoft.Extensions.DependencyInjection.Extensions;using Microsoft.Extensions.Logging;
namespace Mediatracker.Tracking;
/// <summary>/// DI registration for writing the user's own tracking data to their PDS./// </summary>public static class ServiceCollectionExtensions{ /// <summary> /// Registers the tracking services, and the index of the user's records in PostgreSQL that the /// <see cref="IAtProtoRepoClientFactory" /> registered before it (by <c>AddAtProtoCore</c>) then reads from, /// and the services that keep that index current through Tap (without a Tap it only knows what this backend /// writes), and the services for the feeds of other services. /// </summary> public static IServiceCollection AddTrackingCore(this IServiceCollection services, string postgresConnectionString) { services.AddSingleton(TimeProvider.System);
services.AddDbContextFactory<TrackingIndexDbContext>(options => options.UseNpgsql( postgresConnectionString, npgsqlOptions => npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "tracking_index") ) );
services.AddSingleton<ICollectionSyncGate, CollectionSyncGate>(); services.AddSingleton<RecordSnapshotCache>(); services.AddSingleton<IRecordSyncProgress, RecordSyncProgress>();
services.AddSingleton<IRecordSyncOptionsProvider>(provider => new ConfigurationRecordSyncOptionsProvider( provider.GetService<IConfiguration>() ?? new ConfigurationBuilder().Build() ) );
services.AddSingleton( new SourceSettingsDefinition { Name = "RecordSync", Fields = [ new SourceSettingField { Key = "Enabled", ConfigurationPath = "RecordSync:Enabled", Kind = SourceSettingKind.Choice, Options = ["true", "false",], }, ], } );
services.AddSingleton<IRecordIndexWarmer, RecordIndexWarmer>();
services.AddScoped<IRecordIndex>(provider => new SnapshotInvalidatingRecordIndex( ActivatorUtilities.CreateInstance<EfRecordIndex>(provider), provider.GetRequiredService<RecordSnapshotCache>() ) );
DecorateRepoClientFactory(services);
// The index is filled and kept current through Tap (AddTap holds the connection and replaces this default). services.TryAddSingleton<ITapBackfill, NullTapBackfill>(); services.AddSingleton<IRecordIndexAccounts, RecordIndexAccounts>(); services.AddSingleton<IRecordIndexBackfill, RecordIndexBackfill>(); services.AddSingleton<ITapEventHandler, RecordIndexTapHandler>(); services.AddSingleton<RecordIndexTapReconciler>(); services.AddHostedService(provider => provider.GetRequiredService<RecordIndexTapReconciler>()); services.AddSingleton<IBackgroundJob>(provider => provider.GetRequiredService<RecordIndexTapReconciler>());
services.AddScoped<IItemIdentityProvider, ItemIdentityProvider>(); services.AddScoped<IEpisodeIdentityBackfiller, EpisodeIdentityBackfiller>(); services.AddScoped<IItemIdentityBackfiller, ItemIdentityBackfiller>();
services.AddSingleton<IIdentityBackfillOptionsProvider>(provider => new ConfigurationIdentityBackfillOptionsProvider( provider.GetService<IConfiguration>() ?? new ConfigurationBuilder().Build() ) );
services.AddSingleton<IIdentityBackfillActivity, IdentityBackfillActivity>();
services.AddSingleton( new SourceSettingsDefinition { Name = "IdentityBackfill", Fields = [ new SourceSettingField { Key = "Enabled", ConfigurationPath = "IdentityBackfill:Enabled", Kind = SourceSettingKind.Choice, Options = ["true", "false",], }, ], } );
services.AddSingleton<EpisodeIdentityBackfillService>();
services.AddSingleton<IEpisodeIdentityBackfill>(provider => provider.GetRequiredService<EpisodeIdentityBackfillService>() );
services.AddHostedService(provider => provider.GetRequiredService<EpisodeIdentityBackfillService>());
services.AddSingleton<IBackgroundJob>(provider => provider.GetRequiredService<EpisodeIdentityBackfillService>() );
services.AddScoped<IMovieItemResolver, MovieItemResolver>(); services.AddScoped<IMovieTrackingService, MovieTrackingService>(); services.AddScoped<IMovieWatchService, MovieWatchService>(); services.AddScoped<IMovieRatingService, MovieRatingService>(); services.AddScoped<IMovieMigrationService, MovieMigrationService>(); services.AddScoped<IMovieDeletionService, MovieDeletionService>(); services.AddScoped<ITvShowItemResolver, TvShowItemResolver>(); services.AddScoped<ITvShowTrackingService, TvShowTrackingService>(); services.AddScoped<ITvShowEpisodeItemResolver, TvShowEpisodeItemResolver>(); services.AddScoped<ITvShowEpisodeWatchService, TvShowEpisodeWatchService>(); services.AddScoped<ITvShowRatingService, TvShowRatingService>(); services.AddScoped<ITvShowMigrationService, TvShowMigrationService>(); services.AddScoped<ITvShowDeletionService, TvShowDeletionService>(); services.AddScoped<ITvShowEpisodeRatingService, TvShowEpisodeRatingService>(); services.AddScoped<IListService, ListService>();
// Feeds are served by other services; their addresses come from DID documents, so redirects are not followed. // The options are read from the configuration at every use, so what the administration sets applies at once. services.AddSingleton<IFeedsOptionsProvider>(provider => new ConfigurationFeedsOptionsProvider( provider.GetService<IConfiguration>() ?? new ConfigurationBuilder().Build() ) );
services.AddSingleton<IFeedsActivity, FeedsActivity>();
services.AddSingleton( new SourceSettingsDefinition { Name = "Feeds", Fields = [ new SourceSettingField { Key = "Enabled", ConfigurationPath = "Feeds:Enabled", Kind = SourceSettingKind.Choice, Options = ["true", "false",], }, new SourceSettingField { Key = "RequestTimeoutSeconds", ConfigurationPath = "Feeds:RequestTimeoutSeconds", Kind = SourceSettingKind.Number, }, new SourceSettingField { Key = "CacheMinutes", ConfigurationPath = "Feeds:CacheMinutes", Kind = SourceSettingKind.Number, }, new SourceSettingField { Key = "MaxListItems", ConfigurationPath = "Feeds:MaxListItems", Kind = SourceSettingKind.Number, }, ], } );
services.AddMemoryCache();
services.AddHttpClient( FeedGeneratorResolver.HttpClientName, client => { client.DefaultRequestHeaders.UserAgent.ParseAdd("Mediatracker"); client.Timeout = TimeSpan.FromSeconds(30); } ) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { AllowAutoRedirect = false, PooledConnectionLifetime = TimeSpan.FromMinutes(5), } );
services.AddScoped<IFeedGeneratorResolver, FeedGeneratorResolver>();
// The directory of the feeds of the whole network: found through the relay, kept current by Tap, read by the feeds page. services.AddSingleton<IFeedDirectoryOptionsProvider>(provider => new ConfigurationFeedDirectoryOptionsProvider( provider.GetService<IConfiguration>() ?? new ConfigurationBuilder().Build() ) );
services.AddSingleton<IFeedDirectoryActivity, FeedDirectoryActivity>();
services.AddSingleton( new SourceSettingsDefinition { Name = "FeedDirectory", Fields = [ new SourceSettingField { Key = "Enabled", ConfigurationPath = "FeedDirectory:Enabled", Kind = SourceSettingKind.Choice, Options = ["true", "false",], }, new SourceSettingField { Key = "RelayUrl", ConfigurationPath = "FeedDirectory:RelayUrl", Kind = SourceSettingKind.Url, }, ], } );
services.AddSingleton<IFeedDirectoryStore, EfFeedDirectoryStore>(); services.AddSingleton<ITapEventHandler, FeedDirectoryTapHandler>(); services.AddSingleton<IFeedRelayClient, FeedRelayClient>(); services.AddSingleton<IFeedDirectoryRepoReader, FeedDirectoryRepoReader>(); services.AddSingleton<FeedDirectoryBackfillService>(); services.AddHostedService(provider => provider.GetRequiredService<FeedDirectoryBackfillService>()); services.AddSingleton<IBackgroundJob>(provider => provider.GetRequiredService<FeedDirectoryBackfillService>()); services.AddScoped<IFeedDirectoryService, FeedDirectoryService>(); services.AddScoped<FeedServiceClient>(); services.AddScoped<RecordFeedClient>(); services.AddScoped<IFeedServiceClient, RoutingFeedServiceClient>(); services.AddScoped<IFeedService, FeedService>(); services.AddScoped<IMediaMetadataLoader, MediaMetadataLoader>(); services.AddScoped<IResolvedDisplayLoader, ResolvedDisplayLoader>(); services.AddScoped<IMappedRecords, MappedRecords>(); services.AddScoped<ILibraryService, LibraryService>(); services.AddScoped<ICalendarService, CalendarService>(); services.AddSingleton<IIcsCalendarWriter, IcsCalendarWriter>(); services.AddScoped<IAccountResetService, AccountResetService>(); services.AddScoped<IAccountExportService, AccountExportService>(); services.AddScoped<IAccountRestoreService, AccountRestoreService>();
// The restore jobs live in this process's memory (one per account) and outlast the request that starts them. services.AddSingleton<IRestoreJobRunner, RestoreJobRunner>(); services.AddSingleton<IRestoreJobService, RestoreJobService>();
return services; }
private static void DecorateRepoClientFactory(IServiceCollection services) { var registered = services.LastOrDefault(descriptor => descriptor.ServiceType == typeof(IAtProtoRepoClientFactory)) ?? throw new InvalidOperationException( $"{nameof(IAtProtoRepoClientFactory)} must be registered before the tracking services." );
services.Remove(registered);
services.Add( new ServiceDescriptor( typeof(IAtProtoRepoClientFactory), provider => new IndexedAtProtoRepoClientFactory( (IAtProtoRepoClientFactory)CreateRegistered(provider, registered), provider.GetRequiredService<IRecordIndex>(), provider.GetRequiredService<ICollectionSyncGate>(), provider.GetRequiredService<ILogger<IndexedAtProtoRepoClient>>(), provider.GetRequiredService<RecordSnapshotCache>(), provider.GetRequiredService<IRecordSyncProgress>(), provider.GetRequiredService<IRecordIndexBackfill>(), provider.GetRequiredService<IRecordSyncOptionsProvider>() ), registered.Lifetime ) ); }
private static object CreateRegistered(IServiceProvider provider, ServiceDescriptor registered) => registered.ImplementationFactory?.Invoke(provider) ?? ActivatorUtilities.CreateInstance(provider, registered.ImplementationType!);}