Skip to content

Commit

Permalink
fix: more through refresh dispatching logic
Browse files Browse the repository at this point in the history
Made the refresh dispatching logic in the event dispatcher service more through, by distincting the input lists we get from the core, and also tracking which entities has been updated and their parents, and only firing the event if the entity or it's parents haven't been updated since the last usage stalled event or if the clear plugin cache task is ran.
  • Loading branch information
revam committed Nov 17, 2024
1 parent dc584c5 commit 0892eff
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 35 deletions.
141 changes: 107 additions & 34 deletions Shokofin/Events/EventDispatchService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand All @@ -25,7 +26,11 @@
using ImageType = MediaBrowser.Model.Entities.ImageType;
using LibraryOptions = MediaBrowser.Model.Configuration.LibraryOptions;
using MetadataRefreshMode = MediaBrowser.Controller.Providers.MetadataRefreshMode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using Timer = System.Timers.Timer;
using TvEpisode = MediaBrowser.Controller.Entities.TV.Episode;
using TvSeason = MediaBrowser.Controller.Entities.TV.Season;
using TvSeries = MediaBrowser.Controller.Entities.TV.Series;

namespace Shokofin.Events;

Expand All @@ -51,6 +56,8 @@ public class EventDispatchService

private readonly ILogger<EventDispatchService> Logger;

private readonly UsageTracker UsageTracker;

private int ChangesDetectionSubmitterCount = 0;

private readonly Timer ChangesDetectionTimer;
Expand All @@ -64,6 +71,8 @@ public class EventDispatchService
// It's so magical that it matches the magical value in the library monitor in JF core. 🪄
private const int MagicalDelayValue = 45000;

private readonly ConcurrentDictionary<Guid, bool> RecentlyUpdatedEntitiesDict = new();

private static readonly TimeSpan DetectChangesThreshold = TimeSpan.FromSeconds(5);

