Browse Source

sync trakt list to collection (#381)

* sync trakt list to collection

* move trakt client id
pull/382/head
Jason Dove 5 years ago committed by GitHub
parent
commit
45c6d20fd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      CHANGELOG.md
  2. 10
      ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktList.cs
  3. 269
      ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktListHandler.cs
  4. 14
      ErsatzTV.Core/Interfaces/Trakt/ITraktApiClient.cs
  5. 7
      ErsatzTV.Core/Trakt/TraktConfiguration.cs
  6. 10
      ErsatzTV.Core/Trakt/TraktListItemKind.cs
  7. 13
      ErsatzTV.Core/Trakt/TraktListItemWithGuids.cs
  8. 1
      ErsatzTV.Infrastructure/Data/TvContext.cs
  9. 18
      ErsatzTV.Infrastructure/Trakt/ITraktApi.cs
  10. 13
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItem.cs
  11. 10
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItemEpisode.cs
  12. 10
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItemIds.cs
  13. 9
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItemMovie.cs
  14. 8
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItemSeason.cs
  15. 9
      ErsatzTV.Infrastructure/Trakt/Models/TraktListItemShow.cs
  16. 119
      ErsatzTV.Infrastructure/Trakt/TraktApiClient.cs
  17. 1
      ErsatzTV.sln.DotSettings
  18. 41
      ErsatzTV/Pages/CollectionItems.razor
  19. 68
      ErsatzTV/Shared/SyncFromTraktListDialog.razor
  20. 9
      ErsatzTV/Startup.cs
  21. 3
      ErsatzTV/appsettings.json

5
CHANGELOG.md

@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. @@ -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

10
ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktList.cs

@ -0,0 +1,10 @@ @@ -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<Either<BaseError, Unit>>;
}

269
ErsatzTV.Application/MediaCollections/Commands/SyncCollectionFromTraktListHandler.cs

@ -0,0 +1,269 @@ @@ -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<SyncCollectionFromTraktList, Either<BaseError, Unit>>
{
private readonly ITraktApiClient _traktApiClient;
private readonly IDbContextFactory<TvContext> _dbContextFactory;
private readonly IMediaCollectionRepository _mediaCollectionRepository;
private readonly ChannelWriter<IBackgroundServiceRequest> _channel;
private readonly ILogger<SyncCollectionFromTraktListHandler> _logger;
public SyncCollectionFromTraktListHandler(
ITraktApiClient traktApiClient,
IDbContextFactory<TvContext> dbContextFactory,
IMediaCollectionRepository mediaCollectionRepository,
ChannelWriter<IBackgroundServiceRequest> channel,
ILogger<SyncCollectionFromTraktListHandler> logger)
{
_traktApiClient = traktApiClient;
_dbContextFactory = dbContextFactory;
_mediaCollectionRepository = mediaCollectionRepository;
_channel = channel;
_logger = logger;
}
public async Task<Either<BaseError, Unit>> 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<BaseError, List<TraktListItemWithGuids>> maybeItems =
await _traktApiClient.GetUserListItems(user, list);
return await maybeItems.Match(
async items => await SyncCollectionFromItems(request.CollectionId, items),
error => Task.FromResult(Left<BaseError, Unit>(error)));
}
return BaseError.New("Invalid Trakt List URL");
}
private async Task<Either<BaseError, Unit>> SyncCollectionFromItems(int collectionId, List<TraktListItemWithGuids> items)
{
await using TvContext dbContext = _dbContextFactory.CreateDbContext();
Option<Collection> 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<int>();
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<int>();
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<int>();
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<int>();
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<MediaItem> 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<Option<int>> IdentifyMovie(TvContext dbContext, TraktListItemWithGuids item)
{
Option<int> 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<int> 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<Option<int>> IdentifyShow(TvContext dbContext, TraktListItemWithGuids item)
{
Option<int> 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<int> 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<Option<int>> IdentifySeason(TvContext dbContext, TraktListItemWithGuids item)
{
Option<int> 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<int> 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<Option<int>> IdentifyEpisode(TvContext dbContext, TraktListItemWithGuids item)
{
Option<int> 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<int> 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;
}
}
}

14
ErsatzTV.Core/Interfaces/Trakt/ITraktApiClient.cs

@ -0,0 +1,14 @@ @@ -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<Either<BaseError, List<TraktListItemWithGuids>>> GetUserListItems(
string user,
string list);
}
}

