Merge branch 'next/v3' into v3/feature/common-api-endpoints

This commit is contained in:
ByteDream 2022-05-30 12:19:20 +02:00 committed by GitHub
commit 0092867b97
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 367 additions and 150 deletions

View file

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

View file

@ -203,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).

View file

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

View file

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

View file

@ -2,7 +2,7 @@ package cmd
import (
"fmt"
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/ByteDream/crunchyroll-go/v3"
"github.com/spf13/cobra"
"os"
"path/filepath"
@ -12,6 +12,7 @@ var (
loginPersistentFlag bool
loginSessionIDFlag bool
loginEtpRtFlag bool
)
var loginCmd = &cobra.Command{
@ -22,6 +23,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])
}
@ -38,6 +41,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)
}
@ -60,7 +67,7 @@ func loginCredentials(user, password string) error {
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.SessionID), 0600); err != nil {
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
return err
}
@ -73,7 +80,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)
}
@ -84,13 +122,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
}

View file

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

View file

@ -0,0 +1,136 @@
package cmd
import (
"encoding/json"
"fmt"
"github.com/spf13/cobra"
"io"
"os"
"os/exec"
"runtime"
"strings"
)
var (
updateInstallFlag bool
)
var updateCmd = &cobra.Command{
Use: "update",
Short: "Check if updates are available",
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
return update()
},
}
func init() {
updateCmd.Flags().BoolVarP(&updateInstallFlag,
"install",
"i",
false,
"If set and a new version is available, the new version gets installed")
rootCmd.AddCommand(updateCmd)
}
func update() error {
var release map[string]interface{}
resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchyroll-go/releases/latest")
if err != nil {
return err
}
defer resp.Body.Close()
if err = json.NewDecoder(resp.Body).Decode(&release); err != nil {
return err
}
releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
if Version == "development" {
out.Info("Development version, update service not available")
return nil
}
latestRelease := strings.SplitN(releaseVersion, ".", 4)
if len(latestRelease) != 3 {
return fmt.Errorf("latest tag name (%s) is not parsable", releaseVersion)
}
internalVersion := strings.SplitN(Version, ".", 4)
if len(internalVersion) != 3 {
return fmt.Errorf("internal version (%s) is not parsable", Version)
}
out.Info("Installed version is %s", Version)
var hasUpdate bool
for i := 0; i < 3; i++ {
if latestRelease[i] < internalVersion[i] {
out.Info("Local version is newer than version in latest release (%s)", releaseVersion)
return nil
} else if latestRelease[i] > internalVersion[i] {
hasUpdate = true
}
}
if !hasUpdate {
out.Info("Version is up-to-date")
return nil
}
out.Info("A new version is available (%s): https://github.com/ByteDream/crunchyroll-go/releases/tag/v%s", releaseVersion, releaseVersion)
if updateInstallFlag {
if runtime.GOARCH != "amd64" {
return fmt.Errorf("invalid architecture found (%s), only amd64 is currently supported for automatic updating. "+
"You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOARCH)
}
var downloadFile string
switch runtime.GOOS {
case "linux":
yayCommand := exec.Command("pacman -Q crunchyroll-go")
if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
out.Info("crunchyroll-go was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
return nil
}
downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
case "darwin":
downloadFile = fmt.Sprintf("crunchy-v%s_darwin", releaseVersion)
case "windows":
downloadFile = fmt.Sprintf("crunchy-v%s_windows.exe", releaseVersion)
default:
return fmt.Errorf("invalid operation system found (%s), only linux, windows and darwin / macos are currently supported. "+
"You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOOS)
}
out.SetProgress("Updating executable %s", os.Args[0])
perms, err := os.Stat(os.Args[0])
if err != nil {
return err
}
os.Remove(os.Args[0])
executeFile, err := os.OpenFile(os.Args[0], os.O_CREATE|os.O_WRONLY, perms.Mode())
if err != nil {
return err
}
defer executeFile.Close()
resp, err := client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchyroll-go/releases/download/v%s/%s", releaseVersion, downloadFile))
if err != nil {
return err
}
defer resp.Body.Close()
if _, err = io.Copy(executeFile, resp.Body); err != nil {
return err
}
out.StopProgress("Updated executable %s", os.Args[0])
}
return nil
}

View file

@ -2,8 +2,8 @@ package cmd
import (
"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"
@ -147,10 +147,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
@ -168,20 +168,20 @@ func loadCrunchy() {
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
split[0] = url.QueryEscape(split[0])
if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], 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", 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 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")
return

View file

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

View file

@ -13,6 +13,8 @@ crunchyroll-go login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR]
crunchyroll-go download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.br
crunchyroll-go archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.br
crunchyroll-go update [\fB-i\fR \fIINSTALL\fR]
.SH DESCRIPTION
.TP
@ -141,6 +143,13 @@ The video resolution. Can either be specified via the pixels (e.g. 1920x1080), t
\fB-g, --goroutines GOROUTINES\fR
Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
.SH UPDATE COMMAND
Checks if a newer version is available.
.TP
\fB-i, --install INSTALL\fR
If given, the command tries to update the executable with the newer version (if a newer is available).
.SH URL OPTIONS
If you want to download only specific episode of a series, you could either pass every single episode url to the downloader (which is fine for 1 - 3 episodes) or use filtering.
It works pretty simple, just put a specific pattern surrounded by square brackets at the end of the url from the anime you want to download. A season and / or episode as well as a range from where to where episodes should be downloaded can be specified.

View file

@ -54,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
@ -101,72 +104,62 @@ type BrowseOptions struct {
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
}
@ -194,48 +187,71 @@ 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)
json.NewDecoder(resp.Body).Decode(&jsonBody)
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 = "-"
}
crunchy.Config.TokenType = jsonBody["token_type"].(string)
crunchy.Config.AccessToken = jsonBody["access_token"].(string)
// index
endpoint = "https://beta-api.crunchyroll.com/index/v2"
resp, err = crunchy.request(endpoint)
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 'index' response: %w", err)
}
cms := jsonBody["cms"].(map[string]interface{})
if strings.Contains(cms["bucket"].(string), "crunchyroll") {
crunchy.Config.Premium = true
@ -244,67 +260,80 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
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.
@ -325,7 +354,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
}

View file

@ -3,6 +3,7 @@ package crunchyroll
import (
"encoding/json"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
@ -96,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
}

2
go.mod
View file

@ -1,4 +1,4 @@
module github.com/ByteDream/crunchyroll-go/v2
module github.com/ByteDream/crunchyroll-go/v3
go 1.18

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package utils
import (
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/ByteDream/crunchyroll-go/v3"
"sort"
"strconv"
"strings"

View file

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