diff --git a/CHANGELOG.md b/CHANGELOG.md index eb55f7425..3cbbc5904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Error streams will prioritize using `background.png` if it exists - Replacing this `background.png` file will allow custom error/offline backgrounds - Add `Troubleshoot Playback` buttons on movie and episode detail pages +- Add song background and missing album art customization + - Default files start with an underscore; custom versions must remove the underscore ### Fixed - Fix HLS Direct playback with Jellyfin 10.11 diff --git a/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs b/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs index 3d0caeecc..9ca268937 100644 --- a/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs +++ b/ErsatzTV.Application/Channels/Commands/RefreshChannelDataHandler.cs @@ -881,13 +881,9 @@ public class RefreshChannelDataHandler : IRequestHandler private string GetMovieTemplateFileName() { - string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "movie.sbntxt"); - - // fall back to default template - if (!_localFileSystem.FileExists(templateFileName)) - { - templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_movie.sbntxt"); - } + string templateFileName = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ChannelGuideTemplatesFolder, + "movie.sbntxt"); // fail if file doesn't exist if (!_localFileSystem.FileExists(templateFileName)) @@ -904,13 +900,9 @@ public class RefreshChannelDataHandler : IRequestHandler private string GetEpisodeTemplateFileName() { - string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "episode.sbntxt"); - - // fall back to default template - if (!_localFileSystem.FileExists(templateFileName)) - { - templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_episode.sbntxt"); - } + string templateFileName = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ChannelGuideTemplatesFolder, + "episode.sbntxt"); // fail if file doesn't exist if (!_localFileSystem.FileExists(templateFileName)) @@ -927,13 +919,9 @@ public class RefreshChannelDataHandler : IRequestHandler private string GetMusicVideoTemplateFileName() { - string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "musicVideo.sbntxt"); - - // fall back to default template - if (!_localFileSystem.FileExists(templateFileName)) - { - templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_musicVideo.sbntxt"); - } + string templateFileName = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ChannelGuideTemplatesFolder, + "musicVideo.sbntxt"); // fail if file doesn't exist if (!_localFileSystem.FileExists(templateFileName)) @@ -950,13 +938,9 @@ public class RefreshChannelDataHandler : IRequestHandler private string GetSongTemplateFileName() { - string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "song.sbntxt"); - - // fall back to default template - if (!_localFileSystem.FileExists(templateFileName)) - { - templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_song.sbntxt"); - } + string templateFileName = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ChannelGuideTemplatesFolder, + "song.sbntxt"); // fail if file doesn't exist if (!_localFileSystem.FileExists(templateFileName)) @@ -973,13 +957,9 @@ public class RefreshChannelDataHandler : IRequestHandler private string GetOtherVideoTemplateFileName() { - string templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "otherVideo.sbntxt"); - - // fall back to default template - if (!_localFileSystem.FileExists(templateFileName)) - { - templateFileName = Path.Combine(FileSystemLayout.ChannelGuideTemplatesFolder, "_otherVideo.sbntxt"); - } + string templateFileName = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ChannelGuideTemplatesFolder, + "otherVideo.sbntxt"); // fail if file doesn't exist if (!_localFileSystem.FileExists(templateFileName)) diff --git a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs index f244388f1..02874a635 100644 --- a/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs +++ b/ErsatzTV.Core.Tests/Fakes/FakeLocalFileSystem.cs @@ -5,8 +5,6 @@ namespace ErsatzTV.Core.Tests.Fakes; public class FakeLocalFileSystem : ILocalFileSystem { - public static readonly byte[] TestBytes = { 1, 2, 3, 4, 5 }; - private readonly List _files; private readonly List _folders; @@ -75,7 +73,11 @@ public class FakeLocalFileSystem : ILocalFileSystem public Task GetHash(string path) => throw new NotSupportedException(); - public Task ReadAllBytes(string path) => TestBytes.AsTask(); + public string GetCustomOrDefaultFile(string folder, string file) + { + string path = Path.Combine(folder, file); + return FileExists(path) ? path : Path.Combine(folder, $"_{file}"); + } private static List Split(DirectoryInfo path) { diff --git a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs index b189ea286..86188fe90 100644 --- a/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs +++ b/ErsatzTV.Core/FFmpeg/FFmpegLibraryProcessService.cs @@ -30,6 +30,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService private readonly IMpegTsScriptService _mpegTsScriptService; private readonly ILocalStatisticsProvider _localStatisticsProvider; private readonly IMediaItemRepository _mediaItemRepository; + private readonly ILocalFileSystem _localFileSystem; private readonly ICustomStreamSelector _customStreamSelector; private readonly FFmpegProcessService _ffmpegProcessService; private readonly IFFmpegStreamSelector _ffmpegStreamSelector; @@ -49,6 +50,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService IMpegTsScriptService mpegTsScriptService, ILocalStatisticsProvider localStatisticsProvider, IMediaItemRepository mediaItemRepository, + ILocalFileSystem localFileSystem, ILogger logger) { _ffmpegProcessService = ffmpegProcessService; @@ -62,6 +64,7 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService _mpegTsScriptService = mpegTsScriptService; _localStatisticsProvider = localStatisticsProvider; _mediaItemRepository = mediaItemRepository; + _localFileSystem = localFileSystem; _logger = logger; } @@ -759,11 +762,9 @@ public class FFmpegLibraryProcessService : IFFmpegProcessService } } - string videoPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "background.png"); - if (!File.Exists(videoPath)) - { - videoPath = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "_background.png"); - } + string videoPath = _localFileSystem.GetCustomOrDefaultFile( + FileSystemLayout.ResourcesCacheFolder, + "background.png"); var videoVersion = BackgroundImageMediaVersion.ForPath(videoPath, desiredResolution); diff --git a/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs b/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs index 95f4ba993..e2a45d880 100644 --- a/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs +++ b/ErsatzTV.Core/FFmpeg/SongVideoGenerator.cs @@ -3,6 +3,7 @@ using System.Text; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.Images; +using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.FFmpeg.State; namespace ErsatzTV.Core.FFmpeg; @@ -12,6 +13,7 @@ public class SongVideoGenerator : ISongVideoGenerator private static readonly Random Random = new(); private static readonly Lock RandomLock = new(); private readonly IFFmpegProcessService _ffmpegProcessService; + private readonly ILocalFileSystem _localFileSystem; private readonly IImageCache _imageCache; private readonly ITempFilePool _tempFilePool; @@ -19,11 +21,13 @@ public class SongVideoGenerator : ISongVideoGenerator public SongVideoGenerator( ITempFilePool tempFilePool, IImageCache imageCache, - IFFmpegProcessService ffmpegProcessService) + IFFmpegProcessService ffmpegProcessService, + ILocalFileSystem localFileSystem) { _tempFilePool = tempFilePool; _imageCache = imageCache; _ffmpegProcessService = ffmpegProcessService; + _localFileSystem = localFileSystem; } public async Task> GenerateSongVideo( @@ -46,14 +50,14 @@ public class SongVideoGenerator : ISongVideoGenerator }; string[] backgrounds = - { + [ "song_background_1.png", "song_background_2.png", "song_background_3.png" - }; + ]; // use random ETV color by default - string backgroundPath = Path.Combine( + string backgroundPath = _localFileSystem.GetCustomOrDefaultFile( FileSystemLayout.ResourcesCacheFolder, backgrounds[NextRandom(backgrounds.Length)]); @@ -163,7 +167,7 @@ public class SongVideoGenerator : ISongVideoGenerator { Id = 0, ArtworkKind = ArtworkKind.Thumbnail, - Path = Path.Combine(FileSystemLayout.ResourcesCacheFolder, "song_album_cover_512.png") + Path = _localFileSystem.GetCustomOrDefaultFile(FileSystemLayout.ResourcesCacheFolder, "song_album_cover_512.png") }); // signal that we want to use cover art as watermark diff --git a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs index ba4f8e027..6c0c492bf 100644 --- a/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs +++ b/ErsatzTV.Core/Interfaces/Metadata/ILocalFileSystem.cs @@ -18,4 +18,5 @@ public interface ILocalFileSystem Task ReadAllText(string path); Task ReadAllLines(string path); Task GetHash(string path); + string GetCustomOrDefaultFile(string folder, string file); } diff --git a/ErsatzTV.Core/Metadata/LocalFileSystem.cs b/ErsatzTV.Core/Metadata/LocalFileSystem.cs index 40c819c8c..d57d239c5 100644 --- a/ErsatzTV.Core/Metadata/LocalFileSystem.cs +++ b/ErsatzTV.Core/Metadata/LocalFileSystem.cs @@ -194,4 +194,10 @@ public class LocalFileSystem(IClient client, ILogger logger) : await using var stream = File.OpenRead(path); return await md5.ComputeHashAsync(stream); } + + public string GetCustomOrDefaultFile(string folder, string file) + { + string path = Path.Combine(folder, file); + return FileExists(path) ? path : Path.Combine(folder, $"_{file}"); + } } diff --git a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs index 5a5ba11f5..b9ae0c590 100644 --- a/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs +++ b/ErsatzTV.Scanner.Tests/Core/FFmpeg/TranscodingTests.cs @@ -285,9 +285,10 @@ public class TranscodingTests Substitute.For(), Substitute.For(), Substitute.For(), + localFileSystem, LoggerFactory.CreateLogger()); - var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service); + var songVideoGenerator = new SongVideoGenerator(tempFilePool, mockImageCache, service, localFileSystem); var channel = new Channel(Guid.NewGuid()) { @@ -994,6 +995,7 @@ public class TranscodingTests Substitute.For(), Substitute.For(), Substitute.For(), + Substitute.For(), LoggerFactory.CreateLogger()); return service; diff --git a/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs b/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs index 12dd88c3b..c6a216816 100644 --- a/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs +++ b/ErsatzTV.Scanner.Tests/Core/Fakes/FakeLocalFileSystem.cs @@ -6,8 +6,6 @@ namespace ErsatzTV.Scanner.Tests.Core.Fakes; public class FakeLocalFileSystem : ILocalFileSystem { - public static readonly byte[] TestBytes = { 1, 2, 3, 4, 5 }; - private readonly List _files; private readonly List _folders; @@ -64,7 +62,11 @@ public class FakeLocalFileSystem : ILocalFileSystem public Task ReadAllLines(string path) => throw new NotImplementedException(); public Task GetHash(string path) => throw new NotImplementedException(); - public Task ReadAllBytes(string path) => TestBytes.AsTask(); + public string GetCustomOrDefaultFile(string folder, string file) + { + string path = Path.Combine(folder, file); + return FileExists(path) ? path : Path.Combine(folder, $"_{file}"); + } private static List Split(DirectoryInfo path) { diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 566f62306..953313103 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -85,10 +85,10 @@ - - - - + + + + diff --git a/ErsatzTV/Resources/song_album_cover_512.png b/ErsatzTV/Resources/_song_album_cover_512.png similarity index 100% rename from ErsatzTV/Resources/song_album_cover_512.png rename to ErsatzTV/Resources/_song_album_cover_512.png diff --git a/ErsatzTV/Resources/song_background_1.png b/ErsatzTV/Resources/_song_background_1.png similarity index 100% rename from ErsatzTV/Resources/song_background_1.png rename to ErsatzTV/Resources/_song_background_1.png diff --git a/ErsatzTV/Resources/song_background_2.png b/ErsatzTV/Resources/_song_background_2.png similarity index 100% rename from ErsatzTV/Resources/song_background_2.png rename to ErsatzTV/Resources/_song_background_2.png diff --git a/ErsatzTV/Resources/song_background_3.png b/ErsatzTV/Resources/_song_background_3.png similarity index 100% rename from ErsatzTV/Resources/song_background_3.png rename to ErsatzTV/Resources/_song_background_3.png diff --git a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs index 506d72883..0aed40f84 100644 --- a/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs +++ b/ErsatzTV/Services/RunOnce/ResourceExtractorService.cs @@ -17,10 +17,10 @@ public class ResourceExtractorService : BackgroundService Assembly assembly = typeof(ResourceExtractorService).GetTypeInfo().Assembly; await ExtractResource(assembly, "_background.png", stoppingToken); - await ExtractResource(assembly, "song_album_cover_512.png", stoppingToken); - await ExtractResource(assembly, "song_background_1.png", stoppingToken); - await ExtractResource(assembly, "song_background_2.png", stoppingToken); - await ExtractResource(assembly, "song_background_3.png", stoppingToken); + await ExtractResource(assembly, "_song_album_cover_512.png", stoppingToken); + await ExtractResource(assembly, "_song_background_1.png", stoppingToken); + await ExtractResource(assembly, "_song_background_2.png", stoppingToken); + await ExtractResource(assembly, "_song_background_3.png", stoppingToken); await ExtractResource(assembly, "song_progress_overlay.png", stoppingToken); await ExtractResource(assembly, "song_progress_overlay_43.png", stoppingToken); await ExtractResource(assembly, "ErsatzTV.png", stoppingToken);