mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
bf0594c1e8
Index also added to Fixes https://github.com/sbondCo/Watcharr/issues/1027
740 lines
23 KiB
Go
740 lines
23 KiB
Go
package watched
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"strconv"
|
|
|
|
"github.com/sbondCo/Watcharr/database/dbmodel"
|
|
"github.com/sbondCo/Watcharr/database/entity"
|
|
"github.com/sbondCo/Watcharr/domain"
|
|
"github.com/sbondCo/Watcharr/feature/watched/addedtocontent"
|
|
"github.com/sbondCo/Watcharr/util"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type ContentProvider interface {
|
|
GetOrCacheContent(contentType entity.ContentType, tmdbId int) (entity.Content, error)
|
|
}
|
|
|
|
type GameProvider interface {
|
|
GetOrCache(igdbID int) (entity.Game, error)
|
|
}
|
|
|
|
type UserProvider interface {
|
|
UserGetSettings(userId uint) (entity.UserSettings, error)
|
|
}
|
|
|
|
type Service struct {
|
|
db *gorm.DB
|
|
cp ContentProvider
|
|
gameProvider GameProvider
|
|
activityProvider domain.ActivityAddProvider
|
|
userProvider UserProvider
|
|
}
|
|
|
|
func NewService(
|
|
db *gorm.DB,
|
|
cp ContentProvider,
|
|
gameProvider GameProvider,
|
|
activityProvider domain.ActivityAddProvider,
|
|
userProvider UserProvider,
|
|
) *Service {
|
|
return &Service{
|
|
db,
|
|
cp,
|
|
gameProvider,
|
|
activityProvider,
|
|
userProvider,
|
|
}
|
|
}
|
|
|
|
// Get entire watched list
|
|
func (s *Service) getWatched(userId uint) ([]entity.Watched, error) {
|
|
watched := new([]entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Preload("Content").
|
|
Preload("Game").
|
|
Preload("Game.Poster").
|
|
Preload("Activity").
|
|
Preload("WatchedSeasons").
|
|
Preload("WatchedEpisodes").
|
|
Preload("Tags").
|
|
Where("user_id = ?", userId).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("getWatched: Failed!", "error", res.Error)
|
|
return []entity.Watched{}, res.Error
|
|
}
|
|
return *watched, nil
|
|
}
|
|
|
|
// Returns a page of users watched list.
|
|
func (s *Service) GetWatchedPage(
|
|
userId uint,
|
|
pp util.PaginationParams,
|
|
wr domain.WatchedGetPageRequest,
|
|
extraProps *domain.WatchedGetPageExtraProps,
|
|
) (util.PaginationResponse[entity.Watched, util.None], error) {
|
|
slog.Debug("GetWatchedPage: A page was requested.",
|
|
"user_id", userId,
|
|
"pagination_params", pp,
|
|
"wr", wr)
|
|
|
|
pRes := &util.PaginationResponse[entity.Watched, util.None]{}
|
|
|
|
// Get user settings.
|
|
userSettings, err := s.userProvider.UserGetSettings(userId)
|
|
if err != nil {
|
|
return *pRes, errors.New("failed to get user settings")
|
|
}
|
|
|
|
watched := new([]entity.Watched)
|
|
res := s.db.
|
|
Model(&entity.Watched{}).
|
|
Where(&entity.Watched{UserID: userId})
|
|
|
|
if extraProps != nil {
|
|
// Process `WatchedIds` extra prop.
|
|
if len(extraProps.WatchedIds) > 0 {
|
|
res = res.Where("`watcheds`.`id` IN ?", extraProps.WatchedIds)
|
|
}
|
|
|
|
// Search query
|
|
if extraProps.Query != "" {
|
|
q := "%" + extraProps.Query + "%"
|
|
res = res.Where("Content.Title LIKE ? OR Game.Name LIKE ?", q, q)
|
|
}
|
|
}
|
|
|
|
res = res.
|
|
Joins("Content").
|
|
Joins("Game").
|
|
Preload("Game.Poster").
|
|
Preload("Tags").
|
|
Preload("WatchedSeasons").
|
|
Preload("WatchedEpisodes").
|
|
// Apply filters first.
|
|
Scopes(watchedRefineFilter(wr, &userSettings)).
|
|
// Then count results (after filter);
|
|
Count(&pRes.TotalResults).
|
|
// Now calculate pagination properties with a TotalResults
|
|
// that takes filtered out items into account.
|
|
Scopes(util.Paginate(pp, pRes)).
|
|
// Last we can apply our sorting.
|
|
// Note: We must sort *after* we count, because currently
|
|
// our 'Last Finished' sort, causes the COUNT query to also
|
|
// be given extra JOINS that we don't want (because it slows it down).
|
|
Scopes(watchedRefineSort(wr, userId)).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("GetWatchedPage: Failed!", "error", res.Error)
|
|
return util.PaginationResponse[entity.Watched, util.None]{}, res.Error
|
|
}
|
|
pRes.Results = *watched
|
|
pRes.Finished(pp)
|
|
return *pRes, nil
|
|
}
|
|
|
|
// Get a users **public** watchlist.
|
|
func (s *Service) getPublicWatched(
|
|
userId uint,
|
|
username string,
|
|
pp util.PaginationParams,
|
|
wr domain.WatchedGetPageRequest,
|
|
) (util.PaginationResponse[entity.Watched, util.None], error) {
|
|
slog.Debug("getPublicWatched: running",
|
|
"user_id", userId, "username", username)
|
|
|
|
// First we need to make sure the users list is public
|
|
user := new(entity.User)
|
|
// I figure we require knowlege of the users id and name to make it
|
|
// harder to just type in random ids and see someones list.. dunno
|
|
// if this is a thing we need but its here.. for now at least.
|
|
res := s.db.
|
|
Where("id = ? AND username = ?", userId, username).
|
|
Take(&user)
|
|
if res.Error != nil {
|
|
slog.Error("getPublicWatched: Failed to get user.",
|
|
"user_id", userId)
|
|
return util.PaginationResponse[entity.Watched, util.None]{}, errors.New("failed to check privacy settings")
|
|
}
|
|
if user.Private != nil && *user.Private {
|
|
slog.Error("getPublicWatched: This users list is private.",
|
|
"user_id", userId)
|
|
return util.PaginationResponse[entity.Watched, util.None]{}, errors.New("this watched list is private")
|
|
}
|
|
|
|
// Now we know the user is public, return their list
|
|
watched := new([]entity.Watched)
|
|
pRes := &util.PaginationResponse[entity.Watched, util.None]{}
|
|
res = s.db.
|
|
Model(&entity.Watched{}).
|
|
Where(&entity.Watched{UserID: userId}).
|
|
Joins("Content").
|
|
Joins("Game").
|
|
Preload("Game.Poster").
|
|
Preload("Tags").
|
|
Preload("WatchedSeasons").
|
|
Preload("WatchedEpisodes").
|
|
// Apply filters first.
|
|
Scopes(watchedRefineFilter(wr, nil)).
|
|
// Then count results (after filter);
|
|
Count(&pRes.TotalResults).
|
|
// Now calculate pagination properties with a TotalResults
|
|
// that takes filtered out items into account.
|
|
Scopes(util.Paginate(pp, pRes)).
|
|
// Sort options.
|
|
// Note: See note above in GetWatchedPage.
|
|
Scopes(watchedRefineSort(wr, userId)).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("getPublicWatched: Failed!", "error", res.Error)
|
|
return util.PaginationResponse[entity.Watched, util.None]{}, errors.New("failed fetching the list")
|
|
}
|
|
pRes.Results = *watched
|
|
pRes.Finished(pp)
|
|
return *pRes, nil
|
|
}
|
|
|
|
// Get a watched list item by id (must be for `userId`).
|
|
func (s *Service) GetWatchedItemById(userId uint, id uint) (entity.Watched, error) {
|
|
watched := new(entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Preload("Content").
|
|
Where("user_id = ? AND id = ?", userId, id).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("GetWatchedItemById: Failed!", "error", res.Error)
|
|
return entity.Watched{}, res.Error
|
|
}
|
|
return *watched, nil
|
|
}
|
|
|
|
// Get a watched list item by content (tmdb) id (must be for `userId`).
|
|
func (s *Service) GetWatchedItemByTmdbId(userId uint, tmdbId uint, contentType entity.ContentType) (entity.Watched, error) {
|
|
slog.Debug("GetWatchedItemByTmdbId: Running.", "userId", userId, "tmdbId", tmdbId)
|
|
watched := new(entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Joins("Content").
|
|
Preload("Activity").
|
|
Preload("WatchedSeasons").
|
|
Preload("WatchedEpisodes").
|
|
Preload("Tags").
|
|
Where("user_id = ? AND Content.tmdb_id = ? AND Content.type = ?", userId, tmdbId, contentType).
|
|
Take(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("GetWatchedItemByTmdbId: Failed!", "error", res.Error)
|
|
return entity.Watched{}, res.Error
|
|
}
|
|
slog.Debug("GetWatchedItemByTmdbId: Done.", "userId", userId, "tmdbId", tmdbId, "watched_item", watched)
|
|
return *watched, nil
|
|
}
|
|
|
|
// Same as `getWatchedItemByTmdbId` except for getting in bulk (multiple content ids).
|
|
// `c` entries should be in format: [tmdb_id, ContentType] (Note: Couldn't figure out
|
|
// if it's possible to type this to enforce [int, ContentType] type for entries)
|
|
func (s *Service) GetWatchedItemsByTmdbIds(userId uint, c [][]any) ([]entity.Watched, error) {
|
|
slog.Debug("GetWatchedItemsByTmdbIds: Running.", "userId", userId, "c", c)
|
|
watched := new([]entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Joins("Content").
|
|
Preload("Activity").
|
|
Preload("WatchedSeasons").
|
|
Preload("WatchedEpisodes").
|
|
Preload("Tags").
|
|
Where("user_id = ?", userId).
|
|
Where("(Content.tmdb_id, Content.type) IN ?", c).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("GetWatchedItemsByTmdbIds: Failed!", "error", res.Error)
|
|
return []entity.Watched{}, res.Error
|
|
}
|
|
slog.Debug(
|
|
"GetWatchedItemsByTmdbIds: Done.",
|
|
"userId", userId,
|
|
"watcheds_found", len(*watched),
|
|
// "wdev", *watched,
|
|
)
|
|
return *watched, nil
|
|
}
|
|
|
|
// Get a watched list item by game (igdb) id (must be for `userId`).
|
|
func (s *Service) GetWatchedItemByIgdbId(userId uint, igdbId uint) (entity.Watched, error) {
|
|
slog.Debug("getWatchedItemByIgdbId: Running.", "userId", userId, "igdbId", igdbId)
|
|
watched := new(entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Joins("Game").
|
|
Preload("Game.Poster").
|
|
Preload("Activity").
|
|
Preload("Tags").
|
|
Where("user_id = ? AND Game.igdb_id = ?", userId, igdbId).
|
|
Take(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("getWatchedItemByIgdbId: Failed!", "error", res.Error)
|
|
return entity.Watched{}, res.Error
|
|
}
|
|
slog.Debug("getWatchedItemByIgdbId: Done.", "userId", userId, "igdbId", igdbId, "watched_item", watched)
|
|
return *watched, nil
|
|
}
|
|
|
|
// Same as `getWatchedItemByIgdbId` except for getting in bulk (multiple content ids).
|
|
// `c` should be a slice of igdb ids.
|
|
func (s *Service) GetWatchedItemsByIgdbIds(userId uint, c []int) ([]entity.Watched, error) {
|
|
slog.Debug("getWatchedItemsByIgdbIds: Running.", "userId", userId, "c", c)
|
|
watched := new([]entity.Watched)
|
|
res := s.db.Model(&entity.Watched{}).
|
|
Joins("Game").
|
|
Preload("Game.Poster").
|
|
Preload("Activity").
|
|
Preload("Tags").
|
|
Where("user_id = ?", userId).
|
|
Where("(Game.igdb_id) IN ?", c).
|
|
Find(&watched)
|
|
if res.Error != nil {
|
|
slog.Error("getWatchedItemsByIgdbIds: Failed!", "error", res.Error)
|
|
return []entity.Watched{}, res.Error
|
|
}
|
|
slog.Debug(
|
|
"getWatchedItemsByIgdbIds: Done.",
|
|
"userId", userId,
|
|
"watcheds_found", len(*watched),
|
|
// "wdev", *watched,
|
|
)
|
|
return *watched, nil
|
|
}
|
|
|
|
// Get watched item by an id and SupportedMedia type.
|
|
func (s *Service) GetWatchedItemBySupportedMediaId(userId uint, id uint, t util.SupportedMedia) (entity.Watched, error) {
|
|
switch t {
|
|
case util.SupportedMediaGame:
|
|
return s.GetWatchedItemByIgdbId(userId, id)
|
|
case util.SupportedMediaMovie:
|
|
return s.GetWatchedItemByTmdbId(userId, id, entity.MOVIE)
|
|
case util.SupportedMediaShow:
|
|
return s.GetWatchedItemByTmdbId(userId, id, entity.SHOW)
|
|
}
|
|
slog.Error("GetWatchedItemBySupportedMediaId: Unsupported supportedmedia type",
|
|
"type", t)
|
|
return entity.Watched{}, errors.New("unsupported supportedmedia type")
|
|
}
|
|
|
|
// Get a list of watched items by a slice of ids and SupportedMedia types.
|
|
func (s *Service) GetWatchedItemsBySupportedMediaIds(userId uint, c []addedtocontent.IdToTypePair) ([]entity.Watched, error) {
|
|
slog.Debug("GetWatchedItemsBySupportedMediaIds: Running.", "userId", userId, "c", c)
|
|
// First we want to separate `c` into slices we can pass to the respective functions.
|
|
tmdbIds := [][]any{}
|
|
igdbIds := []int{}
|
|
for _, v := range c {
|
|
switch v.Type {
|
|
case util.SupportedMediaMovie:
|
|
tmdbIds = append(tmdbIds, []any{v.Id, entity.MOVIE})
|
|
case util.SupportedMediaShow:
|
|
tmdbIds = append(tmdbIds, []any{v.Id, entity.SHOW})
|
|
case util.SupportedMediaGame:
|
|
igdbIds = append(igdbIds, v.Id)
|
|
}
|
|
}
|
|
// Now call each function relating to an overarching type.
|
|
watcheds := []entity.Watched{}
|
|
if len(tmdbIds) > 0 {
|
|
if w, err := s.GetWatchedItemsByTmdbIds(userId, tmdbIds); err == nil {
|
|
watcheds = append(watcheds, w...)
|
|
} else {
|
|
slog.Error("GetWatchedItemsBySupportedMediaIds: Failed to get items by tmdb ids.", "error", err)
|
|
return []entity.Watched{}, err
|
|
}
|
|
}
|
|
if len(igdbIds) > 0 {
|
|
if w, err := s.GetWatchedItemsByIgdbIds(userId, igdbIds); err == nil {
|
|
watcheds = append(watcheds, w...)
|
|
} else {
|
|
slog.Error("GetWatchedItemsBySupportedMediaIds: Failed to get items by igdb ids.", "error", err)
|
|
return []entity.Watched{}, err
|
|
}
|
|
}
|
|
return watcheds, nil
|
|
}
|
|
|
|
func (s *Service) AddWatched(
|
|
userId uint,
|
|
ar domain.WatchedAddRequest,
|
|
extraProps domain.WatchedAddExtraProps,
|
|
) (entity.Watched, error) {
|
|
slog.Debug("Adding watched item",
|
|
"user_id", userId,
|
|
"add_request", ar,
|
|
"extra_props", extraProps)
|
|
|
|
if extraProps.ActivityType == "" {
|
|
// We ALWAYS require the ActivityType to be provided by any caller.
|
|
return entity.Watched{}, errors.New("extraProp ActivityType not specified")
|
|
}
|
|
|
|
watched := entity.Watched{
|
|
UserID: userId,
|
|
}
|
|
|
|
// Based on the ContentType given in the request, find the correct
|
|
// content to attach to the Watched entry we are creating.
|
|
switch ar.ContentType {
|
|
case "movie", "tv":
|
|
tmdbId := ar.TMDBID
|
|
if tmdbId == 0 && ar.Deprecated_ContentID != 0 {
|
|
// This if provides support for a now deprecated field.
|
|
// When the property is removed, this if will be gone and we can
|
|
// use `ar.TMDBID` directly for below.
|
|
tmdbId = ar.Deprecated_ContentID
|
|
slog.Error("AddWatched: You are using a deprecated field 'contentId', please replace it with 'tmdbId' to keep the same functionality for future releases of Watcharr.")
|
|
}
|
|
if tmdbId == 0 {
|
|
return entity.Watched{}, errors.New("missing tmdb id")
|
|
}
|
|
// Get content cache (or cache it if we don't have it locally)
|
|
content, err := s.cp.GetOrCacheContent(
|
|
entity.ContentType(ar.ContentType), tmdbId)
|
|
if err != nil {
|
|
return entity.Watched{}, err
|
|
}
|
|
// Error if content has no id
|
|
if content.ID == 0 {
|
|
return entity.Watched{}, errors.New("failed to find content id")
|
|
}
|
|
// Add content to watched entry
|
|
watched.ContentID = &content.ID
|
|
case "game":
|
|
if ar.IGDBID == 0 {
|
|
return entity.Watched{}, errors.New("missing igdb id")
|
|
}
|
|
game, err := s.gameProvider.GetOrCache(ar.IGDBID)
|
|
if err != nil {
|
|
return entity.Watched{}, err
|
|
}
|
|
// Error if content has no id
|
|
if game.ID == 0 {
|
|
return entity.Watched{}, errors.New("failed to find game by id")
|
|
}
|
|
watched.GameID = &game.ID
|
|
default:
|
|
return entity.Watched{}, errors.New("invalid content type provided")
|
|
}
|
|
|
|
// Set default status for when content is added by
|
|
// rating it instead of giving status first.
|
|
if ar.Status == "" {
|
|
if ar.ContentType == "movie" || ar.ContentType == "game" {
|
|
ar.Status = entity.FINISHED
|
|
} else {
|
|
ar.Status = entity.WATCHING
|
|
}
|
|
}
|
|
|
|
// Add watched data to struct
|
|
watched.Status = ar.Status
|
|
watched.Rating = ar.Rating
|
|
watched.Thoughts = ar.Thoughts
|
|
|
|
// If custom WatchedDate passed, set CreatedAt and UpdatedAt fields to it.
|
|
if !ar.WatchedDate.IsZero() {
|
|
slog.Debug("Adding watched item: The provided WatchedDate is valid.",
|
|
"userId", userId,
|
|
"request", ar)
|
|
watched.CreatedAt = ar.WatchedDate
|
|
watched.UpdatedAt = ar.WatchedDate
|
|
}
|
|
|
|
// Create the entry in db.
|
|
res := s.db.Create(&watched)
|
|
if res.Error != nil {
|
|
if errors.Is(res.Error, gorm.ErrDuplicatedKey) {
|
|
// The record already exists.
|
|
// It could be soft deleted or not.
|
|
slog.Info("AddWatched: Creating row failed because of a unique key constraint violation.")
|
|
if err := s.restoreWatchedAfterDuplicatedKeyErr(
|
|
userId,
|
|
ar,
|
|
extraProps.DontRestore,
|
|
&watched,
|
|
); err != nil {
|
|
// Try to restore the entry if unique contraint hit.
|
|
slog.Error("AddWatched: Failed to restore existing watched entry.",
|
|
"error", err)
|
|
// Returns watched too because handlers of certain errors
|
|
// may need it (and it's ID since we could have fetched it here)
|
|
return watched, err
|
|
}
|
|
} else {
|
|
slog.Error("AddWatched: Error adding watched to database", "error", res.Error.Error())
|
|
return entity.Watched{}, errors.New("failed adding watched entry to database")
|
|
}
|
|
}
|
|
slog.Debug("AddWatched: Added watched list item", "item", watched)
|
|
|
|
// Finally add activity
|
|
activityAddReq := domain.ActivityAddProps{
|
|
WatchedID: watched.ID,
|
|
Type: extraProps.ActivityType,
|
|
}
|
|
if activityJson, err := json.Marshal(map[string]any{
|
|
"status": ar.Status,
|
|
"rating": ar.Rating,
|
|
}); err != nil {
|
|
slog.Error("AddWatched: Failed to marshal json for add activity request, adding without data",
|
|
"error", err)
|
|
} else {
|
|
activityAddReq.Data = string(activityJson)
|
|
}
|
|
countAsPlay := false
|
|
if ar.Status == entity.FINISHED {
|
|
countAsPlay = true
|
|
}
|
|
act, _ := s.activityProvider.AddActivity(
|
|
userId,
|
|
activityAddReq,
|
|
countAsPlay,
|
|
)
|
|
watched.Activity = append(watched.Activity, act)
|
|
|
|
return watched, nil
|
|
}
|
|
|
|
// Restore a watched entry that was soft deleted.
|
|
// Currently used for AddWatched, when it realizes the entry may exist already
|
|
// as a soft deleted record.
|
|
//
|
|
// NOTE: This function assumes it is being ran after a unique constraint
|
|
// violation (like in AddWatched).
|
|
func (s *Service) restoreWatchedAfterDuplicatedKeyErr(
|
|
userId uint,
|
|
ar domain.WatchedAddRequest,
|
|
dontRestore bool,
|
|
watchedOut *entity.Watched,
|
|
) error {
|
|
slog.Info("restoreWatchedAfterDuplicatedKeyErr: Attempting to restore.",
|
|
"user_id", userId)
|
|
|
|
// Our base where statement to find the row we want to restore by unique
|
|
// indexes (user_id AND (content_id or game_id)).
|
|
whereStmt := entity.Watched{
|
|
UserID: userId,
|
|
}
|
|
|
|
// A check to ensure safety of the following db query:
|
|
// We MUST have a UserID
|
|
if whereStmt.UserID == 0 {
|
|
return errors.New("no userid in provided watched struct")
|
|
}
|
|
// ...AND a (contentId || gameId).
|
|
if watchedOut.ContentID != nil && *watchedOut.ContentID != 0 {
|
|
whereStmt.ContentID = watchedOut.ContentID
|
|
} else if watchedOut.GameID != nil && *watchedOut.GameID != 0 {
|
|
whereStmt.GameID = watchedOut.GameID
|
|
} else {
|
|
return errors.New("no supported media ids in provided watched struct")
|
|
}
|
|
|
|
// First we will try to get the record.
|
|
res := s.db.
|
|
Model(&entity.Watched{}).
|
|
Unscoped().
|
|
// Just loading activity now too so if we restore record,
|
|
// we can give the full activity in response.
|
|
Preload("Activity").
|
|
Where(&whereStmt).
|
|
Take(&watchedOut)
|
|
if res.Error != nil {
|
|
slog.Error("restoreWatchedAfterDuplicatedKeyErr: Select query failed!",
|
|
"error", res.Error)
|
|
return errors.New("errored checking for potential soft deleted record")
|
|
}
|
|
if watchedOut.ID == 0 {
|
|
return errors.New("didn't find an existing record")
|
|
}
|
|
if watchedOut.DeletedAt.Time.IsZero() {
|
|
// If it's not deleted, then it just exists..
|
|
return domain.ErrWatchedExists
|
|
} else if dontRestore {
|
|
// If we are told not to attempt a restore (eg by a sync-er).
|
|
return domain.ErrWatchedExistsSoftDeleted
|
|
}
|
|
|
|
// If we are here, we have a soft deleted record to restore:
|
|
res = s.db.
|
|
// Passing `&watchedOut` makes gorm do another query for inserting
|
|
// all existing rows into db, im not sure why... but just going to
|
|
// manually pass primary key in.
|
|
Model(&entity.Watched{
|
|
GormModel: dbmodel.GormModel{ID: watchedOut.ID},
|
|
}).
|
|
Unscoped().
|
|
Updates(map[string]any{
|
|
"status": ar.Status,
|
|
"rating": ar.Rating,
|
|
"thoughts": ar.Thoughts,
|
|
"deleted_at": nil,
|
|
})
|
|
if res.Error != nil || res.RowsAffected == 0 {
|
|
slog.Error("restoreWatchedAfterDuplicatedKeyErr: Updating record failed!",
|
|
"error", res.Error,
|
|
"rows_affected", res.RowsAffected)
|
|
return errors.New("failed to update record")
|
|
}
|
|
// We have updated the row, update our struct now.
|
|
watchedOut.Status = ar.Status
|
|
watchedOut.Rating = ar.Rating
|
|
watchedOut.Thoughts = ar.Thoughts
|
|
watchedOut.DeletedAt = gorm.DeletedAt{}
|
|
|
|
slog.Info("restoreWatchedAfterDuplicatedKeyErr: Updated record.",
|
|
"user_id", userId)
|
|
|
|
return nil
|
|
}
|
|
|
|
// this method is too ugly to look at please make him look better, future irhm
|
|
func (s *Service) updateWatched(
|
|
userId uint,
|
|
id uint,
|
|
ar domain.WatchedUpdateRequest,
|
|
) (domain.WatchedUpdateResponse, error) {
|
|
slog.Debug("UpdateWatched", "request_data", ar)
|
|
if err := ar.Valid(); err != nil {
|
|
slog.Error("UpdateWatched: UpdateRequest struct is invalid.",
|
|
"id", id, "error", err)
|
|
return domain.WatchedUpdateResponse{}, err
|
|
}
|
|
upwat := entity.Watched{}
|
|
res := s.db.Model(&entity.Watched{}).Where("id = ? AND user_id = ?", id, userId).Take(&upwat)
|
|
if res.Error != nil {
|
|
slog.Error("Watched entry update failed:", "id", id, "error", res.Error.Error())
|
|
return domain.WatchedUpdateResponse{}, errors.New("failed to update watched entry")
|
|
}
|
|
originalThoughts := upwat.Thoughts
|
|
if ar.Rating != 0 {
|
|
upwat.Rating = ar.Rating
|
|
}
|
|
if ar.Status != "" {
|
|
upwat.Status = ar.Status
|
|
}
|
|
if ar.Thoughts != "" {
|
|
upwat.Thoughts = ar.Thoughts
|
|
}
|
|
if ar.RemoveThoughts {
|
|
upwat.Thoughts = ""
|
|
}
|
|
if ar.Pinned != nil {
|
|
upwat.Pinned = *ar.Pinned
|
|
}
|
|
res = s.db.Save(upwat)
|
|
if res.RowsAffected <= 0 {
|
|
return domain.WatchedUpdateResponse{}, errors.New("no watched entry found")
|
|
}
|
|
addedActivity := entity.Activity{}
|
|
if ar.Rating != 0 {
|
|
addedActivity, _ = s.activityProvider.AddActivity(
|
|
userId,
|
|
domain.ActivityAddProps{
|
|
WatchedID: id,
|
|
Type: entity.RATING_CHANGED,
|
|
Data: strconv.Itoa(int(ar.Rating)),
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
if ar.Status != "" {
|
|
countAsPlay := false
|
|
if ar.Status == entity.FINISHED &&
|
|
util.Deref(ar.LetCountAsPlay, true) != false {
|
|
countAsPlay = true
|
|
}
|
|
addedActivity, _ = s.activityProvider.AddActivity(userId,
|
|
domain.ActivityAddProps{
|
|
WatchedID: id,
|
|
Type: entity.STATUS_CHANGED,
|
|
Data: string(ar.Status),
|
|
},
|
|
countAsPlay,
|
|
)
|
|
}
|
|
if ar.Thoughts != "" {
|
|
addedActivity, _ = s.activityProvider.AddActivity(userId,
|
|
domain.ActivityAddProps{
|
|
WatchedID: id,
|
|
Type: entity.THOUGHTS_CHANGED,
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
if ar.RemoveThoughts {
|
|
addedActivity, _ = s.activityProvider.AddActivity(userId,
|
|
domain.ActivityAddProps{
|
|
WatchedID: id,
|
|
Type: entity.THOUGHTS_REMOVED,
|
|
Data: originalThoughts,
|
|
},
|
|
false,
|
|
)
|
|
}
|
|
return domain.WatchedUpdateResponse{NewActivity: addedActivity}, nil
|
|
}
|
|
|
|
func (s *Service) UpdateWatchedLastViewedSeason(
|
|
userId uint,
|
|
id uint,
|
|
seasonNum int,
|
|
) error {
|
|
slog.Debug("UpdateWatchedLastViewedSeason",
|
|
"user_id", userId,
|
|
"id", id,
|
|
"season_num", seasonNum)
|
|
res := s.db.
|
|
Model(&entity.Watched{}).
|
|
Where("id = ? AND user_id = ?", id, userId).
|
|
Update("last_viewed_season", seasonNum)
|
|
if res.Error != nil {
|
|
slog.Error("updateWatchedLastViewedSeason: Failed when updating.",
|
|
"error", res.Error)
|
|
return errors.New("failed to update db")
|
|
}
|
|
if res.RowsAffected == 0 {
|
|
// likely the watched entry does not exist or is not owned by this `userId`.
|
|
slog.Error("updateWatchedLastViewedSeason: Watched entry does not exist.")
|
|
return errors.New("watched entry does not exist")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) removeWatched(
|
|
userId uint,
|
|
id uint,
|
|
) (domain.WatchedRemoveResponse, error) {
|
|
slog.Debug("Removing watched item:", "id", id, "user_id", userId)
|
|
// Our model has a deleted_at field, which will make gorm do a soft delete.
|
|
// Since other tables (eg activities) will link their rows to a watched_id, it's best to soft
|
|
// delete, so if user restores watched item they still have activity for example (also so
|
|
// someone else wont get other users activity if auto increment gives them the same watched id).
|
|
res := s.db.
|
|
Model(&entity.Watched{}).
|
|
Where("id = ? AND user_id = ?", id, userId).
|
|
Delete(&entity.Watched{})
|
|
if res.Error != nil {
|
|
slog.Error("Removing watched entry failed",
|
|
"id", id,
|
|
"error", res.Error.Error())
|
|
return domain.WatchedRemoveResponse{}, errors.New("failed to remove watched entry")
|
|
}
|
|
if res.RowsAffected <= 0 {
|
|
return domain.WatchedRemoveResponse{}, errors.New("no watched entry found")
|
|
}
|
|
addedActivity, _ := s.activityProvider.AddActivity(
|
|
userId,
|
|
domain.ActivityAddProps{
|
|
WatchedID: id,
|
|
Type: entity.REMOVED_WATCHED,
|
|
},
|
|
false,
|
|
)
|
|
return domain.WatchedRemoveResponse{NewActivity: addedActivity}, nil
|
|
}
|