Skip to content

Commit

Permalink
refactor: add abstract user services
Browse files Browse the repository at this point in the history
- Added a new `IUserService` to allow plugins to query for users. We may or may not expose some events in the future, if the need arises for any plugins to react to user added/updated/removed  events.

- Added a new `IUserDataService` to allow plugins to read `IVideoUserData`s and write `VideoUserDataUpdate`s, and to react to user data save events.

- Removed the `WatchedStatusService` in favour of the `IUserDataService`, and moved all usage of it onto the new abstraction service.
  • Loading branch information
revam committed Feb 13, 2025
1 parent da48c9d commit daa670d
Show file tree
Hide file tree
Showing 28 changed files with 959 additions and 574 deletions.
25 changes: 25 additions & 0 deletions Shoko.Plugin.Abstractions/DataModels/IUserData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Shoko.Plugin.Abstractions.DataModels.Shoko;

namespace Shoko.Plugin.Abstractions.DataModels;

/// <summary>
/// Represents user-specific data.
/// </summary>
public interface IUserData
{
/// <summary>
/// Gets the ID of the user.
/// </summary>
int UserID { get; }

/// <summary>
/// Gets the date and time when the user data was last updated.
/// </summary>
DateTime LastUpdatedAt { get; }

/// <summary>
/// Gets the user associated with this video data.
/// </summary>
IShokoUser User { get; }
}
34 changes: 34 additions & 0 deletions Shoko.Plugin.Abstractions/DataModels/IVideoUserData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace Shoko.Plugin.Abstractions.DataModels;

/// <summary>
/// Represents user-specific data associated with a video.
/// </summary>
public interface IVideoUserData : IUserData
{
/// <summary>
/// Gets the ID of the video.
/// </summary>
int VideoID { get; }

/// <summary>
/// Gets the number of times the video has been played.
/// </summary>
int PlaybackCount { get; }

/// <summary>
/// Gets the position in the video where playback was last resumed.
/// </summary>
TimeSpan ResumePosition { get; }

/// <summary>
/// Gets the date and time when the video was last played.
/// </summary>
DateTime? LastPlayedAt { get; }

/// <summary>
/// Gets the video associated with this user data.
/// </summary>
IVideo Video { get; }
}
17 changes: 17 additions & 0 deletions Shoko.Plugin.Abstractions/DataModels/Shoko/IShokoUser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Shoko.Plugin.Abstractions.DataModels.Shoko;

/// <summary>
/// Shoko user.
/// </summary>
public interface IShokoUser
{
/// <summary>
/// Unique ID.
/// </summary>
int ID { get; }

/// <summary>
/// Username.
/// </summary>
string Username { get; }
}
31 changes: 31 additions & 0 deletions Shoko.Plugin.Abstractions/DataModels/VideoUserDataUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;

namespace Shoko.Plugin.Abstractions.DataModels;

/// <summary>
/// Input data for updating a <see cref="IVideoUserData"/>.
/// </summary>
/// <param name="userData">An existing <see cref="IVideoUserData"/> to derive data from.</param>
public class VideoUserDataUpdate(IVideoUserData? userData = null)
{
/// <summary>
/// Override or set the number of times the video has been played.
/// </summary>
public int? PlaybackCount { get; set; } = userData?.PlaybackCount;

/// <summary>
/// Override or set the position at which the video should be resumed.
/// </summary>
public TimeSpan? ResumePosition { get; set; } = userData?.ResumePosition;

/// <summary>
/// Override or set the date and time the video was last played.
/// </summary>
public DateTime? LastPlayedAt { get; set; } = userData?.LastPlayedAt;

/// <summary>
/// Override when the data was last updated. If not set, then the current
/// time will be used.
/// </summary>
public DateTime? LastUpdatedAt { get; set; } = userData?.LastUpdatedAt;
}
48 changes: 48 additions & 0 deletions Shoko.Plugin.Abstractions/Enums/UserDataSaveReason.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

namespace Shoko.Plugin.Abstractions.Enums;

/// <summary>
/// The reason the user data is being saved.
/// </summary>
public enum UserDataSaveReason
{
/// <summary>
/// The user data is being saved for no specific reason.
/// </summary>
None = 0,

/// <summary>
/// The user data is being saved because of user interaction.
/// </summary>
UserInteraction,

/// <summary>
/// The user data is being saved when playback of a video started.
/// </summary>
PlaybackStart,

/// <summary>
/// The user data is being saved when playback of a video was paused.
/// </summary>
PlaybackPause,

/// <summary>
/// The user data is being saved when playback of a video was resumed.
/// </summary>
PlaybackResume,

/// <summary>
/// The user data is being saved when playback of a video progressed.
/// </summary>
PlaybackProgress,

/// <summary>
/// The user data is being saved when playback of a video ended.
/// </summary>
PlaybackEnd,

/// <summary>
/// The user data is being saved during an import from AniDB.
/// </summary>
AnidbImport,
}
47 changes: 47 additions & 0 deletions Shoko.Plugin.Abstractions/Events/VideoUserDataSavedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@

