From 08c46e50bb1cdc6bc23dc73efb94fd5dc0c1cc6a Mon Sep 17 00:00:00 2001 From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com> Date: Fri, 27 May 2022 19:05:35 +0200 Subject: [PATCH 1/6] Add new endpoints --- category.go | 43 +++++++ crunchyroll.go | 300 +++++++++++++++++++++++++++++++++++++++++++++++++ news.go | 19 ++++ simulcast.go | 13 +++ utils.go | 48 ++++++++ 5 files changed, 423 insertions(+) create mode 100644 category.go create mode 100644 news.go create mode 100644 simulcast.go diff --git a/category.go b/category.go new file mode 100644 index 0000000..58855fc --- /dev/null +++ b/category.go @@ -0,0 +1,43 @@ +package crunchyroll + +// Category contains all information about a category. +type Category struct { + Category string `json:"tenant_category"` + + SubCategories []struct { + Category string `json:"tenant_category"` + ParentCategory string `json:"parent_category"` + + Localization struct { + Title string `json:"title"` + Description string `json:"description"` + Locale LOCALE `json:"locale"` + } `json:"localization"` + + Slug string `json:"slug"` + } `json:"sub_categories"` + + Images struct { + Background []struct { + Width int `json:"width"` + Height int `json:"height"` + Type string `json:"type"` + Source string `json:"source"` + } `json:"background"` + + Low []struct { + Width int `json:"width"` + Height int `json:"height"` + Type string `json:"type"` + Source string `json:"source"` + } `json:"low"` + } `json:"images"` + + Localization struct { + Title string `json:"title"` + Description string `json:"description"` + Locale LOCALE `json:"locale"` + } `json:"localization"` + + Slug string `json:"slug"` +} diff --git a/crunchyroll.go b/crunchyroll.go index dcfc375..a81f373 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -30,6 +30,23 @@ const ( AR = "ar-SA" ) +// SortOrder represents a sort order. +type SortOrder string + +const ( + POPULARITY SortOrder = "popularity" + NEWLYADDED = "newly_added" + ALPHABETICAL = "alphabetical" +) + +// MediaType represents a media type. +type MediaType string + +const ( + SERIES MediaType = "series" + MOVIELISTING = "movie_listing" +) + type Crunchyroll struct { // Client is the http.Client to perform all requests over. Client *http.Client @@ -60,6 +77,30 @@ type Crunchyroll struct { cache bool } +// BrowseOptions represents options for browsing the crunchyroll catalog. +type BrowseOptions struct { + // Categories specifies the categories of the results. + Categories []string `param:"categories"` + + // IsDubbed specifies whether the results should be dubbed. + IsDubbed bool `param:"is_dubbed"` + + // IsSubbed specifies whether the results should be subbed. + IsSubbed bool `param:"is_subbed"` + + // SimulcastID specifies a particular simulcast season in which the results have been aired. + SimulcastID string `param:"season_tag"` + + // SortBy specifies how the results should be sorted. + SortBy SortOrder `param:"sort_by"` + + // Start specifies the index from which the results should be returned. + Start uint `param:"start"` + + // Type specifies the media type of the results. + Type MediaType `param:"type"` +} + // LoginWithCredentials logs in via crunchyroll username or email and password. func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s", @@ -446,3 +487,262 @@ func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) { } return } + +// Browse browses the crunchyroll catalog filtered by the specified options and returns all found series and movies within the given limit. +func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m []*Movie, err error) { + query, err := encodeStructToQueryValues(options) + if err != nil { + return nil, nil, err + } + + browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s", + query, limit, c.Locale) + resp, err := c.request(browseEndpoint) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, nil, fmt.Errorf("failed to parse 'browse' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + switch item.(map[string]interface{})["type"] { + case "series": + series := &Series{ + crunchy: c, + } + if err := decodeMapToStruct(item, series); err != nil { + return nil, nil, err + } + if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil { + return nil, nil, err + } + + s = append(s, series) + case "movie_listing": + movie := &Movie{ + crunchy: c, + } + if err := decodeMapToStruct(item, movie); err != nil { + return nil, nil, err + } + + m = append(m, movie) + } + } + + return s, m, nil +} + +// Categories returns all available categories and possible subcategories. +func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) { + tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s", + includeSubcategories, c.Locale) + resp, err := c.request(tenantCategoriesEndpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'tenant_categories' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + category := &Category{} + if err := decodeMapToStruct(item, category); err != nil { + return nil, err + } + + ca = append(ca, category) + } + + return ca, nil +} + +// Simulcasts returns all available simulcast seasons for the current locale. +func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) { + seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale) + resp, err := c.request(seasonListEndpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'season_list' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + simulcast := &Simulcast{} + if err := decodeMapToStruct(item, simulcast); err != nil { + return nil, err + } + + s = append(s, simulcast) + } + + return s, nil +} + +// News returns the top and latest news from crunchyroll for the current locale within the given limits. +func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*LatestNews, err error) { + newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s", + topLimit, latestLimit, c.Locale) + resp, err := c.request(newsFeedEndpoint) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, nil, fmt.Errorf("failed to parse 'news_feed' response: %w", err) + } + + topNews := jsonBody["top_news"].(map[string]interface{}) + for _, item := range topNews["items"].([]interface{}) { + topNews := &TopNews{} + if err := decodeMapToStruct(item, topNews); err != nil { + return nil, nil, err + } + + t = append(t, topNews) + } + + latestNews := jsonBody["latest_news"].(map[string]interface{}) + for _, item := range latestNews["items"].([]interface{}) { + latestNews := &LatestNews{} + if err := decodeMapToStruct(item, latestNews); err != nil { + return nil, nil, err + } + + l = append(l, latestNews) + } + + return t, l, nil +} + +// Recommendations returns series and movie recommendations from crunchyroll based on your account within the given limit. +func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) { + recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s", + c.Config.AccountID, limit, c.Locale) + resp, err := c.request(recommendationsEndpoint) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, nil, fmt.Errorf("failed to parse 'recommendations' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + switch item.(map[string]interface{})["type"] { + case "series": + series := &Series{ + crunchy: c, + } + if err := decodeMapToStruct(item, series); err != nil { + return nil, nil, err + } + if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil { + return nil, nil, err + } + + s = append(s, series) + case "movie_listing": + movie := &Movie{ + crunchy: c, + } + if err := decodeMapToStruct(item, movie); err != nil { + return nil, nil, err + } + + m = append(m, movie) + } + } + + return s, m, nil +} + +// UpNext returns the next episodes that you can continue watching based on your account within the given limit. +func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) { + upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s", + c.Config.AccountID, limit, c.Locale) + resp, err := c.request(upNextAccountEndpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'up_next_account' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + panel := item.(map[string]interface{})["panel"] + + episode := &Episode{ + crunchy: c, + } + if err := decodeMapToStruct(panel, episode); err != nil { + return nil, err + } + + e = append(e, episode) + } + + return e, nil +} + +// SimilarTo returns similar series and movies to the one specified by id within the given limits. +func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) { + similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s", + c.Config.AccountID, id, limit, c.Locale) + resp, err := c.request(similarToEndpoint) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, nil, fmt.Errorf("failed to parse 'similar_to' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + switch item.(map[string]interface{})["type"] { + case "series": + series := &Series{ + crunchy: c, + } + if err := decodeMapToStruct(item, series); err != nil { + return nil, nil, err + } + if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil { + return nil, nil, err + } + + s = append(s, series) + case "movie_listing": + movie := &Movie{ + crunchy: c, + } + if err := decodeMapToStruct(item, movie); err != nil { + return nil, nil, err + } + + m = append(m, movie) + } + } + + return s, m, nil +} diff --git a/news.go b/news.go new file mode 100644 index 0000000..3985cb4 --- /dev/null +++ b/news.go @@ -0,0 +1,19 @@ +package crunchyroll + +// News contains all information about a news. +type News struct { + Title string `json:"title"` + Link string `json:"link"` + Image string `json:"image"` + Creator string `json:"creator"` + PublishDate string `json:"publish_date"` + Description string `json:"description"` +} + +type TopNews struct { + News +} + +type LatestNews struct { + News +} diff --git a/simulcast.go b/simulcast.go new file mode 100644 index 0000000..d02f348 --- /dev/null +++ b/simulcast.go @@ -0,0 +1,13 @@ +package crunchyroll + +// Simulcast contains all information about a simulcast season. +type Simulcast struct { + ID string `json:"id"` + + Localization struct { + Title string `json:"title"` + + // appears to be always an empty string. + Description string `json:"description"` + } `json:"localization"` +} diff --git a/utils.go b/utils.go index a3d4191..672772f 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,10 @@ package crunchyroll import ( "encoding/json" + "fmt" + "net/url" + "reflect" + "strings" ) func decodeMapToStruct(m interface{}, s interface{}) error { @@ -23,3 +27,47 @@ func regexGroups(parsed [][]string, subexpNames ...string) map[string]string { } return groups } + +func encodeStructToQueryValues(s interface{}) (string, error) { + values := make(url.Values) + v := reflect.ValueOf(s) + + for i := 0; i < v.Type().NumField(); i++ { + + // don't include parameters with default or without values in the query to avoid corruption of the API response. + if isEmptyValue(v.Field(i)) { + continue + } + + key := v.Type().Field(i).Tag.Get("param") + var val string + + if v.Field(i).Kind() == reflect.Slice { + var items []string + + for _, i := range v.Field(i).Interface().([]string) { + items = append(items, i) + } + + val = strings.Join(items, ",") + } else { + val = fmt.Sprint(v.Field(i).Interface()) + } + + values.Add(key, val) + } + + return values.Encode(), nil +} + +func isEmptyValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Uint: + return v.Uint() == 0 + } + return false +} From cf3559698582ee2589570fc59f4b67797b7c9554 Mon Sep 17 00:00:00 2001 From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com> Date: Sat, 28 May 2022 19:55:56 +0200 Subject: [PATCH 2/6] Add watch history --- crunchyroll.go | 48 +++++++++++++++++++++++++++++++++++++++++++----- episode.go | 12 ++++++++++++ news.go | 8 -------- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/crunchyroll.go b/crunchyroll.go index a81f373..aefce1a 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -591,7 +591,7 @@ func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) { } // News returns the top and latest news from crunchyroll for the current locale within the given limits. -func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*LatestNews, err error) { +func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) { newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s", topLimit, latestLimit, c.Locale) resp, err := c.request(newsFeedEndpoint) @@ -607,7 +607,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []* topNews := jsonBody["top_news"].(map[string]interface{}) for _, item := range topNews["items"].([]interface{}) { - topNews := &TopNews{} + topNews := &News{} if err := decodeMapToStruct(item, topNews); err != nil { return nil, nil, err } @@ -617,7 +617,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []* latestNews := jsonBody["latest_news"].(map[string]interface{}) for _, item := range latestNews["items"].([]interface{}) { - latestNews := &LatestNews{} + latestNews := &News{} if err := decodeMapToStruct(item, latestNews); err != nil { return nil, nil, err } @@ -672,7 +672,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err return s, m, nil } -// UpNext returns the next episodes that you can continue watching based on your account within the given limit. +// UpNext returns the episodes that are up next based on your account within the given limit. func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) { upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s", c.Config.AccountID, limit, c.Locale) @@ -703,7 +703,7 @@ func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) { return e, nil } -// SimilarTo returns similar series and movies to the one specified by id within the given limits. +// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limits. func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) { similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s", c.Config.AccountID, id, limit, c.Locale) @@ -746,3 +746,41 @@ func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, return s, m, nil } + +// WatchHistory returns the history of watched episodes based on your account from the given page with the given size. +func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) { + watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s", + c.Config.AccountID, page, size, c.Locale) + resp, err := c.request(watchHistoryEndpoint) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'watch-history' response: %w", err) + } + + for _, item := range jsonBody["items"].([]interface{}) { + panel := item.(map[string]interface{})["panel"] + + episode := &Episode{ + crunchy: c, + } + if err := decodeMapToStruct(panel, episode); err != nil { + return nil, err + } + + historyEpisode := &HistoryEpisode{ + Episode: episode, + } + if err := decodeMapToStruct(item, historyEpisode); err != nil { + return nil, err + } + + e = append(e, historyEpisode) + } + + return e, nil +} diff --git a/episode.go b/episode.go index a25844c..07f6705 100644 --- a/episode.go +++ b/episode.go @@ -75,6 +75,18 @@ type Episode struct { StreamID string } +// HistoryEpisode contains additional information about an episode if the account has watched or started to watch the episode. +type HistoryEpisode struct { + *Episode + + ID string `json:"id"` + DatePlayed string `json:"date_played"` + ParentID string `json:"parent_id"` + ParentType MediaType `json:"parent_type"` + Playhead uint `json:"playhead"` + FullyWatched bool `json:"fully_watched"` +} + // 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", diff --git a/news.go b/news.go index 3985cb4..d90dd65 100644 --- a/news.go +++ b/news.go @@ -9,11 +9,3 @@ type News struct { PublishDate string `json:"publish_date"` Description string `json:"description"` } - -type TopNews struct { - News -} - -type LatestNews struct { - News -} From 7897da3baf200a77f9b9f876e7a7da9981d77e05 Mon Sep 17 00:00:00 2001 From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com> Date: Sun, 29 May 2022 00:21:17 +0200 Subject: [PATCH 3/6] Add account and update comments --- account.go | 27 ++++++++++++++++++ crunchyroll.go | 74 ++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 80 insertions(+), 21 deletions(-) create mode 100644 account.go diff --git a/account.go b/account.go new file mode 100644 index 0000000..f6f87f1 --- /dev/null +++ b/account.go @@ -0,0 +1,27 @@ +package crunchyroll + +// Account contains information about a crunchyroll account. +type Account struct { + AccountID string `json:"account_id"` + ExternalID string `json:"external_id"` + EmailVerified bool `json:"email_verified"` + Created string `json:"created"` + + Avatar string `json:"avatar"` + CrBetaOptIn bool `json:"cr_beta_opt_in"` + Email string `json:"email"` + MatureContentFlagManga string `json:"mature_content_flag_manga"` + MaturityRating string `json:"maturity_rating"` + OptOutAndroidInAppMarketing bool `json:"opt_out_android_in_app_marketing"` + OptOutFreeTrials bool `json:"opt_out_free_trials"` + OptOutNewMediaQueueUpdates bool `json:"opt_out_new_media_queue_updates"` + OptOutNewsletters bool `json:"opt_out_newsletters"` + OptOutPmUpdates bool `json:"opt_out_pm_updates"` + OptOutPromotionalUpdates bool `json:"opt_out_promotional_updates"` + OptOutQueueUpdates bool `json:"opt_out_queue_updates"` + OptOutStoreDeals bool `json:"opt_out_store_deals"` + PreferredCommunicationLanguage LOCALE `json:"preferred_communication_language"` + PreferredContentSubtitleLanguage LOCALE `json:"preferred_content_subtitle_language"` + QaUser bool `json:"qa_user"` + Username string `json:"username"` +} diff --git a/crunchyroll.go b/crunchyroll.go index aefce1a..4e5a436 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -30,15 +30,6 @@ const ( AR = "ar-SA" ) -// SortOrder represents a sort order. -type SortOrder string - -const ( - POPULARITY SortOrder = "popularity" - NEWLYADDED = "newly_added" - ALPHABETICAL = "alphabetical" -) - // MediaType represents a media type. type MediaType string @@ -47,6 +38,15 @@ const ( MOVIELISTING = "movie_listing" ) +// SortType represents a sort type. +type SortType string + +const ( + POPULARITY SortType = "popularity" + NEWLYADDED = "newly_added" + ALPHABETICAL = "alphabetical" +) + type Crunchyroll struct { // Client is the http.Client to perform all requests over. Client *http.Client @@ -79,25 +79,25 @@ type Crunchyroll struct { // BrowseOptions represents options for browsing the crunchyroll catalog. type BrowseOptions struct { - // Categories specifies the categories of the results. + // Categories specifies the categories of the entries. Categories []string `param:"categories"` - // IsDubbed specifies whether the results should be dubbed. + // IsDubbed specifies whether the entries should be dubbed. IsDubbed bool `param:"is_dubbed"` - // IsSubbed specifies whether the results should be subbed. + // IsSubbed specifies whether the entries should be subbed. IsSubbed bool `param:"is_subbed"` - // SimulcastID specifies a particular simulcast season in which the results have been aired. - SimulcastID string `param:"season_tag"` + // Simulcast specifies a particular simulcast season by id in which the entries have been aired. + Simulcast string `param:"season_tag"` - // SortBy specifies how the results should be sorted. - SortBy SortOrder `param:"sort_by"` + // Sort specifies how the entries should be sorted. + Sort SortType `param:"sort_by"` - // Start specifies the index from which the results should be returned. + // Start specifies the index from which the entries should be returned. Start uint `param:"start"` - // Type specifies the media type of the results. + // Type specifies the media type of the entries. Type MediaType `param:"type"` } @@ -672,7 +672,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err return s, m, nil } -// UpNext returns the episodes that are up next based on your account within the given limit. +// UpNext returns the episodes that are up next based on the currently logged in account within the given limit. func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) { upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s", c.Config.AccountID, limit, c.Locale) @@ -703,7 +703,7 @@ func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) { return e, nil } -// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limits. +// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limit. func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) { similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s", c.Config.AccountID, id, limit, c.Locale) @@ -747,7 +747,7 @@ func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, return s, m, nil } -// WatchHistory returns the history of watched episodes based on your account from the given page with the given size. +// WatchHistory returns the history of watched episodes based on the currently logged in account from the given page with the given size. func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) { watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s", c.Config.AccountID, page, size, c.Locale) @@ -784,3 +784,35 @@ func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, e return e, 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") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jsonBody map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'me' response: %w", err) + } + + resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + return nil, fmt.Errorf("failed to parse 'profile' response: %w", err) + } + + account := &Account{} + + if err := decodeMapToStruct(jsonBody, account); err != nil { + return nil, err + } + + return account, nil +} From acc6c63ebd3f65e2e325018f20217e4433e7982b Mon Sep 17 00:00:00 2001 From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com> Date: Sun, 29 May 2022 01:42:24 +0200 Subject: [PATCH 4/6] Fix comment --- crunchyroll.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crunchyroll.go b/crunchyroll.go index 4e5a436..5ee3a97 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -628,7 +628,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*New return t, l, nil } -// Recommendations returns series and movie recommendations from crunchyroll based on your account within the given limit. +// Recommendations returns series and movie recommendations from crunchyroll based on the currently logged in account within the given limit. func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) { recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s", c.Config.AccountID, limit, c.Locale) From b53256ca3fde95e834c4019b37eb230f7881713e Mon Sep 17 00:00:00 2001 From: ByteDream <63594396+ByteDream@users.noreply.github.com> Date: Sun, 29 May 2022 10:10:48 +0200 Subject: [PATCH 5/6] Use Account struct directly instead of maps --- crunchyroll.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/crunchyroll.go b/crunchyroll.go index 5ee3a97..d20dd20 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -793,8 +793,9 @@ func (c *Crunchyroll) Account() (*Account, error) { } defer resp.Body.Close() - var jsonBody map[string]interface{} - if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + account := &Account{} + + if err = json.NewDecoder(resp.Body).Decode(&account); err != nil { return nil, fmt.Errorf("failed to parse 'me' response: %w", err) } @@ -804,15 +805,9 @@ func (c *Crunchyroll) Account() (*Account, error) { } defer resp.Body.Close() - if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { + if err = json.NewDecoder(resp.Body).Decode(&account); err != nil { return nil, fmt.Errorf("failed to parse 'profile' response: %w", err) } - account := &Account{} - - if err := decodeMapToStruct(jsonBody, account); err != nil { - return nil, err - } - return account, nil } From 29343d1c6f8326332f4bba57c49de543fd63c2ae Mon Sep 17 00:00:00 2001 From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com> Date: Sun, 29 May 2022 15:08:38 +0200 Subject: [PATCH 6/6] Add requested changes --- account.go | 10 ++++++---- episode.go | 3 +-- utils.go | 27 +++++++++++++-------------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/account.go b/account.go index f6f87f1..d22eba5 100644 --- a/account.go +++ b/account.go @@ -1,11 +1,13 @@ package crunchyroll +import "time" + // Account contains information about a crunchyroll account. type Account struct { - AccountID string `json:"account_id"` - ExternalID string `json:"external_id"` - EmailVerified bool `json:"email_verified"` - Created string `json:"created"` + AccountID string `json:"account_id"` + ExternalID string `json:"external_id"` + EmailVerified bool `json:"email_verified"` + Created time.Time `json:"created"` Avatar string `json:"avatar"` CrBetaOptIn bool `json:"cr_beta_opt_in"` diff --git a/episode.go b/episode.go index 07f6705..de38c65 100644 --- a/episode.go +++ b/episode.go @@ -79,8 +79,7 @@ type Episode struct { type HistoryEpisode struct { *Episode - ID string `json:"id"` - DatePlayed string `json:"date_played"` + DatePlayed time.Time `json:"date_played"` ParentID string `json:"parent_id"` ParentType MediaType `json:"parent_type"` Playhead uint `json:"playhead"` diff --git a/utils.go b/utils.go index 672772f..d32d39b 100644 --- a/utils.go +++ b/utils.go @@ -35,8 +35,19 @@ func encodeStructToQueryValues(s interface{}) (string, error) { for i := 0; i < v.Type().NumField(); i++ { // don't include parameters with default or without values in the query to avoid corruption of the API response. - if isEmptyValue(v.Field(i)) { - continue + switch v.Field(i).Kind() { + case reflect.Slice, reflect.String: + if v.Field(i).Len() == 0 { + continue + } + case reflect.Bool: + if !v.Field(i).Bool() { + continue + } + case reflect.Uint: + if v.Field(i).Uint() == 0 { + continue + } } key := v.Type().Field(i).Tag.Get("param") @@ -59,15 +70,3 @@ func encodeStructToQueryValues(s interface{}) (string, error) { return values.Encode(), nil } - -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Uint: - return v.Uint() == 0 - } - return false -}