diff --git a/README.md b/README.md index f3e3132..bc21cd5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ - [shows](#shows) - [seasons](#seasons) - [episodes](#episodes) + - [sync](#sync) - [users](#users) - [watchlist](#watchlist) @@ -967,6 +968,119 @@ $ ./trakt-sync episodes -a watching -i the-sopranos -season 1 -episode 1 $ ./trakt-sync episodes -a videos -i the-sopranos -season 1 -episode 1 ``` +#### sync: + +##### Last activities +```console +$ ./trakt-sync sync -a last_activities +``` +##### Playback progress from last 60 days +```console +$ ./trakt-sync sync -a playback +``` +##### Playback progress from last 60 days (movies or episodes) +```console +$ ./trakt-sync sync -a playback -t movies +``` +##### Playback progress from 7 days +```console +$ ./trakt-sync sync -a playback -start_at 2025-06-01 -end_at 2025-06-07 +``` +##### Remove playback +```console +$ ./trakt-sync sync -a remove_playback -playback_id 12345 +``` +##### Get collection - movies +```console +$ ./trakt-sync sync -a get_collection -t movies -ex metadata +``` +##### Get collection - shows +```console +$ ./trakt-sync sync -a get_collection -t shows -ex metadata +``` +##### Get collection - episodes +```console +$ ./trakt-sync sync -a get_collection -t episodes -ex metadata +``` +##### Get collection - seasons +```console +$ ./trakt-sync sync -a get_collection -t seasons -ex metadata +``` + +##### Add to collection - via -collection_items flag +```console +$ ./trakt-sync sync -a add_to_collection -collection_items export_sync_collection_movies.json +``` +```console +$ ./trakt-sync sync -a add_to_collection -collection_items export_sync_collection_shows.json +``` +```console +$ ./trakt-sync sync -a add_to_collection -collection_items export_sync_collection_episodes.json +``` +```console +$ ./trakt-sync sync -a add_to_collection -collection_items export_sync_collection_seasons.json +``` +##### Add to collection - via stdin +```console +$ cat export_sync_collection_movies.json | ./trakt-sync sync -a add_to_collection +``` +```console +$ cat export_sync_collection_shows.json | ./trakt-sync sync -a add_to_collection +``` +```console +$ cat export_sync_collection_episodes.json | ./trakt-sync sync -a add_to_collection +``` +```console +$ cat export_sync_collection_seasons.json | ./trakt-sync sync -a add_to_collection +``` + +##### Remove from collection - via -collection_items flag +```console +$ ./trakt-sync sync -a remove_from_collection -collection_items export_sync_collection_movies.json +``` +```console +``` +##### Remove from collection - via stdin +```console +$ cat export_sync_collection_movies.json | ./trakt-sync sync -a remove_from_collection +``` +```console +``` + +##### Get watched - movies +```console +$ ./trakt-sync sync -a get_watched -t movies +``` +##### Get watched - shows +```console +$ ./trakt-sync sync -a get_watched -t shows +``` +##### Get watched - shows - noseasons +```console +$ ./trakt-sync sync -a get_watched -t shows -ex noseasons +``` +##### Get watched - episodes +```console +$ ./trakt-sync sync -a get_watched -t episodes +``` + +##### Get history - movies +```console +$ ./trakt-sync sync -a get_history -t movies -start_at 2025-07-01 -end_at 2025-07-06 +``` +##### Get history - shows - with trakt_id +```console +$ ./trakt-sync sync -a get_history -t shows -i 1388 +``` +##### Get history - shows +```console +$ ./trakt-sync sync -a get_history -t shows +``` +##### Get history - episodes +```console +$ ./trakt-sync sync -a get_history -t episodes +``` + #### users: ##### Export movies or shows or episodes from user lists: diff --git a/Todo.txt b/Todo.txt index 3777c80..5be1e6a 100644 --- a/Todo.txt +++ b/Todo.txt @@ -1,3 +1,2 @@ - add filters where they used - optimize multiple flag usage -- use local time diff --git a/cfg/config.go b/cfg/config.go index 3da2bc4..675572d 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -31,6 +31,7 @@ type Config struct { CountSpecials string `toml:"count_specials"` Days int `toml:"days"` Delete bool `toml:"delete"` + EndAt string `toml:"end_at"` Episode int `toml:"episode"` EpisodeAbs int `toml:"episode_abs"` EpisodeCode string `toml:"episode_code"` @@ -60,6 +61,7 @@ type Config struct { NotesID int `toml:"notes_id"` Output string `toml:"output"` PagesLimit int `toml:"pages_limit"` + PlaybackID int `toml:"playback_id"` PerPage int `toml:"per_page"` Privacy string `toml:"privacy"` Progress float64 `toml:"progress"` @@ -82,6 +84,7 @@ type Config struct { Sort string `toml:"sort"` Specials string `toml:"specials"` Spoiler bool `toml:"spoiler"` + StartAt string `toml:"start_at"` Timezone string `toml:"timezone"` TokenPath string `toml:"token_path"` TraktID int `toml:"trakt_id"` diff --git a/cfg/options.go b/cfg/options.go index 3175c98..92769b4 100644 --- a/cfg/options.go +++ b/cfg/options.go @@ -105,6 +105,18 @@ var ModuleActionConfig = map[string]OptionsConfig{ Type: []string{"all", "personal", "official", "watchlists", "favorites"}, Sort: []string{"popular", "likes", "comments", "items", "added", "updated"}, }, + "sync:playback": { + Type: []string{"movies", "episodes"}, + Sort: []string{}, + }, + "sync:get_watched": { + Type: []string{"movies", "shows", "episodes"}, + Sort: []string{}, + }, + "sync:get_history": { + Type: []string{"movies", "shows", "seasons", "episodes"}, + Sort: []string{}, + }, } // ModuleConfig represents the configuration options for all modules @@ -232,6 +244,10 @@ var ModuleConfig = map[string]OptionsConfig{ "notes": { Privacy: []string{"private", "friends", "public"}, }, + + "sync": { + Type: []string{"all", "movies", "shows"}, + }, } // ValidateConfig validates if the provided configuration is allowed for the given module @@ -500,6 +516,7 @@ func GetOutputForModule(options *str.Options) string { consts.Networks: getOutputForModuleNetworks(options), consts.Notes: getOutputForModuleNotes(options), consts.Recommendations: getOutputForModuleRecommendations(options), + consts.Sync: getOutputForModuleSync(options), } if output, found := allOutputs[options.Module]; found { @@ -508,6 +525,23 @@ func GetOutputForModule(options *str.Options) string { return fmt.Sprintf(consts.DefaultOutputFormat3, options.Module, options.Type, options.Format) } +func getOutputForModuleSync(options *str.Options) string { + switch options.Action { + case consts.GetHistory: + options.Output = fmt.Sprintf(consts.DefaultOutputFormat3, options.Module, consts.History, options.Type) + case consts.GetWatched: + options.Output = fmt.Sprintf(consts.DefaultOutputFormat3, options.Module, consts.Watched, options.Type) + case consts.GetCollection: + options.Output = fmt.Sprintf(consts.DefaultOutputFormat3, options.Module, consts.Collection, options.Type) + case consts.LastActivities, consts.Playback, consts.AddToCollection, consts.RemoveFromCollection: + options.Output = fmt.Sprintf(consts.DefaultOutputFormat2, options.Module, options.Action) + default: + options.Output = fmt.Sprintf(consts.DefaultOutputFormat2, options.Module, options.Type) + } + + return options.Output +} + func getOutputForModuleRecommendations(options *str.Options) string { switch options.Action { case consts.Movies, consts.Shows: diff --git a/cmds/command.go b/cmds/command.go index f75fb35..348eea0 100644 --- a/cmds/command.go +++ b/cmds/command.go @@ -42,6 +42,7 @@ var Avflags = map[string]bool{ "certifications": true, "checkin": true, "collection": true, + "collection_items": true, "comment": true, "comment_id": true, "comment_type": true, @@ -51,10 +52,11 @@ var Avflags = map[string]bool{ "country": true, "days": true, "delete": true, + "end_at": true, + "episode": true, "episode_abs": true, "episode_code": true, "episodes": true, - "episode": true, "ex": true, "f": true, "field": true, @@ -81,6 +83,7 @@ var Avflags = map[string]bool{ "pause": true, "people": true, "period": true, + "playback_id": true, "privacy": true, "progress": true, "q": true, @@ -92,20 +95,22 @@ var Avflags = map[string]bool{ "s": true, "scrobble": true, "search": true, - "seasons": true, "season": true, + "seasons": true, "shows": true, "specials": true, "spoiler": true, "start": true, + "start_at": true, "start_date": true, "stop": true, + "sync": true, "t": true, "trakt_id": true, "translations": true, "u": true, - "users": true, "undo": true, + "users": true, "v": true, "version": true, "watchlist": true, @@ -189,6 +194,11 @@ func (*Command) UpdateShowFlagsValues() { } } +// UpdateSyncFlagsValues update sync flags values only in command +func (*Command) UpdateSyncFlagsValues() { + +} + // UpdateSeasonFlagsValues update season flags values only in command func (*Command) UpdateSeasonFlagsValues() { if *_seasonsSort == "" { @@ -715,7 +725,7 @@ func (c *Command) UpdateOptionsWithCommandFlags(options *str.Options) *str.Optio options = UpdateOptionsWithCommandShowsFlags(c, options) options = UpdateOptionsWithCommandRecommendationsFlags(options) options = UpdateOptionsWithCommandScrobbleFlags(options) - + options = UpdateOptionsWithCommandSyncFlags(c, options) return options } @@ -731,6 +741,42 @@ func UpdateOptionsWithCommandScrobbleFlags(options *str.Options) *str.Options { return options } +// UpdateOptionsWithCommandSyncFlags update options depends on scrobble command flags +func UpdateOptionsWithCommandSyncFlags(c *Command, options *str.Options) *str.Options { + if len(*_syncAction) > consts.ZeroValue { + options.Action = *_syncAction + } + options.Output = cfg.GetOutputForModule(options) + + options.FullHour = true + if len(*_syncStartAt) > consts.ZeroValue { + options.StartDate = c.common.ConvertDateString(*_syncStartAt, consts.DefaultStartDateFormat, options.Timezone, options.FullHour) + } else { + options.StartDate = c.common.DateLastDays(consts.DefaultStartAtDays, options.Timezone, options.FullHour) + } + + if len(*_syncEndAt) > consts.ZeroValue { + options.EndDate = c.common.ConvertDateString(*_syncEndAt, consts.DefaultStartDateFormat, options.Timezone, options.FullHour) + } else { + options.EndDate = c.common.CurrentDateString(options.Timezone, options.FullHour) + } + + options.FullHour = true + + if *_syncPlaybackID > consts.ZeroValue { + options.PlaybackID = *_syncPlaybackID + } + + if len(*_syncCollectionItems) > consts.ZeroValue { + options.CollectionItems = *_syncCollectionItems + } + + if *_syncID > consts.ZeroValue { + options.TraktID = *_syncID + } + return options +} + // UpdateOptionsWithCommandRecommendationsFlags update options depends on recommendations command flags func UpdateOptionsWithCommandRecommendationsFlags(options *str.Options) *str.Options { if len(*_recommendationsIgnoreCollected) > consts.ZeroValue { diff --git a/cmds/command_history.go b/cmds/command_history.go index 6354841..8c3895b 100644 --- a/cmds/command_history.go +++ b/cmds/command_history.go @@ -5,14 +5,11 @@ import ( "encoding/json" "errors" "fmt" - "time" "github.com/mfederowicz/trakt-sync/cfg" "github.com/mfederowicz/trakt-sync/consts" - "github.com/mfederowicz/trakt-sync/internal" "github.com/mfederowicz/trakt-sync/printer" "github.com/mfederowicz/trakt-sync/str" - "github.com/mfederowicz/trakt-sync/uri" "github.com/mfederowicz/trakt-sync/writer" ) @@ -31,7 +28,7 @@ func historyFunc(cmd *Command, _ ...string) error { printer.Println("fetch history lists for:" + options.UserName) - historyLists, err := fetchHistoryList(client, options, consts.DefaultPage) + historyLists, err := cmd.common.FetchHistoryList(client, options, consts.DefaultPage) if err != nil { return fmt.Errorf("fetch history list error:%w", err) } @@ -65,34 +62,3 @@ var ( func init() { HistoryCmd.Run = historyFunc } - -func fetchHistoryList(client *internal.Client, options *str.Options, page int) ([]*str.ExportlistItem, error) { - opts := uri.ListOptions{Page: page, Limit: options.PerPage, Extended: options.ExtendedInfo} - - list, resp, err := client.Sync.GetWatchedHistory( - client.BuildCtxFromOptions(options), - &options.Type, - &opts, - ) - - if err != nil { - return nil, err - } - - // Check if there are more pages - if client.HavePages(page, resp, options.PagesLimit) { - time.Sleep(time.Duration(consts.SleepNumberOfSeconds) * time.Second) - - // Fetch items from the next page - nextPage := page + consts.NextPageStep - nextPageItems, err := fetchHistoryList(client, options, nextPage) - if err != nil { - return nil, err - } - - // Append items from the next page to the current page - list = append(list, nextPageItems...) - } - - return list, nil -} diff --git a/cmds/command_sync.go b/cmds/command_sync.go new file mode 100644 index 0000000..2c01039 --- /dev/null +++ b/cmds/command_sync.go @@ -0,0 +1,72 @@ +// Package cmds used for commands modules +package cmds + +import ( + "fmt" + + "github.com/mfederowicz/trakt-sync/cfg" + "github.com/mfederowicz/trakt-sync/consts" + "github.com/mfederowicz/trakt-sync/handlers" +) + +var ( + _syncAction = SyncCmd.Flag.String("a", cfg.DefaultConfig().Action, consts.ActionUsage) + _syncStartAt = SyncCmd.Flag.String("start_at", cfg.DefaultConfig().StartAt, consts.StartAtUsage) + _syncEndAt = SyncCmd.Flag.String("end_at", cfg.DefaultConfig().EndAt, consts.EndAtUsage) + _syncPlaybackID = SyncCmd.Flag.Int("playback_id", cfg.DefaultConfig().PlaybackID, consts.PlaybackIDUsage) + _syncCollectionItems = SyncCmd.Flag.String("collection_items", consts.EmptyString, consts.CollectionItemsUsage) + _syncID = SyncCmd.Flag.Int("i", cfg.DefaultConfig().TraktID, consts.TraktIDUsage) + + validSyncActions = []string{ + "last_activities", "playback", "remove_playback", "get_collection", + "add_to_collection", "remove_from_collection", "get_watched", + "get_history"} +) + +// SyncCmd returns movies and episodes that a user has watched, sorted by most recent. +var SyncCmd = &Command{ + Name: "sync", + Usage: "", + Summary: "Syncing with trakt", + Help: `sync command`, +} + +func syncFunc(cmd *Command, _ ...string) error { + cmd.UpdateSyncFlagsValues() + options := cmd.Options + client := cmd.Client + options = cmd.UpdateOptionsWithCommandFlags(options) + + var handler handlers.SyncHandler + allHandlers := map[string]handlers.Handler{ + "last_activities": handlers.SyncLastActivitiesHandler{}, + "playback": handlers.SyncPlaybackHandler{}, + "remove_playback": handlers.SyncRemovePlaybackHandler{}, + "get_collection": handlers.SyncGetCollectionHandler{}, + "add_to_collection": handlers.SyncAddToCollectionHandler{}, + "remove_from_collection": handlers.SyncRemoveFromCollectionHandler{}, + "get_watched": handlers.SyncGetWatchedHandler{}, + "get_history": handlers.SyncGetHistoryHandler{}, + } + handler, err := cmd.common.GetHandlerForMap(options.Action, allHandlers) + + if err != nil { + cmd.common.GenActionsUsage(cmd.Name, validSyncActions) + return nil + } + + err = handler.Handle(options, client) + if err != nil { + return fmt.Errorf(cmd.Name+"/"+options.Action+":%s", err) + } + + return nil +} + +var ( + syncDumpTemplate = `` +) + +func init() { + SyncCmd.Run = syncFunc +} diff --git a/cmds/runtime.go b/cmds/runtime.go index 7aa6fe6..0d775db 100644 --- a/cmds/runtime.go +++ b/cmds/runtime.go @@ -35,6 +35,7 @@ var Commands = []*Command{ SearchCmd, SeasonsCmd, ShowsCmd, + SyncCmd, UsersCmd, WatchlistCmd, } diff --git a/consts/glob.go b/consts/glob.go index 453dbda..2ff580b 100644 --- a/consts/glob.go +++ b/consts/glob.go @@ -12,6 +12,7 @@ const ( CheckinError = "checkin error:%w" CheckinMsgUsage = "allow to overwrite msg" ClientNewRequestFatal = "client.NewRequest returned error: %v" + CollectionItemsUsage = "allow to overwrite collection_items file path" CollectionProgressError = "collection progress error:%w" CommaString = "," CommentIDUsage = "allow to overwrite comment_id" @@ -23,25 +24,30 @@ const ( DefaultOutputFormat1 = "export_%s.json" DefaultOutputFormat2 = "export_%s_%s.json" DefaultOutputFormat3 = "export_%s_%s_%s.json" + DefaultDateFormat = time.DateOnly DefaultStartDateFormat = time.RFC3339 + DefaultStartAtDays = 60 DeleteUsage = "allow delete item" EmptyCommentIDMsg = "set commentId ie: -comment_id 123" EmptyHistoryIDMsg = "set historyId ie: -i 12345 from watched history" EmptyIncludeReplies = "set includeReplies ie: -include_replies true or false" - EmptyListIDMsg = "set traktId ie: -trakt_id 55" EmptyInternalIDMsg = "set Trakt ID, Trakt slug, or IMDB ID Example: ie: -i 12345 or -i tron-legacy-2010" + EmptyListIDMsg = "set traktId ie: -trakt_id 55" EmptyMovieIDMsg = "set Trakt ID, Trakt slug, or IMDB ID Example: ie: -i 12345 or -i tron-legacy-2010" EmptyNotesIDMsg = "set notesId ie: -i 12345678" EmptyPersonIDMsg = "set personId ie: -i john-wayne" EmptyResult = "empty result" - EmptyShowIDMsg = "set Trakt ID, Trakt slug, or IMDB ID Example: ie: -i 12345 or -i tron-legacy-2010" EmptySeasonIDMsg = "set Trakt ID, Trakt slug, or IMDB ID Example: ie: -i 12345 or -i tron-legacy-2010" + EmptyShowIDMsg = "set Trakt ID, Trakt slug, or IMDB ID Example: ie: -i 12345 or -i tron-legacy-2010" EmptyString = "" EmptyTraktIDMsg = "set traktId ie: -trakt_id 55" + EndAtUsage = "allow to overwrite end_at" + PlaybackIDUsage = "allow to overwrite playback_id" EpisodeAbsUsage = "episode_abs 1234" EpisodeCodeErr = "episode code error:%w" EpisodeCodeUsage = "episode_code format 01x24" EpisodeErr = "episode error:%w" + EpisodeUsage = "allow to overwrite episode" EpisodesType = "episodes" ErrorRender = "error render: %w" ErrorsPlaceholders = "%v %v: %d %v" @@ -54,9 +60,11 @@ const ( IgnoreCollectedUsage = "allow to overwrite ignore_collected" IgnoreWatchlistedUsage = "allow to overwrite ignore_watchlisted" IncludeRepliesUsage = "allow to overwrite include_replies" + InternalIDUsage = "allow to overwrite trakt ID" InvalidUserForNotes = "invalid user for notes Id:%s" ItemUsage = "item type used in collection/rating ie:movie,show,season,episode,person" JSONDataFormat = " " + LanguageUsage = "allow to overwrite language" ListCommentSortUsage = "allow to overwrite comments sort" ListIDUsage = "allow to export a specific custom list" ListItem = " - %s\n" @@ -64,7 +72,6 @@ const ( ListUsage = "allow to overwrite list" ModuleUsage = "allow use selected module" MovieIDUsage = "allow to overwrite movieID" - InternalIDUsage = "allow to overwrite trakt ID" MoviesCountryUsage = "allow to overwrite country" MoviesLanguageUsage = "allow to overwrite language" MoviesPeriodUsage = "allow to overwrite period" @@ -76,6 +83,7 @@ const ( NotFoundConfigForModule = "not found config for module '%s'" NotesIDUsage = "allow to overwrite notes_id" NotesNotFoundWithID = "notes not found with Id:%s" + PlaybackNotFoundWithID = "playback not found with Id:%d" NotesUsage = "allow to overwrite notes" OutputUsage = "allow to overwrite default output filename" PagesNoLimit = 0 @@ -89,11 +97,8 @@ const ( RemoveUsage = "allow remove item" ReplyUsage = "allow to overwrite reply" ResetAtUsage = "allow to overwrite reset_at" - SeasonUsage = "allow to overwrite season" - EpisodeUsage = "allow to overwrite episode" - TranslationsUsage = "allow to overwrite translations" - LanguageUsage = "allow to overwrite language" ScrobbleError = "scrobble error:%w" + SeasonUsage = "allow to overwrite season" SeparatorString = CommaString ShowEpisodeErr = "show episode error:%w" ShowErr = "show error:%w" @@ -107,12 +112,14 @@ const ( ShowsTypeUsage = "allow to overwrite type" SortUsage = "allow to overwrite sort" SpoilerUsage = "allow to overwrite spoiler" + StartAtUsage = "allow to overwrite start_at" StartDateUsage = "allow to overwrite start_date" StringDigit = "%s%d" StringString = "%s%s" TestURL = "test-url" TestURLNext = "test-url-next" TraktIDUsage = "allow to overwrite trakt_id" + TranslationsUsage = "allow to overwrite translations" TypeUsage = "allow to overwrite type" UndoUsage = "allow undo item" UnknownCheckinAction = "uknown checkin action" diff --git a/consts/keys.go b/consts/keys.go index 5a8f45a..d004255 100644 --- a/consts/keys.go +++ b/consts/keys.go @@ -8,91 +8,99 @@ import ( // usage strings const ( - ActionTypeAll = "all" - Aliases = "aliases" - AllDvd = "all-dvd" - AllFinales = "all-finales" - AllMovies = "all-movies" - AllNewShows = "all-new-shows" - AllSeasonPremieres = "all-season-premieres" - AllShows = "all-shows" - Anticipated = "anticipated" - Boxoffice = "boxoffice" - Calendars = "calendars" - Certifications = "certifications" - Checkin = "checkin" - Collected = "collected" - Collection = "collection" - CollectionProgress = "collection_progress" - Comment = "comment" - Comments = "comments" - Countries = "countries" - Dvd = "dvd" - Episode = "episode" - Favorited = "favorited" - Finales = "finales" - Episodes = "episodes" - Genres = "genres" - History = "history" - IDLookup = "id-lookup" - ImdbFormat = "imdb" - ImdbIDFormat = "Imdb" - Item = "item" - Items = "items" - Languages = "languages" - LastEpisode = "last_episode" - Likes = "likes" - List = "list" - Lists = "lists" - Lookup = "lookup" - Movie = "movie" - Movies = "movies" - MyDvd = "my-dvd" - MyFinales = "my-finales" - MyMovies = "my-movies" - MyNewShows = "my-new-shows" - MySeasonPremieres = "my-season-premieres" - MyShows = "my-shows" - Networks = "networks" - NewShows = "new_shows" - NextEpisode = "next_episode" - Notes = "notes" - People = "people" - Played = "played" - Popular = "popular" - Query = "query" - Ratings = "ratings" - Recommendations = "recommendations" - Related = "related" - Releases = "releases" - Replies = "replies" - SavedFilters = "saved_filters" - Scrobble = "scrobble" - Search = "search" - SeasonPremieres = "season_premieres" - Settings = "settings" - ShowEpisode = "show_episode" - Shows = "shows" - Season = "season" - Seasons = "seasons" - Stats = "stats" - Studios = "studios" - Summary = "summary" - TextQuery = "text-query" - TmdbFormat = "tmdb" - TmdbIDFormat = "Tmdb" - Translations = "translations" - Trending = "trending" - TvdbFormat = "tvdb" - TvdbIDFormat = "Tvdb" - UpdatedIDs = "updated_ids" - Updates = "updates" - Users = "users" - Videos = "videos" - Watched = "watched" - WatchedProgress = "watched_progress" - Watching = "watching" - Watchlist = "watchlist" + AddToCollection = "add_to_collection" + ActionTypeAll = "all" + Aliases = "aliases" + AllDvd = "all-dvd" + AllFinales = "all-finales" + AllMovies = "all-movies" + AllNewShows = "all-new-shows" + AllSeasonPremieres = "all-season-premieres" + AllShows = "all-shows" + Anticipated = "anticipated" + Boxoffice = "boxoffice" + Calendars = "calendars" + Certifications = "certifications" + Checkin = "checkin" + Collected = "collected" + Collection = "collection" + CollectionProgress = "collection_progress" + Comment = "comment" + Comments = "comments" + Countries = "countries" + Dvd = "dvd" + Episode = "episode" + Favorited = "favorited" + Finales = "finales" + Episodes = "episodes" + Genres = "genres" + GetCollection = "get_collection" + GetWatched = "get_watched" + GetHistory = "get_history" + History = "history" + IDLookup = "id-lookup" + ImdbFormat = "imdb" + ImdbIDFormat = "Imdb" + Item = "item" + Items = "items" + Languages = "languages" + LastEpisode = "last_episode" + Likes = "likes" + List = "list" + Lists = "lists" + Lookup = "lookup" + Movie = "movie" + Movies = "movies" + MyDvd = "my-dvd" + MyFinales = "my-finales" + MyMovies = "my-movies" + MyNewShows = "my-new-shows" + MySeasonPremieres = "my-season-premieres" + MyShows = "my-shows" + LastActivities = "last_activities" + Networks = "networks" + NewShows = "new_shows" + NextEpisode = "next_episode" + Notes = "notes" + People = "people" + Played = "played" + Playback = "playback" + Popular = "popular" + Query = "query" + Ratings = "ratings" + Recommendations = "recommendations" + Related = "related" + Releases = "releases" + Replies = "replies" + RemoveFromCollection = "remove_from_collection" + SavedFilters = "saved_filters" + Scrobble = "scrobble" + Search = "search" + SeasonPremieres = "season_premieres" + Settings = "settings" + ShowEpisode = "show_episode" + Shows = "shows" + Season = "season" + Seasons = "seasons" + Stats = "stats" + Studios = "studios" + Summary = "summary" + Sync = "sync" + TextQuery = "text-query" + TmdbFormat = "tmdb" + TmdbIDFormat = "Tmdb" + Translations = "translations" + Trending = "trending" + TvdbFormat = "tvdb" + TvdbIDFormat = "Tvdb" + UpdatedIDs = "updated_ids" + Updates = "updates" + Users = "users" + Videos = "videos" + Watched = "watched" + WatchedProgress = "watched_progress" + Watching = "watching" + Watchlist = "watchlist" ) // Fupper helper function to convert string in title format diff --git a/handlers/commons.go b/handlers/commons.go index 3f478d1..3434b7d 100644 --- a/handlers/commons.go +++ b/handlers/commons.go @@ -2,8 +2,11 @@ package handlers import ( + "encoding/json" "errors" "fmt" + "io" + "os" "strconv" "strings" "time" @@ -54,9 +57,14 @@ type CommonInterface interface { Notes(client *internal.Client, notes *str.Notes) (*str.Notes, *str.Response, error) Reply(client *internal.Client, id *int, comment *str.Comment) (*str.Comment, *str.Response, error) CheckSortAndTypes(options *str.Options) error + CheckTypes(options *str.Options) error ToTimestamp(at string) (*str.Timestamp, error) ConvertDateString(date string, out string) string CurrentDateString(tz string) string + DateLastDays(days int, tz string, full bool) string + CheckDates(from string, to string, tz string) string + ReadInput(items string) (*str.CollectionItems, error) + ConvertBytesToColletionItems(data []byte) (*str.CollectionItems, error) } // CommonLogic struct for common methods @@ -660,6 +668,22 @@ func (*CommonLogic) CheckSortAndTypes(options *str.Options) error { return nil } +// CheckTypes helper function to validate type field depends on module +func (CommonLogic) CheckTypes(options *str.Options) error { + // Check if the provided module exists in ModuleConfig + _, ok := cfg.ModuleConfig[options.Module] + if !ok { + return fmt.Errorf("not found config for module '%s'", options.Module) + } + prefix := options.Module + ":" + options.Action + if !cfg.IsValidConfigType(cfg.ModuleActionConfig[prefix].Type, options.Type) { + return fmt.Errorf("not found type for module '%s'", options.Module) + } + + // Check id_type values + return nil +} + // ValidPrivacy helper function to validate privacy field depends on module func (*CommonLogic) ValidPrivacy(options *str.Options) error { // Check if the provided module exists in ModuleConfig @@ -785,3 +809,176 @@ func (CommonLogic) CurrentDateString(tz string, full bool) string { return currentTime.Format(time.RFC3339) } + +// DateLastDays return last x days from user timezone +func (CommonLogic) DateLastDays(days int, tz string, full bool) string { + // Get the current time + currentTime := time.Now().UTC() + + if tz != time.UTC.String() { + loc, _ := time.LoadLocation(tz) + currentTime = currentTime.In(loc) + } + + past := currentTime.AddDate(0, 0, -days) + if full { + past = past.Truncate(time.Hour) + } + + return past.Format(time.RFC3339) +} + +// CheckDates if dates are ok +func (CommonLogic) CheckDates(from string, to string, tz string) error { + // Load location + loc, err := time.LoadLocation(tz) + if err != nil { + return fmt.Errorf("invalid timezone: %w", err) + } + + // Get current full hour + now := time.Now().In(loc) + fullHour := now.Truncate(time.Hour) + + // Parse from and to if they are present + var fromTime, toTime time.Time + if from != "" { + fromTime, err = time.ParseInLocation(consts.DefaultStartDateFormat, from, loc) + if err != nil { + return fmt.Errorf("invalid from date: %w", err) + } + if fromTime.After(fullHour) { + return fmt.Errorf("'from' date must not be later than the current full hour") + } + } + + if to != "" { + toTime, err = time.ParseInLocation(consts.DefaultStartDateFormat, to, loc) + if err != nil { + return fmt.Errorf("invalid to date: %w", err) + } + if toTime.After(fullHour) { + return fmt.Errorf("'to' date must not be later than:%s", fullHour.Format(consts.DefaultStartDateFormat)) + } + } + + // If both are present, validate range + if from != "" && to != "" { + if fromTime.After(toTime) { + return fmt.Errorf("'from' date must be earlier than or equal to 'to' date") + } + } + + return nil +} + +// ConvertBytesToColletionItems convert bytes to struct +func (CommonLogic) ConvertBytesToColletionItems(data []byte) (*str.CollectionItems, error) { + var list []*str.ExportlistItem + if err := json.Unmarshal(data, &list); err != nil { + return nil, err + } + + items := new(str.CollectionItems) + items.Movies = &[]str.ExportlistItem{} + items.Shows = &[]str.ExportlistItem{} + items.Seasons = &[]str.ExportlistItem{} + items.Episodes = &[]str.ExportlistItem{} + + for _, val := range list { + if val.Movie != nil { + e := str.ExportlistItem{} + e.Title = val.Movie.Title + e.Year = val.Movie.Year + e.IDs = val.Movie.IDs + e.UpdateCollectedData(val) + *items.Movies = append(*items.Movies, e) + } + if val.Show != nil { + e := str.ExportlistItem{} + e.Title = val.Show.Title + e.Year = val.Show.Year + e.IDs = val.Show.IDs + e.UpdateCollectedData(val) + *items.Shows = append(*items.Shows, e) + } + if val.Season != nil { + e := str.ExportlistItem{} + e.IDs = val.Season.IDs + val.Season.UpdateCollectedData(val) + *items.Seasons = append(*items.Seasons, e) + } + if val.Episode != nil { + e := str.ExportlistItem{} + e.IDs = val.Episode.IDs + e.UpdateCollectedData(val) + *items.Episodes = append(*items.Episodes, e) + } + } + + return items, nil +} + +// ReadInput read data from stdin or from file +func (c CommonLogic) ReadInput(filePath string) (*str.CollectionItems, error) { + if filePath != consts.EmptyString { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + return c.ConvertBytesToColletionItems(data) + } + + // Check if there's data in stdin to avoid blocking + fi, err := os.Stdin.Stat() + if err != nil { + return nil, fmt.Errorf("failed to stat stdin: %w", err) + } + + // os.ModeCharDevice means no data is being piped (stdin is a terminal) + if fi.Mode()&os.ModeCharDevice != 0 { + return nil, fmt.Errorf("no --file provided and no data piped to stdin") + } + + // Read all data from stdin + data, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, fmt.Errorf("failed to read from stdin: %w", err) + } + + return c.ConvertBytesToColletionItems(data) +} + +// FetchHistoryList returns movies and episodes that a user has watched, sorted by most recent. +func (c CommonLogic) FetchHistoryList(client *internal.Client, options *str.Options, page int) ([]*str.ExportlistItem, error) { + opts := uri.ListOptions{Page: page, Limit: options.PerPage, StartAt: options.StartDate, EndAt: options.EndDate, Extended: options.ExtendedInfo} + + list, resp, err := client.Sync.GetWatchedHistory( + client.BuildCtxFromOptions(options), + &options.TraktID, + &options.Type, + &opts, + ) + + if err != nil { + return nil, err + } + + // Check if there are more pages + if client.HavePages(page, resp, options.PagesLimit) { + time.Sleep(time.Duration(consts.SleepNumberOfSeconds) * time.Second) + + // Fetch items from the next page + nextPage := page + consts.NextPageStep + nextPageItems, err := c.FetchHistoryList(client, options, nextPage) + if err != nil { + return nil, err + } + + // Append items from the next page to the current page + list = append(list, nextPageItems...) + } + + return list, nil +} diff --git a/handlers/sync_add_to_collection_handler.go b/handlers/sync_add_to_collection_handler.go new file mode 100644 index 0000000..6d04b07 --- /dev/null +++ b/handlers/sync_add_to_collection_handler.go @@ -0,0 +1,46 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncAddToCollectionHandler struct for handler +type SyncAddToCollectionHandler struct{ common CommonLogic } + +// Handle to handle sync: add_to_collection action +func (m SyncAddToCollectionHandler) Handle(options *str.Options, client *internal.Client) error { + items, err := m.common.ReadInput(options.CollectionItems) + if err != nil { + return err + } + printer.Println("Add collection") + result, err := m.syncAddToCollection(client, options, items) + if err != nil { + return fmt.Errorf("add to collection error:%w", err) + } + + print("write result to:" + options.Output) + jsonData, _ := json.MarshalIndent(result, "", " ") + writer.WriteJSON(options, jsonData) + + return nil +} + +func (SyncAddToCollectionHandler) syncAddToCollection(client *internal.Client, options *str.Options, items *str.CollectionItems) (*str.CollectionAddResult, error) { + result, err := client.Sync.AddItemsToCollection( + client.BuildCtxFromOptions(options), + items, + ) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/handlers/sync_get_collection_handler.go b/handlers/sync_get_collection_handler.go new file mode 100644 index 0000000..0980e4d --- /dev/null +++ b/handlers/sync_get_collection_handler.go @@ -0,0 +1,59 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/consts" + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/uri" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncGetCollectionHandler struct for handler +type SyncGetCollectionHandler struct{ common CommonLogic } + +// Handle to handle sync: get_collection action +func (m SyncGetCollectionHandler) Handle(options *str.Options, client *internal.Client) error { + printer.Println("Get collection type:", options.Type) + items, err := m.syncGetCollectionItems(client, options) + if err != nil { + return fmt.Errorf("get collection error:%w", err) + } + + print("write data to:" + options.Output) + jsonData, _ := json.MarshalIndent(items, "", " ") + writer.WriteJSON(options, jsonData) + + return nil +} + +func (SyncGetCollectionHandler) syncGetCollectionItems(client *internal.Client, options *str.Options) ([]*str.ExportlistItem, error) { + opts := uri.ListOptions{Extended: options.ExtendedInfo} + + if options.Type == consts.Seasons { + items, _, err := client.Sync.GetCollectedSeasons( + client.BuildCtxFromOptions(options), + &opts, + ) + if err != nil { + return nil, err + } + + return items, nil + } + + items, _, err := client.Sync.GetCollection( + client.BuildCtxFromOptions(options), + &options.Type, + &opts, + ) + if err != nil { + return nil, err + } + + return items, nil +} diff --git a/handlers/sync_get_history_handler.go b/handlers/sync_get_history_handler.go new file mode 100644 index 0000000..f47e03c --- /dev/null +++ b/handlers/sync_get_history_handler.go @@ -0,0 +1,41 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/consts" + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncGetHistoryHandler struct for handler +type SyncGetHistoryHandler struct{ common CommonLogic } + +// Handle to handle sync: get_history action +func (m SyncGetHistoryHandler) Handle(options *str.Options, client *internal.Client) error { + err := m.common.CheckTypes(options) + if err != nil { + return err + } + + err = m.common.CheckDates(options.StartDate, options.EndDate, options.Timezone) + if err != nil { + return err + } + + printer.Println("Get watched history type:", options.Type) + items, err := m.common.FetchHistoryList(client, options, consts.DefaultPage) + if err != nil { + return fmt.Errorf("get watched error:%w", err) + } + + print("write data to:" + options.Output) + jsonData, _ := json.MarshalIndent(items, "", " ") + writer.WriteJSON(options, jsonData) + + return nil +} diff --git a/handlers/sync_get_watched_handler.go b/handlers/sync_get_watched_handler.go new file mode 100644 index 0000000..0719847 --- /dev/null +++ b/handlers/sync_get_watched_handler.go @@ -0,0 +1,50 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/uri" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncGetWatchedHandler struct for handler +type SyncGetWatchedHandler struct{ common CommonLogic } + +// Handle to handle sync: get_watched action +func (m SyncGetWatchedHandler) Handle(options *str.Options, client *internal.Client) error { + err := m.common.CheckTypes(options) + if err != nil { + return err + } + + printer.Println("Get watched type:", options.Type) + items, err := m.syncGetWatchedItems(client, options) + if err != nil { + return fmt.Errorf("get watched error:%w", err) + } + + print("write data to:" + options.Output) + jsonData, _ := json.MarshalIndent(items, "", " ") + writer.WriteJSON(options, jsonData) + + return nil +} + +func (SyncGetWatchedHandler) syncGetWatchedItems(client *internal.Client, options *str.Options) ([]*str.UserWatched, error) { + opts := uri.ListOptions{Extended: options.ExtendedInfo} + items, _, err := client.Sync.GetWatched( + client.BuildCtxFromOptions(options), + &options.Type, + &opts, + ) + if err != nil { + return nil, err + } + + return items, nil +} diff --git a/handlers/sync_handler.go b/handlers/sync_handler.go new file mode 100644 index 0000000..148197b --- /dev/null +++ b/handlers/sync_handler.go @@ -0,0 +1,12 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/str" +) + +// SyncHandler interface to handle sync module action +type SyncHandler interface { + Handle(options *str.Options, client *internal.Client) error +} diff --git a/handlers/sync_last_activities_handler.go b/handlers/sync_last_activities_handler.go new file mode 100644 index 0000000..7f76842 --- /dev/null +++ b/handlers/sync_last_activities_handler.go @@ -0,0 +1,41 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncLastActivitiesHandler struct for handler +type SyncLastActivitiesHandler struct{} + +// Handle to handle sync: last_activities action +func (m SyncLastActivitiesHandler) Handle(options *str.Options, client *internal.Client) error { + printer.Println("Get last activities") + activities, err := m.syncLastActivities(client, options) + if err != nil { + return fmt.Errorf("fetch last activities error:%w", err) + } + + print("write data to:" + options.Output) + jsonData, _ := json.MarshalIndent(activities, "", " ") + writer.WriteJSON(options, jsonData) + return nil +} + +func (SyncLastActivitiesHandler) syncLastActivities(client *internal.Client, options *str.Options) (*str.UserLastActivities, error) { + activities, _, err := client.Sync.GetLastActivity( + client.BuildCtxFromOptions(options), + ) + + if err != nil { + return nil, err + } + + return activities, nil +} diff --git a/handlers/sync_playback_handler.go b/handlers/sync_playback_handler.go new file mode 100644 index 0000000..2812b6f --- /dev/null +++ b/handlers/sync_playback_handler.go @@ -0,0 +1,73 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "time" + + "github.com/mfederowicz/trakt-sync/consts" + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/uri" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncPlaybackHandler struct for handler +type SyncPlaybackHandler struct{ common CommonLogic } + +// Handle to handle sync: playback action +func (m SyncPlaybackHandler) Handle(options *str.Options, client *internal.Client) error { + printer.Println("Returns playback progress.") + + err := m.common.CheckTypes(options) + if err != nil { + return err + } + + err = m.common.CheckDates(options.StartDate, options.EndDate, options.Timezone) + if err != nil { + return err + } + + result, _, err := m.syncPlayback(client, options, consts.DefaultPage) + + if err != nil { + return err + } + + print("write data to:" + options.Output) + jsonData, _ := json.MarshalIndent(result, "", " ") + writer.WriteJSON(options, jsonData) + return nil +} + +func (m SyncPlaybackHandler) syncPlayback(client *internal.Client, options *str.Options, page int) ([]*str.PlaybackProgress, *str.Response, error) { + opts := uri.ListOptions{Page: page, Limit: options.PerPage, StartAt: options.StartDate, EndAt: options.EndDate} + list, resp, err := client.Sync.GetPlaybackProgress( + client.BuildCtxFromOptions(options), + &options.Type, + &opts, + ) + + if err != nil { + return nil, nil, err + } + + // Check if there are more pages + if client.HavePages(page, resp, options.PagesLimit) { + time.Sleep(time.Duration(consts.SleepNumberOfSeconds) * time.Second) + + // Fetch items from the next page + nextPage := page + consts.NextPageStep + nextPageItems, _, err := m.syncPlayback(client, options, nextPage) + if err != nil { + return nil, nil, err + } + + // Append items from the next page to the current page + list = append(list, nextPageItems...) + } + + return list, resp, nil +} diff --git a/handlers/sync_remove_from_collection_handler.go b/handlers/sync_remove_from_collection_handler.go new file mode 100644 index 0000000..9fe544c --- /dev/null +++ b/handlers/sync_remove_from_collection_handler.go @@ -0,0 +1,46 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "encoding/json" + "fmt" + + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" + "github.com/mfederowicz/trakt-sync/writer" +) + +// SyncRemoveFromCollectionHandler struct for handler +type SyncRemoveFromCollectionHandler struct{ common CommonLogic } + +// Handle to handle sync: add_to_collection action +func (m SyncRemoveFromCollectionHandler) Handle(options *str.Options, client *internal.Client) error { + items, err := m.common.ReadInput(options.CollectionItems) + if err != nil { + return err + } + printer.Println("Remove from collection") + result, err := m.syncRemoveFromCollection(client, options, items) + if err != nil { + return fmt.Errorf("remove from collection error:%w", err) + } + + print("write result to:" + options.Output) + jsonData, _ := json.MarshalIndent(result, "", " ") + writer.WriteJSON(options, jsonData) + + return nil +} + +func (SyncRemoveFromCollectionHandler) syncRemoveFromCollection(client *internal.Client, options *str.Options, items *str.CollectionItems) (*str.CollectionRemoveResult, error) { + result, err := client.Sync.RemoveItemsFromCollection( + client.BuildCtxFromOptions(options), + items, + ) + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/handlers/sync_remove_playback_handler.go b/handlers/sync_remove_playback_handler.go new file mode 100644 index 0000000..a6d2290 --- /dev/null +++ b/handlers/sync_remove_playback_handler.go @@ -0,0 +1,49 @@ +// Package handlers used to handle module actions +package handlers + +import ( + "errors" + "fmt" + "net/http" + + "github.com/mfederowicz/trakt-sync/consts" + "github.com/mfederowicz/trakt-sync/internal" + "github.com/mfederowicz/trakt-sync/printer" + "github.com/mfederowicz/trakt-sync/str" +) + +// SyncRemovePlaybackHandler struct for handler +type SyncRemovePlaybackHandler struct{ common CommonLogic } + +// Handle to handle sync: remove_playback action +func (m SyncRemovePlaybackHandler) Handle(options *str.Options, client *internal.Client) error { + if options.PlaybackID == consts.ZeroValue { + return errors.New("empty playback_id") + } + + printer.Println("Remove playback item:", options.PlaybackID) + _, err := m.syncRemovePlaybackItem(client, options) + + if err != nil { + return err + } + + return nil +} + +func (SyncRemovePlaybackHandler) syncRemovePlaybackItem(client *internal.Client, options *str.Options) (*str.Response, error) { + resp, err := client.Sync.RemovePlaybackItem( + client.BuildCtxFromOptions(options), + &options.PlaybackID, + ) + + if err != nil { + return nil, fmt.Errorf("%w", err) + } + + if resp.StatusCode == http.StatusNoContent { + return nil, fmt.Errorf("result: success, remove playback item:%d", options.PlaybackID) + } + + return nil, nil +} diff --git a/internal/sync_service.go b/internal/sync_service.go index e9840ee..36fb4eb 100644 --- a/internal/sync_service.go +++ b/internal/sync_service.go @@ -5,7 +5,10 @@ import ( "context" "fmt" "net/http" + "slices" + "time" + "github.com/mfederowicz/trakt-sync/consts" "github.com/mfederowicz/trakt-sync/printer" "github.com/mfederowicz/trakt-sync/str" "github.com/mfederowicz/trakt-sync/uri" @@ -52,7 +55,7 @@ func (s *SyncService) GetCollection(ctx context.Context, types *string, opts *ur // GetWatchedHistory Returns movies and episodes that a user has watched, sorted by most recent. // // API docs: https://trakt.docs.apiary.io/#reference/sync/get-watched/get-watched-history -func (s *SyncService) GetWatchedHistory(ctx context.Context, types *string, opts *uri.ListOptions) ([]*str.ExportlistItem, *str.Response, error) { +func (s *SyncService) GetWatchedHistory(ctx context.Context, id *int, types *string, opts *uri.ListOptions) ([]*str.ExportlistItem, *str.Response, error) { var url string if types != nil { @@ -61,6 +64,10 @@ func (s *SyncService) GetWatchedHistory(ctx context.Context, types *string, opts url = "sync/history" } + if *id > consts.ZeroValue { + url = fmt.Sprintf(url+"/%d", *id) + } + url, err := uri.AddQuery(url, opts) if err != nil { return nil, nil, err @@ -113,3 +120,187 @@ func (s *SyncService) GetWatchlist(ctx context.Context, types *string, sort *str return list, resp, nil } + +// GetLastActivity Returns trakt user activity. +// +// API docs: https://trakt.docs.apiary.io/#reference/sync/last-activities/get-last-activity +func (s *SyncService) GetLastActivity(ctx context.Context) (*str.UserLastActivities, *str.Response, error) { + var url string + url = "sync/last_activities" + + printer.Println("fetch last activities url:" + url) + req, err := s.client.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + result := new(str.UserLastActivities) + resp, err := s.client.Do(ctx, req, &result) + + if err != nil { + printer.Println("fetch activities err:" + err.Error()) + return nil, resp, err + } + + return result, resp, nil +} + +// GetPlaybackProgress Returns playback progress. +// +// API docs:https://trakt.docs.apiary.io/#reference/sync/playback/get-playback-progress +func (s *SyncService) GetPlaybackProgress(ctx context.Context, types *string, opts *uri.ListOptions) ([]*str.PlaybackProgress, *str.Response, error) { + var url string + if types != nil { + url = fmt.Sprintf("sync/playback/%s", *types) + } else { + url = "sync/playback" + } + url, err := uri.AddQuery(url, opts) + if err != nil { + return nil, nil, err + } + printer.Println("fetch playback url:" + url) + req, err := s.client.NewRequest("GET", url, nil) + if err != nil { + return nil, nil, err + } + + list := []*str.PlaybackProgress{} + resp, err := s.client.Do(ctx, req, &list) + + if err != nil { + printer.Println("fetch playback err:" + err.Error()) + return nil, resp, err + } + return list, resp, nil +} + +// RemovePlaybackItem removes playback item with selected id +// +// API docs:https://trakt.docs.apiary.io/#reference/sync/remove-playback/remove-a-playback-item +func (s *SyncService) RemovePlaybackItem(ctx context.Context, id *int) (*str.Response, error) { + var url = fmt.Sprintf("sync/playback/%d", *id) + req, err := s.client.NewRequest(http.MethodDelete, url, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + + if resp.StatusCode == http.StatusNotFound { + err = fmt.Errorf(consts.PlaybackNotFoundWithID, *id) + } + + if err != nil { + return nil, err + } + + return resp, nil +} + +// AddItemsToCollection add items to user's collection +// +// API docs:https://trakt.docs.apiary.io/#reference/sync/add-to-collection/add-items-to-collection +func (s *SyncService) AddItemsToCollection(ctx context.Context, items *str.CollectionItems) (*str.CollectionAddResult, error) { + var url = "sync/collection" + printer.Println("add items") + req, err := s.client.NewRequest(http.MethodPost, url, items) + if err != nil { + return nil, err + } + + result := new(str.CollectionAddResult) + _, err = s.client.Do(ctx, req, result) + if err != nil { + return result, err + } + + return result, nil +} + +// GetCollectedSeasons dedicated function do prepare collection: seasons format +func (s *SyncService) GetCollectedSeasons(ctx context.Context, options *uri.ListOptions) ([]*str.ExportlistItem, *str.Response, error) { + // fetch collected shows + strType := consts.Shows + shows, resp, err := s.GetCollection(ctx, &strType, options) + if err != nil { + return nil, resp, err + } + collected := []str.Season{} + for _, val := range shows { + time.Sleep(time.Duration(consts.SleepNumberOfSeconds) * time.Second) + + seasonsNumbers := []int{} + for _, sitem := range *val.Seasons { + seasonsNumbers = append(seasonsNumbers, *sitem.Number) + } + + seasons, _, err := s.client.Shows.GetAllSeasonsForShow(ctx, val.Show.IDs.Slug, options) + if err != nil { + return nil, resp, err + } + for _, sitem := range seasons { + if slices.Contains(seasonsNumbers, *sitem.Number) { + s := str.Season{} + s.IDs = sitem.IDs + collected = append(collected, s) + } + } + } + + strType = consts.Season + list := []*str.ExportlistItem{} + for _, citem := range collected { + item := &str.ExportlistItem{} + item.Type = &strType + item.Season = &citem + list = append(list, item) + } + + return list, nil, nil +} + +// RemoveItemsFromCollection remove items from user's collection +// +// API docs:https://trakt.docs.apiary.io/#reference/sync/remove-from-collection/remove-items-from-collection +func (s *SyncService) RemoveItemsFromCollection(ctx context.Context, items *str.CollectionItems) (*str.CollectionRemoveResult, error) { + var url = "sync/collection/remove" + printer.Println("remove items") + req, err := s.client.NewRequest(http.MethodPost, url, items) + if err != nil { + return nil, err + } + + result := new(str.CollectionRemoveResult) + _, err = s.client.Do(ctx, req, result) + if err != nil { + return result, err + } + + return result, nil +} + +// GetWatched Returns all movies or shows a user has watched sorted by most plays. +// +// API docs:https://trakt.docs.apiary.io/#reference/sync/get-watched/get-watched +func (s *SyncService) GetWatched(ctx context.Context, watchType *string, opts *uri.ListOptions) ([]*str.UserWatched, *str.Response, error) { + var url string + url = fmt.Sprintf("sync/watched/%s", *watchType) + url, err := uri.AddQuery(url, opts) + if err != nil { + return nil, nil, err + } + printer.Println("get watched url:" + url) + req, err := s.client.NewRequest(http.MethodGet, url, nil) + if err != nil { + return nil, nil, err + } + watched := []*str.UserWatched{} + resp, err := s.client.Do(ctx, req, &watched) + + if err != nil { + return nil, resp, err + } + + return watched, resp, nil +} diff --git a/printer/print.go b/printer/print.go index 9e0dde7..49171ea 100644 --- a/printer/print.go +++ b/printer/print.go @@ -2,11 +2,22 @@ package printer import ( + "encoding/json" "fmt" "io" "log" ) +// Printlnjson wraps fmt.Println and handles errors +func Printlnjson(v ...any) { + str, _ := json.Marshal(v) + jsonString := string(str) + _, err := fmt.Println(jsonString) + if err != nil { + log.Printf("Println error: %v", err) + } +} + // Println wraps fmt.Println and handles errors func Println(v ...any) { _, err := fmt.Println(v...) diff --git a/str/account.go b/str/account.go new file mode 100644 index 0000000..a19eb47 --- /dev/null +++ b/str/account.go @@ -0,0 +1,15 @@ +// Package str used for structs +package str + +// Account represents JSON account object +type Account struct { + SettingsAt *Timestamp `json:"settings_at,omitempty"` + FollowedAt *Timestamp `json:"followed_at,omitempty"` + FollowingAt *Timestamp `json:"following_at,omitempty"` + PenndingAt *Timestamp `json:"pending_at,omitempty"` + RequestedAt *Timestamp `json:"requested_at,omitempty"` +} + +func (f Account) String() string { + return Stringify(f) +} diff --git a/str/collection_add_result.go b/str/collection_add_result.go new file mode 100644 index 0000000..d00f229 --- /dev/null +++ b/str/collection_add_result.go @@ -0,0 +1,14 @@ +// Package str used for structs +package str + +// CollectionAddResult represents JSON collection add result object +type CollectionAddResult struct { + Added *CollectionResultCounters `json:"added,omitempty"` + Updated *CollectionResultCounters `json:"updated,omitempty"` + Existing *CollectionResultCounters `json:"existing,omitempty"` + NotFound *CollectionResultNotFound `json:"not_found,omitempty"` +} + +func (c CollectionAddResult) String() string { + return Stringify(c) +} diff --git a/str/collection_items.go b/str/collection_items.go new file mode 100644 index 0000000..b422448 --- /dev/null +++ b/str/collection_items.go @@ -0,0 +1,14 @@ +// Package str used for structs +package str + +// CollectionItems represents JSON collection items object +type CollectionItems struct { + Movies *[]ExportlistItem `json:"movies,omitempty"` + Shows *[]ExportlistItem `json:"shows,omitempty"` + Seasons *[]ExportlistItem `json:"seasons,omitempty"` + Episodes *[]ExportlistItem `json:"episodes,omitempty"` +} + +func (c CollectionItems) String() string { + return Stringify(c) +} diff --git a/str/collection_remove_result.go b/str/collection_remove_result.go new file mode 100644 index 0000000..4783837 --- /dev/null +++ b/str/collection_remove_result.go @@ -0,0 +1,12 @@ +// Package str used for structs +package str + +// CollectionRemoveResult represents JSON collection remove result object +type CollectionRemoveResult struct { + Deleted *CollectionResultCounters `json:"deleted,omitempty"` + NotFound *CollectionResultNotFound `json:"not_found,omitempty"` +} + +func (c CollectionRemoveResult) String() string { + return Stringify(c) +} diff --git a/str/collection_result_counters.go b/str/collection_result_counters.go new file mode 100644 index 0000000..725ac46 --- /dev/null +++ b/str/collection_result_counters.go @@ -0,0 +1,14 @@ +// Package str used for structs +package str + +// CollectionResultCounters represents JSON counters object +type CollectionResultCounters struct { + Movies *int `json:"movies,omitempty"` + Episodes *int `json:"episodes,omitempty"` + Shows *int `json:"shows,omitempty"` + Seasons *int `json:"seasons,omitempty"` +} + +func (c CollectionResultCounters) String() string { + return Stringify(c) +} diff --git a/str/collection_result_not_found.go b/str/collection_result_not_found.go new file mode 100644 index 0000000..ae750b4 --- /dev/null +++ b/str/collection_result_not_found.go @@ -0,0 +1,14 @@ +// Package str used for structs +package str + +// CollectionResultNotFound represents JSON not found object +type CollectionResultNotFound struct { + Movies *[]ExportlistItem `json:"movies,omitempty"` + Shows *[]Show `json:"shows,omitempty"` + Seasons *[]Season `json:"seasons,omitempty"` + Episodes *[]Episodes `json:"episodes,omitempty"` +} + +func (c CollectionResultNotFound) String() string { + return Stringify(c) +} diff --git a/str/comments.go b/str/comments.go new file mode 100644 index 0000000..1f177f5 --- /dev/null +++ b/str/comments.go @@ -0,0 +1,12 @@ +// Package str used for structs +package str + +// Comments represents JSON sesons object +type Comments struct { + LikedAt *Timestamp `json:"liked_at,omitempty"` + BlockedAt *Timestamp `json:"blocked_at,omitempty"` +} + +func (c Comments) String() string { + return Stringify(c) +} diff --git a/str/episode.go b/str/episode.go index 7b1650f..e1296a7 100644 --- a/str/episode.go +++ b/str/episode.go @@ -18,12 +18,32 @@ type Episode struct { UpdatedAt *Timestamp `json:"updated_at,omitempty"` CompletedAt *Timestamp `json:"completed_at,omitempty"` CollectedAt *Timestamp `json:"collected_at,omitempty"` + Metadata *Metadata `json:"metadata,omitempty"` AvailableTranslations *[]string `json:"available_translations,omitempty"` Runtime *int `json:"runtime,omitempty"` EpisodeType *string `json:"episode_type,omitempty"` Completed *bool `json:"completed,omitempty"` + MediaType *string `json:"media_type,omitempty"` + Resolution *string `json:"resolution,omitempty"` + HDR *string `json:"hdr,omitempty"` + Audio *string `json:"audio,omitempty"` + AudioChannels *string `json:"audio_channels,omitempty"` + ThreeD *bool `json:"3d,omitempty"` } -func (s Episode) String() string { - return Stringify(s) +func (e Episode) String() string { + return Stringify(e) +} + +// UpdateCollectedData update meta data of object +func (e *Episode) UpdateCollectedData(item *ExportlistItem) { + if item.Metadata != nil { + e.MediaType = item.Metadata.MediaType + e.Resolution = item.Metadata.Resolution + e.Audio = item.Metadata.Audio + e.AudioChannels = item.Metadata.AudioChannels + e.ThreeD = item.Metadata.ThreeD + } + + e.CollectedAt = item.CollectedAt.UTC() } diff --git a/str/episodes.go b/str/episodes.go index d181e63..94afc10 100644 --- a/str/episodes.go +++ b/str/episodes.go @@ -3,12 +3,19 @@ package str // Episodes represents JSON episodes object type Episodes struct { - Plays *int `json:"plays,omitempty"` - Watched *int `json:"watched,omitempty"` - Minutes *int `json:"minutes,omitempty"` - Collected *int `json:"collected,omitempty"` - Ratings *int `json:"ratings,omitempty"` - Comments *int `json:"comments,omitempty"` + Plays *int `json:"plays,omitempty"` + Watched *int `json:"watched,omitempty"` + Minutes *int `json:"minutes,omitempty"` + Collected *int `json:"collected,omitempty"` + Ratings *int `json:"ratings,omitempty"` + Comments *int `json:"comments,omitempty"` + WatchedAt *Timestamp `json:"watched_at,omitempty"` + CollectedAt *Timestamp `json:"collected_at,omitempty"` + RatedAt *Timestamp `json:"rated_at,omitempty"` + WatchlistedAt *Timestamp `json:"watchlisted_at,omitempty"` + FavoritedAt *Timestamp `json:"favorited_at,omitempty"` + CommentedAt *Timestamp `json:"commented_at,omitempty"` + PausedAt *Timestamp `json:"paused_at,omitempty"` } func (e Episodes) String() string { diff --git a/str/export_list_item.go b/str/export_list_item.go index a803f9a..520fb70 100644 --- a/str/export_list_item.go +++ b/str/export_list_item.go @@ -46,8 +46,11 @@ func (i *ExportlistItemJSON) Uptime(options *Options, data *ExportlistItem) { // ExportlistItem represents JSON for list item type ExportlistItem struct { + Title *string `json:"title,omitempty"` + Year *int `json:"year,omitempty"` Rank *int `json:"rank,omitempty"` ID *int64 `json:"id,omitempty"` + IDs *IDs `json:"ids,omitempty"` WatchedAt *Timestamp `json:"watched_at,omitempty"` ListedAt *Timestamp `json:"listed_at,omitempty"` CollectedAt *Timestamp `json:"collected_at,omitempty"` @@ -59,8 +62,15 @@ type ExportlistItem struct { Movie *Movie `json:"movie,omitempty"` Show *Show `json:"show,omitempty"` Season *Season `json:"season,omitempty"` + Seasons *[]Season `json:"seasons,omitempty"` Episode *Episode `json:"episode,omitempty"` Metadata *Metadata `json:"metadata,omitempty"` + MediaType *string `json:"media_type,omitempty"` + Resolution *string `json:"resolution,omitempty"` + Hdr *string `json:"hdr,omitempty"` + Audio *string `json:"audio,omitempty"` + AudioChannels *string `json:"audio_channels,omitempty"` + ThreeD *bool `json:"3d,omitempty"` } func (i ExportlistItem) String() string { @@ -95,3 +105,41 @@ func (i ExportlistItem) GetTime() *Timestamp { return nil } + +// UpdateCollectedData update meta data of object +func (i *ExportlistItem) UpdateCollectedData(item *ExportlistItem) { + if item.CollectedAt != nil { + i.CollectedAt = item.CollectedAt.UTC() + } + + if item.Metadata != nil { + i.MediaType = item.Metadata.MediaType + i.Resolution = item.Metadata.Resolution + i.Audio = item.Metadata.Audio + i.AudioChannels = item.Metadata.AudioChannels + i.ThreeD = item.Metadata.ThreeD + } + + if item.Seasons != nil { + i.Seasons = &[]Season{} + for _, season := range *item.Seasons { + s := Season{} + s.Number = season.Number + if season.Episodes != nil { + s.Episodes = &[]Episode{} + for _, ep := range *season.Episodes { + e := Episode{} + e.Number = ep.Number + e.MediaType = ep.Metadata.MediaType + e.Resolution = ep.Metadata.Resolution + e.Audio = ep.Metadata.Audio + e.AudioChannels = ep.Metadata.AudioChannels + e.ThreeD = ep.Metadata.ThreeD + + *s.Episodes = append(*s.Episodes, e) + } + } + *i.Seasons = append(*i.Seasons, s) + } + } +} diff --git a/str/favorites.go b/str/favorites.go index c185483..42d3c23 100644 --- a/str/favorites.go +++ b/str/favorites.go @@ -3,7 +3,8 @@ package str // Favorites represents JSON favorites object type Favorites struct { - ItemCount *int `json:"item_count,omitempty"` + ItemCount *int `json:"item_count,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } func (f Favorites) String() string { diff --git a/str/lists.go b/str/lists.go new file mode 100644 index 0000000..f069897 --- /dev/null +++ b/str/lists.go @@ -0,0 +1,13 @@ +// Package str used for structs +package str + +// Lists represents JSON lists object +type Lists struct { + LikedAt *Timestamp `json:"liked_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + CommentedAt *Timestamp `json:"commented_at,omitempty"` +} + +func (l Lists) String() string { + return Stringify(l) +} diff --git a/str/movie.go b/str/movie.go index 74c5c80..cd0992c 100644 --- a/str/movie.go +++ b/str/movie.go @@ -18,12 +18,32 @@ type Movie struct { Votes *int `json:"votes,omitempty"` CommentCount *int `json:"comment_count,omitempty"` UpdatedAt *Timestamp `json:"updated_at,omitempty"` + CollectedAt *Timestamp `json:"collected_at,omitempty"` Language *string `json:"language,omitempty"` Languages *[]string `json:"languages,omitempty"` AvailableTranslations *[]string `json:"available_translations,omitempty"` Genres *[]string `json:"genres,omitempty"` Certification *string `json:"certification,omitempty"` User *UserProfile `json:"user,omitempty"` + MediaType *string `json:"media_type,omitempty"` + Resolution *string `json:"resolution,omitempty"` + HDR *string `json:"hdr,omitempty"` + Audio *string `json:"audio,omitempty"` + AudioChannels *string `json:"audio_channels,omitempty"` + ThreeD *bool `json:"3d,omitempty"` +} + +// UpdateCollectedData update meta data of object +func (m *Movie) UpdateCollectedData(item *ExportlistItem) { + if item.Metadata != nil { + m.MediaType = item.Metadata.MediaType + m.Resolution = item.Metadata.Resolution + m.Audio = item.Metadata.Audio + m.AudioChannels = item.Metadata.AudioChannels + m.ThreeD = item.Metadata.ThreeD + } + + m.CollectedAt = item.CollectedAt.UTC() } func (m Movie) String() string { diff --git a/str/movies.go b/str/movies.go index ef79a4e..640514c 100644 --- a/str/movies.go +++ b/str/movies.go @@ -3,12 +3,20 @@ package str // Movies represents JSON movies object type Movies struct { - Plays *int `json:"plays,omitempty"` - Watched *int `json:"watched,omitempty"` - Minutes *int `json:"minutes,omitempty"` - Collected *int `json:"collected,omitempty"` - Ratings *int `json:"ratings,omitempty"` - Comments *int `json:"comments,omitempty"` + Watched *int `json:"watched,omitempty"` + Collected *int `json:"collected,omitempty"` + Plays *int `json:"plays,omitempty"` + Minutes *int `json:"minutes,omitempty"` + Ratings *int `json:"ratings,omitempty"` + Comments *int `json:"comments,omitempty"` + WatchedAt *Timestamp `json:"watched_at,omitempty"` + CollectedAt *Timestamp `json:"collected_at,omitempty"` + RatedAt *Timestamp `json:"rated_at,omitempty"` + WatchlistedAt *Timestamp `json:"watchlisted_at,omitempty"` + FavoritedAt *Timestamp `json:"favorited_at,omitempty"` + CommentedAt *Timestamp `json:"commented_at,omitempty"` + PausedAt *Timestamp `json:"paused_at,omitempty"` + HiddenAt *Timestamp `json:"hidden_at,omitempty"` } func (m Movies) String() string { diff --git a/str/options.go b/str/options.go index ae4afe3..f86f245 100644 --- a/str/options.go +++ b/str/options.go @@ -4,6 +4,7 @@ package str // Options represents a app opions. type Options struct { Action string + CollectionItems string Comment string CommentID int CommentType string @@ -50,9 +51,11 @@ type Options struct { Specials string Spoiler bool StartDate string + EndDate string Time string Token Token TraktID int + PlaybackID int Translations Slice Type string Timezone string diff --git a/str/playback_progress.go b/str/playback_progress.go new file mode 100644 index 0000000..06eb004 --- /dev/null +++ b/str/playback_progress.go @@ -0,0 +1,17 @@ +// Package str used for structs +package str + +// PlaybackProgress represents JSON playback_progress object +type PlaybackProgress struct { + Progress *float32 `json:"aired,omitempty"` + PausedAt *Timestamp `json:"paused_at,omitempty"` + ID *int64 `json:"id,omitempty"` + Type *string `json:"type,omitempty"` + Episode *Episode `json:"episode,omitempty"` + Show *Show `json:"show,omitempty"` + Movie *Movie `json:"movie,omitempty"` +} + +func (s PlaybackProgress) String() string { + return Stringify(s) +} diff --git a/str/season.go b/str/season.go index 760a5c9..e2db51d 100644 --- a/str/season.go +++ b/str/season.go @@ -12,13 +12,32 @@ type Season struct { Rating *float32 `json:"rating,omitempty"` Votes *int `json:"votes,omitempty"` EpisodeCount *int `json:"episode_count,omitempty"` + Episodes *[]Episode `json:"episodes,omitempty"` AiredEpisodes *int `json:"aired_episodes,omitempty"` Overview *string `json:"overview,omitempty"` FirstAired *Timestamp `json:"first_aired,omitempty"` + CollectedAt *Timestamp `json:"collected_at,omitempty"` UpdatedAt *Timestamp `json:"updated_at,omitempty"` Network *string `json:"network,omitempty"` + MediaType *string `json:"media_type,omitempty"` + Resolution *string `json:"resolution,omitempty"` + HDR *string `json:"hdr,omitempty"` + Audio *string `json:"audio,omitempty"` + AudioChannels *string `json:"audio_channels,omitempty"` + ThreeD *bool `json:"3d,omitempty"` } func (s Season) String() string { return Stringify(s) } + +// UpdateCollectedData update meta data of object +func (s *Season) UpdateCollectedData(item *ExportlistItem) { + if item.Metadata != nil { + s.MediaType = item.Metadata.MediaType + s.Resolution = item.Metadata.Resolution + s.Audio = item.Metadata.Audio + s.AudioChannels = item.Metadata.AudioChannels + s.ThreeD = item.Metadata.ThreeD + } +} diff --git a/str/seasons.go b/str/seasons.go index 10ca3d6..db54e82 100644 --- a/str/seasons.go +++ b/str/seasons.go @@ -3,8 +3,12 @@ package str // Seasons represents JSON sesons object type Seasons struct { - Ratings *int `json:"ratings,omitempty"` - Comments *int `json:"comments,omitempty"` + Ratings *int `json:"ratings,omitempty"` + Comments *int `json:"comments,omitempty"` + RatedAt *Timestamp `json:"rated_at,omitempty"` + WatchlistedAt *Timestamp `json:"watchlisted_at,omitempty"` + CommentedAt *Timestamp `json:"commented_at,omitempty"` + HiddenAt *Timestamp `json:"hidden_at,omitempty"` } func (s Seasons) String() string { diff --git a/str/show.go b/str/show.go index ba0f10c..1ae7e5b 100644 --- a/str/show.go +++ b/str/show.go @@ -21,13 +21,32 @@ type Show struct { Votes *int `json:"votes,omitempty"` CommentCount *int `json:"comment_count,omitempty"` UpdatedAt *Timestamp `json:"updated_at,omitempty"` + CollectedAt *Timestamp `json:"collected_at,omitempty"` Language *string `json:"language,omitempty"` Languages *[]string `json:"languages,omitempty"` AvailableTranslations *[]string `json:"available_translations,omitempty"` Genres *[]string `json:"genres,omitempty"` AiredEpisodes *int `json:"aired_episodes,omitempty"` + MediaType *string `json:"media_type,omitempty"` + Resolution *string `json:"resolution,omitempty"` + HDR *string `json:"hdr,omitempty"` + Audio *string `json:"audio,omitempty"` + AudioChannels *string `json:"audio_channels,omitempty"` + ThreeD *bool `json:"3d,omitempty"` } func (s Show) String() string { return Stringify(s) } + +// UpdateCollectedData update meta data of object +func (s *Show) UpdateCollectedData(item *ExportlistItem) { + if item.Metadata != nil { + s.MediaType = item.Metadata.MediaType + s.Resolution = item.Metadata.Resolution + s.Audio = item.Metadata.Audio + s.AudioChannels = item.Metadata.AudioChannels + s.ThreeD = item.Metadata.ThreeD + } + s.CollectedAt = item.CollectedAt.UTC() +} diff --git a/str/shows.go b/str/shows.go index ad4325b..ed7828e 100644 --- a/str/shows.go +++ b/str/shows.go @@ -3,10 +3,16 @@ package str // Shows represents JSON shows object type Shows struct { - Watched *int `json:"watched,omitempty"` - Collected *int `json:"collected,omitempty"` - Ratings *int `json:"ratings,omitempty"` - Comments *int `json:"comments,omitempty"` + Watched *int `json:"watched,omitempty"` + Collected *int `json:"collected,omitempty"` + Ratings *int `json:"ratings,omitempty"` + Comments *int `json:"comments,omitempty"` + RatedAt *Timestamp `json:"rated_at,omitempty"` + WatchlistedAt *Timestamp `json:"watchlisted_at,omitempty"` + FavoritedAt *Timestamp `json:"favorited_at,omitempty"` + CommentedAt *Timestamp `json:"commented_at,omitempty"` + HiddenAt *Timestamp `json:"hidden_at,omitempty"` + DroppedAt *Timestamp `json:"dropped_at,omitempty"` } func (s Shows) String() string { diff --git a/str/timestamp.go b/str/timestamp.go index 085daf9..e98188f 100644 --- a/str/timestamp.go +++ b/str/timestamp.go @@ -15,6 +15,11 @@ func (t Timestamp) String() string { return t.Time.String() } +// UTC returns a copy of Timestamp with time converted to UTC. +func (t Timestamp) UTC() *Timestamp { + return &Timestamp{Time: t.Time.UTC()} +} + // Define the possible formats const ( dateFormat = "2006-01-02" diff --git a/str/user_last_activities.go b/str/user_last_activities.go new file mode 100644 index 0000000..3421dfe --- /dev/null +++ b/str/user_last_activities.go @@ -0,0 +1,25 @@ +// Package str used for structs +package str + +// UserLastActivities represents JSON user activities object +type UserLastActivities struct { + All *Timestamp `json:"all,omitempty"` + Movies *Movies `json:"movies,omitempty"` + Episodes *Episodes `json:"episodes,omitempty"` + Shows *Shows `json:"shows,omitempty"` + Seasons *Seasons `json:"seasons,omitempty"` + Comments *Comments `json:"comments,omitempty"` + Lists *Lists `json:"lists,omitempty"` + Watchlist *Watchlist `json:"watchlist,omitempty"` + Favorites *Favorites `json:"favorites,omitempty"` + Account *Account `json:"account,omitempty"` + SavedFilters *SavedFilter `json:"saved_filters,omitempty"` + Notes *Notes `json:"notes,omitempty"` + Connections *Connections `json:"connections,omitempty"` + SharingText *SharingText `json:"sharing_text,omitempty"` + Limits *Limits `json:"limits,omitempty"` +} + +func (u UserLastActivities) String() string { + return Stringify(u) +} diff --git a/str/user_watched.go b/str/user_watched.go index b868648..5c5204b 100644 --- a/str/user_watched.go +++ b/str/user_watched.go @@ -9,6 +9,7 @@ type UserWatched struct { ResetAt *Timestamp `json:"reset_at,omitempty"` Movie *Movie `json:"movie,omitempty"` Show *Show `json:"show,omitempty"` + Episode *Episode `json:"episode,omitempty"` Seasons *[]Season `json:"seasons,omitempty"` } diff --git a/str/watchlist.go b/str/watchlist.go index 1822a38..698615b 100644 --- a/str/watchlist.go +++ b/str/watchlist.go @@ -3,7 +3,8 @@ package str // Watchlist represents JSON watchlist object type Watchlist struct { - ItemCount *int `json:"item_count,omitempty"` + ItemCount *int `json:"item_count,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` } func (w Watchlist) String() string { diff --git a/uri/uri.go b/uri/uri.go index 1f78caf..2efc764 100644 --- a/uri/uri.go +++ b/uri/uri.go @@ -53,6 +53,8 @@ type ListOptions struct { Hidden string `url:"hidden,omitempty"` Specials string `url:"specials,omitempty"` CountSpecials string `url:"count_specials,omitempty"` + StartAt string `url:"start_at,omitempty"` + EndAt string `url:"end_at,omitempty"` } // AddQuery adds query parameters to s.