Stream custom live channels using your own media
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

931 lines
38 KiB

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Abstractions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using BlazorSortable;
using Bugsnag.AspNet.Core;
using Dapper;
using ErsatzTV.Application;
using ErsatzTV.Application.Channels;
using ErsatzTV.Application.Streaming;
using ErsatzTV.Core;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Health;
using ErsatzTV.Core.Health.Checks;
using ErsatzTV.Core.Images;
using ErsatzTV.Core.Interfaces.Database;
using ErsatzTV.Core.Interfaces.Emby;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.GitHub;
using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Jellyfin;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata;
using ErsatzTV.Core.Interfaces.Plex;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Scheduling;
using ErsatzTV.Core.Interfaces.Scripting;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Interfaces.Streaming;
using ErsatzTV.Core.Interfaces.Trakt;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using ErsatzTV.Core.Jellyfin;
using ErsatzTV.Core.Metadata;
using ErsatzTV.Core.Plex;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Core.Scheduling.BlockScheduling;
using ErsatzTV.Core.Scheduling.Engine;
using ErsatzTV.Core.Scheduling.ScriptedScheduling;
using ErsatzTV.Core.Scheduling.YamlScheduling;
using ErsatzTV.Core.Search;
using ErsatzTV.Core.Trakt;
using ErsatzTV.Core.Troubleshooting;
using ErsatzTV.FFmpeg.Capabilities;
using ErsatzTV.FFmpeg.Pipeline;
using ErsatzTV.FFmpeg.Runtime;
using ErsatzTV.Filters;
using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.Database;
using ErsatzTV.Infrastructure.Emby;
using ErsatzTV.Infrastructure.FFmpeg;
using ErsatzTV.Infrastructure.GitHub;
using ErsatzTV.Infrastructure.Health;
using ErsatzTV.Infrastructure.Health.Checks;
using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Jellyfin;
using ErsatzTV.Infrastructure.Locking;
using ErsatzTV.Infrastructure.Metadata;
using ErsatzTV.Infrastructure.Plex;
using ErsatzTV.Infrastructure.Runtime;
using ErsatzTV.Infrastructure.Scheduling;
using ErsatzTV.Infrastructure.Scripting;
using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Infrastructure.Sqlite.Data;
using ErsatzTV.Infrastructure.Streaming;
using ErsatzTV.Infrastructure.Streaming.Graphics;
using ErsatzTV.Infrastructure.Trakt;
using ErsatzTV.Serialization;
using ErsatzTV.Services;
using ErsatzTV.Services.RunOnce;
using ErsatzTV.Services.Validators;
using FluentValidation;
using FluentValidation.AspNetCore;
using Ganss.Xss;
using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IO;
using Microsoft.OpenApi;
using MudBlazor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Refit;
using Scalar.AspNetCore;
using Serilog;
using Serilog.Events;
using Testably.Abstractions;
namespace ErsatzTV;
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
CurrentEnvironment = env;
}
public IConfiguration Configuration { get; }
private IWebHostEnvironment CurrentEnvironment { get; }
[SuppressMessage("Performance", "CA1861:Avoid constant arrays as arguments")]
public void ConfigureServices(IServiceCollection services)
{
BugsnagConfiguration bugsnagConfig = Configuration.GetSection("Bugsnag").Get<BugsnagConfiguration>();
services.Configure<BugsnagConfiguration>(Configuration.GetSection("Bugsnag"));
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.All;
options.ForwardLimit = 2;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
services.AddBugsnag(configuration =>
{
configuration.ApiKey = bugsnagConfig.ApiKey;
configuration.ProjectNamespaces = new[] { "ErsatzTV" };
configuration.AppVersion = Assembly.GetEntryAssembly()
?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown";
configuration.AutoNotify = true;
configuration.NotifyReleaseStages = new[] { "public", "develop" };
#if DEBUG || DEBUG_NO_SYNC
configuration.ReleaseStage = "develop";
#else
// effectively "disable" by tweaking app config
configuration.ReleaseStage = bugsnagConfig.Enable ? "public" : "private";
#endif
});
services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(FileSystemLayout.DataProtectionFolder));
services.AddOpenApi("v1", options => { options.ShouldInclude += a => a.GroupName == "general"; });
services.AddOpenApi(
"scripted-schedule-tagged",
options => { options.ShouldInclude += a => a.GroupName == "scripted-schedule"; });
services.AddOpenApi(
"scripted-schedule",
options =>
{
options.ShouldInclude += a => a.GroupName == "scripted-schedule";
var tag = new OpenApiTag { Name = "ScriptedSchedule" };
var tagReference = new OpenApiTagReference("ScriptedSchedule");
options.AddOperationTransformer((operation, _, _) =>
{
operation.Tags.Clear();
operation.Tags.Add(tagReference);
return Task.CompletedTask;
});
options.AddDocumentTransformer((document, _, _) =>
{
document.Tags.Clear();
document.Tags.Add(tag);
return Task.CompletedTask;
});
});
services.ConfigureHttpJsonOptions(o => o.SerializerOptions.NumberHandling = JsonNumberHandling.Strict);
OidcHelper.Init(Configuration);
JwtHelper.Init(Configuration);
SearchHelper.Init(Configuration);
if (OidcHelper.IsEnabled)
{
services.AddAuthentication(options =>
{
options.DefaultScheme = "cookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie(
"cookie",
options =>
{
options.CookieManager = new ChunkingCookieManager();
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
})
.AddOpenIdConnect(
"oidc",
options =>
{
options.Authority = OidcHelper.Authority;
options.ClientId = OidcHelper.ClientId;
options.ClientSecret = OidcHelper.ClientSecret;
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.ResponseMode = OpenIdConnectResponseMode.Query;
options.Scope.Clear();
options.Scope.Add("openid");
options.CallbackPath = new PathString("/callback");
options.SaveTokens = true;
options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always;
options.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
if (!string.IsNullOrWhiteSpace(OidcHelper.LogoutUri))
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = context =>
{
context.Response.Redirect(OidcHelper.LogoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
}
});
}
if (JwtHelper.IsEnabled)
{
services.AddAuthentication().AddJwtBearer(
"jwt",
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = JwtHelper.IssuerSigningKey,
ValidateLifetime = true
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = static context =>
{
StringValues token = context.Request.Query["access_token"];
if (!string.IsNullOrWhiteSpace(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
};
});
}
if (OidcHelper.IsEnabled || JwtHelper.IsEnabled)
{
services.AddAuthorization(options =>
{
if (OidcHelper.IsEnabled)
{
var defaultAuthorizationPolicyBuilder = new AuthorizationPolicyBuilder(
"cookie",
"oidc");
defaultAuthorizationPolicyBuilder =
defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
}
if (JwtHelper.IsEnabled)
{
var onlyJwtSchemePolicyBuilder = new AuthorizationPolicyBuilder("jwt");
options.AddPolicy(
"JwtOnlyScheme",
onlyJwtSchemePolicyBuilder
.RequireAuthenticatedUser()
.Build());
}
}
);
}
services.AddCors(o => o.AddPolicy(
"AllowAll",
builder =>
{
builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
services.AddLocalization();
services.AddControllers(options =>
{
options.OutputFormatters.Insert(0, new ConcatPlaylistOutputFormatter());
options.OutputFormatters.Insert(0, new ChannelPlaylistOutputFormatter());
options.OutputFormatters.Insert(0, new ChannelGuideOutputFormatter());
options.OutputFormatters.Insert(0, new DeviceXmlOutputFormatter());
options.OutputFormatters.Insert(0, new HdhrJsonOutputFormatter());
})
.AddNewtonsoftJson(opt =>
{
opt.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
opt.SerializerSettings.ContractResolver = new CustomContractResolver();
opt.SerializerSettings.Converters.Add(new StringEnumConverter());
});
services.AddScoped(_ => new ConditionalIptvAuthorizeFilter("JwtOnlyScheme"));
services.AddFluentValidationAutoValidation();
services.AddValidatorsFromAssemblyContaining<Startup>();
services.AddMemoryCache();
services.AddRazorPages(options =>
{
if (OidcHelper.IsEnabled)
{
options.Conventions.AuthorizeFolder("/");
}
});
services.AddServerSideBlazor()
.AddHubOptions(hubOptions => hubOptions.MaximumReceiveMessageSize = 1024 * 1024);
services.AddMudServices();
services.AddSortable();
var coreAssembly = Assembly.GetAssembly(typeof(LibraryScanProgress));
if (coreAssembly != null)
{
services.AddCourier(coreAssembly);
}
Console.OutputEncoding = Encoding.UTF8;
string etvVersion = Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "unknown";
Log.Logger.Information("ErsatzTV version {Version}", etvVersion);
Log.Logger.Warning(
"Give feedback at {GitHub} or {Discord}",
"https://github.com/ErsatzTV/ErsatzTV",
"https://discord.ersatztv.org");
CopyMacOsConfigFolderIfNeeded();
List<string> directoriesToCreate =
[
FileSystemLayout.AppDataFolder,
FileSystemLayout.TranscodeFolder,
FileSystemLayout.TempFilePoolFolder,
FileSystemLayout.FontsCacheFolder,
FileSystemLayout.TemplatesFolder,
FileSystemLayout.MusicVideoCreditsTemplatesFolder,
FileSystemLayout.ChannelStreamSelectorsFolder,
FileSystemLayout.ChannelGuideTemplatesFolder,
FileSystemLayout.GraphicsElementsTemplatesFolder,
FileSystemLayout.GraphicsElementsTextTemplatesFolder,
FileSystemLayout.GraphicsElementsImageTemplatesFolder,
FileSystemLayout.GraphicsElementsScriptTemplatesFolder,
FileSystemLayout.GraphicsElementsSubtitleTemplatesFolder,
FileSystemLayout.GraphicsElementsMotionTemplatesFolder,
FileSystemLayout.ScriptsFolder,
FileSystemLayout.MultiEpisodeShuffleTemplatesFolder,
FileSystemLayout.AudioStreamSelectorScriptsFolder,
FileSystemLayout.MpegTsScriptsFolder,
FileSystemLayout.DefaultMpegTsScriptFolder
];
foreach (string directory in directoriesToCreate)
{
if (directory is not null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
}
// until we add a setting for a file-specific scheme://host:port to access
// stream urls contained in this file, it doesn't make sense to do
// for now, continue to use scheme and host from incoming requests
// string xmltvPath = Path.Combine(appDataFolder, "xmltv.xml");
// Log.Logger.Information("XMLTV is at {XmltvPath}", xmltvPath);
string databaseProvider = Configuration.GetValue("provider", Provider.Sqlite.Name);
var sqliteConnectionString = $"Data Source={FileSystemLayout.DatabasePath};foreign keys=true;";
string mySqlConnectionString = Configuration.GetValue<string>("MySql:ConnectionString");
services.AddDbContext<TvContext>(
options =>
{
if (databaseProvider == Provider.Sqlite.Name)
{
TvContext.IsSqlite = true;
options.UseSqlite(
sqliteConnectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure.Sqlite");
});
}
if (databaseProvider == Provider.MySql.Name)
{
TvContext.IsSqlite = false;
options.UseMySql(
mySqlConnectionString,
ServerVersion.AutoDetect(mySqlConnectionString),
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure.MySql");
}
);
}
},
ServiceLifetime.Scoped,
ServiceLifetime.Singleton);
services.AddDbContextFactory<TvContext>(options =>
{
if (databaseProvider == Provider.Sqlite.Name)
{
options.UseSqlite(
sqliteConnectionString,
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure.Sqlite");
});
}
if (databaseProvider == Provider.MySql.Name)
{
options.UseMySql(
mySqlConnectionString,
ServerVersion.AutoDetect(mySqlConnectionString),
o =>
{
o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
o.MigrationsAssembly("ErsatzTV.Infrastructure.MySql");
}
);
}
});
if (databaseProvider == Provider.Sqlite.Name)
{
Log.Logger.Information("Database is at {DatabasePath}", FileSystemLayout.DatabasePath);
TvContext.LastInsertedRowId = "last_insert_rowid()";
TvContext.CaseInsensitiveCollation = "NOCASE";
SqlMapper.AddTypeHandler(new DateTimeOffsetHandler());
SqlMapper.AddTypeHandler(new GuidHandler());
SqlMapper.AddTypeHandler(new TimeSpanHandler());
}
if (databaseProvider == Provider.MySql.Name)
{
TvContext.LastInsertedRowId = "last_insert_id()";
TvContext.CaseInsensitiveCollation = "utf8mb4_general_ci";
}
Log.Logger.Information("Transcode folder is {Folder}", FileSystemLayout.TranscodeFolder);
services.AddMediatR(config => config.RegisterServicesFromAssemblyContaining<GetAllChannels>());
services.AddRefitClient<IPlexTvApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2"));
services.AddRefitClient<ITraktApi>(
new RefitSettings
{
ContentSerializer = new NewtonsoftJsonContentSerializer(
new JsonSerializerSettings
{
ContractResolver = new SnakeCasePropertyNamesContractResolver()
})
})
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://api.trakt.tv");
c.DefaultRequestHeaders.Add("User-Agent", $"ErsatzTV/{etvVersion}");
});
services.Configure<TraktConfiguration>(Configuration.GetSection("Trakt"));
services.AddResponseCompression(options => { options.EnableForHttps = true; });
CustomServices(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
string baseUrl = SystemEnvironment.BaseUrl;
if (!string.IsNullOrWhiteSpace(baseUrl))
{
try
{
app.UsePathBase(baseUrl);
}
catch (Exception ex)
{
Log.Error(
ex,
"Failed to configure ETV_BASE_URL; please check syntax and include leading slash e.g. `/etv`: {BaseUrl}",
baseUrl);
}
}
app.UseCors("AllowAll");
app.UseForwardedHeaders();
//app.UseHttpLogging();
app.UseSerilogRequestLogging(options =>
{
options.IncludeQueryInRequestPath = true;
// Emit debug-level events instead of the defaults
options.GetLevel = (httpContext, elapsed, ex) =>
{
if (ex is not null)
{
return LogEventLevel.Error;
}
if (httpContext.Response.StatusCode > 499)
{
return LogEventLevel.Error;
}
if (httpContext.Request.Path.ToUriComponent().StartsWith(
"/iptv",
StringComparison.OrdinalIgnoreCase))
{
return LogEventLevel.Debug;
}
if (httpContext.Request.Path.ToUriComponent().StartsWith(
"/api",
StringComparison.OrdinalIgnoreCase) &&
!httpContext.Request.Path.ToUriComponent().StartsWith(
"/api/scan",
StringComparison.OrdinalIgnoreCase))
{
return LogEventLevel.Debug;
}
return LogEventLevel.Verbose;
};
options.EnrichDiagnosticContext = (diagnosticContext, httpContext) =>
{
diagnosticContext.Set("RemoteIP", httpContext.Connection.RemoteIpAddress);
diagnosticContext.Set("UserAgent", httpContext.Request.Headers["User-Agent"]);
};
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.00} ms from {UserAgent} at {RemoteIP}";
});
app.UseRequestLocalization(options =>
{
CultureInfo[] cinfo = CultureInfo.GetCultures(CultureTypes.AllCultures & ~CultureTypes.NeutralCultures);
string[] supportedCultures = cinfo.Select(t => t.Name).Distinct().ToArray();
options.AddSupportedCultures(supportedCultures)
.AddSupportedUICultures(supportedCultures)
.SetDefaultCulture("en-US");
});
app.UseStaticFiles();
var extensionProvider = new FileExtensionContentTypeProvider();
// fix static file M3U8 mime type
extensionProvider.Mappings.Add(".m3u8", "application/vnd.apple.mpegurl");
// fix static file TS mime type
extensionProvider.Mappings.Remove(".ts");
extensionProvider.Mappings.Add(".ts", "video/mp2t");
app.UseStaticFiles(
new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(FileSystemLayout.TranscodeFolder),
RequestPath = "/iptv/session",
ContentTypeProvider = extensionProvider,
OnPrepareResponse = ctx =>
{
// Log.Logger.Information("Transcode access: {Test}", ctx.File.PhysicalPath);
ChannelWriter<IFFmpegWorkerRequest> writer = app.ApplicationServices
.GetRequiredService<ChannelWriter<IFFmpegWorkerRequest>>();
writer.TryWrite(new TouchFFmpegSession(ctx.File.PhysicalPath));
},
// to serve m4s
ServeUnknownFileTypes = true
});
app.UseResponseCompression();
app.Use(async (context, next) =>
{
if (!context.Request.Host.Value.StartsWith("localhost", StringComparison.OrdinalIgnoreCase) &&
!IsIptvPath(context.Request.Path) &&
context.Connection.LocalPort != Settings.UiPort)
{
context.Response.StatusCode = 404;
return;
}
await next(context);
});
app.MapWhen(
ctx => !IsIptvPath(ctx.Request.Path),
blazor =>
{
blazor.UseRouting();
if (OidcHelper.IsEnabled)
{
blazor.UseAuthentication();
#pragma warning disable ASP0001
blazor.UseAuthorization();
#pragma warning restore ASP0001
}
blazor.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
if (CurrentEnvironment.IsDevelopment())
{
endpoints.MapOpenApi();
}
endpoints.MapScalarApiReference("/docs", options =>
{
options.AddDocument(
"scripted-schedule",
"Scripted Schedule",
"openapi/scripted-schedule-tagged.json");
options.AddDocument("v1", "General", "openapi/v1.json");
options.HideClientButton = true;
options.DocumentDownloadType = DocumentDownloadType.None;
options.Title = "ErsatzTV API Reference";
});
});
});
app.MapWhen(
ctx => IsIptvPath(ctx.Request.Path),
iptv =>
{
iptv.UseRouting();
iptv.UseEndpoints(endpoints => endpoints.MapControllers());
});
return;
bool IsIptvPath(PathString path)
{
return path.StartsWithSegments("/iptv") ||
path.StartsWithSegments("/discover.json") ||
path.StartsWithSegments("/device.xml") ||
path.StartsWithSegments("/lineup.json") ||
path.StartsWithSegments("/lineup_status.json");
}
}
private static void CustomServices(IServiceCollection services)
{
services.AddSingleton<IEnvironmentValidator, EnvironmentValidator>();
services.AddSingleton<IFileSystem, RealFileSystem>();
services.AddSingleton<IDatabaseMigrations, DatabaseMigrations>();
services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
services.AddSingleton<ITraktApiClient, TraktApiClient>();
services.AddSingleton<IEntityLocker, EntityLocker>();
services.AddSingleton<ISearchTargets, SearchTargets>();
services.AddSingleton<ISmartCollectionCache, SmartCollectionCache>();
services.AddSingleton<SearchQueryParser>();
services.AddSingleton<ITroubleshootingNotifier, TroubleshootingNotifier>();
services.AddSingleton<CustomFontMapper>();
services.AddSingleton<GraphicsEngineFonts>();
services.AddSingleton(Program.InMemoryLogService);
if (SearchHelper.IsElasticSearchEnabled)
{
Log.Logger.Information("Using Elasticsearch (external) search index backend");
ElasticSearchIndex.Uri = new Uri(SearchHelper.ElasticSearchUri);
ElasticSearchIndex.IndexName = SearchHelper.ElasticSearchIndexName;
services.AddSingleton<ISearchIndex, ElasticSearchIndex>();
}
else
{
Log.Logger.Information("Using Lucene (embedded) search index backend");
services.AddSingleton<ISearchIndex, LuceneSearchIndex>();
}
services.AddSingleton<IScannerProxyService, ScannerProxyService>();
services.AddSingleton<IScriptedPlayoutBuilderService, ScriptedPlayoutBuilderService>();
services.AddSingleton<IFFmpegSegmenterService, FFmpegSegmenterService>();
services.AddSingleton<ITempFilePool, TempFilePool>();
services.AddSingleton<IHlsPlaylistFilter, HlsPlaylistFilter>();
services.AddSingleton<RecyclableMemoryStreamManager>();
services.AddSingleton<SystemStartup>();
services.AddSingleton<ILanguageCodeCache, LanguageCodeCache>();
AddChannel<IBackgroundServiceRequest>(services);
AddChannel<IPlexBackgroundServiceRequest>(services);
AddChannel<IJellyfinBackgroundServiceRequest>(services);
AddChannel<IEmbyBackgroundServiceRequest>(services);
AddChannel<IFFmpegWorkerRequest>(services);
AddChannel<ISearchIndexBackgroundServiceRequest>(services);
AddChannel<IScannerBackgroundServiceRequest>(services);
services.AddScoped<IMacOsConfigFolderHealthCheck, MacOsConfigFolderHealthCheck>();
services.AddScoped<IFFmpegVersionHealthCheck, FFmpegVersionHealthCheck>();
services.AddScoped<IFFmpegReportsHealthCheck, FFmpegReportsHealthCheck>();
services.AddScoped<IHardwareAccelerationHealthCheck, HardwareAccelerationHealthCheck>();
services.AddScoped<IMovieMetadataHealthCheck, MovieMetadataHealthCheck>();
services.AddScoped<IEpisodeMetadataHealthCheck, EpisodeMetadataHealthCheck>();
services.AddScoped<IZeroDurationHealthCheck, ZeroDurationHealthCheck>();
services.AddScoped<IFileNotFoundHealthCheck, FileNotFoundHealthCheck>();
services.AddScoped<IUnavailableHealthCheck, UnavailableHealthCheck>();
services.AddScoped<IVaapiDriverHealthCheck, VaapiDriverHealthCheck>();
services.AddScoped<IErrorReportsHealthCheck, ErrorReportsHealthCheck>();
services.AddScoped<IUnifiedDockerHealthCheck, UnifiedDockerHealthCheck>();
services.AddScoped<IDowngradeHealthCheck, DowngradeHealthCheck>();
services.AddScoped<IHealthCheckService, HealthCheckService>();
services.AddScoped<IChannelRepository, ChannelRepository>();
services.AddScoped<IFFmpegProfileRepository, FFmpegProfileRepository>();
services.AddScoped<IMediaSourceRepository, MediaSourceRepository>();
services.AddScoped<IMediaItemRepository, MediaItemRepository>();
services.AddScoped<IMediaCollectionRepository, MediaCollectionRepository>();
services.AddScoped<IConfigElementRepository, ConfigElementRepository>();
services.AddScoped<ITelevisionRepository, TelevisionRepository>();
services.AddScoped<ISearchRepository, SearchRepository>();
services.AddScoped<IMovieRepository, MovieRepository>();
services.AddScoped<IArtistRepository, ArtistRepository>();
services.AddScoped<IMusicVideoRepository, MusicVideoRepository>();
services.AddScoped<IOtherVideoRepository, OtherVideoRepository>();
services.AddScoped<ISongRepository, SongRepository>();
services.AddScoped<IImageRepository, ImageRepository>();
services.AddScoped<IRemoteStreamRepository, RemoteStreamRepository>();
services.AddScoped<ILibraryRepository, LibraryRepository>();
services.AddScoped<IMetadataRepository, MetadataRepository>();
services.AddScoped<IArtworkRepository, ArtworkRepository>();
services.AddScoped<ICollectionEtag, CollectionEtag>();
services.AddScoped<IFFmpegLocator, FFmpegLocator>();
services.AddScoped<IFallbackMetadataProvider, FallbackMetadataProvider>();
services.AddScoped<ILocalStatisticsProvider, LocalStatisticsProvider>();
services.AddScoped<IExternalJsonPlayoutItemProvider, ExternalJsonPlayoutItemProvider>();
services.AddScoped<IPlayoutBuilder, PlayoutBuilder>();
services.AddScoped<IBlockPlayoutBuilder, BlockPlayoutBuilder>();
services.AddScoped<IBlockPlayoutPreviewBuilder, BlockPlayoutPreviewBuilder>();
services.AddScoped<IBlockPlayoutFillerBuilder, BlockPlayoutFillerBuilder>();
services.AddScoped<ISequentialPlayoutBuilder, SequentialPlayoutBuilder>();
services.AddScoped<IScriptedPlayoutBuilder, ScriptedPlayoutBuilder>();
services.AddScoped<ISchedulingEngine, SchedulingEngine>();
services.AddScoped<IExternalJsonPlayoutBuilder, ExternalJsonPlayoutBuilder>();
services.AddScoped<IPlayoutTimeShifter, PlayoutTimeShifter>();
services.AddScoped<IImageCache, ImageCache>();
services.AddScoped<ILocalFileSystem, LocalFileSystem>();
services.AddScoped<IPlexServerApiClient, PlexServerApiClient>();
services.AddScoped<IPlexMovieRepository, PlexMovieRepository>();
services.AddScoped<IPlexTelevisionRepository, PlexTelevisionRepository>();
services.AddScoped<IPlexCollectionRepository, PlexCollectionRepository>();
services.AddScoped<IJellyfinApiClient, JellyfinApiClient>();
services.AddScoped<IJellyfinPathReplacementService, JellyfinPathReplacementService>();
services.AddScoped<IJellyfinTelevisionRepository, JellyfinTelevisionRepository>();
services.AddScoped<IJellyfinCollectionRepository, JellyfinCollectionRepository>();
services.AddScoped<IJellyfinMovieRepository, JellyfinMovieRepository>();
services.AddScoped<IEmbyApiClient, EmbyApiClient>();
services.AddScoped<IEmbyPathReplacementService, EmbyPathReplacementService>();
services.AddScoped<IEmbyTelevisionRepository, EmbyTelevisionRepository>();
services.AddScoped<IEmbyCollectionRepository, EmbyCollectionRepository>();
services.AddScoped<IEmbyMovieRepository, EmbyMovieRepository>();
services.AddScoped<IRuntimeInfo, RuntimeInfo>();
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
services.AddScoped<ICustomStreamSelector, CustomStreamSelector>();
services.AddScoped<IFFmpegStreamSelector, FFmpegStreamSelector>();
services.AddScoped<IStreamSelectorRepository, StreamSelectorRepository>();
services.AddScoped<IHardwareCapabilitiesFactory, HardwareCapabilitiesFactory>();
services.AddScoped<IMultiEpisodeShuffleCollectionEnumeratorFactory,
MultiEpisodeShuffleCollectionEnumeratorFactory>();
services.AddScoped<IRerunHelper, RerunHelper>();
services.AddScoped<IChannelLogoGenerator, ChannelLogoGenerator>();
services.AddScoped<IGraphicsEngine, GraphicsEngine>();
services.AddScoped<IGraphicsElementRepository, GraphicsElementRepository>();
services.AddScoped<ITemplateDataRepository, TemplateDataRepository>();
services.AddScoped<IGraphicsElementLoader, GraphicsElementLoader>();
services.AddScoped<TemplateFunctions>();
services.AddScoped<IDecoSelector, DecoSelector>();
services.AddScoped<IWatermarkSelector, WatermarkSelector>();
services.AddScoped<IGraphicsElementSelector, GraphicsElementSelector>();
services.AddScoped<IHlsInitSegmentCache, HlsInitSegmentCache>();
services.AddScoped<IMpegTsScriptService, MpegTsScriptService>();
services.AddScoped<ILanguageCodeService, LanguageCodeService>();
services.AddScoped<IFFmpegProcessService, FFmpegLibraryProcessService>();
services.AddScoped<IPipelineBuilderFactory, PipelineBuilderFactory>();
services.AddScoped<FFmpegProcessService>();
services.AddScoped<ISongVideoGenerator, SongVideoGenerator>();
services.AddScoped<IMusicVideoCreditsGenerator, MusicVideoCreditsGenerator>();
services.AddScoped<IGitHubApiClient, GitHubApiClient>();
services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(_ =>
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.Add("class");
return sanitizer;
});
services.AddScoped<IJellyfinSecretStore, JellyfinSecretStore>();
services.AddScoped<IEmbySecretStore, EmbySecretStore>();
services.AddScoped<IScriptEngine, ScriptEngine>();
services.AddScoped<ISequentialScheduleValidator, SequentialScheduleValidator>();
services.AddScoped<PlexEtag>();
// services.AddTransient(typeof(IRequestHandler<,>), typeof(GetRecentLogEntriesHandler<>));
// run-once/blocking startup services
services.AddHostedService<EndpointValidatorService>();
services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<DatabaseCleanerService>();
services.AddHostedService<LoadLoggingLevelService>();
services.AddHostedService<CacheCleanerService>();
services.AddHostedService<ResourceExtractorService>();
services.AddHostedService<PlatformSettingsService>();
services.AddHostedService<RebuildSearchIndexService>();
services.AddHostedService<RunHealthChecksService>();
// background services
#if !DEBUG_NO_SYNC
services.AddHostedService<EmbyService>();
services.AddHostedService<JellyfinService>();
services.AddHostedService<PlexService>();
services.AddHostedService<ScannerService>();
#endif
services.AddHostedService<FFmpegLocatorService>();
services.AddHostedService<WorkerService>();
services.AddHostedService<SchedulerService>();
services.AddHostedService<FFmpegWorkerService>();
services.AddHostedService<SearchIndexService>();
}
private static void AddChannel<TMessageType>(IServiceCollection services)
{
services.AddSingleton(
Channel.CreateUnbounded<TMessageType>(new UnboundedChannelOptions { SingleReader = true }));
services.AddSingleton(provider => provider.GetRequiredService<Channel<TMessageType>>().Reader);
services.AddSingleton(provider => provider.GetRequiredService<Channel<TMessageType>>().Writer);
}
private static void CopyMacOsConfigFolderIfNeeded()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return;
}
bool newDbExists = File.Exists(FileSystemLayout.DatabasePath);
if (newDbExists)
{
return;
}
bool oldDbExists = File.Exists(FileSystemLayout.MacOsOldDatabasePath);
if (!oldDbExists)
{
return;
}
// safe to move here since
// - old db exists
// - new db does not exist
Log.Logger.Information(
"Migrating config data from {OldFolder} to {NewFolder}",
FileSystemLayout.MacOsOldAppDataFolder,
FileSystemLayout.AppDataFolder);
try
{
// delete new config folder
if (Directory.Exists(FileSystemLayout.AppDataFolder))
{
Directory.Delete(FileSystemLayout.AppDataFolder, true);
}
// move old config folder to new config folder
Directory.Move(FileSystemLayout.MacOsOldAppDataFolder, FileSystemLayout.AppDataFolder);
}
catch (Exception ex)
{
Log.Logger.Warning(ex, "Failed to migrate config data");
}
}
}