Merge branch 'next/v3' into v3/feature/non-premium-support

This commit is contained in:
ByteDream 2022-06-10 16:04:25 +02:00 committed by GitHub
commit ae075ed4c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1173 additions and 205 deletions

View file

@ -8,11 +8,12 @@ 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"
"math"
"os"
"os/exec"
"os/signal"
@ -98,9 +99,12 @@ var archiveCmd = &cobra.Command{
}
switch archiveResolutionFlag {
case "1080p", "720p", "480p", "360p", "240p":
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
archiveResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(archiveResolutionFlag, "p"))
case "1080p", "720p", "480p", "360p":
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
case "240p":
// 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
archiveResolutionFlag = "428x240"
case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
default:
return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)

View file

@ -3,10 +3,11 @@ 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"
"os"
"os/signal"
"path/filepath"
@ -53,9 +54,12 @@ var downloadCmd = &cobra.Command{
out.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
switch downloadResolutionFlag {
case "1080p", "720p", "480p", "360p", "240p":
case "1080p", "720p", "480p", "360p":
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
downloadResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(downloadResolutionFlag, "p"))
downloadResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
case "240p":
// 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
downloadResolutionFlag = "428x240"
case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
default:
return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)

View file

@ -1,19 +1,25 @@
package cmd
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/ByteDream/crunchyroll-go/v3"
"github.com/spf13/cobra"
"io"
"os"
"os/user"
"path/filepath"
"runtime"
)
var (
loginPersistentFlag bool
loginEncryptFlag bool
loginSessionIDFlag bool
loginEtpRtFlag bool
)
var loginCmd = &cobra.Command{
@ -21,11 +27,13 @@ var loginCmd = &cobra.Command{
Short: "Login to crunchyroll",
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
RunE: func(cmd *cobra.Command, args []string) error {
if loginSessionIDFlag {
loginSessionID(args[0])
return loginSessionID(args[0])
} else if loginEtpRtFlag {
return loginEtpRt(args[0])
} else {
loginCredentials(args[0], args[1])
return loginCredentials(args[0], args[1])
}
},
}
@ -35,47 +43,164 @@ 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",
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)
}
func loginCredentials(user, password string) error {
out.Debug("Logging in via credentials")
if _, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client); err != nil {
out.Err(err.Error())
os.Exit(1)
c, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client)
if err != nil {
return err
}
return os.WriteFile(loginStorePath(), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600)
if loginPersistentFlag {
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"), credentials, 0600); err != nil {
return err
}
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
}
if !loginPersistentFlag {
out.Info("Due to security reasons, you have to login again on the next reboot")
}
return nil
}
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)
}
return os.WriteFile(loginStorePath(), []byte(sessionID), 0600)
}
func loginStorePath() string {
path := filepath.Join(os.TempDir(), ".crunchy")
if loginPersistentFlag {
if runtime.GOOS != "windows" {
usr, _ := user.Current()
path = filepath.Join(usr.HomeDir, ".config/crunchy")
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
}
out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", path)
} else if runtime.GOOS != "windows" {
if !loginPersistentFlag {
out.Info("Due to security reasons, you have to login again on the next reboot")
}
return path
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)
}
var err error
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(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(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
}

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"
@ -27,7 +27,7 @@ var (
)
var rootCmd = &cobra.Command{
Use: "crunchyroll",
Use: "crunchyroll-go",
Version: Version,
Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",

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

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

@ -1,14 +1,16 @@
package cmd
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/ByteDream/crunchyroll-go/v2/utils"
"github.com/ByteDream/crunchyroll-go/v3"
"github.com/ByteDream/crunchyroll-go/v3/utils"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"reflect"
"regexp"
@ -141,57 +143,92 @@ func freeFileName(filename string) (string, bool) {
func loadCrunchy() {
out.SetProgress("Logging in")
files := []string{filepath.Join(os.TempDir(), ".crunchy")}
if runtime.GOOS != "windows" {
usr, _ := user.Current()
files = append(files, filepath.Join(usr.HomeDir, ".config/crunchy"))
}
var err error
for _, file := range files {
if _, err = os.Stat(file); os.IsNotExist(err) {
err = nil
continue
}
var body []byte
if body, err = os.ReadFile(file); err != nil {
tmpFilePath := filepath.Join(os.TempDir(), ".crunchy")
if _, statErr := os.Stat(tmpFilePath); !os.IsNotExist(statErr) {
body, err := os.ReadFile(tmpFilePath)
if err != nil {
out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
} else if body == nil {
continue
}
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err == nil {
out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
}
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 {
if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
continue
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
}
}
if configDir, err := os.UserConfigDir(); err == nil {
persistentFilePath := filepath.Join(configDir, "crunchyroll-go", "crunchy")
if _, statErr := os.Stat(persistentFilePath); statErr == nil {
body, err := os.ReadFile(persistentFilePath)
if err != nil {
out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
}
out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
if file != filepath.Join(os.TempDir(), ".crunchy") {
// the session id 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
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600); err != nil {
out.StopProgress("Failed to write session id to temp file")
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
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("Wrote session id to temp file")
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])
}
// 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
}
out.StopProgress("Logged in")
return
}
if err != nil {
out.StopProgress(err.Error())
} else {
out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
}
out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
os.Exit(1)
}

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
}