Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plex webhook episode detection for non-episode videos #1126

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions Shoko.Server/API/v2/Modules/PlexWebhook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
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;
Expand Down Expand Up @@ -60,20 +63,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()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of passing the user around and having additional parameters, would it be possibly better to have a cached JMMUser property on the payload?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that be done? (Sorry, I haven't worked with C# before)

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;
}

Expand All @@ -84,15 +96,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, _) = GetEpisodeForUser(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 /
Expand All @@ -109,7 +121,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) = GetEpisodeForUser(metadata, user);
if (episode == null)
{
_logger.LogInformation(
Expand All @@ -119,12 +131,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);
Expand All @@ -135,7 +141,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) 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")
Expand Down Expand Up @@ -179,10 +185,28 @@ 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;
Comment on lines +188 to +191
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add this with a guard against people not having a valid plex connection?

The original design was that the webhook did not require user plex login, so it was entirely working off it's own data.

Happy to add this in, but I think it should be at least guarded to allow for users to not have their own plex config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify the scenario you're describing? The user has plex webhooks enabled, but hasn't linked their plex account to Shoko?

If that's the case, I suppose I could fall back original behavior of only matching episodes.


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.
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}.");

return (null, anime);
}


Expand All @@ -204,7 +228,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);
}

Expand Down Expand Up @@ -335,6 +359,13 @@ private T CallPlexHelper<T>(Func<PlexHelper, T> 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
Expand Down
12 changes: 10 additions & 2 deletions Shoko.Server/Plex/Collection/SVR_PlexLibrary.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;
using Shoko.Models.Plex;
using Shoko.Models.Plex.Collection;
using Shoko.Models.Plex.TVShow;
Expand All @@ -17,7 +17,15 @@ public SVR_PlexLibrary(PlexHelper helper)

public Episode[] GetEpisodes()
{
var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{RatingKey}/allLeaves").GetAwaiter()
var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{RatingKey}/allLeaves").Result;
return JsonConvert
.DeserializeObject<MediaContainer<MediaContainer>>(data, Helper.SerializerSettings)
.Container.Metadata;
}

public Episode[] GetEpisode(string ratingKey)
{
var (_, data) = Helper.RequestFromPlexAsync($"/library/metadata/{ratingKey}").GetAwaiter()
.GetResult();
return JsonConvert
.DeserializeObject<MediaContainer<MediaContainer>>(data, Helper.SerializerSettings)
Expand Down
4 changes: 2 additions & 2 deletions Shoko.Server/Plex/TVShow/SVR_Episode.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.IO;
using System.IO;
using Shoko.Models.Plex.TVShow;
using Shoko.Server.Models;
using Shoko.Server.Repositories;
Expand All @@ -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.GetByFilenameAndSize(Path.GetFileName(Media[0].Part[0].File), Media[0].Part[0].Size);

public void Unscrobble()
{
Expand Down
28 changes: 25 additions & 3 deletions Shoko.Server/Repositories/Cached/AnimeEpisodeRepository.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -55,8 +55,8 @@ public SVR_AnimeEpisode GetByAniDBEpisodeID(int epid)
/// <summary>
/// Get the AnimeEpisode
/// </summary>
/// <param name="name">The filename of the anime to search for.</param>
/// <returns>the AnimeEpisode given the file information</returns>
/// <param name="name">The filename of the anime to search for. Only the characters after the last directory separator are considered</param>
/// <returns>the AnimeEpisode given the file name</returns>
public SVR_AnimeEpisode GetByFilename(string name)
{
if (string.IsNullOrEmpty(name))
Expand All @@ -73,6 +73,28 @@ public SVR_AnimeEpisode GetByFilename(string name)
return ep ?? eps.FirstOrDefault();
}

/// <summary>
/// Get the AnimeEpisode
/// </summary>
/// <param name="name">The filename of the anime to search for. Only the characters after the last directory separator are considered</param>
/// <param name="size">The size of the file in bytes</param>
/// <returns>the AnimeEpisode given the file name and size</returns>
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();
}


/// <summary>
/// Get all the AnimeEpisode records associate with an AniDB_File record
Expand Down