Resolve conflics

This commit is contained in:
bytedream 2022-06-26 14:41:42 +02:00
commit cba8968f17
14 changed files with 294 additions and 87 deletions

View file

@ -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)

View file

@ -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)

View file

@ -1,15 +1,22 @@
package cmd
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v3"
"github.com/spf13/cobra"
"io"
"os"
"path/filepath"
)
var (
loginPersistentFlag bool
loginEncryptFlag bool
loginSessionIDFlag bool
loginEtpRtFlag bool
@ -36,6 +43,10 @@ func init() {
"persistent",
false,
"If the given credential should be stored persistent")
loginCmd.Flags().BoolVar(&loginEncryptFlag,
"encrypt",
false,
"Encrypt the given credentials (won't do anything if --session-id is given or --persistent is not given)")
loginCmd.Flags().BoolVar(&loginSessionIDFlag,
"session-id",
@ -60,13 +71,68 @@ func loginCredentials(user, password string) error {
if configDir, err := os.UserConfigDir(); err != nil {
return fmt.Errorf("could not save credentials persistent: %w", err)
} else {
var credentials []byte
if loginEncryptFlag {
var passwd []byte
for {
fmt.Print("Enter password: ")
passwd, err = readLineSilent()
if err != nil {
return err
}
fmt.Println()
fmt.Print("Enter password again: ")
repasswd, err := readLineSilent()
if err != nil {
return err
}
fmt.Println()
if !bytes.Equal(passwd, repasswd) {
fmt.Println("Passwords does not match, try again")
continue
}
hashedPassword := sha256.Sum256(passwd)
block, err := aes.NewCipher(hashedPassword[:])
if err != nil {
out.Err("Failed to create block: %w", err)
os.Exit(1)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
out.Err("Failed to create gcm: %w", err)
os.Exit(1)
}
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
out.Err("Failed to fill nonce: %w", err)
os.Exit(1)
}
b := gcm.Seal(nonce, nonce, []byte(fmt.Sprintf("%s\n%s", user, password)), nil)
credentials = append([]byte("aes:"), b...)
break
}
} else {
credentials = []byte(fmt.Sprintf("%s\n%s", user, password))
}
os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600); err != nil {
if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), credentials, 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 !loginEncryptFlag {
out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s). "+
"To encrypt it, use the `--encrypt` flag", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
}
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
return err
}

View file

@ -0,0 +1,48 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
package cmd
import (
"bufio"
"os"
"os/exec"
"syscall"
)
// https://github.com/bgentry/speakeasy/blob/master/speakeasy_unix.go
var stty string
func init() {
var err error
if stty, err = exec.LookPath("stty"); err != nil {
panic(err)
}
}
func readLineSilent() ([]byte, error) {
pid, err := setEcho(false)
if err != nil {
return nil, err
}
defer setEcho(true)
syscall.Wait4(pid, nil, 0, nil)
l, _, err := bufio.NewReader(os.Stdin).ReadLine()
return l, err
}
func setEcho(on bool) (pid int, err error) {
fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}
if on {
pid, err = syscall.ForkExec(stty, []string{"stty", "echo"}, &syscall.ProcAttr{Files: fds})
} else {
pid, err = syscall.ForkExec(stty, []string{"stty", "-echo"}, &syscall.ProcAttr{Files: fds})
}
if err != nil {
return 0, err
}
return
}

View file

@ -1,6 +1,9 @@
package cmd
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v3"
"github.com/ByteDream/crunchyroll-go/v3/utils"
@ -147,7 +150,7 @@ func loadCrunchy() {
out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
}
if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
if crunchy, err = crunchyroll.LoginWithEtpRt(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 etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
@ -167,22 +170,59 @@ func loadCrunchy() {
}
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
split[0] = url.QueryEscape(split[0])
if strings.HasPrefix(split[0], "aes:") {
encrypted := body[4:]
out.StopProgress("Credentials are encrypted")
fmt.Print("Enter password to encrypt it: ")
passwd, err := readLineSilent()
fmt.Println()
if err != nil {
out.Err("Failed to read password; %w", err)
os.Exit(1)
}
out.SetProgress("Logging in")
hashedPassword := sha256.Sum256(passwd)
block, err := aes.NewCipher(hashedPassword[:])
if err != nil {
out.Err("Failed to create block: %w", err)
os.Exit(1)
}
gcm, err := cipher.NewGCM(block)
if err != nil {
out.Err("Failed to create gcm: %w", err)
os.Exit(1)
}
nonce, c := encrypted[:gcm.NonceSize()], encrypted[gcm.NonceSize():]
b, err := gcm.Open(nil, nonce, c, nil)
if err != nil {
out.StopProgress("Invalid password")
os.Exit(1)
}
split = strings.SplitN(string(b), "\n", 2)
}
}
if len(split) == 2 {
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 credentials")
} else {
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 {
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 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.EtpRt), 0600)
}
// 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.EtpRt), 0600)
out.StopProgress("Logged in")
return
}

View file

@ -0,0 +1,41 @@
//go:build windows
package cmd
import (
"bufio"
"os"
"syscall"
)
// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
func readLineSilent() ([]byte, error) {
var oldMode uint32
if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
return nil, err
}
newMode := oldMode &^ 0x0004
err := setConsoleMode(syscall.Stdin, newMode)
defer setConsoleMode(syscall.Stdin, oldMode)
if err != nil {
return nil, err
}
l, _, err := bufio.NewReader(os.Stdin).ReadLine()
if err != nil {
return nil, err
}
return l, err
}
func setConsoleMode(console syscall.Handle, mode uint32) error {
dll := syscall.MustLoadDLL("kernel32")
proc := dll.MustFindProc("SetConsoleMode")
_, _, err := proc.Call(uintptr(console), uintptr(mode))
return err
}

View file

@ -175,23 +175,10 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
json.NewDecoder(resp.Body).Decode(&jsonBody)
cms := jsonBody["cms"].(map[string]any)
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), "/")
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.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)

View file

@ -97,10 +97,8 @@ const (
// 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,
@ -159,6 +157,8 @@ func (e *Episode) RemoveFromWatchlist() 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 {
@ -247,6 +247,11 @@ func (e *Episode) Comments(options CommentsOptions, page uint, size uint) (c []*
return
}
// 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()
@ -319,10 +324,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,

View file

@ -36,10 +36,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,
@ -65,10 +63,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,
@ -86,10 +82,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,

View file

@ -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)

View file

@ -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]
}

20
url.go
View file

@ -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

View file

@ -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() {

View file

@ -61,10 +61,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,
@ -94,10 +92,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,
@ -157,10 +153,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,
@ -255,10 +249,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,