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
8
Makefile
8
Makefile
|
|
@ -6,7 +6,7 @@ DESTDIR=
|
||||||
PREFIX=/usr
|
PREFIX=/usr
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(BINARY_NAME) .
|
go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/utils.Version=$(VERSION)'" -o $(BINARY_NAME) .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
|
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
|
||||||
|
|
@ -24,8 +24,8 @@ uninstall:
|
||||||
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
|
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
|
||||||
|
|
||||||
release:
|
release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux .
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux .
|
||||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe .
|
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe .
|
||||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
|
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
|
||||||
|
|
||||||
strip $(VERSION_BINARY_NAME)_linux
|
strip $(VERSION_BINARY_NAME)_linux
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
package commands
|
package archive
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"archive/zip"
|
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
"github.com/ByteDream/crunchyroll-go/v3"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -23,8 +22,6 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -42,39 +39,39 @@ var (
|
||||||
archiveGoroutinesFlag int
|
archiveGoroutinesFlag int
|
||||||
)
|
)
|
||||||
|
|
||||||
var archiveCmd = &cobra.Command{
|
var Cmd = &cobra.Command{
|
||||||
Use: "archive",
|
Use: "archive",
|
||||||
Short: "Stores the given videos with all subtitles and multiple audios in a .mkv file",
|
Short: "Stores the given videos with all subtitles and multiple audios in a .mkv file",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
out.Debug("Validating arguments")
|
utils.Log.Debug("Validating arguments")
|
||||||
|
|
||||||
if !hasFFmpeg() {
|
if !utils.HasFFmpeg() {
|
||||||
return fmt.Errorf("ffmpeg is needed to run this command correctly")
|
return fmt.Errorf("ffmpeg is needed to run this command correctly")
|
||||||
}
|
}
|
||||||
out.Debug("FFmpeg detected")
|
utils.Log.Debug("FFmpeg detected")
|
||||||
|
|
||||||
if filepath.Ext(archiveOutputFlag) != ".mkv" {
|
if filepath.Ext(archiveOutputFlag) != ".mkv" {
|
||||||
return fmt.Errorf("currently only matroska / .mkv files are supported")
|
return fmt.Errorf("currently only matroska / .mkv files are supported")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, locale := range archiveLanguagesFlag {
|
for _, locale := range archiveLanguagesFlag {
|
||||||
if !utils.ValidateLocale(crunchyroll.LOCALE(locale)) {
|
if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(locale)) {
|
||||||
// if locale is 'all', match all known locales
|
// if locale is 'all', match all known locales
|
||||||
if locale == "all" {
|
if locale == "all" {
|
||||||
archiveLanguagesFlag = allLocalesAsStrings()
|
archiveLanguagesFlag = utils.LocalesAsStrings()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return fmt.Errorf("%s is not a valid locale. Choose from: %s", locale, strings.Join(allLocalesAsStrings(), ", "))
|
return fmt.Errorf("%s is not a valid locale. Choose from: %s", locale, strings.Join(utils.LocalesAsStrings(), ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.Debug("Using following audio locales: %s", strings.Join(archiveLanguagesFlag, ", "))
|
utils.Log.Debug("Using following audio locales: %s", strings.Join(archiveLanguagesFlag, ", "))
|
||||||
|
|
||||||
var found bool
|
var found bool
|
||||||
for _, mode := range []string{"auto", "audio", "video"} {
|
for _, mode := range []string{"auto", "audio", "video"} {
|
||||||
if archiveMergeFlag == mode {
|
if archiveMergeFlag == mode {
|
||||||
out.Debug("Using %s merge behavior", archiveMergeFlag)
|
utils.Log.Debug("Using %s merge behavior", archiveMergeFlag)
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +84,7 @@ var archiveCmd = &cobra.Command{
|
||||||
found = false
|
found = false
|
||||||
for _, algo := range []string{".tar", ".tar.gz", ".tgz", ".zip"} {
|
for _, algo := range []string{".tar", ".tar.gz", ".tgz", ".zip"} {
|
||||||
if strings.HasSuffix(archiveCompressFlag, algo) {
|
if strings.HasSuffix(archiveCompressFlag, algo) {
|
||||||
out.Debug("Using %s compression", algo)
|
utils.Log.Debug("Using %s compression", algo)
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -100,8 +97,8 @@ var archiveCmd = &cobra.Command{
|
||||||
|
|
||||||
switch archiveResolutionFlag {
|
switch archiveResolutionFlag {
|
||||||
case "1080p", "720p", "480p", "360p":
|
case "1080p", "720p", "480p", "360p":
|
||||||
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
|
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
|
||||||
archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
|
archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(archiveResolutionFlag, "p"))
|
||||||
case "240p":
|
case "240p":
|
||||||
// 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
|
// 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
|
||||||
archiveResolutionFlag = "428x240"
|
archiveResolutionFlag = "428x240"
|
||||||
|
|
@ -109,31 +106,33 @@ var archiveCmd = &cobra.Command{
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)
|
return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)
|
||||||
}
|
}
|
||||||
out.Debug("Using resolution '%s'", archiveResolutionFlag)
|
utils.Log.Debug("Using resolution '%s'", archiveResolutionFlag)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
loadCrunchy()
|
if err := commands.LoadCrunchy(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return archive(args)
|
return archive(args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
archiveCmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
|
Cmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
|
||||||
"language",
|
"language",
|
||||||
"l",
|
"l",
|
||||||
[]string{string(systemLocale(false)), string(crunchyroll.JP)},
|
[]string{string(utils.SystemLocale(false)), string(crunchyroll.JP)},
|
||||||
"Audio locale which should be downloaded. Can be used multiple times")
|
"Audio locale which should be downloaded. Can be used multiple times")
|
||||||
|
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
archiveCmd.Flags().StringVarP(&archiveDirectoryFlag,
|
Cmd.Flags().StringVarP(&archiveDirectoryFlag,
|
||||||
"directory",
|
"directory",
|
||||||
"d",
|
"d",
|
||||||
cwd,
|
cwd,
|
||||||
"The directory to store the files into")
|
"The directory to store the files into")
|
||||||
archiveCmd.Flags().StringVarP(&archiveOutputFlag,
|
Cmd.Flags().StringVarP(&archiveOutputFlag,
|
||||||
"output",
|
"output",
|
||||||
"o",
|
"o",
|
||||||
"{title}.mkv",
|
"{title}.mkv",
|
||||||
|
|
@ -148,13 +147,13 @@ func init() {
|
||||||
"\t{audio} » Audio locale of the video\n"+
|
"\t{audio} » Audio locale of the video\n"+
|
||||||
"\t{subtitle} » Subtitle locale of the video")
|
"\t{subtitle} » Subtitle locale of the video")
|
||||||
|
|
||||||
archiveCmd.Flags().StringVarP(&archiveMergeFlag,
|
Cmd.Flags().StringVarP(&archiveMergeFlag,
|
||||||
"merge",
|
"merge",
|
||||||
"m",
|
"m",
|
||||||
"auto",
|
"auto",
|
||||||
"Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'")
|
"Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'")
|
||||||
|
|
||||||
archiveCmd.Flags().StringVarP(&archiveCompressFlag,
|
Cmd.Flags().StringVarP(&archiveCompressFlag,
|
||||||
"compress",
|
"compress",
|
||||||
"c",
|
"c",
|
||||||
"",
|
"",
|
||||||
|
|
@ -162,7 +161,7 @@ func init() {
|
||||||
"This flag sets the name of the compressed output file. The file ending specifies the compression algorithm. "+
|
"This flag sets the name of the compressed output file. The file ending specifies the compression algorithm. "+
|
||||||
"The following algorithms are supported: gzip, tar, zip")
|
"The following algorithms are supported: gzip, tar, zip")
|
||||||
|
|
||||||
archiveCmd.Flags().StringVarP(&archiveResolutionFlag,
|
Cmd.Flags().StringVarP(&archiveResolutionFlag,
|
||||||
"resolution",
|
"resolution",
|
||||||
"r",
|
"r",
|
||||||
"best",
|
"best",
|
||||||
|
|
@ -171,51 +170,49 @@ func init() {
|
||||||
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
|
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
|
||||||
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
|
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
|
||||||
|
|
||||||
archiveCmd.Flags().IntVarP(&archiveGoroutinesFlag,
|
Cmd.Flags().IntVarP(&archiveGoroutinesFlag,
|
||||||
"goroutines",
|
"goroutines",
|
||||||
"g",
|
"g",
|
||||||
runtime.NumCPU(),
|
runtime.NumCPU(),
|
||||||
"Number of parallel segment downloads")
|
"Number of parallel segment downloads")
|
||||||
|
|
||||||
rootCmd.AddCommand(archiveCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func archive(urls []string) error {
|
func archive(urls []string) error {
|
||||||
for i, url := range urls {
|
for i, url := range urls {
|
||||||
out.SetProgress("Parsing url %d", i+1)
|
utils.Log.SetProcess("Parsing url %d", i+1)
|
||||||
episodes, err := archiveExtractEpisodes(url)
|
episodes, err := archiveExtractEpisodes(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.StopProgress("Failed to parse url %d", i+1)
|
utils.Log.StopProcess("Failed to parse url %d", i+1)
|
||||||
if crunchy.Config.Premium {
|
if utils.Crunchy.Config.Premium {
|
||||||
out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
|
utils.Log.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/crunchy-cli/issues/22 for more information")
|
"try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchy-cli/issues/22 for more information")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out.StopProgress("Parsed url %d", i+1)
|
utils.Log.StopProcess("Parsed url %d", i+1)
|
||||||
|
|
||||||
var compressFile *os.File
|
var compressFile *os.File
|
||||||
var c compress
|
var c Compress
|
||||||
|
|
||||||
if archiveCompressFlag != "" {
|
if archiveCompressFlag != "" {
|
||||||
compressFile, err = os.Create(generateFilename(archiveCompressFlag, ""))
|
compressFile, err = os.Create(utils.GenerateFilename(archiveCompressFlag, ""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create archive file: %v", err)
|
return fmt.Errorf("failed to create archive file: %v", err)
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(archiveCompressFlag, ".tar") {
|
if strings.HasSuffix(archiveCompressFlag, ".tar") {
|
||||||
c = newTarCompress(compressFile)
|
c = NewTarCompress(compressFile)
|
||||||
} else if strings.HasSuffix(archiveCompressFlag, ".tar.gz") || strings.HasSuffix(archiveCompressFlag, ".tgz") {
|
} else if strings.HasSuffix(archiveCompressFlag, ".tar.gz") || strings.HasSuffix(archiveCompressFlag, ".tgz") {
|
||||||
c = newGzipCompress(compressFile)
|
c = NewGzipCompress(compressFile)
|
||||||
} else if strings.HasSuffix(archiveCompressFlag, ".zip") {
|
} else if strings.HasSuffix(archiveCompressFlag, ".zip") {
|
||||||
c = newZipCompress(compressFile)
|
c = NewZipCompress(compressFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, season := range episodes {
|
for _, season := range episodes {
|
||||||
out.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
|
utils.Log.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
|
||||||
|
|
||||||
for j, info := range season {
|
for j, info := range season {
|
||||||
out.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
|
utils.Log.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
|
||||||
j+1,
|
j+1,
|
||||||
info.Title,
|
info.Title,
|
||||||
info.Resolution,
|
info.Resolution,
|
||||||
|
|
@ -224,26 +221,26 @@ func archive(urls []string) error {
|
||||||
info.EpisodeNumber)
|
info.EpisodeNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
|
|
||||||
for j, season := range episodes {
|
for j, season := range episodes {
|
||||||
for k, info := range season {
|
for k, info := range season {
|
||||||
var filename string
|
var filename string
|
||||||
var writeCloser io.WriteCloser
|
var writeCloser io.WriteCloser
|
||||||
if c != nil {
|
if c != nil {
|
||||||
filename = info.Format(archiveOutputFlag)
|
filename = info.FormatString(archiveOutputFlag)
|
||||||
writeCloser, err = c.NewFile(info)
|
writeCloser, err = c.NewFile(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to pre generate new archive file: %v", err)
|
return fmt.Errorf("failed to pre generate new archive file: %v", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dir := info.Format(archiveDirectoryFlag)
|
dir := info.FormatString(archiveDirectoryFlag)
|
||||||
if _, err = os.Stat(dir); os.IsNotExist(err) {
|
if _, err = os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, 0777); err != nil {
|
if err = os.MkdirAll(dir, 0777); err != nil {
|
||||||
return fmt.Errorf("error while creating directory: %v", err)
|
return fmt.Errorf("error while creating directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
filename = generateFilename(info.Format(archiveOutputFlag), dir)
|
filename = utils.GenerateFilename(info.FormatString(archiveOutputFlag), dir)
|
||||||
writeCloser, err = os.Create(filename)
|
writeCloser, err = os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create new file: %v", err)
|
return fmt.Errorf("failed to create new file: %v", err)
|
||||||
|
|
@ -264,7 +261,7 @@ func archive(urls []string) error {
|
||||||
writeCloser.Close()
|
writeCloser.Close()
|
||||||
|
|
||||||
if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
|
if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -278,8 +275,8 @@ func archive(urls []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename string) error {
|
func archiveInfo(info utils.FormatInformation, writeCloser io.WriteCloser, filename string) error {
|
||||||
out.Debug("Entering season %d, episode %d with %d additional formats", info.SeasonNumber, info.EpisodeNumber, len(info.additionalFormats))
|
utils.Log.Debug("Entering season %d, episode %d with %d additional formats", info.SeasonNumber, info.EpisodeNumber, len(info.AdditionalFormats))
|
||||||
|
|
||||||
dp, err := createArchiveProgress(info)
|
dp, err := createArchiveProgress(info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -307,7 +304,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.IsDev() {
|
if utils.Log.IsDev() {
|
||||||
dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
|
dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
|
||||||
} else {
|
} else {
|
||||||
dp.Update()
|
dp.Update()
|
||||||
|
|
@ -325,8 +322,8 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
select {
|
select {
|
||||||
case <-sig:
|
case <-sig:
|
||||||
signal.Stop(sig)
|
signal.Stop(sig)
|
||||||
out.Exit("Exiting... (may take a few seconds)")
|
utils.Log.Err("Exiting... (may take a few seconds)")
|
||||||
out.Exit("To force exit press ctrl+c (again)")
|
utils.Log.Err("To force exit press ctrl+c (again)")
|
||||||
cancel()
|
cancel()
|
||||||
// os.Exit(1) is not called since an immediate exit after the cancel function does not let
|
// os.Exit(1) is not called since an immediate exit after the cancel function does not let
|
||||||
// the download process enough time to stop gratefully. A result of this is that the temporary
|
// the download process enough time to stop gratefully. A result of this is that the temporary
|
||||||
|
|
@ -335,15 +332,15 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
// this is just here to end the goroutine and prevent it from running forever without a reason
|
// this is just here to end the goroutine and prevent it from running forever without a reason
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
out.Debug("Set up signal catcher")
|
utils.Log.Debug("Set up signal catcher")
|
||||||
|
|
||||||
var additionalDownloaderOpts []string
|
var additionalDownloaderOpts []string
|
||||||
var mergeMessage string
|
var mergeMessage string
|
||||||
switch archiveMergeFlag {
|
switch archiveMergeFlag {
|
||||||
case "auto":
|
case "auto":
|
||||||
additionalDownloaderOpts = []string{"-vn"}
|
additionalDownloaderOpts = []string{"-vn"}
|
||||||
for _, format := range info.additionalFormats {
|
for _, format := range info.AdditionalFormats {
|
||||||
if format.Video.Bandwidth != info.format.Video.Bandwidth {
|
if format.Video.Bandwidth != info.Format.Video.Bandwidth {
|
||||||
// revoke the changed FFmpegOpts above
|
// revoke the changed FFmpegOpts above
|
||||||
additionalDownloaderOpts = []string{}
|
additionalDownloaderOpts = []string{}
|
||||||
break
|
break
|
||||||
|
|
@ -361,12 +358,12 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
mergeMessage = "merging video for additional formats"
|
mergeMessage = "merging video for additional formats"
|
||||||
}
|
}
|
||||||
|
|
||||||
out.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
|
utils.Log.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
|
||||||
out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
|
utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
|
||||||
out.Info("\tAudio: %s", info.Audio)
|
utils.Log.Info("\tAudio: %s", info.Audio)
|
||||||
out.Info("\tSubtitle: %s", info.Subtitle)
|
utils.Log.Info("\tSubtitle: %s", info.Subtitle)
|
||||||
out.Info("\tResolution: %spx", info.Resolution)
|
utils.Log.Info("\tResolution: %spx", info.Resolution)
|
||||||
out.Info("\tFPS: %.2f", info.FPS)
|
utils.Log.Info("\tFPS: %.2f", info.FPS)
|
||||||
|
|
||||||
var videoFiles, audioFiles, subtitleFiles []string
|
var videoFiles, audioFiles, subtitleFiles []string
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
@ -376,7 +373,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var f []string
|
var f []string
|
||||||
if f, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.format); err != nil {
|
if f, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.Format); err != nil {
|
||||||
if err != ctx.Err() {
|
if err != ctx.Err() {
|
||||||
return fmt.Errorf("error while downloading: %v", err)
|
return fmt.Errorf("error while downloading: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -387,29 +384,29 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
if len(additionalDownloaderOpts) == 0 {
|
if len(additionalDownloaderOpts) == 0 {
|
||||||
var videos []string
|
var videos []string
|
||||||
downloader.FFmpegOpts = additionalDownloaderOpts
|
downloader.FFmpegOpts = additionalDownloaderOpts
|
||||||
if videos, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.additionalFormats...); err != nil {
|
if videos, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.AdditionalFormats...); err != nil {
|
||||||
return fmt.Errorf("error while downloading additional videos: %v", err)
|
return fmt.Errorf("error while downloading additional videos: %v", err)
|
||||||
}
|
}
|
||||||
downloader.FFmpegOpts = []string{}
|
downloader.FFmpegOpts = []string{}
|
||||||
videoFiles = append(videoFiles, videos...)
|
videoFiles = append(videoFiles, videos...)
|
||||||
} else {
|
} else {
|
||||||
var audios []string
|
var audios []string
|
||||||
if audios, err = archiveDownloadVideos(downloader, filepath.Base(filename), false, info.additionalFormats...); err != nil {
|
if audios, err = archiveDownloadVideos(downloader, filepath.Base(filename), false, info.AdditionalFormats...); err != nil {
|
||||||
return fmt.Errorf("error while downloading additional videos: %v", err)
|
return fmt.Errorf("error while downloading additional videos: %v", err)
|
||||||
}
|
}
|
||||||
audioFiles = append(audioFiles, audios...)
|
audioFiles = append(audioFiles, audios...)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(utils.SubtitlesByLocale(info.format.Subtitles))
|
sort.Sort(crunchyUtils.SubtitlesByLocale(info.Format.Subtitles))
|
||||||
|
|
||||||
sortSubtitles, _ := strconv.ParseBool(os.Getenv("SORT_SUBTITLES"))
|
sortSubtitles, _ := strconv.ParseBool(os.Getenv("SORT_SUBTITLES"))
|
||||||
if sortSubtitles && len(archiveLanguagesFlag) > 0 {
|
if sortSubtitles && len(archiveLanguagesFlag) > 0 {
|
||||||
// this sort the subtitle locales after the languages which were specified
|
// this sort the subtitle locales after the languages which were specified
|
||||||
// with the `archiveLanguagesFlag` flag
|
// with the `archiveLanguagesFlag` flag
|
||||||
for _, language := range archiveLanguagesFlag {
|
for _, language := range archiveLanguagesFlag {
|
||||||
for i, subtitle := range info.format.Subtitles {
|
for i, subtitle := range info.Format.Subtitles {
|
||||||
if subtitle.Locale == crunchyroll.LOCALE(language) {
|
if subtitle.Locale == crunchyroll.LOCALE(language) {
|
||||||
info.format.Subtitles = append([]*crunchyroll.Subtitle{subtitle}, append(info.format.Subtitles[:i], info.format.Subtitles[i+1:]...)...)
|
info.Format.Subtitles = append([]*crunchyroll.Subtitle{subtitle}, append(info.Format.Subtitles[:i], info.Format.Subtitles[i+1:]...)...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +414,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
}
|
}
|
||||||
|
|
||||||
var subtitles []string
|
var subtitles []string
|
||||||
if subtitles, err = archiveDownloadSubtitles(filepath.Base(filename), info.format.Subtitles...); err != nil {
|
if subtitles, err = archiveDownloadSubtitles(filepath.Base(filename), info.Format.Subtitles...); err != nil {
|
||||||
return fmt.Errorf("error while downloading subtitles: %v", err)
|
return fmt.Errorf("error while downloading subtitles: %v", err)
|
||||||
}
|
}
|
||||||
subtitleFiles = append(subtitleFiles, subtitles...)
|
subtitleFiles = append(subtitleFiles, subtitles...)
|
||||||
|
|
@ -429,22 +426,22 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
|
||||||
dp.UpdateMessage("Download finished", false)
|
dp.UpdateMessage("Download finished", false)
|
||||||
|
|
||||||
signal.Stop(sig)
|
signal.Stop(sig)
|
||||||
out.Debug("Stopped signal catcher")
|
utils.Log.Debug("Stopped signal catcher")
|
||||||
|
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
|
func createArchiveProgress(info utils.FormatInformation) (*commands.DownloadProgress, error) {
|
||||||
var progressCount int
|
var progressCount int
|
||||||
if err := info.format.InitVideo(); err != nil {
|
if err := info.Format.InitVideo(); err != nil {
|
||||||
return nil, fmt.Errorf("error while initializing a video: %v", err)
|
return nil, fmt.Errorf("error while initializing a video: %v", err)
|
||||||
}
|
}
|
||||||
// + number of segments a video has +1 is for merging
|
// + number of segments a video has +1 is for merging
|
||||||
progressCount += int(info.format.Video.Chunklist.Count()) + 1
|
progressCount += int(info.Format.Video.Chunklist.Count()) + 1
|
||||||
for _, f := range info.additionalFormats {
|
for _, f := range info.AdditionalFormats {
|
||||||
if f == info.format {
|
if f == info.Format {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,16 +452,16 @@ func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
|
||||||
progressCount += int(f.Video.Chunklist.Count()) + 1
|
progressCount += int(f.Video.Chunklist.Count()) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
dp := &downloadProgress{
|
dp := &commands.DownloadProgress{
|
||||||
Prefix: out.InfoLog.Prefix(),
|
Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
|
||||||
Message: "Downloading video",
|
Message: "Downloading video",
|
||||||
// number of segments a video +1 is for the success message
|
// number of segments a video +1 is for the success message
|
||||||
Total: progressCount + 1,
|
Total: progressCount + 1,
|
||||||
Dev: out.IsDev(),
|
Dev: utils.Log.IsDev(),
|
||||||
Quiet: out.IsQuiet(),
|
Quiet: utils.Log.(*commands.Logger).IsQuiet(),
|
||||||
}
|
}
|
||||||
if out.IsDev() {
|
if utils.Log.IsDev() {
|
||||||
dp.Prefix = out.DebugLog.Prefix()
|
dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
|
||||||
}
|
}
|
||||||
|
|
||||||
return dp, nil
|
return dp, nil
|
||||||
|
|
@ -497,7 +494,7 @@ func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, v
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
out.Debug("Downloaded '%s' video", format.AudioLocale)
|
utils.Log.Debug("Downloaded '%s' video", format.AudioLocale)
|
||||||
}
|
}
|
||||||
|
|
||||||
return files, nil
|
return files, nil
|
||||||
|
|
@ -522,7 +519,7 @@ func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitl
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
out.Debug("Downloaded '%s' subtitles", subtitle.Locale)
|
utils.Log.Debug("Downloaded '%s' subtitles", subtitle.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
return files, nil
|
return files, nil
|
||||||
|
|
@ -537,9 +534,9 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
maps = append(maps, "-map", strconv.Itoa(i))
|
maps = append(maps, "-map", strconv.Itoa(i))
|
||||||
locale := crunchyroll.LOCALE(re.FindStringSubmatch(video)[1])
|
locale := crunchyroll.LOCALE(re.FindStringSubmatch(video)[1])
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("language=%s", locale))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("language=%s", locale))
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("title=%s", utils.LocaleLanguage(locale)))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", locale))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", locale))
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("title=%s", utils.LocaleLanguage(locale)))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, audio := range audioFiles {
|
for i, audio := range audioFiles {
|
||||||
|
|
@ -547,7 +544,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
|
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
|
||||||
locale := crunchyroll.LOCALE(re.FindStringSubmatch(audio)[1])
|
locale := crunchyroll.LOCALE(re.FindStringSubmatch(audio)[1])
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", locale))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", locale))
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("title=%s", utils.LocaleLanguage(locale)))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, subtitle := range subtitleFiles {
|
for i, subtitle := range subtitleFiles {
|
||||||
|
|
@ -555,7 +552,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)+len(audioFiles)))
|
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)+len(audioFiles)))
|
||||||
locale := crunchyroll.LOCALE(re.FindStringSubmatch(subtitle)[1])
|
locale := crunchyroll.LOCALE(re.FindStringSubmatch(subtitle)[1])
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("language=%s", locale))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("language=%s", locale))
|
||||||
metadata = append(metadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("title=%s", utils.LocaleLanguage(locale)))
|
metadata = append(metadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
|
||||||
}
|
}
|
||||||
|
|
||||||
commandOptions := []string{"-y"}
|
commandOptions := []string{"-y"}
|
||||||
|
|
@ -577,7 +574,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
commandOptions = append(commandOptions, "-disposition:s:0", "0", "-c", "copy", "-f", "matroska", file.Name())
|
commandOptions = append(commandOptions, "-disposition:s:0", "0", "-c", "copy", "-f", "matroska", file.Name())
|
||||||
|
|
||||||
// just a little nicer debug output to copy and paste the ffmpeg for debug reasons
|
// just a little nicer debug output to copy and paste the ffmpeg for debug reasons
|
||||||
if out.IsDev() {
|
if utils.Log.IsDev() {
|
||||||
var debugOptions []string
|
var debugOptions []string
|
||||||
|
|
||||||
for _, option := range commandOptions {
|
for _, option := range commandOptions {
|
||||||
|
|
@ -591,7 +588,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
debugOptions = append(debugOptions, option)
|
debugOptions = append(debugOptions, option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.Debug("FFmpeg merge command: ffmpeg %s", strings.Join(debugOptions, " "))
|
utils.Log.Debug("FFmpeg merge command: ffmpeg %s", strings.Join(debugOptions, " "))
|
||||||
}
|
}
|
||||||
|
|
||||||
var errBuf bytes.Buffer
|
var errBuf bytes.Buffer
|
||||||
|
|
@ -611,7 +608,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
|
||||||
var hasJapanese bool
|
var hasJapanese bool
|
||||||
languagesAsLocale := []crunchyroll.LOCALE{crunchyroll.JP}
|
languagesAsLocale := []crunchyroll.LOCALE{crunchyroll.JP}
|
||||||
for _, language := range archiveLanguagesFlag {
|
for _, language := range archiveLanguagesFlag {
|
||||||
|
|
@ -623,7 +620,7 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
episodes, err := extractEpisodes(url, languagesAsLocale...)
|
episodes, err := utils.ExtractEpisodes(url, languagesAsLocale...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -634,9 +631,9 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
|
|
||||||
for i, eps := range episodes {
|
for i, eps := range episodes {
|
||||||
if len(eps) == 0 {
|
if len(eps) == 0 {
|
||||||
out.SetProgress("%s has no matching episodes", languagesAsLocale[i])
|
utils.Log.SetProcess("%s has no matching episodes", languagesAsLocale[i])
|
||||||
} else if len(episodes[0]) > len(eps) {
|
} else if len(episodes[0]) > len(eps) {
|
||||||
out.SetProgress("%s has %d less episodes than existing in japanese (%d)", languagesAsLocale[i], len(episodes[0])-len(eps), len(episodes[0]))
|
utils.Log.SetProcess("%s has %d less episodes than existing in japanese (%d)", languagesAsLocale[i], len(episodes[0])-len(eps), len(episodes[0]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -644,11 +641,11 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
episodes = episodes[1:]
|
episodes = episodes[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
eps := make(map[int]map[int]*formatInformation)
|
eps := make(map[int]map[int]*utils.FormatInformation)
|
||||||
for _, lang := range episodes {
|
for _, lang := range episodes {
|
||||||
for _, season := range utils.SortEpisodesBySeason(lang) {
|
for _, season := range crunchyUtils.SortEpisodesBySeason(lang) {
|
||||||
if _, ok := eps[season[0].SeasonNumber]; !ok {
|
if _, ok := eps[season[0].SeasonNumber]; !ok {
|
||||||
eps[season[0].SeasonNumber] = map[int]*formatInformation{}
|
eps[season[0].SeasonNumber] = map[int]*utils.FormatInformation{}
|
||||||
}
|
}
|
||||||
for _, episode := range season {
|
for _, episode := range season {
|
||||||
format, err := episode.GetFormat(archiveResolutionFlag, "", false)
|
format, err := episode.GetFormat(archiveResolutionFlag, "", false)
|
||||||
|
|
@ -657,9 +654,9 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := eps[episode.SeasonNumber][episode.EpisodeNumber]; !ok {
|
if _, ok := eps[episode.SeasonNumber][episode.EpisodeNumber]; !ok {
|
||||||
eps[episode.SeasonNumber][episode.EpisodeNumber] = &formatInformation{
|
eps[episode.SeasonNumber][episode.EpisodeNumber] = &utils.FormatInformation{
|
||||||
format: format,
|
Format: format,
|
||||||
additionalFormats: make([]*crunchyroll.Format, 0),
|
AdditionalFormats: make([]*crunchyroll.Format, 0),
|
||||||
|
|
||||||
Title: episode.Title,
|
Title: episode.Title,
|
||||||
SeriesName: episode.SeriesTitle,
|
SeriesName: episode.SeriesTitle,
|
||||||
|
|
@ -671,15 +668,15 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
Audio: format.AudioLocale,
|
Audio: format.AudioLocale,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eps[episode.SeasonNumber][episode.EpisodeNumber].additionalFormats = append(eps[episode.SeasonNumber][episode.EpisodeNumber].additionalFormats, format)
|
eps[episode.SeasonNumber][episode.EpisodeNumber].AdditionalFormats = append(eps[episode.SeasonNumber][episode.EpisodeNumber].AdditionalFormats, format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var infoFormat [][]formatInformation
|
var infoFormat [][]utils.FormatInformation
|
||||||
for _, e := range eps {
|
for _, e := range eps {
|
||||||
var tmpFormatInfo []formatInformation
|
var tmpFormatInfo []utils.FormatInformation
|
||||||
|
|
||||||
var keys []int
|
var keys []int
|
||||||
for episodeNumber := range e {
|
for episodeNumber := range e {
|
||||||
|
|
@ -696,124 +693,3 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
|
|
||||||
return infoFormat, nil
|
return infoFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type compress interface {
|
|
||||||
io.Closer
|
|
||||||
|
|
||||||
NewFile(information formatInformation) (io.WriteCloser, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newGzipCompress(file *os.File) *tarCompress {
|
|
||||||
gw := gzip.NewWriter(file)
|
|
||||||
return &tarCompress{
|
|
||||||
parent: gw,
|
|
||||||
dst: tar.NewWriter(gw),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTarCompress(file *os.File) *tarCompress {
|
|
||||||
return &tarCompress{
|
|
||||||
dst: tar.NewWriter(file),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type tarCompress struct {
|
|
||||||
compress
|
|
||||||
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
parent *gzip.Writer
|
|
||||||
dst *tar.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *tarCompress) Close() error {
|
|
||||||
// we have to wait here in case the actual content isn't copied completely into the
|
|
||||||
// writer yet
|
|
||||||
tc.wg.Wait()
|
|
||||||
|
|
||||||
var err, err2 error
|
|
||||||
if tc.parent != nil {
|
|
||||||
err2 = tc.parent.Close()
|
|
||||||
}
|
|
||||||
err = tc.dst.Close()
|
|
||||||
|
|
||||||
if err != nil && err2 != nil {
|
|
||||||
// best way to show double errors at once that I've found
|
|
||||||
return fmt.Errorf("%v\n%v", err, err2)
|
|
||||||
} else if err == nil && err2 != nil {
|
|
||||||
err = err2
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *tarCompress) NewFile(information formatInformation) (io.WriteCloser, error) {
|
|
||||||
rp, wp := io.Pipe()
|
|
||||||
go func() {
|
|
||||||
tc.wg.Add(1)
|
|
||||||
defer tc.wg.Done()
|
|
||||||
var buf bytes.Buffer
|
|
||||||
io.Copy(&buf, rp)
|
|
||||||
|
|
||||||
header := &tar.Header{
|
|
||||||
Name: filepath.Join(fmt.Sprintf("S%2d", information.SeasonNumber), information.Title),
|
|
||||||
ModTime: time.Now(),
|
|
||||||
Mode: 0644,
|
|
||||||
Typeflag: tar.TypeReg,
|
|
||||||
// fun fact: I did not set the size for quiet some time because I thought that it isn't
|
|
||||||
// required. well because of this I debugged this part for multiple hours because without
|
|
||||||
// proper size information only a tiny amount gets copied into the tar (or zip) writer.
|
|
||||||
// this is also the reason why the file content is completely copied into a buffer before
|
|
||||||
// writing it to the writer. I could bypass this and save some memory but this requires
|
|
||||||
// some rewriting and im nearly at the (planned) finish for version 2 so nah in the future
|
|
||||||
// maybe
|
|
||||||
Size: int64(buf.Len()),
|
|
||||||
}
|
|
||||||
tc.dst.WriteHeader(header)
|
|
||||||
io.Copy(tc.dst, &buf)
|
|
||||||
}()
|
|
||||||
return wp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newZipCompress(file *os.File) *zipCompress {
|
|
||||||
return &zipCompress{
|
|
||||||
dst: zip.NewWriter(file),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type zipCompress struct {
|
|
||||||
compress
|
|
||||||
|
|
||||||
wg sync.WaitGroup
|
|
||||||
|
|
||||||
dst *zip.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (zc *zipCompress) Close() error {
|
|
||||||
zc.wg.Wait()
|
|
||||||
return zc.dst.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (zc *zipCompress) NewFile(information formatInformation) (io.WriteCloser, error) {
|
|
||||||
rp, wp := io.Pipe()
|
|
||||||
go func() {
|
|
||||||
zc.wg.Add(1)
|
|
||||||
defer zc.wg.Done()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
io.Copy(&buf, rp)
|
|
||||||
|
|
||||||
header := &zip.FileHeader{
|
|
||||||
Name: filepath.Join(fmt.Sprintf("S%2d", information.SeasonNumber), information.Title),
|
|
||||||
Modified: time.Now(),
|
|
||||||
Method: zip.Deflate,
|
|
||||||
UncompressedSize64: uint64(buf.Len()),
|
|
||||||
}
|
|
||||||
header.SetMode(0644)
|
|
||||||
|
|
||||||
hw, _ := zc.dst.CreateHeader(header)
|
|
||||||
io.Copy(hw, &buf)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return wp, nil
|
|
||||||
}
|
|
||||||
136
cli/commands/archive/compress.go
Normal file
136
cli/commands/archive/compress.go
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package archive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Compress interface {
|
||||||
|
io.Closer
|
||||||
|
|
||||||
|
NewFile(information utils.FormatInformation) (io.WriteCloser, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGzipCompress(file *os.File) *TarCompress {
|
||||||
|
gw := gzip.NewWriter(file)
|
||||||
|
return &TarCompress{
|
||||||
|
parent: gw,
|
||||||
|
dst: tar.NewWriter(gw),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTarCompress(file *os.File) *TarCompress {
|
||||||
|
return &TarCompress{
|
||||||
|
dst: tar.NewWriter(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarCompress struct {
|
||||||
|
Compress
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
parent *gzip.Writer
|
||||||
|
dst *tar.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TarCompress) Close() error {
|
||||||
|
// we have to wait here in case the actual content isn't copied completely into the
|
||||||
|
// writer yet
|
||||||
|
tc.wg.Wait()
|
||||||
|
|
||||||
|
var err, err2 error
|
||||||
|
if tc.parent != nil {
|
||||||
|
err2 = tc.parent.Close()
|
||||||
|
}
|
||||||
|
err = tc.dst.Close()
|
||||||
|
|
||||||
|
if err != nil && err2 != nil {
|
||||||
|
// best way to show double errors at once that I've found
|
||||||
|
return fmt.Errorf("%v\n%v", err, err2)
|
||||||
|
} else if err == nil && err2 != nil {
|
||||||
|
err = err2
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TarCompress) NewFile(information utils.FormatInformation) (io.WriteCloser, error) {
|
||||||
|
rp, wp := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
tc.wg.Add(1)
|
||||||
|
defer tc.wg.Done()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, rp)
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
Name: filepath.Join(fmt.Sprintf("S%2d", information.SeasonNumber), information.Title),
|
||||||
|
ModTime: time.Now(),
|
||||||
|
Mode: 0644,
|
||||||
|
Typeflag: tar.TypeReg,
|
||||||
|
// fun fact: I did not set the size for quiet some time because I thought that it isn't
|
||||||
|
// required. well because of this I debugged this part for multiple hours because without
|
||||||
|
// proper size information only a tiny amount gets copied into the tar (or zip) writer.
|
||||||
|
// this is also the reason why the file content is completely copied into a buffer before
|
||||||
|
// writing it to the writer. I could bypass this and save some memory but this requires
|
||||||
|
// some rewriting and im nearly at the (planned) finish for version 2 so nah in the future
|
||||||
|
// maybe
|
||||||
|
Size: int64(buf.Len()),
|
||||||
|
}
|
||||||
|
tc.dst.WriteHeader(header)
|
||||||
|
io.Copy(tc.dst, &buf)
|
||||||
|
}()
|
||||||
|
return wp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewZipCompress(file *os.File) *ZipCompress {
|
||||||
|
return &ZipCompress{
|
||||||
|
dst: zip.NewWriter(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZipCompress struct {
|
||||||
|
Compress
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
dst *zip.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zc *ZipCompress) Close() error {
|
||||||
|
zc.wg.Wait()
|
||||||
|
return zc.dst.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zc *ZipCompress) NewFile(information utils.FormatInformation) (io.WriteCloser, error) {
|
||||||
|
rp, wp := io.Pipe()
|
||||||
|
go func() {
|
||||||
|
zc.wg.Add(1)
|
||||||
|
defer zc.wg.Done()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, rp)
|
||||||
|
|
||||||
|
header := &zip.FileHeader{
|
||||||
|
Name: filepath.Join(fmt.Sprintf("S%2d", information.SeasonNumber), information.Title),
|
||||||
|
Modified: time.Now(),
|
||||||
|
Method: zip.Deflate,
|
||||||
|
UncompressedSize64: uint64(buf.Len()),
|
||||||
|
}
|
||||||
|
header.SetMode(0644)
|
||||||
|
|
||||||
|
hw, _ := zc.dst.CreateHeader(header)
|
||||||
|
io.Copy(hw, &buf)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return wp, nil
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package commands
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
"github.com/ByteDream/crunchyroll-go/v3"
|
||||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"math"
|
"math"
|
||||||
|
|
@ -29,29 +31,29 @@ var (
|
||||||
downloadGoroutinesFlag int
|
downloadGoroutinesFlag int
|
||||||
)
|
)
|
||||||
|
|
||||||
var downloadCmd = &cobra.Command{
|
var Cmd = &cobra.Command{
|
||||||
Use: "download",
|
Use: "download",
|
||||||
Short: "Download a video",
|
Short: "Download a video",
|
||||||
Args: cobra.MinimumNArgs(1),
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
|
||||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
out.Debug("Validating arguments")
|
utils.Log.Debug("Validating arguments")
|
||||||
|
|
||||||
if filepath.Ext(downloadOutputFlag) != ".ts" {
|
if filepath.Ext(downloadOutputFlag) != ".ts" {
|
||||||
if !hasFFmpeg() {
|
if !utils.HasFFmpeg() {
|
||||||
return fmt.Errorf("the file ending for the output file (%s) is not `.ts`. "+
|
return fmt.Errorf("the file ending for the output file (%s) is not `.ts`. "+
|
||||||
"Install ffmpeg (https://ffmpeg.org/download.html) to use other media file endings (e.g. `.mp4`)", downloadOutputFlag)
|
"Install ffmpeg (https://ffmpeg.org/download.html) to use other media file endings (e.g. `.mp4`)", downloadOutputFlag)
|
||||||
} else {
|
} else {
|
||||||
out.Debug("Custom file ending '%s' (ffmpeg is installed)", filepath.Ext(downloadOutputFlag))
|
utils.Log.Debug("Custom file ending '%s' (ffmpeg is installed)", filepath.Ext(downloadOutputFlag))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !utils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
|
if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
|
||||||
return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(allLocalesAsStrings(), ", "))
|
return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(utils.LocalesAsStrings(), ", "))
|
||||||
} else if downloadSubtitleFlag != "" && !utils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
|
} else if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
|
||||||
return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(allLocalesAsStrings(), ", "))
|
return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(utils.LocalesAsStrings(), ", "))
|
||||||
}
|
}
|
||||||
out.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
|
utils.Log.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
|
||||||
|
|
||||||
switch downloadResolutionFlag {
|
switch downloadResolutionFlag {
|
||||||
case "1080p", "720p", "480p", "360p":
|
case "1080p", "720p", "480p", "360p":
|
||||||
|
|
@ -64,35 +66,37 @@ var downloadCmd = &cobra.Command{
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)
|
return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)
|
||||||
}
|
}
|
||||||
out.Debug("Using resolution '%s'", downloadResolutionFlag)
|
utils.Log.Debug("Using resolution '%s'", downloadResolutionFlag)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
loadCrunchy()
|
if err := commands.LoadCrunchy(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return download(args)
|
return download(args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
downloadCmd.Flags().StringVarP(&downloadAudioFlag, "audio",
|
Cmd.Flags().StringVarP(&downloadAudioFlag, "audio",
|
||||||
"a",
|
"a",
|
||||||
string(systemLocale(false)),
|
string(utils.SystemLocale(false)),
|
||||||
"The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
|
"The locale of the audio. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
|
||||||
downloadCmd.Flags().StringVarP(&downloadSubtitleFlag,
|
Cmd.Flags().StringVarP(&downloadSubtitleFlag,
|
||||||
"subtitle",
|
"subtitle",
|
||||||
"s",
|
"s",
|
||||||
"",
|
"",
|
||||||
"The locale of the subtitle. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
|
"The locale of the subtitle. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
|
||||||
|
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
downloadCmd.Flags().StringVarP(&downloadDirectoryFlag,
|
Cmd.Flags().StringVarP(&downloadDirectoryFlag,
|
||||||
"directory",
|
"directory",
|
||||||
"d",
|
"d",
|
||||||
cwd,
|
cwd,
|
||||||
"The directory to download the file(s) into")
|
"The directory to download the file(s) into")
|
||||||
downloadCmd.Flags().StringVarP(&downloadOutputFlag,
|
Cmd.Flags().StringVarP(&downloadOutputFlag,
|
||||||
"output",
|
"output",
|
||||||
"o",
|
"o",
|
||||||
"{title}.ts",
|
"{title}.ts",
|
||||||
|
|
@ -108,7 +112,7 @@ func init() {
|
||||||
"\t{audio} » Audio locale of the video\n"+
|
"\t{audio} » Audio locale of the video\n"+
|
||||||
"\t{subtitle} » Subtitle locale of the video")
|
"\t{subtitle} » Subtitle locale of the video")
|
||||||
|
|
||||||
downloadCmd.Flags().StringVarP(&downloadResolutionFlag,
|
Cmd.Flags().StringVarP(&downloadResolutionFlag,
|
||||||
"resolution",
|
"resolution",
|
||||||
"r",
|
"r",
|
||||||
"best",
|
"best",
|
||||||
|
|
@ -117,34 +121,32 @@ func init() {
|
||||||
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
|
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
|
||||||
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
|
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
|
||||||
|
|
||||||
downloadCmd.Flags().IntVarP(&downloadGoroutinesFlag,
|
Cmd.Flags().IntVarP(&downloadGoroutinesFlag,
|
||||||
"goroutines",
|
"goroutines",
|
||||||
"g",
|
"g",
|
||||||
runtime.NumCPU(),
|
runtime.NumCPU(),
|
||||||
"Sets how many parallel segment downloads should be used")
|
"Sets how many parallel segment downloads should be used")
|
||||||
|
|
||||||
rootCmd.AddCommand(downloadCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func download(urls []string) error {
|
func download(urls []string) error {
|
||||||
for i, url := range urls {
|
for i, url := range urls {
|
||||||
out.SetProgress("Parsing url %d", i+1)
|
utils.Log.SetProcess("Parsing url %d", i+1)
|
||||||
episodes, err := downloadExtractEpisodes(url)
|
episodes, err := downloadExtractEpisodes(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
out.StopProgress("Failed to parse url %d", i+1)
|
utils.Log.StopProcess("Failed to parse url %d", i+1)
|
||||||
if crunchy.Config.Premium {
|
if utils.Crunchy.Config.Premium {
|
||||||
out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
|
utils.Log.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/crunchy-cli/issues/22 for more information")
|
"try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchy-cli/issues/22 for more information")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
out.StopProgress("Parsed url %d", i+1)
|
utils.Log.StopProcess("Parsed url %d", i+1)
|
||||||
|
|
||||||
for _, season := range episodes {
|
for _, season := range episodes {
|
||||||
out.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
|
utils.Log.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
|
||||||
|
|
||||||
for j, info := range season {
|
for j, info := range season {
|
||||||
out.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
|
utils.Log.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
|
||||||
j+1,
|
j+1,
|
||||||
info.Title,
|
info.Title,
|
||||||
info.Resolution,
|
info.Resolution,
|
||||||
|
|
@ -153,17 +155,17 @@ func download(urls []string) error {
|
||||||
info.EpisodeNumber)
|
info.EpisodeNumber)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
|
|
||||||
for j, season := range episodes {
|
for j, season := range episodes {
|
||||||
for k, info := range season {
|
for k, info := range season {
|
||||||
dir := info.Format(downloadDirectoryFlag)
|
dir := info.FormatString(downloadDirectoryFlag)
|
||||||
if _, err = os.Stat(dir); os.IsNotExist(err) {
|
if _, err = os.Stat(dir); os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(dir, 0777); err != nil {
|
if err = os.MkdirAll(dir, 0777); err != nil {
|
||||||
return fmt.Errorf("error while creating directory: %v", err)
|
return fmt.Errorf("error while creating directory: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
file, err := os.Create(generateFilename(info.Format(downloadOutputFlag), dir))
|
file, err := os.Create(utils.GenerateFilename(info.FormatString(downloadOutputFlag), dir))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create output file: %v", err)
|
return fmt.Errorf("failed to create output file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -176,7 +178,7 @@ func download(urls []string) error {
|
||||||
file.Close()
|
file.Close()
|
||||||
|
|
||||||
if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
|
if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -184,23 +186,23 @@ func download(urls []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadInfo(info formatInformation, file *os.File) error {
|
func downloadInfo(info utils.FormatInformation, file *os.File) error {
|
||||||
out.Debug("Entering season %d, episode %d", info.SeasonNumber, info.EpisodeNumber)
|
utils.Log.Debug("Entering season %d, episode %d", info.SeasonNumber, info.EpisodeNumber)
|
||||||
|
|
||||||
if err := info.format.InitVideo(); err != nil {
|
if err := info.Format.InitVideo(); err != nil {
|
||||||
return fmt.Errorf("error while initializing the video: %v", err)
|
return fmt.Errorf("error while initializing the video: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dp := &downloadProgress{
|
dp := &commands.DownloadProgress{
|
||||||
Prefix: out.InfoLog.Prefix(),
|
Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
|
||||||
Message: "Downloading video",
|
Message: "Downloading video",
|
||||||
// number of segments a video has +2 is for merging and the success message
|
// number of segments a video has +2 is for merging and the success message
|
||||||
Total: int(info.format.Video.Chunklist.Count()) + 2,
|
Total: int(info.Format.Video.Chunklist.Count()) + 2,
|
||||||
Dev: out.IsDev(),
|
Dev: utils.Log.IsDev(),
|
||||||
Quiet: out.IsQuiet(),
|
Quiet: utils.Log.(*commands.Logger).IsQuiet(),
|
||||||
}
|
}
|
||||||
if out.IsDev() {
|
if utils.Log.IsDev() {
|
||||||
dp.Prefix = out.DebugLog.Prefix()
|
dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if dp.Total != dp.Current {
|
if dp.Total != dp.Current {
|
||||||
|
|
@ -217,7 +219,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if out.IsDev() {
|
if utils.Log.IsDev() {
|
||||||
dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
|
dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
|
||||||
} else {
|
} else {
|
||||||
dp.Update()
|
dp.Update()
|
||||||
|
|
@ -228,7 +230,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if hasFFmpeg() {
|
if utils.HasFFmpeg() {
|
||||||
downloader.FFmpegOpts = make([]string, 0)
|
downloader.FFmpegOpts = make([]string, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -238,8 +240,8 @@ func downloadInfo(info formatInformation, file *os.File) error {
|
||||||
select {
|
select {
|
||||||
case <-sig:
|
case <-sig:
|
||||||
signal.Stop(sig)
|
signal.Stop(sig)
|
||||||
out.Exit("Exiting... (may take a few seconds)")
|
utils.Log.Err("Exiting... (may take a few seconds)")
|
||||||
out.Exit("To force exit press ctrl+c (again)")
|
utils.Log.Err("To force exit press ctrl+c (again)")
|
||||||
cancel()
|
cancel()
|
||||||
// os.Exit(1) is not called because an immediate exit after the cancel function does not let
|
// os.Exit(1) is not called because an immediate exit after the cancel function does not let
|
||||||
// the download process enough time to stop gratefully. A result of this is that the temporary
|
// the download process enough time to stop gratefully. A result of this is that the temporary
|
||||||
|
|
@ -248,38 +250,38 @@ func downloadInfo(info formatInformation, file *os.File) error {
|
||||||
// this is just here to end the goroutine and prevent it from running forever without a reason
|
// this is just here to end the goroutine and prevent it from running forever without a reason
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
out.Debug("Set up signal catcher")
|
utils.Log.Debug("Set up signal catcher")
|
||||||
|
|
||||||
out.Info("Downloading episode `%s` to `%s`", info.Title, filepath.Base(file.Name()))
|
utils.Log.Info("Downloading episode `%s` to `%s`", info.Title, filepath.Base(file.Name()))
|
||||||
out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
|
utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
|
||||||
out.Info("\tAudio: %s", info.Audio)
|
utils.Log.Info("\tAudio: %s", info.Audio)
|
||||||
out.Info("\tSubtitle: %s", info.Subtitle)
|
utils.Log.Info("\tSubtitle: %s", info.Subtitle)
|
||||||
out.Info("\tResolution: %spx", info.Resolution)
|
utils.Log.Info("\tResolution: %spx", info.Resolution)
|
||||||
out.Info("\tFPS: %.2f", info.FPS)
|
utils.Log.Info("\tFPS: %.2f", info.FPS)
|
||||||
if err := info.format.Download(downloader); err != nil {
|
if err := info.Format.Download(downloader); err != nil {
|
||||||
return fmt.Errorf("error while downloading: %v", err)
|
return fmt.Errorf("error while downloading: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dp.UpdateMessage("Download finished", false)
|
dp.UpdateMessage("Download finished", false)
|
||||||
|
|
||||||
signal.Stop(sig)
|
signal.Stop(sig)
|
||||||
out.Debug("Stopped signal catcher")
|
utils.Log.Debug("Stopped signal catcher")
|
||||||
|
|
||||||
out.Empty()
|
utils.Log.Empty()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadExtractEpisodes(url string) ([][]formatInformation, error) {
|
func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
|
||||||
episodes, err := extractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
|
episodes, err := utils.ExtractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
japanese := episodes[0]
|
japanese := episodes[0]
|
||||||
custom := episodes[1]
|
custom := episodes[1]
|
||||||
|
|
||||||
sort.Sort(utils.EpisodesByNumber(japanese))
|
sort.Sort(crunchyUtils.EpisodesByNumber(japanese))
|
||||||
sort.Sort(utils.EpisodesByNumber(custom))
|
sort.Sort(crunchyUtils.EpisodesByNumber(custom))
|
||||||
|
|
||||||
var errMessages []string
|
var errMessages []string
|
||||||
|
|
||||||
|
|
@ -303,25 +305,25 @@ func downloadExtractEpisodes(url string) ([][]formatInformation, error) {
|
||||||
|
|
||||||
if len(errMessages) > 10 {
|
if len(errMessages) > 10 {
|
||||||
for _, msg := range errMessages[:10] {
|
for _, msg := range errMessages[:10] {
|
||||||
out.SetProgress(msg)
|
utils.Log.SetProcess(msg)
|
||||||
}
|
}
|
||||||
out.SetProgress("... and %d more", len(errMessages)-10)
|
utils.Log.SetProcess("... and %d more", len(errMessages)-10)
|
||||||
} else {
|
} else {
|
||||||
for _, msg := range errMessages {
|
for _, msg := range errMessages {
|
||||||
out.SetProgress(msg)
|
utils.Log.SetProcess(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var infoFormat [][]formatInformation
|
var infoFormat [][]utils.FormatInformation
|
||||||
for _, season := range utils.SortEpisodesBySeason(final) {
|
for _, season := range crunchyUtils.SortEpisodesBySeason(final) {
|
||||||
tmpFormatInformation := make([]formatInformation, 0)
|
tmpFormatInformation := make([]utils.FormatInformation, 0)
|
||||||
for _, episode := range season {
|
for _, episode := range season {
|
||||||
format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
|
format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
|
return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
|
||||||
}
|
}
|
||||||
tmpFormatInformation = append(tmpFormatInformation, formatInformation{
|
tmpFormatInformation = append(tmpFormatInformation, utils.FormatInformation{
|
||||||
format: format,
|
Format: format,
|
||||||
|
|
||||||
Title: episode.Title,
|
Title: episode.Title,
|
||||||
SeriesName: episode.SeriesTitle,
|
SeriesName: episode.SeriesTitle,
|
||||||
40
cli/commands/info/info.go
Normal file
40
cli/commands/info/info.go
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
package info
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
|
crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cmd = &cobra.Command{
|
||||||
|
Use: "info",
|
||||||
|
Short: "Shows information about the logged in user",
|
||||||
|
Args: cobra.MinimumNArgs(0),
|
||||||
|
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := commands.LoadCrunchy(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return info()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func info() error {
|
||||||
|
account, err := utils.Crunchy.Account()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Username: ", account.Username)
|
||||||
|
fmt.Println("Email: ", account.Email)
|
||||||
|
fmt.Println("Premium: ", utils.Crunchy.Config.Premium)
|
||||||
|
fmt.Println("Interface language:", crunchyUtils.LocaleLanguage(account.PreferredCommunicationLanguage))
|
||||||
|
fmt.Println("Subtitle language: ", crunchyUtils.LocaleLanguage(account.PreferredContentSubtitleLanguage))
|
||||||
|
fmt.Println("Created: ", account.Created)
|
||||||
|
fmt.Println("Account ID: ", account.AccountID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -35,19 +36,7 @@ type progress struct {
|
||||||
stop bool
|
stop bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type logger struct {
|
func NewLogger(debug, info, err bool) *Logger {
|
||||||
DebugLog *log.Logger
|
|
||||||
InfoLog *log.Logger
|
|
||||||
ErrLog *log.Logger
|
|
||||||
|
|
||||||
devView bool
|
|
||||||
|
|
||||||
progress chan progress
|
|
||||||
done chan interface{}
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func newLogger(debug, info, err bool) *logger {
|
|
||||||
initPrefixBecauseWindowsSucksBallsHard()
|
initPrefixBecauseWindowsSucksBallsHard()
|
||||||
|
|
||||||
debugLog, infoLog, errLog := log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0)
|
debugLog, infoLog, errLog := log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0)
|
||||||
|
|
@ -68,7 +57,7 @@ func newLogger(debug, info, err bool) *logger {
|
||||||
errLog = log.New(errLog.Writer(), "[err] ", 0)
|
errLog = log.New(errLog.Writer(), "[err] ", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &logger{
|
return &Logger{
|
||||||
DebugLog: debugLog,
|
DebugLog: debugLog,
|
||||||
InfoLog: infoLog,
|
InfoLog: infoLog,
|
||||||
ErrLog: errLog,
|
ErrLog: errLog,
|
||||||
|
|
@ -77,38 +66,52 @@ func newLogger(debug, info, err bool) *logger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) IsDev() bool {
|
type Logger struct {
|
||||||
|
utils.Logger
|
||||||
|
|
||||||
|
DebugLog *log.Logger
|
||||||
|
InfoLog *log.Logger
|
||||||
|
ErrLog *log.Logger
|
||||||
|
|
||||||
|
devView bool
|
||||||
|
|
||||||
|
progress chan progress
|
||||||
|
done chan interface{}
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) IsDev() bool {
|
||||||
return l.devView
|
return l.devView
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) IsQuiet() bool {
|
func (l *Logger) IsQuiet() bool {
|
||||||
return l.DebugLog.Writer() == io.Discard && l.InfoLog.Writer() == io.Discard && l.ErrLog.Writer() == io.Discard
|
return l.DebugLog.Writer() == io.Discard && l.InfoLog.Writer() == io.Discard && l.ErrLog.Writer() == io.Discard
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) Debug(format string, v ...interface{}) {
|
func (l *Logger) Debug(format string, v ...interface{}) {
|
||||||
l.DebugLog.Printf(format, v...)
|
l.DebugLog.Printf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) Info(format string, v ...interface{}) {
|
func (l *Logger) Info(format string, v ...interface{}) {
|
||||||
l.InfoLog.Printf(format, v...)
|
l.InfoLog.Printf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) Err(format string, v ...interface{}) {
|
func (l *Logger) Warn(format string, v ...interface{}) {
|
||||||
|
l.Err(format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Err(format string, v ...interface{}) {
|
||||||
l.ErrLog.Printf(format, v...)
|
l.ErrLog.Printf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) Exit(format string, v ...interface{}) {
|
func (l *Logger) Empty() {
|
||||||
fmt.Fprintln(l.ErrLog.Writer(), fmt.Sprintf(format, v...))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logger) Empty() {
|
|
||||||
if !l.devView && l.InfoLog.Writer() != io.Discard {
|
if !l.devView && l.InfoLog.Writer() != io.Discard {
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) SetProgress(format string, v ...interface{}) {
|
func (l *Logger) SetProcess(format string, v ...interface{}) {
|
||||||
if out.InfoLog.Writer() == io.Discard {
|
if l.InfoLog.Writer() == io.Discard {
|
||||||
return
|
return
|
||||||
} else if l.devView {
|
} else if l.devView {
|
||||||
l.Debug(format, v...)
|
l.Debug(format, v...)
|
||||||
|
|
@ -175,8 +178,8 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *logger) StopProgress(format string, v ...interface{}) {
|
func (l *Logger) StopProcess(format string, v ...interface{}) {
|
||||||
if out.InfoLog.Writer() == io.Discard {
|
if l.InfoLog.Writer() == io.Discard {
|
||||||
return
|
return
|
||||||
} else if l.devView {
|
} else if l.devView {
|
||||||
l.Debug(format, v...)
|
l.Debug(format, v...)
|
||||||
158
cli/commands/login/login.go
Normal file
158
cli/commands/login/login.go
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
package login
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
|
"github.com/ByteDream/crunchyroll-go/v3"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
loginPersistentFlag bool
|
||||||
|
loginEncryptFlag bool
|
||||||
|
|
||||||
|
loginSessionIDFlag bool
|
||||||
|
loginEtpRtFlag bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cmd = &cobra.Command{
|
||||||
|
Use: "login",
|
||||||
|
Short: "Login to crunchyroll",
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Cmd.Flags().BoolVar(&loginPersistentFlag,
|
||||||
|
"persistent",
|
||||||
|
false,
|
||||||
|
"If the given credential should be stored persistent")
|
||||||
|
Cmd.Flags().BoolVar(&loginEncryptFlag,
|
||||||
|
"encrypt",
|
||||||
|
false,
|
||||||
|
"Encrypt the given credentials (won't do anything if --session-id is given or --persistent is not given)")
|
||||||
|
|
||||||
|
Cmd.Flags().BoolVar(&loginSessionIDFlag,
|
||||||
|
"session-id",
|
||||||
|
false,
|
||||||
|
"Use a session id to login instead of username and password")
|
||||||
|
Cmd.Flags().BoolVar(&loginEtpRtFlag,
|
||||||
|
"etp-rt",
|
||||||
|
false,
|
||||||
|
"Use a etp rt cookie to login instead of username and password")
|
||||||
|
|
||||||
|
Cmd.MarkFlagsMutuallyExclusive("session-id", "etp-rt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginCredentials(user, password string) error {
|
||||||
|
utils.Log.Debug("Logging in via credentials")
|
||||||
|
c, err := crunchyroll.LoginWithCredentials(user, password, utils.SystemLocale(false), utils.Client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginPersistentFlag {
|
||||||
|
var passwd []byte
|
||||||
|
if loginEncryptFlag {
|
||||||
|
for {
|
||||||
|
fmt.Print("Enter password: ")
|
||||||
|
passwd, err = commands.ReadLineSilent()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Print("Enter password again: ")
|
||||||
|
repasswd, err := commands.ReadLineSilent()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if bytes.Equal(passwd, repasswd) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fmt.Println("Passwords does not match, try again")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = utils.SaveCredentialsPersistent(user, password, passwd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !loginEncryptFlag {
|
||||||
|
utils.Log.Warn("The login information will be stored permanently UNENCRYPTED on your drive. " +
|
||||||
|
"To encrypt it, use the `--encrypt` flag")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = utils.SaveSession(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loginPersistentFlag {
|
||||||
|
utils.Log.Info("Due to security reasons, you have to login again on the next reboot")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginSessionID(sessionID string) error {
|
||||||
|
utils.Log.Debug("Logging in via session id")
|
||||||
|
var c *crunchyroll.Crunchyroll
|
||||||
|
var err error
|
||||||
|
if c, err = crunchyroll.LoginWithSessionID(sessionID, utils.SystemLocale(false), utils.Client); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginPersistentFlag {
|
||||||
|
if err = utils.SaveSessionPersistent(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
utils.Log.Warn("The login information will be stored permanently UNENCRYPTED on your drive")
|
||||||
|
}
|
||||||
|
if err = utils.SaveSession(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loginPersistentFlag {
|
||||||
|
utils.Log.Info("Due to security reasons, you have to login again on the next reboot")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loginEtpRt(etpRt string) error {
|
||||||
|
utils.Log.Debug("Logging in via etp rt")
|
||||||
|
var c *crunchyroll.Crunchyroll
|
||||||
|
var err error
|
||||||
|
if c, err = crunchyroll.LoginWithEtpRt(etpRt, utils.SystemLocale(false), utils.Client); err != nil {
|
||||||
|
utils.Log.Err(err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if loginPersistentFlag {
|
||||||
|
if err = utils.SaveSessionPersistent(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
utils.Log.Warn("The login information will be stored permanently UNENCRYPTED on your drive")
|
||||||
|
}
|
||||||
|
if err = utils.SaveSession(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !loginPersistentFlag {
|
||||||
|
utils.Log.Info("Due to security reasons, you have to login again on the next reboot")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,7 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readLineSilent() ([]byte, error) {
|
func ReadLineSilent() ([]byte, error) {
|
||||||
pid, err := setEcho(false)
|
pid, err := setEcho(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package commands
|
package update
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -15,7 +16,7 @@ var (
|
||||||
updateInstallFlag bool
|
updateInstallFlag bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCmd = &cobra.Command{
|
var Cmd = &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Check if updates are available",
|
Short: "Check if updates are available",
|
||||||
Args: cobra.MaximumNArgs(0),
|
Args: cobra.MaximumNArgs(0),
|
||||||
|
|
@ -26,19 +27,17 @@ var updateCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
updateCmd.Flags().BoolVarP(&updateInstallFlag,
|
Cmd.Flags().BoolVarP(&updateInstallFlag,
|
||||||
"install",
|
"install",
|
||||||
"i",
|
"i",
|
||||||
false,
|
false,
|
||||||
"If set and a new version is available, the new version gets installed")
|
"If set and a new version is available, the new version gets installed")
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func update() error {
|
func update() error {
|
||||||
var release map[string]interface{}
|
var release map[string]interface{}
|
||||||
|
|
||||||
resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
|
resp, err := utils.Client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +47,8 @@ func update() error {
|
||||||
}
|
}
|
||||||
releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
|
releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
|
||||||
|
|
||||||
if Version == "development" {
|
if utils.Version == "development" {
|
||||||
out.Info("Development version, update service not available")
|
utils.Log.Info("Development version, update service not available")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,17 +57,17 @@ func update() error {
|
||||||
return fmt.Errorf("latest tag name (%s) is not parsable", releaseVersion)
|
return fmt.Errorf("latest tag name (%s) is not parsable", releaseVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
internalVersion := strings.SplitN(Version, ".", 4)
|
internalVersion := strings.SplitN(utils.Version, ".", 4)
|
||||||
if len(internalVersion) != 3 {
|
if len(internalVersion) != 3 {
|
||||||
return fmt.Errorf("internal version (%s) is not parsable", Version)
|
return fmt.Errorf("internal version (%s) is not parsable", utils.Version)
|
||||||
}
|
}
|
||||||
|
|
||||||
out.Info("Installed version is %s", Version)
|
utils.Log.Info("Installed version is %s", utils.Version)
|
||||||
|
|
||||||
var hasUpdate bool
|
var hasUpdate bool
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
if latestRelease[i] < internalVersion[i] {
|
if latestRelease[i] < internalVersion[i] {
|
||||||
out.Info("Local version is newer than version in latest release (%s)", releaseVersion)
|
utils.Log.Info("Local version is newer than version in latest release (%s)", releaseVersion)
|
||||||
return nil
|
return nil
|
||||||
} else if latestRelease[i] > internalVersion[i] {
|
} else if latestRelease[i] > internalVersion[i] {
|
||||||
hasUpdate = true
|
hasUpdate = true
|
||||||
|
|
@ -76,11 +75,11 @@ func update() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !hasUpdate {
|
if !hasUpdate {
|
||||||
out.Info("Version is up-to-date")
|
utils.Log.Info("Version is up-to-date")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
out.Info("A new version is available (%s): https://github.com/ByteDream/crunchy-cli/releases/tag/v%s", releaseVersion, releaseVersion)
|
utils.Log.Info("A new version is available (%s): https://github.com/ByteDream/crunchy-cli/releases/tag/v%s", releaseVersion, releaseVersion)
|
||||||
|
|
||||||
if updateInstallFlag {
|
if updateInstallFlag {
|
||||||
if runtime.GOARCH != "amd64" {
|
if runtime.GOARCH != "amd64" {
|
||||||
|
|
@ -93,7 +92,7 @@ func update() error {
|
||||||
case "linux":
|
case "linux":
|
||||||
yayCommand := exec.Command("pacman -Q crunchy-cli")
|
yayCommand := exec.Command("pacman -Q crunchy-cli")
|
||||||
if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
|
if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
|
||||||
out.Info("crunchy-cli was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
|
utils.Log.Info("crunchy-cli was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
|
downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
|
||||||
|
|
@ -106,7 +105,7 @@ func update() error {
|
||||||
"You have to update manually (https://github.com/ByteDream/crunchy-cli", runtime.GOOS)
|
"You have to update manually (https://github.com/ByteDream/crunchy-cli", runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
out.SetProgress("Updating executable %s", os.Args[0])
|
utils.Log.SetProcess("Updating executable %s", os.Args[0])
|
||||||
|
|
||||||
perms, err := os.Stat(os.Args[0])
|
perms, err := os.Stat(os.Args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -119,7 +118,7 @@ func update() error {
|
||||||
}
|
}
|
||||||
defer executeFile.Close()
|
defer executeFile.Close()
|
||||||
|
|
||||||
resp, err := client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchy-cli/releases/download/v%s/%s", releaseVersion, downloadFile))
|
resp, err := utils.Client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchy-cli/releases/download/v%s/%s", releaseVersion, downloadFile))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -129,7 +128,7 @@ func update() error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
out.StopProgress("Updated executable %s", os.Args[0])
|
utils.Log.StopProcess("Updated executable %s", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
125
cli/commands/utils.go
Normal file
125
cli/commands/utils.go
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadProgress struct {
|
||||||
|
Prefix string
|
||||||
|
Message string
|
||||||
|
|
||||||
|
Total int
|
||||||
|
Current int
|
||||||
|
|
||||||
|
Dev bool
|
||||||
|
Quiet bool
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp *DownloadProgress) Update() {
|
||||||
|
dp.update("", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp *DownloadProgress) UpdateMessage(msg string, permanent bool) {
|
||||||
|
dp.update(msg, permanent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dp *DownloadProgress) update(msg string, permanent bool) {
|
||||||
|
if dp.Quiet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if dp.Current >= dp.Total {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dp.lock.Lock()
|
||||||
|
defer dp.lock.Unlock()
|
||||||
|
dp.Current++
|
||||||
|
|
||||||
|
if msg == "" {
|
||||||
|
msg = dp.Message
|
||||||
|
}
|
||||||
|
if permanent {
|
||||||
|
dp.Message = msg
|
||||||
|
}
|
||||||
|
|
||||||
|
if dp.Dev {
|
||||||
|
fmt.Printf("%s%s\n", dp.Prefix, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
percentage := float32(dp.Current) / float32(dp.Total) * 100
|
||||||
|
|
||||||
|
pre := fmt.Sprintf("%s%s [", dp.Prefix, msg)
|
||||||
|
post := fmt.Sprintf("]%4d%% %8d/%d", int(percentage), dp.Current, dp.Total)
|
||||||
|
|
||||||
|
// I don't really know why +2 is needed here but without it the Printf below would not print to the line end
|
||||||
|
progressWidth := terminalWidth() - len(pre) - len(post) + 2
|
||||||
|
repeatCount := int(percentage / float32(100) * float32(progressWidth))
|
||||||
|
// it can be lower than zero when the terminal is very tiny
|
||||||
|
if repeatCount < 0 {
|
||||||
|
repeatCount = 0
|
||||||
|
}
|
||||||
|
progressPercentage := strings.Repeat("=", repeatCount)
|
||||||
|
if dp.Current != dp.Total {
|
||||||
|
progressPercentage += ">"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\r%s%-"+fmt.Sprint(progressWidth)+"s%s", pre, progressPercentage, post)
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminalWidth() int {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
cmd := exec.Command("stty", "size")
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
res, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
// on alpine linux the command `stty size` does not respond the terminal size
|
||||||
|
// but something like "stty: standard input". this may also apply to other systems
|
||||||
|
splitOutput := strings.SplitN(strings.ReplaceAll(string(res), "\n", ""), " ", 2)
|
||||||
|
if len(splitOutput) == 1 {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
width, err := strconv.Atoi(splitOutput[1])
|
||||||
|
if err != nil {
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadCrunchy() error {
|
||||||
|
var encryptionKey []byte
|
||||||
|
|
||||||
|
if utils.IsTempSession() {
|
||||||
|
encryptionKey = nil
|
||||||
|
} else {
|
||||||
|
if encrypted, err := utils.IsSavedSessionEncrypted(); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("to use this command, login first. Type `%s login -h` to get help", os.Args[0])
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
} else if encrypted {
|
||||||
|
encryptionKey, err = ReadLineSilent()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read password")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
utils.Crunchy, err = utils.LoadSession(encryptionKey)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
|
// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
|
||||||
func readLineSilent() ([]byte, error) {
|
func ReadLineSilent() ([]byte, error) {
|
||||||
var oldMode uint32
|
var oldMode uint32
|
||||||
|
|
||||||
if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
|
if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
|
||||||
85
cli/root.go
Normal file
85
cli/root.go
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands/archive"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands/download"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands/info"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands/login"
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli/commands/update"
|
||||||
|
"github.com/ByteDream/crunchy-cli/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
quietFlag bool
|
||||||
|
verboseFlag bool
|
||||||
|
|
||||||
|
proxyFlag string
|
||||||
|
|
||||||
|
useragentFlag string
|
||||||
|
)
|
||||||
|
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "crunchy-cli",
|
||||||
|
Version: utils.Version,
|
||||||
|
Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchy-cli/wiki",
|
||||||
|
|
||||||
|
SilenceErrors: true,
|
||||||
|
SilenceUsage: true,
|
||||||
|
|
||||||
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
if verboseFlag {
|
||||||
|
utils.Log = commands.NewLogger(true, true, true)
|
||||||
|
} else if quietFlag {
|
||||||
|
utils.Log = commands.NewLogger(false, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.Log.Debug("Executing `%s` command with %d arg(s)", cmd.Name(), len(args))
|
||||||
|
|
||||||
|
utils.Client, err = utils.CreateOrDefaultClient(proxyFlag, useragentFlag)
|
||||||
|
return
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "Disable all output")
|
||||||
|
RootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Adds debug messages to the normal output")
|
||||||
|
|
||||||
|
RootCmd.PersistentFlags().StringVarP(&proxyFlag, "proxy", "p", "", "Proxy to use")
|
||||||
|
|
||||||
|
RootCmd.PersistentFlags().StringVar(&useragentFlag, "useragent", fmt.Sprintf("crunchy-cli/%s", utils.Version), "Useragent to do all request with")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(archive.Cmd)
|
||||||
|
RootCmd.AddCommand(download.Cmd)
|
||||||
|
RootCmd.AddCommand(info.Cmd)
|
||||||
|
RootCmd.AddCommand(login.Cmd)
|
||||||
|
RootCmd.AddCommand(update.Cmd)
|
||||||
|
|
||||||
|
utils.Log = commands.NewLogger(false, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() {
|
||||||
|
RootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if utils.Log.IsDev() {
|
||||||
|
utils.Log.Err("%v: %s", r, debug.Stack())
|
||||||
|
} else {
|
||||||
|
utils.Log.Err("Unexpected error: %v", r)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := RootCmd.Execute(); err != nil {
|
||||||
|
if !strings.HasSuffix(err.Error(), context.Canceled.Error()) {
|
||||||
|
utils.Log.Err("An error occurred: %v", err)
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var infoCmd = &cobra.Command{
|
|
||||||
Use: "info",
|
|
||||||
Short: "Shows information about the logged in user",
|
|
||||||
Args: cobra.MinimumNArgs(0),
|
|
||||||
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
loadCrunchy()
|
|
||||||
|
|
||||||
return info()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(infoCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func info() error {
|
|
||||||
account, err := crunchy.Account()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Username: ", account.Username)
|
|
||||||
fmt.Println("Email: ", account.Email)
|
|
||||||
fmt.Println("Premium: ", crunchy.Config.Premium)
|
|
||||||
fmt.Println("Interface language:", utils.LocaleLanguage(account.PreferredCommunicationLanguage))
|
|
||||||
fmt.Println("Subtitle language: ", utils.LocaleLanguage(account.PreferredContentSubtitleLanguage))
|
|
||||||
fmt.Println("Created: ", account.Created)
|
|
||||||
fmt.Println("Account ID: ", account.AccountID)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,206 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
loginPersistentFlag bool
|
|
||||||
loginEncryptFlag bool
|
|
||||||
|
|
||||||
loginSessionIDFlag bool
|
|
||||||
loginEtpRtFlag bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var loginCmd = &cobra.Command{
|
|
||||||
Use: "login",
|
|
||||||
Short: "Login to crunchyroll",
|
|
||||||
Args: cobra.RangeArgs(1, 2),
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
loginCmd.Flags().BoolVar(&loginPersistentFlag,
|
|
||||||
"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")
|
|
||||||
c, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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, "crunchy-cli"), 0755)
|
|
||||||
if err = os.WriteFile(filepath.Join(configDir, "crunchy-cli", "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, "crunchy-cli", "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")
|
|
||||||
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, "crunchy-cli"), 0755)
|
|
||||||
if err = os.WriteFile(filepath.Join(configDir, "crunchy-cli", "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, "crunchy-cli", "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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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, "crunchy-cli"), 0755)
|
|
||||||
if err = os.WriteFile(filepath.Join(configDir, "crunchy-cli", "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, "crunchy-cli", "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
|
|
||||||
}
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"runtime/debug"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Version = "development"
|
|
||||||
|
|
||||||
var (
|
|
||||||
client *http.Client
|
|
||||||
crunchy *crunchyroll.Crunchyroll
|
|
||||||
out = newLogger(false, true, true)
|
|
||||||
|
|
||||||
quietFlag bool
|
|
||||||
verboseFlag bool
|
|
||||||
|
|
||||||
proxyFlag string
|
|
||||||
|
|
||||||
useragentFlag string
|
|
||||||
)
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
|
||||||
Use: "crunchy-cli",
|
|
||||||
Version: Version,
|
|
||||||
Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchy-cli/wiki",
|
|
||||||
|
|
||||||
SilenceErrors: true,
|
|
||||||
SilenceUsage: true,
|
|
||||||
|
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
|
|
||||||
if verboseFlag {
|
|
||||||
out = newLogger(true, true, true)
|
|
||||||
} else if quietFlag {
|
|
||||||
out = newLogger(false, false, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Debug("Executing `%s` command with %d arg(s)", cmd.Name(), len(args))
|
|
||||||
|
|
||||||
client, err = createOrDefaultClient(proxyFlag, useragentFlag)
|
|
||||||
return
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&quietFlag, "quiet", "q", false, "Disable all output")
|
|
||||||
rootCmd.PersistentFlags().BoolVarP(&verboseFlag, "verbose", "v", false, "Adds debug messages to the normal output")
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&proxyFlag, "proxy", "p", "", "Proxy to use")
|
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVar(&useragentFlag, "useragent", fmt.Sprintf("crunchy-cli/%s", Version), "Useragent to do all request with")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
if out.IsDev() {
|
|
||||||
out.Err("%v: %s", r, debug.Stack())
|
|
||||||
} else {
|
|
||||||
out.Err("Unexpected error: %v", r)
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
|
||||||
if !strings.HasSuffix(err.Error(), context.Canceled.Error()) {
|
|
||||||
out.Exit("An error occurred: %v", err)
|
|
||||||
}
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,485 +0,0 @@
|
||||||
package commands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"github.com/ByteDream/crunchyroll-go/v3"
|
|
||||||
"github.com/ByteDream/crunchyroll-go/v3/utils"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ahh i love windows :)))
|
|
||||||
invalidWindowsChars = []string{"\\", "<", ">", ":", "\"", "/", "|", "?", "*"}
|
|
||||||
invalidNotWindowsChars = []string{"/"}
|
|
||||||
)
|
|
||||||
|
|
||||||
var urlFilter = regexp.MustCompile(`(S(\d+))?(E(\d+))?((-)(S(\d+))?(E(\d+))?)?(,|$)`)
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
out.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 {
|
|
||||||
out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
|
|
||||||
}
|
|
||||||
l = crunchyroll.US
|
|
||||||
}
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if verbose {
|
|
||||||
out.Err("Failed to get locale, using %s", crunchyroll.US)
|
|
||||||
}
|
|
||||||
return crunchyroll.US
|
|
||||||
}
|
|
||||||
|
|
||||||
func allLocalesAsStrings() (locales []string) {
|
|
||||||
for _, locale := range utils.AllLocales {
|
|
||||||
locales = append(locales, string(locale))
|
|
||||||
}
|
|
||||||
sort.Strings(locales)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
out.Info("Using custom proxy %s", proxy)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 loadCrunchy() {
|
|
||||||
out.SetProgress("Logging in")
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
if crunchy, err = crunchyroll.LoginWithEtpRt(string(body), systemLocale(true), client); err != nil {
|
|
||||||
out.Debug("Failed to login with temp etp rt cookie: %v", err)
|
|
||||||
} else {
|
|
||||||
out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
|
|
||||||
|
|
||||||
out.StopProgress("Logged in")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configDir, err := os.UserConfigDir(); err == nil {
|
|
||||||
persistentFilePath := filepath.Join(configDir, "crunchy-cli", "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)
|
|
||||||
}
|
|
||||||
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("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("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasFFmpeg() bool {
|
|
||||||
return exec.Command("ffmpeg", "-h").Run() == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func terminalWidth() int {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
cmd := exec.Command("stty", "size")
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
res, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return 60
|
|
||||||
}
|
|
||||||
// on alpine linux the command `stty size` does not respond the terminal size
|
|
||||||
// but something like "stty: standard input". this may also apply to other systems
|
|
||||||
splitOutput := strings.SplitN(strings.ReplaceAll(string(res), "\n", ""), " ", 2)
|
|
||||||
if len(splitOutput) == 1 {
|
|
||||||
return 60
|
|
||||||
}
|
|
||||||
width, err := strconv.Atoi(splitOutput[1])
|
|
||||||
if err != nil {
|
|
||||||
return 60
|
|
||||||
}
|
|
||||||
return width
|
|
||||||
}
|
|
||||||
return 60
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateFilename(name, directory string) string {
|
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
for _, char := range invalidNotWindowsChars {
|
|
||||||
name = strings.ReplaceAll(name, char, "")
|
|
||||||
}
|
|
||||||
out.Debug("Replaced invalid characters (not windows)")
|
|
||||||
} else {
|
|
||||||
for _, char := range invalidWindowsChars {
|
|
||||||
name = strings.ReplaceAll(name, char, "")
|
|
||||||
}
|
|
||||||
out.Debug("Replaced invalid characters (windows)")
|
|
||||||
}
|
|
||||||
|
|
||||||
filename, changed := freeFileName(filepath.Join(directory, name))
|
|
||||||
if changed {
|
|
||||||
out.Debug("File `%s` already exists, changing name to `%s`", filepath.Base(name), filepath.Base(filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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) Format(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 invalidNotWindowsChars {
|
|
||||||
valueAsString = strings.ReplaceAll(valueAsString, char, "")
|
|
||||||
}
|
|
||||||
out.Debug("Replaced invalid characters (not windows)")
|
|
||||||
} else {
|
|
||||||
for _, char := range invalidWindowsChars {
|
|
||||||
valueAsString = strings.ReplaceAll(valueAsString, char, "")
|
|
||||||
}
|
|
||||||
out.Debug("Replaced invalid characters (windows)")
|
|
||||||
}
|
|
||||||
|
|
||||||
source = strings.ReplaceAll(source, "{"+fields.Field(i).Tag.Get("json")+"}", valueAsString)
|
|
||||||
}
|
|
||||||
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
||||||
type downloadProgress struct {
|
|
||||||
Prefix string
|
|
||||||
Message string
|
|
||||||
|
|
||||||
Total int
|
|
||||||
Current int
|
|
||||||
|
|
||||||
Dev bool
|
|
||||||
Quiet bool
|
|
||||||
|
|
||||||
lock sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dp *downloadProgress) Update() {
|
|
||||||
dp.update("", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dp *downloadProgress) UpdateMessage(msg string, permanent bool) {
|
|
||||||
dp.update(msg, permanent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dp *downloadProgress) update(msg string, permanent bool) {
|
|
||||||
if dp.Quiet {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if dp.Current >= dp.Total {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dp.lock.Lock()
|
|
||||||
defer dp.lock.Unlock()
|
|
||||||
dp.Current++
|
|
||||||
|
|
||||||
if msg == "" {
|
|
||||||
msg = dp.Message
|
|
||||||
}
|
|
||||||
if permanent {
|
|
||||||
dp.Message = msg
|
|
||||||
}
|
|
||||||
|
|
||||||
if dp.Dev {
|
|
||||||
fmt.Printf("%s%s\n", dp.Prefix, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
percentage := float32(dp.Current) / float32(dp.Total) * 100
|
|
||||||
|
|
||||||
pre := fmt.Sprintf("%s%s [", dp.Prefix, msg)
|
|
||||||
post := fmt.Sprintf("]%4d%% %8d/%d", int(percentage), dp.Current, dp.Total)
|
|
||||||
|
|
||||||
// I don't really know why +2 is needed here but without it the Printf below would not print to the line end
|
|
||||||
progressWidth := terminalWidth() - len(pre) - len(post) + 2
|
|
||||||
repeatCount := int(percentage / float32(100) * float32(progressWidth))
|
|
||||||
// it can be lower than zero when the terminal is very tiny
|
|
||||||
if repeatCount < 0 {
|
|
||||||
repeatCount = 0
|
|
||||||
}
|
|
||||||
progressPercentage := strings.Repeat("=", repeatCount)
|
|
||||||
if dp.Current != dp.Total {
|
|
||||||
progressPercentage += ">"
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("\r%s%-"+fmt.Sprint(progressWidth)+"s%s", pre, progressPercentage, post)
|
|
||||||
}
|
|
||||||
|
|
@ -61,7 +61,7 @@ NOTE: The credentials are stored in plain text and if you not use \fB--session-i
|
||||||
Login via a session id (which can be extracted from a crunchyroll browser cookie) instead of using username and password.
|
Login via a session id (which can be extracted from a crunchyroll browser cookie) instead of using username and password.
|
||||||
|
|
||||||
.SH DOWNLOAD COMMAND
|
.SH DOWNLOAD COMMAND
|
||||||
A command to simply download videos. The output file is stored as a \fI.ts\fR file. \fIffmpeg\fR has to be installed if you want to change the format the videos are stored in.
|
A command to simply download videos. The output file is stored as a \fI.ts\fR file. \fIffmpeg\fR has to be installed if you want to change the Format the videos are stored in.
|
||||||
.TP
|
.TP
|
||||||
|
|
||||||
\fB-a, --audio AUDIO\fR
|
\fB-a, --audio AUDIO\fR
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -3,7 +3,7 @@ module github.com/ByteDream/crunchy-cli
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c
|
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220630135625-ed58b3fe8cc1
|
||||||
github.com/grafov/m3u8 v0.11.1
|
github.com/grafov/m3u8 v0.11.1
|
||||||
github.com/spf13/cobra v1.5.0
|
github.com/spf13/cobra v1.5.0
|
||||||
)
|
)
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -1,5 +1,7 @@
|
||||||
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c h1:jPabd/Zl/zdoSo8ZGtZLm43+42nIFHIJABvrvdMOYtY=
|
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c h1:jPabd/Zl/zdoSo8ZGtZLm43+42nIFHIJABvrvdMOYtY=
|
||||||
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c/go.mod h1:L4M1sOPjJ4ui0YXFnpVUb4AzQIa+D/i/B0QG5iz9op4=
|
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c/go.mod h1:L4M1sOPjJ4ui0YXFnpVUb4AzQIa+D/i/B0QG5iz9op4=
|
||||||
|
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220630135625-ed58b3fe8cc1 h1:hOL4xzDc6oCcrpf6GrrdUgvqwsQo6dI2zL4nA8rl9hg=
|
||||||
|
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220630135625-ed58b3fe8cc1/go.mod h1:L4M1sOPjJ4ui0YXFnpVUb4AzQIa+D/i/B0QG5iz9op4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
|
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
|
||||||
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||||
|
|
|
||||||
6
main.go
6
main.go
|
|
@ -1,7 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/ByteDream/crunchy-cli/commands"
|
import (
|
||||||
|
"github.com/ByteDream/crunchy-cli/cli"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
commands.Execute()
|
cli.Execute()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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