using Shoko.Plugin.Abstractions.DataModels;
using Shoko.Plugin.Abstractions.DataModels.Shoko;
using Shoko.Plugin.Abstractions.Enums;

namespace Shoko.Plugin.Abstractions.Events;

/// <summary>
/// Dispatched when video user data was updated.
/// </summary>
public class VideoUserDataSavedEventArgs
{
/// <summary>
/// The reason why the user data was updated.
/// </summary>
public UserDataSaveReason Reason { get; }

/// <summary>
/// The user which had their data updated.
/// </summary>
public IShokoUser User { get; }

/// <summary>
/// The video which had its user data updated.
/// </summary>
public IVideo Video { get; }

/// <summary>
/// The updated video user data.
/// </summary>
public IVideoUserData UserData { get; }

/// <summary>
/// Initializes a new instance of the <see cref="VideoUserDataSavedEventArgs"/> class.
/// </summary>
/// <param name="reason">The reason why the user data was updated.</param>
/// <param name="user">The user which had their data updated.</param>
/// <param name="video">The video which had its user data updated.</param>
/// <param name="userData">The updated video user data.</param>
public VideoUserDataSavedEventArgs(UserDataSaveReason reason, IShokoUser user, IVideo video, IVideoUserData userData)
{
Reason = reason;
User = user;
Video = video;
UserData = userData;
}
}
95 changes: 95 additions & 0 deletions Shoko.Plugin.Abstractions/Services/IUserDataService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@


using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Shoko.Plugin.Abstractions.DataModels;
using Shoko.Plugin.Abstractions.DataModels.Shoko;
using Shoko.Plugin.Abstractions.Enums;
using Shoko.Plugin.Abstractions.Events;

namespace Shoko.Plugin.Abstractions.Services;
/// <summary>
/// User data service.
/// </summary>
public interface IUserDataService
{
#region Video User Data

/// <summary>
/// Dispatched when video user data is saved.
/// </summary>
event EventHandler<VideoUserDataSavedEventArgs> VideoUserDataSaved;

/// <summary>
/// Gets user video data for the given user and video.
/// </summary>
/// <param name="userID">The user ID.</param>
/// <param name="videoID">The video ID.</param>
/// <returns>The user video data.</returns>
IVideoUserData? GetVideoUserData(int userID, int videoID);

/// <summary>
/// Gets all user video data for the given user.
/// </summary>
/// <param name="userID">The user ID.</param>
/// <returns>The user video data.</returns>
IReadOnlyList<IVideoUserData> GetVideoUserDataForUser(int userID);

/// <summary>
/// Gets all user video data for the given video.
/// </summary>
/// <param name="videoID">The video ID.</param>
/// <returns>A list of user video data.</returns>
IReadOnlyList<IVideoUserData> GetVideoUserDataForVideo(int videoID);

/// <summary>
/// Sets the video watch status.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="video">The video.</param>
/// <param name="watched">Optional. If set to <c>true</c> the video is watched; otherwise, <c>false</c>.</param>
/// <param name="watchedAt">Optional. The watched at.</param>
/// <param name="reason">Optional. The reason why the video watch status was updated.</param>
/// <param name="updateStatsNow">if set to <c>true</c> will update the series stats immediately after saving.</param>
/// <exception cref="ArgumentNullException">The <paramref name="user"/> is null.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="video"/> is null.</exception>
/// <returns>A task.</returns>
Task SetVideoWatchedStatus(IShokoUser user, IVideo video, bool watched = true, DateTime? watchedAt = null, UserDataSaveReason reason = UserDataSaveReason.None, bool updateStatsNow = true);

/// <summary>
/// Saves the video user data.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="video">The video.</param>
/// <param name="userDataUpdate">The user data update.</param>
/// <param name="reason">The reason why the user data was updated.</param>
/// <param name="updateStatsNow">if set to <c>true</c> will update the series stats immediately after saving.</param>
/// <exception cref="ArgumentNullException">The <paramref name="user"/> is null.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="video"/> is null.</exception>
/// <returns>The task containing the new or updated user video data.</returns>
Task<IVideoUserData> SaveVideoUserData(IShokoUser user, IVideo video, VideoUserDataUpdate userDataUpdate, UserDataSaveReason reason = UserDataSaveReason.None, bool updateStatsNow = true);

#endregion

#region Episode User Data

/// <summary>
/// Sets the episode watch status.
/// </summary>
/// <remarks>
/// Attempting to set the episode watch status for an episode without files will result in a no-op.
/// </remarks>
/// <param name="user">The user.</param>
/// <param name="episode">The episode.</param>
/// <param name="watched">Optional. If set to <c>true</c> the episode is watched; otherwise, <c>false</c>.</param>
/// <param name="watchedAt">Optional. The watched at.</param>
/// <param name="reason">Optional. The reason why the episode watch status was updated.</param>
/// <param name="updateStatsNow">if set to <c>true</c> will update the series stats immediately after saving.</param>
/// <exception cref="ArgumentNullException">The <paramref name="user"/> is null.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="episode"/> is null.</exception>
/// <returns>The task containing the result if the episode watch status was updated.</returns>
Task<bool> SetEpisodeWatchedStatus(IShokoUser user, IShokoEpisode episode, bool watched = true, DateTime? watchedAt = null, UserDataSaveReason reason = UserDataSaveReason.None, bool updateStatsNow = true);

#endregion
}
23 changes: 23 additions & 0 deletions Shoko.Plugin.Abstractions/Services/IUserService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Linq;
using Shoko.Plugin.Abstractions.DataModels.Shoko;

