diff --git a/crunchyroll.go b/crunchyroll.go index 38b889b..7618fdf 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -38,13 +38,28 @@ const ( MOVIELISTING = "movie_listing" ) -// SortType represents a sort type. -type SortType string +// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after. +type BrowseSortType string const ( - POPULARITY SortType = "popularity" - NEWLYADDED = "newly_added" - ALPHABETICAL = "alphabetical" + BROWSESORTPOPULARITY BrowseSortType = "popularity" + BROWSESORTNEWLYADDED = "newly_added" + BROWSESORTALPHABETICAL = "alphabetical" +) + +// WatchlistLanguageType represents a filter type to filter Crunchyroll.WatchList entries after. +type WatchlistLanguageType int + +const ( + WATCHLISTLANGUAGESUBBED WatchlistLanguageType = iota + 1 + WATCHLISTLANGUAGEDUBBED +) + +type WatchlistContentType string + +const ( + WATCHLISTCONTENTSERIES WatchlistContentType = "series" + WATCHLISTCONTENTMOVIES = "movie_listing" ) type Crunchyroll struct { @@ -95,7 +110,7 @@ type BrowseOptions struct { Simulcast string `param:"season_tag"` // Sort specifies how the entries should be sorted. - Sort SortType `param:"sort_by"` + Sort BrowseSortType `param:"sort_by"` // Start specifies the index from which the entries should be returned. Start uint `param:"start"` @@ -329,6 +344,10 @@ func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, e if err != nil { return nil, err } + return c.requestFull(req) +} + +func (c *Crunchyroll) requestFull(req *http.Request) (*http.Response, error) { req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken)) return request(req, c.Client) @@ -812,6 +831,68 @@ func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, e return e, nil } +type WatchlistOptions struct { + // OrderAsc specified whether the results should be order ascending or descending. + OrderAsc bool + + // OnlyFavorites specifies whether only episodes which are marked as favorite should be returned. + OnlyFavorites bool + + // LanguageType specifies whether returning episodes should be only subbed or dubbed. + LanguageType WatchlistLanguageType + + // ContentType specified whether returning videos should only be series episodes or movies. + // But tbh all movies I've searched on crunchy were flagged as series too, so this + // parameter is kinda useless. + ContentType WatchlistContentType +} + +func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) { + values := url.Values{} + if options.OrderAsc { + values.Set("order", "asc") + } else { + values.Set("order", "desc") + } + if options.OnlyFavorites { + values.Set("only_favorites", "true") + } + switch options.LanguageType { + case WATCHLISTLANGUAGESUBBED: + values.Set("is_subbed", "true") + case WATCHLISTLANGUAGEDUBBED: + values.Set("is_dubbed", "true") + } + values.Set("n", strconv.Itoa(int(limit))) + values.Set("locale", string(c.Locale)) + + endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/%s/watchlist?%s", c.Config.AccountID, values.Encode()) + resp, err := c.request(endpoint, http.MethodGet) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + json.NewDecoder(resp.Body).Decode(&jsonBody) + + var watchlistEntries []*WatchlistEntry + if err := decodeMapToStruct(jsonBody["items"], &watchlistEntries); err != nil { + return nil, err + } + + for _, entry := range watchlistEntries { + switch entry.Panel.Type { + case WATCHLISTENTRYEPISODE: + entry.Panel.EpisodeMetadata.crunchy = c + case WATCHLISTENTRYSERIES: + entry.Panel.SeriesMetadata.crunchy = c + } + } + + return watchlistEntries, nil +} + // Account returns information about the currently logged in crunchyroll account. func (c *Crunchyroll) Account() (*Account, error) { resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet) diff --git a/episode.go b/episode.go index 0405e0e..e99bd12 100644 --- a/episode.go +++ b/episode.go @@ -1,6 +1,7 @@ package crunchyroll import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -39,11 +40,14 @@ type Episode struct { NextEpisodeID string `json:"next_episode_id"` NextEpisodeTitle string `json:"next_episode_title"` - HDFlag bool `json:"hd_flag"` - IsMature bool `json:"is_mature"` - MatureBlocked bool `json:"mature_blocked"` + HDFlag bool `json:"hd_flag"` + MaturityRatings []string `json:"maturity_ratings"` + IsMature bool `json:"is_mature"` + MatureBlocked bool `json:"mature_blocked"` - EpisodeAirDate time.Time `json:"episode_air_date"` + EpisodeAirDate time.Time `json:"episode_air_date"` + FreeAvailableDate time.Time `json:"free_available_date"` + PremiumAvailableDate time.Time `json:"premium_available_date"` IsSubbed bool `json:"is_subbed"` IsDubbed bool `json:"is_dubbed"` @@ -52,8 +56,9 @@ type Episode struct { SeoDescription string `json:"seo_description"` SeasonTags []string `json:"season_tags"` - AvailableOffline bool `json:"available_offline"` - Slug string `json:"slug"` + AvailableOffline bool `json:"available_offline"` + MediaType MediaType `json:"media_type"` + Slug string `json:"slug"` Images struct { Thumbnail [][]struct { @@ -87,6 +92,62 @@ type HistoryEpisode struct { FullyWatched bool `json:"fully_watched"` } +// WatchlistEntryType specifies which type a watchlist entry has. +type WatchlistEntryType string + +const ( + WATCHLISTENTRYEPISODE = "episode" + WATCHLISTENTRYSERIES = "series" +) + +// WatchlistEntry contains information about an entry on the watchlist. +type WatchlistEntry struct { + Panel struct { + Title string `json:"title"` + PromoTitle string `json:"promo_title"` + Slug string `json:"slug"` + Playback string `json:"playback"` + PromoDescription string `json:"promo_description"` + Images struct { + Thumbnail [][]struct { + Height int `json:"height"` + Source string `json:"source"` + Type string `json:"type"` + Width int `json:"width"` + } `json:"thumbnail"` + PosterTall [][]struct { + Width int `json:"width"` + Height int `json:"height"` + Type string `json:"type"` + Source string `json:"source"` + } `json:"poster_tall"` + PosterWide [][]struct { + Width int `json:"width"` + Height int `json:"height"` + Type string `json:"type"` + Source string `json:"source"` + } `json:"poster_wide"` + } `json:"images"` + ID string `json:"id"` + Description string `json:"description"` + ChannelID string `json:"channel_id"` + Type WatchlistEntryType `json:"type"` + ExternalID string `json:"external_id"` + SlugTitle string `json:"slug_title"` + // not null if Type is WATCHLISTENTRYEPISODE + EpisodeMetadata *Episode `json:"episode_metadata"` + // not null if Type is WATCHLISTENTRYSERIES + SeriesMetadata *Series `json:"series_metadata"` + } + + New bool `json:"new"` + NewContent bool `json:"new_content"` + IsFavorite bool `json:"is_favorite"` + NeverWatched bool `json:"never_watched"` + CompleteStatus bool `json:"complete_status"` + Playahead uint `json:"playahead"` +} + // EpisodeFromID returns an episode by its api id. func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) { resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", @@ -122,6 +183,30 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) { return episode, nil } +// AddToWatchlist adds the current episode to the watchlist. +// There is currently a bug, or as I like to say in context of the crunchyroll api, feature, +// that only series and not individual episode can be added to the watchlist. Even though +// I somehow got an episode to my watchlist on the crunchyroll website, it never worked with the +// api here. So this function actually adds the whole series to the watchlist. +func (e *Episode) AddToWatchlist() error { + endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", e.crunchy.Config.AccountID, e.crunchy.Locale) + body, _ := json.Marshal(map[string]string{"content_id": e.SeriesID}) + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + _, err = e.crunchy.requestFull(req) + return err +} + +// RemoveFromWatchlist removes the current episode from the watchlist. +func (e *Episode) RemoveFromWatchlist() error { + endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", e.crunchy.Config.AccountID, e.SeriesID, e.crunchy.Locale) + _, err := e.crunchy.request(endpoint, http.MethodDelete) + return err +} + // AudioLocale returns the audio locale of the episode. // Every episode in a season (should) have the same audio locale, // so if you want to get the audio locale of a season, just call diff --git a/video.go b/video.go index 365f954..42efacd 100644 --- a/video.go +++ b/video.go @@ -1,6 +1,7 @@ package crunchyroll import ( + "bytes" "encoding/json" "fmt" "net/http" @@ -193,6 +194,26 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) { return series, nil } +// AddToWatchlist adds the current episode to the watchlist. +func (s *Series) AddToWatchlist() error { + endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", s.crunchy.Config.AccountID, s.crunchy.Locale) + body, _ := json.Marshal(map[string]string{"content_id": s.ID}) + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body)) + if err != nil { + return err + } + req.Header.Add("Content-Type", "application/json") + _, err = s.crunchy.requestFull(req) + return err +} + +// RemoveFromWatchlist removes the current episode from the watchlist. +func (s *Series) RemoveFromWatchlist() error { + endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", s.crunchy.Config.AccountID, s.ID, s.crunchy.Locale) + _, err := s.crunchy.request(endpoint, http.MethodDelete) + return err +} + // Seasons returns all seasons of a series. func (s *Series) Seasons() (seasons []*Season, err error) { if s.children != nil {