Browse Source

add option to auto refresh trakt lists (#2169)

pull/2171/head
Jason Dove 4 weeks ago committed by GitHub
parent
commit
70fbd4c746
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 7
      ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs
  3. 10
      ErsatzTV.Application/MediaCollections/Commands/AddTraktListHandler.cs
  4. 4
      ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs
  5. 5
      ErsatzTV.Application/MediaCollections/Commands/UpdateTraktList.cs
  6. 29
      ErsatzTV.Application/MediaCollections/Commands/UpdateTraktListHandler.cs
  7. 3
      ErsatzTV.Application/MediaCollections/Mapper.cs
  8. 3
      ErsatzTV.Application/MediaCollections/Queries/GetTraktListById.cs
  9. 18
      ErsatzTV.Application/MediaCollections/Queries/GetTraktListByIdHandler.cs
  10. 9
      ErsatzTV.Application/MediaCollections/TraktListViewModel.cs
  11. 3
      ErsatzTV.Core/Domain/Collection/TraktList.cs
  12. 5923
      ErsatzTV.Infrastructure.MySql/Migrations/20250719133243_Add_TraktListAutoRefresh.Designer.cs
  13. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250719133243_Add_TraktListAutoRefresh.cs
  14. 5926
      ErsatzTV.Infrastructure.MySql/Migrations/20250719135252_Add_TraktListLastUpdate.Designer.cs
  15. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250719135252_Add_TraktListLastUpdate.cs
  16. 5929
      ErsatzTV.Infrastructure.MySql/Migrations/20250719141021_Add_TraktListLastMatch.Designer.cs
  17. 29
      ErsatzTV.Infrastructure.MySql/Migrations/20250719141021_Add_TraktListLastMatch.cs
  18. 9
      ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs
  19. 5762
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719133055_Add_TraktListAutoRefresh.Designer.cs
  20. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719133055_Add_TraktListAutoRefresh.cs
  21. 5765
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719135316_Add_TraktListLastUpdate.Designer.cs
  22. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719135316_Add_TraktListLastUpdate.cs
  23. 5768
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719140947_Add_TraktListLastMatch.Designer.cs
  24. 29
      ErsatzTV.Infrastructure.Sqlite/Migrations/20250719140947_Add_TraktListLastMatch.cs
  25. 9
      ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs
  26. 86
      ErsatzTV/Pages/TraktListEditor.razor
  27. 10
      ErsatzTV/Pages/TraktLists.razor
  28. 27
      ErsatzTV/Services/SchedulerService.cs
  29. 8
      ErsatzTV/ViewModels/TraktListEditViewModel.cs

1
CHANGELOG.md

@ -99,6 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). @@ -99,6 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `Awesome Episode (Part One)`
- `Better Episode (Part Two)`
- `Not So Great (Part Three)`
- Add Trakt List option `Auto Refresh` to automatically update list from trakt.tv once each day
### Changed
- Allow `Other Video` libraries and `Image` libraries to use the same folders

7
ErsatzTV.Application/MediaCollections/Commands/AddTraktList.cs

@ -2,4 +2,9 @@ @@ -2,4 +2,9 @@
namespace ErsatzTV.Application.MediaCollections;
public record AddTraktList(string TraktListUrl) : IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest;
public record AddTraktList(string TraktListUrl, string User, string List, bool Unlock)
: IRequest<Either<BaseError, Unit>>, IBackgroundServiceRequest
{
public static AddTraktList FromUrl(string traktListUrl) => new(traktListUrl, string.Empty, string.Empty, true);
public static AddTraktList Existing(string user, string list, bool unlock) => new(string.Empty, user, list, unlock);
}

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