7
ErsatzTV.Core/Trakt/TraktConfiguration.cs

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
namespace ErsatzTV.Core.Trakt
{
public class TraktConfiguration
{
public string ClientId { get; set; }
}
}

10
ErsatzTV.Core/Trakt/TraktListItemKind.cs

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
namespace ErsatzTV.Core.Trakt
{
public enum TraktListItemKind
{
Movie,
Show,
Season,
Episode
}
}

13
ErsatzTV.Core/Trakt/TraktListItemWithGuids.cs

@ -0,0 +1,13 @@ @@ -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<string> Guids);
}

1
ErsatzTV.Infrastructure/Data/TvContext.cs

@ -42,6 +42,7 @@ namespace ErsatzTV.Infrastructure.Data @@ -42,6 +42,7 @@ namespace ErsatzTV.Infrastructure.Data
public DbSet<Show> Shows { get; set; }
public DbSet<ShowMetadata> ShowMetadata { get; set; }
public DbSet<Season> Seasons { get; set; }
public DbSet<SeasonMetadata> SeasonMetadata { get; set; }
public DbSet<Episode> Episodes { get; set; }
public DbSet<EpisodeMetadata> EpisodeMetadata { get; set; }
public DbSet<PlexMovie> PlexMovies { get; set; }

18
ErsatzTV.Infrastructure/Trakt/ITraktApi.cs

@ -0,0 +1,18 @@ @@ -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<List<TraktListItem>> GetUserListItems(
[Header("trakt-api-key")]
string clientId,
string user,
string list);
}
}

13
ErsatzTV.Infrastructure/Trakt/Models/TraktListItem.cs

@ -0,0 +1,13 @@ @@ -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; }
}
}

10
ErsatzTV.Infrastructure/Trakt/Models/TraktListItemEpisode.cs

@ -0,0 +1,10 @@ @@ -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; }
}
}

10
ErsatzTV.Infrastructure/Trakt/Models/TraktListItemIds.cs

@ -0,0 +1,10 @@ @@ -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; }
}
}

9
ErsatzTV.Infrastructure/Trakt/Models/TraktListItemMovie.cs

@ -0,0 +1,9 @@ @@ -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; }
}
}

8
ErsatzTV.Infrastructure/Trakt/Models/TraktListItemSeason.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.Infrastructure.Trakt.Models
{
public class TraktListItemSeason
{
public int Number { get; set; }
public TraktListItemIds Ids { get; set; }
}
}

9
ErsatzTV.Infrastructure/Trakt/Models/TraktListItemShow.cs

@ -0,0 +1,9 @@ @@ -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; }
}
}

119
ErsatzTV.Infrastructure/Trakt/TraktApiClient.cs

@ -0,0 +1,119 @@ @@ -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> _traktConfiguration;
private readonly ILogger<TraktApiClient> _logger;
public TraktApiClient(
ITraktApi traktApi,
IOptions<TraktConfiguration> traktConfiguration,
ILogger<TraktApiClient> logger)
{
_traktApi = traktApi;
_traktConfiguration = traktConfiguration;
_logger = logger;
}
public async Task<Either<BaseError, List<TraktListItemWithGuids>>> GetUserListItems(
string user,
string list)
{
try
{
var result = new List<TraktListItemWithGuids>();
List<TraktListItem> 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<string> GuidsFromIds(TraktListItemIds ids)
{
var result = new List<string>();
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;
}
}
}

1
ErsatzTV.sln.DotSettings

@ -45,6 +45,7 @@ @@ -45,6 +45,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=setsar/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=showtitle/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=strm/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Trakt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvdb/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=tvshow/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=uniqueid/@EntryIndexedValue">True</s:Boolean>

