mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Resolve conflics
This commit is contained in:
commit
cba8968f17
14 changed files with 294 additions and 87 deletions
|
|
@ -186,8 +186,10 @@ func archive(urls []string) error {
|
||||||
episodes, err := archiveExtractEpisodes(url)
|
episodes, err := archiveExtractEpisodes(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.StopProgress("Failed to parse url %d", i+1)
|
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, " +
|
if crunchy.Config.Premium {
|
||||||
"try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
|
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
|
return err
|
||||||
}
|
}
|
||||||
out.StopProgress("Parsed url %d", i+1)
|
out.StopProgress("Parsed url %d", i+1)
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,10 @@ func download(urls []string) error {
|
||||||
episodes, err := downloadExtractEpisodes(url)
|
episodes, err := downloadExtractEpisodes(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.StopProgress("Failed to parse url %d", i+1)
|
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, " +
|
if crunchy.Config.Premium {
|
||||||
"try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
|
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
|
return err
|
||||||
}
|
}
|
||||||
out.StopProgress("Parsed url %d", i+1)
|
out.StopProgress("Parsed url %d", i+1)
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,22 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
"github.com/ByteDream/crunchyroll-go/v3"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
loginPersistentFlag bool
|
loginPersistentFlag bool
|
||||||
|
loginEncryptFlag bool
|
||||||
|
|
||||||
loginSessionIDFlag bool
|
loginSessionIDFlag bool
|
||||||
loginEtpRtFlag bool
|
loginEtpRtFlag bool
|
||||||
|
|
@ -36,6 +43,10 @@ func init() {
|
||||||
"persistent",
|
"persistent",
|
||||||
false,
|
false,
|
||||||
"If the given credential should be stored persistent")
|
"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,
|
loginCmd.Flags().BoolVar(&loginSessionIDFlag,
|
||||||
"session-id",
|
"session-id",
|
||||||
|
|
@ -60,13 +71,68 @@ func loginCredentials(user, password string) error {
|
||||||
if configDir, err := os.UserConfigDir(); err != nil {
|
if configDir, err := os.UserConfigDir(); err != nil {
|
||||||
return fmt.Errorf("could not save credentials persistent: %w", err)
|
return fmt.Errorf("could not save credentials persistent: %w", err)
|
||||||
} else {
|
} 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)
|
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
|
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 {
|
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
48
cmd/crunchyroll-go/cmd/unix.go
Normal file
48
cmd/crunchyroll-go/cmd/unix.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
"github.com/ByteDream/crunchyroll-go/v3"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||||
|
|
@ -147,7 +150,7 @@ func loadCrunchy() {
|
||||||
out.StopProgress("Failed to read login information: %v", err)
|
out.StopProgress("Failed to read login information: %v", err)
|
||||||
os.Exit(1)
|
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)
|
out.Debug("Failed to login with temp etp rt cookie: %v", err)
|
||||||
} else {
|
} 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)
|
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)
|
split := strings.SplitN(string(body), "\n", 2)
|
||||||
if len(split) == 1 || split[1] == "" {
|
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 {
|
if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], systemLocale(true), client); err != nil {
|
||||||
out.StopProgress(err.Error())
|
out.StopProgress(err.Error())
|
||||||
os.Exit(1)
|
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])
|
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")
|
out.StopProgress("Logged in")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
cmd/crunchyroll-go/cmd/windows.go
Normal file
41
cmd/crunchyroll-go/cmd/windows.go
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -175,23 +175,10 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
|
||||||
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
||||||
|
|
||||||
cms := jsonBody["cms"].(map[string]any)
|
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), "/")
|
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.Policy = cms["policy"].(string)
|
||||||
crunchy.Config.Signature = cms["signature"].(string)
|
crunchy.Config.Signature = cms["signature"].(string)
|
||||||
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
|
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
|
||||||
|
|
|
||||||
19
episode.go
19
episode.go
|
|
@ -97,10 +97,8 @@ const (
|
||||||
|
|
||||||
// EpisodeFromID returns an episode by its api id.
|
// EpisodeFromID returns an episode by its api id.
|
||||||
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -159,6 +157,8 @@ func (e *Episode) RemoveFromWatchlist() error {
|
||||||
// Every episode in a season (should) have the same audio locale,
|
// 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
|
// so if you want to get the audio locale of a season, just call
|
||||||
// this method on the first episode of the season.
|
// 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) {
|
func (e *Episode) AudioLocale() (LOCALE, error) {
|
||||||
streams, err := e.Streams()
|
streams, err := e.Streams()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -247,6 +247,11 @@ func (e *Episode) Comments(options CommentsOptions, page uint, size uint) (c []*
|
||||||
return
|
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.
|
// GetFormat returns the format which matches the given resolution and subtitle locale.
|
||||||
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
|
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
|
||||||
streams, err := e.Streams()
|
streams, err := e.Streams()
|
||||||
|
|
@ -319,10 +324,8 @@ func (e *Episode) Streams() ([]*Stream, error) {
|
||||||
return e.children, nil
|
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",
|
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.CountryCode,
|
e.crunchy.Config.Bucket,
|
||||||
e.crunchy.Config.MaturityRating,
|
|
||||||
e.crunchy.Config.Channel,
|
|
||||||
e.StreamID,
|
e.StreamID,
|
||||||
e.crunchy.Locale,
|
e.crunchy.Locale,
|
||||||
e.crunchy.Config.Signature,
|
e.crunchy.Config.Signature,
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,8 @@ type MovieListing struct {
|
||||||
|
|
||||||
// MovieListingFromID returns a movie listing by its api id.
|
// MovieListingFromID returns a movie listing by its api id.
|
||||||
func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -65,10 +63,8 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
|
||||||
|
|
||||||
// AudioLocale is same as Episode.AudioLocale.
|
// AudioLocale is same as Episode.AudioLocale.
|
||||||
func (ml *MovieListing) AudioLocale() (LOCALE, error) {
|
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",
|
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.CountryCode,
|
ml.crunchy.Config.Bucket,
|
||||||
ml.crunchy.Config.MaturityRating,
|
|
||||||
ml.crunchy.Config.Channel,
|
|
||||||
ml.ID,
|
ml.ID,
|
||||||
ml.crunchy.Locale,
|
ml.crunchy.Locale,
|
||||||
ml.crunchy.Config.Signature,
|
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.
|
// Streams returns all streams which are available for the movie listing.
|
||||||
func (ml *MovieListing) Streams() ([]*Stream, error) {
|
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",
|
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.CountryCode,
|
ml.crunchy.Config.Bucket,
|
||||||
ml.crunchy.Config.MaturityRating,
|
|
||||||
ml.crunchy.Config.Channel,
|
|
||||||
ml.ID,
|
ml.ID,
|
||||||
ml.crunchy.Locale,
|
ml.crunchy.Locale,
|
||||||
ml.crunchy.Config.Signature,
|
ml.crunchy.Config.Signature,
|
||||||
|
|
|
||||||
29
season.go
29
season.go
|
|
@ -38,17 +38,15 @@ type Season struct {
|
||||||
|
|
||||||
AvailabilityNotes string `json:"availability_notes"`
|
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
|
AudioLocales []LOCALE
|
||||||
SubtitleLocales []LOCALE
|
SubtitleLocales []LOCALE
|
||||||
}
|
}
|
||||||
|
|
||||||
// SeasonFromID returns a season by its api id.
|
// SeasonFromID returns a season by its api id.
|
||||||
func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -73,6 +71,8 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioLocale returns the audio locale of the season.
|
// 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) {
|
func (s *Season) AudioLocale() (LOCALE, error) {
|
||||||
episodes, err := s.Episodes()
|
episodes, err := s.Episodes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -81,16 +81,23 @@ func (s *Season) AudioLocale() (LOCALE, error) {
|
||||||
return episodes[0].AudioLocale()
|
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.
|
// Episodes returns all episodes which are available for the season.
|
||||||
func (s *Season) Episodes() (episodes []*Episode, err error) {
|
func (s *Season) Episodes() (episodes []*Episode, err error) {
|
||||||
if s.children != nil {
|
if s.children != nil {
|
||||||
return 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",
|
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.CountryCode,
|
s.crunchy.Config.Bucket,
|
||||||
s.crunchy.Config.MaturityRating,
|
|
||||||
s.crunchy.Config.Channel,
|
|
||||||
s.ID,
|
s.ID,
|
||||||
s.crunchy.Locale,
|
s.crunchy.Locale,
|
||||||
s.crunchy.Config.Signature,
|
s.crunchy.Config.Signature,
|
||||||
|
|
@ -112,8 +119,10 @@ func (s *Season) Episodes() (episodes []*Episode, err error) {
|
||||||
}
|
}
|
||||||
if episode.Playback != "" {
|
if episode.Playback != "" {
|
||||||
streamHref := item.(map[string]interface{})["__links__"].(map[string]interface{})["streams"].(map[string]interface{})["href"].(string)
|
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]
|
episode.StreamID = match[0][1]
|
||||||
|
} else {
|
||||||
|
fmt.Println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
episodes = append(episodes, episode)
|
episodes = append(episodes, episode)
|
||||||
|
|
|
||||||
16
stream.go
16
stream.go
|
|
@ -25,10 +25,8 @@ type Stream struct {
|
||||||
|
|
||||||
// StreamsFromID returns a stream by its api id.
|
// StreamsFromID returns a stream by its api id.
|
||||||
func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -82,8 +80,12 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
|
||||||
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
json.NewDecoder(resp.Body).Decode(&jsonBody)
|
||||||
|
|
||||||
if len(jsonBody) == 0 {
|
if len(jsonBody) == 0 {
|
||||||
// this may get thrown when the crunchyroll account has just a normal account and not one with premium
|
// this may get thrown when the crunchyroll account is just a normal account and not one with premium
|
||||||
return nil, fmt.Errorf("no stream available")
|
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)
|
audioLocale := jsonBody["audio_locale"].(string)
|
||||||
|
|
@ -105,7 +107,7 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
|
||||||
var id string
|
var id string
|
||||||
var formatType FormatType
|
var formatType FormatType
|
||||||
href := jsonBody["__links__"].(map[string]interface{})["resource"].(map[string]interface{})["href"].(string)
|
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])
|
formatType = FormatType(match[0][1])
|
||||||
id = match[0][2]
|
id = match[0][2]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
20
url.go
20
url.go
|
|
@ -13,6 +13,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
|
||||||
}
|
}
|
||||||
|
|
||||||
var eps []*Episode
|
var eps []*Episode
|
||||||
|
var notAvailableContinue bool
|
||||||
|
|
||||||
if series != nil {
|
if series != nil {
|
||||||
seasons, err := series.Seasons()
|
seasons, err := series.Seasons()
|
||||||
|
|
@ -21,6 +22,13 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
|
||||||
}
|
}
|
||||||
for _, season := range seasons {
|
for _, season := range seasons {
|
||||||
if audio != nil {
|
if audio != nil {
|
||||||
|
if available, err := season.Available(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !available {
|
||||||
|
notAvailableContinue = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
locale, err := season.AudioLocale()
|
locale, err := season.AudioLocale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -49,6 +57,12 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, episode := range episodes {
|
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()
|
locale, err := episode.AudioLocale()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -71,7 +85,11 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(eps) == 0 {
|
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
|
return eps, nil
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCAL
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var lock sync.Mutex
|
var lock sync.Mutex
|
||||||
for _, episode := range episodes {
|
for _, episode := range episodes {
|
||||||
|
if !episode.Available() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
episode := episode
|
episode := episode
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|
|
||||||
24
video.go
24
video.go
|
|
@ -61,10 +61,8 @@ type Movie struct {
|
||||||
|
|
||||||
// MovieFromID returns a movie by its api id.
|
// MovieFromID returns a movie by its api id.
|
||||||
func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -94,10 +92,8 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
|
||||||
return m.children, nil
|
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",
|
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.CountryCode,
|
m.crunchy.Config.Bucket,
|
||||||
m.crunchy.Config.MaturityRating,
|
|
||||||
m.crunchy.Config.Channel,
|
|
||||||
m.ID,
|
m.ID,
|
||||||
m.crunchy.Locale,
|
m.crunchy.Locale,
|
||||||
m.crunchy.Config.Signature,
|
m.crunchy.Config.Signature,
|
||||||
|
|
@ -157,10 +153,8 @@ type Series struct {
|
||||||
|
|
||||||
// SeriesFromID returns a series by its api id.
|
// SeriesFromID returns a series by its api id.
|
||||||
func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
|
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",
|
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.CountryCode,
|
crunchy.Config.Bucket,
|
||||||
crunchy.Config.MaturityRating,
|
|
||||||
crunchy.Config.Channel,
|
|
||||||
id,
|
id,
|
||||||
crunchy.Locale,
|
crunchy.Locale,
|
||||||
crunchy.Config.Signature,
|
crunchy.Config.Signature,
|
||||||
|
|
@ -255,10 +249,8 @@ func (s *Series) Seasons() (seasons []*Season, err error) {
|
||||||
return s.children, nil
|
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",
|
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.CountryCode,
|
s.crunchy.Config.Bucket,
|
||||||
s.crunchy.Config.MaturityRating,
|
|
||||||
s.crunchy.Config.Channel,
|
|
||||||
s.ID,
|
s.ID,
|
||||||
s.crunchy.Locale,
|
s.crunchy.Locale,
|
||||||
s.crunchy.Config.Signature,
|
s.crunchy.Config.Signature,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue