using System; using System.Data; using System.IO; using System.Reflection; using System.Threading.Channels; using Dapper; using ErsatzTV.Application; using ErsatzTV.Application.Channels.Queries; using ErsatzTV.Core; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; 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.Metadata; using ErsatzTV.Core.Plex; using ErsatzTV.Core.Scheduling; using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Locking; using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Serialization; using ErsatzTV.Services; using FluentValidation.AspNetCore; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using MudBlazor.Services; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Refit; using Serilog; namespace ErsatzTV { public class Startup { public Startup(IConfiguration configuration) => Configuration = configuration; public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { 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()); }) .AddFluentValidation( options => { options.RegisterValidatorsFromAssemblyContaining(); options.ImplicitlyValidateChildProperties = true; }); services.AddSwaggerGen( c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "ErsatzTV API", Version = "v1" }); }); services.AddSwaggerGenNewtonsoftSupport(); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddMudServices(); Log.Logger.Information( "ErsatzTV version {Version}", Assembly.GetEntryAssembly().GetCustomAttribute() ?.InformationalVersion ?? "unknown"); Log.Logger.Warning("This is pre-alpha software and is likely to be unstable"); Log.Logger.Warning( "Give feedback at {GitHub} or {Discord}", "https://github.com/jasongdove/ErsatzTV", "https://discord.gg/hHaJm3yGy6"); Log.Logger.Information( "Server will listen on port {Port} - try UI at {UI}", 8409, "http://localhost:8409"); if (!Directory.Exists(FileSystemLayout.AppDataFolder)) { Directory.CreateDirectory(FileSystemLayout.AppDataFolder); } Log.Logger.Information("Database is at {DatabasePath}", FileSystemLayout.DatabasePath); // 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); var connectionString = $"Data Source={FileSystemLayout.DatabasePath}"; services.AddDbContext( options => options.UseSqlite( connectionString, o => { o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); o.MigrationsAssembly("ErsatzTV.Infrastructure"); }), ServiceLifetime.Scoped, ServiceLifetime.Singleton); services.AddDbContextFactory( options => options.UseSqlite( connectionString, o => { o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); o.MigrationsAssembly("ErsatzTV.Infrastructure"); })); services.AddTransient(_ => new SqliteConnection(connectionString)); SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); SqlMapper.AddTypeHandler(new GuidHandler()); SqlMapper.AddTypeHandler(new TimeSpanHandler()); services.AddDbContext( options => options.UseSqlite($"Data Source={FileSystemLayout.LogDatabasePath}")); services.AddMediatR(typeof(GetAllChannels).Assembly); services.AddRefitClient() .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2")); CustomServices(services); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // app.UseSerilogRequestLogging(); app.UseSwagger(); app.UseSwaggerUI( c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "ErsatzTV API"); }); app.UseStaticFiles(); app.UseRouting(); app.UseEndpoints( endpoints => { endpoints.MapControllers(); endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); } private void CustomServices(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // TODO: does this need to be singleton? services.AddSingleton(); AddChannel(services); AddChannel(services); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); } private void AddChannel(IServiceCollection services) { services.AddSingleton( Channel.CreateUnbounded(new UnboundedChannelOptions { SingleReader = true })); services.AddSingleton( provider => provider.GetRequiredService>().Reader); services.AddSingleton( provider => provider.GetRequiredService>().Writer); } } }