41
ErsatzTV/Pages/CollectionItems.razor

@ -2,6 +2,7 @@ @@ -2,6 +2,7 @@
@using ErsatzTV.Application.MediaCards
@using ErsatzTV.Application.MediaCards.Queries
@using ErsatzTV.Application.MediaCollections.Commands
@using Unit = LanguageExt.Unit
@inherits MultiSelectBase<CollectionItems>
@inject NavigationManager _navigationManager
@inject ChannelWriter<IBackgroundServiceRequest> _channel
@ -30,11 +31,19 @@ @@ -30,11 +31,19 @@
}
else
{
<div style="display: flex; flex-direction: row;">
<div style="display: flex; flex-direction: row; align-items: center">
<MudText Typo="Typo.h4">@_data.Name</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Link="@($"/media/collections/{Id}/edit")"
Style="margin-bottom: auto; margin-top: auto;"/>
Link="@($"/media/collections/{Id}/edit")"/>
<MudTooltip Text="Sync From Trakt List">
<MudButton Class="ml-3"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.Download"
OnClick="@(_ => SyncFromTraktList())">
Trakt
</MudButton>
</MudTooltip>
</div>
@if (_data.MovieCards.Any())
{
@ -387,4 +396,30 @@ @@ -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<SyncFromTraktListDialog>("Sync From Trakt List", options);
DialogResult result = await dialog.Result;
if (!result.Cancelled && result.Data is string url)
{
Either<BaseError, Unit> 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();
}
}
}
}

68
ErsatzTV/Shared/SyncFromTraktListDialog.razor

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
@using Microsoft.Extensions.Caching.Memory
@inject IMediator _mediator
@inject IMemoryCache _memoryCache
@inject ISnackbar _snackbar
@inject ILogger<SyncFromTraktListDialog> _logger
<MudDialog>
<DialogContent>
<EditForm Model="@_dummyModel" OnSubmit="@(_ => Submit())">
<MudContainer Class="mb-6">
<MudText>
Enter Trakt List URL
</MudText>
</MudContainer>
<MudTextFieldString Label="Trakt List URL"
Placeholder="https://trakt.tv/users/username/lists/list-name"
@bind-Text="@_traktListUrl"
Style="min-width: 400px"
Class="mb-6 mx-4">
</MudTextFieldString>
</EditForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel" ButtonType="ButtonType.Reset">Cancel</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Submit">
Sync
</MudButton>
</DialogActions>
</MudDialog>
@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();
}
}
}

9
ErsatzTV/Startup.cs

@ -26,11 +26,13 @@ using ErsatzTV.Core.Interfaces.Repositories; @@ -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; @@ -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 @@ -170,6 +173,11 @@ namespace ErsatzTV
services.AddRefitClient<IPlexTvApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://plex.tv/api/v2"));
services.AddRefitClient<ITraktApi>()
.ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.trakt.tv"));
services.Configure<TraktConfiguration>(Configuration.GetSection("Trakt"));
CustomServices(services);
}
@ -195,6 +203,7 @@ namespace ErsatzTV @@ -195,6 +203,7 @@ namespace ErsatzTV
services.AddSingleton<FFmpegPlaybackSettingsCalculator>();
services.AddSingleton<IPlexSecretStore, PlexSecretStore>();
services.AddSingleton<IPlexTvApiClient, PlexTvApiClient>(); // TODO: does this need to be singleton?
services.AddSingleton<ITraktApiClient, TraktApiClient>();
services.AddSingleton<IEntityLocker, EntityLocker>();
services.AddSingleton<ISearchIndex, SearchIndex>();
AddChannel<IBackgroundServiceRequest>(services);

3
ErsatzTV/appsettings.json

@ -31,5 +31,8 @@ @@ -31,5 +31,8 @@
"Url": "http://+:8409"
}
}
},
"Trakt": {
"ClientId": "5a4e78338b7f177cf980d6a8881dd5a52dee680746864e9e442b42d5f4d4ac82"
}
}
Loading…
Cancel
Save