From a0789b33094c9d5ff6d45907435ad6e31d00548c Mon Sep 17 00:00:00 2001 From: Dash <2391085-dash.peters@users.noreply.gitlab.com> Date: Sat, 11 May 2024 15:59:25 -0700 Subject: [PATCH 1/6] Plex webhook episode detection for non-episode videos Use title + episode number (or TVDB season + episode #) for non-Episode type videos. --- Shoko.Server/API/v2/Modules/PlexWebhook.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Shoko.Server/API/v2/Modules/PlexWebhook.cs b/Shoko.Server/API/v2/Modules/PlexWebhook.cs index 437dc6330..c442862d4 100644 --- a/Shoko.Server/API/v2/Modules/PlexWebhook.cs +++ b/Shoko.Server/API/v2/Modules/PlexWebhook.cs @@ -182,7 +182,19 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) if (episodeType != EpisodeType.Episode || metadata.Index == 0) //metadata.index = 0 when it's something else. { - return (null, anime); //right now no clean way to detect the episode. I could do by title. + var nameMatches = anime + .GetAnimeEpisodes().Where(a => a.AniDB_Episode != null) + .Where(a => a.EpisodeTypeEnum == episodeType) + .Where(a => a.Title.Equals(metadata.Title)) + .Where(a => a.AniDB_Episode.EpisodeNumber == episodeNumber + || (a.TvDBEpisode?.SeasonNumber == series && a.TvDBEpisode?.EpisodeNumber == episodeNumber)).ToList(); + + if (nameMatches.Count == 1) return (nameMatches.First(), anime); + + _logger.LogInformation( + $"Unable to work out the metadata for {metadata.Guid} using title {metadata.Title}."); + + return (null, anime); } @@ -204,7 +216,7 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) //catch all _logger.LogInformation( - $"Unable to work out the metadata for {metadata.Guid}, this might be a clash of multipl episodes linked, but no tvdb link."); + $"Unable to work out the metadata for {metadata.Guid}, this might be a clash of multiple episodes linked, but no tvdb link."); return (null, anime); } From 56ffc8ba2dc3a26fdb241d4f54857648ab7d3bf0 Mon Sep 17 00:00:00 2001 From: flashmeow <33206210+LordFlashmeow@users.noreply.github.com> Date: Sun, 12 May 2024 14:12:14 -0700 Subject: [PATCH 2/6] Use Plex API to get file path for webhooks --- Shoko.Server/API/v2/Modules/PlexWebhook.cs | 73 ++++++++++++------- .../Plex/Collection/SVR_PlexLibrary.cs | 11 ++- 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/Shoko.Server/API/v2/Modules/PlexWebhook.cs b/Shoko.Server/API/v2/Modules/PlexWebhook.cs index c442862d4..38195309b 100644 --- a/Shoko.Server/API/v2/Modules/PlexWebhook.cs +++ b/Shoko.Server/API/v2/Modules/PlexWebhook.cs @@ -14,13 +14,15 @@ using Shoko.Models.Enums; using Shoko.Models.Plex.Collection; using Shoko.Models.Plex.Connections; -using Shoko.Models.Plex.Libraries; +using Shoko.Models.Plex.TVShow; using Shoko.Models.Server; using Shoko.Server.API.v2.Models.core; using Shoko.Server.Extensions; using Shoko.Server.Models; using Shoko.Server.Plex; +using Shoko.Server.Plex.Collection; using Shoko.Server.Plex.Libraries; +using Shoko.Server.Plex.TVShow; using Shoko.Server.Providers.TraktTV; using Shoko.Server.Repositories; using Shoko.Server.Scheduling; @@ -60,20 +62,29 @@ public ActionResult WebhookPost([FromForm] [ModelBinder(BinderType = typeof(Plex } _logger.LogTrace($"{payload.Event}: {payload.Metadata?.Guid}"); + + var user = User; + user ??= RepoFactory.JMMUser.GetAll().FirstOrDefault(u => payload.Account.Title.FindIn(u.GetPlexUsers())); + if (user == null) + { + _logger.LogInformation("Unable to determine who \"{AccountTitle}\" is in Shoko, make sure this is set under user settings in Desktop", payload.Account.Title); + return Ok(); //At this point in time, we don't want to scrobble for unknown users + } + switch (payload.Event) { case "media.scrobble": - Scrobble(payload, User); + Scrobble(payload, user); break; case "media.resume": case "media.play": - TraktScrobble(payload, ScrobblePlayingStatus.Start); + TraktScrobble(payload, ScrobblePlayingStatus.Start, user); break; case "media.pause": - TraktScrobble(payload, ScrobblePlayingStatus.Pause); + TraktScrobble(payload, ScrobblePlayingStatus.Pause, user); break; case "media.stop": - TraktScrobble(payload, ScrobblePlayingStatus.Stop); + TraktScrobble(payload, ScrobblePlayingStatus.Stop, user); break; } @@ -84,15 +95,15 @@ public ActionResult WebhookPost([FromForm] [ModelBinder(BinderType = typeof(Plex #region Plex events [NonAction] - private void TraktScrobble(PlexEvent evt, ScrobblePlayingStatus type) + private void TraktScrobble(PlexEvent evt, ScrobblePlayingStatus type, JMMUser user) { var metadata = evt.Metadata; - var (episode, _) = GetEpisode(metadata); + var (episode, _) = GetEpisode(metadata, user); if (episode == null) return; var vl = RepoFactory.VideoLocal.GetByAniDBEpisodeID(episode.AniDB_EpisodeID).FirstOrDefault(); - if (vl == null || vl.Duration == 0) return; + if (vl == null || vl.Duration == 0) return; var per = 100 * (metadata.ViewOffset / @@ -109,7 +120,7 @@ private void TraktScrobble(PlexEvent evt, ScrobblePlayingStatus type) private void Scrobble(PlexEvent data, SVR_JMMUser user) { var metadata = data.Metadata; - var (episode, anime) = GetEpisode(metadata); + var (episode, anime) = GetEpisode(metadata, user); if (episode == null) { _logger.LogInformation( @@ -119,12 +130,6 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) _logger.LogTrace("Got anime: {Anime}, ep: {EpisodeNumber}", anime, episode.AniDB_Episode.EpisodeNumber); - user ??= RepoFactory.JMMUser.GetAll().FirstOrDefault(u => data.Account.Title.FindIn(u.GetPlexUsers())); - if (user == null) - { - _logger.LogInformation("Unable to determine who \"{AccountTitle}\" is in Shoko, make sure this is set under user settings in Desktop", data.Account.Title); - return; //At this point in time, we don't want to scrobble for unknown users - } episode.ToggleWatchedStatus(true, true, FromUnixTime(metadata.LastViewedAt), false, user.JMMUserID, true); @@ -135,7 +140,7 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) #endregion [NonAction] - private (SVR_AnimeEpisode, SVR_AnimeSeries) GetEpisode(PlexEvent.PlexMetadata metadata) + private (SVR_AnimeEpisode, SVR_AnimeSeries) GetEpisode(PlexEvent.PlexMetadata metadata, JMMUser user) { var guid = new Uri(metadata.Guid); if (guid.Scheme != "com.plexapp.agents.shoko" && guid.Scheme != "com.plexapp.agents.shokorelay") @@ -179,20 +184,29 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) break; } + var key = metadata.Key.Split("/").LastOrDefault(); // '/library/metadata/{key}' + + var plexEpisode = (SVR_Episode)GetPlexEpisodeData(key, user); + var episode = plexEpisode?.AnimeEpisode; + if (episodeType != EpisodeType.Episode || metadata.Index == 0) //metadata.index = 0 when it's something else. { - var nameMatches = anime - .GetAnimeEpisodes().Where(a => a.AniDB_Episode != null) - .Where(a => a.EpisodeTypeEnum == episodeType) - .Where(a => a.Title.Equals(metadata.Title)) - .Where(a => a.AniDB_Episode.EpisodeNumber == episodeNumber - || (a.TvDBEpisode?.SeasonNumber == series && a.TvDBEpisode?.EpisodeNumber == episodeNumber)).ToList(); - - if (nameMatches.Count == 1) return (nameMatches.First(), anime); + if (episode == null) + { + _logger.LogInformation( + $"Failed to get anime episode from plex using key {metadata.Key}."); + return (null, anime); + } + + if (episode.EpisodeTypeEnum == episodeType + && anime.GetAnimeEpisodes().Contains(episode)) + { + return (episode, anime); + } _logger.LogInformation( - $"Unable to work out the metadata for {metadata.Guid} using title {metadata.Title}."); + $"Unable to work out the metadata for {metadata.Guid}."); return (null, anime); } @@ -285,7 +299,7 @@ public async Task SyncForUser(int id) [Authorize] [HttpGet("libraries")] - public ActionResult GetLibraries() + public ActionResult GetLibraries() { var result = CallPlexHelper(h => h.GetDirectories()); @@ -347,6 +361,13 @@ private T CallPlexHelper(Func act) return act(PlexHelper.GetForUser(user)); } + [NonAction] + private Episode GetPlexEpisodeData(string ratingKey, JMMUser user) + { + var helper = PlexHelper.GetForUser(user); + return new SVR_PlexLibrary(helper).GetEpisode(ratingKey).FirstOrDefault(); + } + #region plexapi #pragma warning disable 0649 diff --git a/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs b/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs index 69b7d745a..c4fb0d36e 100644 --- a/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs +++ b/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json; +using Newtonsoft.Json; using Shoko.Models.Plex; using Shoko.Models.Plex.Collection; using Shoko.Models.Plex.TVShow; @@ -23,4 +23,13 @@ public Episode[] GetEpisodes() .DeserializeObject>(data, Helper.SerializerSettings) .Container.Metadata; } + + public Episode[] GetEpisode(string ratingKey) + { + var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{ratingKey}").GetAwaiter() + .GetResult(); + return JsonConvert + .DeserializeObject>(data, Helper.SerializerSettings) + .Container.Metadata; + } } From ef7c2819f04cfd2bd17587c65c8d54ef565b170c Mon Sep 17 00:00:00 2001 From: flashmeow <33206210+LordFlashmeow@users.noreply.github.com> Date: Sun, 12 May 2024 15:05:59 -0700 Subject: [PATCH 3/6] Support filtering by file size in GetByFilename --- Shoko.Server/Plex/TVShow/SVR_Episode.cs | 4 ++-- Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Shoko.Server/Plex/TVShow/SVR_Episode.cs b/Shoko.Server/Plex/TVShow/SVR_Episode.cs index 0069b79c1..3726c1833 100644 --- a/Shoko.Server/Plex/TVShow/SVR_Episode.cs +++ b/Shoko.Server/Plex/TVShow/SVR_Episode.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using Shoko.Models.Plex.TVShow; using Shoko.Server.Models; using Shoko.Server.Repositories; @@ -15,7 +15,7 @@ public SVR_Episode(PlexHelper helper) } public SVR_AnimeEpisode AnimeEpisode => - RepoFactory.AnimeEpisode.GetByFilename(Path.GetFileName(Media[0].Part[0].File)); + RepoFactory.AnimeEpisode.GetByFilename(Path.GetFileName(Media[0].Part[0].File), Media[0].Part[0].Size); public void Unscrobble() { diff --git a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs index aa2ba949f..f613071bc 100644 --- a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -56,8 +56,9 @@ public SVR_AnimeEpisode GetByAniDBEpisodeID(int epid) /// Get the AnimeEpisode /// /// The filename of the anime to search for. + /// The size of the file in bytes /// the AnimeEpisode given the file information - public SVR_AnimeEpisode GetByFilename(string name) + public SVR_AnimeEpisode GetByFilename(string name, long? size = null) { if (string.IsNullOrEmpty(name)) { @@ -68,6 +69,7 @@ public SVR_AnimeEpisode GetByFilename(string name) .Where(v => name.Equals(v?.FilePath?.Split(Path.DirectorySeparatorChar).LastOrDefault(), StringComparison.InvariantCultureIgnoreCase)) .Select(a => RepoFactory.VideoLocal.GetByID(a.VideoLocalID)).Where(a => a != null) + .Where(a => size == null || a.FileSize == size) .SelectMany(a => GetByHash(a.Hash)).ToArray(); var ep = eps.FirstOrDefault(a => a.AniDB_Episode.EpisodeType == (int)EpisodeType.Episode); return ep ?? eps.FirstOrDefault(); From 9df5c1baff0eeeef8f5a1cd628a2f0c671c729f9 Mon Sep 17 00:00:00 2001 From: Dash <33206210+LordFlashmeow@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:01:47 -0700 Subject: [PATCH 4/6] Apply suggestions from code review Co-authored-by: Cazzar --- Shoko.Server/API/v2/Modules/PlexWebhook.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Shoko.Server/API/v2/Modules/PlexWebhook.cs b/Shoko.Server/API/v2/Modules/PlexWebhook.cs index 38195309b..e5ea781d5 100644 --- a/Shoko.Server/API/v2/Modules/PlexWebhook.cs +++ b/Shoko.Server/API/v2/Modules/PlexWebhook.cs @@ -194,19 +194,16 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) { if (episode == null) { - _logger.LogInformation( - $"Failed to get anime episode from plex using key {metadata.Key}."); + _logger.LogInformation($"Failed to get anime episode from plex using key {metadata.Key}."); return (null, anime); } - if (episode.EpisodeTypeEnum == episodeType - && anime.GetAnimeEpisodes().Contains(episode)) + if (episode.EpisodeTypeEnum == episodeType && anime.GetAnimeEpisodes().Contains(episode)) { return (episode, anime); } - _logger.LogInformation( - $"Unable to work out the metadata for {metadata.Guid}."); + _logger.LogInformation($"Unable to work out the metadata for {metadata.Guid}."); return (null, anime); } @@ -299,7 +296,7 @@ public async Task SyncForUser(int id) [Authorize] [HttpGet("libraries")] - public ActionResult GetLibraries() + public ActionResult GetLibraries() { var result = CallPlexHelper(h => h.GetDirectories()); From e57fb93a2c44c7873a7a1d23f6a89ddadf9690a0 Mon Sep 17 00:00:00 2001 From: flashmeow <33206210+LordFlashmeow@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:27:03 -0700 Subject: [PATCH 5/6] Changes based on comments --- Shoko.Server/API/v2/Modules/PlexWebhook.cs | 7 ++++--- Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs | 3 +-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Shoko.Server/API/v2/Modules/PlexWebhook.cs b/Shoko.Server/API/v2/Modules/PlexWebhook.cs index e5ea781d5..debcdc47f 100644 --- a/Shoko.Server/API/v2/Modules/PlexWebhook.cs +++ b/Shoko.Server/API/v2/Modules/PlexWebhook.cs @@ -14,6 +14,7 @@ using Shoko.Models.Enums; using Shoko.Models.Plex.Collection; using Shoko.Models.Plex.Connections; +using Shoko.Models.Plex.Libraries; using Shoko.Models.Plex.TVShow; using Shoko.Models.Server; using Shoko.Server.API.v2.Models.core; @@ -98,7 +99,7 @@ public ActionResult WebhookPost([FromForm] [ModelBinder(BinderType = typeof(Plex private void TraktScrobble(PlexEvent evt, ScrobblePlayingStatus type, JMMUser user) { var metadata = evt.Metadata; - var (episode, _) = GetEpisode(metadata, user); + var (episode, _) = GetEpisodeForUser(metadata, user); if (episode == null) return; @@ -120,7 +121,7 @@ private void TraktScrobble(PlexEvent evt, ScrobblePlayingStatus type, JMMUser us private void Scrobble(PlexEvent data, SVR_JMMUser user) { var metadata = data.Metadata; - var (episode, anime) = GetEpisode(metadata, user); + var (episode, anime) = GetEpisodeForUser(metadata, user); if (episode == null) { _logger.LogInformation( @@ -140,7 +141,7 @@ private void Scrobble(PlexEvent data, SVR_JMMUser user) #endregion [NonAction] - private (SVR_AnimeEpisode, SVR_AnimeSeries) GetEpisode(PlexEvent.PlexMetadata metadata, JMMUser user) + private (SVR_AnimeEpisode, SVR_AnimeSeries) GetEpisodeForUser(PlexEvent.PlexMetadata metadata, JMMUser user) { var guid = new Uri(metadata.Guid); if (guid.Scheme != "com.plexapp.agents.shoko" && guid.Scheme != "com.plexapp.agents.shokorelay") diff --git a/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs b/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs index c4fb0d36e..d0e6af891 100644 --- a/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs +++ b/Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs @@ -17,8 +17,7 @@ public SVR_PlexLibrary(PlexHelper helper) public Episode[] GetEpisodes() { - var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{RatingKey}/allLeaves").GetAwaiter() - .GetResult(); + var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{RatingKey}/allLeaves").Result; return JsonConvert .DeserializeObject>(data, Helper.SerializerSettings) .Container.Metadata; From d122259513b874932a53531e9bb676b6a8f40416 Mon Sep 17 00:00:00 2001 From: flashmeow <33206210+LordFlashmeow@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:37:23 -0700 Subject: [PATCH 6/6] Split GetByFilenameAndSize from GetByFilename --- Shoko.Server/Plex/TVShow/SVR_Episode.cs | 2 +- .../Cached/AnimeEpisodeRepository.cs | 30 +++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Shoko.Server/Plex/TVShow/SVR_Episode.cs b/Shoko.Server/Plex/TVShow/SVR_Episode.cs index 3726c1833..fa99e94e4 100644 --- a/Shoko.Server/Plex/TVShow/SVR_Episode.cs +++ b/Shoko.Server/Plex/TVShow/SVR_Episode.cs @@ -15,7 +15,7 @@ public SVR_Episode(PlexHelper helper) } public SVR_AnimeEpisode AnimeEpisode => - RepoFactory.AnimeEpisode.GetByFilename(Path.GetFileName(Media[0].Part[0].File), Media[0].Part[0].Size); + RepoFactory.AnimeEpisode.GetByFilenameAndSize(Path.GetFileName(Media[0].Part[0].File), Media[0].Part[0].Size); public void Unscrobble() { diff --git a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs index f613071bc..cc40a711a 100644 --- a/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs +++ b/Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs @@ -55,10 +55,9 @@ public SVR_AnimeEpisode GetByAniDBEpisodeID(int epid) /// /// Get the AnimeEpisode /// - /// The filename of the anime to search for. - /// The size of the file in bytes - /// the AnimeEpisode given the file information - public SVR_AnimeEpisode GetByFilename(string name, long? size = null) + /// The filename of the anime to search for. Only the characters after the last directory separator are considered + /// the AnimeEpisode given the file name + public SVR_AnimeEpisode GetByFilename(string name) { if (string.IsNullOrEmpty(name)) { @@ -69,12 +68,33 @@ public SVR_AnimeEpisode GetByFilename(string name, long? size = null) .Where(v => name.Equals(v?.FilePath?.Split(Path.DirectorySeparatorChar).LastOrDefault(), StringComparison.InvariantCultureIgnoreCase)) .Select(a => RepoFactory.VideoLocal.GetByID(a.VideoLocalID)).Where(a => a != null) - .Where(a => size == null || a.FileSize == size) .SelectMany(a => GetByHash(a.Hash)).ToArray(); var ep = eps.FirstOrDefault(a => a.AniDB_Episode.EpisodeType == (int)EpisodeType.Episode); return ep ?? eps.FirstOrDefault(); } + /// + /// Get the AnimeEpisode + /// + /// The filename of the anime to search for. Only the characters after the last directory separator are considered + /// The size of the file in bytes + /// the AnimeEpisode given the file name and size + public SVR_AnimeEpisode GetByFilenameAndSize(string name, long size) + { + if (string.IsNullOrEmpty(name)) + { + return null; + } + var eps = RepoFactory.VideoLocalPlace.GetAll() + .Where(v => name.Equals(v?.FilePath?.Split(Path.DirectorySeparatorChar).LastOrDefault(), + StringComparison.InvariantCultureIgnoreCase)) + .Select(a => RepoFactory.VideoLocal.GetByID(a.VideoLocalID)).Where(a => a != null) + .Where(a => a.FileSize == size) + .SelectMany(a => GetByHash(a.Hash)).ToArray(); + var ep = eps.FirstOrDefault(a => a.AniDB_Episode.EpisodeType == (int)EpisodeType.Episode); + return ep ?? eps.FirstOrDefault(); + } + /// /// Get all the AnimeEpisode records associate with an AniDB_File record