feat(actions): implement plugin-registrable action system#1383
Draft
revam wants to merge 3 commits into
Draft
Conversation
Implemented the plugin-registrable actions system as specified by the RFC discussion #1380, allowing plugins (and core) to register discrete, invokable units of work that appear in the Actions menu and can be triggered by ID via the API. **Abstractions layer** (Shoko.Abstractions.Action): - Added `IExecutableAction` base interface with `Name`, `Description`, `Category` (hard enum `ActionCategory`, defaults to `PluginInferred`), and `RequiresConfirmation` properties. - Added scope sub-interfaces: `IExecutableGlobalAction`, `IExecutableGlobalUserAction`, `IExecutableGroupAction`, `IExecutableGroupUserAction`, `IExecutableSeriesAction`, `IExecutableSeriesUserAction`, `IExecutableEpisodeAction`, `IExecutableEpisodeUserAction`. - Added `ExecutableActionInfo` DTO with stable UUIDv5 derived from class name + plugin ID namespace. - Added `IActionService` for registration (`AddParts`), discovery (`GetActions`), direct execution, and queued scheduling. - Expanded `ActionCategory` enum with explicit hex values (e.g. `Maintenance = 0xF1`, `Mischievous = 0xF2`, `Destructive = 0xFE`, `PluginInferred = 0xFF`) and doc comments on all members. - Added `ActionScope` enum with `Global = 1`, `GlobalUser = 2`, `Group = 3`, `GroupUser = 4`, `Series = 5`, `SeriesUser = 6`, `Episode = 7`, `EpisodeUser = 8`. **Registration & DI**: - `PluginManager.InitPlugins()` calls `IActionService.AddParts()` via `GetExports<IExecutableAction>`, same pattern as other plugin extension points. - Action instances are transient via `ActivatorUtilities`; lifetime management is the plugin's responsibility. - Stores action types, not cached instances; fresh instance created from type on each execution via `IPluginManager.GetExport<T>`. **Service layer** (`ActionService`): - Implements `IActionService` with `_registeredActions` list. - `AddParts` resolves plugin info, derives UUIDs, collects all matching scopes via `typeof(T).IsAssignableFrom`, and propagates `RequiresConfirmation` - `GetActions` accepts `IEnumerable<ActionScope>?`, `IEnumerable<ActionCategory>?`, `IEnumerable<string>?` for set-based filtering (scope overlap, category/category-name contains). - Result ordering: category value (ensures deterministic core category ordering), then category name (ensures deterministic plugin ordering), then name (ensures deterministic action ordering). - `Execute*` methods resolve a fresh instance each time, validate scope presence upfront, throw `InvalidOperationException` on mismatch - `ScheduleExecuteOf*` validates scope before enqueuing via `ExecuteActionJob`. - Added 7 new methods migrated from the API controller: `RunImport_SyncVotes`, `PurgeAllTmdbLinks`, `UpdateAniDBCalendar`, `PlexSyncAll`, `AddAllManualLinksToMyList`, `GetAniDBNotifications`, `AVDumpMismatchedFiles`. **Global action implementations** (Shoko.Server.Services.Actions): - 35 `IExecutableGlobalAction` classes across 5 domains: - Import (3): RunImport, ImportNewFiles, RemoveMissingFiles - AniDB (11): DownloadMissingAniDBAnimeData, Creators, UpdateAllAniDBInfo, UpdateMissingAniDBFileInfo, RefreshAniDBMovedFiles, VerifyAllRelations, SyncAniDBMyList, SyncAniDBVotes, UpdateAniDBCalendar, AddAllManualLinksToMyList, GetAniDBNotifications, AVDumpMismatchedFiles - TMDB (10): SearchForMatches, UpdateAllTMDBMovies (+WithImages), UpdateAllTMDBShows (+WithImages), DownloadMissingPeople, PurgeAllUnusedMovies, Collections, Shows, ShowAlternateOrderings, PurgeAllLinks - Images (3): UpdateAllImages, ValidateAllImages, PurgeAllUnusedTMDBImages - Maintenance (8): UpdateAllMediaInfo, UpdateSeriesStats, RecreateAllGroups, RenameAllGroups, CreateMissingSeries, PurgeAllUsedReleases, PurgeAllUnusedReleases, PlexSyncAll - Destructive actions set `RequiresConfirmation = true` - `RunImportAction` and `SyncAnidbMyListAction` use `IJobFactory` **Series action implementations** (Shoko.Server.Services.Actions.*.Series): - 17 `IExecutableSeriesAction` classes across 4 sub-namespaces: - AniDB.Series (4): UpdateAnidbInfo (cache+remote fallback), UpdateAnidbInfoRemote (remote, respects checks), UpdateAnidbInfoForce (bypasses time check + HTTP bans, requires confirmation), UpdateAnidbInfoXmlCache (local XML only) - Tmdb.Series (7): AutoSearchTmdbMatch, UpdateTmdbInfo, UpdateTmdbImagesForce, RefreshTmdbMovies, DownloadTmdbMovieImages, AutoMatchTmdbEpisodes, ResetTmdbEpisodeMappings (requires confirmation) - Import.Series (3): RescanFiles, RehashFiles, RelocateFiles - Destructive.Series (3): DeleteKeepFiles, DeleteRemoveFiles, DeleteAllData (all require confirmation) - Actions are self-contained: they inject services directly and operate on the `IShokoSeries` passed at invocation **Group action implementations** (Shoko.Server.Services.Actions.Import.Group): - 3 `IExecutableGroupAction` classes mirroring the series file actions: RescanGroupFiles, RehashGroupFiles, RelocateGroupFiles - Each iterates all series within the group to operate on their files **Queue job** (`ExecuteActionJob`): - Stores `ActionID`, `UserID`, `GroupID`, `AnimeID`, and `EpisodeID` - Uses `PostInit` to hydrate display names via `IActionService`, `IUserService`, and `IMetadataService` - `ScopeToName` infers the scope label from which of (GroupID, AnimeID, EpisodeID, UserID) are set, covering all 8 scope combinations - Routes to the appropriate `IActionService.Execute*` method **API layer** (v3): - `GET /api/v3/Action` — lists global/user-scoped actions grouped by `CategoryName` (B2 shape), filtered by user permissions - `POST /api/v3/Action/{id}/Execute?scope=` — invokes a global/user action; scope defaults by role priority; admin check for Global/Series/Group/Episode, any auth for user-scoped variants - `GET /api/v3/Action/Group/{groupID}/Actions` — lists group-scoped actions - `POST /api/v3/Action/Group/{groupID}/Actions/{id}/Execute?scope=` — invokes a group/group-user action with group existence validation - `GET /api/v3/Action/Series/{seriesID}/Actions` — lists series-scoped actions - `POST /api/v3/Action/Series/{seriesID}/Actions/{id}/Execute?scope=` — invokes a series/series-user action with series existence validation - `GET /api/v3/Action/Episode/{episodeID}/Actions` — lists episode-scoped actions (hookup for future) - `POST /api/v3/Action/Episode/{episodeID}/Actions/{id}/Execute?scope=` — invokes an episode/episode-user action with episode existence validation - Scope-aware auth: non-admin users only see `GlobalUser`, `GroupUser`, `SeriesUser`, `EpisodeUser` scopes in listings - Plugin info included in `ActionInfo` response DTO - Old hardcoded endpoints marked `[Obsolete]`; implementation delegated to `ActionService` **Differences from RFC #1380**: - `Execute` remains parameterless for all scopes; entity/user context supplied by caller via `IActionService` overloads (consistent with Gap 18 resolution) - Scope enum is flat (not flags), with 8 discrete values for each entity level (Global/Group/Series/Episode) and their user variants — extends the RFC's original Global/Series scope pair - `GetActions` uses set-based overlap filtering instead of single scope matching - `CategoryName` string on `ExecutableActionInfo` carries the display label; no separate group description in API responses - `RequiresConfirmation` added as a UI hint for destructive actions (not specified in RFC but a natural follow-on) - Direct execution (`Execute*` methods) exists on the service for internal use, but the API always schedules (per RFC Gap 15) - Actions store types, not instances; fresh instance per invocation - `ActionCategory` default changed from `Mischievous` to `PluginInferred`, so actions without an explicit category are grouped by their plugin's name Refs: #1380
6e989ca to
5b36ce8
Compare
|
Member
@revam Why do we need that many?
Spec needs to be changed if that is really the case.
Either code or spec needs to be changed.
looks like a good change spec needs to be updated. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Implemented the plugin-registrable actions system as specified by the RFC
discussion #1380, allowing plugins (and core) to register discrete, invokable
units of work that appear in the Actions menu and can be triggered by ID via the
API.
Abstractions layer (Shoko.Abstractions.Action):
IExecutableActionbase interface withName,Description,Category(hard enumActionCategory, defaults toPluginInferred), andRequiresConfirmationproperties.IExecutableGlobalAction,IExecutableGlobalUserAction,IExecutableGroupAction,IExecutableGroupUserAction,IExecutableSeriesAction,IExecutableSeriesUserAction,IExecutableEpisodeAction,IExecutableEpisodeUserAction.ExecutableActionInfoDTO with stable UUIDv5 derived from class name +plugin ID namespace.
IActionServicefor registration (AddParts), discovery(
GetActions), direct execution, and queued scheduling.ActionCategoryenum with explicit hex values (e.g.Maintenance = 0xF1,Mischievous = 0xF2,Destructive = 0xFE,PluginInferred = 0xFF) and doc comments on all members.ActionScopeenum withGlobal = 1,GlobalUser = 2,Group = 3,GroupUser = 4,Series = 5,SeriesUser = 6,Episode = 7,EpisodeUser = 8.Registration & DI:
PluginManager.InitPlugins()callsIActionService.AddParts()viaGetExports<IExecutableAction>, same pattern as other plugin extensionpoints.
ActivatorUtilities; lifetime managementis the plugin's responsibility.
each execution via
IPluginManager.GetExport<T>.Service layer (
ActionService):IActionServicewith_registeredActionslist.AddPartsresolves plugin info, derives UUIDs, collects all matching scopesvia
typeof(T).IsAssignableFrom, and propagatesRequiresConfirmation-GetActionsacceptsIEnumerable<ActionScope>?,IEnumerable<ActionCategory>?,IEnumerable<string>?for set-based filtering(scope overlap, category/category-name contains).
ordering), then category name (ensures deterministic plugin ordering), then
name (ensures deterministic action ordering).
Execute*methods resolve a fresh instance each time, validate scope presenceupfront, throw
InvalidOperationExceptionon mismatch -ScheduleExecuteOf*validates scope before enqueuing via
ExecuteActionJob.RunImport_SyncVotes,PurgeAllTmdbLinks,UpdateAniDBCalendar,PlexSyncAll,AddAllManualLinksToMyList,GetAniDBNotifications,AVDumpMismatchedFiles.Global action implementations (Shoko.Server.Services.Actions):
IExecutableGlobalActionclasses across 5 domains:UpdateAllAniDBInfo, UpdateMissingAniDBFileInfo,
RefreshAniDBMovedFiles, VerifyAllRelations, SyncAniDBMyList,
SyncAniDBVotes, UpdateAniDBCalendar,
AddAllManualLinksToMyList, GetAniDBNotifications,
AVDumpMismatchedFiles
UpdateAllTMDBShows (+WithImages), DownloadMissingPeople,
PurgeAllUnusedMovies, Collections, Shows,
ShowAlternateOrderings, PurgeAllLinks
PurgeAllUnusedTMDBImages
RecreateAllGroups, RenameAllGroups, CreateMissingSeries,
PurgeAllUsedReleases, PurgeAllUnusedReleases, PlexSyncAll
RequiresConfirmation = trueRunImportActionandSyncAnidbMyListActionuseIJobFactorySeries action implementations (Shoko.Server.Services.Actions.*.Series):
IExecutableSeriesActionclasses across 4 sub-namespaces:UpdateAnidbInfoRemote (remote, respects checks),
UpdateAnidbInfoForce (bypasses time check + HTTP bans,
requires confirmation), UpdateAnidbInfoXmlCache (local XML only)
UpdateTmdbImagesForce, RefreshTmdbMovies,
DownloadTmdbMovieImages, AutoMatchTmdbEpisodes,
ResetTmdbEpisodeMappings (requires confirmation)
DeleteAllData (all require confirmation)
operate on the
IShokoSeriespassed at invocationGroup action implementations (Shoko.Server.Services.Actions.Import.Group):
IExecutableGroupActionclasses mirroring the series file actions:RescanGroupFiles, RehashGroupFiles, RelocateGroupFiles
Queue job (
ExecuteActionJob):ActionID,UserID,GroupID,AnimeID, andEpisodeIDPostInitto hydrate display names viaIActionService,IUserService, andIMetadataServiceScopeToNameinfers the scope label from which of(GroupID, AnimeID, EpisodeID, UserID) are set,
covering all 8 scope combinations
IActionService.Execute*methodAPI layer (v3):
GET /api/v3/Action— lists global/user-scoped actions groupedby
CategoryName(B2 shape), filtered by user permissionsPOST /api/v3/Action/{id}/Execute?scope=— invokes aglobal/user action; scope defaults by role priority; admin check
for Global/Series/Group/Episode, any auth for user-scoped variants
GET /api/v3/Action/Group/{groupID}/Actions— listsgroup-scoped actions
POST /api/v3/Action/Group/{groupID}/Actions/{id}/Execute?scope=— invokes a group/group-user action with group existence validation
GET /api/v3/Action/Series/{seriesID}/Actions— listsseries-scoped actions
POST /api/v3/Action/Series/{seriesID}/Actions/{id}/Execute?scope=— invokes a series/series-user action with series existence
validation
GET /api/v3/Action/Episode/{episodeID}/Actions— listsepisode-scoped actions (hookup for future)
POST /api/v3/Action/Episode/{episodeID}/Actions/{id}/Execute?scope=— invokes an episode/episode-user action with episode existence
validation
GlobalUser,GroupUser,SeriesUser,EpisodeUserscopes in listingsActionInforesponse DTO[Obsolete]; implementationdelegated to
ActionServiceDifferences from RFC #1380:
Executeremains parameterless for all scopes; entity/user contextsupplied by caller via
IActionServiceoverloads (consistent withGap 18 resolution)
entity level (Global/Group/Series/Episode) and their user variants
— extends the RFC's original Global/Series scope pair
GetActionsuses set-based overlap filtering instead of singlescope matching
CategoryNamestring onExecutableActionInfocarries the displaylabel; no separate group description in API responses
RequiresConfirmationadded as a UI hint for destructive actions(not specified in RFC but a natural follow-on)
Execute*methods) exists on the service forinternal use, but the API always schedules (per RFC Gap 15)
ActionCategorydefault changed fromMischievoustoPluginInferred, so actions without an explicit category aregrouped by their plugin's name
Refs: #1380