@ -42,12 +42,20 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add @@ -42,12 +42,20 @@ public partial class AddTraktListHandler : TraktCommandBase, IRequestHandler<Add
}
finally
{
_entityLocker.UnlockTrakt();
if (request.Unlock)
{
_entityLocker.UnlockTrakt();
}
}
}
private static Validation<BaseError, Parameters> ValidateUrl(AddTraktList request)
{
if (!string.IsNullOrWhiteSpace(request.User) && !string.IsNullOrWhiteSpace(request.List))
{
return new Parameters(request.User, request.List);
}
// if we get a url, ensure it's for trakt.tv
Match match = Uri.IsWellFormedUriString(request.TraktListUrl, UriKind.Absolute)
? UriTraktListRegex().Match(request.TraktListUrl)

4
ErsatzTV.Application/MediaCollections/Commands/TraktCommandBase.cs

@ -92,6 +92,7 @@ public abstract class TraktCommandBase @@ -92,6 +92,7 @@ public abstract class TraktCommandBase
list.Items.RemoveAll(toRemove.Contains);
list.Items.AddRange(toAdd.Map(a => ProjectItem(list, a)));
list.LastUpdate = DateTime.UtcNow;
foreach (TraktListItem existing in toUpdate)
{
@ -181,6 +182,9 @@ public abstract class TraktCommandBase @@ -181,6 +182,9 @@ public abstract class TraktCommandBase
_searchIndex.Commit();
list.LastMatch = DateTime.UtcNow;
await dbContext.SaveChangesAsync();
return list;
}
catch (Exception ex)

5
ErsatzTV.Application/MediaCollections/Commands/UpdateTraktList.cs

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
using ErsatzTV.Core;
namespace ErsatzTV.Application.MediaCollections;
public record UpdateTraktList(int Id, bool AutoRefresh) : IRequest<Option<BaseError>>;

29
ErsatzTV.Application/MediaCollections/Commands/UpdateTraktListHandler.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using ErsatzTV.Core;
using ErsatzTV.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class UpdateTraktListHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<UpdateTraktList, Option<BaseError>>
{
public async Task<Option<BaseError>> Handle(UpdateTraktList request, CancellationToken cancellationToken)
{
try
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
await dbContext.TraktLists
.Where(tl => tl.Id == request.Id)
.ExecuteUpdateAsync(
u => u.SetProperty(p => p.AutoRefresh, p => request.AutoRefresh),
cancellationToken);
return Option<BaseError>.None;
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
}

3
ErsatzTV.Application/MediaCollections/Mapper.cs

@ -30,7 +30,8 @@ internal static class Mapper @@ -30,7 +30,8 @@ internal static class Mapper
$"{traktList.User}/{traktList.List}",
traktList.Name,
traktList.ItemCount,
traktList.Items.Count(i => i.MediaItemId.HasValue));
traktList.Items.Count(i => i.MediaItemId.HasValue),
traktList.AutoRefresh);
private static MultiCollectionItemViewModel ProjectToViewModel(MultiCollectionItem multiCollectionItem) =>
new(

3
ErsatzTV.Application/MediaCollections/Queries/GetTraktListById.cs

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
namespace ErsatzTV.Application.MediaCollections;
public record GetTraktListById(int Id) : IRequest<Option<TraktListViewModel>>;

18
ErsatzTV.Application/MediaCollections/Queries/GetTraktListByIdHandler.cs

@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Extensions;
using Microsoft.EntityFrameworkCore;
namespace ErsatzTV.Application.MediaCollections;
public class GetTraktListByIdHandler(IDbContextFactory<TvContext> dbContextFactory)
: IRequestHandler<GetTraktListById, Option<TraktListViewModel>>
{
public async Task<Option<TraktListViewModel>> Handle(GetTraktListById request, CancellationToken cancellationToken)
{
await using TvContext dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
return await dbContext.TraktLists
.Include(tl => tl.Items)
.SelectOneAsync(tl => tl.Id, tl => tl.Id == request.Id)
.MapT(Mapper.ProjectToViewModel);
}
}

9
ErsatzTV.Application/MediaCollections/TraktListViewModel.cs

@ -1,3 +1,10 @@ @@ -1,3 +1,10 @@
namespace ErsatzTV.Application.MediaCollections;
public record TraktListViewModel(int Id, int TraktId, string Slug, string Name, int ItemCount, int MatchCount);
public record TraktListViewModel(
int Id,
int TraktId,
string Slug,
string Name,
int ItemCount,
int MatchCount,
bool AutoRefresh);

3
ErsatzTV.Core/Domain/Collection/TraktList.cs

@ -9,5 +9,8 @@ public class TraktList @@ -9,5 +9,8 @@ public class TraktList
public string Name { get; set; }
public string Description { get; set; }
public int ItemCount { get; set; }
public bool AutoRefresh { get; set; }
public DateTime? LastUpdate { get; set; }
public DateTime? LastMatch { get; set; }
public List<TraktListItem> Items { get; set; }
}

5923
ErsatzTV.Infrastructure.MySql/Migrations/20250719133243_Add_TraktListAutoRefresh.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250719133243_Add_TraktListAutoRefresh.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListAutoRefresh : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AutoRefresh",
table: "TraktList",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AutoRefresh",
table: "TraktList");
}
}
}

