Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e515df93fd | ||
|
|
fedc18f7db | ||
|
|
59d75fe08f | ||
|
|
49d9b1c714 | ||
|
|
2f066d5b62 | ||
|
|
63db2edb99 | ||
|
|
5d01276ef3 | ||
|
|
050aaaa288 | ||
|
|
7c07c5f522 | ||
|
|
d8d21996b4 | ||
|
|
e368d4a075 | ||
|
|
466059e2aa | ||
|
|
e951ecb650 | ||
|
|
1d1f53da01 | ||
|
|
a854294cb6 | ||
|
|
f89f3d2225 | ||
|
|
a2700e087c | ||
|
|
34fbfce0a5 | ||
|
|
993293c104 | ||
|
|
ececa62446 | ||
|
|
237729e79d | ||
|
|
9c0ada2df5 | ||
|
|
dee264597b | ||
|
|
a8db294043 | ||
|
|
a2a63e0120 | ||
|
|
c7881aec14 | ||
|
|
558bdcb6b0 | ||
|
|
24f2b4b727 | ||
|
|
667887f387 | ||
|
|
98eb72fcfe | ||
|
|
c2f92fd054 | ||
|
|
f04ddd3a40 | ||
|
|
aa0942384d | ||
|
|
cd100be3a2 | ||
|
|
2b26a5411c | ||
|
|
baf81f31cd | ||
|
|
bfa290790b | ||
|
|
b975922a77 | ||
|
|
1a39978a77 | ||
|
|
436c9119fa | ||
|
|
33642a13ce | ||
|
|
09b349d1cb | ||
|
|
2be729c10e | ||
|
|
0aac702853 | ||
|
|
3f406ac556 | ||
|
|
454e2edf7c | ||
|
|
b3f4fa8c23 | ||
|
|
a6496db58d | ||
|
|
3eed79b5e1 | ||
|
|
79bfba6428 | ||
|
|
9f6d4114a6 | ||
|
|
9809c60924 | ||
|
|
16072fed1c | ||
|
|
3fb6da0754 | ||
|
|
24cdf6295f | ||
|
|
c1b41e2865 | ||
|
|
d249e95f12 | ||
|
|
efae005447 | ||
|
|
cead787c55 | ||
|
|
77a69af1a8 | ||
|
|
8fea24a3a5 | ||
|
|
6b44873474 |
@@ -79,3 +79,7 @@ indent_size=2
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
tab_width=4
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
@@ -35,7 +36,7 @@ jobs:
|
||||
name: Build & Publish to Docker Hub
|
||||
needs: build_and_test
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no ci]')
|
||||
if: github.event_name == 'push' && !contains(github.event.head_commit.message, '[no docker]')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Publish docs via GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Deploy docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout master
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Deploy docs
|
||||
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CUSTOM_DOMAIN: ersatztv.org
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace ErsatzTV.Application.Artists
|
||||
{
|
||||
public record ArtistViewModel(
|
||||
string Name,
|
||||
string Disambiguation,
|
||||
string Biography,
|
||||
string Thumbnail,
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Styles,
|
||||
List<string> Moods,
|
||||
List<CultureInfo> Languages);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Artists
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static ArtistViewModel ProjectToViewModel(Artist artist, List<string> languages)
|
||||
{
|
||||
ArtistMetadata metadata = Optional(artist.ArtistMetadata).Flatten().Head();
|
||||
return new ArtistViewModel(
|
||||
metadata.Title,
|
||||
metadata.Disambiguation,
|
||||
metadata.Biography,
|
||||
Artwork(metadata, ArtworkKind.Thumbnail),
|
||||
Artwork(metadata, ArtworkKind.FanArt),
|
||||
metadata.Genres.Map(g => g.Name).ToList(),
|
||||
metadata.Styles.Map(s => s.Name).ToList(),
|
||||
metadata.Moods.Map(m => m.Name).ToList(),
|
||||
LanguagesForArtist(languages));
|
||||
}
|
||||
|
||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
private static List<CultureInfo> LanguagesForArtist(List<string> languages)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return languages
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Sequence()
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public record GetArtistById(int ArtistId) : IRequest<Option<ArtistViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Artists.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Artists.Queries
|
||||
{
|
||||
public class GetArtistByIdHandler : IRequestHandler<GetArtistById, Option<ArtistViewModel>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
|
||||
public GetArtistByIdHandler(IArtistRepository artistRepository, ISearchRepository searchRepository)
|
||||
{
|
||||
_artistRepository = artistRepository;
|
||||
_searchRepository = searchRepository;
|
||||
}
|
||||
|
||||
public async Task<Option<ArtistViewModel>> Handle(
|
||||
GetArtistById request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<Artist> maybeArtist = await _artistRepository.GetArtist(request.ArtistId);
|
||||
return await maybeArtist.Match<Task<Option<ArtistViewModel>>>(
|
||||
async artist =>
|
||||
{
|
||||
List<string> languages = await _searchRepository.GetLanguagesForArtist(artist);
|
||||
return ProjectToViewModel(artist, languages);
|
||||
},
|
||||
() => Task.FromResult(Option<ArtistViewModel>.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Commands
|
||||
{
|
||||
public record SaveConfigElementByKey(ConfigElementKey Key, string Value) : MediatR.IRequest<Unit>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Commands
|
||||
{
|
||||
public class SaveConfigElementByKeyHandler : MediatR.IRequestHandler<SaveConfigElementByKey, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public SaveConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public async Task<Unit> Handle(SaveConfigElementByKey request, CancellationToken cancellationToken)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(request.Key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = request.Value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = request.Key.Key, Value = request.Value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace ErsatzTV.Application.Configuration
|
||||
{
|
||||
public record ConfigElementViewModel(string Key, string Value);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static ConfigElementViewModel ProjectToViewModel(ConfigElement element) =>
|
||||
new(element.Key, element.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Queries
|
||||
{
|
||||
public record GetConfigElementByKey(ConfigElementKey Key) : IRequest<Option<ConfigElementViewModel>>;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.Configuration.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Configuration.Queries
|
||||
{
|
||||
public class GetConfigElementByKeyHandler : IRequestHandler<GetConfigElementByKey, Option<ConfigElementViewModel>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetConfigElementByKeyHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<Option<ConfigElementViewModel>> Handle(
|
||||
GetConfigElementByKey request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_configElementRepository.Get(request.Key).MapT(ProjectToViewModel);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
AudioCodec = request.AudioCodec,
|
||||
AudioBitrate = request.AudioBitrate,
|
||||
AudioBufferSize = request.AudioBufferSize,
|
||||
NormalizeLoudness= request.NormalizeLoudness,
|
||||
NormalizeLoudness = request.NormalizeLoudness,
|
||||
AudioChannels = request.AudioChannels,
|
||||
AudioSampleRate = request.AudioSampleRate,
|
||||
NormalizeAudio = request.NormalizeAudio,
|
||||
|
||||
@@ -86,8 +86,10 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private Task Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
private async Task Upsert(ConfigElementKey key, string value)
|
||||
{
|
||||
Option<ConfigElement> maybeElement = await _configElementRepository.Get(key);
|
||||
await maybeElement.Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
@@ -98,5 +100,6 @@ namespace ErsatzTV.Application.FFmpegProfiles.Commands
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public record UpdateHDHRTunerCount(int TunerCount) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Commands
|
||||
{
|
||||
public class UpdateHDHRTunerCountHandler : MediatR.IRequestHandler<UpdateHDHRTunerCount, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public UpdateHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
UpdateHDHRTunerCount request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => Upsert(ConfigElementKey.HDHRTunerCount, request.TunerCount.ToString()))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private Task<Validation<BaseError, Unit>> Validate(UpdateHDHRTunerCount request) =>
|
||||
Optional(request.TunerCount)
|
||||
.Filter(tc => tc > 0)
|
||||
.Map(_ => Unit.Default)
|
||||
.ToValidation<BaseError>("Tuner count must be greater than zero")
|
||||
.AsTask();
|
||||
|
||||
private Task<Unit> Upsert(ConfigElementKey key, string value) =>
|
||||
_configElementRepository.Get(key).Match(
|
||||
ce =>
|
||||
{
|
||||
ce.Value = value;
|
||||
return _configElementRepository.Update(ce);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
var ce = new ConfigElement { Key = key.Key, Value = value };
|
||||
return _configElementRepository.Add(ce);
|
||||
}).ToUnit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public record GetHDHRTunerCount : IRequest<int>;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.HDHR.Queries
|
||||
{
|
||||
public class GetHDHRTunerCountHandler : IRequestHandler<GetHDHRTunerCount, int>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetHDHRTunerCountHandler(IConfigElementRepository configElementRepository) =>
|
||||
_configElementRepository = configElementRepository;
|
||||
|
||||
public Task<int> Handle(GetHDHRTunerCount request, CancellationToken cancellationToken) =>
|
||||
_configElementRepository.GetValue<int>(ConfigElementKey.HDHRTunerCount)
|
||||
.Map(result => result.IfNone(2));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
using System;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
public record LogEntryViewModel(
|
||||
int Id,
|
||||
DateTime Timestamp,
|
||||
string Level,
|
||||
LogEventLevel Level,
|
||||
string Exception,
|
||||
string RenderedMessage,
|
||||
string Properties);
|
||||
string Message);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
using ErsatzTV.Core.Domain;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ErsatzTV.Application.Logs
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry) =>
|
||||
new(
|
||||
internal static LogEntryViewModel ProjectToViewModel(LogEntry logEntry)
|
||||
{
|
||||
string message = logEntry.RenderedMessage;
|
||||
if (!string.IsNullOrWhiteSpace(logEntry.Properties))
|
||||
{
|
||||
foreach (KeyValuePair<string, JToken> property in JObject.Parse(logEntry.Properties))
|
||||
{
|
||||
var token = $"{{{property.Key}}}";
|
||||
if (message.Contains(token))
|
||||
{
|
||||
message = message.Replace(token, property.Value.ToString());
|
||||
}
|
||||
|
||||
var destructureToken = $"{{@{property.Key}}}";
|
||||
if (message.Contains(destructureToken))
|
||||
{
|
||||
message = message.Replace(destructureToken, property.Value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Enum.TryParse(logEntry.Level, out LogEventLevel level))
|
||||
{
|
||||
level = LogEventLevel.Debug;
|
||||
}
|
||||
|
||||
return new LogEntryViewModel(
|
||||
logEntry.Id,
|
||||
logEntry.Timestamp,
|
||||
logEntry.Level,
|
||||
level,
|
||||
logEntry.Exception,
|
||||
logEntry.RenderedMessage,
|
||||
logEntry.Properties);
|
||||
message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ActorCardViewModel(int Id, string Name, string Role, string Thumb) :
|
||||
MediaCardViewModel(Id, Name, Role, Name, Thumb);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ArtistCardResultsViewModel(
|
||||
int Count,
|
||||
List<ArtistCardViewModel> Cards,
|
||||
Option<SearchPageMap> PageMap);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record ArtistCardViewModel
|
||||
(int ArtistId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||
ArtistId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace ErsatzTV.Application.MediaCards
|
||||
List<TelevisionShowCardViewModel> ShowCards,
|
||||
List<TelevisionSeasonCardViewModel> SeasonCards,
|
||||
List<TelevisionEpisodeCardViewModel> EpisodeCards,
|
||||
List<ArtistCardViewModel> ArtistCards,
|
||||
List<MusicVideoCardViewModel> MusicVideoCards)
|
||||
{
|
||||
public bool UseCustomPlaybackOrder { get; set; }
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using static LanguageExt.Prelude;
|
||||
@@ -55,11 +54,20 @@ namespace ErsatzTV.Application.MediaCards
|
||||
internal static MusicVideoCardViewModel ProjectToViewModel(MusicVideoMetadata musicVideoMetadata) =>
|
||||
new(
|
||||
musicVideoMetadata.MusicVideoId,
|
||||
$"{musicVideoMetadata.Title} ({musicVideoMetadata.Artist})",
|
||||
musicVideoMetadata.Year?.ToString(),
|
||||
musicVideoMetadata.Title,
|
||||
musicVideoMetadata.MusicVideo.Artist.ArtistMetadata.Head().Title,
|
||||
musicVideoMetadata.SortTitle,
|
||||
musicVideoMetadata.Plot,
|
||||
GetThumbnail(musicVideoMetadata));
|
||||
|
||||
internal static ArtistCardViewModel ProjectToViewModel(ArtistMetadata artistMetadata) =>
|
||||
new(
|
||||
artistMetadata.ArtistId,
|
||||
artistMetadata.Title,
|
||||
artistMetadata.Disambiguation,
|
||||
artistMetadata.SortTitle,
|
||||
GetThumbnail(artistMetadata));
|
||||
|
||||
internal static CollectionCardResultsViewModel
|
||||
ProjectToViewModel(Collection collection) =>
|
||||
new(
|
||||
@@ -73,19 +81,18 @@ namespace ErsatzTV.Application.MediaCards
|
||||
collection.MediaItems.OfType<Season>().Map(ProjectToViewModel).ToList(),
|
||||
collection.MediaItems.OfType<Episode>().Map(e => ProjectToViewModel(e.EpisodeMetadata.Head()))
|
||||
.ToList(),
|
||||
collection.MediaItems.OfType<Artist>().Map(a => ProjectToViewModel(a.ArtistMetadata.Head())).ToList(),
|
||||
collection.MediaItems.OfType<MusicVideo>().Map(mv => ProjectToViewModel(mv.MusicVideoMetadata.Head()))
|
||||
.ToList()) { UseCustomPlaybackOrder = collection.UseCustomPlaybackOrder };
|
||||
|
||||
internal static ActorCardViewModel ProjectToViewModel(Actor actor) =>
|
||||
new(actor.Id, actor.Name, actor.Role, actor.Artwork?.Path);
|
||||
|
||||
private static int GetCustomIndex(Collection collection, int mediaItemId) =>
|
||||
Optional(collection.CollectionItems.Find(ci => ci.MediaItemId == mediaItemId))
|
||||
.Map(ci => ci.CustomIndex ?? 0)
|
||||
.IfNone(0);
|
||||
|
||||
internal static SearchCardResultsViewModel ProjectToSearchResults(List<MediaItem> items) =>
|
||||
new(
|
||||
items.OfType<Movie>().Map(m => ProjectToViewModel(m.MovieMetadata.Head())).ToList(),
|
||||
items.OfType<Show>().Map(s => ProjectToViewModel(s.ShowMetadata.Head())).ToList());
|
||||
|
||||
private static string GetSeasonName(int number) =>
|
||||
number == 0 ? "Specials" : $"Season {number}";
|
||||
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
namespace ErsatzTV.Application.MediaCards
|
||||
{
|
||||
public record MusicVideoCardViewModel
|
||||
(int MusicVideoId, string Title, string Subtitle, string SortTitle, string Poster) : MediaCardViewModel(
|
||||
MusicVideoId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster)
|
||||
(
|
||||
int MusicVideoId,
|
||||
string Title,
|
||||
string Subtitle,
|
||||
string SortTitle,
|
||||
string Plot,
|
||||
string Poster) : MediaCardViewModel(
|
||||
MusicVideoId,
|
||||
Title,
|
||||
Subtitle,
|
||||
SortTitle,
|
||||
Poster)
|
||||
{
|
||||
public int CustomIndex { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public record GetMusicVideoCards
|
||||
(int ArtistId, int PageNumber, int PageSize) : IRequest<MusicVideoCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCards.Queries
|
||||
{
|
||||
public class GetMusicVideoCardsHandler : IRequestHandler<GetMusicVideoCards, MusicVideoCardResultsViewModel>
|
||||
{
|
||||
private readonly IMusicVideoRepository _musicVideoRepository;
|
||||
|
||||
public GetMusicVideoCardsHandler(IMusicVideoRepository musicVideoRepository) =>
|
||||
_musicVideoRepository = musicVideoRepository;
|
||||
|
||||
public async Task<MusicVideoCardResultsViewModel> Handle(
|
||||
GetMusicVideoCards request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _musicVideoRepository.GetMusicVideoCount(request.ArtistId);
|
||||
|
||||
List<MusicVideoCardViewModel> results = await _musicVideoRepository
|
||||
.GetPagedMusicVideos(request.ArtistId, request.PageNumber, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new MusicVideoCardResultsViewModel(count, results, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public record AddArtistToCollection
|
||||
(int CollectionId, int ArtistId) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.Playouts.Commands;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
{
|
||||
public class
|
||||
AddArtistToCollectionHandler : MediatR.IRequestHandler<AddArtistToCollection, Either<BaseError, Unit>>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public AddArtistToCollectionHandler(
|
||||
IMediaCollectionRepository mediaCollectionRepository,
|
||||
IArtistRepository artistRepository,
|
||||
ChannelWriter<IBackgroundServiceRequest> channel)
|
||||
{
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
_artistRepository = artistRepository;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, Unit>> Handle(
|
||||
AddArtistToCollection request,
|
||||
CancellationToken cancellationToken) =>
|
||||
Validate(request)
|
||||
.MapT(_ => ApplyAddArtistRequest(request))
|
||||
.Bind(v => v.ToEitherAsync());
|
||||
|
||||
private async Task<Unit> ApplyAddArtistRequest(AddArtistToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItem(request.CollectionId, request.ArtistId))
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
.PlayoutIdsUsingCollection(request.CollectionId))
|
||||
{
|
||||
await _channel.WriteAsync(new BuildPlayout(playoutId, true));
|
||||
}
|
||||
}
|
||||
|
||||
return Unit.Default;
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Unit>> Validate(AddArtistToCollection request) =>
|
||||
(await CollectionMustExist(request), await ValidateArtist(request))
|
||||
.Apply((_, _) => Unit.Default);
|
||||
|
||||
private Task<Validation<BaseError, Unit>> CollectionMustExist(AddArtistToCollection request) =>
|
||||
_mediaCollectionRepository.GetCollectionWithItems(request.CollectionId)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Collection does not exist."));
|
||||
|
||||
private Task<Validation<BaseError, Unit>> ValidateArtist(AddArtistToCollection request) =>
|
||||
LoadArtist(request)
|
||||
.MapT(_ => Unit.Default)
|
||||
.Map(v => v.ToValidation<BaseError>("Music video does not exist"));
|
||||
|
||||
private Task<Option<Artist>> LoadArtist(AddArtistToCollection request) =>
|
||||
_artistRepository.GetArtist(request.ArtistId);
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,6 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
int CollectionId,
|
||||
List<int> MovieIds,
|
||||
List<int> ShowIds,
|
||||
List<int> ArtistIds,
|
||||
List<int> MusicVideoIds) : MediatR.IRequest<Either<BaseError, Unit>>;
|
||||
}
|
||||
|
||||
@@ -39,9 +39,13 @@ namespace ErsatzTV.Application.MediaCollections.Commands
|
||||
|
||||
private async Task<Unit> ApplyAddItemsRequest(AddItemsToCollection request)
|
||||
{
|
||||
if (await _mediaCollectionRepository.AddMediaItems(
|
||||
request.CollectionId,
|
||||
request.MovieIds.Append(request.ShowIds).Append(request.MusicVideoIds).ToList()))
|
||||
var allItems = request.MovieIds
|
||||
.Append(request.ShowIds)
|
||||
.Append(request.ArtistIds)
|
||||
.Append(request.MusicVideoIds)
|
||||
.ToList();
|
||||
|
||||
if (await _mediaCollectionRepository.AddMediaItems(request.CollectionId, allItems))
|
||||
{
|
||||
// rebuild all playouts that use this collection
|
||||
foreach (int playoutId in await _mediaCollectionRepository
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections
|
||||
{
|
||||
public record PagedMediaCollectionsViewModel(int TotalCount, List<MediaCollectionViewModel> Page);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public record GetPagedCollections(int PageNum, int PageSize) : IRequest<PagedMediaCollectionsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCollections.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.MediaCollections.Queries
|
||||
{
|
||||
public class GetPagedCollectionsHandler : IRequestHandler<GetPagedCollections, PagedMediaCollectionsViewModel>
|
||||
{
|
||||
private readonly IMediaCollectionRepository _mediaCollectionRepository;
|
||||
|
||||
public GetPagedCollectionsHandler(IMediaCollectionRepository mediaCollectionRepository) =>
|
||||
_mediaCollectionRepository = mediaCollectionRepository;
|
||||
|
||||
public async Task<PagedMediaCollectionsViewModel> Handle(
|
||||
GetPagedCollections request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
int count = await _mediaCollectionRepository.CountAllCollections();
|
||||
|
||||
List<MediaCollectionViewModel> page = await _mediaCollectionRepository
|
||||
.GetPagedCollections(request.PageNum, request.PageSize)
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new PagedMediaCollectionsViewModel(count, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public record GetAllLanguageCodes : IRequest<List<CultureInfo>>;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.MediaItems.Queries
|
||||
{
|
||||
public class GetAllLanguageCodesHandler : IRequestHandler<GetAllLanguageCodes, List<CultureInfo>>
|
||||
{
|
||||
private readonly IMediaItemRepository _mediaItemRepository;
|
||||
|
||||
public GetAllLanguageCodesHandler(IMediaItemRepository mediaItemRepository) =>
|
||||
_mediaItemRepository = mediaItemRepository;
|
||||
|
||||
public async Task<List<CultureInfo>> Handle(GetAllLanguageCodes request, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = new List<CultureInfo>();
|
||||
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
List<string> allLanguageCodes = await _mediaItemRepository.GetAllLanguageCodes();
|
||||
foreach (string code in allLanguageCodes)
|
||||
{
|
||||
Option<CultureInfo> maybeCulture = allCultures.Find(
|
||||
ci => string.Equals(code, ci.ThreeLetterISOLanguageName, StringComparison.OrdinalIgnoreCase));
|
||||
await maybeCulture.IfSomeAsync(cultureInfo => result.Add(cultureInfo));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,15 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
int LibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
bool Rescan { get; }
|
||||
}
|
||||
|
||||
public record ScanLocalLibraryIfNeeded(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => false;
|
||||
public bool Rescan => false;
|
||||
}
|
||||
|
||||
public record ForceScanLocalLibrary(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
public bool Rescan => false;
|
||||
}
|
||||
|
||||
public record ForceRescanLocalLibrary(int LibraryId) : IScanLocalLibrary
|
||||
{
|
||||
public bool ForceScan => true;
|
||||
public bool Rescan => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Locking;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -16,13 +17,13 @@ using Unit = LanguageExt.Unit;
|
||||
namespace ErsatzTV.Application.MediaSources.Commands
|
||||
{
|
||||
public class ScanLocalLibraryHandler : IRequestHandler<ForceScanLocalLibrary, Either<BaseError, string>>,
|
||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>,
|
||||
IRequestHandler<ForceRescanLocalLibrary, Either<BaseError, string>>
|
||||
IRequestHandler<ScanLocalLibraryIfNeeded, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<ScanLocalLibraryHandler> _logger;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IMovieFolderScanner _movieFolderScanner;
|
||||
private readonly IMusicVideoFolderScanner _musicVideoFolderScanner;
|
||||
private readonly ITelevisionFolderScanner _televisionFolderScanner;
|
||||
@@ -34,6 +35,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
ITelevisionFolderScanner televisionFolderScanner,
|
||||
IMusicVideoFolderScanner musicVideoFolderScanner,
|
||||
IEntityLocker entityLocker,
|
||||
IMediator mediator,
|
||||
ILogger<ScanLocalLibraryHandler> logger)
|
||||
{
|
||||
_libraryRepository = libraryRepository;
|
||||
@@ -42,13 +44,10 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
_televisionFolderScanner = televisionFolderScanner;
|
||||
_musicVideoFolderScanner = musicVideoFolderScanner;
|
||||
_entityLocker = entityLocker;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceRescanLocalLibrary request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
|
||||
public Task<Either<BaseError, string>> Handle(
|
||||
ForceScanLocalLibrary request,
|
||||
CancellationToken cancellationToken) => Handle(request);
|
||||
@@ -65,47 +64,63 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
|
||||
private async Task<Unit> PerformScan(RequestParameters parameters)
|
||||
{
|
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan, bool rescan) = parameters;
|
||||
(LocalLibrary localLibrary, string ffprobePath, bool forceScan) = parameters;
|
||||
|
||||
var lastScan = new DateTimeOffset(localLibrary.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
for (var i = 0; i < localLibrary.Paths.Count; i++)
|
||||
{
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
LibraryPath libraryPath = localLibrary.Paths[i];
|
||||
|
||||
DateTimeOffset effectiveLastScan = rescan ? DateTimeOffset.MinValue : lastScan;
|
||||
decimal progressMin = (decimal) i / localLibrary.Paths.Count;
|
||||
decimal progressMax = (decimal) (i + 1) / localLibrary.Paths.Count;
|
||||
|
||||
foreach (LibraryPath libraryPath in localLibrary.Paths)
|
||||
var lastScan = new DateTimeOffset(libraryPath.LastScan ?? DateTime.MinValue, TimeSpan.Zero);
|
||||
if (forceScan || lastScan < DateTimeOffset.Now - TimeSpan.FromHours(6))
|
||||
{
|
||||
switch (localLibrary.MediaKind)
|
||||
{
|
||||
case LibraryMediaKind.Movies:
|
||||
await _movieFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
await _movieFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
case LibraryMediaKind.Shows:
|
||||
await _televisionFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
await _televisionFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
case LibraryMediaKind.MusicVideos:
|
||||
await _musicVideoFolderScanner.ScanFolder(libraryPath, ffprobePath, effectiveLastScan);
|
||||
await _musicVideoFolderScanner.ScanFolder(
|
||||
libraryPath,
|
||||
ffprobePath,
|
||||
lastScan,
|
||||
progressMin,
|
||||
progressMax);
|
||||
break;
|
||||
}
|
||||
|
||||
libraryPath.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(libraryPath);
|
||||
}
|
||||
|
||||
localLibrary.LastScan = DateTime.UtcNow;
|
||||
await _libraryRepository.UpdateLastScan(localLibrary);
|
||||
await _mediator.Publish(new LibraryScanProgress(libraryPath.LibraryId, progressMax));
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"Scan of library {Name} completed in {Duration}",
|
||||
localLibrary.Name,
|
||||
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Skipping unforced scan of library {Name}",
|
||||
localLibrary.Name);
|
||||
}
|
||||
sw.Stop();
|
||||
_logger.LogDebug(
|
||||
"Scan of library {Name} completed in {Duration}",
|
||||
localLibrary.Name,
|
||||
TimeSpan.FromMilliseconds(sw.ElapsedMilliseconds));
|
||||
|
||||
await _mediator.Publish(new LibraryScanProgress(localLibrary.Id, 0));
|
||||
|
||||
_entityLocker.UnlockLibrary(localLibrary.Id);
|
||||
return Unit.Default;
|
||||
@@ -117,8 +132,7 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
(library, ffprobePath) => new RequestParameters(
|
||||
library,
|
||||
ffprobePath,
|
||||
request.ForceScan,
|
||||
request.Rescan));
|
||||
request.ForceScan));
|
||||
|
||||
private Task<Validation<BaseError, LocalLibrary>> LocalLibraryMustExist(
|
||||
IScanLocalLibrary request) =>
|
||||
@@ -133,6 +147,6 @@ namespace ErsatzTV.Application.MediaSources.Commands
|
||||
ffprobePath =>
|
||||
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||
|
||||
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan, bool Rescan);
|
||||
private record RequestParameters(LocalLibrary LocalLibrary, string FFprobePath, bool ForceScan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Movies
|
||||
@@ -17,7 +21,26 @@ namespace ErsatzTV.Application.Movies
|
||||
Artwork(metadata, ArtworkKind.FanArt),
|
||||
metadata.Genres.Map(g => g.Name).ToList(),
|
||||
metadata.Tags.Map(t => t.Name).ToList(),
|
||||
metadata.Studios.Map(s => s.Name).ToList());
|
||||
metadata.Studios.Map(s => s.Name).ToList(),
|
||||
LanguagesForMovie(movie),
|
||||
metadata.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id).Map(MediaCards.Mapper.ProjectToViewModel)
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static List<CultureInfo> LanguagesForMovie(Movie movie)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return movie.MediaVersions
|
||||
.Map(mv => mv.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).Map(s => s.Language))
|
||||
.Flatten()
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Sequence()
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string Artwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
|
||||
namespace ErsatzTV.Application.Movies
|
||||
{
|
||||
@@ -10,5 +12,7 @@ namespace ErsatzTV.Application.Movies
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Tags,
|
||||
List<string> Studios);
|
||||
List<string> Studios,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, Playout>> Validate(CreatePlayout request) =>
|
||||
(await ChannelMustExist(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
|
||||
(await ValidateChannel(request), await ProgramScheduleMustExist(request), ValidatePlayoutType(request))
|
||||
.Apply(
|
||||
(channel, programSchedule, playoutType) => new Playout
|
||||
{
|
||||
@@ -57,10 +57,19 @@ namespace ErsatzTV.Application.Playouts.Commands
|
||||
ProgramSchedulePlayoutType = playoutType
|
||||
});
|
||||
|
||||
private Task<Validation<BaseError, Channel>> ValidateChannel(CreatePlayout createPlayout) =>
|
||||
ChannelMustExist(createPlayout).BindT(ChannelMustNotHavePlayouts);
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustExist(CreatePlayout createPlayout) =>
|
||||
(await _channelRepository.Get(createPlayout.ChannelId))
|
||||
.ToValidation<BaseError>("Channel does not exist.");
|
||||
|
||||
private async Task<Validation<BaseError, Channel>> ChannelMustNotHavePlayouts(Channel channel) =>
|
||||
Optional(await _channelRepository.CountPlayouts(channel.Id))
|
||||
.Filter(count => count == 0)
|
||||
.Map(_ => channel)
|
||||
.ToValidation<BaseError>("Channel already has one playout.");
|
||||
|
||||
private async Task<Validation<BaseError, ProgramSchedule>> ProgramScheduleMustExist(
|
||||
CreatePlayout createPlayout) =>
|
||||
(await _programScheduleRepository.GetWithPlayouts(createPlayout.ProgramScheduleId))
|
||||
|
||||
@@ -37,7 +37,10 @@ namespace ErsatzTV.Application.Playouts
|
||||
case Movie m:
|
||||
return m.MovieMetadata.HeadOrNone().Map(mm => mm.Title).IfNone("[unknown movie]");
|
||||
case MusicVideo mv:
|
||||
return mv.MusicVideoMetadata.HeadOrNone().Map(mvm => $"{mvm.Artist} - {mvm.Title}")
|
||||
string artistName = mv.Artist.ArtistMetadata.HeadOrNone()
|
||||
.Map(am => $"{am.Title} - ").IfNone(string.Empty);
|
||||
return mv.MusicVideoMetadata.HeadOrNone()
|
||||
.Map(mvm => $"{artistName}{mvm.Title}")
|
||||
.IfNone("[unknown music video]");
|
||||
default:
|
||||
return string.Empty;
|
||||
|
||||
@@ -4,7 +4,7 @@ using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IBackgroundServiceRequest
|
||||
public interface ISynchronizePlexLibraryById : IRequest<Either<BaseError, string>>, IPlexBackgroundServiceRequest
|
||||
{
|
||||
int PlexLibraryId { get; }
|
||||
bool ForceScan { get; }
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IRequestHandler<SynchronizePlexLibraryByIdIfNeeded, Either<BaseError, string>>
|
||||
{
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILibraryRepository _libraryRepository;
|
||||
private readonly ILogger<SynchronizePlexLibraryByIdHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexMovieLibraryScanner _plexMovieLibraryScanner;
|
||||
@@ -31,6 +32,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IPlexSecretStore plexSecretStore,
|
||||
IPlexMovieLibraryScanner plexMovieLibraryScanner,
|
||||
IPlexTelevisionLibraryScanner plexTelevisionLibraryScanner,
|
||||
ILibraryRepository libraryRepository,
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexLibraryByIdHandler> logger)
|
||||
{
|
||||
@@ -38,6 +40,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
_plexSecretStore = plexSecretStore;
|
||||
_plexMovieLibraryScanner = plexMovieLibraryScanner;
|
||||
_plexTelevisionLibraryScanner = plexTelevisionLibraryScanner;
|
||||
_libraryRepository = libraryRepository;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
@@ -78,7 +81,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
}
|
||||
|
||||
parameters.Library.LastScan = DateTime.UtcNow;
|
||||
await _mediaSourceRepository.Update(parameters.Library);
|
||||
await _libraryRepository.UpdateLastScan(parameters.Library);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ using ErsatzTV.Core.Interfaces.Plex;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
@@ -19,6 +20,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
{
|
||||
private readonly ChannelWriter<IPlexBackgroundServiceRequest> _channel;
|
||||
private readonly IEntityLocker _entityLocker;
|
||||
private readonly ILogger<SynchronizePlexMediaSourcesHandler> _logger;
|
||||
private readonly IMediaSourceRepository _mediaSourceRepository;
|
||||
private readonly IPlexTvApiClient _plexTvApiClient;
|
||||
|
||||
@@ -26,12 +28,14 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
IMediaSourceRepository mediaSourceRepository,
|
||||
IPlexTvApiClient plexTvApiClient,
|
||||
ChannelWriter<IPlexBackgroundServiceRequest> channel,
|
||||
IEntityLocker entityLocker)
|
||||
IEntityLocker entityLocker,
|
||||
ILogger<SynchronizePlexMediaSourcesHandler> logger)
|
||||
{
|
||||
_mediaSourceRepository = mediaSourceRepository;
|
||||
_plexTvApiClient = plexTvApiClient;
|
||||
_channel = channel;
|
||||
_entityLocker = entityLocker;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<Either<BaseError, List<PlexMediaSource>>> Handle(
|
||||
@@ -47,6 +51,14 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
await SynchronizeServer(allExisting, server);
|
||||
}
|
||||
|
||||
// delete removed servers
|
||||
foreach (PlexMediaSource removed in allExisting.Filter(
|
||||
s => servers.All(pms => pms.ClientIdentifier != s.ClientIdentifier)))
|
||||
{
|
||||
_logger.LogWarning("Deleting removed Plex server {ServerName}!", removed.Id.ToString());
|
||||
await _mediaSourceRepository.DeletePlex(removed);
|
||||
}
|
||||
|
||||
foreach (PlexMediaSource mediaSource in await _mediaSourceRepository.GetAllPlex())
|
||||
{
|
||||
await _channel.WriteAsync(new SynchronizePlexLibraries(mediaSource.Id));
|
||||
@@ -72,7 +84,7 @@ namespace ErsatzTV.Application.Plex.Commands
|
||||
.Filter(connection => existing.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
var toRemove = existing.Connections
|
||||
.Filter(connection => server.Connections.All(c => c.Uri != connection.Uri)).ToList();
|
||||
return _mediaSourceRepository.Update(existing, toAdd, toRemove);
|
||||
return _mediaSourceRepository.Update(existing, server.Connections, toAdd, toRemove);
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
@@ -14,6 +15,7 @@ namespace ErsatzTV.Application.Search.Commands
|
||||
public class RebuildSearchIndexHandler : MediatR.IRequestHandler<RebuildSearchIndex, Unit>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
private readonly ILocalFileSystem _localFileSystem;
|
||||
private readonly ILogger<RebuildSearchIndexHandler> _logger;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
@@ -22,18 +24,22 @@ namespace ErsatzTV.Application.Search.Commands
|
||||
ISearchIndex searchIndex,
|
||||
ISearchRepository searchRepository,
|
||||
IConfigElementRepository configElementRepository,
|
||||
ILocalFileSystem localFileSystem,
|
||||
ILogger<RebuildSearchIndexHandler> logger)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_logger = logger;
|
||||
_searchRepository = searchRepository;
|
||||
_configElementRepository = configElementRepository;
|
||||
_localFileSystem = localFileSystem;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(RebuildSearchIndex request, CancellationToken cancellationToken)
|
||||
{
|
||||
bool indexFolderExists = Directory.Exists(FileSystemLayout.SearchIndexFolder);
|
||||
|
||||
await _searchIndex.Initialize(_localFileSystem);
|
||||
|
||||
if (!indexFolderExists ||
|
||||
await _configElementRepository.GetValue<int>(ConfigElementKey.SearchIndexVersion) <
|
||||
_searchIndex.Version)
|
||||
@@ -41,7 +47,7 @@ namespace ErsatzTV.Application.Search.Commands
|
||||
_logger.LogDebug("Migrating search index to version {Version}", _searchIndex.Version);
|
||||
|
||||
List<int> itemIds = await _searchRepository.GetItemIdsToIndex();
|
||||
await _searchIndex.Rebuild(itemIds);
|
||||
await _searchIndex.Rebuild(_searchRepository, itemIds);
|
||||
|
||||
Option<ConfigElement> maybeVersion =
|
||||
await _configElementRepository.Get(ConfigElementKey.SearchIndexVersion);
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
using ErsatzTV.Core.Search;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndex(string Query) : IRequest<SearchResult>;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexAllItems(string Query) : IRequest<SearchResultAllItemsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class
|
||||
QuerySearchIndexAllItemsHandler : IRequestHandler<QuerySearchIndexAllItems, SearchResultAllItemsViewModel>
|
||||
{
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexAllItemsHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
|
||||
|
||||
public async Task<SearchResultAllItemsViewModel> Handle(
|
||||
QuerySearchIndexAllItems request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
List<int> movieIds = await _searchIndex.Search($"type:movie AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> showIds = await _searchIndex.Search($"type:show AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> artistIds = await _searchIndex.Search($"type:artist AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
List<int> musicVideoIds = await _searchIndex.Search($"type:music_video AND ({request.Query})", 0, 0)
|
||||
.Map(result => result.Items.Map(i => i.Id).ToList());
|
||||
|
||||
return new SearchResultAllItemsViewModel(movieIds, showIds, artistIds, musicVideoIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public record QuerySearchIndexArtists
|
||||
(string Query, int PageNumber, int PageSize) : IRequest<ArtistCardResultsViewModel>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using static ErsatzTV.Application.MediaCards.Mapper;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class
|
||||
QuerySearchIndexArtistsHandler : IRequestHandler<QuerySearchIndexArtists, ArtistCardResultsViewModel
|
||||
>
|
||||
{
|
||||
private readonly IArtistRepository _artistRepository;
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexArtistsHandler(ISearchIndex searchIndex, IArtistRepository artistRepository)
|
||||
{
|
||||
_searchIndex = searchIndex;
|
||||
_artistRepository = artistRepository;
|
||||
}
|
||||
|
||||
public async Task<ArtistCardResultsViewModel> Handle(
|
||||
QuerySearchIndexArtists request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
SearchResult searchResult = await _searchIndex.Search(
|
||||
request.Query,
|
||||
(request.PageNumber - 1) * request.PageSize,
|
||||
request.PageSize);
|
||||
|
||||
List<ArtistCardViewModel> items = await _artistRepository
|
||||
.GetArtistsForCards(searchResult.Items.Map(i => i.Id).ToList())
|
||||
.Map(list => list.Map(ProjectToViewModel).ToList());
|
||||
|
||||
return new ArtistCardResultsViewModel(searchResult.TotalCount, items, searchResult.PageMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Interfaces.Search;
|
||||
using ErsatzTV.Core.Search;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Search.Queries
|
||||
{
|
||||
public class QuerySearchIndexHandler : IRequestHandler<QuerySearchIndex, SearchResult>
|
||||
{
|
||||
private readonly ISearchIndex _searchIndex;
|
||||
|
||||
public QuerySearchIndexHandler(ISearchIndex searchIndex) => _searchIndex = searchIndex;
|
||||
|
||||
public Task<SearchResult> Handle(QuerySearchIndex request, CancellationToken cancellationToken) =>
|
||||
_searchIndex.Search(request.Query, 0, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Application.Search
|
||||
{
|
||||
public record SearchResultAllItemsViewModel(
|
||||
List<int> MovieIds,
|
||||
List<int> ShowIds,
|
||||
List<int> ArtistIds,
|
||||
List<int> MusicVideoIds);
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Application.Television
|
||||
{
|
||||
internal static class Mapper
|
||||
{
|
||||
internal static TelevisionShowViewModel ProjectToViewModel(Show show) =>
|
||||
internal static TelevisionShowViewModel ProjectToViewModel(Show show, List<string> languages) =>
|
||||
new(
|
||||
show.Id,
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Title ?? string.Empty).IfNone(string.Empty),
|
||||
@@ -18,7 +22,13 @@ namespace ErsatzTV.Application.Television
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Genres.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Tags.Map(g => g.Name).ToList()).IfNone(new List<string>()),
|
||||
show.ShowMetadata.HeadOrNone().Map(m => m.Studios.Map(s => s.Name).ToList())
|
||||
.IfNone(new List<string>()));
|
||||
.IfNone(new List<string>()),
|
||||
LanguagesForShow(languages),
|
||||
show.ShowMetadata.HeadOrNone()
|
||||
.Map(
|
||||
m => m.Actors.OrderBy(a => a.Order).ThenBy(a => a.Id).Map(MediaCards.Mapper.ProjectToViewModel)
|
||||
.ToList())
|
||||
.IfNone(new List<ActorCardViewModel>()));
|
||||
|
||||
internal static TelevisionSeasonViewModel ProjectToViewModel(Season season) =>
|
||||
new(
|
||||
@@ -48,5 +58,19 @@ namespace ErsatzTV.Application.Television
|
||||
private static string GetArtwork(Metadata metadata, ArtworkKind artworkKind) =>
|
||||
Optional(metadata.Artwork.FirstOrDefault(a => a.ArtworkKind == artworkKind))
|
||||
.Match(a => a.Path, string.Empty);
|
||||
|
||||
private static List<CultureInfo> LanguagesForShow(List<string> languages)
|
||||
{
|
||||
CultureInfo[] allCultures = CultureInfo.GetCultures(CultureTypes.NeutralCultures);
|
||||
|
||||
return languages
|
||||
.Distinct()
|
||||
.Map(
|
||||
lang => allCultures.Filter(
|
||||
ci => string.Equals(ci.ThreeLetterISOLanguageName, lang, StringComparison.OrdinalIgnoreCase)))
|
||||
.Sequence()
|
||||
.Flatten()
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
@@ -9,15 +11,29 @@ namespace ErsatzTV.Application.Television.Queries
|
||||
{
|
||||
public class GetTelevisionShowByIdHandler : IRequestHandler<GetTelevisionShowById, Option<TelevisionShowViewModel>>
|
||||
{
|
||||
private readonly ISearchRepository _searchRepository;
|
||||
private readonly ITelevisionRepository _televisionRepository;
|
||||
|
||||
public GetTelevisionShowByIdHandler(ITelevisionRepository televisionRepository) =>
|
||||
public GetTelevisionShowByIdHandler(
|
||||
ITelevisionRepository televisionRepository,
|
||||
ISearchRepository searchRepository)
|
||||
{
|
||||
_televisionRepository = televisionRepository;
|
||||
_searchRepository = searchRepository;
|
||||
}
|
||||
|
||||
public Task<Option<TelevisionShowViewModel>> Handle(
|
||||
public async Task<Option<TelevisionShowViewModel>> Handle(
|
||||
GetTelevisionShowById request,
|
||||
CancellationToken cancellationToken) =>
|
||||
_televisionRepository.GetShow(request.Id)
|
||||
.MapT(ProjectToViewModel);
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Option<Show> maybeShow = await _televisionRepository.GetShow(request.Id);
|
||||
return await maybeShow.Match<Task<Option<TelevisionShowViewModel>>>(
|
||||
async show =>
|
||||
{
|
||||
List<string> languages = await _searchRepository.GetLanguagesForShow(show);
|
||||
return ProjectToViewModel(show, languages);
|
||||
},
|
||||
() => Task.FromResult(Option<TelevisionShowViewModel>.None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using ErsatzTV.Application.MediaCards;
|
||||
|
||||
namespace ErsatzTV.Application.Television
|
||||
{
|
||||
@@ -11,5 +13,7 @@ namespace ErsatzTV.Application.Television
|
||||
string FanArt,
|
||||
List<string> Genres,
|
||||
List<string> Tags,
|
||||
List<string> Studios);
|
||||
List<string> Studios,
|
||||
List<CultureInfo> Languages,
|
||||
List<ActorCardViewModel> Actors);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,11 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<List<Collection>> GetAll() => throw new NotSupportedException();
|
||||
public Task<int> CountAllCollections() => throw new NotSupportedException();
|
||||
|
||||
public Task<List<Collection>> GetPagedCollections(int pageNumber, int pageSize) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public Task<Option<List<MediaItem>>> GetItems(int id) => Some(_data[id].ToList()).AsTask();
|
||||
Task<bool> IMediaCollectionRepository.Update(Collection collection) => throw new NotSupportedException();
|
||||
public Task Delete(int collectionId) => throw new NotSupportedException();
|
||||
|
||||
@@ -79,6 +79,9 @@ namespace ErsatzTV.Core.Tests.Fakes
|
||||
public Task<bool> AddTag(ShowMetadata metadata, Tag tag) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> AddStudio(ShowMetadata metadata, Studio studio) => throw new NotSupportedException();
|
||||
public Task<bool> AddActor(ShowMetadata metadata, Actor actor) => throw new NotSupportedException();
|
||||
|
||||
public Task<bool> AddActor(EpisodeMetadata metadata, Actor actor) => throw new NotSupportedException();
|
||||
|
||||
public Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -14,10 +14,12 @@ using ErsatzTV.Core.Metadata;
|
||||
using ErsatzTV.Core.Tests.Fakes;
|
||||
using FluentAssertions;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using static LanguageExt.Prelude;
|
||||
using Unit = LanguageExt.Unit;
|
||||
|
||||
namespace ErsatzTV.Core.Tests.Metadata
|
||||
{
|
||||
@@ -84,7 +86,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsLeft.Should().BeTrue();
|
||||
result.IfLeft(error => error.Should().BeOfType<MediaSourceInaccessible>());
|
||||
@@ -107,7 +111,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -146,7 +152,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -186,7 +194,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -230,7 +240,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -277,7 +289,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -324,7 +338,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -370,7 +386,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -412,7 +430,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -448,7 +468,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -486,7 +508,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -513,7 +537,9 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
Either<BaseError, Unit> result = await service.ScanFolder(
|
||||
libraryPath,
|
||||
FFprobePath,
|
||||
DateTimeOffset.MinValue);
|
||||
DateTimeOffset.MinValue,
|
||||
0,
|
||||
1);
|
||||
|
||||
result.IsRight.Should().BeTrue();
|
||||
|
||||
@@ -531,6 +557,8 @@ namespace ErsatzTV.Core.Tests.Metadata
|
||||
new Mock<IMetadataRepository>().Object,
|
||||
_imageCache.Object,
|
||||
new Mock<ISearchIndex>().Object,
|
||||
new Mock<ISearchRepository>().Object,
|
||||
new Mock<IMediator>().Object,
|
||||
new Mock<ILogger<MovieFolderScanner>>().Object
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,5 +13,7 @@
|
||||
public static ConfigElementKey FFmpegSaveReports => new("ffmpeg.save_reports");
|
||||
public static ConfigElementKey FFmpegPreferredLanguageCode => new("ffmpeg.preferred_language_code");
|
||||
public static ConfigElementKey SearchIndexVersion => new("search_index.version");
|
||||
public static ConfigElementKey HDHRTunerCount => new("hdhr.tuner_count");
|
||||
public static ConfigElementKey CollectionsPageSize => new("pages.collections.page_size");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
@@ -6,6 +7,7 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Path { get; set; }
|
||||
public DateTime? LastScan { get; set; }
|
||||
|
||||
public int LibraryId { get; set; }
|
||||
public Library Library { get; set; }
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class Artist : MediaItem
|
||||
{
|
||||
public List<MusicVideo> MusicVideos { get; set; }
|
||||
public List<ArtistMetadata> ArtistMetadata { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class MusicVideo : MediaItem
|
||||
{
|
||||
public int ArtistId { get; set; }
|
||||
public Artist Artist { get; set; }
|
||||
public List<MusicVideoMetadata> MusicVideoMetadata { get; set; }
|
||||
public List<MediaVersion> MediaVersions { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class Actor
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Role { get; set; }
|
||||
public int? Order { get; set; }
|
||||
public int? ArtworkId { get; set; }
|
||||
public Artwork Artwork { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class ArtistMetadata : Metadata
|
||||
{
|
||||
public string Disambiguation { get; set; }
|
||||
public string Biography { get; set; }
|
||||
public string Formed { get; set; }
|
||||
public int ArtistId { get; set; }
|
||||
public Artist Artist { get; set; }
|
||||
public List<Style> Styles { get; set; }
|
||||
public List<Mood> Moods { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,6 @@ namespace ErsatzTV.Core.Domain
|
||||
public List<Genre> Genres { get; set; }
|
||||
public List<Tag> Tags { get; set; }
|
||||
public List<Studio> Studios { get; set; }
|
||||
public List<Actor> Actors { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class Mood
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
{
|
||||
public string Album { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public int MusicVideoId { get; set; }
|
||||
public MusicVideo MusicVideo { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Domain
|
||||
{
|
||||
public class Style
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" />
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="16.9.60">
|
||||
|
||||
@@ -4,9 +4,6 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
public static class DisplaySizeExtensions
|
||||
{
|
||||
internal static IDisplaySize PadToEven(this IDisplaySize size) =>
|
||||
new DisplaySize(size.Width + size.Width % 2, size.Height + size.Height % 2);
|
||||
|
||||
internal static bool IsSameSizeAs(this IDisplaySize @this, IDisplaySize that) =>
|
||||
@this.Width == that.Width && @this.Height == that.Height;
|
||||
}
|
||||
|
||||
@@ -68,12 +68,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, int audioStreamIndex)
|
||||
public Option<FFmpegComplexFilter> Build(int videoStreamIndex, Option<int> audioStreamIndex)
|
||||
{
|
||||
var complexFilter = new StringBuilder();
|
||||
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
string audioLabel = audioStreamIndex.Match(index => $"0:{index}", () => "0:a");
|
||||
|
||||
HardwareAccelerationKind acceleration = _hardwareAccelerationKind.IfNone(HardwareAccelerationKind.None);
|
||||
bool isHardwareDecode = acceleration switch
|
||||
|
||||
@@ -22,6 +22,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using LanguageExt;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
@@ -46,7 +47,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
FFmpegProfile ffmpegProfile,
|
||||
MediaVersion version,
|
||||
MediaStream videoStream,
|
||||
MediaStream audioStream,
|
||||
Option<MediaStream> audioStream,
|
||||
DateTimeOffset start,
|
||||
DateTimeOffset now)
|
||||
{
|
||||
@@ -113,10 +114,14 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
result.AudioBitrate = ffmpegProfile.AudioBitrate;
|
||||
result.AudioBufferSize = ffmpegProfile.AudioBufferSize;
|
||||
|
||||
if (audioStream.Channels != ffmpegProfile.AudioChannels)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
}
|
||||
audioStream.IfSome(
|
||||
stream =>
|
||||
{
|
||||
if (stream.Channels != ffmpegProfile.AudioChannels)
|
||||
{
|
||||
result.AudioChannels = ffmpegProfile.AudioChannels;
|
||||
}
|
||||
});
|
||||
|
||||
result.AudioSampleRate = ffmpegProfile.AudioSampleRate;
|
||||
result.AudioDuration = version.Duration;
|
||||
|
||||
@@ -199,6 +199,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"-c", "copy",
|
||||
"-muxdelay", "0",
|
||||
"-muxpreload", "0"
|
||||
// "-avoid_negative_ts", "make_zero"
|
||||
};
|
||||
_arguments.AddRange(arguments);
|
||||
return this;
|
||||
@@ -353,12 +354,15 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithFilterComplex(int videoStreamIndex, int audioStreamIndex)
|
||||
public FFmpegProcessBuilder WithFilterComplex(MediaStream videoStream, Option<MediaStream> maybeAudioStream)
|
||||
{
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{audioStreamIndex}";
|
||||
int videoStreamIndex = videoStream.Index;
|
||||
Option<int> maybeIndex = maybeAudioStream.Map(ms => ms.Index);
|
||||
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, audioStreamIndex);
|
||||
var videoLabel = $"0:{videoStreamIndex}";
|
||||
var audioLabel = $"0:{maybeIndex.Match(i => i.ToString(), () => "a")}";
|
||||
|
||||
Option<FFmpegComplexFilter> maybeFilter = _complexFilterBuilder.Build(videoStreamIndex, maybeIndex);
|
||||
maybeFilter.IfSome(
|
||||
filter =>
|
||||
{
|
||||
|
||||
@@ -30,14 +30,14 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
DateTimeOffset now)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, version);
|
||||
MediaStream audioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
|
||||
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, version);
|
||||
|
||||
FFmpegPlaybackSettings playbackSettings = _playbackSettingsCalculator.CalculateSettings(
|
||||
channel.StreamingMode,
|
||||
channel.FFmpegProfile,
|
||||
version,
|
||||
videoStream,
|
||||
audioStream,
|
||||
maybeAudioStream,
|
||||
start,
|
||||
now);
|
||||
|
||||
@@ -60,14 +60,13 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithScaling(scaledSize);
|
||||
|
||||
scaledSize = scaledSize.PadToEven();
|
||||
if (NeedToPad(channel.FFmpegProfile.Resolution, scaledSize))
|
||||
{
|
||||
builder = builder.WithBlackBars(channel.FFmpegProfile.Resolution);
|
||||
}
|
||||
|
||||
builder = builder
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
.WithFilterComplex(videoStream, maybeAudioStream);
|
||||
},
|
||||
() =>
|
||||
{
|
||||
@@ -76,18 +75,18 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
builder = builder
|
||||
.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithBlackBars(channel.FFmpegProfile.Resolution)
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
.WithFilterComplex(videoStream, maybeAudioStream);
|
||||
}
|
||||
else if (playbackSettings.Deinterlace)
|
||||
{
|
||||
builder = builder.WithDeinterlace(playbackSettings.Deinterlace)
|
||||
.WithAlignedAudio(playbackSettings.AudioDuration)
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
.WithFilterComplex(videoStream, maybeAudioStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = builder
|
||||
.WithFilterComplex(videoStream.Index, audioStream.Index);
|
||||
.WithFilterComplex(videoStream, maybeAudioStream);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using ErsatzTV.Core.Interfaces.FFmpeg;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static LanguageExt.Prelude;
|
||||
|
||||
namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
@@ -25,8 +26,17 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
public Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version) =>
|
||||
version.Streams.First(s => s.MediaStreamKind == MediaStreamKind.Video).AsTask();
|
||||
|
||||
public async Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version)
|
||||
public async Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version)
|
||||
{
|
||||
if (channel.StreamingMode == StreamingMode.HttpLiveStreaming &&
|
||||
string.IsNullOrWhiteSpace(channel.PreferredLanguageCode))
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"Channel {Number} is HLS with no preferred language; using all audio streams",
|
||||
channel.Number);
|
||||
return None;
|
||||
}
|
||||
|
||||
var audioStreams = version.Streams.Filter(s => s.MediaStreamKind == MediaStreamKind.Audio).ToList();
|
||||
|
||||
string language = (channel.PreferredLanguageCode ?? string.Empty).ToLowerInvariant();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
{
|
||||
public interface IFFmpegStreamSelector
|
||||
{
|
||||
Task<MediaStream> SelectVideoStream(Channel channel, MediaVersion version);
|
||||
Task<MediaStream> SelectAudioStream(Channel channel, MediaVersion version);
|
||||
Task<Option<MediaStream>> SelectAudioStream(Channel channel, MediaVersion version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.GitHub
|
||||
{
|
||||
public interface IGitHubApiClient
|
||||
{
|
||||
Task<Either<BaseError, string>> GetLatestReleaseNotes();
|
||||
Task<Either<BaseError, string>> GetReleaseNotes(string tag);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
using System;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IFallbackMetadataProvider
|
||||
{
|
||||
ShowMetadata GetFallbackMetadataForShow(string showFolder);
|
||||
ArtistMetadata GetFallbackMetadataForArtist(string artistFolder);
|
||||
Tuple<EpisodeMetadata, int> GetFallbackMetadata(Episode episode);
|
||||
MovieMetadata GetFallbackMetadata(Movie movie);
|
||||
Option<MusicVideoMetadata> GetFallbackMetadata(MusicVideo musicVideo);
|
||||
string GetSortTitle(string title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ILocalMetadataProvider
|
||||
{
|
||||
Task<ShowMetadata> GetMetadataForShow(string showFolder);
|
||||
Task<Option<MusicVideoMetadata>> GetMetadataForMusicVideo(string filePath);
|
||||
Task<ArtistMetadata> GetMetadataForArtist(string artistFolder);
|
||||
Task<bool> RefreshSidecarMetadata(Movie movie, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Show televisionShow, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Episode episode, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(Artist artist, string nfoFileName);
|
||||
Task<bool> RefreshSidecarMetadata(MusicVideo musicVideo, string nfoFileName);
|
||||
Task<bool> RefreshFallbackMetadata(Movie movie);
|
||||
Task<bool> RefreshFallbackMetadata(Episode episode);
|
||||
Task<bool> RefreshFallbackMetadata(Artist artist, string artistFolder);
|
||||
Task<bool> RefreshFallbackMetadata(MusicVideo musicVideo);
|
||||
Task<bool> RefreshFallbackMetadata(Show televisionShow, string showFolder);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IMovieFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
DateTimeOffset lastScan,
|
||||
decimal progressMin,
|
||||
decimal progressMax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface IMusicVideoFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
DateTimeOffset lastScan,
|
||||
decimal progressMin,
|
||||
decimal progressMax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace ErsatzTV.Core.Interfaces.Metadata
|
||||
{
|
||||
public interface ITelevisionFolderScanner
|
||||
{
|
||||
Task<Either<BaseError, Unit>> ScanFolder(LibraryPath libraryPath, string ffprobePath, DateTimeOffset lastScan);
|
||||
Task<Either<BaseError, Unit>> ScanFolder(
|
||||
LibraryPath libraryPath,
|
||||
string ffprobePath,
|
||||
DateTimeOffset lastScan,
|
||||
decimal progressMin,
|
||||
decimal progressMax);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary plexMediaSourceLibrary);
|
||||
PlexLibrary library);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,18 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, MovieMetadata>> GetMovieMetadata(
|
||||
PlexLibrary library,
|
||||
string key,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, ShowMetadata>> GetShowMetadata(
|
||||
PlexLibrary library,
|
||||
string key,
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token);
|
||||
|
||||
Task<Either<BaseError, MediaVersion>> GetStatistics(
|
||||
string key,
|
||||
PlexConnection connection,
|
||||
|
||||
@@ -10,6 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Plex
|
||||
Task<Either<BaseError, Unit>> ScanLibrary(
|
||||
PlexConnection connection,
|
||||
PlexServerAuthToken token,
|
||||
PlexLibrary plexMediaSourceLibrary);
|
||||
PlexLibrary library);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Metadata;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IArtistRepository
|
||||
{
|
||||
Task<Option<Artist>> GetArtistByMetadata(int libraryPathId, ArtistMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<Artist>>> AddArtist(
|
||||
int libraryPathId,
|
||||
string artistFolder,
|
||||
ArtistMetadata metadata);
|
||||
|
||||
Task<List<int>> DeleteEmptyArtists(LibraryPath libraryPath);
|
||||
Task<Option<Artist>> GetArtist(int artistId);
|
||||
Task<List<ArtistMetadata>> GetArtistsForCards(List<int> ids);
|
||||
Task<bool> AddGenre(ArtistMetadata metadata, Genre genre);
|
||||
Task<bool> AddStyle(ArtistMetadata metadata, Style style);
|
||||
Task<bool> AddMood(ArtistMetadata metadata, Mood mood);
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<List<Channel>> GetAllForGuide();
|
||||
Task Update(Channel channel);
|
||||
Task Delete(int channelId);
|
||||
Task<int> CountPlayouts(int channelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<Option<LocalLibrary>> GetLocal(int libraryId);
|
||||
Task<List<Library>> GetAll();
|
||||
Task<Unit> UpdateLastScan(Library library);
|
||||
Task<Unit> UpdateLastScan(LibraryPath libraryPath);
|
||||
Task<List<LibraryPath>> GetLocalPaths(int libraryId);
|
||||
Task<Option<LibraryPath>> GetPath(int libraryPathId);
|
||||
Task<int> CountMediaItemsByPath(int libraryPathId);
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<Option<Collection>> GetCollectionWithItemsUntracked(int id);
|
||||
Task<Option<Collection>> GetCollectionWithCollectionItemsUntracked(int id);
|
||||
Task<List<Collection>> GetAll();
|
||||
Task<int> CountAllCollections();
|
||||
Task<List<Collection>> GetPagedCollections(int pageNumber, int pageSize);
|
||||
Task<Option<List<MediaItem>>> GetItems(int id);
|
||||
Task<bool> Update(Collection collection);
|
||||
Task Delete(int collectionId);
|
||||
|
||||
@@ -10,5 +10,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<Option<MediaItem>> Get(int id);
|
||||
Task<List<MediaItem>> GetAll();
|
||||
Task<bool> Update(MediaItem mediaItem);
|
||||
Task<List<string>> GetAllLanguageCodes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,12 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<List<PlexPathReplacement>> GetPlexPathReplacementsByLibraryId(int plexLibraryPathId);
|
||||
Task<int> CountMediaItems(int id);
|
||||
Task Update(LocalMediaSource localMediaSource);
|
||||
Task Update(PlexMediaSource plexMediaSource, List<PlexConnection> toAdd, List<PlexConnection> toDelete);
|
||||
|
||||
Task Update(
|
||||
PlexMediaSource plexMediaSource,
|
||||
List<PlexConnection> prioritizedConnections,
|
||||
List<PlexConnection> toAdd,
|
||||
List<PlexConnection> toDelete);
|
||||
|
||||
Task<Unit> UpdateLibraries(
|
||||
int plexMediaSourceId,
|
||||
@@ -36,6 +41,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task Update(PlexLibrary plexMediaSourceLibrary);
|
||||
Task Delete(int mediaSourceId);
|
||||
Task<List<int>> DeleteAllPlex();
|
||||
Task<List<int>> DeletePlex(PlexMediaSource plexMediaSource);
|
||||
Task<List<int>> DisablePlexLibrarySync(List<int> libraryIds);
|
||||
Task EnablePlexLibrarySync(IEnumerable<int> libraryIds);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<bool> RemoveGenre(Genre genre);
|
||||
Task<bool> RemoveTag(Tag tag);
|
||||
Task<bool> RemoveStudio(Studio studio);
|
||||
Task<bool> RemoveStyle(Style style);
|
||||
Task<bool> RemoveMood(Mood mood);
|
||||
Task<bool> RemoveActor(Actor actor);
|
||||
Task<bool> Update(Domain.Metadata metadata);
|
||||
Task<bool> Add(Domain.Metadata metadata);
|
||||
Task<bool> UpdateLocalStatistics(int mediaVersionId, MediaVersion incoming, bool updateVersion = true);
|
||||
@@ -20,5 +23,6 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<Unit> MarkAsUpdated(ShowMetadata metadata, DateTime dateUpdated);
|
||||
Task<Unit> MarkAsUpdated(SeasonMetadata metadata, DateTime dateUpdated);
|
||||
Task<Unit> MarkAsUpdated(MovieMetadata metadata, DateTime dateUpdated);
|
||||
Task<Unit> MarkAsUpdated(EpisodeMetadata metadata, DateTime dateUpdated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<bool> AddGenre(MovieMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(MovieMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(MovieMetadata metadata, Studio studio);
|
||||
Task<bool> AddActor(MovieMetadata metadata, Actor actor);
|
||||
Task<List<int>> RemoveMissingPlexMovies(PlexLibrary library, List<string> movieKeys);
|
||||
Task<bool> UpdateSortTitle(MovieMetadata movieMetadata);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,10 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public interface IMusicVideoRepository
|
||||
{
|
||||
Task<Option<MusicVideo>> GetByMetadata(LibraryPath libraryPath, MusicVideoMetadata metadata);
|
||||
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> Add(
|
||||
Task<Either<BaseError, MediaItemScanResult<MusicVideo>>> GetOrAdd(
|
||||
Artist artist,
|
||||
LibraryPath libraryPath,
|
||||
string filePath,
|
||||
MusicVideoMetadata metadata);
|
||||
string path);
|
||||
|
||||
Task<IEnumerable<string>> FindMusicVideoPaths(LibraryPath libraryPath);
|
||||
Task<List<int>> DeleteByPath(LibraryPath libraryPath, string path);
|
||||
@@ -22,5 +20,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<bool> AddStudio(MusicVideoMetadata metadata, Studio studio);
|
||||
Task<List<MusicVideoMetadata>> GetMusicVideosForCards(List<int> ids);
|
||||
Task<Option<MusicVideo>> GetMusicVideo(int musicVideoId);
|
||||
Task<IEnumerable<string>> FindOrphanPaths(LibraryPath libraryPath);
|
||||
Task<int> GetMusicVideoCount(int artistId);
|
||||
Task<List<MusicVideoMetadata>> GetPagedMusicVideos(int artistId, int pageNumber, int pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
{
|
||||
public Task<List<int>> GetItemIdsToIndex();
|
||||
public Task<Option<MediaItem>> GetItemToIndex(int id);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByTitle(string query);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByGenre(string genre);
|
||||
public Task<List<MediaItem>> SearchMediaItemsByTag(string tag);
|
||||
public Task<List<string>> GetLanguagesForShow(Show show);
|
||||
public Task<List<string>> GetLanguagesForArtist(Artist artist);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace ErsatzTV.Core.Interfaces.Repositories
|
||||
Task<bool> AddGenre(ShowMetadata metadata, Genre genre);
|
||||
Task<bool> AddTag(ShowMetadata metadata, Tag tag);
|
||||
Task<bool> AddStudio(ShowMetadata metadata, Studio studio);
|
||||
Task<bool> AddActor(ShowMetadata metadata, Actor actor);
|
||||
Task<bool> AddActor(EpisodeMetadata metadata, Actor actor);
|
||||
Task<List<int>> RemoveMissingPlexShows(PlexLibrary library, List<string> showKeys);
|
||||
Task<Unit> RemoveMissingPlexSeasons(string showKey, List<string> seasonKeys);
|
||||
Task<Unit> RemoveMissingPlexEpisodes(string seasonKey, List<string> episodeKeys);
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Metadata;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using ErsatzTV.Core.Search;
|
||||
using LanguageExt;
|
||||
|
||||
namespace ErsatzTV.Core.Interfaces.Search
|
||||
{
|
||||
public interface ISearchIndex
|
||||
public interface ISearchIndex : IDisposable
|
||||
{
|
||||
public int Version { get; }
|
||||
Task<bool> Initialize();
|
||||
Task<Unit> Rebuild(List<int> itemIds);
|
||||
Task<Unit> AddItems(List<MediaItem> items);
|
||||
Task<Unit> UpdateItems(List<MediaItem> items);
|
||||
Task<bool> Initialize(ILocalFileSystem localFileSystem);
|
||||
Task<Unit> Rebuild(ISearchRepository searchRepository, List<int> itemIds);
|
||||
Task<Unit> AddItems(ISearchRepository searchRepository, List<MediaItem> items);
|
||||
Task<Unit> UpdateItems(ISearchRepository searchRepository, List<MediaItem> items);
|
||||
Task<Unit> RemoveItems(List<int> ids);
|
||||
Task<SearchResult> Search(string query, int skip, int limit, string searchField = "");
|
||||
void Commit();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user