mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
Mostly finalize episode automations
This commit is contained in:
+4
-1
@@ -10,11 +10,13 @@ import (
|
||||
|
||||
type ActivityType string
|
||||
|
||||
// _AUTO activities are for when logic updates something for the user (automations basically).
|
||||
var (
|
||||
ADDED_WATCHED ActivityType = "ADDED_WATCHED"
|
||||
REMOVED_WATCHED ActivityType = "REMOVED_WATCHED"
|
||||
RATING_CHANGED ActivityType = "RATING_CHANGED"
|
||||
STATUS_CHANGED ActivityType = "STATUS_CHANGED"
|
||||
STATUS_CHANGED_AUTO ActivityType = "STATUS_CHANGED_AUTO"
|
||||
THOUGHTS_CHANGED ActivityType = "THOUGHTS_CHANGED"
|
||||
THOUGHTS_REMOVED ActivityType = "THOUGHTS_REMOVED"
|
||||
IMPORTED_WATCHED ActivityType = "IMPORTED_WATCHED"
|
||||
@@ -25,12 +27,13 @@ var (
|
||||
IMPORTED_ADDED_WATCHED_JF ActivityType = "IMPORTED_ADDED_WATCHED_JF"
|
||||
IMPORTED_ADDED_WATCHED_PLEX ActivityType = "IMPORTED_ADDED_WATCHED_PLEX"
|
||||
SEASON_ADDED ActivityType = "SEASON_ADDED"
|
||||
SEASON_ADDED_AUTO ActivityType = "SEASON_ADDED_AUTO" // When a season is added because of a different, but related user action (eg: setting an episode to watched)
|
||||
SEASON_ADDED_AUTO ActivityType = "SEASON_ADDED_AUTO"
|
||||
SEASON_ADDED_JF ActivityType = "SEASON_ADDED_JF"
|
||||
SEASON_ADDED_PLEX ActivityType = "SEASON_ADDED_PLEX"
|
||||
SEASON_REMOVED ActivityType = "SEASON_REMOVED"
|
||||
SEASON_RATING_CHANGED ActivityType = "SEASON_RATING_CHANGED"
|
||||
SEASON_STATUS_CHANGED ActivityType = "SEASON_STATUS_CHANGED"
|
||||
SEASON_STATUS_CHANGED_AUTO ActivityType = "SEASON_STATUS_CHANGED_AUTO"
|
||||
EPISODE_ADDED ActivityType = "EPISODE_ADDED"
|
||||
EPISODE_ADDED_JF ActivityType = "EPISODE_ADDED_JF"
|
||||
EPISODE_ADDED_PLEX ActivityType = "EPISODE_ADDED_PLEX"
|
||||
|
||||
@@ -39,6 +39,8 @@ type WatchedEpisodeAddRequest struct {
|
||||
type WatchedEpisodeAddResponse struct {
|
||||
WatchedEpisodes []WatchedEpisode `json:"watchedEpisodes"`
|
||||
AddedActivity Activity `json:"addedActivity"`
|
||||
// Response from hook
|
||||
EpisodeStatusChangedHookResponse EpisodeStatusChangedHookResponse `json:"episodeStatusChangedHookResponse,omitempty"`
|
||||
}
|
||||
|
||||
// Add/edit a watched episode.
|
||||
@@ -115,14 +117,15 @@ func addWatchedEpisodes(db *gorm.DB, userId uint, ar WatchedEpisodeAddRequest) (
|
||||
}
|
||||
addedActivity, _ = addActivity(db, userId, act)
|
||||
}
|
||||
if ar.Status != "" {
|
||||
slog.Debug("addWatchedEpisodes: Episode status was changed, calling hook.")
|
||||
hookEpisodeStatusChanged(db, userId, ar.WatchedID, ar.SeasonNumber, ar.EpisodeNumber, ar.Status)
|
||||
}
|
||||
return WatchedEpisodeAddResponse{
|
||||
episodeAddResp := WatchedEpisodeAddResponse{
|
||||
WatchedEpisodes: w.WatchedEpisodes,
|
||||
AddedActivity: addedActivity,
|
||||
}, nil
|
||||
}
|
||||
if ar.Status != "" {
|
||||
slog.Debug("addWatchedEpisodes: Episode status was changed, calling hook.")
|
||||
episodeAddResp.EpisodeStatusChangedHookResponse = hookEpisodeStatusChanged(db, userId, ar.WatchedID, ar.SeasonNumber, ar.EpisodeNumber, ar.Status)
|
||||
}
|
||||
return episodeAddResp, nil
|
||||
}
|
||||
|
||||
// Remove a watched episode
|
||||
|
||||
+64
-13
@@ -1,28 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TODO add our own activity when we update a season/show status!
|
||||
|
||||
// TODO might need to use pointers
|
||||
type EpisodeStatusChangedHookResponse struct {
|
||||
// The watched shows status if we modified it.
|
||||
NewShowStatus WatchedStatus `json:"newShowStatus,omitempty"`
|
||||
// The full watched season (if created or modified).
|
||||
WatchedSeason WatchedSeason `json:"watchedSeason,omitempty"`
|
||||
// All activies we have added.
|
||||
AddedActivities []Activity `json:"addedActivities,omitempty"`
|
||||
// All errors (fatal and non-fatal) that were encountered.
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// Called after an episode watched status has been set.
|
||||
func hookEpisodeStatusChanged(db *gorm.DB, userId uint, watchedId uint, seasonNum int, episodeNum int, newEpisodeStatus WatchedStatus) {
|
||||
func hookEpisodeStatusChanged(db *gorm.DB, userId uint, watchedId uint, seasonNum int, episodeNum int, newEpisodeStatus WatchedStatus) EpisodeStatusChangedHookResponse {
|
||||
// 1. Only continue if the episode was not marked dropped.
|
||||
if newEpisodeStatus == DROPPED {
|
||||
slog.Error("hookEpisodeStatusChanged: newEpisodeStatus is DROPPED, not continuing.")
|
||||
return
|
||||
return EpisodeStatusChangedHookResponse{}
|
||||
}
|
||||
|
||||
hookResponse := EpisodeStatusChangedHookResponse{}
|
||||
|
||||
addHookActivity := func(aType ActivityType, data string) {
|
||||
addedActivity, _ := addActivity(db, userId, ActivityAddRequest{WatchedID: watchedId, Type: aType, Data: (data)})
|
||||
hookResponse.AddedActivities = append(hookResponse.AddedActivities, addedActivity)
|
||||
}
|
||||
|
||||
// 2. If the season (this episode is in) has no status or is planned, set season to watching.
|
||||
watchedSeason, err := getWatchedSeason(db, userId, watchedId, seasonNum)
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Cannot continue, failed to get watchedSeason!", "error", err)
|
||||
return
|
||||
return EpisodeStatusChangedHookResponse{Errors: []string{("failed to query db for watched season")}}
|
||||
}
|
||||
// If season not found, create it.
|
||||
if watchedSeason == nil {
|
||||
@@ -31,19 +51,35 @@ func hookEpisodeStatusChanged(db *gorm.DB, userId uint, watchedId uint, seasonNu
|
||||
if newEpisodeStatus == FINISHED {
|
||||
seasonStatus = WATCHING
|
||||
}
|
||||
_, err := addWatchedSeason(db, userId, WatchedSeasonAddRequest{
|
||||
addActivity: SEASON_ADDED_AUTO,
|
||||
WatchedID: watchedId,
|
||||
SeasonNumber: seasonNum,
|
||||
Status: seasonStatus,
|
||||
resp, err := addWatchedSeason(db, userId, WatchedSeasonAddRequest{
|
||||
addActivity: SEASON_ADDED_AUTO,
|
||||
addActivityData: map[string]interface{}{"reason": fmt.Sprintf("Episode %d was set to %s while the season had no status.", episodeNum, newEpisodeStatus)},
|
||||
WatchedID: watchedId,
|
||||
SeasonNumber: seasonNum,
|
||||
Status: seasonStatus,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to add watched season!", "error", err)
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to add watched season")
|
||||
} else {
|
||||
// addWatchedSeason returns all watched seasons, get the one just added. (may be best to retrofit addWatchedSeason later to return id of season/row created)
|
||||
justAddedWatchedSeason, err := getWatchedSeason(db, userId, watchedId, seasonNum)
|
||||
if err != nil {
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to get newly added watched season for response")
|
||||
} else {
|
||||
hookResponse.WatchedSeason = *justAddedWatchedSeason
|
||||
}
|
||||
hookResponse.AddedActivities = append(hookResponse.AddedActivities, resp.AddedActivity)
|
||||
}
|
||||
} else if watchedSeason.Status == "" || watchedSeason.Status == PLANNED {
|
||||
watchedSeason.Status = WATCHING
|
||||
if res := db.Save(watchedSeason); res.Error != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to update season status!", "error", res.Error)
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to update season status")
|
||||
} else {
|
||||
hookResponse.WatchedSeason = *watchedSeason
|
||||
json, _ := json.Marshal(map[string]interface{}{"season": seasonNum, "status": watchedSeason.Status, "reason": fmt.Sprintf("Episode %d was set to %s while the season had no status.", episodeNum, newEpisodeStatus)})
|
||||
addHookActivity(SEASON_STATUS_CHANGED_AUTO, string(json))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,13 +87,18 @@ func hookEpisodeStatusChanged(db *gorm.DB, userId uint, watchedId uint, seasonNu
|
||||
watchedShow, err := getWatchedItemById(db, userId, watchedId)
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to get watched show, cant continue to update show status.", "error", err)
|
||||
return
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to get watched item for show")
|
||||
return hookResponse
|
||||
} else {
|
||||
// Show status shouldn't be empty, but watevs, handle it just incase
|
||||
if watchedShow.Status == "" || watchedShow.Status == PLANNED {
|
||||
watchedShow.Status = WATCHING
|
||||
if res := db.Save(watchedShow); res.Error != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to update show status!", "error", res.Error)
|
||||
} else {
|
||||
hookResponse.NewShowStatus = watchedShow.Status
|
||||
json, _ := json.Marshal(map[string]interface{}{"status": watchedShow.Status, "reason": fmt.Sprintf("S%dE%d was set to %s.", seasonNum, episodeNum, newEpisodeStatus)})
|
||||
addHookActivity(STATUS_CHANGED_AUTO, string(json))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,20 +109,30 @@ func hookEpisodeStatusChanged(db *gorm.DB, userId uint, watchedId uint, seasonNu
|
||||
seasonDetails, err := seasonDetails(tmdbIdStr, seasonNumStr)
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to get season details!", "error", err)
|
||||
return
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to get season details for show")
|
||||
return hookResponse
|
||||
}
|
||||
allEpisodesCount := len(seasonDetails.Episodes)
|
||||
finishedEpisodesCount, err := getNumberOfWatchedEpisodesInSeason(db, userId, watchedId, seasonNum, []WatchedStatus{FINISHED, DROPPED})
|
||||
if err != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to get number of watched episodes in this season!", "error", err)
|
||||
return
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to get number of watched episodes in this season")
|
||||
return hookResponse
|
||||
}
|
||||
slog.Debug("hookEpisodeStatusChanged: Got episode counts.", "allEpisodesCount", allEpisodesCount, "finishedEpisodesCount", finishedEpisodesCount)
|
||||
if finishedEpisodesCount >= int64(allEpisodesCount) {
|
||||
slog.Debug("hookEpisodeStatusChanged: All episodes have been completed (finished or dropped). Marking season finished.")
|
||||
if res := db.Model(&WatchedSeason{}).Where("watched_id = ? AND season_number = ? AND user_id = ?", watchedId, seasonNum, userId).Update("status", FINISHED); res.Error != nil {
|
||||
newStatus := FINISHED
|
||||
if res := db.Model(&WatchedSeason{}).Where("watched_id = ? AND season_number = ? AND user_id = ?", watchedId, seasonNum, userId).Update("status", newStatus); res.Error != nil {
|
||||
slog.Error("hookEpisodeStatusChanged: Failed to update season status to finished:", "error", res.Error.Error())
|
||||
return
|
||||
hookResponse.Errors = append(hookResponse.Errors, "failed to update season status to finished")
|
||||
return hookResponse
|
||||
} else {
|
||||
hookResponse.WatchedSeason.Status = newStatus
|
||||
json, _ := json.Marshal(map[string]interface{}{"season": seasonNum, "status": newStatus, "reason": fmt.Sprintf("The season was deemed completed when episode %d was set to %s.", episodeNum, newEpisodeStatus)})
|
||||
addHookActivity(SEASON_STATUS_CHANGED_AUTO, string(json))
|
||||
}
|
||||
}
|
||||
|
||||
return hookResponse
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ type WatchedSeasonAddRequest struct {
|
||||
Rating int8 `json:"rating"`
|
||||
addActivity ActivityType `json:"-"`
|
||||
addActivityDate time.Time `json:"-"`
|
||||
// Data to add to activity if the season is created.
|
||||
// Combined with data we already add.
|
||||
addActivityData map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
type WatchedSeasonAddResponse struct {
|
||||
@@ -98,7 +101,15 @@ func addWatchedSeason(db *gorm.DB, userId uint, ar WatchedSeasonAddRequest) (Wat
|
||||
}
|
||||
}
|
||||
} else {
|
||||
json, _ := json.Marshal(map[string]interface{}{"season": ar.SeasonNumber, "status": ar.Status, "rating": ar.Rating})
|
||||
actData := map[string]interface{}{"season": ar.SeasonNumber, "status": ar.Status, "rating": ar.Rating}
|
||||
if len(ar.addActivityData) > 0 {
|
||||
for k, v := range ar.addActivityData {
|
||||
if _, ok := ar.addActivityData[k]; ok {
|
||||
actData[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
json, _ := json.Marshal(actData)
|
||||
act := ActivityAddRequest{WatchedID: w.ID, Type: SEASON_ADDED, Data: string(json)}
|
||||
if ar.addActivity != "" {
|
||||
act.Type = ar.addActivity
|
||||
|
||||
+41
-1
@@ -2,6 +2,8 @@
|
||||
import type { Activity } from "@/types";
|
||||
import { getOrdinalSuffix, months, seasonAndEpToReadable } from "./util/helpers";
|
||||
import ActivityEditor from "./ActivityEditor.svelte";
|
||||
import Icon from "./Icon.svelte";
|
||||
import tooltip from "./actions/tooltip";
|
||||
|
||||
export let activity: Activity[] | undefined;
|
||||
export let wListId: number;
|
||||
@@ -36,6 +38,12 @@
|
||||
return `Status Changed to ${toFullTitleCase(a.data)}`;
|
||||
}
|
||||
return "Status Changed";
|
||||
case "STATUS_CHANGED_AUTO":
|
||||
if (a.data) {
|
||||
const data = JSON.parse(a.data);
|
||||
return `Status Changed to ${toFullTitleCase(data.status)}`;
|
||||
}
|
||||
return "Status Changed";
|
||||
case "THOUGHTS_CHANGED":
|
||||
return "Thoughts Changed";
|
||||
case "THOUGHTS_REMOVED":
|
||||
@@ -60,6 +68,7 @@
|
||||
case "IMPORTED_ADDED_WATCHED_PLEX":
|
||||
return "Imported Watch Date";
|
||||
case "SEASON_ADDED":
|
||||
case "SEASON_ADDED_AUTO":
|
||||
if (a.data) {
|
||||
const data = JSON.parse(a.data);
|
||||
return `Season ${data.season} Added as ${toFullTitleCase(data.status)}`;
|
||||
@@ -79,6 +88,7 @@
|
||||
}
|
||||
return "Season Rating Changed";
|
||||
case "SEASON_STATUS_CHANGED":
|
||||
case "SEASON_STATUS_CHANGED_AUTO":
|
||||
if (a.data) {
|
||||
const data = JSON.parse(a.data);
|
||||
return `Changed Season ${data.season} Status to ${toFullTitleCase(data.status)}`;
|
||||
@@ -172,6 +182,16 @@
|
||||
isActivityEditorVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
function getActivityDataParsed(a: Activity) {
|
||||
try {
|
||||
if (a.data) {
|
||||
return JSON.parse(a.data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("getActivityDataParsed: Failed!", err);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isActivityEditorVisible}
|
||||
@@ -197,6 +217,21 @@
|
||||
<span title={d.toDateString()}>{toDayTime(d)}</span>
|
||||
<span>{getMsg(a)}</span>
|
||||
</button>
|
||||
{#if a.type?.endsWith("_AUTO")}
|
||||
{@const data = getActivityDataParsed(a)}
|
||||
<i
|
||||
use:tooltip={{
|
||||
text:
|
||||
data && data.reason
|
||||
? `Automated because ${data.reason}`
|
||||
: "Completed by an automation.",
|
||||
pos: "bot"
|
||||
}}
|
||||
style="width: 20px; height: 20px;"
|
||||
>
|
||||
<Icon i="sparkles" wh={20} />
|
||||
</i>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/each}
|
||||
@@ -235,6 +270,11 @@
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
all: unset;
|
||||
display: flex;
|
||||
@@ -254,7 +294,7 @@
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:last-of-type {
|
||||
background-color: $accent-color;
|
||||
color: $text-color;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -332,6 +332,12 @@
|
||||
d="M680-840v80h-40v327L313-760l-33-33v-47h400ZM480-40l-40-40v-240H240v-80l80-80v-46L56-792l56-56 736 736-58 56-264-264h-6v240l-40 40Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else if i === "sparkles"}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={wh} height={wh} viewBox="0 0 512 512">
|
||||
<path
|
||||
d="M208 512a24.84 24.84 0 01-23.34-16l-39.84-103.6a16.06 16.06 0 00-9.19-9.19L32 343.34a25 25 0 010-46.68l103.6-39.84a16.06 16.06 0 009.19-9.19L184.66 144a25 25 0 0146.68 0l39.84 103.6a16.06 16.06 0 009.19 9.19l103 39.63a25.49 25.49 0 0116.63 24.1 24.82 24.82 0 01-16 22.82l-103.6 39.84a16.06 16.06 0 00-9.19 9.19L231.34 496A24.84 24.84 0 01208 512zm66.85-254.84zM88 176a14.67 14.67 0 01-13.69-9.4l-16.86-43.84a7.28 7.28 0 00-4.21-4.21L9.4 101.69a14.67 14.67 0 010-27.38l43.84-16.86a7.31 7.31 0 004.21-4.21L74.16 9.79A15 15 0 0186.23.11a14.67 14.67 0 0115.46 9.29l16.86 43.84a7.31 7.31 0 004.21 4.21l43.84 16.86a14.67 14.67 0 010 27.38l-43.84 16.86a7.28 7.28 0 00-4.21 4.21l-16.86 43.84A14.67 14.67 0 0188 176zM400 256a16 16 0 01-14.93-10.26l-22.84-59.37a8 8 0 00-4.6-4.6l-59.37-22.84a16 16 0 010-29.86l59.37-22.84a8 8 0 004.6-4.6l22.67-58.95a16.45 16.45 0 0113.17-10.57 16 16 0 0116.86 10.15l22.84 59.37a8 8 0 004.6 4.6l59.37 22.84a16 16 0 010 29.86l-59.37 22.84a8 8 0 00-4.6 4.6l-22.84 59.37A16 16 0 01400 256z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -53,6 +53,47 @@
|
||||
} else {
|
||||
wEntry.activity = [r.data.addedActivity];
|
||||
}
|
||||
try {
|
||||
const epHookResp = r?.data?.episodeStatusChangedHookResponse;
|
||||
if (epHookResp) {
|
||||
if (epHookResp.errors && epHookResp.errors.length > 0) {
|
||||
console.error(
|
||||
"episodeStatusChangedHookResponse contained errors! All possible automations may not have been completed.",
|
||||
epHookResp.errors
|
||||
);
|
||||
notify({
|
||||
type: "error",
|
||||
text: "Some automations have failed, check console for more info."
|
||||
});
|
||||
}
|
||||
if (epHookResp.addedActivities && epHookResp.addedActivities.length > 0) {
|
||||
wEntry.activity.push(...epHookResp.addedActivities);
|
||||
}
|
||||
if (epHookResp.watchedSeason) {
|
||||
if (!wEntry.watchedSeasons) {
|
||||
wEntry.watchedSeasons = [epHookResp.watchedSeason];
|
||||
} else {
|
||||
const watchedSeasonIdx = wEntry.watchedSeasons.findIndex(
|
||||
(s) => s.id === epHookResp.watchedSeason?.id
|
||||
);
|
||||
if (watchedSeasonIdx === -1) {
|
||||
wEntry.watchedSeasons.push(epHookResp.watchedSeason);
|
||||
} else {
|
||||
wEntry.watchedSeasons[watchedSeasonIdx] = epHookResp.watchedSeason;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (epHookResp.newShowStatus) {
|
||||
wEntry.status = epHookResp.newShowStatus;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to process episodeStatusChangedHookResponse", err);
|
||||
notify({
|
||||
type: "error",
|
||||
text: "Failed to process automation response, check console for more info."
|
||||
});
|
||||
}
|
||||
watchedList.update((w) => w);
|
||||
notify({ id: nid, text: `Saved!`, type: "success" });
|
||||
}
|
||||
|
||||
+10
-1
@@ -39,7 +39,8 @@ export type Icon =
|
||||
| "film"
|
||||
| "tv"
|
||||
| "pin"
|
||||
| "unpin";
|
||||
| "unpin"
|
||||
| "sparkles";
|
||||
|
||||
export type Theme = "light" | "dark";
|
||||
|
||||
@@ -145,6 +146,14 @@ export interface WatchedSeasonAddResponse {
|
||||
export interface WatchedEpisodeAddResponse {
|
||||
watchedEpisodes: WatchedEpisode[];
|
||||
addedActivity: Activity;
|
||||
episodeStatusChangedHookResponse?: EpisodeStatusChangedHookResponse;
|
||||
}
|
||||
|
||||
export interface EpisodeStatusChangedHookResponse {
|
||||
newShowStatus?: WatchedStatus;
|
||||
watchedSeason?: WatchedSeason;
|
||||
addedActivities?: Activity[];
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
|
||||
Reference in New Issue
Block a user