public EventDispatchService(
Expand All @@ -76,7 +85,8 @@ public EventDispatchService(
LibraryScanWatcher libraryScanWatcher,
IFileSystem fileSystem,
IDirectoryService directoryService,
ILogger<EventDispatchService> logger
ILogger<EventDispatchService> logger,
UsageTracker usageTracker
)
{
ApiManager = apiManager;
Expand All @@ -89,16 +99,25 @@ ILogger<EventDispatchService> logger
FileSystem = fileSystem;
DirectoryService = directoryService;
Logger = logger;
UsageTracker = usageTracker;
UsageTracker.Stalled += OnStalled;
ChangesDetectionTimer = new() { AutoReset = true, Interval = TimeSpan.FromSeconds(4).TotalMilliseconds };
ChangesDetectionTimer.Elapsed += OnIntervalElapsed;
}

~EventDispatchService()
{

UsageTracker.Stalled -= OnStalled;
ChangesDetectionTimer.Elapsed -= OnIntervalElapsed;
}

private void OnStalled(object? sender, EventArgs eventArgs)
{
Clear();
}

public void Clear() => RecentlyUpdatedEntitiesDict.Clear();

#region Event Detection

public IDisposable RegisterEventSubmitter()
Expand Down Expand Up @@ -596,16 +615,25 @@ private async Task<int> ProcessSeriesEvents(ShowInfo showInfo, List<IMetadataUpd
var animeEvent = changes.Find(e => e.Kind is BaseItemKind.Series || e.Kind is BaseItemKind.Episode && e.Reason is UpdateReason.Removed);
if (animeEvent is not null) {
var shows = LibraryManager
.GetItemList(
new() {
IncludeItemTypes = [BaseItemKind.Series],
HasAnyProviderId = new Dictionary<string, string> { { ShokoSeriesId.Name, showInfo.Id } },
DtoOptions = new(true),
},
true
)
.GetItemList(new() {
IncludeItemTypes = [BaseItemKind.Series],
HasAnyProviderId = new Dictionary<string, string> { { ShokoSeriesId.Name, showInfo.Id } },
DtoOptions = new(true),
})
.DistinctBy(s => s.Id)
.OfType<TvSeries>()
.ToList();
foreach (var show in shows) {
if (RecentlyUpdatedEntitiesDict.ContainsKey(show.Id)) {
Logger.LogTrace("Show {ShowName} is already being updated. (Check=1,Show={ShowId},Series={SeriesId})", show.Name, show.Id, showInfo.Id);
continue;
}

if (!RecentlyUpdatedEntitiesDict.TryAdd(show.Id, true)) {
Logger.LogTrace("Show {ShowName} is already being updated. (Check=2,Show={ShowId},Series={SeriesId})", show.Name, show.Id, showInfo.Id);
continue;
}

Logger.LogInformation("Refreshing show {ShowName}. (Show={ShowId},Series={SeriesId})", show.Name, show.Id, showInfo.Id);
await show.RefreshMetadata(new(DirectoryService) {
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
Expand Down Expand Up @@ -635,16 +663,31 @@ await show.RefreshMetadata(new(DirectoryService) {
.ToList();
foreach (var seasonInfo in seasonList) {
var seasons = LibraryManager
.GetItemList(
new() {
IncludeItemTypes = [BaseItemKind.Season],
HasAnyProviderId = new Dictionary<string, string> { { ShokoSeriesId.Name, seasonInfo.Id } },
DtoOptions = new(true),
},
true
)
.GetItemList(new() {
IncludeItemTypes = [BaseItemKind.Season],
HasAnyProviderId = new Dictionary<string, string> { { ShokoSeriesId.Name, seasonInfo.Id } },
DtoOptions = new(true),
})
.DistinctBy(s => s.Id)
.OfType<TvSeason>()
.ToList();
foreach (var season in seasons) {
var showId = season.SeriesId;
if (RecentlyUpdatedEntitiesDict.ContainsKey(showId)) {
Logger.LogTrace("Show is already being updated. (Check=1,Show={ShowId},Season={SeasonId},Series={SeriesId})", showId, season.Id, seasonInfo.Id);
continue;
}

if (RecentlyUpdatedEntitiesDict.ContainsKey(season.Id)) {
Logger.LogTrace("Season is already being updated. (Check=2,Show={ShowId},Season={SeasonId},Series={SeriesId})", showId, season.Id, seasonInfo.Id);
continue;
}

if (!RecentlyUpdatedEntitiesDict.TryAdd(season.Id, true)) {
Logger.LogTrace("Season is already being updated. (Check=3,Show={ShowId},Season={SeasonId},Series={SeriesId})", showId, season.Id, seasonInfo.Id);
continue;
}

Logger.LogInformation("Refreshing season {SeasonName}. (Season={SeasonId},Series={SeriesId},ExtraSeries={ExtraIds})", season.Name, season.Id, seasonInfo.Id, seasonInfo.ExtraIds);
await season.RefreshMetadata(new(DirectoryService) {
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
Expand All @@ -666,16 +709,37 @@ await season.RefreshMetadata(new(DirectoryService) {
.ToList();
foreach (var episodeInfo in episodeList) {
var episodes = LibraryManager
.GetItemList(
new() {
IncludeItemTypes = [BaseItemKind.Episode],
HasAnyProviderId = new Dictionary<string, string> { { ShokoEpisodeId.Name, episodeInfo.Id } },
DtoOptions = new(true),
},
true
)
.GetItemList(new() {
IncludeItemTypes = [BaseItemKind.Episode],
HasAnyProviderId = new Dictionary<string, string> { { ShokoEpisodeId.Name, episodeInfo.Id } },
DtoOptions = new(true),
})
.DistinctBy(e => e.Id)
.OfType<TvEpisode>()
.ToList();
foreach (var episode in episodes) {
var showId = episode.SeriesId;
var seasonId = episode.SeasonId;
if (RecentlyUpdatedEntitiesDict.ContainsKey(showId)) {
Logger.LogTrace("Show is already being updated. (Check=1,Show={ShowId},Season={SeasonId},Episode={EpisodeId},Episode={EpisodeId},Series={SeriesId})", showId, seasonId, episode.Id, episodeInfo.Id, episodeInfo.SeriesId);
continue;
}

if (RecentlyUpdatedEntitiesDict.ContainsKey(seasonId)) {
Logger.LogTrace("Season is already being updated. (Check=2,Show={ShowId},Season={SeasonId},Episode={EpisodeId},Episode={EpisodeId},Series={SeriesId})", showId, seasonId, episode.Id, episodeInfo.Id, episodeInfo.SeriesId);
continue;
}

if (RecentlyUpdatedEntitiesDict.ContainsKey(episode.Id)) {
Logger.LogTrace("Episode is already being updated. (Check=3,Show={ShowId},Season={SeasonId},Episode={EpisodeId},Episode={EpisodeId},Series={SeriesId})", showId, seasonId, episode.Id, episodeInfo.Id, episodeInfo.SeriesId);
continue;
}

if (!RecentlyUpdatedEntitiesDict.TryAdd(episode.Id, true)) {
Logger.LogTrace("Episode is already being updated. (Check=4,Show={ShowId},Season={SeasonId},Episode={EpisodeId},Episode={EpisodeId},Series={SeriesId})", showId, seasonId, episode.Id, episodeInfo.Id, episodeInfo.SeriesId);
continue;
}

Logger.LogInformation("Refreshing episode {EpisodeName}. (Episode={EpisodeId},Episode={EpisodeId},Series={SeriesId})", episode.Name, episode.Id, episodeInfo.Id, episodeInfo.SeriesId);
await episode.RefreshMetadata(new(DirectoryService) {
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
Expand Down Expand Up @@ -709,16 +773,25 @@ private async Task<int> ProcessMovieEvents(SeasonInfo seasonInfo, List<IMetadata
.ToList();
foreach (var episodeInfo in episodeList) {
var movies = LibraryManager
.GetItemList(
new() {
IncludeItemTypes = [BaseItemKind.Movie],
HasAnyProviderId = new Dictionary<string, string> { { ShokoEpisodeId.Name, episodeInfo.Id } },
DtoOptions = new(true),
},
true
)
.GetItemList(new() {
IncludeItemTypes = [BaseItemKind.Movie],
HasAnyProviderId = new Dictionary<string, string> { { ShokoEpisodeId.Name, episodeInfo.Id } },
DtoOptions = new(true),
})
.DistinctBy(e => e.Id)
.OfType<Movie>()
.ToList();
foreach (var movie in movies) {
if (RecentlyUpdatedEntitiesDict.ContainsKey(movie.Id)) {
Logger.LogTrace("Movie is already being updated. (Check=1,Movie={MovieId},Episode={EpisodeId},Series={SeriesId},ExtraSeries={ExtraIds})", movie.Id, episodeInfo.Id, seasonInfo.Id, seasonInfo.ExtraIds);
continue;
}

if (!RecentlyUpdatedEntitiesDict.TryAdd(movie.Id, true)) {
Logger.LogTrace("Movie is already being updated. (Check=2,Movie={MovieId},Episode={EpisodeId},Series={SeriesId},ExtraSeries={ExtraIds})", movie.Id, episodeInfo.Id, seasonInfo.Id, seasonInfo.ExtraIds);
continue;
}

Logger.LogInformation("Refreshing movie {MovieName}. (Movie={MovieId},Episode={EpisodeId},Series={SeriesId},ExtraSeries={ExtraIds})", movie.Name, movie.Id, episodeInfo.Id, seasonInfo.Id, seasonInfo.ExtraIds);
await movie.RefreshMetadata(new(DirectoryService) {
MetadataRefreshMode = MetadataRefreshMode.FullRefresh,
Expand Down
6 changes: 5 additions & 1 deletion Shokofin/Tasks/ClearPluginCacheTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
using System.Threading.Tasks;
using MediaBrowser.Model.Tasks;
using Shokofin.API;
using Shokofin.Events;
using Shokofin.Resolvers;

namespace Shokofin.Tasks;

/// <summary>
/// Forcefully clear the plugin cache. For debugging and troubleshooting. DO NOT RUN THIS TASK WHILE A LIBRARY SCAN IS RUNNING.
/// </summary>
public class ClearPluginCacheTask(ShokoAPIManager apiManager, ShokoAPIClient apiClient, VirtualFileSystemService vfsService) : IScheduledTask, IConfigurableScheduledTask
public class ClearPluginCacheTask(ShokoAPIManager apiManager, ShokoAPIClient apiClient, VirtualFileSystemService vfsService, EventDispatchService eventDispatchService) : IScheduledTask, IConfigurableScheduledTask
{
/// <inheritdoc />
public string Name => "Clear Plugin Cache";
Expand Down Expand Up @@ -40,6 +41,8 @@ public class ClearPluginCacheTask(ShokoAPIManager apiManager, ShokoAPIClient api

private readonly VirtualFileSystemService _vfsService = vfsService;

private readonly EventDispatchService _eventDispatchService = eventDispatchService;

public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
=> [];

Expand All @@ -48,6 +51,7 @@ public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellat
_apiClient.Clear();
_apiManager.Clear();
_vfsService.Clear();
_eventDispatchService.Clear();
return Task.CompletedTask;
}
}

0 comments on commit 0892eff

Please sign in to comment.