namespace Shoko.Plugin.Abstractions.Services;

/// <summary>
/// User manager.
/// </summary>
public interface IUserService
{
/// <summary>
/// Get all users as a queryable list.
/// </summary>
/// <returns>The users.</returns>
IQueryable<IShokoUser> GetUsers();

/// <summary>
/// Get a user by ID.
/// </summary>
/// <param name="id">The ID.</param>
/// <returns>The user.</returns>
IShokoUser? GetUserByID(int id);
}
17 changes: 6 additions & 11 deletions Shoko.Server/API/v0/Controllers/PlexWebhook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
using Shoko.Models.Plex.Connections;
using Shoko.Models.Plex.Libraries;
using Shoko.Models.Server;
using Shoko.Plugin.Abstractions.Services;
using Shoko.Server.Extensions;
using Shoko.Server.Models;
using Shoko.Server.Plex;
using Shoko.Server.Providers.TraktTV;
using Shoko.Server.Repositories;
using Shoko.Server.Scheduling;
using Shoko.Server.Scheduling.Jobs.Plex;
using Shoko.Server.Services;
using Shoko.Server.Settings;
using Shoko.Server.Utilities;

#if DEBUG
using Shoko.Models.Plex.Collection;
using Shoko.Server.Plex.Libraries;
Expand All @@ -41,16 +42,14 @@ public class PlexWebhook : BaseController
private readonly ILogger<PlexWebhook> _logger;
private readonly TraktTVHelper _traktHelper;
private readonly ISchedulerFactory _schedulerFactory;
private readonly AnimeSeriesService _seriesService;
private readonly AnimeGroupService _groupService;
private readonly IUserDataService _userDataService;

public PlexWebhook(ILogger<PlexWebhook> logger, TraktTVHelper traktHelper, ISettingsProvider settingsProvider, ISchedulerFactory schedulerFactory, AnimeSeriesService seriesService, AnimeGroupService groupService) : base(settingsProvider)
public PlexWebhook(ILogger<PlexWebhook> logger, TraktTVHelper traktHelper, ISettingsProvider settingsProvider, ISchedulerFactory schedulerFactory, IUserDataService userDataService) : base(settingsProvider)
{
_logger = logger;
_traktHelper = traktHelper;
_schedulerFactory = schedulerFactory;
_seriesService = seriesService;
_groupService = groupService;
_userDataService = userDataService;
}

//The second one is to just make sure
Expand Down Expand Up @@ -133,11 +132,7 @@ private async Task Scrobble(PlexEvent data, SVR_JMMUser user)
return; //At this point in time, we don't want to scrobble for unknown users
}

var watchedService = Utils.ServiceContainer.GetRequiredService<WatchedStatusService>();
await watchedService.SetWatchedStatus(episode, true, true, FromUnixTime(metadata.LastViewedAt), false, user.JMMUserID,
true);
_seriesService.UpdateStats(anime, true, false);
_groupService.UpdateStatsFromTopLevel(anime.AnimeGroup?.TopLevelAnimeGroup, true, true);
await _userDataService.SetEpisodeWatchedStatus(user, episode, true, FromUnixTime(metadata.LastViewedAt));
}

#endregion
Expand Down
Loading

0 comments on commit daa670d

Please sign in to comment.