5926
ErsatzTV.Infrastructure.MySql/Migrations/20250719135252_Add_TraktListLastUpdate.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250719135252_Add_TraktListLastUpdate.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListLastUpdate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastUpdate",
table: "TraktList",
type: "datetime(6)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastUpdate",
table: "TraktList");
}
}
}

5929
ErsatzTV.Infrastructure.MySql/Migrations/20250719141021_Add_TraktListLastMatch.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.MySql/Migrations/20250719141021_Add_TraktListLastMatch.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.MySql.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListLastMatch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastMatch",
table: "TraktList",
type: "datetime(6)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastMatch",
table: "TraktList");
}
}
}

9
ErsatzTV.Infrastructure.MySql/Migrations/TvContextModelSnapshot.cs

@ -3084,12 +3084,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations @@ -3084,12 +3084,21 @@ namespace ErsatzTV.Infrastructure.MySql.Migrations
MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property<int>("Id"));
b.Property<bool>("AutoRefresh")
.HasColumnType("tinyint(1)");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<int>("ItemCount")
.HasColumnType("int");
b.Property<DateTime?>("LastMatch")
.HasColumnType("datetime(6)");
b.Property<DateTime?>("LastUpdate")
.HasColumnType("datetime(6)");
b.Property<string>("List")
.HasColumnType("longtext");

5762
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719133055_Add_TraktListAutoRefresh.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719133055_Add_TraktListAutoRefresh.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListAutoRefresh : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AutoRefresh",
table: "TraktList",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AutoRefresh",
table: "TraktList");
}
}
}

5765
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719135316_Add_TraktListLastUpdate.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719135316_Add_TraktListLastUpdate.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListLastUpdate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastUpdate",
table: "TraktList",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastUpdate",
table: "TraktList");
}
}
}

5768
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719140947_Add_TraktListLastMatch.Designer.cs generated

File diff suppressed because it is too large Load Diff

29
ErsatzTV.Infrastructure.Sqlite/Migrations/20250719140947_Add_TraktListLastMatch.cs

@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ErsatzTV.Infrastructure.Sqlite.Migrations
{
/// <inheritdoc />
public partial class Add_TraktListLastMatch : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "LastMatch",
table: "TraktList",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "LastMatch",
table: "TraktList");
}
}
}

9
ErsatzTV.Infrastructure.Sqlite/Migrations/TvContextModelSnapshot.cs

@ -2933,12 +2933,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations @@ -2933,12 +2933,21 @@ namespace ErsatzTV.Infrastructure.Sqlite.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("AutoRefresh")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<int>("ItemCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastMatch")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastUpdate")
.HasColumnType("TEXT");
b.Property<string>("List")
.HasColumnType("TEXT");

86
ErsatzTV/Pages/TraktListEditor.razor

@ -0,0 +1,86 @@ @@ -0,0 +1,86 @@
@page "/media/trakt/lists/{Id:int}"
@using ErsatzTV.Application.MediaCollections
@implements IDisposable
@inject NavigationManager NavigationManager
@inject ILogger<ScheduleEditor> Logger
@inject ISnackbar Snackbar
@inject IMediator Mediator
<MudForm @ref="_form" @bind-IsValid="@_success" Style="max-height: 100%">
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="@HandleSubmitAsync" StartIcon="@Icons.Material.Filled.Save">
Save Trakt List
</MudButton>
</MudPaper>
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto">
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudText Typo="Typo.h5" Class="mb-2">Trakt List</MudText>
<MudDivider Class="mb-6"/>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Id</MudText>
</div>
<MudTextField Value="_model.Slug" Disabled="true" />
</MudStack>
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5">
<div class="d-flex">
<MudText>Automatic Refresh</MudText>
</div>
<MudCheckBox @bind-Value="@_model.AutoRefresh" For="@(() => _model.AutoRefresh)" Dense="true">
<MudText Typo="Typo.caption" Style="font-weight: normal">Update list from trakt.tv once each day</MudText>
</MudCheckBox>
</MudStack>
</MudContainer>
</div>
</MudForm>
@code {
private readonly CancellationTokenSource _cts = new();
[Parameter]
public int Id { get; set; }
private readonly TraktListEditViewModel _model = new();
private MudForm _form;
private bool _success;
public void Dispose()
{
_cts.Cancel();
_cts.Dispose();
}
protected override async Task OnParametersSetAsync()
{
Option<TraktListViewModel> maybeTraktList = await Mediator.Send(new GetTraktListById(Id), _cts.Token);
maybeTraktList.Match(
viewModel =>
{
_model.Id = viewModel.Id;
_model.Slug = viewModel.Slug;
_model.AutoRefresh = viewModel.AutoRefresh;
},
() => NavigationManager.NavigateTo("404"));
}
private async Task HandleSubmitAsync()
{
await _form.Validate();
if (_success)
{
var request = new UpdateTraktList(_model.Id, _model.AutoRefresh);
Option<BaseError> result = await Mediator.Send(request, _cts.Token);
foreach (BaseError error in result)
{
Snackbar.Add(error.Value, Severity.Error);
Logger.LogError("Unexpected error saving trakt list: {Error}", error.Value);
}
if (result.IsNone)
{
NavigationManager.NavigateTo("/media/trakt/lists");
}
}
}
}

