mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 02:38:55 -04:00
Fix Seeker not reporting an episode grab (#776)
This commit is contained in:
1 parent
eee5af9268
commit
bba4e943bc
12 files changed
+2006
-7
No files matched your search
+141
@@ -808,6 +808,147 @@ public class SeekerCommandMonitorTests : IAsyncDisposable
|
||||
_timeProvider.GetUtcNow() - TimeSpan.FromMinutes(60));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_the_grab_of_a_sonarr_episode_search()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_dataContext);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(sonarrInstance.Id, externalItemId: 42, episodeId: 9001, itemTitle: "Example Show - S03E04"),
|
||||
new QueueRecord { Id = 1, SeriesId = 42, EpisodeId = 9001, SeasonNumber = 3, Title = "Example.Show.S03E04.1080p", DownloadId = "EP9001", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldNotBeNull();
|
||||
grabbedItems!.ShouldBe(["Example.Show.S03E04.1080p"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ignores_another_episode_of_the_same_series()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_dataContext);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(sonarrInstance.Id, externalItemId: 42, episodeId: 9001, itemTitle: "Example Show - S03E04"),
|
||||
new QueueRecord { Id = 1, SeriesId = 42, EpisodeId = 9001, SeasonNumber = 3, Title = "Example.Show.S03E04.1080p", DownloadId = "EP9001", Protocol = "torrent", Status = "downloading" },
|
||||
new QueueRecord { Id = 2, SeriesId = 42, EpisodeId = 9002, SeasonNumber = 3, Title = "Example.Show.S03E05.1080p", DownloadId = "EP9002", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldNotBeNull();
|
||||
grabbedItems!.ShouldBe(["Example.Show.S03E04.1080p"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ignores_another_season_of_a_specials_search()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_dataContext);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(sonarrInstance.Id, externalItemId: 42, seasonNumber: 0, itemTitle: "Example Show S00"),
|
||||
new QueueRecord { Id = 1, SeriesId = 42, EpisodeId = 9001, SeasonNumber = 3, Title = "Example.Show.S03E04.1080p", DownloadId = "EP9001", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_the_grab_of_a_whisparr_v3_search()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance whisparrInstance = TestDataContextFactory.AddWhisparrInstance(_dataContext, version: 3);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(whisparrInstance.Id, externalItemId: 55, itemTitle: "Example Scene"),
|
||||
new QueueRecord { Id = 1, MovieId = 55, Title = "Example.Scene.1080p", DownloadId = "MV55", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldNotBeNull();
|
||||
grabbedItems!.ShouldBe(["Example.Scene.1080p"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_the_grab_of_a_lidarr_search()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance lidarrInstance = TestDataContextFactory.AddLidarrInstance(_dataContext);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(lidarrInstance.Id, externalItemId: 77, itemTitle: "Example Album"),
|
||||
new QueueRecord { Id = 1, AlbumId = 77, Title = "Example.Album.FLAC", DownloadId = "AL77", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldNotBeNull();
|
||||
grabbedItems!.ShouldBe(["Example.Album.FLAC"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reports_the_grab_of_a_readarr_search()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance readarrInstance = TestDataContextFactory.AddReadarrInstance(_dataContext);
|
||||
|
||||
// Act
|
||||
List<string>? grabbedItems = await CaptureGrabbedItemsAsync(
|
||||
Tracker(readarrInstance.Id, externalItemId: 88, itemTitle: "Example Book"),
|
||||
new QueueRecord { Id = 1, BookId = 88, Title = "Example.Book.EPUB", DownloadId = "BK88", Protocol = "torrent", Status = "downloading" });
|
||||
|
||||
// Assert
|
||||
grabbedItems.ShouldNotBeNull();
|
||||
grabbedItems!.ShouldBe(["Example.Book.EPUB"]);
|
||||
}
|
||||
|
||||
private SeekerCommandTracker Tracker(
|
||||
Guid arrInstanceId,
|
||||
long externalItemId,
|
||||
long episodeId = 0,
|
||||
int seasonNumber = 0,
|
||||
string itemTitle = "Test Item") =>
|
||||
new()
|
||||
{
|
||||
ArrInstanceId = arrInstanceId,
|
||||
CommandId = 1,
|
||||
EventId = Guid.NewGuid(),
|
||||
ExternalItemId = externalItemId,
|
||||
EpisodeId = episodeId,
|
||||
ItemTitle = itemTitle,
|
||||
SeasonNumber = seasonNumber,
|
||||
Status = SearchCommandStatus.Pending,
|
||||
CreatedAt = _timeProvider.GetUtcNow().UtcDateTime,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Runs one monitor cycle over a completed command and returns the titles it attributed.
|
||||
/// </summary>
|
||||
private async Task<List<string>?> CaptureGrabbedItemsAsync(SeekerCommandTracker tracker, params QueueRecord[] records)
|
||||
{
|
||||
_eventsContext.SeekerCommandTrackers.Add(tracker);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
await _eventsContext.SaveChangesAsync();
|
||||
|
||||
StubCommandState(ArrCommandState.Completed, tracker.CommandId);
|
||||
|
||||
_arrClient.GetQueueItemsAsync(Arg.Any<ArrInstance>(), Arg.Any<int>())
|
||||
.Returns(new QueueListResponse { TotalRecords = records.Length, Records = [.. records] });
|
||||
|
||||
TaskCompletionSource<List<string>?> publishTcs = new();
|
||||
_eventPublisher.PublishSearchCompleted(
|
||||
Arg.Any<Guid>(), Arg.Any<SearchCommandStatus>(), Arg.Any<InstanceType>(), Arg.Any<string>(), Arg.Any<List<string>?>())
|
||||
.Returns(Task.CompletedTask)
|
||||
.AndDoes(ci => publishTcs.TrySetResult(ci.ArgAt<List<string>?>(4)));
|
||||
|
||||
await _sut.StartAsync(_cts.Token);
|
||||
_timeProvider.Advance(TimeSpan.FromSeconds(11));
|
||||
|
||||
return await publishTcs.Task.WaitAsync(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
private void StubCommandState(ArrCommandState state, long commandId = 1)
|
||||
{
|
||||
_arrClient.GetCommandsAsync(Arg.Any<ArrInstance>())
|
||||
|
||||
@@ -4107,6 +4107,127 @@ public class SeekerTests : IDisposable
|
||||
capturedSearchItem.SearchType.ShouldBe(SeriesSearchType.Season);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SonarrEpisodeReplacement_TracksSeriesAndEpisode()
|
||||
{
|
||||
// Arrange — an episode replacement carries the episode id in ItemId
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.EventsContext.SearchQueue.Add(new SearchQueueItem
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ItemId = 9001,
|
||||
SeriesId = 42,
|
||||
SearchType = "Episode",
|
||||
Title = "Example Show - S03E04",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>())
|
||||
.Returns(100L);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert — the tracker holds the series for the queue lookup and the episode for the match
|
||||
SeekerCommandTracker? tracker = await _fixture.EventsContext.SeekerCommandTrackers
|
||||
.FirstOrDefaultAsync(t => t.ArrInstanceId == sonarrInstance.Id);
|
||||
tracker.ShouldNotBeNull();
|
||||
tracker.ExternalItemId.ShouldBe(42);
|
||||
tracker.EpisodeId.ShouldBe(9001);
|
||||
tracker.SeasonNumber.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_SonarrSeasonReplacement_TracksSeriesAndSeason()
|
||||
{
|
||||
// Arrange — a season replacement carries the season number in ItemId
|
||||
ArrInstance sonarrInstance = TestDataContextFactory.AddSonarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.EventsContext.SearchQueue.Add(new SearchQueueItem
|
||||
{
|
||||
ArrInstanceId = sonarrInstance.Id,
|
||||
ItemId = 3,
|
||||
SeriesId = 42,
|
||||
SearchType = "Season",
|
||||
Title = "Example Show S03",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>())
|
||||
.Returns(100L);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Sonarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
SeekerCommandTracker? tracker = await _fixture.EventsContext.SeekerCommandTrackers
|
||||
.FirstOrDefaultAsync(t => t.ArrInstanceId == sonarrInstance.Id);
|
||||
tracker.ShouldNotBeNull();
|
||||
tracker.ExternalItemId.ShouldBe(42);
|
||||
tracker.EpisodeId.ShouldBe(0);
|
||||
tracker.SeasonNumber.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RadarrReplacement_TracksTheMovie()
|
||||
{
|
||||
// Arrange
|
||||
ArrInstance radarrInstance = TestDataContextFactory.AddRadarrInstance(_fixture.DataContext);
|
||||
|
||||
_fixture.EventsContext.SearchQueue.Add(new SearchQueueItem
|
||||
{
|
||||
ArrInstanceId = radarrInstance.Id,
|
||||
ItemId = 55,
|
||||
Title = "Example Movie",
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await _fixture.DataContext.SaveChangesAsync();
|
||||
await _fixture.EventsContext.SaveChangesAsync();
|
||||
|
||||
IArrClient mockArrClient = Substitute.For<IArrClient>();
|
||||
mockArrClient
|
||||
.SearchItemAsync(Arg.Any<ArrInstance>(), Arg.Any<SearchItem>())
|
||||
.Returns(100L);
|
||||
|
||||
_fixture.ArrClientFactory
|
||||
.GetClient(InstanceType.Radarr, Arg.Any<float>())
|
||||
.Returns(mockArrClient);
|
||||
|
||||
var sut = CreateSut();
|
||||
|
||||
// Act
|
||||
await sut.ExecuteAsync();
|
||||
|
||||
// Assert
|
||||
SeekerCommandTracker? tracker = await _fixture.EventsContext.SeekerCommandTrackers
|
||||
.FirstOrDefaultAsync(t => t.ArrInstanceId == radarrInstance.Id);
|
||||
tracker.ShouldNotBeNull();
|
||||
tracker.ExternalItemId.ShouldBe(55);
|
||||
tracker.EpisodeId.ShouldBe(0);
|
||||
tracker.SeasonNumber.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_ReplacementSearchFails_StillDequesItem()
|
||||
{
|
||||
|
||||
@@ -179,7 +179,10 @@ public sealed class Seeker : IHandler
|
||||
|
||||
if (!isDryRun)
|
||||
{
|
||||
await SaveCommandTrackerAsync(commandId, eventId, arrInstance.Id, arrInstance.ArrConfig.Type, item.ItemId, item.Title);
|
||||
(long externalItemId, long episodeId, int seasonNumber) = ResolveTrackedIds(searchItem);
|
||||
|
||||
await SaveCommandTrackerAsync(commandId, eventId, arrInstance.Id, arrInstance.ArrConfig.Type,
|
||||
externalItemId, item.Title, seasonNumber, episodeId);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Replacement search triggered for '{Title}' on {InstanceName}",
|
||||
@@ -215,6 +218,18 @@ public sealed class Seeker : IHandler
|
||||
return new SearchItem { Id = item.ItemId };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a search item into the ids the SeekerCommandMonitor matches queue records against.
|
||||
/// An episode search targets the episode, every other search targets its library item.
|
||||
/// </summary>
|
||||
private static (long ExternalItemId, long EpisodeId, int SeasonNumber) ResolveTrackedIds(SearchItem searchItem) =>
|
||||
searchItem switch
|
||||
{
|
||||
SeriesSearchItem { SearchType: SeriesSearchType.Episode } series => (series.SeriesId, series.Id, 0),
|
||||
SeriesSearchItem { SearchType: SeriesSearchType.Season } series => (series.SeriesId, 0, (int)series.Id),
|
||||
_ => (searchItem.Id, 0, 0),
|
||||
};
|
||||
|
||||
private async Task ProcessProactiveSearchAsync(SeekerConfig config, bool isDryRun, CancellationToken cancellationToken)
|
||||
{
|
||||
List<SeekerInstanceConfig> instanceConfigs = await _dataContext.SeekerInstanceConfigs
|
||||
@@ -992,7 +1007,8 @@ public sealed class Seeker : IHandler
|
||||
InstanceType instanceType,
|
||||
long externalItemId,
|
||||
string itemTitle,
|
||||
int seasonNumber = 0)
|
||||
int seasonNumber = 0,
|
||||
long episodeId = 0)
|
||||
{
|
||||
_eventsContext.SeekerCommandTrackers.Add(new SeekerCommandTracker
|
||||
{
|
||||
@@ -1000,6 +1016,7 @@ public sealed class Seeker : IHandler
|
||||
CommandId = commandId,
|
||||
EventId = eventId,
|
||||
ExternalItemId = externalItemId,
|
||||
EpisodeId = episodeId,
|
||||
ItemTitle = itemTitle,
|
||||
SeasonNumber = seasonNumber,
|
||||
CreatedAt = _timeProvider.GetUtcNow(),
|
||||
|
||||
@@ -402,10 +402,7 @@ public class SeekerCommandMonitor : BackgroundService
|
||||
IReadOnlyList<QueueRecord> queue)
|
||||
{
|
||||
List<string> grabbedTitles = queue
|
||||
.Where(r => arrInstance.ArrConfig.Type == InstanceType.Radarr
|
||||
? r.MovieId == tracker.ExternalItemId
|
||||
: r.SeriesId == tracker.ExternalItemId
|
||||
&& (tracker.SeasonNumber == 0 || r.SeasonNumber == tracker.SeasonNumber))
|
||||
.Where(r => MatchesTracker(r, tracker, arrInstance))
|
||||
.Where(r => !string.IsNullOrEmpty(r.DownloadId))
|
||||
.GroupBy(r => r.DownloadId)
|
||||
.Select(g => g.First())
|
||||
@@ -421,4 +418,20 @@ public class SeekerCommandMonitor : BackgroundService
|
||||
|
||||
return grabbedTitles.Count > 0 ? grabbedTitles : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tells whether a queue record holds what the search asked for.
|
||||
/// Each arr fills its own id on the record, so match the tracker against that id.
|
||||
/// </summary>
|
||||
private static bool MatchesTracker(QueueRecord record, SeekerCommandTracker tracker, ArrInstance arrInstance) =>
|
||||
arrInstance.ArrConfig.Type switch
|
||||
{
|
||||
InstanceType.Radarr => record.MovieId == tracker.ExternalItemId,
|
||||
InstanceType.Whisparr when arrInstance.Version is 3 => record.MovieId == tracker.ExternalItemId,
|
||||
InstanceType.Lidarr => record.AlbumId == tracker.ExternalItemId,
|
||||
InstanceType.Readarr => record.BookId == tracker.ExternalItemId,
|
||||
_ => tracker.EpisodeId > 0
|
||||
? record.EpisodeId == tracker.EpisodeId
|
||||
: record.SeriesId == tracker.ExternalItemId && record.SeasonNumber == tracker.SeasonNumber,
|
||||
};
|
||||
}
|
||||
+765
@@ -0,0 +1,765 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Postgres.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20260908071959_AddEpisodeIdToSeekerCommandTracker")]
|
||||
partial class AddEpisodeIdToSeekerCommandTracker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("events")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<string>("CleanReason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("clean_reason");
|
||||
|
||||
b.Property<string>("CleanedCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("cleaned_category");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Guid?>("CycleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("cycle_id");
|
||||
|
||||
b.Property<string>("DeleteReason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("delete_reason");
|
||||
|
||||
b.Property<Guid?>("DownloadClientId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("download_client_id");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("FailedImportReasons")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("failed_import_reasons");
|
||||
|
||||
b.PrimitiveCollection<List<string>>("GrabbedItems")
|
||||
.IsRequired()
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("grabbed_items");
|
||||
|
||||
b.Property<bool?>("IsCategoryTag")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_category_tag");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<string>("ItemHash")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("item_hash");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("NewCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("new_category");
|
||||
|
||||
b.Property<string>("OldCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("old_category");
|
||||
|
||||
b.Property<bool?>("RemoveFromClient")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("remove_from_client");
|
||||
|
||||
b.Property<string>("SearchReason")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("search_reason");
|
||||
|
||||
b.Property<string>("SearchStatus")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("search_status");
|
||||
|
||||
b.Property<string>("SearchType")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("search_type");
|
||||
|
||||
b.Property<double?>("SeedRatio")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("seed_ratio");
|
||||
|
||||
b.Property<double?>("SeedingTimeHours")
|
||||
.HasColumnType("double precision")
|
||||
.HasColumnName("seeding_time_hours");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<int?>("StrikeCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("strike_count");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_events_arr_instance_id");
|
||||
|
||||
b.HasIndex("CycleId")
|
||||
.HasDatabaseName("ix_events_cycle_id");
|
||||
|
||||
b.HasIndex("DeleteReason")
|
||||
.HasDatabaseName("ix_events_delete_reason");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<string>("ItemHash")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("item_hash");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<DateTimeOffset?>("ResolvedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("resolved_at");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<int?>("StrikeCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("strike_count");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.HasIndex("Type", "ItemHash")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_manual_events_type_item_hash")
|
||||
.HasFilter("is_resolved = false");
|
||||
|
||||
b.ToTable("manual_events", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.CustomFormatScoreEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<int>("CurrentScore")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("current_score");
|
||||
|
||||
b.Property<int>("CutoffScore")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("cutoff_score");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<long>("FileId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<bool>("IsMonitored")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_monitored");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<DateTimeOffset>("LastSyncedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_synced_at");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastUpgradedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_upgraded_at");
|
||||
|
||||
b.Property<string>("QualityProfileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("quality_profile_name");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_format_score_entries");
|
||||
|
||||
b.HasIndex("LastUpgradedAt")
|
||||
.HasDatabaseName("ix_custom_format_score_entries_last_upgraded_at");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "EpisodeId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_custom_format_score_entries_arr_instance_id_external_item_i");
|
||||
|
||||
b.ToTable("custom_format_score_entries", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.CustomFormatScoreHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<int>("CutoffScore")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("cutoff_score");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<DateTimeOffset>("RecordedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("recorded_at");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_format_score_history");
|
||||
|
||||
b.HasIndex("RecordedAt")
|
||||
.HasDatabaseName("ix_custom_format_score_history_recorded_at");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "EpisodeId")
|
||||
.HasDatabaseName("ix_custom_format_score_history_arr_instance_id_external_item_i");
|
||||
|
||||
b.ToTable("custom_format_score_history", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<bool>("IsMarkedForRemoval")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_marked_for_removal");
|
||||
|
||||
b.Property<bool>("IsRemoved")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_removed");
|
||||
|
||||
b.Property<bool>("IsReturning")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_returning");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<DateTimeOffset>("StartedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SearchQueueItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("item_id");
|
||||
|
||||
b.Property<string>("SearchType")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("search_type");
|
||||
|
||||
b.Property<long?>("SeriesId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("series_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_search_queue");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_search_queue_arr_instance_id");
|
||||
|
||||
b.ToTable("search_queue", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SeekerCommandTracker", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<long>("CommandId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("command_id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("event_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("season_number");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_seeker_command_trackers");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_seeker_command_trackers_arr_instance_id");
|
||||
|
||||
b.ToTable("seeker_command_trackers", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SeekerHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<Guid>("CycleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("cycle_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<DateTimeOffset>("LastSearchedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_searched_at");
|
||||
|
||||
b.Property<int>("SearchCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("search_count");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("season_number");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_seeker_history");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "ItemType", "SeasonNumber", "CycleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_seeker_history_arr_instance_id_external_item_id_item_type_s");
|
||||
|
||||
b.ToTable("seeker_history", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", "events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Postgres.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEpisodeIdToSeekerCommandTracker : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "episode_id",
|
||||
schema: "events",
|
||||
table: "seeker_command_trackers",
|
||||
type: "bigint",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "episode_id",
|
||||
schema: "events",
|
||||
table: "seeker_command_trackers");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -556,6 +556,10 @@ namespace Cleanuparr.Persistence.Postgres.Migrations.Events
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("event_id");
|
||||
|
||||
+767
@@ -0,0 +1,767 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Cleanuparr.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
[DbContext(typeof(EventsContext))]
|
||||
[Migration("20260908071938_AddEpisodeIdToSeekerCommandTracker")]
|
||||
partial class AddEpisodeIdToSeekerCommandTracker
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.1");
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid?>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<string>("CleanReason")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("clean_reason");
|
||||
|
||||
b.Property<string>("CleanedCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cleaned_category");
|
||||
|
||||
b.Property<string>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<Guid?>("CycleId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cycle_id");
|
||||
|
||||
b.Property<string>("DeleteReason")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("delete_reason");
|
||||
|
||||
b.Property<Guid?>("DownloadClientId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_id");
|
||||
|
||||
b.Property<string>("EventType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_type");
|
||||
|
||||
b.PrimitiveCollection<string>("FailedImportReasons")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("failed_import_reasons");
|
||||
|
||||
b.PrimitiveCollection<string>("GrabbedItems")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("grabbed_items");
|
||||
|
||||
b.Property<bool?>("IsCategoryTag")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_category_tag");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<string>("ItemHash")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_hash");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("NewCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("new_category");
|
||||
|
||||
b.Property<string>("OldCategory")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("old_category");
|
||||
|
||||
b.Property<bool?>("RemoveFromClient")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("remove_from_client");
|
||||
|
||||
b.Property<string>("SearchReason")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("search_reason");
|
||||
|
||||
b.Property<string>("SearchStatus")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("search_status");
|
||||
|
||||
b.Property<string>("SearchType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("search_type");
|
||||
|
||||
b.Property<double?>("SeedRatio")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("seed_ratio");
|
||||
|
||||
b.Property<double?>("SeedingTimeHours")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("seeding_time_hours");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<int?>("StrikeCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("strike_count");
|
||||
|
||||
b.Property<Guid?>("StrikeId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("strike_id");
|
||||
|
||||
b.Property<string>("Timestamp")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<Guid?>("TrackingId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("tracking_id");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_events");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_events_arr_instance_id");
|
||||
|
||||
b.HasIndex("CycleId")
|
||||
.HasDatabaseName("ix_events_cycle_id");
|
||||
|
||||
b.HasIndex("DeleteReason")
|
||||
.HasDatabaseName("ix_events_delete_reason");
|
||||
|
||||
b.HasIndex("EventType")
|
||||
.HasDatabaseName("ix_events_event_type");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_events_job_run_id");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_events_severity");
|
||||
|
||||
b.HasIndex("StrikeId")
|
||||
.HasDatabaseName("ix_events_strike_id");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_events_timestamp");
|
||||
|
||||
b.ToTable("events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadClientName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_name");
|
||||
|
||||
b.Property<string>("DownloadClientType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_client_type");
|
||||
|
||||
b.Property<string>("InstanceType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_type");
|
||||
|
||||
b.Property<string>("InstanceUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("instance_url");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<bool>("IsResolved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_resolved");
|
||||
|
||||
b.Property<string>("ItemHash")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_hash");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<Guid?>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<string>("Message")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("message");
|
||||
|
||||
b.Property<string>("ResolvedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("resolved_at");
|
||||
|
||||
b.Property<string>("Severity")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<int?>("StrikeCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("strike_count");
|
||||
|
||||
b.Property<string>("Timestamp")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_manual_events");
|
||||
|
||||
b.HasIndex("InstanceType")
|
||||
.HasDatabaseName("ix_manual_events_instance_type");
|
||||
|
||||
b.HasIndex("IsResolved")
|
||||
.HasDatabaseName("ix_manual_events_is_resolved");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_manual_events_job_run_id");
|
||||
|
||||
b.HasIndex("Message")
|
||||
.HasDatabaseName("ix_manual_events_message");
|
||||
|
||||
b.HasIndex("Severity")
|
||||
.HasDatabaseName("ix_manual_events_severity");
|
||||
|
||||
b.HasIndex("Timestamp")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_manual_events_timestamp");
|
||||
|
||||
b.HasIndex("Type", "ItemHash")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_manual_events_type_item_hash")
|
||||
.HasFilter("\"is_resolved\" = 0");
|
||||
|
||||
b.ToTable("manual_events", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.CustomFormatScoreEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<int>("CurrentScore")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("current_score");
|
||||
|
||||
b.Property<int>("CutoffScore")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("cutoff_score");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<long>("FileId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("file_id");
|
||||
|
||||
b.Property<bool>("IsMonitored")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_monitored");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<string>("LastSyncedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_synced_at");
|
||||
|
||||
b.Property<string>("LastUpgradedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_upgraded_at");
|
||||
|
||||
b.Property<string>("QualityProfileName")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("quality_profile_name");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_format_score_entries");
|
||||
|
||||
b.HasIndex("LastUpgradedAt")
|
||||
.HasDatabaseName("ix_custom_format_score_entries_last_upgraded_at");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "EpisodeId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_custom_format_score_entries_arr_instance_id_external_item_id_episode_id");
|
||||
|
||||
b.ToTable("custom_format_score_entries", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.CustomFormatScoreHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<int>("CutoffScore")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("cutoff_score");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<string>("RecordedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("recorded_at");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_custom_format_score_history");
|
||||
|
||||
b.HasIndex("RecordedAt")
|
||||
.HasDatabaseName("ix_custom_format_score_history_recorded_at");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "EpisodeId")
|
||||
.HasDatabaseName("ix_custom_format_score_history_arr_instance_id_external_item_id_episode_id");
|
||||
|
||||
b.ToTable("custom_format_score_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("DownloadId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_id");
|
||||
|
||||
b.Property<bool>("IsMarkedForRemoval")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_marked_for_removal");
|
||||
|
||||
b.Property<bool>("IsRemoved")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_removed");
|
||||
|
||||
b.Property<bool>("IsReturning")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_returning");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_download_items");
|
||||
|
||||
b.HasIndex("DownloadId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_download_items_download_id");
|
||||
|
||||
b.ToTable("download_items", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CompletedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("completed_at");
|
||||
|
||||
b.Property<string>("StartedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("started_at");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_job_runs");
|
||||
|
||||
b.HasIndex("StartedAt")
|
||||
.IsDescending()
|
||||
.HasDatabaseName("ix_job_runs_started_at");
|
||||
|
||||
b.HasIndex("Type")
|
||||
.HasDatabaseName("ix_job_runs_type");
|
||||
|
||||
b.ToTable("job_runs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SearchQueueItem", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<string>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("ItemId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("item_id");
|
||||
|
||||
b.Property<string>("SearchType")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("search_type");
|
||||
|
||||
b.Property<long?>("SeriesId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("series_id");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_search_queue");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_search_queue_arr_instance_id");
|
||||
|
||||
b.ToTable("search_queue", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SeekerCommandTracker", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<long>("CommandId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("command_id");
|
||||
|
||||
b.Property<string>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("season_number");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_seeker_command_trackers");
|
||||
|
||||
b.HasIndex("ArrInstanceId")
|
||||
.HasDatabaseName("ix_seeker_command_trackers_arr_instance_id");
|
||||
|
||||
b.ToTable("seeker_command_trackers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.SeekerHistory", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<Guid>("ArrInstanceId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("arr_instance_id");
|
||||
|
||||
b.Property<Guid>("CycleId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("cycle_id");
|
||||
|
||||
b.Property<long>("ExternalItemId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("external_item_id");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<string>("ItemTitle")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_title");
|
||||
|
||||
b.Property<string>("ItemType")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("item_type");
|
||||
|
||||
b.Property<string>("LastSearchedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_searched_at");
|
||||
|
||||
b.Property<int>("SearchCount")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("search_count");
|
||||
|
||||
b.Property<int>("SeasonNumber")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("season_number");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_seeker_history");
|
||||
|
||||
b.HasIndex("ArrInstanceId", "ExternalItemId", "ItemType", "SeasonNumber", "CycleId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_seeker_history_arr_instance_id_external_item_id_item_type_season_number_cycle_id");
|
||||
|
||||
b.ToTable("seeker_history", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<Guid>("DownloadItemId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("download_item_id");
|
||||
|
||||
b.Property<bool>("IsDryRun")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("is_dry_run");
|
||||
|
||||
b.Property<Guid>("JobRunId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("job_run_id");
|
||||
|
||||
b.Property<long?>("LastDownloadedBytes")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("last_downloaded_bytes");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_strikes");
|
||||
|
||||
b.HasIndex("CreatedAt")
|
||||
.HasDatabaseName("ix_strikes_created_at");
|
||||
|
||||
b.HasIndex("JobRunId")
|
||||
.HasDatabaseName("ix_strikes_job_run_id");
|
||||
|
||||
b.HasIndex("DownloadItemId", "Type")
|
||||
.HasDatabaseName("ix_strikes_download_item_id_type");
|
||||
|
||||
b.ToTable("strikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.AppEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Events")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_job_runs_job_run_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.Strike", "Strike")
|
||||
.WithMany()
|
||||
.HasForeignKey("StrikeId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_events_strikes_strike_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
|
||||
b.Navigation("Strike");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.Events.ManualEvent", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("ManualEvents")
|
||||
.HasForeignKey("JobRunId")
|
||||
.HasConstraintName("fk_manual_events_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.Strike", b =>
|
||||
{
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.DownloadItem", "DownloadItem")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("DownloadItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_download_items_download_item_id");
|
||||
|
||||
b.HasOne("Cleanuparr.Persistence.Models.State.JobRun", "JobRun")
|
||||
.WithMany("Strikes")
|
||||
.HasForeignKey("JobRunId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_strikes_job_runs_job_run_id");
|
||||
|
||||
b.Navigation("DownloadItem");
|
||||
|
||||
b.Navigation("JobRun");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.DownloadItem", b =>
|
||||
{
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Cleanuparr.Persistence.Models.State.JobRun", b =>
|
||||
{
|
||||
b.Navigation("Events");
|
||||
|
||||
b.Navigation("ManualEvents");
|
||||
|
||||
b.Navigation("Strikes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Cleanuparr.Persistence.Migrations.Events
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEpisodeIdToSeekerCommandTracker : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "episode_id",
|
||||
table: "seeker_command_trackers",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0L);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "episode_id",
|
||||
table: "seeker_command_trackers");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -556,6 +556,10 @@ namespace Cleanuparr.Persistence.Migrations.Events
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<long>("EpisodeId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("episode_id");
|
||||
|
||||
b.Property<Guid>("EventId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("event_id");
|
||||
|
||||
@@ -31,10 +31,16 @@ public sealed record SeekerCommandTracker
|
||||
public Guid EventId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The external item ID that was searched (movieId or seriesId)
|
||||
/// The library-level item that was searched (seriesId, movieId, albumId or bookId)
|
||||
/// </summary>
|
||||
public long ExternalItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For Sonarr episode-level searches, the episode that was searched.
|
||||
/// 0 when the search targeted a whole season, movie, album or book.
|
||||
/// </summary>
|
||||
public long EpisodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Display name of the item that was searched
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { test, expect } from '../fixtures/base';
|
||||
import { indexerMock } from '../helpers/live-arr';
|
||||
import {
|
||||
RUN_TAG,
|
||||
SONARR,
|
||||
TRANSITION_TIMEOUT,
|
||||
TRIGGERED_SEARCH_TIMEOUT,
|
||||
arrangeInstance,
|
||||
arrangeQueueCleaner,
|
||||
createStallRule,
|
||||
listSearchEvents,
|
||||
resetLiveArrState,
|
||||
teardownInstances,
|
||||
teardownQueueCleaner,
|
||||
triggerSeeker,
|
||||
waitForArrQueue,
|
||||
} from '../helpers/seeker-live';
|
||||
import { grabbableRelease } from '../helpers/mocks/torznab-stubs';
|
||||
|
||||
/**
|
||||
* What a replacement search records once the arr grabs for it.
|
||||
*
|
||||
* Cover for #774, so it deliberately avoids S01E01.
|
||||
* The seed gives that episode id 1, the same number as its series,
|
||||
* and a series id then reads the same as an episode id.
|
||||
* S01E02 has episode id 2, which tells the two apart.
|
||||
*/
|
||||
|
||||
const SETTLE_MS = 30_000;
|
||||
|
||||
/** The rule's floor is above one, so the cleaner needs this many runs to remove. */
|
||||
const MAX_STRIKES = 3;
|
||||
|
||||
const EPISODE = 'Agatha.All.Along.S01E02.1080p.WEB-DL.DDP5.1.H.264';
|
||||
|
||||
test.describe('Seeker replacement grabs', () => {
|
||||
test.beforeEach(async () => {
|
||||
await resetLiveArrState();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ api }) => {
|
||||
await teardownQueueCleaner(api);
|
||||
await teardownInstances(api);
|
||||
await resetLiveArrState();
|
||||
});
|
||||
|
||||
test('records the Sonarr grab on the replacement search event', async ({ api }) => {
|
||||
test.setTimeout(300_000);
|
||||
|
||||
const first = grabbableRelease(SONARR.searchMode, `${EPISODE}-E2EA${RUN_TAG}`, SONARR.category);
|
||||
await indexerMock.stubMany(first.mappings);
|
||||
|
||||
await arrangeQueueCleaner(api);
|
||||
|
||||
const instanceId = await arrangeInstance(api, SONARR);
|
||||
await triggerSeeker(api);
|
||||
await waitForArrQueue(SONARR.arr, first.downloadId, TRIGGERED_SEARCH_TIMEOUT);
|
||||
|
||||
await createStallRule(api, MAX_STRIKES);
|
||||
|
||||
// One strike per run, and the removal happens on the run that reaches the limit.
|
||||
for (let strike = 0; strike <= MAX_STRIKES; strike++) {
|
||||
const triggered = await api.jobs.trigger('QueueCleaner');
|
||||
expect(triggered.status).toBeLessThan(300);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3_000));
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await SONARR.arr.refreshMonitoredDownloads();
|
||||
return (await SONARR.arr.queue()).some((record) => record.downloadId === first.downloadId);
|
||||
},
|
||||
{ timeout: SETTLE_MS },
|
||||
)
|
||||
.toBe(false);
|
||||
|
||||
// Cleanuparr deletes with blocklist=true, so the replacement needs a release of its own.
|
||||
const replacement = grabbableRelease(SONARR.searchMode, `${EPISODE}-E2EB${RUN_TAG}`, SONARR.category);
|
||||
await indexerMock.resetAll();
|
||||
await indexerMock.stubMany(replacement.mappings);
|
||||
|
||||
await triggerSeeker(api);
|
||||
await waitForArrQueue(SONARR.arr, replacement.downloadId, TRIGGERED_SEARCH_TIMEOUT);
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const events = await listSearchEvents(api, instanceId);
|
||||
return events.find((event) => event.searchReason === 'Replacement')?.searchStatus;
|
||||
},
|
||||
{ timeout: TRANSITION_TIMEOUT },
|
||||
)
|
||||
.toBe('Completed');
|
||||
|
||||
const events = await listSearchEvents(api, instanceId);
|
||||
const replacementEvent = events.find((event) => event.searchReason === 'Replacement');
|
||||
|
||||
expect(replacementEvent?.grabbedItems).toContain(replacement.title);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user