diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce942bed..ea6ed0298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Add ability to sync Trakt lists to a collection + - This sync is one-time (manual button) and one-way (Trakt => ErsatzTV) + - After synchronization, the collection will *only* contain media items found in the Trakt list + ### Fixed - Fix local television scanner to properly update episode metadata when NFO files have been added/changed - Properly detect ffmpeg nvenc (cuda) support in Hardware Acceleration health check diff --git a/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktList.cs b/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktList.cs new file mode 100644 index 000000000..59cb72ab2 --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktList.cs @@ -0,0 +1,10 @@ +using ErsatzTV.Core; +using LanguageExt; +using MediatR; +using Unit = LanguageExt.Unit; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public record SyncCollectionFromTraktList + (int CollectionId, string TraktListUrl) : IRequest>; +} diff --git a/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktListHandler.cs b/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktListHandler.cs new file mode 100644 index 000000000..b8023ef2b --- /dev/null +++ b/ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktListHandler.cs @@ -0,0 +1,269 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +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 ErsatzTV.Core.Interfaces.Trakt; +using ErsatzTV.Core.Trakt; +using ErsatzTV.Infrastructure.Data; +using ErsatzTV.Infrastructure.Extensions; +using LanguageExt; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using static LanguageExt.Prelude; +using Unit = LanguageExt.Unit; + +namespace ErsatzTV.Application.MediaCollections.Commands +{ + public class + SyncCollectionFromTraktListHandler : IRequestHandler> + { + private readonly ITraktApiClient _traktApiClient; + private readonly IDbContextFactory _dbContextFactory; + private readonly IMediaCollectionRepository _mediaCollectionRepository; + private readonly ChannelWriter _channel; + private readonly ILogger _logger; + + public SyncCollectionFromTraktListHandler( + ITraktApiClient traktApiClient, + IDbContextFactory dbContextFactory, + IMediaCollectionRepository mediaCollectionRepository, + ChannelWriter channel, + ILogger logger) + { + _traktApiClient = traktApiClient; + _dbContextFactory = dbContextFactory; + _mediaCollectionRepository = mediaCollectionRepository; + _channel = channel; + _logger = logger; + } + + public async Task> Handle( + SyncCollectionFromTraktList request, + CancellationToken cancellationToken) + { + // validate and parse user/list from URL + const string PATTERN = @"users\/([\w\-_]+)\/lists\/([\w\-_]+)"; + Match match = Regex.Match(request.TraktListUrl, PATTERN); + if (match.Success) + { + string user = match.Groups[1].Value; + string list = match.Groups[2].Value; + + Either> maybeItems = + await _traktApiClient.GetUserListItems(user, list); + return await maybeItems.Match( + async items => await SyncCollectionFromItems(request.CollectionId, items), + error => Task.FromResult(Left(error))); + } + + return BaseError.New("Invalid Trakt List URL"); + } + + private async Task> SyncCollectionFromItems(int collectionId, List items) + { + await using TvContext dbContext = _dbContextFactory.CreateDbContext(); + + Option maybeCollection = await dbContext.Collections + .Include(c => c.MediaItems) + .SelectOneAsync(c => c.Id, c => c.Id == collectionId); + + foreach (Collection collection in maybeCollection) + { + var movieIds = new System.Collections.Generic.HashSet(); + foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Movie)) + { + foreach (int movieId in await IdentifyMovie(dbContext, item)) + { + movieIds.Add(movieId); + } + } + + var showIds = new System.Collections.Generic.HashSet(); + foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Show)) + { + foreach (int showId in await IdentifyShow(dbContext, item)) + { + showIds.Add(showId); + } + } + + var seasonIds = new System.Collections.Generic.HashSet(); + foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Season)) + { + foreach (int seasonId in await IdentifySeason(dbContext, item)) + { + seasonIds.Add(seasonId); + } + } + + var episodeIds = new System.Collections.Generic.HashSet(); + foreach (TraktListItemWithGuids item in items.Filter(i => i.Kind == TraktListItemKind.Episode)) + { + foreach (int episodeId in await IdentifyEpisode(dbContext, item)) + { + episodeIds.Add(episodeId); + } + } + + var allIds = movieIds + .Append(showIds) + .Append(seasonIds) + .Append(episodeIds) + .ToList(); + + collection.MediaItems.RemoveAll(mi => !allIds.Contains(mi.Id)); + + List toAdd = await dbContext.MediaItems + .Filter(mi => allIds.Contains(mi.Id)) + .ToListAsync(); + + collection.MediaItems.AddRange(toAdd); + + if (await dbContext.SaveChangesAsync() > 0) + { + // rebuild all playouts that use this collection + foreach (int playoutId in await _mediaCollectionRepository.PlayoutIdsUsingCollection(collectionId)) + { + await _channel.WriteAsync(new BuildPlayout(playoutId, true)); + } + } + } + + return Unit.Default; + } + + private async Task> IdentifyMovie(TvContext dbContext, TraktListItemWithGuids item) + { + Option maybeMovieByGuid = await dbContext.MovieMetadata + .Filter(mm => mm.Guids.Any(g => item.Guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(mm => mm.MovieId); + + foreach (int movieId in maybeMovieByGuid) + { + _logger.LogDebug("Located trakt movie {Title} by id", item.DisplayTitle); + return movieId; + } + + Option maybeMovieByTitleYear = await dbContext.MovieMetadata + .Filter(mm => mm.Title == item.Title && mm.Year == item.Year) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(mm => mm.MovieId); + + foreach (int movieId in maybeMovieByTitleYear) + { + _logger.LogDebug("Located trakt movie {Title} by title/year", item.DisplayTitle); + return movieId; + } + + _logger.LogDebug("Unable to locate trakt movie {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifyShow(TvContext dbContext, TraktListItemWithGuids item) + { + Option maybeShowByGuid = await dbContext.ShowMetadata + .Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.ShowId); + + foreach (int showId in maybeShowByGuid) + { + _logger.LogDebug("Located trakt show {Title} by id", item.DisplayTitle); + return showId; + } + + Option maybeShowByTitleYear = await dbContext.ShowMetadata + .Filter(sm => sm.Title == item.Title && sm.Year == item.Year) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.ShowId); + + foreach (int showId in maybeShowByTitleYear) + { + _logger.LogDebug("Located trakt show {Title} by title/year", item.Title); + return showId; + } + + _logger.LogDebug("Unable to locate trakt show {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifySeason(TvContext dbContext, TraktListItemWithGuids item) + { + Option maybeSeasonByGuid = await dbContext.SeasonMetadata + .Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.SeasonId); + + foreach (int seasonId in maybeSeasonByGuid) + { + _logger.LogDebug("Located trakt season {Title} by id", item.DisplayTitle); + return seasonId; + } + + Option maybeSeasonByTitleYear = await dbContext.SeasonMetadata + .Filter(sm => sm.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) + .Filter(sm => sm.Season.SeasonNumber == item.Season) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.SeasonId); + + foreach (int seasonId in maybeSeasonByTitleYear) + { + _logger.LogDebug("Located trakt season {Title} by title/year/season", item.DisplayTitle); + return seasonId; + } + + _logger.LogDebug("Unable to locate trakt season {Title}", item.DisplayTitle); + + return None; + } + + private async Task> IdentifyEpisode(TvContext dbContext, TraktListItemWithGuids item) + { + Option maybeEpisodeByGuid = await dbContext.EpisodeMetadata + .Filter(sm => sm.Guids.Any(g => item.Guids.Contains(g.Guid))) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.EpisodeId); + + foreach (int episodeId in maybeEpisodeByGuid) + { + _logger.LogDebug("Located trakt episode {Title} by id", item.DisplayTitle); + return episodeId; + } + + Option maybeEpisodeByTitleYear = await dbContext.EpisodeMetadata + .Filter(sm => sm.Episode.Season.Show.ShowMetadata.Any(s => s.Title == item.Title && s.Year == item.Year)) + .Filter(em => em.Episode.Season.SeasonNumber == item.Season) + .Filter(sm => sm.Episode.EpisodeMetadata.Any(e => e.EpisodeNumber == item.Episode)) + .FirstOrDefaultAsync() + .Map(Optional) + .MapT(sm => sm.EpisodeId); + + foreach (int episodeId in maybeEpisodeByTitleYear) + { + _logger.LogDebug("Located trakt episode {Title} by title/year/season/episode", item.DisplayTitle); + return episodeId; + } + + _logger.LogDebug("Unable to locate trakt episode {Title}", item.DisplayTitle); + + return None; + } + } +} diff --git a/ErsatzTV.Core/Interfaces/Trakt/ITraktApiClient.cs b/ErsatzTV.Core/Interfaces/Trakt/ITraktApiClient.cs new file mode 100644 index 000000000..2bc35a005 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/Trakt/ITraktApiClient.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core.Trakt; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.Trakt +{ + public interface ITraktApiClient + { + Task>> GetUserListItems( + string user, + string list); + } +} diff --git a/ErsatzTV.Core/Trakt/TraktConfiguration.cs b/ErsatzTV.Core/Trakt/TraktConfiguration.cs new file mode 100644 index 000000000..cafae7f10 --- /dev/null +++ b/ErsatzTV.Core/Trakt/TraktConfiguration.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Core.Trakt +{ + public class TraktConfiguration + { + public string ClientId { get; set; } + } +} diff --git a/ErsatzTV.Core/Trakt/TraktListItemKind.cs b/ErsatzTV.Core/Trakt/TraktListItemKind.cs new file mode 100644 index 000000000..a72110bdc --- /dev/null +++ b/ErsatzTV.Core/Trakt/TraktListItemKind.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Core.Trakt +{ + public enum TraktListItemKind + { + Movie, + Show, + Season, + Episode + } +} diff --git a/ErsatzTV.Core/Trakt/TraktListItemWithGuids.cs b/ErsatzTV.Core/Trakt/TraktListItemWithGuids.cs new file mode 100644 index 000000000..3ad6e947a --- /dev/null +++ b/ErsatzTV.Core/Trakt/TraktListItemWithGuids.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace ErsatzTV.Core.Trakt +{ + public record TraktListItemWithGuids( + string DisplayTitle, + string Title, + int Year, + int Season, + int Episode, + TraktListItemKind Kind, + List Guids); +} diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index c96f07a33..378a56e67 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -42,6 +42,7 @@ namespace ErsatzTV.Infrastructure.Data public DbSet Shows { get; set; } public DbSet ShowMetadata { get; set; } public DbSet Seasons { get; set; } + public DbSet SeasonMetadata { get; set; } public DbSet Episodes { get; set; } public DbSet EpisodeMetadata { get; set; } public DbSet PlexMovies { get; set; } diff --git a/ErsatzTV.Infrastructure/Trakt/ITraktApi.cs b/ErsatzTV.Infrastructure/Trakt/ITraktApi.cs new file mode 100644 index 000000000..2e586d0d5 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/ITraktApi.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Infrastructure.Trakt.Models; +using Refit; + +namespace ErsatzTV.Infrastructure.Trakt +{ + [Headers("Accept: application/json", "trakt-api-version: 2")] + public interface ITraktApi + { + [Get("/users/{user}/lists/{list}/items")] + Task> GetUserListItems( + [Header("trakt-api-key")] + string clientId, + string user, + string list); + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItem.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItem.cs new file mode 100644 index 000000000..536208fa8 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItem.cs @@ -0,0 +1,13 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItem + { + public int Rank { get; set; } + public int Id { get; set; } + public string Type { get; set; } + public TraktListItemMovie Movie { get; set; } + public TraktListItemShow Show { get; set; } + public TraktListItemSeason Season { get; set; } + public TraktListItemEpisode Episode { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemEpisode.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemEpisode.cs new file mode 100644 index 000000000..4e4b2d7a3 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemEpisode.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItemEpisode + { + public int Season { get; set; } + public int Number { get; set; } + public string Title { get; set; } + public TraktListItemIds Ids { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemIds.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemIds.cs new file mode 100644 index 000000000..7079fd1d5 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemIds.cs @@ -0,0 +1,10 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItemIds + { + public int Trakt { get; set; } + public int? Tvdb { get; set; } + public string Imdb { get; set; } + public int? Tmdb { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemMovie.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemMovie.cs new file mode 100644 index 000000000..bdb146915 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemMovie.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItemMovie + { + public string Title { get; set; } + public int Year { get; set; } + public TraktListItemIds Ids { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemSeason.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemSeason.cs new file mode 100644 index 000000000..7e6787704 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemSeason.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItemSeason + { + public int Number { get; set; } + public TraktListItemIds Ids { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemShow.cs b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemShow.cs new file mode 100644 index 000000000..8dc34de99 --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/Models/TraktListItemShow.cs @@ -0,0 +1,9 @@ +namespace ErsatzTV.Infrastructure.Trakt.Models +{ + public class TraktListItemShow + { + public string Title { get; set; } + public int Year { get; set; } + public TraktListItemIds Ids { get; set; } + } +} diff --git a/ErsatzTV.Infrastructure/Trakt/TraktApiClient.cs b/ErsatzTV.Infrastructure/Trakt/TraktApiClient.cs new file mode 100644 index 000000000..39a4b1bbf --- /dev/null +++ b/ErsatzTV.Infrastructure/Trakt/TraktApiClient.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.Trakt; +using ErsatzTV.Core.Trakt; +using ErsatzTV.Infrastructure.Trakt.Models; +using LanguageExt; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace ErsatzTV.Infrastructure.Trakt +{ + public class TraktApiClient : ITraktApiClient + { + private readonly ITraktApi _traktApi; + private readonly IOptions _traktConfiguration; + private readonly ILogger _logger; + + public TraktApiClient( + ITraktApi traktApi, + IOptions traktConfiguration, + ILogger logger) + { + _traktApi = traktApi; + _traktConfiguration = traktConfiguration; + _logger = logger; + } + + public async Task>> GetUserListItems( + string user, + string list) + { + try + { + var result = new List(); + + List apiItems = await _traktApi.GetUserListItems( + _traktConfiguration.Value.ClientId, + user, + list); + + foreach (TraktListItem apiItem in apiItems) + { + TraktListItemWithGuids item = apiItem.Type.ToLowerInvariant() switch + { + "movie" => new TraktListItemWithGuids( + $"{apiItem.Movie.Title} ({apiItem.Movie.Year})", + apiItem.Movie.Title, + apiItem.Movie.Year, + 0, + 0, + TraktListItemKind.Movie, + GuidsFromIds(apiItem.Movie.Ids)), + "show" => new TraktListItemWithGuids( + $"{apiItem.Show.Title} ({apiItem.Show.Year})", + apiItem.Show.Title, + apiItem.Show.Year, + 0, + 0, + TraktListItemKind.Show, + GuidsFromIds(apiItem.Show.Ids)), + "season" => new TraktListItemWithGuids( + $"{apiItem.Show.Title} ({apiItem.Show.Year}) S{apiItem.Season.Number:00}", + apiItem.Show.Title, + apiItem.Show.Year, + apiItem.Season.Number, + 0, + TraktListItemKind.Season, + GuidsFromIds(apiItem.Season.Ids)), + "episode" => new TraktListItemWithGuids( + $"{apiItem.Show.Title} ({apiItem.Show.Year}) S{apiItem.Episode.Season:00}E{apiItem.Episode.Number:00}", + apiItem.Show.Title, + apiItem.Show.Year, + apiItem.Episode.Season, + apiItem.Episode.Number, + TraktListItemKind.Episode, + GuidsFromIds(apiItem.Episode.Ids)), + _ => null + }; + + if (item != null) + { + result.Add(item); + } + } + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error getting trakt list items"); + return BaseError.New(ex.Message); + } + } + + private static List GuidsFromIds(TraktListItemIds ids) + { + var result = new List(); + + if (!string.IsNullOrWhiteSpace(ids.Imdb)) + { + result.Add($"imdb://{ids.Imdb}"); + } + + if (ids.Tmdb.HasValue) + { + result.Add($"tmdb://{ids.Tmdb.Value}"); + } + + if (ids.Tvdb.HasValue) + { + result.Add($"tvdb://{ids.Tvdb.Value}"); + } + + return result; + } + } +} diff --git a/ErsatzTV.sln.DotSettings b/ErsatzTV.sln.DotSettings index 9493040e0..c34c86341 100644 --- a/ErsatzTV.sln.DotSettings +++ b/ErsatzTV.sln.DotSettings @@ -45,6 +45,7 @@ True True True + True True True True diff --git a/ErsatzTV/Pages/CollectionItems.razor b/ErsatzTV/Pages/CollectionItems.razor index f98c1a66b..565a39025 100644 --- a/ErsatzTV/Pages/CollectionItems.razor +++ b/ErsatzTV/Pages/CollectionItems.razor @@ -2,6 +2,7 @@ @using ErsatzTV.Application.MediaCards @using ErsatzTV.Application.MediaCards.Queries @using ErsatzTV.Application.MediaCollections.Commands +@using Unit = LanguageExt.Unit @inherits MultiSelectBase @inject NavigationManager _navigationManager @inject ChannelWriter _channel @@ -30,11 +31,19 @@ } else { -
+
@_data.Name + Link="@($"/media/collections/{Id}/edit")"/> + + + Trakt + +
@if (_data.MovieCards.Any()) { @@ -387,4 +396,30 @@ await Mediator.Send(request); } + private async Task SyncFromTraktList() + { + var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small }; + IDialogReference dialog = Dialog.Show("Sync From Trakt List", options); + DialogResult result = await dialog.Result; + if (!result.Cancelled && result.Data is string url) + { + Either syncResult = await Mediator.Send(new SyncCollectionFromTraktList(Id, url)); + syncResult.Match( + Left: error => + { + Snackbar.Add($"Unexpected error synchronizing from Trakt: {error.Value}"); + Logger.LogError("Unexpected error synchronizing from Trakt: {Error}", error.Value); + }, + Right: _ => + { + Snackbar.Add("Successfully synchronized collection from Trakt", Severity.Success); + }); + + if (syncResult.IsRight) + { + await RefreshData(); + } + } + } + } \ No newline at end of file diff --git a/ErsatzTV/Shared/SyncFromTraktListDialog.razor b/ErsatzTV/Shared/SyncFromTraktListDialog.razor new file mode 100644 index 000000000..a2f8e25c7 --- /dev/null +++ b/ErsatzTV/Shared/SyncFromTraktListDialog.razor @@ -0,0 +1,68 @@ +@using Microsoft.Extensions.Caching.Memory +@inject IMediator _mediator +@inject IMemoryCache _memoryCache +@inject ISnackbar _snackbar +@inject ILogger _logger + + + + + + + Enter Trakt List URL + + + + + + + + Cancel + + Sync + + + + + +@code { + + [CascadingParameter] + MudDialogInstance MudDialog { get; set; } + + private string _traktListUrl; + + private record DummyModel; + + private readonly DummyModel _dummyModel = new(); + + private bool CanSubmit() => !string.IsNullOrWhiteSpace(_traktListUrl); + + private void Submit() + { + if (!CanSubmit()) + { + return; + } + + MudDialog.Close(DialogResult.Ok(_traktListUrl)); + } + + private void Cancel(MouseEventArgs e) + { + // this is gross, but [enter] seems to sometimes trigger cancel instead of submit + if (e.Detail == 0) + { + Submit(); + } + else + { + MudDialog.Cancel(); + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 53083d464..e8edf3948 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -26,11 +26,13 @@ using ErsatzTV.Core.Interfaces.Repositories; using ErsatzTV.Core.Interfaces.Runtime; using ErsatzTV.Core.Interfaces.Scheduling; using ErsatzTV.Core.Interfaces.Search; +using ErsatzTV.Core.Interfaces.Trakt; using ErsatzTV.Core.Jellyfin; using ErsatzTV.Core.Metadata; using ErsatzTV.Core.Metadata.Nfo; using ErsatzTV.Core.Plex; using ErsatzTV.Core.Scheduling; +using ErsatzTV.Core.Trakt; using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; @@ -44,6 +46,7 @@ using ErsatzTV.Infrastructure.Locking; using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Infrastructure.Runtime; using ErsatzTV.Infrastructure.Search; +using ErsatzTV.Infrastructure.Trakt; using ErsatzTV.Serialization; using ErsatzTV.Services; using ErsatzTV.Services.RunOnce; @@ -170,6 +173,11 @@ namespace ErsatzTV services.AddRefitClient() .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2")); + services.AddRefitClient() + .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.trakt.tv")); + + services.Configure(Configuration.GetSection("Trakt")); + CustomServices(services); } @@ -195,6 +203,7 @@ namespace ErsatzTV services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); // TODO: does this need to be singleton? + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); AddChannel(services); diff --git a/ErsatzTV/appsettings.json b/ErsatzTV/appsettings.json index 0857a4046..2921d7a8d 100644 --- a/ErsatzTV/appsettings.json +++ b/ErsatzTV/appsettings.json @@ -31,5 +31,8 @@ "Url": "http://+:8409" } } + }, + "Trakt": { + "ClientId": "5a4e78338b7f177cf980d6a8881dd5a52dee680746864e9e442b42d5f4d4ac82" } } \ No newline at end of file