diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go index d4a7e48..0d47917 100644 --- a/cmd/crunchyroll-go/cmd/archive.go +++ b/cmd/crunchyroll-go/cmd/archive.go @@ -186,8 +186,10 @@ func archive(urls []string) error { episodes, err := archiveExtractEpisodes(url) if err != nil { out.StopProgress("Failed to parse url %d", i+1) - out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " + - "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information") + if crunchy.Config.Premium { + out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " + + "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information") + } return err } out.StopProgress("Parsed url %d", i+1) diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go index 3dddf48..74cc3f2 100644 --- a/cmd/crunchyroll-go/cmd/download.go +++ b/cmd/crunchyroll-go/cmd/download.go @@ -132,8 +132,10 @@ func download(urls []string) error { episodes, err := downloadExtractEpisodes(url) if err != nil { out.StopProgress("Failed to parse url %d", i+1) - out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " + - "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information") + if crunchy.Config.Premium { + out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " + + "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information") + } return err } out.StopProgress("Parsed url %d", i+1) diff --git a/crunchyroll.go b/crunchyroll.go index 38b889b..be5af03 100644 --- a/crunchyroll.go +++ b/crunchyroll.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -50,7 +51,7 @@ const ( type Crunchyroll struct { // Client is the http.Client to perform all requests over. Client *http.Client - // Context can be used to stop requests with Client and is context.Background by default. + // Context can be used to stop requests with Client. Context context.Context // Locale specifies in which language all results should be returned / requested. Locale LOCALE @@ -67,7 +68,6 @@ type Crunchyroll struct { CountryCode string Premium bool - Channel string Policy string Signature string KeyPairID string @@ -111,6 +111,9 @@ type loginResponse struct { Scope string `json:"scope"` Country string `json:"country"` AccountID string `json:"account_id"` + + Error bool `json:"error"` + Message string `json:"message"` } // LoginWithCredentials logs in via crunchyroll username or email and password. @@ -136,6 +139,11 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h var loginResp loginResponse json.NewDecoder(resp.Body).Decode(&loginResp) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to auth with credentials: %s", resp.Status) + } else if loginResp.Error { + return nil, fmt.Errorf("an unexpected login error occoured: %s", loginResp.Message) + } var etpRt string for _, cookie := range resp.Cookies() { @@ -173,9 +181,16 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (* 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{}) + + user := data["user"] + if user == nil { + return nil, errors.New("invalid session id, user is not logged in") + } var etpRt string for _, cookie := range resp.Cookies() { @@ -244,22 +259,11 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt 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 = "-" - } - - if strings.Contains(cms["bucket"].(string), "crunchyroll") { - crunchy.Config.Premium = true - crunchy.Config.Channel = "crunchyroll" - } else { - crunchy.Config.Premium = false - crunchy.Config.Channel = "-" - } + crunchy.Config.Premium = strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") + // / is trimmed so that urls which require it must be in .../{bucket}/... like format. + // this just looks cleaner + crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/") crunchy.Config.Policy = cms["policy"].(string) crunchy.Config.Signature = cms["signature"].(string) crunchy.Config.KeyPairID = cms["key_pair_id"].(string) diff --git a/episode.go b/episode.go index 0405e0e..c90a5c7 100644 --- a/episode.go +++ b/episode.go @@ -89,10 +89,8 @@ type HistoryEpisode struct { // 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", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -126,6 +124,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) { // 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 // this method on the first episode of the season. +// Will fail if no streams are available, thus use Episode.Available +// to prevent any misleading errors. func (e *Episode) AudioLocale() (LOCALE, error) { streams, err := e.Streams() if err != nil { @@ -134,6 +134,11 @@ func (e *Episode) AudioLocale() (LOCALE, error) { return streams[0].AudioLocale, nil } +// Available returns if downloadable streams for this episodes are available. +func (e *Episode) Available() bool { + return e.crunchy.Config.Premium || !e.IsPremiumOnly +} + // GetFormat returns the format which matches the given resolution and subtitle locale. func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) { streams, err := e.Streams() @@ -206,10 +211,8 @@ func (e *Episode) Streams() ([]*Stream, error) { return e.children, nil } - streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - e.crunchy.Config.CountryCode, - e.crunchy.Config.MaturityRating, - e.crunchy.Config.Channel, + streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + e.crunchy.Config.Bucket, e.StreamID, e.crunchy.Locale, e.crunchy.Config.Signature, diff --git a/movie_listing.go b/movie_listing.go index 110f67a..ec49e99 100644 --- a/movie_listing.go +++ b/movie_listing.go @@ -41,10 +41,8 @@ type MovieListing struct { // MovieListingFromID returns a movie listing by its api id. func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) { - resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -70,10 +68,8 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) // AudioLocale is same as Episode.AudioLocale. func (ml *MovieListing) AudioLocale() (LOCALE, error) { - resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - ml.crunchy.Config.CountryCode, - ml.crunchy.Config.MaturityRating, - ml.crunchy.Config.Channel, + resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + ml.crunchy.Config.Bucket, ml.ID, ml.crunchy.Locale, ml.crunchy.Config.Signature, @@ -91,10 +87,8 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) { // Streams returns all streams which are available for the movie listing. func (ml *MovieListing) Streams() ([]*Stream, error) { - return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - ml.crunchy.Config.CountryCode, - ml.crunchy.Config.MaturityRating, - ml.crunchy.Config.Channel, + return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + ml.crunchy.Config.Bucket, ml.ID, ml.crunchy.Locale, ml.crunchy.Config.Signature, diff --git a/season.go b/season.go index f5513f5..907122d 100644 --- a/season.go +++ b/season.go @@ -38,17 +38,15 @@ type Season struct { AvailabilityNotes string `json:"availability_notes"` - // the locales are always empty, idk why this may change in the future + // the locales are always empty, idk why, this may change in the future AudioLocales []LOCALE SubtitleLocales []LOCALE } // SeasonFromID returns a season by its api id. func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) { - resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -73,6 +71,8 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) { } // AudioLocale returns the audio locale of the season. +// Will fail if no streams are available, thus use Season.Available +// to prevent any misleading errors. func (s *Season) AudioLocale() (LOCALE, error) { episodes, err := s.Episodes() if err != nil { @@ -81,16 +81,23 @@ func (s *Season) AudioLocale() (LOCALE, error) { return episodes[0].AudioLocale() } +// Available returns if downloadable streams for this season are available. +func (s *Season) Available() (bool, error) { + episodes, err := s.Episodes() + if err != nil { + return false, err + } + return episodes[0].Available(), nil +} + // Episodes returns all episodes which are available for the season. func (s *Season) Episodes() (episodes []*Episode, err error) { if s.children != nil { return s.children, nil } - resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - s.crunchy.Config.CountryCode, - s.crunchy.Config.MaturityRating, - s.crunchy.Config.Channel, + resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + s.crunchy.Config.Bucket, s.ID, s.crunchy.Locale, s.crunchy.Config.Signature, @@ -112,8 +119,10 @@ func (s *Season) Episodes() (episodes []*Episode, err error) { } if episode.Playback != "" { streamHref := item.(map[string]interface{})["__links__"].(map[string]interface{})["streams"].(map[string]interface{})["href"].(string) - if match := regexp.MustCompile(`(?m)^/cms/v2/\S+videos/(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 { + if match := regexp.MustCompile(`(?m)(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 { episode.StreamID = match[0][1] + } else { + fmt.Println() } } episodes = append(episodes, episode) diff --git a/stream.go b/stream.go index 00060ef..e5ce54f 100644 --- a/stream.go +++ b/stream.go @@ -25,10 +25,8 @@ type Stream struct { // StreamsFromID returns a stream by its api id. func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) { - return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -82,8 +80,12 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, json.NewDecoder(resp.Body).Decode(&jsonBody) if len(jsonBody) == 0 { - // this may get thrown when the crunchyroll account has just a normal account and not one with premium - return nil, fmt.Errorf("no stream available") + // this may get thrown when the crunchyroll account is just a normal account and not one with premium + if !crunchy.Config.Premium { + return nil, fmt.Errorf("no stream available, this might be the result of using a non-premium account") + } else { + return nil, fmt.Errorf("no stream available") + } } audioLocale := jsonBody["audio_locale"].(string) @@ -105,7 +107,7 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, var id string var formatType FormatType href := jsonBody["__links__"].(map[string]interface{})["resource"].(map[string]interface{})["href"].(string) - if match := regexp.MustCompile(`(?sm)^/cms/v2/\S+/crunchyroll/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 { + if match := regexp.MustCompile(`(?sm)/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 { formatType = FormatType(match[0][1]) id = match[0][2] } diff --git a/url.go b/url.go index 32603fc..95f3951 100644 --- a/url.go +++ b/url.go @@ -13,6 +13,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep } var eps []*Episode + var notAvailableContinue bool if series != nil { seasons, err := series.Seasons() @@ -21,6 +22,13 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep } for _, season := range seasons { if audio != nil { + if available, err := season.Available(); err != nil { + return nil, err + } else if !available { + notAvailableContinue = true + continue + } + locale, err := season.AudioLocale() if err != nil { return nil, err @@ -49,6 +57,12 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep } for _, episode := range episodes { + // if no episode streams are available, calling episode.AudioLocale + // will result in an unwanted error + if !episode.Available() { + notAvailableContinue = true + continue + } locale, err := episode.AudioLocale() if err != nil { return nil, err @@ -71,7 +85,11 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep } if len(eps) == 0 { - return nil, fmt.Errorf("could not find any matching episode") + if notAvailableContinue { + return nil, fmt.Errorf("could not find any matching episode which is accessable with a non-premium account") + } else { + return nil, fmt.Errorf("could not find any matching episode") + } } return eps, nil diff --git a/utils/sort.go b/utils/sort.go index eacb96b..661eea8 100644 --- a/utils/sort.go +++ b/utils/sort.go @@ -52,6 +52,9 @@ func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCAL var wg sync.WaitGroup var lock sync.Mutex for _, episode := range episodes { + if !episode.Available() { + continue + } episode := episode wg.Add(1) go func() { diff --git a/video.go b/video.go index 365f954..f72a327 100644 --- a/video.go +++ b/video.go @@ -70,10 +70,8 @@ type Movie struct { // MovieFromID returns a movie by its api id. func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) { - resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -103,10 +101,8 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) { return m.children, nil } - resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - m.crunchy.Config.CountryCode, - m.crunchy.Config.MaturityRating, - m.crunchy.Config.Channel, + resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + m.crunchy.Config.Bucket, m.ID, m.crunchy.Locale, m.crunchy.Config.Signature, @@ -166,10 +162,8 @@ type Series struct { // SeriesFromID returns a series by its api id. func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) { - resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - crunchy.Config.CountryCode, - crunchy.Config.MaturityRating, - crunchy.Config.Channel, + resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + crunchy.Config.Bucket, id, crunchy.Locale, crunchy.Config.Signature, @@ -199,10 +193,8 @@ func (s *Series) Seasons() (seasons []*Season, err error) { return s.children, nil } - resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", - s.crunchy.Config.CountryCode, - s.crunchy.Config.MaturityRating, - s.crunchy.Config.Channel, + resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s", + s.crunchy.Config.Bucket, s.ID, s.crunchy.Locale, s.crunchy.Config.Signature,