diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44c7ac6..7c54dd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.16 + go-version: 1.18 - name: Build run: go build -v cmd/crunchyroll-go/main.go diff --git a/Makefile b/Makefile index 3dd5c3e..a747c1d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=2.2.2 +VERSION=development BINARY_NAME=crunchy VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION) @@ -6,7 +6,7 @@ DESTDIR= PREFIX=/usr build: - go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go + go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go clean: rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_* @@ -24,8 +24,8 @@ uninstall: rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE release: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go + CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go + CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go strip $(VERSION_BINARY_NAME)_linux diff --git a/README.md b/README.md index c83493c..46862f0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,3 @@ -
Version 2 is out 🥳, see all the changes.
- # crunchyroll-go A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api. To use it, you need a crunchyroll premium account to for full (api) access. @@ -205,10 +203,10 @@ These flags you can use across every sub-command: Download the library via `go get` ```shell -$ go get github.com/ByteDream/crunchyroll-go/v2 +$ go get github.com/ByteDream/crunchyroll-go/v3 ``` -The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v2). +The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v3). Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library). diff --git a/account.go b/account.go new file mode 100644 index 0000000..d22eba5 --- /dev/null +++ b/account.go @@ -0,0 +1,29 @@ +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 time.Time `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/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/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go index 6dcf61b..d4a7e48 100644 --- a/cmd/crunchyroll-go/cmd/archive.go +++ b/cmd/crunchyroll-go/cmd/archive.go @@ -8,8 +8,8 @@ import ( "compress/gzip" "context" "fmt" - "github.com/ByteDream/crunchyroll-go/v2" - "github.com/ByteDream/crunchyroll-go/v2/utils" + "github.com/ByteDream/crunchyroll-go/v3" + "github.com/ByteDream/crunchyroll-go/v3/utils" "github.com/grafov/m3u8" "github.com/spf13/cobra" "io" diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go index 92b6027..3dddf48 100644 --- a/cmd/crunchyroll-go/cmd/download.go +++ b/cmd/crunchyroll-go/cmd/download.go @@ -3,8 +3,8 @@ package cmd import ( "context" "fmt" - "github.com/ByteDream/crunchyroll-go/v2" - "github.com/ByteDream/crunchyroll-go/v2/utils" + "github.com/ByteDream/crunchyroll-go/v3" + "github.com/ByteDream/crunchyroll-go/v3/utils" "github.com/grafov/m3u8" "github.com/spf13/cobra" "math" diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go index d31179e..bfef794 100644 --- a/cmd/crunchyroll-go/cmd/login.go +++ b/cmd/crunchyroll-go/cmd/login.go @@ -7,7 +7,7 @@ import ( "crypto/rand" "crypto/sha256" "fmt" - "github.com/ByteDream/crunchyroll-go/v2" + "github.com/ByteDream/crunchyroll-go/v3" "github.com/spf13/cobra" "io" "os" @@ -19,6 +19,7 @@ var ( loginEncryptFlag bool loginSessionIDFlag bool + loginEtpRtFlag bool ) var loginCmd = &cobra.Command{ @@ -29,6 +30,8 @@ var loginCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { if loginSessionIDFlag { return loginSessionID(args[0]) + } else if loginEtpRtFlag { + return loginEtpRt(args[0]) } else { return loginCredentials(args[0], args[1]) } @@ -49,6 +52,10 @@ func init() { "session-id", false, "Use a session id to login instead of username and password") + loginCmd.Flags().BoolVar(&loginEtpRtFlag, + "etp-rt", + false, + "Use a etp rt cookie to login instead of username and password") rootCmd.AddCommand(loginCmd) } @@ -129,6 +136,12 @@ func loginCredentials(user, password string) error { } } } +<<<<<<< v3/feature/encrypted-credentials +======= + if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil { + return err + } +>>>>>>> next/v3 if !loginPersistentFlag { out.Info("Due to security reasons, you have to login again on the next reboot") @@ -139,7 +152,38 @@ func loginCredentials(user, password string) error { func loginSessionID(sessionID string) error { out.Debug("Logging in via session id") - if _, err := crunchyroll.LoginWithSessionID(sessionID, systemLocale(false), client); err != nil { + var c *crunchyroll.Crunchyroll + var err error + if c, err = crunchyroll.LoginWithSessionID(sessionID, systemLocale(false), client); err != nil { + out.Err(err.Error()) + os.Exit(1) + } + + if loginPersistentFlag { + if configDir, err := os.UserConfigDir(); err != nil { + return fmt.Errorf("could not save credentials persistent: %w", err) + } else { + os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755) + if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(c.EtpRt), 0600); err != nil { + return err + } + out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy")) + } + } + if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil { + return err + } + + if !loginPersistentFlag { + out.Info("Due to security reasons, you have to login again on the next reboot") + } + + return nil +} + +func loginEtpRt(etpRt string) error { + out.Debug("Logging in via etp rt") + if _, err := crunchyroll.LoginWithEtpRt(etpRt, systemLocale(false), client); err != nil { out.Err(err.Error()) os.Exit(1) } @@ -150,13 +194,13 @@ func loginSessionID(sessionID string) error { return fmt.Errorf("could not save credentials persistent: %w", err) } else { os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755) - if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(sessionID), 0600); err != nil { + if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(etpRt), 0600); err != nil { return err } out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy")) } } - if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(sessionID), 0600); err != nil { + if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(etpRt), 0600); err != nil { return err } diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go index 82ee133..02873f1 100644 --- a/cmd/crunchyroll-go/cmd/root.go +++ b/cmd/crunchyroll-go/cmd/root.go @@ -3,7 +3,7 @@ package cmd import ( "context" "fmt" - "github.com/ByteDream/crunchyroll-go/v2" + "github.com/ByteDream/crunchyroll-go/v3" "github.com/spf13/cobra" "net/http" "os" diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go index d7eba3d..7fe498f 100644 --- a/cmd/crunchyroll-go/cmd/utils.go +++ b/cmd/crunchyroll-go/cmd/utils.go @@ -5,8 +5,8 @@ import ( "crypto/cipher" "crypto/sha256" "fmt" - "github.com/ByteDream/crunchyroll-go/v2" - "github.com/ByteDream/crunchyroll-go/v2/utils" + "github.com/ByteDream/crunchyroll-go/v3" + "github.com/ByteDream/crunchyroll-go/v3/utils" "net/http" "net/url" "os" @@ -150,10 +150,10 @@ func loadCrunchy() { out.StopProgress("Failed to read login information: %v", err) os.Exit(1) } - if crunchy, err = crunchyroll.LoginWithSessionID(url.QueryEscape(string(body)), systemLocale(true), client); err != nil { - out.Debug("Failed to login with temp session id: %w", err) + if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil { + out.Debug("Failed to login with temp etp rt cookie: %v", err) } else { - out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body) + out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body) out.StopProgress("Logged in") return @@ -170,6 +170,7 @@ func loadCrunchy() { } split := strings.SplitN(string(body), "\n", 2) if len(split) == 1 || split[1] == "" { +<<<<<<< v3/feature/encrypted-credentials if strings.HasPrefix(split[0], "aes:") { encrypted := body[4:] @@ -213,14 +214,23 @@ func loadCrunchy() { } if len(split) == 2 { +======= + split[0] = url.QueryEscape(split[0]) + if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], systemLocale(true), client); err != nil { + out.StopProgress(err.Error()) + os.Exit(1) + } + out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0]) + } else { +>>>>>>> next/v3 if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil { out.StopProgress(err.Error()) os.Exit(1) } - out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.SessionID) - // the session id is written to a temp file to reduce the amount of re-logging in. + out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.EtpRt) + // the etp rt is written to a temp file to reduce the amount of re-logging in. // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time - os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600) + os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.EtpRt), 0600) } out.StopProgress("Logged in") diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go index a502afb..0ced8aa 100644 --- a/cmd/crunchyroll-go/main.go +++ b/cmd/crunchyroll-go/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd" + "github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd" ) func main() { diff --git a/crunchyroll.go b/crunchyroll.go index 1dafee7..38b889b 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -10,6 +10,7 @@ import ( "net/url" "regexp" "strconv" + "strings" ) // LOCALE represents a locale / language. @@ -29,6 +30,23 @@ const ( AR = "ar-SA" ) +// MediaType represents a media type. +type MediaType string + +const ( + SERIES MediaType = "series" + 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 @@ -36,14 +54,17 @@ type Crunchyroll struct { Context context.Context // Locale specifies in which language all results should be returned / requested. Locale LOCALE - // SessionID is the crunchyroll session id which was used for authentication. - SessionID string + // EtpRt is the crunchyroll beta equivalent to a session id (prior SessionID field in + // this struct in v2 and below). + EtpRt string // Config stores parameters which are needed by some api calls. Config struct { TokenType string AccessToken string + Bucket string + CountryCode string Premium bool Channel string @@ -59,72 +80,86 @@ type Crunchyroll struct { cache bool } +// BrowseOptions represents options for browsing the crunchyroll catalog. +type BrowseOptions struct { + // Categories specifies the categories of the entries. + Categories []string `param:"categories"` + + // IsDubbed specifies whether the entries should be dubbed. + IsDubbed bool `param:"is_dubbed"` + + // IsSubbed specifies whether the entries should be subbed. + IsSubbed bool `param:"is_subbed"` + + // Simulcast specifies a particular simulcast season by id in which the entries have been aired. + Simulcast string `param:"season_tag"` + + // Sort specifies how the entries should be sorted. + Sort SortType `param:"sort_by"` + + // Start specifies the index from which the entries should be returned. + Start uint `param:"start"` + + // Type specifies the media type of the entries. + Type MediaType `param:"type"` +} + +type loginResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + Country string `json:"country"` + AccountID string `json:"account_id"` +} + // 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", - "LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI") - sessResp, err := client.Get(sessionIDEndpoint) + endpoint := "https://beta-api.crunchyroll.com/auth/v1/token" + values := url.Values{} + values.Set("username", user) + values.Set("password", password) + values.Set("grant_type", "password") + + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(values.Encode())) if err != nil { return nil, err } - defer sessResp.Body.Close() + req.Header.Set("Authorization", "Basic aHJobzlxM2F3dnNrMjJ1LXRzNWE6cHROOURteXRBU2Z6QjZvbXVsSzh6cUxzYTczVE1TY1k=") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - if sessResp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to start session for credentials login: %s", sessResp.Status) - } - - var data map[string]interface{} - body, _ := io.ReadAll(sessResp.Body) - if err = json.Unmarshal(body, &data); err != nil { - return nil, fmt.Errorf("failed to parse start session with credentials response: %w", err) - } - - sessionID := data["data"].(map[string]interface{})["session_id"].(string) - - loginEndpoint := "https://api.crunchyroll.com/login.0.json" - authValues := url.Values{} - authValues.Set("session_id", sessionID) - authValues.Set("account", user) - authValues.Set("password", password) - loginResp, err := client.Post(loginEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(authValues.Encode())) + resp, err := request(req, client) if err != nil { return nil, err } - defer loginResp.Body.Close() + defer resp.Body.Close() - if loginResp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status) - } else { - var loginRespBody map[string]interface{} - json.NewDecoder(loginResp.Body).Decode(&loginRespBody) + var loginResp loginResponse + json.NewDecoder(resp.Body).Decode(&loginResp) - if loginRespBody["error"].(bool) { - return nil, fmt.Errorf("an unexpected login error occoured: %s", loginRespBody["message"]) + var etpRt string + for _, cookie := range resp.Cookies() { + if cookie.Name == "etp_rt" { + etpRt = cookie.Value + break } } - return LoginWithSessionID(sessionID, locale, client) + return postLogin(loginResp, etpRt, locale, client) } // LoginWithSessionID logs in via a crunchyroll session id. // Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com. +// +// Deprecated: Login via session id caused some trouble in the past (e.g. #15 or #30) which resulted in +// login not working. Use LoginWithEtpRt instead. EtpRt practically the crunchyroll beta equivalent to +// a session id. +// The method will stay in the library until session id login is removed completely or login with it +// does not work for a longer period of time. func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { - crunchy := &Crunchyroll{ - Client: client, - Context: context.Background(), - Locale: locale, - SessionID: sessionID, - cache: true, - } - var endpoint string - var err error - var resp *http.Response - var jsonBody map[string]interface{} - - // start session - endpoint = fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?session_id=%s", + endpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?session_id=%s", sessionID) - resp, err = client.Get(endpoint) + resp, err := client.Get(endpoint) if err != nil { return nil, err } @@ -134,26 +169,13 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (* return nil, fmt.Errorf("failed to start session: %s", resp.Status) } + var jsonBody map[string]any if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { return nil, fmt.Errorf("failed to parse start session with session id response: %w", err) } if isError, ok := jsonBody["error"]; ok && isError.(bool) { return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"]) } - data := jsonBody["data"].(map[string]interface{}) - - crunchy.Config.CountryCode = data["country_code"].(string) - user := data["user"] - if user == nil { - return nil, fmt.Errorf("invalid session id, user is not logged in") - } - if user.(map[string]interface{})["premium"] == "" { - crunchy.Config.Premium = false - crunchy.Config.Channel = "-" - } else { - crunchy.Config.Premium = true - crunchy.Config.Channel = "crunchyroll" - } var etpRt string for _, cookie := range resp.Cookies() { @@ -163,110 +185,153 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (* } } - // token - endpoint = "https://beta-api.crunchyroll.com/auth/v1/token" + return LoginWithEtpRt(etpRt, locale, client) +} + +// LoginWithEtpRt logs in via the crunchyroll etp rt cookie. This cookie is the crunchyroll beta +// equivalent to the classic session id. +// The etp_rt cookie is automatically set when visiting https://beta.crunchyroll.com. Note that you +// need a crunchyroll account to access it. +func LoginWithEtpRt(etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { + endpoint := "https://beta-api.crunchyroll.com/auth/v1/token" grantType := url.Values{} grantType.Set("grant_type", "etp_rt_cookie") - authRequest, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(grantType.Encode())) + req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(grantType.Encode())) if err != nil { return nil, err } - authRequest.Header.Add("Authorization", "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6") - authRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded") - authRequest.AddCookie(&http.Cookie{ - Name: "session_id", - Value: sessionID, - }) - authRequest.AddCookie(&http.Cookie{ + req.Header.Add("Authorization", "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.AddCookie(&http.Cookie{ Name: "etp_rt", Value: etpRt, }) + resp, err := request(req, client) + if err != nil { + return nil, err + } - resp, err = client.Do(authRequest) + var loginResp loginResponse + json.NewDecoder(resp.Body).Decode(&loginResp) + + return postLogin(loginResp, etpRt, locale, client) +} + +func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) { + crunchy := &Crunchyroll{ + Client: client, + Context: context.Background(), + Locale: locale, + EtpRt: etpRt, + cache: true, + } + + crunchy.Config.TokenType = loginResp.TokenType + crunchy.Config.AccessToken = loginResp.AccessToken + crunchy.Config.AccountID = loginResp.AccountID + crunchy.Config.CountryCode = loginResp.Country + + var jsonBody map[string]any + + endpoint := "https://beta-api.crunchyroll.com/index/v2" + resp, err := crunchy.request(endpoint, http.MethodGet) 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 'token' response: %w", err) - } - crunchy.Config.TokenType = jsonBody["token_type"].(string) - crunchy.Config.AccessToken = jsonBody["access_token"].(string) + json.NewDecoder(resp.Body).Decode(&jsonBody) - // index - endpoint = "https://beta-api.crunchyroll.com/index/v2" - resp, err = crunchy.request(endpoint) - if err != nil { - return nil, err + cms := jsonBody["cms"].(map[string]any) + crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/") + if strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") { + crunchy.Config.Premium = true + crunchy.Config.Channel = "crunchyroll" + } else { + crunchy.Config.Premium = false + crunchy.Config.Channel = "-" } - defer resp.Body.Close() - if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil { - return nil, fmt.Errorf("failed to parse 'index' response: %w", err) + + if strings.Contains(cms["bucket"].(string), "crunchyroll") { + crunchy.Config.Premium = true + crunchy.Config.Channel = "crunchyroll" + } else { + crunchy.Config.Premium = false + crunchy.Config.Channel = "-" } - cms := jsonBody["cms"].(map[string]interface{}) crunchy.Config.Policy = cms["policy"].(string) crunchy.Config.Signature = cms["signature"].(string) crunchy.Config.KeyPairID = cms["key_pair_id"].(string) - // me endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me" - resp, err = crunchy.request(endpoint) + resp, err = crunchy.request(endpoint, http.MethodGet) 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 'me' response: %w", err) - } - - crunchy.Config.AccountID = jsonBody["account_id"].(string) + json.NewDecoder(resp.Body).Decode(&jsonBody) crunchy.Config.ExternalID = jsonBody["external_id"].(string) - //profile endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me/profile" - resp, err = crunchy.request(endpoint) + resp, err = crunchy.request(endpoint, http.MethodGet) 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) - } - + json.NewDecoder(resp.Body).Decode(&jsonBody) crunchy.Config.MaturityRating = jsonBody["maturity_rating"].(string) return crunchy, nil } +func request(req *http.Request, client *http.Client) (*http.Response, error) { + resp, err := client.Do(req) + if err == nil { + var buf bytes.Buffer + io.Copy(&buf, resp.Body) + defer resp.Body.Close() + defer func() { + resp.Body = io.NopCloser(&buf) + }() + + if buf.Len() != 0 { + var errMap map[string]any + + if err = json.Unmarshal(buf.Bytes(), &errMap); err != nil { + return nil, fmt.Errorf("invalid json response: %w", err) + } + + if val, ok := errMap["error"]; ok { + if errorAsString, ok := val.(string); ok { + if code, ok := errMap["code"].(string); ok { + return nil, fmt.Errorf("error for endpoint %s (%d): %s - %s", req.URL.String(), resp.StatusCode, errorAsString, code) + } + return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, errorAsString) + } else if errorAsBool, ok := val.(bool); ok && errorAsBool { + if msg, ok := errMap["message"].(string); ok { + return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, msg) + } + } + } + } + + if resp.StatusCode >= 400 { + return nil, fmt.Errorf("error for endpoint %s: %s", req.URL.String(), resp.Status) + } + } + return resp, err +} + // request is a base function which handles api requests. -func (c *Crunchyroll) request(endpoint string) (*http.Response, error) { - req, err := http.NewRequest(http.MethodGet, endpoint, nil) +func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, error) { + req, err := http.NewRequest(method, endpoint, nil) if err != nil { return nil, err } req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken)) - resp, err := c.Client.Do(req) - if err == nil { - defer resp.Body.Close() - bodyAsBytes, _ := io.ReadAll(resp.Body) - defer resp.Body.Close() - if resp.StatusCode == http.StatusUnauthorized { - return nil, fmt.Errorf("invalid access token") - } else { - var errStruct struct { - Message string `json:"message"` - } - json.NewDecoder(bytes.NewBuffer(bodyAsBytes)).Decode(&errStruct) - if errStruct.Message != "" { - return nil, fmt.Errorf(errStruct.Message) - } - } - resp.Body = io.NopCloser(bytes.NewBuffer(bodyAsBytes)) - } - return resp, err + return request(req, c.Client) } // IsCaching returns if data gets cached or not. @@ -287,7 +352,7 @@ func (c *Crunchyroll) SetCaching(caching bool) { func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) { searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s", query, limit, c.Locale) - resp, err := c.request(searchEndpoint) + resp, err := c.request(searchEndpoint, http.MethodGet) if err != nil { return nil, nil, err } @@ -449,3 +514,327 @@ 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, http.MethodGet) + 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, http.MethodGet) + 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, http.MethodGet) + 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 []*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, http.MethodGet) + 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 := &News{} + 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 := &News{} + 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 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) + resp, err := c.request(recommendationsEndpoint, http.MethodGet) + 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 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) + resp, err := c.request(upNextAccountEndpoint, http.MethodGet) + 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 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) + resp, err := c.request(similarToEndpoint, http.MethodGet) + 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 +} + +// 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) + resp, err := c.request(watchHistoryEndpoint, http.MethodGet) + 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 +} + +// 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) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + account := &Account{} + + if err = json.NewDecoder(resp.Body).Decode(&account); 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", http.MethodGet) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = json.NewDecoder(resp.Body).Decode(&account); err != nil { + return nil, fmt.Errorf("failed to parse 'profile' response: %w", err) + } + + return account, nil +} diff --git a/episode.go b/episode.go index a25844c..0405e0e 100644 --- a/episode.go +++ b/episode.go @@ -3,6 +3,7 @@ package crunchyroll import ( "encoding/json" "fmt" + "net/http" "regexp" "strconv" "strings" @@ -75,6 +76,17 @@ 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 + + DatePlayed time.Time `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", @@ -85,7 +97,7 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) { crunchy.Locale, crunchy.Config.Signature, crunchy.Config.Policy, - crunchy.Config.KeyPairID)) + crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index 9a38468..5c64ddf 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/ByteDream/crunchyroll-go/v2 +module github.com/ByteDream/crunchyroll-go/v3 go 1.18 diff --git a/movie_listing.go b/movie_listing.go index 63d7fab..110f67a 100644 --- a/movie_listing.go +++ b/movie_listing.go @@ -3,6 +3,7 @@ package crunchyroll import ( "encoding/json" "fmt" + "net/http" ) // MovieListing contains information about something which is called @@ -48,7 +49,7 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) crunchy.Locale, crunchy.Config.Signature, crunchy.Config.Policy, - crunchy.Config.KeyPairID)) + crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } @@ -77,7 +78,7 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) { ml.crunchy.Locale, ml.crunchy.Config.Signature, ml.crunchy.Config.Policy, - ml.crunchy.Config.KeyPairID)) + ml.crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return "", err } diff --git a/news.go b/news.go new file mode 100644 index 0000000..d90dd65 --- /dev/null +++ b/news.go @@ -0,0 +1,11 @@ +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"` +} diff --git a/season.go b/season.go index 825a816..f5513f5 100644 --- a/season.go +++ b/season.go @@ -3,6 +3,7 @@ package crunchyroll import ( "encoding/json" "fmt" + "net/http" "regexp" ) @@ -94,7 +95,7 @@ func (s *Season) Episodes() (episodes []*Episode, err error) { s.crunchy.Locale, s.crunchy.Config.Signature, s.crunchy.Config.Policy, - s.crunchy.Config.KeyPairID)) + s.crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } 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/stream.go b/stream.go index 7505a24..00060ef 100644 --- a/stream.go +++ b/stream.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "github.com/grafov/m3u8" + "net/http" "regexp" ) @@ -72,7 +73,7 @@ func (s *Stream) Formats() ([]*Format, error) { // fromVideoStreams returns all streams which are accessible via the endpoint. func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) { - resp, err := crunchy.request(endpoint) + resp, err := crunchy.request(endpoint, http.MethodGet) if err != nil { return nil, err } diff --git a/utils.go b/utils.go index a3d4191..d32d39b 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,46 @@ 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. + 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") + 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 +} diff --git a/utils/locale.go b/utils/locale.go index 537b165..85a8650 100644 --- a/utils/locale.go +++ b/utils/locale.go @@ -1,7 +1,7 @@ package utils import ( - "github.com/ByteDream/crunchyroll-go/v2" + "github.com/ByteDream/crunchyroll-go/v3" ) // AllLocales is an array of all available locales. diff --git a/utils/sort.go b/utils/sort.go index a44717d..eacb96b 100644 --- a/utils/sort.go +++ b/utils/sort.go @@ -1,7 +1,7 @@ package utils import ( - "github.com/ByteDream/crunchyroll-go/v2" + "github.com/ByteDream/crunchyroll-go/v3" "sort" "strconv" "strings" diff --git a/video.go b/video.go index 00b7734..365f954 100644 --- a/video.go +++ b/video.go @@ -3,6 +3,7 @@ package crunchyroll import ( "encoding/json" "fmt" + "net/http" ) type video struct { @@ -77,7 +78,7 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) { crunchy.Locale, crunchy.Config.Signature, crunchy.Config.Policy, - crunchy.Config.KeyPairID)) + crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } @@ -110,7 +111,7 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) { m.crunchy.Locale, m.crunchy.Config.Signature, m.crunchy.Config.Policy, - m.crunchy.Config.KeyPairID)) + m.crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } @@ -173,7 +174,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) { crunchy.Locale, crunchy.Config.Signature, crunchy.Config.Policy, - crunchy.Config.KeyPairID)) + crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err } @@ -206,7 +207,7 @@ func (s *Series) Seasons() (seasons []*Season, err error) { s.crunchy.Locale, s.crunchy.Config.Signature, s.crunchy.Config.Policy, - s.crunchy.Config.KeyPairID)) + s.crunchy.Config.KeyPairID), http.MethodGet) if err != nil { return nil, err }