10
ErsatzTV/Pages/TraktLists.razor

@ -32,7 +32,7 @@ @@ -32,7 +32,7 @@
<col/>
<col/>
<col/>
<col style="width: 180px;"/>
<col style="width: 240px;"/>
</MudHidden>
</ColGroup>
<HeaderContent>
@ -47,6 +47,12 @@ @@ -47,6 +47,12 @@
<MudTd>@context.MatchCount of @context.ItemCount</MudTd>
<MudTd>
<div style="align-items: center; display: flex;">
<MudTooltip Text="Edit Trakt List Properties">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Disabled="@Locker.IsTraktLocked()"
Href="@($"media/trakt/lists/{context.Id}")">
</MudIconButton>
</MudTooltip>
<MudTooltip Text="Search Trakt List">
<MudIconButton Icon="@Icons.Material.Filled.Search"
Disabled="@Locker.IsTraktLocked()"
@ -160,7 +166,7 @@ @@ -160,7 +166,7 @@
DialogResult result = await dialog.Result;
if (result is { Canceled: false, Data: string url })
{
await WorkerChannel.WriteAsync(new AddTraktList(url), _cts.Token);
await WorkerChannel.WriteAsync(Application.MediaCollections.AddTraktList.FromUrl(url), _cts.Token);
}
else
{

27
ErsatzTV/Services/SchedulerService.cs

@ -125,6 +125,7 @@ public class SchedulerService : BackgroundService @@ -125,6 +125,7 @@ public class SchedulerService : BackgroundService
await ScanJellyfinMediaSources(cancellationToken);
await ScanEmbyMediaSources(cancellationToken);
#endif
await RefreshTraktLists(cancellationToken);
await MatchTraktLists(cancellationToken);
await ReleaseMemory(cancellationToken);
@ -320,12 +321,38 @@ public class SchedulerService : BackgroundService @@ -320,12 +321,38 @@ public class SchedulerService : BackgroundService
}
}
private async Task RefreshTraktLists(CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
DateTime target = DateTime.UtcNow.AddDays(-1);
List<TraktList> traktLists = await dbContext.TraktLists
.Filter(tl => tl.AutoRefresh && (tl.LastUpdate == null || tl.LastUpdate <= target))
.ToListAsync(cancellationToken);
if (traktLists.Count != 0 && _entityLocker.LockTrakt())
{
TraktList last = traktLists.Last();
foreach (TraktList list in traktLists)
{
await _workerChannel.WriteAsync(
AddTraktList.Existing(list.User, list.List, list == last),
cancellationToken);
}
}
}
private async Task MatchTraktLists(CancellationToken cancellationToken)
{
using IServiceScope scope = _serviceScopeFactory.CreateScope();
TvContext dbContext = scope.ServiceProvider.GetRequiredService<TvContext>();
DateTime target = DateTime.UtcNow.AddHours(-1);
List<TraktList> traktLists = await dbContext.TraktLists
.Filter(tl => tl.LastMatch == null || tl.LastMatch <= target)
.ToListAsync(cancellationToken);
if (traktLists.Count != 0 && _entityLocker.LockTrakt())

8
ErsatzTV/ViewModels/TraktListEditViewModel.cs

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
namespace ErsatzTV.ViewModels;
public class TraktListEditViewModel
{
public int Id { get; set; }
public string Slug { get; set; }
public bool AutoRefresh { get; set; }
}
Loading…
Cancel
Save