Compare commits
41
Commits
v0.3.1-alpha
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0440f7643b | ||
|
|
0f4219f731 | ||
|
|
cbe5d47611 | ||
|
|
afa52ccc89 | ||
|
|
7d1163c68f | ||
|
|
883492bd33 | ||
|
|
a4eac4feea | ||
|
|
dab58f5840 | ||
|
|
176f136c23 | ||
|
|
816d77e15b | ||
|
|
7c4d47a211 | ||
|
|
d9d2cfa8be | ||
|
|
8036e46966 | ||
|
|
594ce437fb | ||
|
|
004c43f895 | ||
|
|
257384ea9b | ||
|
|
637f3a0c8b | ||
|
|
7346808059 | ||
|
|
4210d97ee2 | ||
|
|
6a8ecd2532 | ||
|
|
9b834f7cbe | ||
|
|
7b73677bad | ||
|
|
85b2a46353 | ||
|
|
6f40f2cbd6 | ||
|
|
b62ee4dee9 | ||
|
|
a6e7f192cc | ||
|
|
59a1a4a8dc | ||
|
|
85a9afb51c | ||
|
|
246b4d7591 | ||
|
|
ae2c6350e1 | ||
|
|
ce228604e8 | ||
|
|
3656e932d3 | ||
|
|
73887706ed | ||
|
|
abc103308b | ||
|
|
3773bbec19 | ||
|
|
e223d6a43f | ||
|
|
8369111e31 | ||
|
|
35ba2bab2c | ||
|
|
094ed71ad0 | ||
|
|
89e24b2b78 | ||
|
|
848795af32 |
@@ -10,7 +10,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout master
|
- name: Checkout master
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Deploy docs
|
- name: Deploy docs
|
||||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||||
|
|||||||
@@ -47,14 +47,11 @@ jobs:
|
|||||||
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
release_name="ErsatzTV-$tag-${{ matrix.target }}"
|
||||||
|
|
||||||
# Build everything
|
# Build everything
|
||||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:PublishSingleFile=true --self-contained true
|
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" /property:InformationalVersion="${tag:1}-${{ matrix.target }}" /property:EnableCompressionInSingleFile=true /property:DebugType=Embedded /property:PublishSingleFile=true --self-contained true
|
||||||
|
|
||||||
# Pack files
|
# Pack files
|
||||||
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
if [ "${{ matrix.target }}" == "win-x64" ]; then
|
||||||
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
||||||
elif [ "${{ matrix.target }}" == "linux-arm" ]; then
|
|
||||||
cp lib/linux-arm/* "$release_name/"
|
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
|
||||||
else
|
else
|
||||||
tar czvf "${release_name}.tar.gz" "$release_name"
|
tar czvf "${release_name}.tar.gz" "$release_name"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+81
-1
@@ -5,6 +5,80 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- Fix local folder scanners to properly detect removed/re-added folders with unchanged contents
|
||||||
|
- Fix double-click startup on mac
|
||||||
|
- Fix trakt list sync when show does not contain a year
|
||||||
|
- Properly unlock libraries when a scan is unable to be performed because ffmpeg or ffprobe have not been found
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add trash system for local libraries to maintain collection and schedule integrity through media share outages
|
||||||
|
- When items are missing from disk, they will be flagged and present in the `Media` > `Trash` page
|
||||||
|
- The trash page can be used to permanently remove missing items from the database
|
||||||
|
- When items reappear at the expected location on disk, they will be unflagged and removed from the trash
|
||||||
|
- Add basic Mac hardware acceleration using VideoToolbox
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Local libraries only: when items are missing from disk, they will be added to the trash and no longer removed from collections, etc.
|
||||||
|
- Show song thumbnail in song list
|
||||||
|
|
||||||
|
## [0.3.6-alpha] - 2022-01-10
|
||||||
|
### Fixed
|
||||||
|
- Properly index `minutes` field when adding new items during scan (vs when rebuilding index)
|
||||||
|
- Fix some nvenc edge cases where only padding is needed for normalization
|
||||||
|
- Properly overwrite environment variables for ffmpeg processes (`LIBVA_DRIVER_NAME`, `FFREPORT`)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Add music video `artist` to search index
|
||||||
|
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Remove `HLS Hybrid` streaming mode; all channels have been reconfigured to use the superior `HLS Segmenter` streaming mode
|
||||||
|
- Update `MPEG-TS` streaming mode to internally use the HLS segmenter
|
||||||
|
- This improves compatibility with many clients and also improves performance at program boundaries
|
||||||
|
- Renamed existing `MPEG-TS` mode as `MPEG-TS (Legacy)`
|
||||||
|
- This mode will be removed in a future release
|
||||||
|
|
||||||
|
## [0.3.5-alpha] - 2022-01-05
|
||||||
|
### Fixed
|
||||||
|
- Fix bundled ffmpeg version in base docker image (NOT nvidia or vaapi) which prevented playback since v0.3.0-alpha
|
||||||
|
- Use software decoding for mpeg4 content when VAAPI acceleration is enabled
|
||||||
|
- Fix hardware acceleration health check to recognize QSV on non-Windows platforms
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Treat `setsar` as a hardware filter, avoiding unneeded `hwdownload` and `hwupload` steps when padding isn't required
|
||||||
|
|
||||||
|
## [0.3.4-alpha] - 2021-12-21
|
||||||
|
### Fixed
|
||||||
|
- Fix other video and song scanners to include videos contained directly in top-level folders that are added to a library
|
||||||
|
- Allow saving ffmpeg troubleshooting reports on Windows
|
||||||
|
|
||||||
|
## [0.3.3-alpha] - 2021-12-12
|
||||||
|
### Fixed
|
||||||
|
- Fix bug with saving multiple blurhash versions for cover art; all cover art will be automatically rescanned
|
||||||
|
- Fix song detail margin when no cover art exists and no watermark exists
|
||||||
|
- Fix synchronizing virtual shows and seasons from Jellyfin
|
||||||
|
- Properly sort channels in M3U
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use blurhash of ErsatzTV colors instead of solid colors for default song backgrounds
|
||||||
|
- Use select control instead of autocomplete control in many places
|
||||||
|
- The autocomplete control is not intuitive to use and has focus bugs
|
||||||
|
|
||||||
|
## [0.3.2-alpha] - 2021-12-03
|
||||||
|
### Fixed
|
||||||
|
- Fix artwork upload on Windows
|
||||||
|
- Fix unicode song metadata on Windows
|
||||||
|
- Fix unicode console output on Windows
|
||||||
|
- Fix TV Show NFO metadata processing when `year` is missing
|
||||||
|
- Fix song detail outline to help legibility on white backgrounds
|
||||||
|
- Optimize song artwork scanning to prevent re-processing album artwork for each song
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use custom log database backend which should be more portable (i.e. work in osx-arm64)
|
||||||
|
- Use cover art blurhashes for song backgrounds instead of solid colors or box blur
|
||||||
|
|
||||||
|
## [0.3.1-alpha] - 2021-11-30
|
||||||
|
### Fixed
|
||||||
- Fix song page links in UI
|
- Fix song page links in UI
|
||||||
- Show song artist in playout detail
|
- Show song artist in playout detail
|
||||||
- Include song artist and cover art in channel guide (xmltv)
|
- Include song artist and cover art in channel guide (xmltv)
|
||||||
@@ -835,7 +909,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
- Initial release to facilitate testing outside of Docker.
|
- Initial release to facilitate testing outside of Docker.
|
||||||
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.0-alpha...HEAD
|
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...HEAD
|
||||||
|
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
|
||||||
|
[0.3.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.4-alpha...v0.3.5-alpha
|
||||||
|
[0.3.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.3-alpha...v0.3.4-alpha
|
||||||
|
[0.3.3-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.2-alpha...v0.3.3-alpha
|
||||||
|
[0.3.2-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.1-alpha...v0.3.2-alpha
|
||||||
|
[0.3.1-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.0-alpha...v0.3.1-alpha
|
||||||
[0.3.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
|
[0.3.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.5-alpha...v0.3.0-alpha
|
||||||
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
|
[0.2.5-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.4-alpha...v0.2.5-alpha
|
||||||
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
|
[0.2.4-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.2.3-alpha...v0.2.4-alpha
|
||||||
|
|||||||
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Channels
|
|||||||
string PreferredLanguageCode,
|
string PreferredLanguageCode,
|
||||||
StreamingMode StreamingMode,
|
StreamingMode StreamingMode,
|
||||||
int? WatermarkId,
|
int? WatermarkId,
|
||||||
int? FallbackFillerId);
|
int? FallbackFillerId,
|
||||||
|
int PlayoutCount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace ErsatzTV.Application.Channels
|
|||||||
channel.PreferredLanguageCode,
|
channel.PreferredLanguageCode,
|
||||||
channel.StreamingMode,
|
channel.StreamingMode,
|
||||||
channel.WatermarkId,
|
channel.WatermarkId,
|
||||||
channel.FallbackFillerId);
|
channel.FallbackFillerId,
|
||||||
|
channel.Playouts?.Count ?? 0);
|
||||||
|
|
||||||
private static string GetLogo(Channel channel) =>
|
private static string GetLogo(Channel channel) =>
|
||||||
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
Optional(channel.Artwork.FirstOrDefault(a => a.ArtworkKind == ArtworkKind.Logo))
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||||
@@ -16,16 +14,13 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
{
|
{
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
|
||||||
|
|
||||||
public UpdateFFmpegSettingsHandler(
|
public UpdateFFmpegSettingsHandler(
|
||||||
IConfigElementRepository configElementRepository,
|
IConfigElementRepository configElementRepository,
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem)
|
||||||
IRuntimeInfo runtimeInfo)
|
|
||||||
{
|
{
|
||||||
_configElementRepository = configElementRepository;
|
_configElementRepository = configElementRepository;
|
||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
_runtimeInfo = runtimeInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Either<BaseError, Unit>> Handle(
|
public Task<Either<BaseError, Unit>> Handle(
|
||||||
@@ -36,8 +31,8 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
.Bind(v => v.ToEitherAsync());
|
.Bind(v => v.ToEitherAsync());
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
private async Task<Validation<BaseError, Unit>> Validate(UpdateFFmpegSettings request) =>
|
||||||
(await FFmpegMustExist(request), await FFprobeMustExist(request), ReportsAreNotSupportedOnWindows(request))
|
(await FFmpegMustExist(request), await FFprobeMustExist(request))
|
||||||
.Apply((_, _, _) => Unit.Default);
|
.Apply((_, _) => Unit.Default);
|
||||||
|
|
||||||
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFmpegMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
ValidateToolPath(request.Settings.FFmpegPath, "ffmpeg");
|
||||||
@@ -45,16 +40,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
|||||||
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
private Task<Validation<BaseError, Unit>> FFprobeMustExist(UpdateFFmpegSettings request) =>
|
||||||
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
ValidateToolPath(request.Settings.FFprobePath, "ffprobe");
|
||||||
|
|
||||||
private Validation<BaseError, Unit> ReportsAreNotSupportedOnWindows(UpdateFFmpegSettings request)
|
|
||||||
{
|
|
||||||
if (request.Settings.SaveReports && _runtimeInfo.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return BaseError.New("FFmpeg reports are not supported on Windows");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Unit.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
private async Task<Validation<BaseError, Unit>> ValidateToolPath(string path, string name)
|
||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using LanguageExt;
|
||||||
|
using MediatR;
|
||||||
|
using Unit = LanguageExt.Unit;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands;
|
||||||
|
|
||||||
|
public record DeleteItemsFromDatabase(List<int> MediaItemIds) : IRequest<Either<BaseError, Unit>>;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Search;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Maintenance.Commands
|
||||||
|
{
|
||||||
|
public class
|
||||||
|
DeleteItemsFromDatabaseHandler : MediatR.IRequestHandler<DeleteItemsFromDatabase, Either<BaseError, Unit>>
|
||||||
|
{
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
private readonly ISearchIndex _searchIndex;
|
||||||
|
|
||||||
|
public DeleteItemsFromDatabaseHandler(
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
|
ISearchIndex searchIndex)
|
||||||
|
{
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
|
_searchIndex = searchIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> Handle(
|
||||||
|
DeleteItemsFromDatabase request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Either<BaseError, Unit> deleteResult = await _mediaItemRepository.DeleteItems(request.MediaItemIds);
|
||||||
|
if (deleteResult.IsRight)
|
||||||
|
{
|
||||||
|
await _searchIndex.RemoveItems(request.MediaItemIds);
|
||||||
|
_searchIndex.Commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleteResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) :
|
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb, MediaItemState State) :
|
||||||
MediaCardViewModel(Id, Name, Role, Name, Thumb);
|
MediaCardViewModel(Id, Name, Role, Name, Thumb, State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record ArtistCardViewModel
|
public record ArtistCardViewModel
|
||||||
(int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
ArtistId,
|
int ArtistId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster);
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
ArtistId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using static LanguageExt.Prelude;
|
using static LanguageExt.Prelude;
|
||||||
@@ -19,7 +20,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
showMetadata.Title,
|
showMetadata.Title,
|
||||||
showMetadata.Year?.ToString(),
|
showMetadata.Year?.ToString(),
|
||||||
showMetadata.SortTitle,
|
showMetadata.SortTitle,
|
||||||
GetPoster(showMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(showMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
showMetadata.Show.State);
|
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
Season season,
|
Season season,
|
||||||
@@ -34,7 +36,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
GetSeasonName(season.SeasonNumber),
|
GetSeasonName(season.SeasonNumber),
|
||||||
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
season.SeasonMetadata.HeadOrNone().Map(sm => GetPoster(sm, maybeJellyfin, maybeEmby))
|
||||||
.IfNone(string.Empty),
|
.IfNone(string.Empty),
|
||||||
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString());
|
season.SeasonNumber == 0 ? "S" : season.SeasonNumber.ToString(),
|
||||||
|
season.State);
|
||||||
|
|
||||||
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
internal static TelevisionSeasonCardViewModel ProjectToViewModel(
|
||||||
SeasonMetadata seasonMetadata,
|
SeasonMetadata seasonMetadata,
|
||||||
@@ -53,7 +56,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
GetSeasonName(seasonMetadata.Season.SeasonNumber),
|
GetSeasonName(seasonMetadata.Season.SeasonNumber),
|
||||||
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
|
$"{showTitle}_{seasonMetadata.Season.SeasonNumber:0000}",
|
||||||
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
GetPoster(seasonMetadata, maybeJellyfin, maybeEmby),
|
||||||
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString());
|
seasonMetadata.Season.SeasonNumber == 0 ? "S" : seasonMetadata.Season.SeasonNumber.ToString(),
|
||||||
|
seasonMetadata.Season.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
internal static TelevisionEpisodeCardViewModel ProjectToViewModel(
|
||||||
@@ -80,7 +84,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
? GetEpisodePoster(episodeMetadata, maybeJellyfin, maybeEmby)
|
||||||
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
: GetThumbnail(episodeMetadata, maybeJellyfin, maybeEmby),
|
||||||
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
episodeMetadata.Directors.Map(d => d.Name).ToList(),
|
||||||
episodeMetadata.Writers.Map(w => w.Name).ToList());
|
episodeMetadata.Writers.Map(w => w.Name).ToList(),
|
||||||
|
episodeMetadata.Episode.State,
|
||||||
|
episodeMetadata.Episode.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
internal static MovieCardViewModel ProjectToViewModel(
|
internal static MovieCardViewModel ProjectToViewModel(
|
||||||
MovieMetadata movieMetadata,
|
MovieMetadata movieMetadata,
|
||||||
@@ -91,7 +97,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
movieMetadata.Title,
|
movieMetadata.Title,
|
||||||
movieMetadata.Year?.ToString(),
|
movieMetadata.Year?.ToString(),
|
||||||
movieMetadata.SortTitle,
|
movieMetadata.SortTitle,
|
||||||
GetPoster(movieMetadata, maybeJellyfin, maybeEmby));
|
GetPoster(movieMetadata, maybeJellyfin, maybeEmby),
|
||||||
|
movieMetadata.Movie.State);
|
||||||
|
|
||||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||||
new(
|
new(
|
||||||
@@ -101,14 +108,17 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
musicVideoMetadata.SortTitle,
|
musicVideoMetadata.SortTitle,
|
||||||
musicVideoMetadata.Plot,
|
musicVideoMetadata.Plot,
|
||||||
musicVideoMetadata.Album,
|
musicVideoMetadata.Album,
|
||||||
GetThumbnail(musicVideoMetadata, None, None));
|
GetThumbnail(musicVideoMetadata, None, None),
|
||||||
|
musicVideoMetadata.MusicVideo.State,
|
||||||
|
musicVideoMetadata.MusicVideo.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
|
||||||
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
internal static OtherVideoCardViewModel ProjectToViewModel(OtherVideoMetadata otherVideoMetadata) =>
|
||||||
new(
|
new(
|
||||||
otherVideoMetadata.OtherVideoId,
|
otherVideoMetadata.OtherVideoId,
|
||||||
otherVideoMetadata.Title,
|
otherVideoMetadata.Title,
|
||||||
otherVideoMetadata.OriginalTitle,
|
otherVideoMetadata.OriginalTitle,
|
||||||
otherVideoMetadata.SortTitle);
|
otherVideoMetadata.SortTitle,
|
||||||
|
otherVideoMetadata.OtherVideo.State);
|
||||||
|
|
||||||
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata)
|
internal static SongCardViewModel ProjectToViewModel(SongMetadata songMetadata)
|
||||||
{
|
{
|
||||||
@@ -117,7 +127,9 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
songMetadata.SongId,
|
songMetadata.SongId,
|
||||||
songMetadata.Title,
|
songMetadata.Title,
|
||||||
songMetadata.Artist + album,
|
songMetadata.Artist + album,
|
||||||
songMetadata.SortTitle);
|
songMetadata.SortTitle,
|
||||||
|
GetThumbnail(songMetadata, None, None),
|
||||||
|
songMetadata.Song.State);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||||
@@ -126,7 +138,8 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
artistMetadata.Title,
|
artistMetadata.Title,
|
||||||
artistMetadata.Disambiguation,
|
artistMetadata.Disambiguation,
|
||||||
artistMetadata.SortTitle,
|
artistMetadata.SortTitle,
|
||||||
GetThumbnail(artistMetadata, None, None));
|
GetThumbnail(artistMetadata, None, None),
|
||||||
|
artistMetadata.Artist.State);
|
||||||
|
|
||||||
internal static CollectionCardResultsViewModel
|
internal static CollectionCardResultsViewModel
|
||||||
ProjectToViewModel(
|
ProjectToViewModel(
|
||||||
@@ -174,7 +187,7 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
.SetQueryParam("maxHeight", 440);
|
.SetQueryParam("maxHeight", 440);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork);
|
return new ActorCardViewModel(actor.Id, actor.Name, actor.Role, artwork, MediaItemState.Normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MediaCardViewModel(int MediaItemId, string Title, string Subtitle, string SortTitle, string Poster);
|
public record MediaCardViewModel(
|
||||||
|
int MediaItemId,
|
||||||
|
string Title,
|
||||||
|
string Subtitle,
|
||||||
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MovieCardViewModel
|
public record MovieCardViewModel
|
||||||
(int MovieId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
MovieId,
|
int MovieId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster)
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
MovieId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record MusicVideoCardViewModel
|
public record MusicVideoCardViewModel
|
||||||
(
|
(
|
||||||
@@ -8,12 +10,15 @@
|
|||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Plot,
|
string Plot,
|
||||||
string Album,
|
string Album,
|
||||||
string Poster) : MediaCardViewModel(
|
string Poster,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
MusicVideoId,
|
MusicVideoId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster)
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record OtherVideoCardViewModel
|
public record OtherVideoCardViewModel
|
||||||
(
|
(
|
||||||
int OtherVideoId,
|
int OtherVideoId,
|
||||||
string Title,
|
string Title,
|
||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle) : MediaCardViewModel(
|
string SortTitle,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
OtherVideoId,
|
OtherVideoId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
null)
|
null,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record SongCardViewModel
|
public record SongCardViewModel
|
||||||
(
|
(
|
||||||
int SongId,
|
int SongId,
|
||||||
string Title,
|
string Title,
|
||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle) : MediaCardViewModel(
|
string SortTitle,
|
||||||
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
SongId,
|
SongId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
null)
|
Poster,
|
||||||
|
State)
|
||||||
{
|
{
|
||||||
public int CustomIndex { get; set; }
|
public int CustomIndex { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCards
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
@@ -17,10 +18,13 @@ namespace ErsatzTV.Application.MediaCards
|
|||||||
string Plot,
|
string Plot,
|
||||||
string Poster,
|
string Poster,
|
||||||
List<string> Directors,
|
List<string> Directors,
|
||||||
List<string> Writers) : MediaCardViewModel(
|
List<string> Writers,
|
||||||
|
MediaItemState State,
|
||||||
|
string Path) : MediaCardViewModel(
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
Title,
|
Title,
|
||||||
$"Episode {Episode}",
|
$"Episode {Episode}",
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionSeasonCardViewModel
|
public record TelevisionSeasonCardViewModel
|
||||||
(
|
(
|
||||||
@@ -9,10 +11,12 @@
|
|||||||
string Subtitle,
|
string Subtitle,
|
||||||
string SortTitle,
|
string SortTitle,
|
||||||
string Poster,
|
string Poster,
|
||||||
string Placeholder) : MediaCardViewModel(
|
string Placeholder,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
TelevisionSeasonId,
|
TelevisionSeasonId,
|
||||||
Title,
|
Title,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
SortTitle,
|
SortTitle,
|
||||||
Poster);
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
namespace ErsatzTV.Application.MediaCards
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.MediaCards
|
||||||
{
|
{
|
||||||
public record TelevisionShowCardViewModel
|
public record TelevisionShowCardViewModel
|
||||||
(int TelevisionShowId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
(
|
||||||
TelevisionShowId,
|
int TelevisionShowId,
|
||||||
Title,
|
string Title,
|
||||||
Subtitle,
|
string Subtitle,
|
||||||
SortTitle,
|
string SortTitle,
|
||||||
Poster);
|
string Poster,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
|
TelevisionShowId,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
SortTitle,
|
||||||
|
Poster,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace ErsatzTV.Application.MediaCollections
|
|||||||
internal static class Mapper
|
internal static class Mapper
|
||||||
{
|
{
|
||||||
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
|
internal static MediaCollectionViewModel ProjectToViewModel(Collection collection) =>
|
||||||
new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder);
|
new(collection.Id, collection.Name, collection.UseCustomPlaybackOrder, MediaItemState.Normal);
|
||||||
|
|
||||||
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) =>
|
internal static MultiCollectionViewModel ProjectToViewModel(MultiCollection multiCollection) =>
|
||||||
new(
|
new(
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using ErsatzTV.Application.MediaCards;
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.MediaCollections
|
namespace ErsatzTV.Application.MediaCollections
|
||||||
{
|
{
|
||||||
public record MediaCollectionViewModel(int Id, string Name, bool UseCustomPlaybackOrder) : MediaCardViewModel(
|
public record MediaCollectionViewModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
bool UseCustomPlaybackOrder,
|
||||||
|
MediaItemState State) : MediaCardViewModel(
|
||||||
Id,
|
Id,
|
||||||
Name,
|
Name,
|
||||||
string.Empty,
|
string.Empty,
|
||||||
Name,
|
Name,
|
||||||
string.Empty);
|
string.Empty,
|
||||||
|
State);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,15 +160,36 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
|||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request) =>
|
private async Task<Validation<BaseError, RequestParameters>> Validate(IScanLocalLibrary request)
|
||||||
(await LocalLibraryMustExist(request), await ValidateFFprobePath(), await ValidateFFmpegPath(), await ValidateLibraryRefreshInterval())
|
{
|
||||||
.Apply(
|
Validation<BaseError, LocalLibrary> libraryResult = await LocalLibraryMustExist(request);
|
||||||
(library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters(
|
Validation<BaseError, string> ffprobePathResult = await ValidateFFprobePath();
|
||||||
library,
|
Validation<BaseError, string> ffmpegPathResult = await ValidateFFmpegPath();
|
||||||
ffprobePath,
|
Validation<BaseError, int> refreshIntervalResult = await ValidateLibraryRefreshInterval();
|
||||||
ffmpegPath,
|
|
||||||
request.ForceScan,
|
try
|
||||||
libraryRefreshInterval));
|
{
|
||||||
|
return (libraryResult, ffprobePathResult, ffmpegPathResult, refreshIntervalResult)
|
||||||
|
.Apply(
|
||||||
|
(library, ffprobePath, ffmpegPath, libraryRefreshInterval) => new RequestParameters(
|
||||||
|
library,
|
||||||
|
ffprobePath,
|
||||||
|
ffmpegPath,
|
||||||
|
request.ForceScan,
|
||||||
|
libraryRefreshInterval));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// ensure we unlock the library if any validation is unsuccessful
|
||||||
|
foreach (LocalLibrary library in libraryResult.SuccessToSeq())
|
||||||
|
{
|
||||||
|
if (ffprobePathResult.IsFail || ffmpegPathResult.IsFail || refreshIntervalResult.IsFail)
|
||||||
|
{
|
||||||
|
_entityLocker.UnlockLibrary(library.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
||||||
IScanLocalLibrary request) =>
|
IScanLocalLibrary request) =>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Emby;
|
using ErsatzTV.Core.Emby;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Jellyfin;
|
using ErsatzTV.Core.Jellyfin;
|
||||||
using Flurl;
|
using Flurl;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -34,7 +35,9 @@ namespace ErsatzTV.Application.Movies
|
|||||||
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
.Map(a => MediaCards.Mapper.ProjectToViewModel(a, maybeJellyfin, maybeEmby))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
metadata.Directors.Map(d => d.Name).ToList(),
|
metadata.Directors.Map(d => d.Name).ToList(),
|
||||||
metadata.Writers.Map(w => w.Name).ToList())
|
metadata.Writers.Map(w => w.Name).ToList(),
|
||||||
|
movie.GetHeadVersion().MediaFiles.Head().Path,
|
||||||
|
movie.State)
|
||||||
{
|
{
|
||||||
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby),
|
Poster = Artwork(metadata, ArtworkKind.Poster, maybeJellyfin, maybeEmby),
|
||||||
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby)
|
FanArt = Artwork(metadata, ArtworkKind.FanArt, maybeJellyfin, maybeEmby)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using ErsatzTV.Application.MediaCards;
|
using ErsatzTV.Application.MediaCards;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.Movies
|
namespace ErsatzTV.Application.Movies
|
||||||
{
|
{
|
||||||
@@ -15,7 +16,9 @@ namespace ErsatzTV.Application.Movies
|
|||||||
List<CultureInfo> Languages,
|
List<CultureInfo> Languages,
|
||||||
List<ActorCardViewModel> Actors,
|
List<ActorCardViewModel> Actors,
|
||||||
List<string> Directors,
|
List<string> Directors,
|
||||||
List<string> Writers)
|
List<string> Writers,
|
||||||
|
string Path,
|
||||||
|
MediaItemState MediaItemState)
|
||||||
{
|
{
|
||||||
public string Poster { get; set; }
|
public string Poster { get; set; }
|
||||||
public string FanArt { get; set; }
|
public string FanArt { get; set; }
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ namespace ErsatzTV.Application.Movies.Queries
|
|||||||
GetMovieById request,
|
GetMovieById request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
Option<JellyfinMediaSource> maybeJellyfin = await _mediaSourceRepository.GetAllJellyfin()
|
||||||
.Map(list => list.HeadOrNone());
|
.Map(list => list.HeadOrNone());
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
|
|
||||||
public async Task<Either<BaseError, PlayoutItemProcessModel>> Handle(T request, CancellationToken cancellationToken)
|
public async Task<Either<BaseError, PlayoutItemProcessModel>> Handle(T request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request);
|
Validation<BaseError, Tuple<Channel, string>> validation = await Validate(dbContext, request);
|
||||||
return await validation.Match(
|
return await validation.Match(
|
||||||
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2),
|
tuple => GetProcess(dbContext, request, tuple.Item1, tuple.Item2),
|
||||||
@@ -56,7 +56,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
{
|
{
|
||||||
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
|
"hls-direct" => StreamingMode.HttpLiveStreamingDirect,
|
||||||
"segmenter" => StreamingMode.HttpLiveStreamingSegmenter,
|
"segmenter" => StreamingMode.HttpLiveStreamingSegmenter,
|
||||||
"ts" => StreamingMode.TransportStream,
|
"ts" => StreamingMode.TransportStreamHybrid,
|
||||||
|
"ts-legacy" => StreamingMode.TransportStream,
|
||||||
_ => channel.StreamingMode
|
_ => channel.StreamingMode
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
{
|
{
|
||||||
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
|
public GetConcatProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
|
||||||
channelNumber,
|
channelNumber,
|
||||||
"ts",
|
"ts-legacy",
|
||||||
DateTimeOffset.Now,
|
DateTimeOffset.Now,
|
||||||
false,
|
false,
|
||||||
true)
|
true)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -16,16 +14,13 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
|
public class GetConcatProcessByChannelNumberHandler : FFmpegProcessHandler<GetConcatProcessByChannelNumber>
|
||||||
{
|
{
|
||||||
private readonly IFFmpegProcessService _ffmpegProcessService;
|
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
|
||||||
|
|
||||||
public GetConcatProcessByChannelNumberHandler(
|
public GetConcatProcessByChannelNumberHandler(
|
||||||
IDbContextFactory<TvContext> dbContextFactory,
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService)
|
||||||
IRuntimeInfo runtimeInfo)
|
|
||||||
: base(dbContextFactory)
|
: base(dbContextFactory)
|
||||||
{
|
{
|
||||||
_ffmpegProcessService = ffmpegProcessService;
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
_runtimeInfo = runtimeInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
||||||
@@ -34,7 +29,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
Channel channel,
|
Channel channel,
|
||||||
string ffmpegPath)
|
string ffmpegPath)
|
||||||
{
|
{
|
||||||
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
|
bool saveReports = await dbContext.ConfigElements
|
||||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||||
.Map(result => result.IfNone(false));
|
.Map(result => result.IfNone(false));
|
||||||
|
|
||||||
|
|||||||
+1
-6
@@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core;
|
using ErsatzTV.Core;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -15,7 +14,6 @@ using ErsatzTV.Core.Interfaces.Jellyfin;
|
|||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Plex;
|
using ErsatzTV.Core.Interfaces.Plex;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Runtime;
|
|
||||||
using ErsatzTV.Core.Scheduling;
|
using ErsatzTV.Core.Scheduling;
|
||||||
using ErsatzTV.Infrastructure.Data;
|
using ErsatzTV.Infrastructure.Data;
|
||||||
using ErsatzTV.Infrastructure.Extensions;
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
@@ -36,7 +34,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
private readonly IJellyfinPathReplacementService _jellyfinPathReplacementService;
|
||||||
private readonly ILocalFileSystem _localFileSystem;
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
private readonly IPlexPathReplacementService _plexPathReplacementService;
|
||||||
private readonly IRuntimeInfo _runtimeInfo;
|
|
||||||
private readonly ISongVideoGenerator _songVideoGenerator;
|
private readonly ISongVideoGenerator _songVideoGenerator;
|
||||||
|
|
||||||
public GetPlayoutItemProcessByChannelNumberHandler(
|
public GetPlayoutItemProcessByChannelNumberHandler(
|
||||||
@@ -49,7 +46,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
IMediaCollectionRepository mediaCollectionRepository,
|
IMediaCollectionRepository mediaCollectionRepository,
|
||||||
ITelevisionRepository televisionRepository,
|
ITelevisionRepository televisionRepository,
|
||||||
IArtistRepository artistRepository,
|
IArtistRepository artistRepository,
|
||||||
IRuntimeInfo runtimeInfo,
|
|
||||||
ISongVideoGenerator songVideoGenerator)
|
ISongVideoGenerator songVideoGenerator)
|
||||||
: base(dbContextFactory)
|
: base(dbContextFactory)
|
||||||
{
|
{
|
||||||
@@ -61,7 +57,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
_mediaCollectionRepository = mediaCollectionRepository;
|
_mediaCollectionRepository = mediaCollectionRepository;
|
||||||
_televisionRepository = televisionRepository;
|
_televisionRepository = televisionRepository;
|
||||||
_artistRepository = artistRepository;
|
_artistRepository = artistRepository;
|
||||||
_runtimeInfo = runtimeInfo;
|
|
||||||
_songVideoGenerator = songVideoGenerator;
|
_songVideoGenerator = songVideoGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +137,7 @@ namespace ErsatzTV.Application.Streaming.Queries
|
|||||||
ffmpegPath);
|
ffmpegPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool saveReports = !_runtimeInfo.IsOSPlatform(OSPlatform.Windows) && await dbContext.ConfigElements
|
bool saveReports = await dbContext.ConfigElements
|
||||||
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||||
.Map(result => result.IfNone(false));
|
.Map(result => result.IfNone(false));
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Streaming.Queries
|
||||||
|
{
|
||||||
|
public record GetWrappedProcessByChannelNumber : FFmpegProcessRequest
|
||||||
|
{
|
||||||
|
public GetWrappedProcessByChannelNumber(string scheme, string host, string channelNumber) : base(
|
||||||
|
channelNumber,
|
||||||
|
"ts",
|
||||||
|
DateTimeOffset.Now,
|
||||||
|
false,
|
||||||
|
true)
|
||||||
|
{
|
||||||
|
Scheme = scheme;
|
||||||
|
Host = host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Scheme { get; }
|
||||||
|
public string Host { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using ErsatzTV.Infrastructure.Extensions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Application.Streaming.Queries
|
||||||
|
{
|
||||||
|
public class GetWrappedProcessByChannelNumberHandler : FFmpegProcessHandler<GetWrappedProcessByChannelNumber>
|
||||||
|
{
|
||||||
|
private readonly IFFmpegProcessService _ffmpegProcessService;
|
||||||
|
|
||||||
|
public GetWrappedProcessByChannelNumberHandler(
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IFFmpegProcessService ffmpegProcessService)
|
||||||
|
: base(dbContextFactory)
|
||||||
|
{
|
||||||
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task<Either<BaseError, PlayoutItemProcessModel>> GetProcess(
|
||||||
|
TvContext dbContext,
|
||||||
|
GetWrappedProcessByChannelNumber request,
|
||||||
|
Channel channel,
|
||||||
|
string ffmpegPath)
|
||||||
|
{
|
||||||
|
bool saveReports = await dbContext.ConfigElements
|
||||||
|
.GetValue<bool>(ConfigElementKey.FFmpegSaveReports)
|
||||||
|
.Map(result => result.IfNone(false));
|
||||||
|
|
||||||
|
Process process = _ffmpegProcessService.WrapSegmenter(
|
||||||
|
ffmpegPath,
|
||||||
|
saveReports,
|
||||||
|
channel,
|
||||||
|
request.Scheme,
|
||||||
|
request.Host);
|
||||||
|
|
||||||
|
return new PlayoutItemProcessModel(process, DateTimeOffset.MaxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands
|
|
||||||
{
|
|
||||||
[Command("channel", Description = "Create or rename a channel")]
|
|
||||||
public class ChannelCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ChannelsApi _channelsApi;
|
|
||||||
private readonly FFmpegProfileApi _ffmpegProfileApi;
|
|
||||||
private readonly ILogger<ChannelCommand> _logger;
|
|
||||||
|
|
||||||
public ChannelCommand(IConfiguration configuration, ILogger<ChannelCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_channelsApi = new ChannelsApi(configuration["ServerUrl"]);
|
|
||||||
_ffmpegProfileApi = new FFmpegProfileApi(configuration["ServerUrl"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "channel-number", Description = "The channel number")]
|
|
||||||
public int Number { get; set; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "channel-name", Description = "The channel name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[CommandParameter(2, Name = "streaming-mode", Description = "The streaming mode")]
|
|
||||||
public StreamingMode StreamingMode { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("ffmpeg-profile", Description = "The ffmpeg profile name")]
|
|
||||||
public string FFmpegProfileName { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Option<ChannelViewModel> maybeChannel = await _channelsApi.ApiChannelsGetAsync()
|
|
||||||
.Map(list => Optional(list.SingleOrDefault(c => c.Number == Number)));
|
|
||||||
|
|
||||||
FFmpegProfileViewModel ffmpegProfile = await _ffmpegProfileApi.ApiFfmpegProfilesGetAsync()
|
|
||||||
.Map(
|
|
||||||
list => Optional(list.SingleOrDefault(p => p.Name == FFmpegProfileName))
|
|
||||||
.IfNone(new FFmpegProfileViewModel { Id = 1 }));
|
|
||||||
|
|
||||||
await maybeChannel.Match(
|
|
||||||
channel => RenameChannel(channel, ffmpegProfile),
|
|
||||||
() => AddChannel(ffmpegProfile));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to synchronize channel: {Message}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask RenameChannel(ChannelViewModel existing, FFmpegProfileViewModel ffmpegProfile)
|
|
||||||
{
|
|
||||||
int newFFmpegProfileId = string.IsNullOrWhiteSpace(FFmpegProfileName)
|
|
||||||
? existing.FfmpegProfileId
|
|
||||||
: ffmpegProfile.Id;
|
|
||||||
|
|
||||||
if (existing.Name != Name || existing.FfmpegProfileId != newFFmpegProfileId ||
|
|
||||||
existing.StreamingMode != StreamingMode)
|
|
||||||
{
|
|
||||||
var updateChannel = new UpdateChannel(
|
|
||||||
existing.Id,
|
|
||||||
Name,
|
|
||||||
existing.Number,
|
|
||||||
newFFmpegProfileId,
|
|
||||||
existing.Logo,
|
|
||||||
StreamingMode);
|
|
||||||
|
|
||||||
await _channelsApi.ApiChannelsPatchAsync(updateChannel);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Successfully synchronized channel {ChannelNumber} - {ChannelName}",
|
|
||||||
Number,
|
|
||||||
Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask AddChannel(FFmpegProfileViewModel ffmpegProfile)
|
|
||||||
{
|
|
||||||
var createChannel = new CreateChannel(
|
|
||||||
Name,
|
|
||||||
Number,
|
|
||||||
ffmpegProfile.Id,
|
|
||||||
null,
|
|
||||||
StreamingMode);
|
|
||||||
|
|
||||||
await _channelsApi.ApiChannelsPostAsync(createChannel);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Successfully created channel {ChannelNumber} - {ChannelName}",
|
|
||||||
Number,
|
|
||||||
Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands
|
|
||||||
{
|
|
||||||
[Command("config", Description = "Configure ErsatzTV server url")]
|
|
||||||
public class ConfigCommand : ICommand
|
|
||||||
{
|
|
||||||
[CommandParameter(0, Name = "server-url", Description = "The url of the ErsatzTV server")]
|
|
||||||
public string ServerUrl { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
// TODO: validate URL
|
|
||||||
|
|
||||||
string configFolder = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"ersatztv");
|
|
||||||
|
|
||||||
string configFile = Path.Combine(configFolder, "cli.json");
|
|
||||||
|
|
||||||
var config = new Config { ServerUrl = ServerUrl };
|
|
||||||
string contents = JsonSerializer.Serialize(config);
|
|
||||||
await File.WriteAllTextAsync(configFile, contents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands
|
|
||||||
{
|
|
||||||
[Command("ffmpeg-profile", Description = "Synchronize an ffmpeg profile")]
|
|
||||||
public class FFmpegProfileCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly FFmpegProfileApi _ffmpegProfileApi;
|
|
||||||
private readonly ILogger<FFmpegProfileCommand> _logger;
|
|
||||||
|
|
||||||
public FFmpegProfileCommand(IConfiguration configuration, ILogger<FFmpegProfileCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_ffmpegProfileApi = new FFmpegProfileApi(configuration["ServerUrl"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "profile-name", Description = "The ffmpeg profile name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("thread-count", Description = "The number of threads")]
|
|
||||||
public int ThreadCount { get; set; } = 0;
|
|
||||||
|
|
||||||
[CommandOption("transcode", Description = "Whether to transcode all media")]
|
|
||||||
public bool Transcode { get; set; } = true;
|
|
||||||
|
|
||||||
// public int ResolutionId { get; set; } = resolution.Id;
|
|
||||||
// Resolution { get; set; } = resolution;
|
|
||||||
[CommandOption("resolution", Description = "The resolution")]
|
|
||||||
public DesiredResolution Resolution { get; set; } = DesiredResolution.W1920H1080;
|
|
||||||
|
|
||||||
[CommandOption("video-codec", Description = "The video codec")]
|
|
||||||
public string VideoCodec { get; set; } = "libx264";
|
|
||||||
|
|
||||||
[CommandOption("audio-codec", Description = "The audio codec")]
|
|
||||||
public string AudioCodec { get; set; } = "ac3";
|
|
||||||
|
|
||||||
[CommandOption("video-bitrate", Description = "The video bitrate in kBit/s")]
|
|
||||||
public int VideoBitrate { get; set; } = 2000;
|
|
||||||
|
|
||||||
[CommandOption("video-buffer-size", Description = "The video buffer size in kBit")]
|
|
||||||
public int VideoBufferSize { get; set; } = 2000;
|
|
||||||
|
|
||||||
[CommandOption("audio-bitrate", Description = "The audio bitrate in kBit/s")]
|
|
||||||
public int AudioBitrate { get; set; } = 192;
|
|
||||||
|
|
||||||
[CommandOption("audio-buffer-size", Description = "The audio buffer size in kBits")]
|
|
||||||
public int AudioBufferSize { get; set; } = 50;
|
|
||||||
|
|
||||||
[CommandOption("audio-volume", Description = "The audio volume as a whole number percent")]
|
|
||||||
public int AudioVolume { get; set; } = 100;
|
|
||||||
|
|
||||||
[CommandOption("audio-channels", Description = "The number of audio channels")]
|
|
||||||
public int AudioChannels { get; set; } = 2;
|
|
||||||
|
|
||||||
[CommandOption("audio-sample-rate", Description = "The audio sample rate in kHz")]
|
|
||||||
public int AudioSampleRate { get; set; } = 48;
|
|
||||||
|
|
||||||
[CommandOption("normalize-resolution", Description = "Whether to normalize the resolution of all media")]
|
|
||||||
public bool NormalizeResolution { get; set; } = true;
|
|
||||||
|
|
||||||
[CommandOption("normalize-video-codec", Description = "Whether to normalize the video codec of all media")]
|
|
||||||
public bool NormalizeVideoCodec { get; set; } = true;
|
|
||||||
|
|
||||||
[CommandOption("normalize-audio-codec", Description = "Whether to normalize the audio codec of all media")]
|
|
||||||
public bool NormalizeAudioCodec { get; set; } = true;
|
|
||||||
|
|
||||||
[CommandOption(
|
|
||||||
"normalize-audio",
|
|
||||||
Description = "Whether to normalize audio channels and sample rate of all media")]
|
|
||||||
public bool NormalizeAudio { get; set; } = true;
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Option<FFmpegProfileViewModel> maybeFFmpegProfile = await _ffmpegProfileApi.ApiFfmpegProfilesGetAsync()
|
|
||||||
.Map(list => Optional(list.SingleOrDefault(p => p.Name == Name)));
|
|
||||||
|
|
||||||
await maybeFFmpegProfile.Match(UpdateProfile, AddProfile);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to synchronize ffmpeg profile: {Message}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask UpdateProfile(FFmpegProfileViewModel existing)
|
|
||||||
{
|
|
||||||
var updateFFmpegProfile = new UpdateFFmpegProfile(
|
|
||||||
existing.Id,
|
|
||||||
Name,
|
|
||||||
ThreadCount,
|
|
||||||
Transcode,
|
|
||||||
(int) Resolution,
|
|
||||||
NormalizeResolution,
|
|
||||||
VideoCodec,
|
|
||||||
NormalizeVideoCodec,
|
|
||||||
VideoBitrate,
|
|
||||||
VideoBufferSize,
|
|
||||||
AudioCodec,
|
|
||||||
NormalizeAudioCodec,
|
|
||||||
AudioBitrate,
|
|
||||||
AudioBufferSize,
|
|
||||||
AudioVolume,
|
|
||||||
AudioChannels,
|
|
||||||
AudioSampleRate,
|
|
||||||
NormalizeAudio);
|
|
||||||
|
|
||||||
await _ffmpegProfileApi.ApiFfmpegProfilesPatchAsync(updateFFmpegProfile);
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully synchronized ffmpeg profile {ProfileName}", Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask AddProfile()
|
|
||||||
{
|
|
||||||
var createFFmpegProfile = new CreateFFmpegProfile(
|
|
||||||
Name,
|
|
||||||
ThreadCount,
|
|
||||||
Transcode,
|
|
||||||
(int) Resolution,
|
|
||||||
NormalizeResolution,
|
|
||||||
VideoCodec,
|
|
||||||
NormalizeVideoCodec,
|
|
||||||
VideoBitrate,
|
|
||||||
VideoBufferSize,
|
|
||||||
AudioCodec,
|
|
||||||
NormalizeAudioCodec,
|
|
||||||
AudioBitrate,
|
|
||||||
AudioBufferSize,
|
|
||||||
AudioVolume,
|
|
||||||
AudioChannels,
|
|
||||||
AudioSampleRate,
|
|
||||||
NormalizeAudio);
|
|
||||||
|
|
||||||
|
|
||||||
await _ffmpegProfileApi.ApiFfmpegProfilesPostAsync(createFFmpegProfile);
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully created ffmpeg profile {ProfileName}", Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using LanguageExt.Common;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands.MediaCollections
|
|
||||||
{
|
|
||||||
[Command("collection clear", Description = "Removes all items from a media collection")]
|
|
||||||
public class MediaCollectionClearCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<MediaCollectionClearCommand> _logger;
|
|
||||||
private readonly string _serverUrl;
|
|
||||||
|
|
||||||
public MediaCollectionClearCommand(IConfiguration configuration, ILogger<MediaCollectionClearCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_serverUrl = configuration["ServerUrl"];
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CancellationToken cancellationToken = console.GetCancellationToken();
|
|
||||||
|
|
||||||
Either<Error, Unit> result = await ClearMediaCollection(cancellationToken);
|
|
||||||
|
|
||||||
result.Match(
|
|
||||||
_ => _logger.LogInformation("Successfully cleared media collection {MediaCollection}", Name),
|
|
||||||
error => _logger.LogError(
|
|
||||||
"Unable to clear media collection: {Error}",
|
|
||||||
error.Message));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to clear media collection: {Error}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Either<Error, Unit>> ClearMediaCollection(CancellationToken cancellationToken) =>
|
|
||||||
await EnsureMediaCollectionExists(cancellationToken)
|
|
||||||
.BindAsync(mediaCollectionId => ClearMediaCollectionImpl(mediaCollectionId, cancellationToken));
|
|
||||||
|
|
||||||
private async Task<Either<Error, int>> EnsureMediaCollectionExists(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
|
|
||||||
Option<MediaCollectionViewModel> maybeExisting =
|
|
||||||
(await mediaCollectionsApi.ApiMediaCollectionsGetAsync(cancellationToken))
|
|
||||||
.SingleOrDefault(mc => mc.Name == Name);
|
|
||||||
return await maybeExisting.MatchAsync(
|
|
||||||
existing => existing.Id,
|
|
||||||
async () =>
|
|
||||||
{
|
|
||||||
var data = new CreateSimpleMediaCollection(Name);
|
|
||||||
MediaCollectionViewModel result =
|
|
||||||
await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken);
|
|
||||||
return result.Id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Either<Error, Unit>> ClearMediaCollectionImpl(
|
|
||||||
int mediaCollectionId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
|
|
||||||
await mediaCollectionsApi.ApiMediaCollectionsIdItemsPutAsync(
|
|
||||||
mediaCollectionId,
|
|
||||||
new List<int>(),
|
|
||||||
cancellationToken);
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using LanguageExt.Common;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands.MediaCollections
|
|
||||||
{
|
|
||||||
[Command("collection create", Description = "Creates a new media collection")]
|
|
||||||
public class MediaCollectionCreateCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<MediaCollectionCreateCommand> _logger;
|
|
||||||
private readonly string _serverUrl;
|
|
||||||
|
|
||||||
public MediaCollectionCreateCommand(IConfiguration configuration, ILogger<MediaCollectionCreateCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_serverUrl = configuration["ServerUrl"];
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CancellationToken cancellationToken = console.GetCancellationToken();
|
|
||||||
|
|
||||||
Either<Error, Unit> result = await CreateMediaCollection(cancellationToken);
|
|
||||||
result.IfLeft(error => _logger.LogError("Unable to create media collection: {Error}", error.Message));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to create media collection: {Error}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Either<Error, Unit>> CreateMediaCollection(CancellationToken cancellationToken) =>
|
|
||||||
await EnsureMediaCollectionExists(cancellationToken);
|
|
||||||
|
|
||||||
private async Task<Either<Error, Unit>> EnsureMediaCollectionExists(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
|
|
||||||
|
|
||||||
bool needToAdd = await mediaCollectionsApi
|
|
||||||
.ApiMediaCollectionsGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.All(mc => mc.Name != Name));
|
|
||||||
|
|
||||||
if (needToAdd)
|
|
||||||
{
|
|
||||||
var data = new CreateSimpleMediaCollection(Name);
|
|
||||||
await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken);
|
|
||||||
_logger.LogInformation("Successfully created media collection {MediaCollection}", Name);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Media collection {MediaCollection} is already present", Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands
|
|
||||||
{
|
|
||||||
[Command("playout build", Description = "Builds a playout with the requested channel and schedule")]
|
|
||||||
public class PlayoutCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<PlayoutCommand> _logger;
|
|
||||||
private readonly string _serverUrl;
|
|
||||||
|
|
||||||
public PlayoutCommand(IConfiguration configuration, ILogger<PlayoutCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_serverUrl = configuration["ServerUrl"];
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "channel-number", Description = "The channel number")]
|
|
||||||
public int ChannelNumber { get; set; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "schedule-name", Description = "The schedule name")]
|
|
||||||
public string ScheduleName { get; set; }
|
|
||||||
|
|
||||||
// [Option("--type <type>")]
|
|
||||||
// [Required]
|
|
||||||
// public ProgramSchedulePlayoutType PlayoutType { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CancellationToken cancellationToken = console.GetCancellationToken();
|
|
||||||
|
|
||||||
var channelsApi = new ChannelsApi(_serverUrl);
|
|
||||||
Option<ChannelViewModel> maybeChannel = await channelsApi.ApiChannelsGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(c => c.Number == ChannelNumber));
|
|
||||||
|
|
||||||
await maybeChannel.Match(
|
|
||||||
channel => BuildPlayout(cancellationToken, channel),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to locate channel number {ChannelNumber}", ChannelNumber);
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to build playout: {Error}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask BuildPlayout(CancellationToken cancellationToken, ChannelViewModel channel)
|
|
||||||
{
|
|
||||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl);
|
|
||||||
Option<ProgramScheduleViewModel> maybeSchedule = await programScheduleApi
|
|
||||||
.ApiSchedulesGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(s => s.Name == ScheduleName));
|
|
||||||
|
|
||||||
await maybeSchedule.Match(
|
|
||||||
schedule => SynchronizePlayoutAsync(channel.Id, schedule.Id, cancellationToken),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to locate schedule {Schedule}", ScheduleName);
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask SynchronizePlayoutAsync(
|
|
||||||
int channelId,
|
|
||||||
int scheduleId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var playoutApi = new PlayoutApi(_serverUrl);
|
|
||||||
Option<PlayoutViewModel> maybeExisting = await playoutApi.ApiPlayoutsGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(p => p.Channel.Id == channelId));
|
|
||||||
await maybeExisting.Match(
|
|
||||||
existing =>
|
|
||||||
{
|
|
||||||
var data = new UpdatePlayout(existing.Id, channelId, scheduleId, ProgramSchedulePlayoutType.Flood);
|
|
||||||
if (existing.Channel.Id != data.ChannelId ||
|
|
||||||
existing.ProgramSchedule.Id != data.ProgramScheduleId ||
|
|
||||||
existing.ProgramSchedulePlayoutType != data.ProgramSchedulePlayoutType)
|
|
||||||
{
|
|
||||||
return playoutApi.ApiPlayoutsPatchAsync(data, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var data = new CreatePlayout(channelId, scheduleId, ProgramSchedulePlayoutType.Flood);
|
|
||||||
return playoutApi.ApiPlayoutsPostAsync(data, cancellationToken);
|
|
||||||
});
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully built playout for schedule {Schedule}", ScheduleName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands.Schedules
|
|
||||||
{
|
|
||||||
[Command("schedule add-item", Description = "Adds an item to the end of a schedule")]
|
|
||||||
public class ScheduleAddItemCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<ScheduleAddItemCommand> _logger;
|
|
||||||
private readonly string _serverUrl;
|
|
||||||
|
|
||||||
public ScheduleAddItemCommand(IConfiguration configuration, ILogger<ScheduleAddItemCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_serverUrl = configuration["ServerUrl"];
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "schedule-name", Description = "The schedule name")]
|
|
||||||
public string ScheduleName { get; set; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "collection-name", Description = "The media collection name")]
|
|
||||||
public string CollectionName { get; set; }
|
|
||||||
|
|
||||||
// [CommandParameter(2, Description = "The collection playback order")]
|
|
||||||
// public PlaybackOrder Order { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("start-type", 's', Description = "The playout start type")]
|
|
||||||
public StartType StartType { get; set; } = StartType.Dynamic;
|
|
||||||
|
|
||||||
[CommandOption("start-time", 't', Description = "The playout start time (of day)")]
|
|
||||||
public string StartTime { get; set; } = null;
|
|
||||||
|
|
||||||
[CommandOption("playout-mode", 'm', Description = "The playout mode")]
|
|
||||||
public PlayoutMode PlayoutMode { get; set; } = PlayoutMode.Flood;
|
|
||||||
|
|
||||||
[CommandOption(
|
|
||||||
"multiple-count",
|
|
||||||
'c',
|
|
||||||
Description = "How many items to play from the collection (for Multiple playout mode)")]
|
|
||||||
public int? MultipleCount { get; set; } = null;
|
|
||||||
|
|
||||||
[CommandOption(
|
|
||||||
"playout-duration",
|
|
||||||
'd',
|
|
||||||
Description = "How long to play items from the collection (for Duration playout mode)")]
|
|
||||||
public string PlayoutDuration { get; set; } = null;
|
|
||||||
|
|
||||||
[CommandOption(
|
|
||||||
"offline-tail",
|
|
||||||
'o',
|
|
||||||
Description =
|
|
||||||
"Whether to remain offline for the entire duration, or to start the next item immediately (for Duration playout mode)")]
|
|
||||||
public bool? OfflineTail { get; set; } = null;
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CancellationToken cancellationToken = console.GetCancellationToken();
|
|
||||||
|
|
||||||
Option<ProgramScheduleViewModel> maybeSchedule = await GetSchedule(cancellationToken);
|
|
||||||
await maybeSchedule.Match(
|
|
||||||
programSchedule => AddItemToSchedule(cancellationToken, programSchedule),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to locate schedule {Schedule}", ScheduleName);
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to add item to schedule: {Error}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask AddItemToSchedule(
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
ProgramScheduleViewModel programSchedule)
|
|
||||||
{
|
|
||||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl);
|
|
||||||
Option<MediaCollectionViewModel> maybeMediaCollection = await mediaCollectionsApi
|
|
||||||
.ApiMediaCollectionsGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(mc => mc.Name == CollectionName));
|
|
||||||
|
|
||||||
await maybeMediaCollection.Match(
|
|
||||||
collection =>
|
|
||||||
AddScheduleItem(programSchedule.Id, collection.Id, cancellationToken),
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
_logger.LogError(
|
|
||||||
"Unable to locate collection {Collection}",
|
|
||||||
CollectionName);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Option<ProgramScheduleViewModel>> GetSchedule(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl);
|
|
||||||
return await programScheduleApi.ApiSchedulesGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(schedule => schedule.Name == ScheduleName));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddScheduleItem(
|
|
||||||
int programScheduleId,
|
|
||||||
int mediaCollectionId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl);
|
|
||||||
|
|
||||||
var request = new AddProgramScheduleItem
|
|
||||||
{
|
|
||||||
ProgramScheduleId = programScheduleId,
|
|
||||||
StartType = StartType,
|
|
||||||
StartTime = StartTime,
|
|
||||||
PlayoutMode = PlayoutMode,
|
|
||||||
MediaCollectionId = mediaCollectionId,
|
|
||||||
PlayoutDuration = PlayoutDuration,
|
|
||||||
MultipleCount = MultipleCount,
|
|
||||||
OfflineTail = OfflineTail
|
|
||||||
};
|
|
||||||
|
|
||||||
await programScheduleApi.ApiSchedulesItemsAddPostAsync(request, cancellationToken);
|
|
||||||
|
|
||||||
_logger.LogInformation(
|
|
||||||
"Collection {Collection} has been added to schedule {Schedule}",
|
|
||||||
CollectionName,
|
|
||||||
ScheduleName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using CliFx.Attributes;
|
|
||||||
using ErsatzTV.Api.Sdk.Api;
|
|
||||||
using ErsatzTV.Api.Sdk.Model;
|
|
||||||
using LanguageExt;
|
|
||||||
using LanguageExt.Common;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using static LanguageExt.Prelude;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine.Commands.Schedules
|
|
||||||
{
|
|
||||||
[Command("schedule create", Description = "Creates a new schedule")]
|
|
||||||
public class ScheduleCreateCommand : ICommand
|
|
||||||
{
|
|
||||||
private readonly ILogger<ScheduleCreateCommand> _logger;
|
|
||||||
private readonly string _serverUrl;
|
|
||||||
|
|
||||||
public ScheduleCreateCommand(IConfiguration configuration, ILogger<ScheduleCreateCommand> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_serverUrl = configuration["ServerUrl"];
|
|
||||||
}
|
|
||||||
|
|
||||||
[CommandParameter(0, Name = "schedule-name", Description = "The schedule name")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[CommandParameter(1, Name = "playback-order", Description = "The collection playback order")]
|
|
||||||
public PlaybackOrder Order { get; set; }
|
|
||||||
|
|
||||||
[CommandOption("reset", Description = "Resets the schedule to contain no items")]
|
|
||||||
public bool Reset { get; set; }
|
|
||||||
|
|
||||||
public async ValueTask ExecuteAsync(IConsole console)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CancellationToken cancellationToken = console.GetCancellationToken();
|
|
||||||
|
|
||||||
Either<Error, Unit> result = await EnsureScheduleExistsAsync(cancellationToken);
|
|
||||||
result.IfLeft(error => _logger.LogError("Unable to create schedule: {Error}", error.Message));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("Unable to create schedule: {Error}", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<Either<Error, Unit>> EnsureScheduleExistsAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl);
|
|
||||||
|
|
||||||
Option<ProgramScheduleViewModel> maybeExisting = await programScheduleApi
|
|
||||||
.ApiSchedulesGetAsync(cancellationToken)
|
|
||||||
.Map(list => list.SingleOrDefault(schedule => schedule.Name == Name));
|
|
||||||
|
|
||||||
await maybeExisting.Match(
|
|
||||||
existing =>
|
|
||||||
{
|
|
||||||
// TODO: update playback order if changed?
|
|
||||||
_logger.LogInformation("Schedule {Schedule} is already present", Name);
|
|
||||||
|
|
||||||
if (Reset)
|
|
||||||
{
|
|
||||||
return programScheduleApi
|
|
||||||
.ApiSchedulesProgramScheduleIdItemsDeleteAsync(existing.Id, cancellationToken)
|
|
||||||
.Iter(_ => _logger.LogInformation("Successfully reset schedule {Schedule}", Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
() =>
|
|
||||||
{
|
|
||||||
var data = new CreateProgramSchedule(Name, Order);
|
|
||||||
return programScheduleApi.ApiSchedulesPostAsync(data, cancellationToken)
|
|
||||||
.Iter(_ => _logger.LogInformation("Successfully created schedule {Schedule}", Name));
|
|
||||||
});
|
|
||||||
|
|
||||||
return unit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace ErsatzTV.CommandLine
|
|
||||||
{
|
|
||||||
public class Config
|
|
||||||
{
|
|
||||||
public string ServerUrl { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace ErsatzTV.CommandLine
|
|
||||||
{
|
|
||||||
public enum DesiredResolution
|
|
||||||
{
|
|
||||||
W720H480 = 1,
|
|
||||||
W1280H720 = 2,
|
|
||||||
W1920H1080 = 3,
|
|
||||||
W3840H2160 = 4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
|
||||||
<AssemblyName>ersatztv-cli</AssemblyName>
|
|
||||||
<LangVersion>9</LangVersion>
|
|
||||||
<PackageVersion>0.0.1</PackageVersion>
|
|
||||||
<AssemblyVersion>0.0.1</AssemblyVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CliFx" Version="1.6.0" />
|
|
||||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
|
|
||||||
<PackageReference Include="RestSharp" Version="106.11.7" />
|
|
||||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" />
|
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
|
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\generated\ErsatzTV.Api.Sdk\src\ErsatzTV.Api.Sdk\ErsatzTV.Api.Sdk.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions, Version=5.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
|
|
||||||
<HintPath>..\..\..\..\..\..\usr\share\dotnet\packs\Microsoft.AspNetCore.App.Ref\5.0.0\ref\net5.0\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CliFx;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Serilog;
|
|
||||||
using Serilog.Events;
|
|
||||||
|
|
||||||
namespace ErsatzTV.CommandLine
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static async Task<int> Main(string[] args)
|
|
||||||
{
|
|
||||||
Log.Logger = new LoggerConfiguration()
|
|
||||||
.MinimumLevel.Information()
|
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
|
||||||
.Enrich.FromLogContext()
|
|
||||||
.WriteTo.Console()
|
|
||||||
.CreateLogger();
|
|
||||||
|
|
||||||
IHost host = CreateHostBuilder(args).Build();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await new CliApplicationBuilder()
|
|
||||||
.AddCommandsFromThisAssembly()
|
|
||||||
.UseTypeActivator(host.Services.GetService)
|
|
||||||
.Build()
|
|
||||||
.RunAsync(args);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Fatal(ex, "Host terminated unexpectedly");
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Log.CloseAndFlush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
|
||||||
Host.CreateDefaultBuilder(args)
|
|
||||||
.ConfigureServices(
|
|
||||||
(_, services) =>
|
|
||||||
{
|
|
||||||
services.AddSingleton<IConsole, SystemConsole>();
|
|
||||||
IEnumerable<Type> typesThatImplementICommand = typeof(Program).Assembly.GetTypes()
|
|
||||||
.Where(x => typeof(ICommand).IsAssignableFrom(x))
|
|
||||||
.Where(x => !x.IsAbstract);
|
|
||||||
foreach (Type t in typesThatImplementICommand)
|
|
||||||
{
|
|
||||||
services.AddTransient(t);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.ConfigureAppConfiguration(
|
|
||||||
(_, configuration) =>
|
|
||||||
{
|
|
||||||
configuration.Sources.Clear();
|
|
||||||
|
|
||||||
string configFolder = Path.Combine(
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
||||||
"ersatztv");
|
|
||||||
|
|
||||||
configuration.SetBasePath(configFolder);
|
|
||||||
configuration.AddJsonFile("cli.json", true, true);
|
|
||||||
})
|
|
||||||
.UseSerilog()
|
|
||||||
.UseConsoleLifetime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.2.0" />
|
<PackageReference Include="FluentAssertions" Version="6.3.0" />
|
||||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Moq" Version="4.16.1" />
|
<PackageReference Include="Moq" Version="4.16.1" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||||
|
|||||||
@@ -297,37 +297,37 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]deinterlace_qsv,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]deinterlace_qsv,scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,hwupload=extra_hw_frames=64[v]",
|
"[0:0]scale_qsv=w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]scale_qsv=w=1920:h=1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
"[0:0]scale_qsv=w=1920:h=1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload=extra_hw_frames=64[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_QSV_Video_Filter(
|
public void Should_Return_QSV_Video_Filter(
|
||||||
bool deinterlace,
|
bool deinterlace,
|
||||||
@@ -368,37 +368,37 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]yadif_cuda,scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]yadif_cuda,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]yadif_cuda,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]yadif_cuda,scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]yadif_cuda,scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,hwupload[v]",
|
"[0:0]scale_cuda=1920:1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]scale_cuda=1920:1000,hwdownload,format=nv12,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]scale_cuda=1920:1000,setsar=1,hwdownload,format=nv12,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_NVENC_Video_Filter(
|
public void Should_Return_NVENC_Video_Filter(
|
||||||
bool deinterlace,
|
bool deinterlace,
|
||||||
@@ -409,7 +409,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
{
|
{
|
||||||
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
FFmpegComplexFilterBuilder builder = new FFmpegComplexFilterBuilder()
|
||||||
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
|
.WithHardwareAcceleration(HardwareAccelerationKind.Nvenc)
|
||||||
.WithDeinterlace(deinterlace);
|
.WithDeinterlace(deinterlace)
|
||||||
|
.WithInputPixelFormat("h264");
|
||||||
|
|
||||||
if (scale)
|
if (scale)
|
||||||
{
|
{
|
||||||
@@ -440,42 +441,42 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"h264",
|
"h264",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
|
[TestCase("mpeg4", true, false, false, "[0:0]hwupload,deinterlace_vaapi[v]", "[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
@@ -483,28 +484,28 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwupload,deinterlace_vaapi,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,deinterlace_vaapi,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,hwupload[v]",
|
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
[TestCase(
|
[TestCase(
|
||||||
"mpeg4",
|
"mpeg4",
|
||||||
@@ -518,7 +519,7 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,hwdownload,format=nv12|vaapi,setsar=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
"[0:0]hwupload,scale_vaapi=format=nv12:w=1920:h=1000,setsar=1,hwdownload,format=nv12|vaapi,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,hwupload[v]",
|
||||||
"[v]")]
|
"[v]")]
|
||||||
public void Should_Return_VAAPI_Video_Filter(
|
public void Should_Return_VAAPI_Video_Filter(
|
||||||
string codec,
|
string codec,
|
||||||
|
|||||||
@@ -94,6 +94,17 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
{
|
{
|
||||||
HardwareAccelerationKind.Vaapi
|
HardwareAccelerationKind.Vaapi
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static string[] VideoToolboxCodecs =
|
||||||
|
{
|
||||||
|
"h264_videotoolbox",
|
||||||
|
"hevc_videotoolbox"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static HardwareAccelerationKind[] VideoToolboxAcceleration =
|
||||||
|
{
|
||||||
|
HardwareAccelerationKind.VideoToolbox
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test, Combinatorial]
|
[Test, Combinatorial]
|
||||||
@@ -104,21 +115,27 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
string inputPixelFormat,
|
string inputPixelFormat,
|
||||||
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
|
[ValueSource(typeof(TestData), nameof(TestData.Resolutions))]
|
||||||
Resolution profileResolution,
|
Resolution profileResolution,
|
||||||
|
[Values(true, false)]
|
||||||
|
bool pad,
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
|
// [ValueSource(typeof(TestData), nameof(TestData.SoftwareCodecs))] string profileCodec,
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.NoAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec,
|
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaCodecs))] string profileCodec,
|
||||||
[ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.NvidiaAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
|
// [ValueSource(typeof(TestData), nameof(TestData.VaapiCodecs))] string profileCodec,
|
||||||
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
// [ValueSource(typeof(TestData), nameof(TestData.VaapiAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
|
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxCodecs))] string profileCodec,
|
||||||
|
[ValueSource(typeof(TestData), nameof(TestData.VideoToolboxAcceleration))] HardwareAccelerationKind profileAcceleration)
|
||||||
{
|
{
|
||||||
string name = GetStringSha256Hash(
|
string name = GetStringSha256Hash(
|
||||||
$"{inputCodec}_{inputPixelFormat}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
$"{inputCodec}_{inputPixelFormat}_{pad}_{profileResolution}_{profileCodec}_{profileAcceleration}");
|
||||||
|
|
||||||
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
|
string file = Path.Combine(TestContext.CurrentContext.TestDirectory, $"{name}.mkv");
|
||||||
if (!File.Exists(file))
|
if (!File.Exists(file))
|
||||||
{
|
{
|
||||||
|
string resolution = pad ? "1920x1060" : "1920x1080";
|
||||||
|
|
||||||
var args =
|
var args =
|
||||||
$"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size=1920x1080:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}";
|
$"-y -f lavfi -i anullsrc=channel_layout=stereo:sample_rate=44100 -f lavfi -i testsrc=duration=1:size={resolution}:rate=30 -c:a aac -c:v {inputCodec} -shortest -pix_fmt {inputPixelFormat} -strict -2 {file}";
|
||||||
var p1 = new Process
|
var p1 = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
@@ -146,8 +163,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
|
|
||||||
var metadataRepository = new Mock<IMetadataRepository>();
|
var metadataRepository = new Mock<IMetadataRepository>();
|
||||||
metadataRepository
|
metadataRepository
|
||||||
.Setup(r => r.UpdateLocalStatistics(It.IsAny<int>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
.Setup(r => r.UpdateLocalStatistics(It.IsAny<MediaItem>(), It.IsAny<MediaVersion>(), It.IsAny<bool>()))
|
||||||
.Callback<int, MediaVersion, bool>((_, version, _) => v = version);
|
.Callback<MediaItem, MediaVersion, bool>((_, version, _) => v = version);
|
||||||
|
|
||||||
var localStatisticsProvider = new LocalStatisticsProvider(
|
var localStatisticsProvider = new LocalStatisticsProvider(
|
||||||
metadataRepository.Object,
|
metadataRepository.Object,
|
||||||
@@ -200,6 +217,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
|||||||
TimeSpan.FromSeconds(5));
|
TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
process.StartInfo.RedirectStandardError = true;
|
process.StartInfo.RedirectStandardError = true;
|
||||||
|
|
||||||
|
// Console.WriteLine($"ffmpeg arguments {string.Join(" ", process.StartInfo.ArgumentList)}");
|
||||||
|
|
||||||
process.Start().Should().BeTrue();
|
process.Start().Should().BeTrue();
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
.IfNone(SystemTime.MinValueUtc);
|
.IfNone(SystemTime.MinValueUtc);
|
||||||
|
|
||||||
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
|
public bool IsLibraryPathAccessible(LibraryPath libraryPath) =>
|
||||||
_files.Any(f => f.Path.StartsWith(libraryPath.Path + Path.DirectorySeparatorChar));
|
_folders.Any(f => f.Path == libraryPath.Path);
|
||||||
|
|
||||||
public IEnumerable<string> ListSubdirectories(string folder) =>
|
public IEnumerable<string> ListSubdirectories(string folder) =>
|
||||||
_folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder);
|
_folders.Map(f => f.Path).Filter(f => f.StartsWith(folder) && Directory.GetParent(f)?.FullName == folder);
|
||||||
@@ -53,6 +53,7 @@ namespace ErsatzTV.Core.Tests.Fakes
|
|||||||
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
|
_files.Map(f => f.Path).Filter(f => Path.GetDirectoryName(f) == folder);
|
||||||
|
|
||||||
public bool FileExists(string path) => _files.Any(f => f.Path == path);
|
public bool FileExists(string path) => _files.Any(f => f.Path == path);
|
||||||
|
public bool FolderExists(string folder) => false;
|
||||||
|
|
||||||
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
|
public Task<byte[]> ReadAllBytes(string path) => TestBytes.AsTask();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,211 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
|
using ErsatzTV.Core.Interfaces.Runtime;
|
||||||
|
using ErsatzTV.Core.Jellyfin;
|
||||||
|
using FluentAssertions;
|
||||||
|
using LanguageExt;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Tests.Jellyfin
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class JellyfinPathReplacementServiceTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinWindows_To_EtvWindows()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"C:\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinWindows_To_EtvLinux()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"C:\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinWindows_To_EtvLinux_UncPath()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinWindows_To_EtvLinux_UncPathWithTrailingSlash()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"\\192.168.1.100\Something\Some Shared Folder\",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder/",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Windows" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinLinux_To_EtvWindows()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"/mnt/something/Some Shared Folder",
|
||||||
|
LocalPath = @"C:\Something Else\Some Shared Folder",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task JellyfinLinux_To_EtvLinux()
|
||||||
|
{
|
||||||
|
var replacements = new List<JellyfinPathReplacement>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
JellyfinPath = @"/mnt/something/Some Shared Folder",
|
||||||
|
LocalPath = @"/mnt/something else/Some Shared Folder",
|
||||||
|
JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var repo = new Mock<IMediaSourceRepository>();
|
||||||
|
repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny<int>())).Returns(replacements.AsTask());
|
||||||
|
|
||||||
|
var runtime = new Mock<IRuntimeInfo>();
|
||||||
|
runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false);
|
||||||
|
|
||||||
|
var service = new JellyfinPathReplacementService(
|
||||||
|
repo.Object,
|
||||||
|
runtime.Object,
|
||||||
|
new Mock<ILogger<JellyfinPathReplacementService>>().Object);
|
||||||
|
|
||||||
|
string result = await service.GetReplacementJellyfinPath(
|
||||||
|
0,
|
||||||
|
@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
|
||||||
|
result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Errors;
|
using ErsatzTV.Core.Errors;
|
||||||
using ErsatzTV.Core.FFmpeg;
|
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
using ErsatzTV.Core.Interfaces.Images;
|
using ErsatzTV.Core.Interfaces.Images;
|
||||||
using ErsatzTV.Core.Interfaces.Metadata;
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
@@ -54,6 +53,10 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
||||||
.Returns(new List<string>().AsEnumerable().AsTask());
|
.Returns(new List<string>().AsEnumerable().AsTask());
|
||||||
|
|
||||||
|
_mediaItemRepository = new Mock<IMediaItemRepository>();
|
||||||
|
_mediaItemRepository.Setup(x => x.FlagFileNotFound(It.IsAny<LibraryPath>(), It.IsAny<string>()))
|
||||||
|
.Returns(new List<int>().AsTask());
|
||||||
|
|
||||||
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
|
_localStatisticsProvider = new Mock<ILocalStatisticsProvider>();
|
||||||
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
|
_localMetadataProvider = new Mock<ILocalMetadataProvider>();
|
||||||
|
|
||||||
@@ -73,6 +76,7 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Mock<IMovieRepository> _movieRepository;
|
private Mock<IMovieRepository> _movieRepository;
|
||||||
|
private Mock<IMediaItemRepository> _mediaItemRepository;
|
||||||
private Mock<ILocalStatisticsProvider> _localStatisticsProvider;
|
private Mock<ILocalStatisticsProvider> _localStatisticsProvider;
|
||||||
private Mock<ILocalMetadataProvider> _localMetadataProvider;
|
private Mock<ILocalMetadataProvider> _localMetadataProvider;
|
||||||
private Mock<IImageCache> _imageCache;
|
private Mock<IImageCache> _imageCache;
|
||||||
@@ -535,6 +539,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
[Test]
|
[Test]
|
||||||
public async Task RenamedMovie_Should_Delete_Old_Movie()
|
public async Task RenamedMovie_Should_Delete_Old_Movie()
|
||||||
{
|
{
|
||||||
|
// TODO: handle this case more elegantly
|
||||||
|
// ideally, detect that the movie was renamed and still delete the old one (or update the path?)
|
||||||
|
|
||||||
string movieFolder = Path.Combine(FakeRoot, "Movie (2020)");
|
string movieFolder = Path.Combine(FakeRoot, "Movie (2020)");
|
||||||
string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi");
|
string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi");
|
||||||
|
|
||||||
@@ -557,12 +564,14 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
_movieRepository.Verify(x => x.DeleteByPath(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
|
_mediaItemRepository.Verify(
|
||||||
_movieRepository.Verify(x => x.DeleteByPath(libraryPath, oldMoviePath), Times.Once);
|
x => x.FlagFileNotFound(It.IsAny<LibraryPath>(), It.IsAny<string>()),
|
||||||
|
Times.Once);
|
||||||
|
_mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task DeletedMovieAndFolder_Should_Delete_Old_Movie()
|
public async Task DeletedMovieAndFolder_Should_Flag_File_Not_Found()
|
||||||
{
|
{
|
||||||
string movieFolder = Path.Combine(FakeRoot, "Movie (2020)");
|
string movieFolder = Path.Combine(FakeRoot, "Movie (2020)");
|
||||||
string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi");
|
string oldMoviePath = Path.Combine(movieFolder, "Movie (2020).avi");
|
||||||
@@ -570,10 +579,8 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
_movieRepository.Setup(x => x.FindMoviePaths(It.IsAny<LibraryPath>()))
|
||||||
.Returns(new List<string> { oldMoviePath }.AsEnumerable().AsTask());
|
.Returns(new List<string> { oldMoviePath }.AsEnumerable().AsTask());
|
||||||
|
|
||||||
string moviePath = Path.Combine(movieFolder, "Movie (2020).mkv");
|
|
||||||
|
|
||||||
MovieFolderScanner service = GetService(
|
MovieFolderScanner service = GetService(
|
||||||
new FakeFileEntry(moviePath) { LastWriteTime = DateTime.Now }
|
new FakeFolderEntry(FakeRoot)
|
||||||
);
|
);
|
||||||
var libraryPath = new LibraryPath
|
var libraryPath = new LibraryPath
|
||||||
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
{ Id = 1, Path = FakeRoot, LibraryFolders = new List<LibraryFolder>() };
|
||||||
@@ -586,11 +593,12 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
|
|
||||||
result.IsRight.Should().BeTrue();
|
result.IsRight.Should().BeTrue();
|
||||||
|
|
||||||
_movieRepository.Verify(x => x.DeleteByPath(It.IsAny<LibraryPath>(), It.IsAny<string>()), Times.Once);
|
_mediaItemRepository.Verify(
|
||||||
_movieRepository.Verify(x => x.DeleteByPath(libraryPath, oldMoviePath), Times.Once);
|
x => x.FlagFileNotFound(It.IsAny<LibraryPath>(), It.IsAny<string>()),
|
||||||
|
Times.Once);
|
||||||
|
_mediaItemRepository.Verify(x => x.FlagFileNotFound(libraryPath, oldMoviePath), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private MovieFolderScanner GetService(params FakeFileEntry[] files) =>
|
private MovieFolderScanner GetService(params FakeFileEntry[] files) =>
|
||||||
new(
|
new(
|
||||||
new FakeLocalFileSystem(new List<FakeFileEntry>(files)),
|
new FakeLocalFileSystem(new List<FakeFileEntry>(files)),
|
||||||
@@ -602,6 +610,25 @@ namespace ErsatzTV.Core.Tests.Metadata
|
|||||||
new Mock<ISearchIndex>().Object,
|
new Mock<ISearchIndex>().Object,
|
||||||
new Mock<ISearchRepository>().Object,
|
new Mock<ISearchRepository>().Object,
|
||||||
new Mock<ILibraryRepository>().Object,
|
new Mock<ILibraryRepository>().Object,
|
||||||
|
_mediaItemRepository.Object,
|
||||||
|
new Mock<IMediator>().Object,
|
||||||
|
null,
|
||||||
|
new Mock<ITempFilePool>().Object,
|
||||||
|
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||||
|
);
|
||||||
|
|
||||||
|
private MovieFolderScanner GetService(params FakeFolderEntry[] folders) =>
|
||||||
|
new(
|
||||||
|
new FakeLocalFileSystem(new List<FakeFileEntry>(), new List<FakeFolderEntry>(folders)),
|
||||||
|
_movieRepository.Object,
|
||||||
|
_localStatisticsProvider.Object,
|
||||||
|
_localMetadataProvider.Object,
|
||||||
|
new Mock<IMetadataRepository>().Object,
|
||||||
|
_imageCache.Object,
|
||||||
|
new Mock<ISearchIndex>().Object,
|
||||||
|
new Mock<ISearchRepository>().Object,
|
||||||
|
new Mock<ILibraryRepository>().Object,
|
||||||
|
_mediaItemRepository.Object,
|
||||||
new Mock<IMediator>().Object,
|
new Mock<IMediator>().Object,
|
||||||
null,
|
null,
|
||||||
new Mock<ITempFilePool>().Object,
|
new Mock<ITempFilePool>().Object,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
None = 0,
|
None = 0,
|
||||||
Qsv = 1,
|
Qsv = 1,
|
||||||
Nvenc = 2,
|
Nvenc = 2,
|
||||||
Vaapi = 3
|
Vaapi = 3,
|
||||||
|
VideoToolbox = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Domain
|
namespace ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
public class MediaItem
|
||||||
{
|
{
|
||||||
public class MediaItem
|
public int Id { get; set; }
|
||||||
{
|
public int LibraryPathId { get; set; }
|
||||||
public int Id { get; set; }
|
public LibraryPath LibraryPath { get; set; }
|
||||||
public int LibraryPathId { get; set; }
|
public List<Collection> Collections { get; set; }
|
||||||
public LibraryPath LibraryPath { get; set; }
|
public List<CollectionItem> CollectionItems { get; set; }
|
||||||
public List<Collection> Collections { get; set; }
|
public List<TraktListItem> TraktListItems { get; set; }
|
||||||
public List<CollectionItem> CollectionItems { get; set; }
|
public MediaItemState State { get; set; }
|
||||||
public List<TraktListItem> TraktListItems { get; set; }
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
|
public enum MediaItemState
|
||||||
|
{
|
||||||
|
Normal = 0,
|
||||||
|
FileNotFound = 1
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@ namespace ErsatzTV.Core.Domain
|
|||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
public string SourcePath { get; set; }
|
||||||
|
public string BlurHash43 { get; set; }
|
||||||
|
public string BlurHash54 { get; set; }
|
||||||
|
public string BlurHash64 { get; set; }
|
||||||
public ArtworkKind ArtworkKind { get; set; }
|
public ArtworkKind ArtworkKind { get; set; }
|
||||||
public DateTime DateAdded { get; set; }
|
public DateTime DateAdded { get; set; }
|
||||||
public DateTime DateUpdated { get; set; }
|
public DateTime DateUpdated { get; set; }
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
{
|
{
|
||||||
TransportStream = 1,
|
TransportStream = 1,
|
||||||
HttpLiveStreamingDirect = 2,
|
HttpLiveStreamingDirect = 2,
|
||||||
HttpLiveStreamingHybrid = 3,
|
// HttpLiveStreamingHybrid = 3,
|
||||||
HttpLiveStreamingSegmenter = 4
|
HttpLiveStreamingSegmenter = 4,
|
||||||
|
TransportStreamHybrid = 5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
private string _videoEncoder;
|
private string _videoEncoder;
|
||||||
private Option<string> _subtitle;
|
private Option<string> _subtitle;
|
||||||
private bool _boxBlur;
|
private bool _boxBlur;
|
||||||
private Option<int> _randomColor;
|
|
||||||
|
|
||||||
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
|
public FFmpegComplexFilterBuilder WithHardwareAcceleration(HardwareAccelerationKind hardwareAccelerationKind)
|
||||||
{
|
{
|
||||||
@@ -102,12 +101,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public FFmpegComplexFilterBuilder WithRandomColor(Option<int> randomColor)
|
|
||||||
{
|
|
||||||
_randomColor = randomColor;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FFmpegComplexFilterBuilder WithSubtitleFile(Option<string> subtitleFile)
|
public FFmpegComplexFilterBuilder WithSubtitleFile(Option<string> subtitleFile)
|
||||||
{
|
{
|
||||||
foreach (string file in subtitleFile)
|
foreach (string file in subtitleFile)
|
||||||
@@ -149,8 +142,14 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
bool isHardwareDecode = acceleration switch
|
bool isHardwareDecode = acceleration switch
|
||||||
{
|
{
|
||||||
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4",
|
HardwareAccelerationKind.Vaapi => !isSong && _inputCodec != "mpeg4",
|
||||||
|
|
||||||
|
// we need an initial hwupload_cuda when only padding with these pixel formats
|
||||||
|
HardwareAccelerationKind.Nvenc when _scaleToSize.IsNone && _padToSize.IsSome =>
|
||||||
|
!isSong && !_pixelFormat.Contains("p10le") && !_pixelFormat.Contains("444"),
|
||||||
|
|
||||||
HardwareAccelerationKind.Nvenc => !isSong,
|
HardwareAccelerationKind.Nvenc => !isSong,
|
||||||
HardwareAccelerationKind.Qsv => !isSong,
|
HardwareAccelerationKind.Qsv => !isSong,
|
||||||
|
HardwareAccelerationKind.VideoToolbox => false,
|
||||||
_ => false
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -171,9 +170,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
audioFilterQueue.Add($"apad=whole_dur={durationString}ms");
|
audioFilterQueue.Add($"apad=whole_dur={durationString}ms");
|
||||||
});
|
});
|
||||||
|
|
||||||
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None && !isHardwareDecode &&
|
bool usesHardwareFilters = acceleration != HardwareAccelerationKind.None &&
|
||||||
|
acceleration != HardwareAccelerationKind.VideoToolbox &&
|
||||||
|
!isHardwareDecode &&
|
||||||
(_deinterlace || _scaleToSize.IsSome);
|
(_deinterlace || _scaleToSize.IsSome);
|
||||||
|
|
||||||
if (isSong)
|
if (isSong)
|
||||||
{
|
{
|
||||||
switch (acceleration)
|
switch (acceleration)
|
||||||
@@ -250,14 +251,19 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
_ => $"scale={size.Width}:{size.Height}:flags=fast_bilinear"
|
||||||
};
|
};
|
||||||
|
|
||||||
if (_randomColor.IsNone && !string.IsNullOrWhiteSpace(filter))
|
if (!string.IsNullOrWhiteSpace(filter))
|
||||||
{
|
{
|
||||||
videoFilterQueue.Add(filter);
|
videoFilterQueue.Add(filter);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome;
|
bool scaleOrPad = _scaleToSize.IsSome || _padToSize.IsSome;
|
||||||
bool usesSoftwareFilters = scaleOrPad || _watermark.IsSome;
|
bool usesSoftwareFilters = _padToSize.IsSome || _watermark.IsSome;
|
||||||
|
|
||||||
|
if (scaleOrPad && _boxBlur == false)
|
||||||
|
{
|
||||||
|
videoFilterQueue.Add("setsar=1");
|
||||||
|
}
|
||||||
|
|
||||||
if (usesSoftwareFilters)
|
if (usesSoftwareFilters)
|
||||||
{
|
{
|
||||||
@@ -275,21 +281,15 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
};
|
};
|
||||||
videoFilterQueue.Add(format);
|
videoFilterQueue.Add(format);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scaleOrPad && _boxBlur == false && _randomColor.IsNone)
|
|
||||||
{
|
|
||||||
videoFilterQueue.Add("setsar=1");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_boxBlur)
|
if (_boxBlur)
|
||||||
{
|
{
|
||||||
videoFilterQueue.Add("boxblur=40");
|
videoFilterQueue.Add("boxblur=40");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (int color in _randomColor)
|
if (videoOnly)
|
||||||
{
|
{
|
||||||
videoFilterQueue.Add(
|
videoFilterQueue.Add("deband");
|
||||||
$"palettegen=max_colors=8,crop=1:1:{color}:0,scale={_resolution.Width}:{_resolution.Height},setsar=1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ChannelWatermark watermark in _watermark)
|
foreach (ChannelWatermark watermark in _watermark)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
result.VideoCodec = "copy";
|
result.VideoCodec = "copy";
|
||||||
result.Deinterlace = false;
|
result.Deinterlace = false;
|
||||||
break;
|
break;
|
||||||
case StreamingMode.HttpLiveStreamingHybrid:
|
case StreamingMode.TransportStreamHybrid:
|
||||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||||
case StreamingMode.TransportStream:
|
case StreamingMode.TransportStream:
|
||||||
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
|
result.HardwareAcceleration = ffmpegProfile.HardwareAcceleration;
|
||||||
@@ -121,6 +121,11 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
(HardwareAccelerationKind.Qsv, "h264", _) => "h264_qsv",
|
(HardwareAccelerationKind.Qsv, "h264", _) => "h264_qsv",
|
||||||
(HardwareAccelerationKind.Qsv, "hevc", _) => "hevc_qsv",
|
(HardwareAccelerationKind.Qsv, "hevc", _) => "hevc_qsv",
|
||||||
(HardwareAccelerationKind.Qsv, "mpeg2video", _) => "mpeg2_qsv",
|
(HardwareAccelerationKind.Qsv, "mpeg2video", _) => "mpeg2_qsv",
|
||||||
|
|
||||||
|
// temp disable mpeg4 hardware decoding for all vaapi
|
||||||
|
// TODO: check for codec support
|
||||||
|
(HardwareAccelerationKind.Vaapi, "mpeg4", _) => "mpeg4",
|
||||||
|
|
||||||
_ => null
|
_ => null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
@@ -107,6 +108,10 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
_arguments.Add("-hwaccel_output_format");
|
_arguments.Add("-hwaccel_output_format");
|
||||||
_arguments.Add("vaapi");
|
_arguments.Add("vaapi");
|
||||||
break;
|
break;
|
||||||
|
case HardwareAccelerationKind.VideoToolbox:
|
||||||
|
_arguments.Add("-hwaccel");
|
||||||
|
_arguments.Add("videotoolbox");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel);
|
_complexFilterBuilder = _complexFilterBuilder.WithHardwareAcceleration(hwAccel);
|
||||||
@@ -201,6 +206,13 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FFmpegProcessBuilder WithCopyCodec()
|
||||||
|
{
|
||||||
|
_arguments.Add("-c");
|
||||||
|
_arguments.Add("copy");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public FFmpegProcessBuilder WithWatermark(
|
public FFmpegProcessBuilder WithWatermark(
|
||||||
Option<WatermarkOptions> watermarkOptions,
|
Option<WatermarkOptions> watermarkOptions,
|
||||||
IDisplaySize resolution)
|
IDisplaySize resolution)
|
||||||
@@ -285,8 +297,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
string videoPath,
|
string videoPath,
|
||||||
Option<string> codec,
|
Option<string> codec,
|
||||||
Option<string> pixelFormat,
|
Option<string> pixelFormat,
|
||||||
bool boxBlur,
|
bool boxBlur)
|
||||||
Option<int> randomColor)
|
|
||||||
{
|
{
|
||||||
_noAutoScale = true;
|
_noAutoScale = true;
|
||||||
_outputFramerate = 30;
|
_outputFramerate = 30;
|
||||||
@@ -294,8 +305,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
_complexFilterBuilder = _complexFilterBuilder
|
_complexFilterBuilder = _complexFilterBuilder
|
||||||
.WithInputCodec(codec)
|
.WithInputCodec(codec)
|
||||||
.WithInputPixelFormat(pixelFormat)
|
.WithInputPixelFormat(pixelFormat)
|
||||||
.WithBoxBlur(boxBlur)
|
.WithBoxBlur(boxBlur);
|
||||||
.WithRandomColor(randomColor);
|
|
||||||
|
|
||||||
_arguments.Add("-i");
|
_arguments.Add("-i");
|
||||||
_arguments.Add(videoPath);
|
_arguments.Add(videoPath);
|
||||||
@@ -627,13 +637,13 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
switch (_vaapiDriver)
|
switch (_vaapiDriver)
|
||||||
{
|
{
|
||||||
case VaapiDriver.i965:
|
case VaapiDriver.i965:
|
||||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "i965");
|
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "i965";
|
||||||
break;
|
break;
|
||||||
case VaapiDriver.iHD:
|
case VaapiDriver.iHD:
|
||||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "iHD");
|
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "iHD";
|
||||||
break;
|
break;
|
||||||
case VaapiDriver.RadeonSI:
|
case VaapiDriver.RadeonSI:
|
||||||
startInfo.EnvironmentVariables.Add("LIBVA_DRIVER_NAME", "radeonsi");
|
startInfo.EnvironmentVariables["LIBVA_DRIVER_NAME"] = "radeonsi";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,7 +653,18 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
string fileName = _isConcat
|
string fileName = _isConcat
|
||||||
? Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-concat.log")
|
? Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-concat.log")
|
||||||
: Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-transcode.log");
|
: Path.Combine(FileSystemLayout.FFmpegReportsFolder, "ffmpeg-%t-transcode.log");
|
||||||
startInfo.EnvironmentVariables.Add("FFREPORT", $"file={fileName}:level=32");
|
|
||||||
|
// rework filename in a format that works on windows
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
// \ is escape, so use / for directory separators
|
||||||
|
fileName = fileName.Replace(@"\", @"/");
|
||||||
|
|
||||||
|
// colon after drive letter needs to be escaped
|
||||||
|
fileName = fileName.Replace(@":/", @"\:/");
|
||||||
|
}
|
||||||
|
|
||||||
|
startInfo.EnvironmentVariables["FFREPORT"] = $"file={fileName}:level=32";
|
||||||
}
|
}
|
||||||
|
|
||||||
startInfo.ArgumentList.Add("-nostdin");
|
startInfo.ArgumentList.Add("-nostdin");
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
outPoint);
|
outPoint);
|
||||||
|
|
||||||
Option<WatermarkOptions> watermarkOptions =
|
Option<WatermarkOptions> watermarkOptions =
|
||||||
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None);
|
await GetWatermarkOptions(channel, globalWatermark, videoVersion, None, None);
|
||||||
|
|
||||||
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
FFmpegProcessBuilder builder = new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||||
.WithThreads(playbackSettings.ThreadCount)
|
.WithThreads(playbackSettings.ThreadCount)
|
||||||
@@ -247,6 +247,24 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host)
|
||||||
|
{
|
||||||
|
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.ConcatSettings;
|
||||||
|
|
||||||
|
return new FFmpegProcessBuilder(ffmpegPath, saveReports, _logger)
|
||||||
|
.WithThreads(1)
|
||||||
|
.WithQuiet()
|
||||||
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
|
.WithRealtimeOutput(playbackSettings.RealtimeOutput)
|
||||||
|
.WithInput($"http://localhost:{Settings.ListenPort}/iptv/channel/{channel.Number}.m3u8?mode=segmenter")
|
||||||
|
.WithMap("0")
|
||||||
|
.WithCopyCodec()
|
||||||
|
.WithMetadata(channel, None)
|
||||||
|
.WithFormat("mpegts")
|
||||||
|
.WithPipe()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
|
public Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile)
|
||||||
{
|
{
|
||||||
return new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
return new FFmpegProcessBuilder(ffmpegPath, false, _logger)
|
||||||
@@ -276,7 +294,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
MediaVersion videoVersion,
|
MediaVersion videoVersion,
|
||||||
string videoPath,
|
string videoPath,
|
||||||
bool boxBlur,
|
bool boxBlur,
|
||||||
Option<int> randomColor,
|
Option<string> watermarkPath,
|
||||||
ChannelWatermarkLocation watermarkLocation,
|
ChannelWatermarkLocation watermarkLocation,
|
||||||
int horizontalMarginPercent,
|
int horizontalMarginPercent,
|
||||||
int verticalMarginPercent,
|
int verticalMarginPercent,
|
||||||
@@ -303,7 +321,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
: None;
|
: None;
|
||||||
|
|
||||||
Option<WatermarkOptions> watermarkOptions =
|
Option<WatermarkOptions> watermarkOptions =
|
||||||
await GetWatermarkOptions(channel, globalWatermark, videoVersion, watermarkOverride);
|
await GetWatermarkOptions(channel, globalWatermark, videoVersion, watermarkOverride, watermarkPath);
|
||||||
|
|
||||||
FFmpegPlaybackSettings playbackSettings =
|
FFmpegPlaybackSettings playbackSettings =
|
||||||
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
||||||
@@ -323,7 +341,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.WithThreads(1)
|
.WithThreads(1)
|
||||||
.WithQuiet()
|
.WithQuiet()
|
||||||
.WithFormatFlags(playbackSettings.FormatFlags)
|
.WithFormatFlags(playbackSettings.FormatFlags)
|
||||||
.WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur, randomColor)
|
.WithSongInput(videoPath, videoStream.Codec, videoStream.PixelFormat, boxBlur)
|
||||||
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
|
.WithWatermark(watermarkOptions, channel.FFmpegProfile.Resolution)
|
||||||
.WithSubtitleFile(subtitleFile);
|
.WithSubtitleFile(subtitleFile);
|
||||||
|
|
||||||
@@ -370,7 +388,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
Channel channel,
|
Channel channel,
|
||||||
Option<ChannelWatermark> globalWatermark,
|
Option<ChannelWatermark> globalWatermark,
|
||||||
MediaVersion videoVersion,
|
MediaVersion videoVersion,
|
||||||
Option<ChannelWatermark> watermarkOverride)
|
Option<ChannelWatermark> watermarkOverride,
|
||||||
|
Option<string> watermarkPath)
|
||||||
{
|
{
|
||||||
if (videoVersion is BackgroundImageMediaVersion)
|
if (videoVersion is BackgroundImageMediaVersion)
|
||||||
{
|
{
|
||||||
@@ -384,7 +403,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
{
|
{
|
||||||
return new WatermarkOptions(
|
return new WatermarkOptions(
|
||||||
watermarkOverride,
|
watermarkOverride,
|
||||||
videoVersion.MediaFiles.Head().Path,
|
await watermarkPath.IfNoneAsync(videoVersion.MediaFiles.Head().Path),
|
||||||
0,
|
0,
|
||||||
false);
|
false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
@@ -50,22 +51,22 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
|
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
string[] backgrounds =
|
string[] backgrounds =
|
||||||
{
|
{
|
||||||
"background_blank.png",
|
"song_background_1.png",
|
||||||
"background_e.png",
|
"song_background_2.png",
|
||||||
"background_t.png",
|
"song_background_3.png"
|
||||||
"background_v.png"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// use random ETV color by default
|
// use random ETV color by default
|
||||||
string artworkPath = Path.Combine(
|
string backgroundPath = Path.Combine(
|
||||||
FileSystemLayout.ResourcesCacheFolder,
|
FileSystemLayout.ResourcesCacheFolder,
|
||||||
backgrounds[NextRandom(backgrounds.Length)]);
|
backgrounds[NextRandom(backgrounds.Length)]);
|
||||||
|
|
||||||
|
Option<string> watermarkPath = None;
|
||||||
|
|
||||||
var boxBlur = false;
|
var boxBlur = false;
|
||||||
Option<int> randomColor = None;
|
|
||||||
|
|
||||||
const int HORIZONTAL_MARGIN_PERCENT = 3;
|
const int HORIZONTAL_MARGIN_PERCENT = 3;
|
||||||
const int VERTICAL_MARGIN_PERCENT = 5;
|
const int VERTICAL_MARGIN_PERCENT = 5;
|
||||||
@@ -115,15 +116,18 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
int leftMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
int leftMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
||||||
int rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
int rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
||||||
|
|
||||||
switch (watermarkLocation)
|
if (metadata.Artwork.Any(a => a.ArtworkKind == ArtworkKind.Thumbnail))
|
||||||
{
|
{
|
||||||
case ChannelWatermarkLocation.BottomLeft:
|
switch (watermarkLocation)
|
||||||
leftMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
{
|
||||||
break;
|
case ChannelWatermarkLocation.BottomLeft:
|
||||||
case ChannelWatermarkLocation.BottomRight:
|
leftMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
||||||
leftMarginPercent = rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
break;
|
||||||
rightMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
case ChannelWatermarkLocation.BottomRight:
|
||||||
break;
|
leftMarginPercent = rightMarginPercent = HORIZONTAL_MARGIN_PERCENT;
|
||||||
|
rightMarginPercent += WATERMARK_WIDTH_PERCENT + HORIZONTAL_MARGIN_PERCENT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width);
|
var leftMargin = (int)Math.Round(leftMarginPercent / 100.0 * channel.FFmpegProfile.Resolution.Width);
|
||||||
@@ -135,7 +139,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
.WithFontName("OPTIKabel-Heavy")
|
.WithFontName("OPTIKabel-Heavy")
|
||||||
.WithFontSize(fontSize)
|
.WithFontSize(fontSize)
|
||||||
.WithPrimaryColor("&HFFFFFF")
|
.WithPrimaryColor("&HFFFFFF")
|
||||||
.WithOutlineColor("&H555555")
|
.WithOutlineColor("&H444444")
|
||||||
.WithAlignment(0)
|
.WithAlignment(0)
|
||||||
.WithMarginRight(rightMargin)
|
.WithMarginRight(rightMargin)
|
||||||
.WithMarginLeft(leftMargin)
|
.WithMarginLeft(leftMargin)
|
||||||
@@ -149,23 +153,6 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
foreach (Artwork artwork in Optional(
|
foreach (Artwork artwork in Optional(
|
||||||
metadata.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Thumbnail)))
|
metadata.Artwork.Find(a => a.ArtworkKind == ArtworkKind.Thumbnail)))
|
||||||
{
|
{
|
||||||
int backgroundRoll = NextRandom(16);
|
|
||||||
if (backgroundRoll < 8)
|
|
||||||
{
|
|
||||||
randomColor = backgroundRoll;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
boxBlur = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string customPath = _imageCache.GetPathForImage(
|
|
||||||
artwork.Path,
|
|
||||||
ArtworkKind.Thumbnail,
|
|
||||||
Option<int>.None);
|
|
||||||
|
|
||||||
artworkPath = customPath;
|
|
||||||
|
|
||||||
// signal that we want to use cover art as watermark
|
// signal that we want to use cover art as watermark
|
||||||
videoVersion = new CoverArtMediaVersion
|
videoVersion = new CoverArtMediaVersion
|
||||||
{
|
{
|
||||||
@@ -179,10 +166,42 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
|
new() { MediaStreamKind = MediaStreamKind.Video, Index = 0 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
string customPath = _imageCache.GetPathForImage(
|
||||||
|
artwork.Path,
|
||||||
|
ArtworkKind.Thumbnail,
|
||||||
|
Option<int>.None);
|
||||||
|
|
||||||
|
watermarkPath = customPath;
|
||||||
|
|
||||||
|
// randomize selected blur hash
|
||||||
|
var hashes = new List<string>
|
||||||
|
{
|
||||||
|
artwork.BlurHash43,
|
||||||
|
artwork.BlurHash54,
|
||||||
|
artwork.BlurHash64
|
||||||
|
}.Filter(s => !string.IsNullOrWhiteSpace(s)).ToList();
|
||||||
|
|
||||||
|
if (hashes.Any())
|
||||||
|
{
|
||||||
|
string hash = hashes[NextRandom(hashes.Count)];
|
||||||
|
|
||||||
|
backgroundPath = await _imageCache.WriteBlurHash(
|
||||||
|
hash,
|
||||||
|
channel.FFmpegProfile.Resolution);
|
||||||
|
|
||||||
|
videoVersion.Height = channel.FFmpegProfile.Resolution.Height;
|
||||||
|
videoVersion.Width = channel.FFmpegProfile.Resolution.Width;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backgroundPath = customPath;
|
||||||
|
boxBlur = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string videoPath = artworkPath;
|
string videoPath = backgroundPath;
|
||||||
|
|
||||||
videoVersion.MediaFiles = new List<MediaFile>
|
videoVersion.MediaFiles = new List<MediaFile>
|
||||||
{
|
{
|
||||||
@@ -197,7 +216,7 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
videoVersion,
|
videoVersion,
|
||||||
videoPath,
|
videoPath,
|
||||||
boxBlur,
|
boxBlur,
|
||||||
randomColor,
|
watermarkPath,
|
||||||
watermarkLocation,
|
watermarkLocation,
|
||||||
HORIZONTAL_MARGIN_PERCENT,
|
HORIZONTAL_MARGIN_PERCENT,
|
||||||
VERTICAL_MARGIN_PERCENT,
|
VERTICAL_MARGIN_PERCENT,
|
||||||
|
|||||||
@@ -118,8 +118,8 @@ namespace ErsatzTV.Core.FFmpeg
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine("[V4+ Styles]");
|
sb.AppendLine("[V4+ Styles]");
|
||||||
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BorderStyle, Shadow, Alignment, Encoding");
|
sb.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BorderStyle, Outline, Shadow, Alignment, Encoding");
|
||||||
sb.AppendLine($"Style: Default,{await _fontName.IfNoneAsync("")},{await _fontSize.IfNoneAsync(32)},{await _primaryColor.IfNoneAsync("")},{await _outlineColor.IfNoneAsync("")},{await _borderStyle.IfNoneAsync(0)},{await _shadow.IfNoneAsync(0)}, {await _alignment.IfNoneAsync(0)},1");
|
sb.AppendLine($"Style: Default,{await _fontName.IfNoneAsync("")},{await _fontSize.IfNoneAsync(32)},{await _primaryColor.IfNoneAsync("")},{await _outlineColor.IfNoneAsync("")},{await _borderStyle.IfNoneAsync(0)},1,{await _shadow.IfNoneAsync(0)},{await _alignment.IfNoneAsync(0)},1");
|
||||||
|
|
||||||
sb.AppendLine("[Events]");
|
sb.AppendLine("[Events]");
|
||||||
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
|
sb.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ namespace ErsatzTV.Core.Hdhr
|
|||||||
|
|
||||||
public string URL => _channel.StreamingMode switch
|
public string URL => _channel.StreamingMode switch
|
||||||
{
|
{
|
||||||
StreamingMode.HttpLiveStreamingDirect or StreamingMode.HttpLiveStreamingHybrid =>
|
StreamingMode.HttpLiveStreamingDirect => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
|
||||||
$"{_scheme}://{_host}/iptv/channel/{_channel.Number}.m3u8",
|
|
||||||
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
|
_ => $"{_scheme}://{_host}/iptv/channel/{_channel.Number}.ts"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ErsatzTV.Core.Health.Checks;
|
||||||
|
|
||||||
|
public interface IFileNotFoundHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace ErsatzTV.Core.Health
|
using LanguageExt;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Core.Health
|
||||||
{
|
{
|
||||||
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message);
|
public record HealthCheckResult(string Title, HealthCheckStatus Status, string Message, Option<string> Link);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
|
|||||||
|
|
||||||
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||||
|
|
||||||
|
Process WrapSegmenter(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||||
|
|
||||||
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
|
Process ConvertToPng(string ffmpegPath, string inputFile, string outputFile);
|
||||||
|
|
||||||
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
|
Process ExtractAttachedPicAsPng(string ffmpegPath, string inputFile, int streamIndex, string outputFile);
|
||||||
@@ -50,7 +52,7 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
|
|||||||
MediaVersion videoVersion,
|
MediaVersion videoVersion,
|
||||||
string videoPath,
|
string videoPath,
|
||||||
bool boxBlur,
|
bool boxBlur,
|
||||||
Option<int> randomColor,
|
Option<string> watermarkPath,
|
||||||
ChannelWatermarkLocation watermarkLocation,
|
ChannelWatermarkLocation watermarkLocation,
|
||||||
int horizontalMarginPercent,
|
int horizontalMarginPercent,
|
||||||
int verticalMarginPercent,
|
int verticalMarginPercent,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Images
|
namespace ErsatzTV.Core.Interfaces.Images
|
||||||
@@ -12,5 +13,7 @@ namespace ErsatzTV.Core.Interfaces.Images
|
|||||||
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
Task<Either<BaseError, string>> CopyArtworkToCache(string path, ArtworkKind artworkKind);
|
||||||
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
|
string GetPathForImage(string fileName, ArtworkKind artworkKind, Option<int> maybeMaxHeight);
|
||||||
Task<bool> IsAnimated(string fileName);
|
Task<bool> IsAnimated(string fileName);
|
||||||
|
Task<string> CalculateBlurHash(string fileName, ArtworkKind artworkKind, int x, int y);
|
||||||
|
Task<string> WriteBlurHash(string blurHash, IDisplaySize targetSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
|||||||
IEnumerable<string> ListSubdirectories(string folder);
|
IEnumerable<string> ListSubdirectories(string folder);
|
||||||
IEnumerable<string> ListFiles(string folder);
|
IEnumerable<string> ListFiles(string folder);
|
||||||
bool FileExists(string path);
|
bool FileExists(string path);
|
||||||
|
bool FolderExists(string folder);
|
||||||
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
|
Task<Either<BaseError, Unit>> CopyFile(string source, string destination);
|
||||||
Unit EmptyFolder(string folder);
|
Unit EmptyFolder(string folder);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<List<int>> GetMediaIdsByLocalPath(int libraryPathId);
|
Task<List<int>> GetMediaIdsByLocalPath(int libraryPathId);
|
||||||
Task DeleteLocalPath(int libraryPathId);
|
Task DeleteLocalPath(int libraryPathId);
|
||||||
Task<Unit> SetEtag(LibraryPath libraryPath, Option<LibraryFolder> knownFolder, string path, string etag);
|
Task<Unit> SetEtag(LibraryPath libraryPath, Option<LibraryFolder> knownFolder, string path, string etag);
|
||||||
|
Task<Unit> CleanEtagsForLibraryPath(LibraryPath libraryPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using LanguageExt;
|
||||||
|
|
||||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||||
{
|
{
|
||||||
public interface IMediaItemRepository
|
public interface IMediaItemRepository
|
||||||
{
|
{
|
||||||
Task<List<string>> GetAllLanguageCodes();
|
Task<List<string>> GetAllLanguageCodes();
|
||||||
|
Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path);
|
||||||
|
Task<Unit> FlagNormal(MediaItem mediaItem);
|
||||||
|
Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,17 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
|||||||
Task<bool> RemoveActor(Actor actor);
|
Task<bool> RemoveActor(Actor actor);
|
||||||
Task<bool> Update(Domain.Metadata metadata);
|
Task<bool> Update(Domain.Metadata metadata);
|
||||||
Task<bool> Add(Domain.Metadata metadata);
|
Task<bool> Add(Domain.Metadata metadata);
|
||||||
Task<bool> UpdateLocalStatistics(int mediaVersionId, MediaVersion incoming, bool updateVersion = true);
|
Task<bool> UpdateLocalStatistics(MediaItem mediaItem, MediaVersion incoming, bool updateVersion = true);
|
||||||
Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming);
|
Task<bool> UpdatePlexStatistics(int mediaVersionId, MediaVersion incoming);
|
||||||
Task<Unit> UpdateArtworkPath(Artwork artwork);
|
Task<Unit> UpdateArtworkPath(Artwork artwork);
|
||||||
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
|
Task<Unit> AddArtwork(Domain.Metadata metadata, Artwork artwork);
|
||||||
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);
|
Task<Unit> RemoveArtwork(Domain.Metadata metadata, ArtworkKind artworkKind);
|
||||||
|
Task<bool> CloneArtwork(
|
||||||
|
Domain.Metadata metadata,
|
||||||
|
Option<Artwork> maybeArtwork,
|
||||||
|
ArtworkKind artworkKind,
|
||||||
|
string sourcePath,
|
||||||
|
DateTime lastWriteTime);
|
||||||
Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated);
|
Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated);
|
||||||
Task<Unit> MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated);
|
Task<Unit> MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated);
|
||||||
Task<Unit> MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated);
|
Task<Unit> MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ namespace ErsatzTV.Core.Interfaces.Search
|
|||||||
public int Version { get; }
|
public int Version { get; }
|
||||||
Task<bool> Initialize(ILocalFileSystem localFileSystem);
|
Task<bool> Initialize(ILocalFileSystem localFileSystem);
|
||||||
Task<Unit> Rebuild(ISearchRepository searchRepository, List<int> itemIds);
|
Task<Unit> Rebuild(ISearchRepository searchRepository, List<int> itemIds);
|
||||||
|
Task<Unit> RebuildItems(ISearchRepository searchRepository, List<int> itemIds);
|
||||||
Task<Unit> AddItems(ISearchRepository searchRepository, List<MediaItem> items);
|
Task<Unit> AddItems(ISearchRepository searchRepository, List<MediaItem> items);
|
||||||
Task<Unit> UpdateItems(ISearchRepository searchRepository, List<MediaItem> items);
|
Task<Unit> UpdateItems(ISearchRepository searchRepository, List<MediaItem> items);
|
||||||
Task<Unit> RemoveItems(List<int> ids);
|
Task<Unit> RemoveItems(List<int> ids);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ namespace ErsatzTV.Core.Iptv
|
|||||||
|
|
||||||
var xmltv = $"{_scheme}://{_host}/iptv/xmltv.xml";
|
var xmltv = $"{_scheme}://{_host}/iptv/xmltv.xml";
|
||||||
sb.AppendLine($"#EXTM3U url-tvg=\"{xmltv}\" x-tvg-url=\"{xmltv}\"");
|
sb.AppendLine($"#EXTM3U url-tvg=\"{xmltv}\" x-tvg-url=\"{xmltv}\"");
|
||||||
foreach (Channel channel in _channels.OrderBy(c => c.Number))
|
foreach (Channel channel in _channels.OrderBy(c => decimal.Parse(c.Number)))
|
||||||
{
|
{
|
||||||
string logo = Optional(channel.Artwork).Flatten()
|
string logo = Optional(channel.Artwork).Flatten()
|
||||||
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
.Filter(a => a.ArtworkKind == ArtworkKind.Logo)
|
||||||
@@ -43,9 +43,9 @@ namespace ErsatzTV.Core.Iptv
|
|||||||
string format = channel.StreamingMode switch
|
string format = channel.StreamingMode switch
|
||||||
{
|
{
|
||||||
StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct",
|
StreamingMode.HttpLiveStreamingDirect => "m3u8?mode=hls-direct",
|
||||||
StreamingMode.HttpLiveStreamingHybrid => "m3u8",
|
|
||||||
StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter",
|
StreamingMode.HttpLiveStreamingSegmenter => "m3u8?mode=segmenter",
|
||||||
_ => "ts"
|
StreamingMode.TransportStreamHybrid => "ts",
|
||||||
|
_ => "ts?mode=legacy"
|
||||||
};
|
};
|
||||||
|
|
||||||
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();
|
string vcodec = channel.FFmpegProfile.VideoCodec.Split("_").Head();
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
|
|
||||||
public bool FileExists(string path) => File.Exists(path);
|
public bool FileExists(string path) => File.Exists(path);
|
||||||
|
|
||||||
|
public bool FolderExists(string folder) => Directory.Exists(folder);
|
||||||
|
|
||||||
public async Task<Either<BaseError, Unit>> CopyFile(string source, string destination)
|
public async Task<Either<BaseError, Unit>> CopyFile(string source, string destination)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -57,11 +57,13 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
private readonly ILocalStatisticsProvider _localStatisticsProvider;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMetadataRepository _metadataRepository;
|
private readonly IMetadataRepository _metadataRepository;
|
||||||
|
private readonly IMediaItemRepository _mediaItemRepository;
|
||||||
|
|
||||||
protected LocalFolderScanner(
|
protected LocalFolderScanner(
|
||||||
ILocalFileSystem localFileSystem,
|
ILocalFileSystem localFileSystem,
|
||||||
ILocalStatisticsProvider localStatisticsProvider,
|
ILocalStatisticsProvider localStatisticsProvider,
|
||||||
IMetadataRepository metadataRepository,
|
IMetadataRepository metadataRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IImageCache imageCache,
|
IImageCache imageCache,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
@@ -70,6 +72,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
_localFileSystem = localFileSystem;
|
_localFileSystem = localFileSystem;
|
||||||
_localStatisticsProvider = localStatisticsProvider;
|
_localStatisticsProvider = localStatisticsProvider;
|
||||||
_metadataRepository = metadataRepository;
|
_metadataRepository = metadataRepository;
|
||||||
|
_mediaItemRepository = mediaItemRepository;
|
||||||
_imageCache = imageCache;
|
_imageCache = imageCache;
|
||||||
_ffmpegProcessService = ffmpegProcessService;
|
_ffmpegProcessService = ffmpegProcessService;
|
||||||
_tempFilePool = tempFilePool;
|
_tempFilePool = tempFilePool;
|
||||||
@@ -139,6 +142,17 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
_logger.LogDebug("Refreshing {Attribute} from {Path}", artworkKind, artworkFile);
|
||||||
|
|
||||||
|
string sourcePath = artworkFile;
|
||||||
|
if (await _metadataRepository.CloneArtwork(
|
||||||
|
metadata,
|
||||||
|
maybeArtwork,
|
||||||
|
artworkKind,
|
||||||
|
sourcePath,
|
||||||
|
lastWriteTime))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// if ffmpeg path is passed, we need pre-processing
|
// if ffmpeg path is passed, we need pre-processing
|
||||||
foreach (string path in ffmpegPath)
|
foreach (string path in ffmpegPath)
|
||||||
{
|
{
|
||||||
@@ -179,7 +193,28 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
async artwork =>
|
async artwork =>
|
||||||
{
|
{
|
||||||
artwork.Path = cacheName;
|
artwork.Path = cacheName;
|
||||||
|
artwork.SourcePath = sourcePath;
|
||||||
artwork.DateUpdated = lastWriteTime;
|
artwork.DateUpdated = lastWriteTime;
|
||||||
|
|
||||||
|
if (metadata is SongMetadata)
|
||||||
|
{
|
||||||
|
artwork.BlurHash43 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
4,
|
||||||
|
3);
|
||||||
|
artwork.BlurHash54 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
5,
|
||||||
|
4);
|
||||||
|
artwork.BlurHash64 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
6,
|
||||||
|
4);
|
||||||
|
}
|
||||||
|
|
||||||
await _metadataRepository.UpdateArtworkPath(artwork);
|
await _metadataRepository.UpdateArtworkPath(artwork);
|
||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
@@ -187,10 +222,31 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
var artwork = new Artwork
|
var artwork = new Artwork
|
||||||
{
|
{
|
||||||
Path = cacheName,
|
Path = cacheName,
|
||||||
|
SourcePath = sourcePath,
|
||||||
DateAdded = DateTime.UtcNow,
|
DateAdded = DateTime.UtcNow,
|
||||||
DateUpdated = lastWriteTime,
|
DateUpdated = lastWriteTime,
|
||||||
ArtworkKind = artworkKind
|
ArtworkKind = artworkKind
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (metadata is SongMetadata)
|
||||||
|
{
|
||||||
|
artwork.BlurHash43 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
4,
|
||||||
|
3);
|
||||||
|
artwork.BlurHash54 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
5,
|
||||||
|
4);
|
||||||
|
artwork.BlurHash64 = await _imageCache.CalculateBlurHash(
|
||||||
|
cacheName,
|
||||||
|
artworkKind,
|
||||||
|
6,
|
||||||
|
4);
|
||||||
|
}
|
||||||
|
|
||||||
metadata.Artwork.Add(artwork);
|
metadata.Artwork.Add(artwork);
|
||||||
await _metadataRepository.AddArtwork(metadata, artwork);
|
await _metadataRepository.AddArtwork(metadata, artwork);
|
||||||
});
|
});
|
||||||
@@ -212,6 +268,29 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path) =>
|
||||||
|
_mediaItemRepository.FlagFileNotFound(libraryPath, path);
|
||||||
|
|
||||||
|
protected async Task<Either<BaseError, MediaItemScanResult<T>>> FlagNormal<T>(MediaItemScanResult<T> result)
|
||||||
|
where T : MediaItem
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
T mediaItem = result.Item;
|
||||||
|
if (mediaItem.State != MediaItemState.Normal)
|
||||||
|
{
|
||||||
|
await _mediaItemRepository.FlagNormal(mediaItem);
|
||||||
|
result.IsUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return BaseError.New(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected bool ShouldIncludeFolder(string folder) =>
|
protected bool ShouldIncludeFolder(string folder) =>
|
||||||
!Path.GetFileName(folder).StartsWith('.') &&
|
!Path.GetFileName(folder).StartsWith('.') &&
|
||||||
!_localFileSystem.FileExists(Path.Combine(folder, ".etvignore"));
|
!_localFileSystem.FileExists(Path.Combine(folder, ".etvignore"));
|
||||||
|
|||||||
@@ -969,9 +969,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int? GetYear(int year, string premiered)
|
private static int? GetYear(int? year, string premiered)
|
||||||
{
|
{
|
||||||
if (year > 1000)
|
if (year is > 1000)
|
||||||
{
|
{
|
||||||
return year;
|
return year;
|
||||||
}
|
}
|
||||||
@@ -989,9 +989,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DateTime? GetAired(int year, string aired)
|
private static DateTime? GetAired(int? year, string aired)
|
||||||
{
|
{
|
||||||
DateTime? fallback = year > 1000 ? new DateTime(year, 1, 1) : null;
|
DateTime? fallback = year is > 1000 ? new DateTime(year.Value, 1, 1) : null;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(aired))
|
if (string.IsNullOrWhiteSpace(aired))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Extensions;
|
using ErsatzTV.Core.Extensions;
|
||||||
@@ -131,7 +132,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
|
|
||||||
version.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
|
version.DateUpdated = _localFileSystem.GetLastWriteTime(filePath);
|
||||||
|
|
||||||
return await _metadataRepository.UpdateLocalStatistics(mediaItemVersion.Id, version) && durationChange;
|
return await _metadataRepository.UpdateLocalStatistics(mediaItem, version) && durationChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
private Task<Either<BaseError, FFprobe>> GetProbeOutput(string ffprobePath, string filePath)
|
||||||
@@ -141,7 +142,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
FileName = ffprobePath,
|
FileName = ffprobePath,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true,
|
RedirectStandardError = true,
|
||||||
UseShellExecute = false
|
UseShellExecute = false,
|
||||||
|
StandardOutputEncoding = Encoding.UTF8,
|
||||||
|
StandardErrorEncoding = Encoding.UTF8
|
||||||
};
|
};
|
||||||
|
|
||||||
startInfo.ArgumentList.Add("-v");
|
startInfo.ArgumentList.Add("-v");
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ISearchRepository searchRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
@@ -48,6 +49,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
localFileSystem,
|
localFileSystem,
|
||||||
localStatisticsProvider,
|
localStatisticsProvider,
|
||||||
metadataRepository,
|
metadataRepository,
|
||||||
|
mediaItemRepository,
|
||||||
imageCache,
|
imageCache,
|
||||||
ffmpegProcessService,
|
ffmpegProcessService,
|
||||||
tempFilePool,
|
tempFilePool,
|
||||||
@@ -140,7 +142,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
.BindT(movie => UpdateStatistics(movie, ffprobePath))
|
.BindT(movie => UpdateStatistics(movie, ffprobePath))
|
||||||
.BindT(UpdateMetadata)
|
.BindT(UpdateMetadata)
|
||||||
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster))
|
.BindT(movie => UpdateArtwork(movie, ArtworkKind.Poster))
|
||||||
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt));
|
.BindT(movie => UpdateArtwork(movie, ArtworkKind.FanArt))
|
||||||
|
.BindT(FlagNormal);
|
||||||
|
|
||||||
await maybeMovie.Match(
|
await maybeMovie.Match(
|
||||||
async result =>
|
async result =>
|
||||||
@@ -168,9 +171,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing missing movie at {Path}", path);
|
_logger.LogInformation("Flagging missing movie at {Path}", path);
|
||||||
List<int> ids = await _movieRepository.DeleteByPath(libraryPath, path);
|
List<int> ids = await FlagFileNotFound(libraryPath, path);
|
||||||
await _searchIndex.RemoveItems(ids);
|
await _searchIndex.RebuildItems(_searchRepository, ids);
|
||||||
}
|
}
|
||||||
else if (Path.GetFileName(path).StartsWith("._"))
|
else if (Path.GetFileName(path).StartsWith("._"))
|
||||||
{
|
{
|
||||||
@@ -180,6 +183,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath);
|
||||||
|
|
||||||
_searchIndex.Commit();
|
_searchIndex.Commit();
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
IArtistRepository artistRepository,
|
IArtistRepository artistRepository,
|
||||||
IMusicVideoRepository musicVideoRepository,
|
IMusicVideoRepository musicVideoRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
@@ -48,6 +49,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
localFileSystem,
|
localFileSystem,
|
||||||
localStatisticsProvider,
|
localStatisticsProvider,
|
||||||
metadataRepository,
|
metadataRepository,
|
||||||
|
mediaItemRepository,
|
||||||
imageCache,
|
imageCache,
|
||||||
ffmpegProcessService,
|
ffmpegProcessService,
|
||||||
tempFilePool,
|
tempFilePool,
|
||||||
@@ -135,9 +137,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing missing music video at {Path}", path);
|
_logger.LogInformation("Flagging missing music video at {Path}", path);
|
||||||
List<int> musicVideoIds = await _musicVideoRepository.DeleteByPath(libraryPath, path);
|
List<int> musicVideoIds = await FlagFileNotFound(libraryPath, path);
|
||||||
await _searchIndex.RemoveItems(musicVideoIds);
|
await _searchIndex.RebuildItems(_searchRepository, musicVideoIds);
|
||||||
}
|
}
|
||||||
else if (Path.GetFileName(path).StartsWith("._"))
|
else if (Path.GetFileName(path).StartsWith("._"))
|
||||||
{
|
{
|
||||||
@@ -146,6 +148,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
await _searchIndex.RemoveItems(musicVideoIds);
|
await _searchIndex.RemoveItems(musicVideoIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath);
|
||||||
|
|
||||||
List<int> artistIds = await _artistRepository.DeleteEmptyArtists(libraryPath);
|
List<int> artistIds = await _artistRepository.DeleteEmptyArtists(libraryPath);
|
||||||
await _searchIndex.RemoveItems(artistIds);
|
await _searchIndex.RemoveItems(artistIds);
|
||||||
@@ -276,7 +280,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
.GetOrAdd(artist, libraryPath, file)
|
.GetOrAdd(artist, libraryPath, file)
|
||||||
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
|
.BindT(musicVideo => UpdateStatistics(musicVideo, ffprobePath))
|
||||||
.BindT(UpdateMetadata)
|
.BindT(UpdateMetadata)
|
||||||
.BindT(UpdateThumbnail);
|
.BindT(UpdateThumbnail)
|
||||||
|
.BindT(FlagNormal);
|
||||||
|
|
||||||
await maybeMusicVideo.Match(
|
await maybeMusicVideo.Match(
|
||||||
async result =>
|
async result =>
|
||||||
|
|||||||
@@ -9,8 +9,15 @@ namespace ErsatzTV.Core.Metadata.Nfo
|
|||||||
[XmlElement("title")]
|
[XmlElement("title")]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
[XmlIgnore]
|
||||||
|
public int? Year { get; set; }
|
||||||
|
|
||||||
[XmlElement("year")]
|
[XmlElement("year")]
|
||||||
public int Year { get; set; }
|
public string YearAsText
|
||||||
|
{
|
||||||
|
get => Year.HasValue ? Year.ToString() : null;
|
||||||
|
set => Year = !string.IsNullOrWhiteSpace(value) ? int.Parse(value) : default(int?);
|
||||||
|
}
|
||||||
|
|
||||||
[XmlElement("plot")]
|
[XmlElement("plot")]
|
||||||
public string Plot { get; set; }
|
public string Plot { get; set; }
|
||||||
|
|||||||
@@ -40,12 +40,14 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
ISearchRepository searchRepository,
|
ISearchRepository searchRepository,
|
||||||
IOtherVideoRepository otherVideoRepository,
|
IOtherVideoRepository otherVideoRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
ILogger<OtherVideoFolderScanner> logger) : base(
|
ILogger<OtherVideoFolderScanner> logger) : base(
|
||||||
localFileSystem,
|
localFileSystem,
|
||||||
localStatisticsProvider,
|
localStatisticsProvider,
|
||||||
metadataRepository,
|
metadataRepository,
|
||||||
|
mediaItemRepository,
|
||||||
imageCache,
|
imageCache,
|
||||||
ffmpegProcessService,
|
ffmpegProcessService,
|
||||||
tempFilePool,
|
tempFilePool,
|
||||||
@@ -77,9 +79,15 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
var foldersCompleted = 0;
|
var foldersCompleted = 0;
|
||||||
|
|
||||||
var folderQueue = new Queue<string>();
|
var folderQueue = new Queue<string>();
|
||||||
|
|
||||||
|
if (ShouldIncludeFolder(libraryPath.Path))
|
||||||
|
{
|
||||||
|
folderQueue.Enqueue(libraryPath.Path);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
||||||
.Filter(ShouldIncludeFolder)
|
.Filter(ShouldIncludeFolder)
|
||||||
.OrderBy(identity))
|
.OrderBy(identity))
|
||||||
{
|
{
|
||||||
folderQueue.Enqueue(folder);
|
folderQueue.Enqueue(folder);
|
||||||
}
|
}
|
||||||
@@ -127,7 +135,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
|
Either<BaseError, MediaItemScanResult<OtherVideo>> maybeVideo = await _otherVideoRepository
|
||||||
.GetOrAdd(libraryPath, file)
|
.GetOrAdd(libraryPath, file)
|
||||||
.BindT(video => UpdateStatistics(video, ffprobePath))
|
.BindT(video => UpdateStatistics(video, ffprobePath))
|
||||||
.BindT(UpdateMetadata);
|
.BindT(UpdateMetadata)
|
||||||
|
.BindT(FlagNormal);
|
||||||
|
|
||||||
await maybeVideo.Match(
|
await maybeVideo.Match(
|
||||||
async result =>
|
async result =>
|
||||||
@@ -155,9 +164,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing missing other video at {Path}", path);
|
_logger.LogInformation("Flagging missing other video at {Path}", path);
|
||||||
List<int> otherVideoIds = await _otherVideoRepository.DeleteByPath(libraryPath, path);
|
List<int> otherVideoIds = await FlagFileNotFound(libraryPath, path);
|
||||||
await _searchIndex.RemoveItems(otherVideoIds);
|
await _searchIndex.RebuildItems(_searchRepository, otherVideoIds);
|
||||||
}
|
}
|
||||||
else if (Path.GetFileName(path).StartsWith("._"))
|
else if (Path.GetFileName(path).StartsWith("._"))
|
||||||
{
|
{
|
||||||
@@ -166,6 +175,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
await _searchIndex.RemoveItems(otherVideoIds);
|
await _searchIndex.RemoveItems(otherVideoIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath);
|
||||||
|
|
||||||
_searchIndex.Commit();
|
_searchIndex.Commit();
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
ISearchRepository searchRepository,
|
ISearchRepository searchRepository,
|
||||||
ISongRepository songRepository,
|
ISongRepository songRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
ILogger<SongFolderScanner> logger) : base(
|
ILogger<SongFolderScanner> logger) : base(
|
||||||
localFileSystem,
|
localFileSystem,
|
||||||
localStatisticsProvider,
|
localStatisticsProvider,
|
||||||
metadataRepository,
|
metadataRepository,
|
||||||
|
mediaItemRepository,
|
||||||
imageCache,
|
imageCache,
|
||||||
ffmpegProcessService,
|
ffmpegProcessService,
|
||||||
tempFilePool,
|
tempFilePool,
|
||||||
@@ -79,9 +81,15 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
var foldersCompleted = 0;
|
var foldersCompleted = 0;
|
||||||
|
|
||||||
var folderQueue = new Queue<string>();
|
var folderQueue = new Queue<string>();
|
||||||
|
|
||||||
|
if (ShouldIncludeFolder(libraryPath.Path))
|
||||||
|
{
|
||||||
|
folderQueue.Enqueue(libraryPath.Path);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
foreach (string folder in _localFileSystem.ListSubdirectories(libraryPath.Path)
|
||||||
.Filter(ShouldIncludeFolder)
|
.Filter(ShouldIncludeFolder)
|
||||||
.OrderBy(identity))
|
.OrderBy(identity))
|
||||||
{
|
{
|
||||||
folderQueue.Enqueue(folder);
|
folderQueue.Enqueue(folder);
|
||||||
}
|
}
|
||||||
@@ -130,7 +138,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
.GetOrAdd(libraryPath, file)
|
.GetOrAdd(libraryPath, file)
|
||||||
.BindT(video => UpdateStatistics(video, ffprobePath))
|
.BindT(video => UpdateStatistics(video, ffprobePath))
|
||||||
.BindT(video => UpdateMetadata(video, ffprobePath))
|
.BindT(video => UpdateMetadata(video, ffprobePath))
|
||||||
.BindT(video => UpdateThumbnail(video, ffmpegPath));
|
.BindT(video => UpdateThumbnail(video, ffmpegPath))
|
||||||
|
.BindT(FlagNormal);
|
||||||
|
|
||||||
await maybeSong.Match(
|
await maybeSong.Match(
|
||||||
async result =>
|
async result =>
|
||||||
@@ -158,9 +167,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing missing song at {Path}", path);
|
_logger.LogInformation("Flagging missing song at {Path}", path);
|
||||||
List<int> songIds = await _songRepository.DeleteByPath(libraryPath, path);
|
List<int> songIds = await FlagFileNotFound(libraryPath, path);
|
||||||
await _searchIndex.RemoveItems(songIds);
|
await _searchIndex.RebuildItems(_searchRepository, songIds);
|
||||||
}
|
}
|
||||||
else if (Path.GetFileName(path).StartsWith("._"))
|
else if (Path.GetFileName(path).StartsWith("._"))
|
||||||
{
|
{
|
||||||
@@ -169,6 +178,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
await _searchIndex.RemoveItems(songIds);
|
await _searchIndex.RemoveItems(songIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath);
|
||||||
|
|
||||||
_searchIndex.Commit();
|
_searchIndex.Commit();
|
||||||
return Unit.Default;
|
return Unit.Default;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
ISearchIndex searchIndex,
|
ISearchIndex searchIndex,
|
||||||
ISearchRepository searchRepository,
|
ISearchRepository searchRepository,
|
||||||
ILibraryRepository libraryRepository,
|
ILibraryRepository libraryRepository,
|
||||||
|
IMediaItemRepository mediaItemRepository,
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
IFFmpegProcessService ffmpegProcessService,
|
IFFmpegProcessService ffmpegProcessService,
|
||||||
ITempFilePool tempFilePool,
|
ITempFilePool tempFilePool,
|
||||||
@@ -47,6 +48,7 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
localFileSystem,
|
localFileSystem,
|
||||||
localStatisticsProvider,
|
localStatisticsProvider,
|
||||||
metadataRepository,
|
metadataRepository,
|
||||||
|
mediaItemRepository,
|
||||||
imageCache,
|
imageCache,
|
||||||
ffmpegProcessService,
|
ffmpegProcessService,
|
||||||
tempFilePool,
|
tempFilePool,
|
||||||
@@ -126,8 +128,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
{
|
{
|
||||||
if (!_localFileSystem.FileExists(path))
|
if (!_localFileSystem.FileExists(path))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Removing missing episode at {Path}", path);
|
_logger.LogInformation("Flagging missing episode at {Path}", path);
|
||||||
await _televisionRepository.DeleteByPath(libraryPath, path);
|
List<int> episodeIds = await FlagFileNotFound(libraryPath, path);
|
||||||
|
await _searchIndex.RebuildItems(_searchRepository, episodeIds);
|
||||||
}
|
}
|
||||||
else if (Path.GetFileName(path).StartsWith("._"))
|
else if (Path.GetFileName(path).StartsWith("._"))
|
||||||
{
|
{
|
||||||
@@ -135,6 +138,8 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
await _televisionRepository.DeleteByPath(libraryPath, path);
|
await _televisionRepository.DeleteByPath(libraryPath, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _libraryRepository.CleanEtagsForLibraryPath(libraryPath);
|
||||||
|
|
||||||
await _televisionRepository.DeleteEmptySeasons(libraryPath);
|
await _televisionRepository.DeleteEmptySeasons(libraryPath);
|
||||||
List<int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath);
|
List<int> ids = await _televisionRepository.DeleteEmptyShows(libraryPath);
|
||||||
@@ -231,7 +236,9 @@ namespace ErsatzTV.Core.Metadata
|
|||||||
episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffprobePath)
|
episode => UpdateStatistics(new MediaItemScanResult<Episode>(episode), ffprobePath)
|
||||||
.MapT(_ => episode))
|
.MapT(_ => episode))
|
||||||
.BindT(UpdateMetadata)
|
.BindT(UpdateMetadata)
|
||||||
.BindT(UpdateThumbnail);
|
.BindT(UpdateThumbnail)
|
||||||
|
.BindT(e => FlagNormal(new MediaItemScanResult<Episode>(e)))
|
||||||
|
.MapT(r => r.Item);
|
||||||
|
|
||||||
await maybeEpisode.Match(
|
await maybeEpisode.Match(
|
||||||
async episode =>
|
async episode =>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using ErsatzTV.Core.Interfaces.Scheduling;
|
using ErsatzTV.Core.Interfaces.Scheduling;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
@@ -254,6 +255,7 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
foreach ((CollectionKey _, List<MediaItem> items) in collectionMediaItems)
|
foreach ((CollectionKey _, List<MediaItem> items) in collectionMediaItems)
|
||||||
{
|
{
|
||||||
var zeroItems = new List<MediaItem>();
|
var zeroItems = new List<MediaItem>();
|
||||||
|
// var missingItems = new List<MediaItem>();
|
||||||
|
|
||||||
foreach (MediaItem item in items)
|
foreach (MediaItem item in items)
|
||||||
{
|
{
|
||||||
@@ -272,6 +274,17 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// if (item.State == MediaItemState.FileNotFound)
|
||||||
|
// {
|
||||||
|
// _logger.LogWarning(
|
||||||
|
// "Skipping media item that does not exist on disk {MediaItem} - {MediaItemTitle} - {Path}",
|
||||||
|
// item.Id,
|
||||||
|
// DisplayTitle(item),
|
||||||
|
// item.GetHeadVersion().MediaFiles.Head().Path);
|
||||||
|
//
|
||||||
|
// missingItems.Add(item);
|
||||||
|
// }
|
||||||
|
// else
|
||||||
if (isZero)
|
if (isZero)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(
|
_logger.LogWarning(
|
||||||
@@ -283,7 +296,8 @@ namespace ErsatzTV.Core.Scheduling
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
items.RemoveAll(i => zeroItems.Contains(i));
|
// items.RemoveAll(missingItems.Contains);
|
||||||
|
items.RemoveAll(zeroItems.Contains);
|
||||||
}
|
}
|
||||||
|
|
||||||
return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
return collectionMediaItems.Find(c => !c.Value.Any()).Map(c => c.Key);
|
||||||
|
|||||||
@@ -123,10 +123,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<ArtistMetadata>> GetArtistsForCards(List<int> ids)
|
public async Task<List<ArtistMetadata>> GetArtistsForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.ArtistMetadata
|
return await dbContext.ArtistMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(am => ids.Contains(am.ArtistId))
|
.Filter(am => ids.Contains(am.ArtistId))
|
||||||
|
.Include(am => am.Artist)
|
||||||
.Include(am => am.Artwork)
|
.Include(am => am.Artwork)
|
||||||
.OrderBy(am => am.SortTitle)
|
.OrderBy(am => am.SortTitle)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
@@ -149,12 +150,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<MusicVideo>> GetArtistItems(int artistId)
|
public async Task<List<MusicVideo>> GetArtistItems(int artistId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.MusicVideos
|
return await dbContext.MusicVideos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(mv => mv.MusicVideoMetadata)
|
.Include(mv => mv.MusicVideoMetadata)
|
||||||
.Include(mv => mv.MediaVersions)
|
.Include(mv => mv.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(mv => mv.Artist)
|
.Include(mv => mv.Artist)
|
||||||
.ThenInclude(a => a.ArtistMetadata)
|
.ThenInclude(a => a.ArtistMetadata)
|
||||||
.Filter(mv => mv.ArtistId == artistId)
|
.Filter(mv => mv.ArtistId == artistId)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using LanguageExt;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using static LanguageExt.Prelude;
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Data.Repositories
|
namespace ErsatzTV.Infrastructure.Data.Repositories
|
||||||
{
|
{
|
||||||
public class ChannelRepository : IChannelRepository
|
public class ChannelRepository : IChannelRepository
|
||||||
@@ -42,10 +43,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<Channel>> GetAll()
|
public async Task<List<Channel>> GetAll()
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Channels
|
return await dbContext.Channels
|
||||||
.Include(c => c.FFmpegProfile)
|
.Include(c => c.FFmpegProfile)
|
||||||
.Include(c => c.Artwork)
|
.Include(c => c.Artwork)
|
||||||
|
.Include(c => c.Playouts)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Interfaces.Metadata;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -14,10 +15,15 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
public class LibraryRepository : ILibraryRepository
|
public class LibraryRepository : ILibraryRepository
|
||||||
{
|
{
|
||||||
private readonly IDbConnection _dbConnection;
|
private readonly IDbConnection _dbConnection;
|
||||||
|
private readonly ILocalFileSystem _localFileSystem;
|
||||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
public LibraryRepository(IDbContextFactory<TvContext> dbContextFactory, IDbConnection dbConnection)
|
public LibraryRepository(
|
||||||
|
ILocalFileSystem localFileSystem,
|
||||||
|
IDbContextFactory<TvContext> dbContextFactory,
|
||||||
|
IDbConnection dbConnection)
|
||||||
{
|
{
|
||||||
|
_localFileSystem = localFileSystem;
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_dbConnection = dbConnection;
|
_dbConnection = dbConnection;
|
||||||
}
|
}
|
||||||
@@ -120,7 +126,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
},
|
},
|
||||||
async () =>
|
async () =>
|
||||||
{
|
{
|
||||||
await using TvContext context = _dbContextFactory.CreateDbContext();
|
await using TvContext context = await _dbContextFactory.CreateDbContextAsync();
|
||||||
await context.LibraryFolders.AddAsync(
|
await context.LibraryFolders.AddAsync(
|
||||||
new LibraryFolder
|
new LibraryFolder
|
||||||
{
|
{
|
||||||
@@ -130,5 +136,23 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
});
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
}).ToUnit();
|
}).ToUnit();
|
||||||
|
|
||||||
|
public async Task<Unit> CleanEtagsForLibraryPath(LibraryPath libraryPath)
|
||||||
|
{
|
||||||
|
IEnumerable<string> folders = await _dbConnection.QueryAsync<string>(
|
||||||
|
@"SELECT LF.Path
|
||||||
|
FROM LibraryFolder LF
|
||||||
|
WHERE LF.LibraryPathId = @LibraryPathId",
|
||||||
|
new { LibraryPathId = libraryPath.Id });
|
||||||
|
|
||||||
|
foreach (string folder in folders.Where(f => !_localFileSystem.FolderExists(f)))
|
||||||
|
{
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
|
@"DELETE FROM LibraryFolder WHERE LibraryPathId = @LibraryPathId AND Path = @Path",
|
||||||
|
new { LibraryPathId = libraryPath.Id, Path = folder });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -371,6 +371,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.MovieMetadata)
|
.Include(m => m.MovieMetadata)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => movieIds.Contains(m.Id))
|
.Filter(m => movieIds.Contains(m.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -395,6 +397,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.MusicVideoMetadata)
|
.Include(m => m.MusicVideoMetadata)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => musicVideoIds.Contains(m.Id))
|
.Filter(m => musicVideoIds.Contains(m.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -427,6 +431,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.MusicVideoMetadata)
|
.Include(m => m.MusicVideoMetadata)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => musicVideoIds.Contains(m.Id))
|
.Filter(m => musicVideoIds.Contains(m.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -446,6 +452,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.OtherVideoMetadata)
|
.Include(m => m.OtherVideoMetadata)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => otherVideoIds.Contains(m.Id))
|
.Filter(m => otherVideoIds.Contains(m.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -465,6 +473,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.SongMetadata)
|
.Include(m => m.SongMetadata)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => songIds.Contains(m.Id))
|
.Filter(m => songIds.Contains(m.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
@@ -486,6 +496,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.Include(e => e.MediaVersions)
|
.Include(e => e.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(e => e.Season)
|
.Include(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
@@ -521,6 +533,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.Include(e => e.MediaVersions)
|
.Include(e => e.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(e => e.Season)
|
.Include(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
@@ -554,6 +568,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.Include(e => e.MediaVersions)
|
.Include(e => e.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(e => e.Season)
|
.Include(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ using System.Data;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
|
using ErsatzTV.Core;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
|
|
||||||
@@ -24,5 +26,44 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
GROUP BY LanguageCode
|
GROUP BY LanguageCode
|
||||||
ORDER BY COUNT(LanguageCode) DESC")
|
ORDER BY COUNT(LanguageCode) DESC")
|
||||||
.Map(result => result.ToList());
|
.Map(result => result.ToList());
|
||||||
|
|
||||||
|
public async Task<List<int>> FlagFileNotFound(LibraryPath libraryPath, string path)
|
||||||
|
{
|
||||||
|
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
|
@"SELECT M.Id
|
||||||
|
FROM MediaItem M
|
||||||
|
INNER JOIN MediaVersion MV on M.Id = COALESCE(MovieId, MusicVideoId, OtherVideoId, SongId, EpisodeId)
|
||||||
|
INNER JOIN MediaFile MF on MV.Id = MF.MediaVersionId
|
||||||
|
WHERE M.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||||
|
new { LibraryPathId = libraryPath.Id, Path = path })
|
||||||
|
.Map(result => result.ToList());
|
||||||
|
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
|
@"UPDATE MediaItem SET State = 1 WHERE Id IN @Ids",
|
||||||
|
new { Ids = ids });
|
||||||
|
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> FlagNormal(MediaItem mediaItem)
|
||||||
|
{
|
||||||
|
mediaItem.State = MediaItemState.Normal;
|
||||||
|
|
||||||
|
return await _dbConnection.ExecuteAsync(
|
||||||
|
@"UPDATE MediaItem SET State = 0 WHERE Id = @Id",
|
||||||
|
new { mediaItem.Id }).ToUnit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Either<BaseError, Unit>> DeleteItems(List<int> mediaItemIds)
|
||||||
|
{
|
||||||
|
foreach (int mediaItemId in mediaItemIds)
|
||||||
|
{
|
||||||
|
await _dbConnection.ExecuteAsync(
|
||||||
|
"DELETE FROM MediaItem WHERE Id = @Id",
|
||||||
|
new { Id = mediaItemId });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Unit.Default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
using ErsatzTV.Core.Interfaces.Repositories;
|
using ErsatzTV.Core.Interfaces.Repositories;
|
||||||
using LanguageExt;
|
using LanguageExt;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -110,14 +111,17 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> UpdateLocalStatistics(
|
public async Task<bool> UpdateLocalStatistics(
|
||||||
int mediaVersionId,
|
MediaItem mediaItem,
|
||||||
MediaVersion incoming,
|
MediaVersion incoming,
|
||||||
bool updateVersion = true)
|
bool updateVersion = true)
|
||||||
{
|
{
|
||||||
|
int mediaVersionId = mediaItem.GetHeadVersion().Id;
|
||||||
|
|
||||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<MediaVersion> maybeVersion = await dbContext.MediaVersions
|
Option<MediaVersion> maybeVersion = await dbContext.MediaVersions
|
||||||
.Include(v => v.Streams)
|
.Include(v => v.Streams)
|
||||||
.Include(v => v.Chapters)
|
.Include(v => v.Chapters)
|
||||||
|
.Include(v => v.MediaFiles)
|
||||||
.OrderBy(v => v.Id)
|
.OrderBy(v => v.Id)
|
||||||
.SingleOrDefaultAsync(v => v.Id == mediaVersionId)
|
.SingleOrDefaultAsync(v => v.Id == mediaVersionId)
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
@@ -191,7 +195,37 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
existingChapter.Title = incomingChapter.Title;
|
existingChapter.Title = incomingChapter.Title;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await dbContext.SaveChangesAsync() > 0;
|
if (await dbContext.SaveChangesAsync() <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload the media versions so we can properly index the duration
|
||||||
|
switch (mediaItem)
|
||||||
|
{
|
||||||
|
case Movie movie:
|
||||||
|
movie.MediaVersions.Clear();
|
||||||
|
movie.MediaVersions.Add(existing);
|
||||||
|
break;
|
||||||
|
case Episode episode:
|
||||||
|
episode.MediaVersions.Clear();
|
||||||
|
episode.MediaVersions.Add(existing);
|
||||||
|
break;
|
||||||
|
case MusicVideo musicVideo:
|
||||||
|
musicVideo.MediaVersions.Clear();
|
||||||
|
musicVideo.MediaVersions.Add(existing);
|
||||||
|
break;
|
||||||
|
case OtherVideo otherVideo:
|
||||||
|
otherVideo.MediaVersions.Clear();
|
||||||
|
otherVideo.MediaVersions.Add(existing);
|
||||||
|
break;
|
||||||
|
case Song song:
|
||||||
|
song.MediaVersions.Clear();
|
||||||
|
song.MediaVersions.Add(existing);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
() => Task.FromResult(false));
|
() => Task.FromResult(false));
|
||||||
}
|
}
|
||||||
@@ -209,51 +243,52 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public Task<Unit> UpdateArtworkPath(Artwork artwork) =>
|
public Task<Unit> UpdateArtworkPath(Artwork artwork) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
"UPDATE Artwork SET Path = @Path, DateUpdated = @DateUpdated WHERE Id = @Id",
|
"UPDATE Artwork SET Path = @Path, SourcePath = @SourcePath, DateUpdated = @DateUpdated, BlurHash43 = @BlurHash43, BlurHash54 = @BlurHash54, BlurHash64 = @BlurHash64 WHERE Id = @Id",
|
||||||
new { artwork.Path, artwork.DateUpdated, artwork.Id }).ToUnit();
|
new { artwork.Path, artwork.SourcePath, artwork.DateUpdated, artwork.BlurHash43, artwork.BlurHash54, artwork.BlurHash64, artwork.Id }).ToUnit();
|
||||||
|
|
||||||
public Task<Unit> AddArtwork(Metadata metadata, Artwork artwork)
|
public Task<Unit> AddArtwork(Metadata metadata, Artwork artwork)
|
||||||
{
|
{
|
||||||
var parameters = new
|
var parameters = new
|
||||||
{
|
{
|
||||||
artwork.ArtworkKind, metadata.Id, artwork.DateAdded, artwork.DateUpdated, artwork.Path
|
artwork.ArtworkKind, metadata.Id, artwork.DateAdded, artwork.DateUpdated, artwork.Path,
|
||||||
|
artwork.SourcePath, artwork.BlurHash43, artwork.BlurHash54, artwork.BlurHash64
|
||||||
};
|
};
|
||||||
|
|
||||||
return metadata switch
|
return metadata switch
|
||||||
{
|
{
|
||||||
MovieMetadata => _dbConnection.ExecuteAsync(
|
MovieMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, MovieMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, MovieMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
ShowMetadata => _dbConnection.ExecuteAsync(
|
ShowMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, ShowMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, ShowMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
SeasonMetadata => _dbConnection.ExecuteAsync(
|
SeasonMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, SeasonMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, SeasonMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
EpisodeMetadata => _dbConnection.ExecuteAsync(
|
EpisodeMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, EpisodeMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, EpisodeMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
ArtistMetadata => _dbConnection.ExecuteAsync(
|
ArtistMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, ArtistMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, ArtistMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
Values (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
Values (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
MusicVideoMetadata => _dbConnection.ExecuteAsync(
|
MusicVideoMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, MusicVideoMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
SongMetadata => _dbConnection.ExecuteAsync(
|
SongMetadata => _dbConnection.ExecuteAsync(
|
||||||
@"INSERT INTO Artwork (ArtworkKind, SongMetadataId, DateAdded, DateUpdated, Path)
|
@"INSERT INTO Artwork (ArtworkKind, SongMetadataId, DateAdded, DateUpdated, Path, SourcePath, BlurHash43, BlurHash54, BlurHash64)
|
||||||
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path)",
|
VALUES (@ArtworkKind, @Id, @DateAdded, @DateUpdated, @Path, @SourcePath, @BlurHash43, @BlurHash54, @BlurHash64)",
|
||||||
parameters)
|
parameters)
|
||||||
.ToUnit(),
|
.ToUnit(),
|
||||||
_ => Task.FromResult(Unit.Default)
|
_ => Task.FromResult(Unit.Default)
|
||||||
@@ -266,6 +301,52 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
OR ShowMetadataId = @Id OR SeasonMetadataId = @Id OR EpisodeMetadataId = @Id)",
|
OR ShowMetadataId = @Id OR SeasonMetadataId = @Id OR EpisodeMetadataId = @Id)",
|
||||||
new { ArtworkKind = artworkKind, metadata.Id }).ToUnit();
|
new { ArtworkKind = artworkKind, metadata.Id }).ToUnit();
|
||||||
|
|
||||||
|
public async Task<bool> CloneArtwork(
|
||||||
|
Metadata metadata,
|
||||||
|
Option<Artwork> maybeArtwork,
|
||||||
|
ArtworkKind artworkKind,
|
||||||
|
string sourcePath,
|
||||||
|
DateTime lastWriteTime)
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
Option<Artwork> maybeExisting = await dbContext.Artwork
|
||||||
|
.AsNoTracking()
|
||||||
|
.Filter(
|
||||||
|
a => a.SourcePath == sourcePath && a.ArtworkKind == artworkKind && a.DateUpdated == lastWriteTime)
|
||||||
|
.FirstOrDefaultAsync()
|
||||||
|
.Map(Optional);
|
||||||
|
foreach (Artwork existing in maybeExisting)
|
||||||
|
{
|
||||||
|
Artwork artwork = await maybeArtwork.IfNoneAsync(new Artwork());
|
||||||
|
if (maybeArtwork.IsNone)
|
||||||
|
{
|
||||||
|
metadata.Artwork.Add(artwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
artwork.Path = existing.Path;
|
||||||
|
artwork.SourcePath = existing.SourcePath;
|
||||||
|
artwork.ArtworkKind = artworkKind;
|
||||||
|
artwork.BlurHash43 = existing.BlurHash43;
|
||||||
|
artwork.BlurHash54 = existing.BlurHash54;
|
||||||
|
artwork.BlurHash64 = existing.BlurHash64;
|
||||||
|
artwork.DateAdded = existing.DateAdded;
|
||||||
|
artwork.DateUpdated = existing.DateUpdated;
|
||||||
|
|
||||||
|
if (maybeArtwork.IsNone)
|
||||||
|
{
|
||||||
|
await AddArtwork(metadata, artwork);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await UpdateArtworkPath(artwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated) =>
|
public Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
@"UPDATE ShowMetadata SET DateUpdated = @DateUpdated WHERE Id = @Id",
|
@"UPDATE ShowMetadata SET DateUpdated = @DateUpdated WHERE Id = @Id",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Writers)
|
.ThenInclude(mm => mm.Writers)
|
||||||
.Include(m => m.MediaVersions)
|
.Include(m => m.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Streams)
|
.ThenInclude(mv => mv.Streams)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.OrderBy(m => m.Id)
|
.OrderBy(m => m.Id)
|
||||||
.SingleOrDefaultAsync(m => m.Id == movieId)
|
.SingleOrDefaultAsync(m => m.Id == movieId)
|
||||||
.Map(Optional);
|
.Map(Optional);
|
||||||
@@ -63,7 +65,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(LibraryPath libraryPath, string path)
|
public async Task<Either<BaseError, MediaItemScanResult<Movie>>> GetOrAdd(LibraryPath libraryPath, string path)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<Movie> maybeExisting = await dbContext.Movies
|
Option<Movie> maybeExisting = await dbContext.Movies
|
||||||
.Include(i => i.MovieMetadata)
|
.Include(i => i.MovieMetadata)
|
||||||
.ThenInclude(mm => mm.Artwork)
|
.ThenInclude(mm => mm.Artwork)
|
||||||
@@ -146,12 +148,13 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<MovieMetadata>> GetMoviesForCards(List<int> ids)
|
public async Task<List<MovieMetadata>> GetMoviesForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.MovieMetadata
|
return await dbContext.MovieMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(mm => ids.Contains(mm.MovieId))
|
.Filter(mm => ids.Contains(mm.MovieId))
|
||||||
.Include(mm => mm.Artwork)
|
.Include(mm => mm.Artwork)
|
||||||
.OrderBy(mm => mm.SortTitle)
|
.OrderBy(mm => mm.SortTitle)
|
||||||
|
.Include(mm => mm.Movie)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +170,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
|
public async Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
List<int> ids = await _dbConnection.QueryAsync<int>(
|
List<int> ids = await _dbConnection.QueryAsync<int>(
|
||||||
@"SELECT M.Id
|
@"SELECT M.Id
|
||||||
FROM Movie M
|
FROM Movie M
|
||||||
@@ -181,13 +184,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
foreach (int movieId in ids)
|
foreach (int movieId in ids)
|
||||||
{
|
{
|
||||||
Movie movie = await dbContext.Movies.FindAsync(movieId);
|
Movie movie = await dbContext.Movies.FindAsync(movieId);
|
||||||
dbContext.Movies.Remove(movie);
|
if (movie != null)
|
||||||
|
{
|
||||||
|
dbContext.Movies.Remove(movie);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool changed = await dbContext.SaveChangesAsync() > 0;
|
bool changed = await dbContext.SaveChangesAsync() > 0;
|
||||||
return changed ? ids : new List<int>();
|
return changed ? ids : new List<int>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> AddGenre(MovieMetadata metadata, Genre genre) =>
|
public Task<bool> AddGenre(MovieMetadata metadata, Genre genre) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
"INSERT INTO Genre (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
"INSERT INTO Genre (Name, MovieMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
LibraryPath libraryPath,
|
LibraryPath libraryPath,
|
||||||
string path)
|
string path)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<MusicVideo> maybeExisting = await dbContext.MusicVideos
|
Option<MusicVideo> maybeExisting = await dbContext.MusicVideos
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.Include(mv => mv.Artist)
|
||||||
|
.ThenInclude(a => a.ArtistMetadata)
|
||||||
.Include(mv => mv.MusicVideoMetadata)
|
.Include(mv => mv.MusicVideoMetadata)
|
||||||
.ThenInclude(mvm => mvm.Artwork)
|
.ThenInclude(mvm => mvm.Artwork)
|
||||||
.Include(mv => mv.MusicVideoMetadata)
|
.Include(mv => mv.MusicVideoMetadata)
|
||||||
@@ -92,18 +94,21 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||||
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
|
new { LibraryPathId = libraryPath.Id, Path = path }).Map(result => result.ToList());
|
||||||
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
foreach (int musicVideoId in ids)
|
foreach (int musicVideoId in ids)
|
||||||
{
|
{
|
||||||
MusicVideo musicVideo = await dbContext.MusicVideos.FindAsync(musicVideoId);
|
MusicVideo musicVideo = await dbContext.MusicVideos.FindAsync(musicVideoId);
|
||||||
dbContext.MusicVideos.Remove(musicVideo);
|
if (musicVideo != null)
|
||||||
|
{
|
||||||
|
dbContext.MusicVideos.Remove(musicVideo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre) =>
|
public Task<bool> AddGenre(MusicVideoMetadata metadata, Genre genre) =>
|
||||||
_dbConnection.ExecuteAsync(
|
_dbConnection.ExecuteAsync(
|
||||||
"INSERT INTO Genre (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
|
"INSERT INTO Genre (Name, MusicVideoMetadataId) VALUES (@Name, @MetadataId)",
|
||||||
@@ -121,7 +126,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids)
|
public async Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.MusicVideoMetadata
|
return await dbContext.MusicVideoMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(mvm => ids.Contains(mvm.MusicVideoId))
|
.Filter(mvm => ids.Contains(mvm.MusicVideoId))
|
||||||
@@ -161,6 +166,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.Include(m => m.MusicVideo)
|
.Include(m => m.MusicVideo)
|
||||||
.ThenInclude(mv => mv.Artist)
|
.ThenInclude(mv => mv.Artist)
|
||||||
.ThenInclude(a => a.ArtistMetadata)
|
.ThenInclude(a => a.ArtistMetadata)
|
||||||
|
.Include(m => m.MusicVideo)
|
||||||
|
.ThenInclude(mv => mv.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Filter(m => m.MusicVideo.ArtistId == artistId)
|
.Filter(m => m.MusicVideo.ArtistId == artistId)
|
||||||
.OrderBy(m => m.SortTitle)
|
.OrderBy(m => m.SortTitle)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
@@ -196,6 +204,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
await dbContext.MusicVideos.AddAsync(musicVideo);
|
await dbContext.MusicVideos.AddAsync(musicVideo);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
await dbContext.Entry(musicVideo).Reference(m => m.Artist).LoadAsync();
|
||||||
|
await dbContext.Entry(musicVideo.Artist).Collection(a => a.ArtistMetadata).LoadAsync();
|
||||||
await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync();
|
await dbContext.Entry(musicVideo).Reference(m => m.LibraryPath).LoadAsync();
|
||||||
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
await dbContext.Entry(musicVideo.LibraryPath).Reference(lp => lp.Library).LoadAsync();
|
||||||
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
return new MediaItemScanResult<MusicVideo>(musicVideo) { IsAdded = true };
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(mm => mm.Studios)
|
.ThenInclude(mm => mm.Studios)
|
||||||
.Include(mi => (mi as Show).ShowMetadata)
|
.Include(mi => (mi as Show).ShowMetadata)
|
||||||
.ThenInclude(mm => mm.Actors)
|
.ThenInclude(mm => mm.Actors)
|
||||||
|
.Include(mi => (mi as MusicVideo).Artist)
|
||||||
|
.ThenInclude(mm => mm.ArtistMetadata)
|
||||||
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||||
.ThenInclude(mm => mm.Genres)
|
.ThenInclude(mm => mm.Genres)
|
||||||
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
.Include(mi => (mi as MusicVideo).MusicVideoMetadata)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<Show>> GetAllShows()
|
public async Task<List<Show>> GetAllShows()
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Shows
|
return await dbContext.Shows
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(s => s.ShowMetadata)
|
.Include(s => s.ShowMetadata)
|
||||||
@@ -55,7 +55,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Option<Show>> GetShow(int showId)
|
public async Task<Option<Show>> GetShow(int showId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Shows
|
return await dbContext.Shows
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(s => s.Id == showId)
|
.Filter(s => s.Id == showId)
|
||||||
@@ -77,18 +77,19 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<ShowMetadata>> GetShowsForCards(List<int> ids)
|
public async Task<List<ShowMetadata>> GetShowsForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.ShowMetadata
|
return await dbContext.ShowMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(sm => ids.Contains(sm.ShowId))
|
.Filter(sm => ids.Contains(sm.ShowId))
|
||||||
.Include(sm => sm.Artwork)
|
.Include(sm => sm.Artwork)
|
||||||
|
.Include(sm => sm.Show)
|
||||||
.OrderBy(sm => sm.SortTitle)
|
.OrderBy(sm => sm.SortTitle)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<SeasonMetadata>> GetSeasonsForCards(List<int> ids)
|
public async Task<List<SeasonMetadata>> GetSeasonsForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.SeasonMetadata
|
return await dbContext.SeasonMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(s => ids.Contains(s.SeasonId))
|
.Filter(s => ids.Contains(s.SeasonId))
|
||||||
@@ -105,7 +106,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids)
|
public async Task<List<EpisodeMetadata>> GetEpisodesForCards(List<int> ids)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.EpisodeMetadata
|
return await dbContext.EpisodeMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(em => ids.Contains(em.EpisodeId))
|
.Filter(em => ids.Contains(em.EpisodeId))
|
||||||
@@ -121,13 +122,16 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
|
.Include(em => em.Episode)
|
||||||
|
.ThenInclude(e => e.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.OrderBy(em => em.SortTitle)
|
.OrderBy(em => em.SortTitle)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<Season>> GetAllSeasons()
|
public async Task<List<Season>> GetAllSeasons()
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Seasons
|
return await dbContext.Seasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(s => s.SeasonMetadata)
|
.Include(s => s.SeasonMetadata)
|
||||||
@@ -140,7 +144,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Option<Season>> GetSeason(int seasonId)
|
public async Task<Option<Season>> GetSeason(int seasonId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Seasons
|
return await dbContext.Seasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(s => s.SeasonMetadata)
|
.Include(s => s.SeasonMetadata)
|
||||||
@@ -155,7 +159,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<int> GetSeasonCount(int showId)
|
public async Task<int> GetSeasonCount(int showId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Seasons
|
return await dbContext.Seasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.CountAsync(s => s.ShowId == showId);
|
.CountAsync(s => s.ShowId == showId);
|
||||||
@@ -171,7 +175,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
new { ShowId = televisionShowId })
|
new { ShowId = televisionShowId })
|
||||||
.Map(results => results.ToList());
|
.Map(results => results.ToList());
|
||||||
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Seasons
|
return await dbContext.Seasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(s => showIds.Contains(s.ShowId))
|
.Where(s => showIds.Contains(s.ShowId))
|
||||||
@@ -187,7 +191,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<int> GetEpisodeCount(int seasonId)
|
public async Task<int> GetEpisodeCount(int seasonId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Episodes
|
return await dbContext.Episodes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.CountAsync(e => e.SeasonId == seasonId);
|
.CountAsync(e => e.SeasonId == seasonId);
|
||||||
@@ -195,7 +199,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize)
|
public async Task<List<EpisodeMetadata>> GetPagedEpisodes(int seasonId, int pageNumber, int pageSize)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.EpisodeMetadata
|
return await dbContext.EpisodeMetadata
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Filter(em => em.Episode.SeasonId == seasonId)
|
.Filter(em => em.Episode.SeasonId == seasonId)
|
||||||
@@ -208,6 +212,9 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
.ThenInclude(sm => sm.Actors)
|
.ThenInclude(sm => sm.Actors)
|
||||||
.ThenInclude(a => a.Artwork)
|
.ThenInclude(a => a.Artwork)
|
||||||
|
.Include(em => em.Episode)
|
||||||
|
.ThenInclude(e => e.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.OrderBy(em => em.EpisodeNumber)
|
.OrderBy(em => em.EpisodeNumber)
|
||||||
.Skip((pageNumber - 1) * pageSize)
|
.Skip((pageNumber - 1) * pageSize)
|
||||||
.Take(pageSize)
|
.Take(pageSize)
|
||||||
@@ -216,7 +223,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata)
|
public async Task<Option<Show>> GetShowByMetadata(int libraryPathId, ShowMetadata metadata)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<int> maybeId = await dbContext.ShowMetadata
|
Option<int> maybeId = await dbContext.ShowMetadata
|
||||||
.Where(s => s.Title == metadata.Title && s.Year == metadata.Year)
|
.Where(s => s.Title == metadata.Title && s.Year == metadata.Year)
|
||||||
.Where(s => s.Show.LibraryPathId == libraryPathId)
|
.Where(s => s.Show.LibraryPathId == libraryPathId)
|
||||||
@@ -258,7 +265,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
string showFolder,
|
string showFolder,
|
||||||
ShowMetadata metadata)
|
ShowMetadata metadata)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -291,7 +298,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber)
|
public async Task<Either<BaseError, Season>> GetOrAddSeason(Show show, int libraryPathId, int seasonNumber)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<Season> maybeExisting = await dbContext.Seasons
|
Option<Season> maybeExisting = await dbContext.Seasons
|
||||||
.Include(s => s.SeasonMetadata)
|
.Include(s => s.SeasonMetadata)
|
||||||
.ThenInclude(sm => sm.Artwork)
|
.ThenInclude(sm => sm.Artwork)
|
||||||
@@ -315,7 +322,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
LibraryPath libraryPath,
|
LibraryPath libraryPath,
|
||||||
string path)
|
string path)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<Episode> maybeExisting = await dbContext.Episodes
|
Option<Episode> maybeExisting = await dbContext.Episodes
|
||||||
.Include(i => i.EpisodeMetadata)
|
.Include(i => i.EpisodeMetadata)
|
||||||
.ThenInclude(em => em.Artwork)
|
.ThenInclude(em => em.Artwork)
|
||||||
@@ -387,11 +394,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
WHERE MI.LibraryPathId = @LibraryPathId AND MF.Path = @Path",
|
||||||
new { LibraryPathId = libraryPath.Id, Path = path });
|
new { LibraryPathId = libraryPath.Id, Path = path });
|
||||||
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
foreach (int episodeId in ids)
|
foreach (int episodeId in ids)
|
||||||
{
|
{
|
||||||
Episode episode = await dbContext.Episodes.FindAsync(episodeId);
|
Episode episode = await dbContext.Episodes.FindAsync(episodeId);
|
||||||
dbContext.Episodes.Remove(episode);
|
if (episode != null)
|
||||||
|
{
|
||||||
|
dbContext.Episodes.Remove(episode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
@@ -401,7 +411,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath)
|
public async Task<Unit> DeleteEmptySeasons(LibraryPath libraryPath)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
List<Season> seasons = await dbContext.Seasons
|
List<Season> seasons = await dbContext.Seasons
|
||||||
.Filter(s => s.LibraryPathId == libraryPath.Id)
|
.Filter(s => s.LibraryPathId == libraryPath.Id)
|
||||||
.Filter(s => s.Episodes.Count == 0)
|
.Filter(s => s.Episodes.Count == 0)
|
||||||
@@ -413,7 +423,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath)
|
public async Task<List<int>> DeleteEmptyShows(LibraryPath libraryPath)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
List<Show> shows = await dbContext.Shows
|
List<Show> shows = await dbContext.Shows
|
||||||
.Filter(s => s.LibraryPathId == libraryPath.Id)
|
.Filter(s => s.LibraryPathId == libraryPath.Id)
|
||||||
.Filter(s => s.Seasons.Count == 0)
|
.Filter(s => s.Seasons.Count == 0)
|
||||||
@@ -428,7 +438,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
PlexLibrary library,
|
PlexLibrary library,
|
||||||
PlexShow item)
|
PlexShow item)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<PlexShow> maybeExisting = await dbContext.PlexShows
|
Option<PlexShow> maybeExisting = await dbContext.PlexShows
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.ShowMetadata)
|
.Include(i => i.ShowMetadata)
|
||||||
@@ -459,7 +469,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item)
|
public async Task<Either<BaseError, PlexSeason>> GetOrAddPlexSeason(PlexLibrary library, PlexSeason item)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
|
Option<PlexSeason> maybeExisting = await dbContext.PlexSeasons
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.SeasonMetadata)
|
.Include(i => i.SeasonMetadata)
|
||||||
@@ -480,7 +490,7 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item)
|
public async Task<Either<BaseError, PlexEpisode>> GetOrAddPlexEpisode(PlexLibrary library, PlexEpisode item)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
|
Option<PlexEpisode> maybeExisting = await dbContext.PlexEpisodes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(i => i.EpisodeMetadata)
|
.Include(i => i.EpisodeMetadata)
|
||||||
@@ -577,12 +587,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
WHERE Show.Id = @ShowId",
|
WHERE Show.Id = @ShowId",
|
||||||
new { ShowId = showId });
|
new { ShowId = showId });
|
||||||
|
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Episodes
|
return await dbContext.Episodes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.Include(e => e.MediaVersions)
|
.Include(e => e.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(e => e.Season)
|
.Include(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
@@ -592,12 +604,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories
|
|||||||
|
|
||||||
public async Task<List<Episode>> GetSeasonItems(int seasonId)
|
public async Task<List<Episode>> GetSeasonItems(int seasonId)
|
||||||
{
|
{
|
||||||
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
return await dbContext.Episodes
|
return await dbContext.Episodes
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(e => e.EpisodeMetadata)
|
.Include(e => e.EpisodeMetadata)
|
||||||
.Include(e => e.MediaVersions)
|
.Include(e => e.MediaVersions)
|
||||||
.ThenInclude(mv => mv.Chapters)
|
.ThenInclude(mv => mv.Chapters)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
.Include(e => e.Season)
|
.Include(e => e.Season)
|
||||||
.ThenInclude(s => s.Show)
|
.ThenInclude(s => s.Show)
|
||||||
.ThenInclude(s => s.ShowMetadata)
|
.ThenInclude(s => s.ShowMetadata)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace ErsatzTV.Infrastructure.Data
|
|||||||
public DbSet<MediaStream> MediaStreams { get; set; }
|
public DbSet<MediaStream> MediaStreams { get; set; }
|
||||||
public DbSet<Movie> Movies { get; set; }
|
public DbSet<Movie> Movies { get; set; }
|
||||||
public DbSet<MovieMetadata> MovieMetadata { get; set; }
|
public DbSet<MovieMetadata> MovieMetadata { get; set; }
|
||||||
|
public DbSet<Artwork> Artwork { get; set; }
|
||||||
public DbSet<Artist> Artists { get; set; }
|
public DbSet<Artist> Artists { get; set; }
|
||||||
public DbSet<ArtistMetadata> ArtistMetadata { get; set; }
|
public DbSet<ArtistMetadata> ArtistMetadata { get; set; }
|
||||||
public DbSet<MusicVideo> MusicVideos { get; set; }
|
public DbSet<MusicVideo> MusicVideos { get; set; }
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||||
<NoWarn>VSTHRD200</NoWarn>
|
<NoWarn>VSTHRD200</NoWarn>
|
||||||
<DebugType>embedded</DebugType>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blurhash.ImageSharp" Version="1.1.1" />
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00015" />
|
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00015" />
|
||||||
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00015" />
|
<PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00015" />
|
||||||
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
|
<PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00015" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ErsatzTV.Core.Health;
|
using ErsatzTV.Core.Health;
|
||||||
using LanguageExt;
|
|
||||||
using Lucene.Net.Util;
|
using Lucene.Net.Util;
|
||||||
|
using static LanguageExt.Prelude;
|
||||||
|
|
||||||
namespace ErsatzTV.Infrastructure.Health.Checks
|
namespace ErsatzTV.Infrastructure.Health.Checks
|
||||||
{
|
{
|
||||||
@@ -12,22 +12,25 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
|||||||
protected abstract string Title { get; }
|
protected abstract string Title { get; }
|
||||||
|
|
||||||
protected HealthCheckResult Result(HealthCheckStatus status, string message) =>
|
protected HealthCheckResult Result(HealthCheckStatus status, string message) =>
|
||||||
new(Title, status, message);
|
new(Title, status, message, None);
|
||||||
|
|
||||||
protected HealthCheckResult NotApplicableResult() =>
|
protected HealthCheckResult NotApplicableResult() =>
|
||||||
new(Title, HealthCheckStatus.NotApplicable, string.Empty);
|
new(Title, HealthCheckStatus.NotApplicable, string.Empty, None);
|
||||||
|
|
||||||
protected HealthCheckResult OkResult() =>
|
protected HealthCheckResult OkResult() =>
|
||||||
new(Title, HealthCheckStatus.Pass, string.Empty);
|
new(Title, HealthCheckStatus.Pass, string.Empty, None);
|
||||||
|
|
||||||
protected HealthCheckResult FailResult(string message) =>
|
protected HealthCheckResult FailResult(string message) =>
|
||||||
new(Title, HealthCheckStatus.Fail, message);
|
new(Title, HealthCheckStatus.Fail, message, None);
|
||||||
|
|
||||||
protected HealthCheckResult WarningResult(string message) =>
|
protected HealthCheckResult WarningResult(string message) =>
|
||||||
new(Title, HealthCheckStatus.Warning, message);
|
new(Title, HealthCheckStatus.Warning, message, None);
|
||||||
|
|
||||||
|
protected HealthCheckResult WarningResult(string message, string link) =>
|
||||||
|
new(Title, HealthCheckStatus.Warning, message, link);
|
||||||
|
|
||||||
protected HealthCheckResult InfoResult(string message) =>
|
protected HealthCheckResult InfoResult(string message) =>
|
||||||
new(Title, HealthCheckStatus.Info, message);
|
new(Title, HealthCheckStatus.Info, message, None);
|
||||||
|
|
||||||
protected static async Task<string> GetProcessOutput(string path, IEnumerable<string> arguments)
|
protected static async Task<string> GetProcessOutput(string path, IEnumerable<string> arguments)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
|||||||
{
|
{
|
||||||
public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck
|
public class FFmpegVersionHealthCheck : BaseHealthCheck, IFFmpegVersionHealthCheck
|
||||||
{
|
{
|
||||||
private const string BundledVersion = "N-104321-ga742ba60f1";
|
private const string BundledVersion = "N-105153-g8abc192236";
|
||||||
private readonly IConfigElementRepository _configElementRepository;
|
private readonly IConfigElementRepository _configElementRepository;
|
||||||
|
|
||||||
public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository)
|
public FFmpegVersionHealthCheck(IConfigElementRepository configElementRepository)
|
||||||
@@ -66,7 +66,7 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty);
|
return new HealthCheckResult("FFmpeg Version", HealthCheckStatus.Pass, string.Empty, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Option<HealthCheckResult> ValidateVersion(string version, string app)
|
private Option<HealthCheckResult> ValidateVersion(string version, string app)
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ErsatzTV.Core.Domain;
|
||||||
|
using ErsatzTV.Core.Extensions;
|
||||||
|
using ErsatzTV.Core.Health;
|
||||||
|
using ErsatzTV.Core.Health.Checks;
|
||||||
|
using ErsatzTV.Infrastructure.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace ErsatzTV.Infrastructure.Health.Checks;
|
||||||
|
|
||||||
|
public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public FileNotFoundHealthCheck(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
|
||||||
|
protected override string Title => "File Not Found";
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> Check()
|
||||||
|
{
|
||||||
|
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
|
List<Episode> episodes = await dbContext.Episodes
|
||||||
|
.Filter(e => e.State == MediaItemState.FileNotFound)
|
||||||
|
.Include(e => e.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<Movie> movies = await dbContext.Movies
|
||||||
|
.Filter(m => m.State == MediaItemState.FileNotFound)
|
||||||
|
.Include(m => m.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<MusicVideo> musicVideos = await dbContext.MusicVideos
|
||||||
|
.Filter(mv => mv.State == MediaItemState.FileNotFound)
|
||||||
|
.Include(mv => mv.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<OtherVideo> otherVideos = await dbContext.OtherVideos
|
||||||
|
.Filter(ov => ov.State == MediaItemState.FileNotFound)
|
||||||
|
.Include(ov => ov.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
List<Song> songs = await dbContext.Songs
|
||||||
|
.Filter(s => s.State == MediaItemState.FileNotFound)
|
||||||
|
.Include(s => s.MediaVersions)
|
||||||
|
.ThenInclude(mv => mv.MediaFiles)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
var all = movies.Map(m => m.MediaVersions.Head().MediaFiles.Head().Path)
|
||||||
|
.Append(episodes.Map(e => e.MediaVersions.Head().MediaFiles.Head().Path))
|
||||||
|
.Append(musicVideos.Map(mv => mv.GetHeadVersion().MediaFiles.Head().Path))
|
||||||
|
.Append(otherVideos.Map(ov => ov.GetHeadVersion().MediaFiles.Head().Path))
|
||||||
|
.Append(songs.Map(s => s.GetHeadVersion().MediaFiles.Head().Path))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (all.Any())
|
||||||
|
{
|
||||||
|
var paths = all.Take(5).ToList();
|
||||||
|
|
||||||
|
var files = string.Join(", ", paths);
|
||||||
|
|
||||||
|
return WarningResult(
|
||||||
|
$"There are {all.Count} files that do not exist on disk, including the following: {files}",
|
||||||
|
$"/search?query=state%3AFileNotFound");
|
||||||
|
}
|
||||||
|
|
||||||
|
return OkResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,11 +113,10 @@ namespace ErsatzTV.Infrastructure.Health.Checks
|
|||||||
result.Add(HardwareAccelerationKind.Nvenc);
|
result.Add(HardwareAccelerationKind.Nvenc);
|
||||||
break;
|
break;
|
||||||
case "qsv":
|
case "qsv":
|
||||||
// qsv is only supported on windows
|
result.Add(HardwareAccelerationKind.Qsv);
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
break;
|
||||||
{
|
case "videotoolbox":
|
||||||
result.Add(HardwareAccelerationKind.Qsv);
|
result.Add(HardwareAccelerationKind.VideoToolbox);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user