mirror of
https://github.com/crunchy-labs/crunchy-cli.git
synced 2026-01-21 12:12:00 -06:00
Move and refactor files and some more changes :3
This commit is contained in:
parent
781e520591
commit
303689ecbb
29 changed files with 1305 additions and 1160 deletions
94
utils/extract.go
Normal file
94
utils/extract.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ByteDream/crunchyroll-go/v3"
|
||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var urlFilter = regexp.MustCompile(`(S(\d+))?(E(\d+))?((-)(S(\d+))?(E(\d+))?)?(,|$)`)
|
||||
|
||||
func ExtractEpisodes(url string, locales ...crunchyroll.LOCALE) ([][]*crunchyroll.Episode, error) {
|
||||
var matches [][]string
|
||||
|
||||
lastOpen := strings.LastIndex(url, "[")
|
||||
if strings.HasSuffix(url, "]") && lastOpen != -1 && lastOpen < len(url) {
|
||||
matches = urlFilter.FindAllStringSubmatch(url[lastOpen+1:len(url)-1], -1)
|
||||
|
||||
var all string
|
||||
for _, match := range matches {
|
||||
all += match[0]
|
||||
}
|
||||
if all != url[lastOpen+1:len(url)-1] {
|
||||
return nil, fmt.Errorf("invalid episode filter")
|
||||
}
|
||||
url = url[:lastOpen]
|
||||
}
|
||||
|
||||
final := make([][]*crunchyroll.Episode, len(locales))
|
||||
episodes, err := Crunchy.ExtractEpisodesFromUrl(url, locales...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get episodes: %v", err)
|
||||
}
|
||||
|
||||
if len(episodes) == 0 {
|
||||
return nil, fmt.Errorf("no episodes found")
|
||||
}
|
||||
|
||||
if matches != nil {
|
||||
for _, match := range matches {
|
||||
fromSeason, fromEpisode, toSeason, toEpisode := -1, -1, -1, -1
|
||||
if match[2] != "" {
|
||||
fromSeason, _ = strconv.Atoi(match[2])
|
||||
}
|
||||
if match[4] != "" {
|
||||
fromEpisode, _ = strconv.Atoi(match[4])
|
||||
}
|
||||
if match[8] != "" {
|
||||
toSeason, _ = strconv.Atoi(match[8])
|
||||
}
|
||||
if match[10] != "" {
|
||||
toEpisode, _ = strconv.Atoi(match[10])
|
||||
}
|
||||
|
||||
if match[6] != "-" {
|
||||
toSeason = fromSeason
|
||||
toEpisode = fromEpisode
|
||||
}
|
||||
|
||||
tmpEps := make([]*crunchyroll.Episode, 0)
|
||||
for _, episode := range episodes {
|
||||
if fromSeason != -1 && (episode.SeasonNumber < fromSeason || (fromEpisode != -1 && episode.EpisodeNumber < fromEpisode)) {
|
||||
continue
|
||||
} else if fromSeason == -1 && fromEpisode != -1 && episode.EpisodeNumber < fromEpisode {
|
||||
continue
|
||||
} else if toSeason != -1 && (episode.SeasonNumber > toSeason || (toEpisode != -1 && episode.EpisodeNumber > toEpisode)) {
|
||||
continue
|
||||
} else if toSeason == -1 && toEpisode != -1 && episode.EpisodeNumber > toEpisode {
|
||||
continue
|
||||
} else {
|
||||
tmpEps = append(tmpEps, episode)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tmpEps) == 0 {
|
||||
return nil, fmt.Errorf("no episodes are matching the given filter")
|
||||
}
|
||||
|
||||
episodes = tmpEps
|
||||
}
|
||||
}
|
||||
|
||||
localeSorted, err := utils.SortEpisodesByAudio(episodes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audio locale: %v", err)
|
||||
}
|
||||
for i, locale := range locales {
|
||||
final[i] = append(final[i], localeSorted[locale]...)
|
||||
}
|
||||
|
||||
return final, nil
|
||||
}
|
||||
49
utils/file.go
Normal file
49
utils/file.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FreeFileName(filename string) (string, bool) {
|
||||
ext := filepath.Ext(filename)
|
||||
base := strings.TrimSuffix(filename, ext)
|
||||
// checks if a .tar stands before the "actual" file ending
|
||||
if extraExt := filepath.Ext(base); extraExt == ".tar" {
|
||||
ext = extraExt + ext
|
||||
base = strings.TrimSuffix(base, extraExt)
|
||||
}
|
||||
j := 0
|
||||
for ; ; j++ {
|
||||
if _, stat := os.Stat(filename); stat != nil && !os.IsExist(stat) {
|
||||
break
|
||||
}
|
||||
filename = fmt.Sprintf("%s (%d)%s", base, j+1, ext)
|
||||
}
|
||||
return filename, j != 0
|
||||
}
|
||||
|
||||
func GenerateFilename(name, directory string) string {
|
||||
if runtime.GOOS != "windows" {
|
||||
for _, char := range []string{"/"} {
|
||||
name = strings.ReplaceAll(name, char, "")
|
||||
}
|
||||
Log.Debug("Replaced invalid characters (not windows)")
|
||||
} else {
|
||||
// ahh i love windows :)))
|
||||
for _, char := range []string{"\\", "<", ">", ":", "\"", "/", "|", "?", "*"} {
|
||||
name = strings.ReplaceAll(name, char, "")
|
||||
}
|
||||
Log.Debug("Replaced invalid characters (windows)")
|
||||
}
|
||||
|
||||
filename, changed := FreeFileName(filepath.Join(directory, name))
|
||||
if changed {
|
||||
Log.Debug("File `%s` already exists, changing name to `%s`", filepath.Base(name), filepath.Base(filename))
|
||||
}
|
||||
|
||||
return filename
|
||||
}
|
||||
63
utils/format.go
Normal file
63
utils/format.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ByteDream/crunchyroll-go/v3"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FormatInformation struct {
|
||||
// the Format to download
|
||||
Format *crunchyroll.Format
|
||||
|
||||
// additional formats which are only used by archive.go
|
||||
AdditionalFormats []*crunchyroll.Format
|
||||
|
||||
Title string `json:"title"`
|
||||
SeriesName string `json:"series_name"`
|
||||
SeasonName string `json:"season_name"`
|
||||
SeasonNumber int `json:"season_number"`
|
||||
EpisodeNumber int `json:"episode_number"`
|
||||
Resolution string `json:"resolution"`
|
||||
FPS float64 `json:"fps"`
|
||||
Audio crunchyroll.LOCALE `json:"audio"`
|
||||
Subtitle crunchyroll.LOCALE `json:"subtitle"`
|
||||
}
|
||||
|
||||
func (fi FormatInformation) FormatString(source string) string {
|
||||
fields := reflect.TypeOf(fi)
|
||||
values := reflect.ValueOf(fi)
|
||||
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
var valueAsString string
|
||||
switch value := values.Field(i); value.Kind() {
|
||||
case reflect.String:
|
||||
valueAsString = value.String()
|
||||
case reflect.Int:
|
||||
valueAsString = fmt.Sprintf("%02d", value.Int())
|
||||
case reflect.Float64:
|
||||
valueAsString = fmt.Sprintf("%.2f", value.Float())
|
||||
case reflect.Bool:
|
||||
valueAsString = fields.Field(i).Tag.Get("json")
|
||||
if !value.Bool() {
|
||||
valueAsString = "no " + valueAsString
|
||||
}
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
for _, char := range []string{"/"} {
|
||||
valueAsString = strings.ReplaceAll(valueAsString, char, "")
|
||||
}
|
||||
} else {
|
||||
for _, char := range []string{"\\", "<", ">", ":", "\"", "/", "|", "?", "*"} {
|
||||
valueAsString = strings.ReplaceAll(valueAsString, char, "")
|
||||
}
|
||||
}
|
||||
|
||||
source = strings.ReplaceAll(source, "{"+fields.Field(i).Tag.Get("json")+"}", valueAsString)
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
51
utils/http.go
Normal file
51
utils/http.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type headerRoundTripper struct {
|
||||
http.RoundTripper
|
||||
header map[string]string
|
||||
}
|
||||
|
||||
func (rht headerRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
resp, err := rht.RoundTripper.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, v := range rht.header {
|
||||
resp.Header.Set(k, v)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func CreateOrDefaultClient(proxy, useragent string) (*http.Client, error) {
|
||||
if proxy == "" {
|
||||
return http.DefaultClient, nil
|
||||
} else {
|
||||
proxyURL, err := url.Parse(proxy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rt http.RoundTripper = &http.Transport{
|
||||
DisableCompression: true,
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
}
|
||||
if useragent != "" {
|
||||
rt = headerRoundTripper{
|
||||
RoundTripper: rt,
|
||||
header: map[string]string{"User-Agent": useragent},
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: rt,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
59
utils/locale.go
Normal file
59
utils/locale.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ByteDream/crunchyroll-go/v3"
|
||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SystemLocale receives the system locale
|
||||
// https://stackoverflow.com/questions/51829386/golang-get-system-language/51831590#51831590
|
||||
func SystemLocale(verbose bool) crunchyroll.LOCALE {
|
||||
if runtime.GOOS != "windows" {
|
||||
if lang, ok := os.LookupEnv("LANG"); ok {
|
||||
var l crunchyroll.LOCALE
|
||||
if preSuffix := strings.Split(strings.Split(lang, ".")[0], "_"); len(preSuffix) == 1 {
|
||||
l = crunchyroll.LOCALE(preSuffix[0])
|
||||
} else {
|
||||
prefix := strings.Split(lang, "_")[0]
|
||||
l = crunchyroll.LOCALE(fmt.Sprintf("%s-%s", prefix, preSuffix[1]))
|
||||
}
|
||||
if !utils.ValidateLocale(l) {
|
||||
if verbose {
|
||||
Log.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
|
||||
}
|
||||
l = crunchyroll.US
|
||||
}
|
||||
return l
|
||||
}
|
||||
} else {
|
||||
cmd := exec.Command("powershell", "Get-Culture | select -exp Name")
|
||||
if output, err := cmd.Output(); err == nil {
|
||||
l := crunchyroll.LOCALE(strings.Trim(string(output), "\r\n"))
|
||||
if !utils.ValidateLocale(l) {
|
||||
if verbose {
|
||||
Log.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
|
||||
}
|
||||
l = crunchyroll.US
|
||||
}
|
||||
return l
|
||||
}
|
||||
}
|
||||
if verbose {
|
||||
Log.Err("Failed to get locale, using %s", crunchyroll.US)
|
||||
}
|
||||
return crunchyroll.US
|
||||
}
|
||||
|
||||
func LocalesAsStrings() (locales []string) {
|
||||
for _, locale := range utils.AllLocales {
|
||||
locales = append(locales, string(locale))
|
||||
}
|
||||
sort.Strings(locales)
|
||||
return
|
||||
}
|
||||
12
utils/logger.go
Normal file
12
utils/logger.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package utils
|
||||
|
||||
type Logger interface {
|
||||
IsDev() bool
|
||||
Debug(format string, v ...any)
|
||||
Info(format string, v ...any)
|
||||
Warn(format string, v ...any)
|
||||
Err(format string, v ...any)
|
||||
Empty()
|
||||
SetProcess(format string, v ...any)
|
||||
StopProcess(format string, v ...any)
|
||||
}
|
||||
177
utils/save.go
Normal file
177
utils/save.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/ByteDream/crunchyroll-go/v3"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SaveSession(crunchy *crunchyroll.Crunchyroll) error {
|
||||
file := filepath.Join(os.TempDir(), ".crunchy")
|
||||
return os.WriteFile(file, []byte(crunchy.EtpRt), 0600)
|
||||
}
|
||||
|
||||
func SaveCredentialsPersistent(user, password string, encryptionKey []byte) error {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file := filepath.Join(configDir, "crunchy-cli", "crunchy")
|
||||
|
||||
var credentials []byte
|
||||
if encryptionKey != nil {
|
||||
hashedEncryptionKey := sha256.Sum256(encryptionKey)
|
||||
block, err := aes.NewCipher(hashedEncryptionKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
b := gcm.Seal(nonce, nonce, []byte(fmt.Sprintf("%s\n%s", user, password)), nil)
|
||||
credentials = append([]byte("aes:"), b...)
|
||||
} else {
|
||||
credentials = []byte(fmt.Sprintf("%s\n%s", user, password))
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Join(configDir, "crunchy-cli"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(file, credentials, 0600)
|
||||
}
|
||||
|
||||
func SaveSessionPersistent(crunchy *crunchyroll.Crunchyroll) error {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
file := filepath.Join(configDir, "crunchy-cli", "crunchy")
|
||||
|
||||
if err = os.MkdirAll(filepath.Join(configDir, "crunchy-cli"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(file, []byte(crunchy.EtpRt), 0600)
|
||||
}
|
||||
|
||||
func IsTempSession() bool {
|
||||
file := filepath.Join(os.TempDir(), ".crunchy")
|
||||
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func IsSavedSessionEncrypted() (bool, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
file := filepath.Join(configDir, "crunchy-cli", "crunchy")
|
||||
body, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strings.HasPrefix(string(body), "aes:"), nil
|
||||
}
|
||||
|
||||
func LoadSession(encryptionKey []byte) (*crunchyroll.Crunchyroll, error) {
|
||||
file := filepath.Join(os.TempDir(), ".crunchy")
|
||||
crunchy, err := loadTempSession(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if crunchy != nil {
|
||||
return crunchy, nil
|
||||
}
|
||||
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file = filepath.Join(configDir, "crunchy-cli", "crunchy")
|
||||
crunchy, err = loadPersistentSession(file, encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if crunchy != nil {
|
||||
return crunchy, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("not logged in")
|
||||
}
|
||||
|
||||
func loadTempSession(file string) (*crunchyroll.Crunchyroll, error) {
|
||||
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
body, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
crunchy, err := crunchyroll.LoginWithEtpRt(string(body), SystemLocale(true), Client)
|
||||
if err != nil {
|
||||
Log.Debug("Failed to login with temp etp rt cookie: %v", err)
|
||||
} else {
|
||||
Log.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
|
||||
return crunchy, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func loadPersistentSession(file string, encryptionKey []byte) (crunchy *crunchyroll.Crunchyroll, err error) {
|
||||
if _, err = os.Stat(file); !os.IsNotExist(err) {
|
||||
body, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
split := strings.SplitN(string(body), "\n", 2)
|
||||
if len(split) == 1 || split[1] == "" && strings.HasPrefix(split[0], "aes:") {
|
||||
encrypted := body[4:]
|
||||
hashedEncryptionKey := sha256.Sum256(encryptionKey)
|
||||
block, err := aes.NewCipher(hashedEncryptionKey[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce, cypherText := encrypted[:gcm.NonceSize()], encrypted[gcm.NonceSize():]
|
||||
b, err := gcm.Open(nil, nonce, cypherText, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
Log.Debug("Logged in with credentials")
|
||||
} else {
|
||||
if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], SystemLocale(true), Client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
Log.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
|
||||
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.EtpRt), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
7
utils/system.go
Normal file
7
utils/system.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package utils
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func HasFFmpeg() bool {
|
||||
return exec.Command("ffmpeg", "-h").Run() == nil
|
||||
}
|
||||
14
utils/vars.go
Normal file
14
utils/vars.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/ByteDream/crunchyroll-go/v3"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var Version = "development"
|
||||
|
||||
var (
|
||||
Crunchy *crunchyroll.Crunchyroll
|
||||
Client *http.Client
|
||||
Log Logger
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue