From 56015d31749259dfb5387cf6b84f9a107807de45 Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sat, 20 Nov 2021 00:47:52 +0100
Subject: [PATCH 001/732] Fix typos
---
crunchyroll.go | 2 +-
format.go | 4 ++--
utils/sort.go | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index e9b1e63..189ad80 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -280,7 +280,7 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
return s, m, nil
}
-// FindVideo fins a Video (Season or Movie) by a crunchyroll link
+// FindVideo finds a Video (Season or Movie) by a crunchyroll link
// e.g. https://www.crunchyroll.com/darling-in-the-franxx
func (c *Crunchyroll) FindVideo(seriesUrl string) (Video, error) {
if series, ok := MatchVideo(seriesUrl); ok {
diff --git a/format.go b/format.go
index 691cd47..7fbdb58 100644
--- a/format.go
+++ b/format.go
@@ -180,7 +180,7 @@ func (f *Format) getCrypt(segment *m3u8.MediaSegment) (block cipher.Block, iv []
return block, iv, nil
}
-// downloadSegment downloads a segments, decrypts it and names it after the given index
+// downloadSegment downloads a segment, decrypts it and names it after the given index
func (f *Format) downloadSegment(segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
// every segment is aes-128 encrypted and has to be decrypted when downloaded
content, err := decryptSegment(f.crunchy.Client, segment, block, iv)
@@ -200,7 +200,7 @@ func (f *Format) downloadSegment(segment *m3u8.MediaSegment, filename string, bl
return file, nil
}
-// mergeSegments reads every file in tempPath and write their content to output
+// mergeSegments reads every file in tempPath and writes their content to output
func (f *Format) mergeSegments(tempPath string, output *os.File) error {
dir, err := os.ReadDir(tempPath)
if err != nil {
diff --git a/utils/sort.go b/utils/sort.go
index 92bd35c..628af64 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -19,7 +19,7 @@ func (mlbd MovieListingsByDuration) Less(i, j int) bool {
return mlbd[i].DurationMS < mlbd[j].DurationMS
}
-// EpisodesByDuration episodes by their duration
+// EpisodesByDuration sorts episodes by their duration
type EpisodesByDuration []*crunchyroll.Episode
func (ebd EpisodesByDuration) Len() int {
@@ -32,7 +32,7 @@ func (ebd EpisodesByDuration) Less(i, j int) bool {
return ebd[i].DurationMS < ebd[j].DurationMS
}
-// FormatsByResolution sort formats after their resolution
+// FormatsByResolution sorts formats after their resolution
type FormatsByResolution []*crunchyroll.Format
func (fbr FormatsByResolution) Len() int {
From 58ce651134f16259cabb1cb606e1292f2ac60cbb Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Tue, 23 Nov 2021 09:29:49 +0100
Subject: [PATCH 002/732] Typo fix
---
crunchyroll-go.1 | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 99bcb5d..40c7f83 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -119,7 +119,7 @@ Download a episode with japanese audio and american subtitles.
$ crunchyroll-go download -a ja-JP -s en-US https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
.SH BUGS
-If you notice any bug or want a enhancement, feel free to create a new issue or pull request in the GitHub repository.
+If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
.SH AUTHOR
ByteDream
From 8587cd44acc78c5cb47bdca3bd20c511e9a38fe0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 1 Dec 2021 16:37:14 +0100
Subject: [PATCH 003/732] Added support for beta urls in
`EpisodeStructure.GetEpisodeByURL(...)`
---
utils/structure.go | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/utils/structure.go b/utils/structure.go
index 87928bf..127a379 100644
--- a/utils/structure.go
+++ b/utils/structure.go
@@ -501,6 +501,19 @@ func (es *EpisodeStructure) GetEpisodeByFormat(format *crunchyroll.Format) (*cru
func (es *EpisodeStructure) GetEpisodeByURL(url string) (*crunchyroll.Episode, error) {
_, title, episodeNumber, _, ok := crunchyroll.ParseEpisodeURL(url)
if !ok {
+ if episodeid, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
+ episodes, err := es.Episodes()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, episode := range episodes {
+ if episode.ID == episodeid {
+ return episode, nil
+ }
+ }
+ }
+
return nil, errors.New("invalid url")
}
From 0721ecee63dade7358c048a2f415f638ed35a382 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 1 Dec 2021 17:19:19 +0100
Subject: [PATCH 004/732] Updated url regex: minus sign in normal url; locale
in beta url
---
crunchyroll.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 189ad80..e47182a 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -357,7 +357,7 @@ func MatchEpisode(url string) (seriesName, title string, ok bool) {
// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
// the episode number will be 235
func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, webId int, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P\w+)-(?P\d+).*`)
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P.+)-(?P\d+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
groups := regexGroups(urlMatch, pattern.SubexpNames()...)
seriesName = groups["series"]
@@ -374,7 +374,7 @@ func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, w
// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season
func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/series/(?P\w+).*`)
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
groups := regexGroups(urlMatch, pattern.SubexpNames()...)
seasonId = groups["seasonId"]
@@ -385,7 +385,7 @@ func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode
func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/watch/(?P\w+).*`)
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
groups := regexGroups(urlMatch, pattern.SubexpNames()...)
episodeId = groups["episodeId"]
From 149f497d1c7db96e6433fffb7f1eb9e5ae31a9e9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 1 Dec 2021 17:22:11 +0100
Subject: [PATCH 005/732] Added support for beta urls
---
cmd/crunchyroll-go/cmd/download.go | 101 +++++++++++++++++++++++------
1 file changed, 80 insertions(+), 21 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 3b67236..0465f78 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -13,6 +13,7 @@ import (
"path"
"path/filepath"
"reflect"
+ "regexp"
"runtime"
"sort"
"strconv"
@@ -301,33 +302,32 @@ func download(urls []string) {
func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successes int) {
videoDupes := map[string]utils.VideoStructure{}
+ betaUrl := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com*`)
+
for i, url := range urls {
out.StartProgressf("Parsing url %d", i+1)
var localTotal, localSuccesses int
- var seriesName string
- var ok bool
- if seriesName, _, _, _, ok = crunchyroll.ParseEpisodeURL(url); !ok {
- seriesName, _ = crunchyroll.MatchVideo(url)
- }
+ var err error
+ var video utils.VideoStructure
+ var episode *crunchyroll.Episode
+ if betaUrl.MatchString(url) {
+ if episodeid, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
+ episode, err = crunchyroll.EpisodeFromID(crunchy, episodeid)
+ } else if seriesid, ok := crunchyroll.ParseBetaSeriesURL(url); ok {
+ var vid crunchyroll.Video
+ vid, err = crunchyroll.SeriesFromID(crunchy, seriesid)
- if seriesName != "" {
- dupe, ok := videoDupes[seriesName]
- if !ok {
- video, err := crunchy.FindVideo(fmt.Sprintf("https://www.crunchyroll.com/%s", seriesName))
- if err != nil {
- continue
- }
- switch video.(type) {
+ switch vid.(type) {
case *crunchyroll.Series:
seasons, err := video.(*crunchyroll.Series).Seasons()
if err != nil {
out.EndProgressf(false, "Failed to get seasons for url %s: %s\n", url, err)
continue
}
- dupe = utils.NewSeasonStructure(seasons).EpisodeStructure
- if err := dupe.(*utils.EpisodeStructure).InitAll(); err != nil {
+ video = utils.NewSeasonStructure(seasons).EpisodeStructure
+ if err := video.(*utils.EpisodeStructure).InitAll(); err != nil {
out.EndProgressf(false, "Failed to initialize series for url %s\n", url)
continue
}
@@ -337,23 +337,80 @@ func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successe
out.EndProgressf(false, "Failed to get movie listing for url %s\n", url)
continue
}
- dupe = utils.NewMovieListingStructure(movieListings)
- if err := dupe.(*utils.MovieListingStructure).InitAll(); err != nil {
+ video = utils.NewMovieListingStructure(movieListings)
+ if err := video.(*utils.MovieListingStructure).InitAll(); err != nil {
out.EndProgressf(false, "Failed to initialize movie for url %s\n", url)
continue
}
}
- videoDupes[seriesName] = dupe
+ }
+ } else {
+ var seriesName string
+ var ok bool
+ if seriesName, _, _, _, ok = crunchyroll.ParseEpisodeURL(url); !ok {
+ seriesName, ok = crunchyroll.MatchVideo(url)
}
+ if ok {
+ dupe, ok := videoDupes[seriesName]
+ if !ok {
+ var vid crunchyroll.Video
+ vid, err = crunchy.FindVideo(fmt.Sprintf("https://www.crunchyroll.com/%s", seriesName))
+
+ switch vid.(type) {
+ case *crunchyroll.Series:
+ seasons, err := vid.(*crunchyroll.Series).Seasons()
+ if err != nil {
+ out.EndProgressf(false, "Failed to get seasons for url %s: %s\n", url, err)
+ continue
+ }
+ dupe = utils.NewSeasonStructure(seasons).EpisodeStructure
+ if err := dupe.(*utils.EpisodeStructure).InitAll(); err != nil {
+ out.EndProgressf(false, "Failed to initialize series for url %s\n", url)
+ continue
+ }
+ case *crunchyroll.Movie:
+ movieListings, err := vid.(*crunchyroll.Movie).MovieListing()
+ if err != nil {
+ out.EndProgressf(false, "Failed to get movie listing for url %s\n", url)
+ continue
+ }
+ dupe = utils.NewMovieListingStructure(movieListings)
+ if err := dupe.(*utils.MovieListingStructure).InitAll(); err != nil {
+ out.EndProgressf(false, "Failed to initialize movie for url %s\n", url)
+ continue
+ }
+ }
+ }
+ video = dupe
+ } else {
+ err = fmt.Errorf("")
+ }
+ }
+
+ if err != nil {
+ out.EndProgressf(false, "URL %d seems to be invalid\n", i+1)
+ } else if episode != nil {
+ epstruct := utils.NewEpisodeStructure([]*crunchyroll.Episode{episode})
+
+ if err = epstruct.InitAll(); err != nil {
+ out.EndProgressf(false, "Could not init url %d, skipping\n", i+1)
+ } else if ep := parseEpisodes(epstruct, url); ep.Format != nil {
+ allEpisodes = append(allEpisodes, ep)
+ localSuccesses++
+ } else {
+ out.EndProgressf(false, "Could not parse url %d, skipping\n", i+1)
+ }
+ localTotal++
+ } else if video != nil {
if _, ok := crunchyroll.MatchVideo(url); ok {
out.Debugf("Parsed url %d as video\n", i+1)
var parsed []episodeInformation
- parsed, localTotal, localSuccesses = parseVideo(dupe, url)
+ parsed, localTotal, localSuccesses = parseVideo(video, url)
allEpisodes = append(allEpisodes, parsed...)
} else if _, _, _, _, ok = crunchyroll.ParseEpisodeURL(url); ok {
out.Debugf("Parsed url %d as episode\n", i+1)
- if episode := parseEpisodes(dupe.(*utils.EpisodeStructure), url); episode.Format != nil {
+ if episode := parseEpisodes(video.(*utils.EpisodeStructure), url); episode.Format != nil {
allEpisodes = append(allEpisodes, episode)
localSuccesses++
} else {
@@ -364,10 +421,12 @@ func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successe
out.EndProgressf(false, "Could not parse url %d, skipping\n", i+1)
continue
}
- out.EndProgressf(true, "Parsed url %d with %d successes and %d fails\n", i+1, localSuccesses, localTotal-localSuccesses)
} else {
out.EndProgressf(false, "URL %d seems to be invalid\n", i+1)
}
+
+ out.EndProgressf(true, "Parsed url %d with %d successes and %d fails\n", i+1, localSuccesses, localTotal-localSuccesses)
+
total += localTotal
successes += localSuccesses
}
From ccc598634c71087e25c315268bf04afa08b6d336 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 1 Dec 2021 17:35:18 +0100
Subject: [PATCH 006/732] Typo fix
---
episode.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/episode.go b/episode.go
index 4947a57..247967f 100644
--- a/episode.go
+++ b/episode.go
@@ -102,7 +102,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// AudioLocale returns the audio locale of the episode.
// Every episode in a season (should) have the same audio locale,
// so if you want to get the audio locale of a season, just call this method on the first episode of the season.
-// Otherwise, this function will cause massive heap on a season which many episodes
+// Otherwise, if you call this function on every episode it will cause a massive delay and redundant network
+// overload since it calls an api endpoint every time
func (e *Episode) AudioLocale() (LOCALE, error) {
resp, err := e.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
e.crunchy.Config.CountryCode,
From b5ade6066ee70fe8fa261cb1fa303e521bf44ed0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 1 Dec 2021 17:35:31 +0100
Subject: [PATCH 007/732] Added support for beta urls
---
Makefile | 4 +++-
README.md | 14 +++++++-------
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/Makefile b/Makefile
index f34a82f..561e38b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.2.2
+VERSION=1.2.3
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
@@ -29,4 +29,6 @@ release:
cd cmd/crunchyroll-go && GOOS=windows GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_windows.exe
cd cmd/crunchyroll-go && GOOS=darwin GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_darwin
+ strip cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_{linux,windows.exe}
+
mv cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_* .
diff --git a/README.md b/README.md
index b5b3132..ae7ccb2 100644
--- a/README.md
+++ b/README.md
@@ -38,21 +38,21 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
#### Get the executable
- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below
- - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.1/crunchy-v1.2.1_linux)
- - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.1/crunchy-v1.2.1_windows.exe)
- - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.1/crunchy-v1.2.1_darwin)
-- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/)
+ - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_linux)
+ - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_windows.exe)
+ - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_darwin)
+- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```
$ yay -S crunchyroll-go
```
- 🛠 Build it yourself
- - use `make` (requires `go` to be installed)
+ - use `make` (requires `go` to be installed):
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go
$ make && sudo make install
```
- - use `go`
+ - use `go`:
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go/cmd/crunchyroll-go
@@ -64,7 +64,7 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
#### Login
Before you can do something, you have to login first.
-This can be performed via crunchyroll account email and password
+This can be performed via crunchyroll account email and password.
```
$ crunchy login user@example.com password
```
From 8cfc279fb86a4d503be5924a520d8bf9891abcc7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 2 Dec 2021 22:20:45 +0100
Subject: [PATCH 008/732] Removed strip for windows binary in release rule
(#11)
---
Makefile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
index 561e38b..8a26748 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.2.3
+VERSION=1.2.4
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
@@ -29,6 +29,6 @@ release:
cd cmd/crunchyroll-go && GOOS=windows GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_windows.exe
cd cmd/crunchyroll-go && GOOS=darwin GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_darwin
- strip cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_{linux,windows.exe}
+ strip cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_linux
mv cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_* .
From deafe492477d06d980dbabfe2d326471a613d52d Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Wed, 8 Dec 2021 17:59:17 +0100
Subject: [PATCH 009/732] Fix latest download links
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index ae7ccb2..1ac4dbc 100644
--- a/README.md
+++ b/README.md
@@ -38,9 +38,9 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
#### Get the executable
- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below
- - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_linux)
- - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_windows.exe)
- - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.3/crunchy-v1.2.3_darwin)
+ - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_linux)
+ - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_windows.exe)
+ - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```
$ yay -S crunchyroll-go
From 65b2d4cc47b426542e73b733b270ae9b78db3d35 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Wed, 8 Dec 2021 18:02:02 +0100
Subject: [PATCH 010/732] Badge updates
---
README.md | 19 +++++++++++--------
1 file changed, 11 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index 1ac4dbc..5551ff1 100644
--- a/README.md
+++ b/README.md
@@ -6,16 +6,19 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
+
+
+
+
From 055ebe1910a5b826bad948f5b222198aa945c765 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 26 Dec 2021 12:58:31 +0100
Subject: [PATCH 011/732] Update README.md
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index 5551ff1..ad93756 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
+
+
+
From be281d64a7de527c59f1432e9a67630c71c6e140 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 9 Jan 2022 20:46:20 +0100
Subject: [PATCH 012/732] Updated direct download links using smartrelease :3
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index ad93756..82c2083 100644
--- a/README.md
+++ b/README.md
@@ -44,9 +44,9 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
#### Get the executable
- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below
- - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_linux)
- - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_windows.exe)
- - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.4/crunchy-v1.2.4_darwin)
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```
$ yay -S crunchyroll-go
From 1c42e526bdcf13efbb2fe19fb9efe6e4b97577ba Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 00:23:23 +0100
Subject: [PATCH 013/732] Added unexpected download stop notice
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 82c2083..66eb204 100644
--- a/README.md
+++ b/README.md
@@ -342,6 +342,8 @@ $ go test .
# 🗒️ Notice
+Sometimes the download stops without a reason on linux and does not go further. In this case the `tmpfs` / `/tmp` directory may be full. Execute `df /tmp` to see how much of the space is used.
+
I would really appreciate if someone rewrites the complete cli. I'm not satisfied with it's current structure but at the moment I have no time and no desire to do it myself.
# ⚖ License
From 87f57bacfca53393b93fede7d437ac90f5e75397 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 09:16:26 +0100
Subject: [PATCH 014/732] Renamed MatchVideo to ParseVideoURL
---
crunchyroll.go | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index e47182a..6eccde5 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -283,7 +283,7 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
// FindVideo finds a Video (Season or Movie) by a crunchyroll link
// e.g. https://www.crunchyroll.com/darling-in-the-franxx
func (c *Crunchyroll) FindVideo(seriesUrl string) (Video, error) {
- if series, ok := MatchVideo(seriesUrl); ok {
+ if series, ok := ParseVideoURL(seriesUrl); ok {
s, m, err := c.Search(series, 1)
if err != nil {
return nil, err
@@ -331,8 +331,16 @@ func (c *Crunchyroll) FindEpisode(url string) ([]*Episode, error) {
return nil, errors.New("invalid url")
}
-// MatchVideo tries to extract the crunchyroll series / movie name out of the given url
-func MatchVideo(url string) (seriesName string, ok bool) {
+// MatchEpisode tries to extract the crunchyroll series name and title out of the given url
+//
+// Deprecated: Use ParseEpisodeURL instead
+func MatchEpisode(url string) (seriesName, title string, ok bool) {
+ seriesName, title, _, _, ok = ParseEpisodeURL(url)
+ return
+}
+
+// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url
+func ParseVideoURL(url string) (seriesName string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/?$`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
groups := regexGroups(urlMatch, pattern.SubexpNames()...)
@@ -345,14 +353,6 @@ func MatchVideo(url string) (seriesName string, ok bool) {
return
}
-// MatchEpisode tries to extract the crunchyroll series name and title out of the given url
-//
-// Deprecated: Use ParseEpisodeURL instead
-func MatchEpisode(url string) (seriesName, title string, ok bool) {
- seriesName, title, _, _, ok = ParseEpisodeURL(url)
- return
-}
-
// ParseEpisodeURL tries to extract the crunchyroll series name, title, episode number and web id out of the given crunchyroll url
// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
// the episode number will be 235
From 6b2d7c6e275c56f89664a3a22f81e39f8c939cff Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 13:44:48 +0100
Subject: [PATCH 015/732] Removed FindVideo & FindEpisode; added
FindVideoByName & FindEpisodeByName
---
crunchyroll.go | 80 +++++++++++++++++++++++---------------------------
1 file changed, 37 insertions(+), 43 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 6eccde5..457df0d 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -280,55 +280,49 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
return s, m, nil
}
-// FindVideo finds a Video (Season or Movie) by a crunchyroll link
-// e.g. https://www.crunchyroll.com/darling-in-the-franxx
-func (c *Crunchyroll) FindVideo(seriesUrl string) (Video, error) {
- if series, ok := ParseVideoURL(seriesUrl); ok {
- s, m, err := c.Search(series, 1)
- if err != nil {
- return nil, err
- }
-
- if len(s) > 0 {
- return s[0], nil
- } else if len(m) > 0 {
- return m[0], nil
- }
- return nil, errors.New("no series or movie could be found")
+// FindVideoByName finds a Video (Season or Movie) by its name.
+// Use this in combination with ParseVideoURL and hand over the corresponding results
+// to this function.
+func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
+ s, m, err := c.Search(seriesName, 1)
+ if err != nil {
+ return nil, err
}
- return nil, errors.New("invalid url")
+ if len(s) > 0 {
+ return s[0], nil
+ } else if len(m) > 0 {
+ return m[0], nil
+ }
+ return nil, errors.New("no series or movie could be found")
}
-// FindEpisode finds an episode by its crunchyroll link
-// e.g. https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
-func (c *Crunchyroll) FindEpisode(url string) ([]*Episode, error) {
- if series, title, _, _, ok := ParseEpisodeURL(url); ok {
- video, err := c.FindVideo(fmt.Sprintf("https://www.crunchyroll.com/%s", series))
- if err != nil {
- return nil, err
- }
- seasons, err := video.(*Series).Seasons()
- if err != nil {
- return nil, err
- }
-
- var matchingEpisodes []*Episode
- for _, season := range seasons {
- episodes, err := season.Episodes()
- if err != nil {
- return nil, err
- }
- for _, episode := range episodes {
- if episode.SlugTitle == title {
- matchingEpisodes = append(matchingEpisodes, episode)
- }
- }
- }
- return matchingEpisodes, nil
+// FindEpisodeByName finds an episode by its crunchyroll series name and episode title.
+// Use this in combination with ParseEpisodeURL and hand over the corresponding results
+// to this function.
+func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Episode, error) {
+ video, err := c.FindVideoByName(seriesName)
+ if err != nil {
+ return nil, err
+ }
+ seasons, err := video.(*Series).Seasons()
+ if err != nil {
+ return nil, err
}
- return nil, errors.New("invalid url")
+ var matchingEpisodes []*Episode
+ for _, season := range seasons {
+ episodes, err := season.Episodes()
+ if err != nil {
+ return nil, err
+ }
+ for _, episode := range episodes {
+ if episode.SlugTitle == episodeTitle {
+ matchingEpisodes = append(matchingEpisodes, episode)
+ }
+ }
+ }
+ return matchingEpisodes, nil
}
// MatchEpisode tries to extract the crunchyroll series name and title out of the given url
From 08dd79ff478efd0e79e5b1e09f5aed27aa142c1e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 15:22:18 +0100
Subject: [PATCH 016/732] Removed structure utils api
---
utils/structure.go | 693 ---------------------------------------------
1 file changed, 693 deletions(-)
delete mode 100644 utils/structure.go
diff --git a/utils/structure.go b/utils/structure.go
deleted file mode 100644
index 127a379..0000000
--- a/utils/structure.go
+++ /dev/null
@@ -1,693 +0,0 @@
-package utils
-
-import (
- "errors"
- "github.com/ByteDream/crunchyroll-go"
- "sync"
-)
-
-// StructureError is the error type which is thrown whenever a structure fails
-// to receive information (formats, episodes, ...) from the api endpoint
-type StructureError struct {
- error
-}
-
-func IsStructureError(err error) (ok bool) {
- if err != nil {
- _, ok = err.(*StructureError)
- }
- return
-}
-
-// FormatStructure is the basic structure which every other structure implements.
-// With it, and all other structures the api usage can be simplified magnificent
-type FormatStructure struct {
- // initState is true if every format, stream, ... in the structure tree is initialized
- initState bool
-
- // getFunc specified the function which will be called if crunchyroll.Format is empty / not initialized yet.
- // It returns the formats itself, the parent streams (might be nil) and an error if one occurs
- getFunc func() ([]*crunchyroll.Format, []*crunchyroll.Stream, error)
- // formats holds all formats which were given
- formats []*crunchyroll.Format
- // parents holds all parents which were given
- parents []*crunchyroll.Stream
-}
-
-func newFormatStructure(parentStructure *StreamStructure) *FormatStructure {
- return &FormatStructure{
- getFunc: func() (formats []*crunchyroll.Format, parents []*crunchyroll.Stream, err error) {
- streams, err := parentStructure.Streams()
- if err != nil {
- return
- }
-
- var wg sync.WaitGroup
- var lock sync.Mutex
-
- for _, stream := range streams {
- wg.Add(1)
- stream := stream
- go func() {
- defer wg.Done()
- f, err := stream.Formats()
- if err != nil {
- errors.As(err, &StructureError{})
- return
- }
- lock.Lock()
- defer lock.Unlock()
- for _, format := range f {
- formats = append(formats, format)
- parents = append(parents, stream)
- }
- }()
- }
- wg.Wait()
- return
- },
- }
-}
-
-// NewFormatStructure returns a new FormatStructure, based on the given formats
-func NewFormatStructure(formats []*crunchyroll.Format) *FormatStructure {
- return &FormatStructure{
- getFunc: func() ([]*crunchyroll.Format, []*crunchyroll.Stream, error) {
- return formats, nil, nil
- },
- }
-}
-
-// Formats returns all stored formats
-func (fs *FormatStructure) Formats() ([]*crunchyroll.Format, error) {
- var err error
- if fs.formats == nil {
- if fs.formats, fs.parents, err = fs.getFunc(); err != nil {
- return nil, err
- }
- fs.initState = true
- }
- return fs.formats, nil
-}
-
-// FormatParent returns the parent stream of a format (if present).
-// If the format or parent is not stored, an error will be returned
-func (fs *FormatStructure) FormatParent(format *crunchyroll.Format) (*crunchyroll.Stream, error) {
- formats, err := fs.Formats()
- if err != nil {
- return nil, err
- }
-
- if fs.parents == nil {
- return nil, errors.New("no parents are given")
- }
-
- for i, f := range formats {
- if f == format {
- return fs.parents[i], nil
- }
- }
- return nil, errors.New("given format could not be found")
-}
-
-// InitAll recursive requests all given information.
-// All functions of FormatStructure or other structs in this file which are executed after this have a much lesser chance to return any error,
-// so the error return value of these functions can be pretty safely ignored.
-// This function should only be called if you need to the access to any function of FormatStructure which returns a crunchyroll.Format (or an array of it).
-// Re-calling this method can lead to heavy problems (believe me, it caused a simple bug and i've tried to fix it for several hours).
-// Check FormatStructure.InitAllState if you can call this method without causing bugs
-func (fs *FormatStructure) InitAll() error {
- var err error
- if fs.formats, fs.parents, err = fs.getFunc(); err != nil {
- return err
- }
- fs.initState = true
- return nil
-}
-
-// InitAllState returns FormatStructure.InitAll or FormatStructure.Formats was called.
-// If so, all errors which are returned by functions of structs in this file can be safely ignored
-func (fs *FormatStructure) InitAllState() bool {
- return fs.initState
-}
-
-// AvailableLocales returns all available audio, subtitle and hardsub locales for all formats.
-// If includeEmpty is given, locales with no value are included too
-func (fs *FormatStructure) AvailableLocales(includeEmpty bool) (audioLocales []crunchyroll.LOCALE, subtitleLocales []crunchyroll.LOCALE, hardsubLocales []crunchyroll.LOCALE, err error) {
- var formats []*crunchyroll.Format
- if formats, err = fs.Formats(); err != nil {
- return
- }
-
- audioMap := map[crunchyroll.LOCALE]interface{}{}
- subtitleMap := map[crunchyroll.LOCALE]interface{}{}
- hardsubMap := map[crunchyroll.LOCALE]interface{}{}
- for _, format := range formats {
- // audio locale should always have a valid locale
- if includeEmpty || !includeEmpty && format.AudioLocale != "" {
- audioMap[format.AudioLocale] = nil
- }
- if format.Subtitles != nil {
- for _, subtitle := range format.Subtitles {
- if subtitle.Locale == "" && !includeEmpty {
- continue
- }
- subtitleMap[subtitle.Locale] = nil
- }
- }
- if includeEmpty || !includeEmpty && format.Hardsub != "" {
- hardsubMap[format.Hardsub] = nil
- }
- }
-
- for k := range audioMap {
- audioLocales = append(audioLocales, k)
- }
- for k := range subtitleMap {
- subtitleLocales = append(subtitleLocales, k)
- }
- for k := range hardsubMap {
- hardsubLocales = append(hardsubLocales, k)
- }
- return
-}
-
-// FilterFormatsByAudio returns all formats which have the given locale as their audio locale
-func (fs *FormatStructure) FilterFormatsByAudio(locale crunchyroll.LOCALE) (f []*crunchyroll.Format, err error) {
- var formats []*crunchyroll.Format
- if formats, err = fs.Formats(); err != nil {
- return nil, err
- }
- for _, format := range formats {
- if format.AudioLocale == locale {
- f = append(f, format)
- }
- }
- return
-}
-
-// FilterFormatsBySubtitle returns all formats which have the given locale as their subtitle locale.
-// Hardsub indicates if the subtitle should be shown on the video itself
-func (fs *FormatStructure) FilterFormatsBySubtitle(locale crunchyroll.LOCALE, hardsub bool) (f []*crunchyroll.Format, err error) {
- var formats []*crunchyroll.Format
- if formats, err = fs.Formats(); err != nil {
- return nil, err
- }
- for _, format := range formats {
- if hardsub && format.Hardsub == locale {
- f = append(f, format)
- } else if !hardsub && format.Hardsub == "" {
- f = append(f, format)
- }
- }
- return
-}
-
-// FilterFormatsByLocales returns all formats which have the given locales as their property.
-// Hardsub is the same as in FormatStructure.FilterFormatsBySubtitle
-func (fs *FormatStructure) FilterFormatsByLocales(audioLocale, subtitleLocale crunchyroll.LOCALE, hardsub bool) ([]*crunchyroll.Format, error) {
- var f []*crunchyroll.Format
-
- formats, err := fs.Formats()
- if err != nil {
- return nil, err
- }
- for _, format := range formats {
- if format.AudioLocale == audioLocale {
- if hardsub && format.Hardsub == subtitleLocale {
- f = append(f, format)
- } else if !hardsub && format.Hardsub == "" {
- f = append(f, format)
- }
- }
- }
- if len(f) == 0 {
- return nil, errors.New("could not find any matching format")
- }
- return f, nil
-}
-
-// OrderFormatsByID loops through all stored formats and returns a 2d slice
-// where a row represents an id and the column all formats which have this id
-func (fs *FormatStructure) OrderFormatsByID() ([][]*crunchyroll.Format, error) {
- formats, err := fs.Formats()
- if err != nil {
- return nil, err
- }
-
- formatsMap := map[string][]*crunchyroll.Format{}
- for _, format := range formats {
- if _, ok := formatsMap[format.ID]; !ok {
- formatsMap[format.ID] = make([]*crunchyroll.Format, 0)
- }
- formatsMap[format.ID] = append(formatsMap[format.ID], format)
- }
-
- var orderedFormats [][]*crunchyroll.Format
- for _, v := range formatsMap {
- var f []*crunchyroll.Format
- for _, format := range v {
- f = append(f, format)
- }
- orderedFormats = append(orderedFormats, f)
- }
- return orderedFormats, nil
-}
-
-// StreamStructure fields are nearly same as FormatStructure
-type StreamStructure struct {
- *FormatStructure
-
- getFunc func() ([]*crunchyroll.Stream, []crunchyroll.Video, error)
- streams []*crunchyroll.Stream
- parents []crunchyroll.Video
-}
-
-func newStreamStructure(structure VideoStructure) *StreamStructure {
- var getFunc func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error)
- switch structure.(type) {
- case *EpisodeStructure:
- episodeStructure := structure.(*EpisodeStructure)
- getFunc = func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error) {
- episodes, err := episodeStructure.Episodes()
- if err != nil {
- return
- }
-
- var wg sync.WaitGroup
- var lock sync.Mutex
-
- for _, episode := range episodes {
- wg.Add(1)
- episode := episode
- go func() {
- defer wg.Done()
- s, err := episode.Streams()
- if err != nil {
- errors.As(err, &StructureError{})
- return
- }
- lock.Lock()
- defer lock.Unlock()
- for _, stream := range s {
- streams = append(streams, stream)
- parents = append(parents, episode)
- }
- }()
- }
- wg.Wait()
- return
- }
- case *MovieListingStructure:
- movieListingStructure := structure.(*MovieListingStructure)
- getFunc = func() (streams []*crunchyroll.Stream, parents []crunchyroll.Video, err error) {
- movieListings, err := movieListingStructure.MovieListings()
- if err != nil {
- return
- }
-
- var wg sync.WaitGroup
- var lock sync.Mutex
-
- for _, movieListing := range movieListings {
- wg.Add(1)
- movieListing := movieListing
- go func() {
- defer wg.Done()
- s, err := movieListing.Streams()
- if err != nil {
- errors.As(err, &StructureError{})
- return
- }
- lock.Lock()
- defer lock.Unlock()
- for _, stream := range s {
- streams = append(streams, stream)
- parents = append(parents, movieListing)
- }
- }()
- }
- wg.Wait()
- return
- }
- }
-
- ss := &StreamStructure{
- getFunc: getFunc,
- }
- ss.FormatStructure = newFormatStructure(ss)
- return ss
-}
-
-// NewStreamStructure returns a new StreamStructure, based on the given formats
-func NewStreamStructure(streams []*crunchyroll.Stream) *StreamStructure {
- ss := &StreamStructure{
- getFunc: func() ([]*crunchyroll.Stream, []crunchyroll.Video, error) {
- return streams, nil, nil
- },
- }
- ss.FormatStructure = newFormatStructure(ss)
- return ss
-}
-
-// Streams returns all stored streams
-func (ss *StreamStructure) Streams() ([]*crunchyroll.Stream, error) {
- if ss.streams == nil {
- var err error
- if ss.streams, ss.parents, err = ss.getFunc(); err != nil {
- return nil, err
- }
- }
- return ss.streams, nil
-}
-
-// StreamParent returns the parent video (type crunchyroll.Series or crunchyroll.Movie) of a stream (if present).
-// If the stream or parent is not stored, an error will be returned
-func (ss *StreamStructure) StreamParent(stream *crunchyroll.Stream) (crunchyroll.Video, error) {
- streams, err := ss.Streams()
- if err != nil {
- return nil, err
- }
-
- if ss.parents == nil {
- return nil, errors.New("no parents are given")
- }
-
- for i, s := range streams {
- if s == stream {
- return ss.parents[i], nil
- }
- }
- return nil, errors.New("given stream could not be found")
-}
-
-// VideoStructure is an interface which is implemented by EpisodeStructure and MovieListingStructure
-type VideoStructure interface{}
-
-// EpisodeStructure fields are nearly same as FormatStructure
-type EpisodeStructure struct {
- VideoStructure
- *StreamStructure
-
- getFunc func() ([]*crunchyroll.Episode, []*crunchyroll.Season, error)
- episodes []*crunchyroll.Episode
- parents []*crunchyroll.Season
-}
-
-func newEpisodeStructure(structure *SeasonStructure) *EpisodeStructure {
- es := &EpisodeStructure{
- getFunc: func() (episodes []*crunchyroll.Episode, parents []*crunchyroll.Season, err error) {
- seasons, err := structure.Seasons()
- if err != nil {
- return
- }
-
- var wg sync.WaitGroup
- var lock sync.Mutex
-
- for _, season := range seasons {
- wg.Add(1)
- season := season
- go func() {
- defer wg.Done()
- e, err := season.Episodes()
- if err != nil {
- errors.As(err, &StructureError{})
- return
- }
- lock.Lock()
- defer lock.Unlock()
- for _, episode := range e {
- episodes = append(episodes, episode)
- parents = append(parents, season)
- }
- }()
- }
- wg.Wait()
- return
- },
- }
- es.StreamStructure = newStreamStructure(es)
- return es
-}
-
-// NewEpisodeStructure returns a new EpisodeStructure, based on the given formats
-func NewEpisodeStructure(episodes []*crunchyroll.Episode) *EpisodeStructure {
- es := &EpisodeStructure{
- getFunc: func() ([]*crunchyroll.Episode, []*crunchyroll.Season, error) {
- return episodes, nil, nil
- },
- }
- es.StreamStructure = newStreamStructure(es)
- return es
-}
-
-// Episodes returns all stored episodes
-func (es *EpisodeStructure) Episodes() ([]*crunchyroll.Episode, error) {
- if es.episodes == nil {
- var err error
- if es.episodes, es.parents, err = es.getFunc(); err != nil {
- return nil, err
- }
- }
- return es.episodes, nil
-}
-
-// EpisodeParent returns the parent season of a stream (if present).
-// If the stream or parent is not stored, an error will be returned
-func (es *EpisodeStructure) EpisodeParent(episode *crunchyroll.Episode) (*crunchyroll.Season, error) {
- episodes, err := es.Episodes()
- if err != nil {
- return nil, err
- }
-
- if es.parents == nil {
- return nil, errors.New("no parents are given")
- }
-
- for i, e := range episodes {
- if e == episode {
- return es.parents[i], nil
- }
- }
- return nil, errors.New("given episode could not be found")
-}
-
-// GetEpisodeByFormat returns the episode to which the given format belongs to.
-// If the format or the parent is not stored, an error will be returned
-func (es *EpisodeStructure) GetEpisodeByFormat(format *crunchyroll.Format) (*crunchyroll.Episode, error) {
- if !es.initState {
- if err := es.InitAll(); err != nil {
- return nil, err
- }
- }
-
- formatParent, err := es.FormatParent(format)
- if err != nil {
- return nil, err
- }
- streamParent, err := es.StreamParent(formatParent)
- if err != nil {
- return nil, err
- }
- episode, ok := streamParent.(*crunchyroll.Episode)
- if !ok {
- return nil, errors.New("could not find parent episode")
- }
- return episode, nil
-}
-
-// GetEpisodeByURL returns an episode by its url
-func (es *EpisodeStructure) GetEpisodeByURL(url string) (*crunchyroll.Episode, error) {
- _, title, episodeNumber, _, ok := crunchyroll.ParseEpisodeURL(url)
- if !ok {
- if episodeid, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- episodes, err := es.Episodes()
- if err != nil {
- return nil, err
- }
-
- for _, episode := range episodes {
- if episode.ID == episodeid {
- return episode, nil
- }
- }
- }
-
- return nil, errors.New("invalid url")
- }
-
- episodes, err := es.Episodes()
- if err != nil {
- return nil, err
- }
-
- for _, episode := range episodes {
- if episode.SlugTitle == title {
- return episode, nil
- }
- }
-
- for _, episode := range episodes {
- if episode.EpisodeNumber == episodeNumber {
- return episode, nil
- }
- }
- return nil, errors.New("no episode could be found")
-}
-
-// OrderEpisodeByID orders episodes by their ids
-func (es *EpisodeStructure) OrderEpisodeByID() ([][]*crunchyroll.Episode, error) {
- episodes, err := es.Episodes()
- if err != nil {
- return nil, err
- }
-
- episodesMap := map[string][]*crunchyroll.Episode{}
- for _, episode := range episodes {
- if _, ok := episodesMap[episode.ID]; !ok {
- episodesMap[episode.ID] = make([]*crunchyroll.Episode, 0)
- }
- episodesMap[episode.ID] = append(episodesMap[episode.ID], episode)
- }
-
- var orderedEpisodes [][]*crunchyroll.Episode
- for _, v := range episodesMap {
- orderedEpisodes = append(orderedEpisodes, v)
- }
- return orderedEpisodes, nil
-}
-
-// OrderFormatsByEpisodeNumber orders episodes by their episode number.
-// Episode number 1 is on position 1 in the slice, number 2 on position 2, and so on.
-// This was made intentionally because there is a chance that episodes with the episode number 0 are existing
-// and position 0 in the slice is reserved for them.
-// Therefore, if the first episode number is, for example, 20, the first 19 array entries will be nil
-func (es *EpisodeStructure) OrderFormatsByEpisodeNumber() ([][]*crunchyroll.Format, error) {
- formats, err := es.Formats()
- if err != nil {
- return nil, err
- }
-
- formatsMap := map[int][]*crunchyroll.Format{}
- for _, format := range formats {
- stream, err := es.FormatParent(format)
- if err != nil {
- return nil, err
- }
- video, err := es.StreamParent(stream)
- if err != nil {
- return nil, err
- }
-
- episode, ok := video.(*crunchyroll.Episode)
- if !ok {
- continue
- }
- if _, ok := formatsMap[episode.EpisodeNumber]; !ok {
- formatsMap[episode.EpisodeNumber] = make([]*crunchyroll.Format, 0)
- }
- formatsMap[episode.EpisodeNumber] = append(formatsMap[episode.EpisodeNumber], format)
- }
-
- var highest int
- for key := range formatsMap {
- if key > highest {
- highest = key
- }
- }
-
- var orderedFormats [][]*crunchyroll.Format
- for i := 0; i < highest+1; i++ {
- if formats, ok := formatsMap[i]; ok {
- orderedFormats = append(orderedFormats, formats)
- } else {
- // simply adds nil in case that no episode with number i exists
- orderedFormats = append(orderedFormats, nil)
- }
- }
- return orderedFormats, nil
-}
-
-// SeasonStructure fields are nearly same as FormatStructure
-type SeasonStructure struct {
- *EpisodeStructure
-
- getFunc func() ([]*crunchyroll.Season, error)
- seasons []*crunchyroll.Season
-}
-
-// NewSeasonStructure returns a new SeasonStructure, based on the given formats
-func NewSeasonStructure(seasons []*crunchyroll.Season) *SeasonStructure {
- ss := &SeasonStructure{
- seasons: seasons,
- }
- ss.EpisodeStructure = newEpisodeStructure(ss)
- return ss
-}
-
-// Seasons returns all stored seasons
-func (ss *SeasonStructure) Seasons() ([]*crunchyroll.Season, error) {
- if ss.seasons == nil {
- var err error
- if ss.seasons, err = ss.getFunc(); err != nil {
- return nil, err
- }
- }
- return ss.seasons, nil
-}
-
-// MovieListingStructure fields are nearly same as FormatStructure
-type MovieListingStructure struct {
- VideoStructure
- *StreamStructure
-
- getFunc func() ([]*crunchyroll.MovieListing, error)
- movieListings []*crunchyroll.MovieListing
-}
-
-// NewMovieListingStructure returns a new MovieListingStructure, based on the given formats
-func NewMovieListingStructure(movieListings []*crunchyroll.MovieListing) *MovieListingStructure {
- ml := &MovieListingStructure{
- getFunc: func() ([]*crunchyroll.MovieListing, error) {
- return movieListings, nil
- },
- }
- ml.StreamStructure = newStreamStructure(ml)
- return ml
-}
-
-// MovieListings returns all stored movie listings
-func (ml *MovieListingStructure) MovieListings() ([]*crunchyroll.MovieListing, error) {
- if ml.movieListings == nil {
- var err error
- if ml.movieListings, err = ml.getFunc(); err != nil {
- return nil, err
- }
- }
- return ml.movieListings, nil
-}
-
-// GetMovieListingByFormat returns the movie listing to which the given format belongs to.
-// If the format or the parent is not stored, an error will be returned
-func (ml *MovieListingStructure) GetMovieListingByFormat(format *crunchyroll.Format) (*crunchyroll.MovieListing, error) {
- if !ml.initState {
- if err := ml.InitAll(); err != nil {
- return nil, err
- }
- }
-
- formatParent, err := ml.FormatParent(format)
- if err != nil {
- return nil, err
- }
- streamParent, err := ml.StreamParent(formatParent)
- if err != nil {
- return nil, err
- }
- movieListing, ok := streamParent.(*crunchyroll.MovieListing)
- if !ok {
- return nil, errors.New("could not find parent movie listing")
- }
- return movieListing, nil
-}
From d1235fea1af060a2127847b1d8f0993850e4e5bc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 15:53:28 +0100
Subject: [PATCH 017/732] Added walk utils api
---
utils/walk.go | 210 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 210 insertions(+)
create mode 100644 utils/walk.go
diff --git a/utils/walk.go b/utils/walk.go
new file mode 100644
index 0000000..4eb9ff3
--- /dev/null
+++ b/utils/walk.go
@@ -0,0 +1,210 @@
+package utils
+
+import (
+ "fmt"
+ "github.com/ByteDream/crunchyroll-go"
+)
+
+// EpisodeWalker is an easy-to-use struct which walks down given urls
+// and triggers functions where you can perform further actions with
+// the delivered objects
+type EpisodeWalker struct {
+ // The Crunchyroll instance to perform all actions on
+ Crunchyroll *crunchyroll.Crunchyroll
+
+ // If CheckDuplicates is true, duplicated urls, seasons and episodes
+ // are filtered out and the values given in OnSeason for example is
+ // always unique
+ CheckDuplicates bool
+
+ // OnUrl gets called when an url is parsed.
+ // The error is generally only nil when the url is invalid
+ OnUrl func(url string, err error) error
+ // OnSeason gets called when a season was parsed
+ OnSeason func(season *crunchyroll.Season, err error) error
+ // OnEpisode gets called when a season was parsed
+ OnEpisode func(episode *crunchyroll.Episode, err error) error
+}
+
+// WalkURLs walks through all urls.
+// Urls to seasons and episodes are support, normal as well as beta urls
+func (ew EpisodeWalker) WalkURLs(urls []string) error {
+ var episodeIds, seasonIds, seriesNames []string
+ var episodeNames map[string][]string
+
+ for _, url := range urls {
+ if episodeId, ok := crunchyroll.ParseBetaEpisodeURL(url); ok && !(ew.CheckDuplicates && sliceContains(episodeIds, episodeId)) {
+ episodeIds = append(episodeIds, episodeId)
+ } else if seasonId, ok := crunchyroll.ParseBetaSeriesURL(url); ok && !(ew.CheckDuplicates && sliceContains(seasonIds, seasonId)) {
+ seasonIds = append(seasonIds, seasonId)
+ } else if seriesName, title, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ if eps, ok := episodeNames[seriesName]; ok {
+ if !ew.CheckDuplicates || !sliceContains(eps, title) {
+ eps = append(eps, title)
+ }
+ } else {
+ episodeNames[seriesName] = []string{title}
+ }
+ } else if seriesName, ok := crunchyroll.ParseVideoURL(url); ok && !(ew.CheckDuplicates && sliceContains(seriesNames, seriesName)) {
+ seriesNames = append(seriesNames, seriesName)
+ } else {
+ err := fmt.Errorf("invalid url %s", url)
+ if ew.OnUrl != nil {
+ if err = ew.OnUrl(url, err); err != nil {
+ return err
+ }
+ continue
+ } else {
+ return err
+ }
+ }
+
+ if ew.OnUrl != nil {
+ if err := ew.OnUrl(url, nil); err != nil {
+ return err
+ }
+ }
+ }
+
+ for _, name := range seriesNames {
+ video, err := ew.Crunchyroll.FindVideoByName(name)
+ if err != nil {
+ return err
+ }
+ // in all cases i've ever tested video was a series - even
+ // if it was listed as a movie on the crunchyroll website.
+ // i just hope no error occurs here :)
+ seasons, err := video.(*crunchyroll.Series).Seasons()
+ if err != nil {
+ return err
+ }
+ for _, season := range seasons {
+ if ew.CheckDuplicates {
+ if sliceContains(seasonIds, season.ID) {
+ continue
+ }
+ seasonIds = append(seasonIds, season.ID)
+ }
+ if ew.OnSeason != nil {
+ if err := ew.OnSeason(season, nil); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ if err := ew.walkEpisodeIds(episodeIds); err != nil {
+ return err
+ } else if err := ew.walkSeasonIds(seasonIds); err != nil {
+ return err
+ } else if err := ew.walkEpisodeNames(episodeNames); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (ew EpisodeWalker) walkEpisodeIds(episodeIds []string) error {
+ var episodeIdsCheck []string
+
+ for _, id := range episodeIds {
+ if ew.CheckDuplicates {
+ if sliceContains(episodeIdsCheck, id) {
+ continue
+ }
+ episodeIdsCheck = append(episodeIdsCheck, id)
+ }
+
+ episode, err := crunchyroll.EpisodeFromID(ew.Crunchyroll, id)
+ if ew.OnEpisode != nil {
+ if err = ew.OnEpisode(episode, err); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (ew EpisodeWalker) walkSeasonIds(seasonIds []string) error {
+ var episodeIdsCheck []string
+
+ for _, id := range seasonIds {
+ season, err := crunchyroll.SeasonFromID(ew.Crunchyroll, id)
+ if ew.OnSeason != nil {
+ if err = ew.OnSeason(season, err); err != nil {
+ return err
+ }
+ } else if err != nil {
+ return err
+ }
+ eps, err := season.Episodes()
+ if err != nil {
+ return err
+ }
+ for _, ep := range eps {
+ if ew.CheckDuplicates {
+ if sliceContains(episodeIdsCheck, ep.ID) {
+ continue
+ }
+ episodeIdsCheck = append(episodeIdsCheck, ep.ID)
+ }
+
+ if ew.OnEpisode != nil {
+ if err = ew.OnEpisode(ep, nil); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (ew EpisodeWalker) walkEpisodeNames(episodeNames map[string][]string) error {
+ var episodeIdsCheck []string
+
+ for seriesName, episodeName := range episodeNames {
+ video, err := ew.Crunchyroll.FindVideoByName(seriesName)
+ if err != nil {
+ return err
+ }
+ series := video.(*crunchyroll.Series)
+ seasons, err := series.Seasons()
+ if err != nil {
+ return err
+ }
+ for _, season := range seasons {
+ eps, err := season.Episodes()
+ if err != nil {
+ return err
+ }
+ for _, ep := range eps {
+ for _, name := range episodeName {
+ if ep.SlugTitle == name {
+ if ew.OnEpisode != nil {
+ if ew.CheckDuplicates {
+ if sliceContains(episodeIdsCheck, ep.ID) {
+ continue
+ }
+ episodeIdsCheck = append(episodeIdsCheck, ep.ID)
+ }
+
+ if err = ew.OnEpisode(ep, nil); err != nil {
+ return err
+ }
+ }
+ break
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func sliceContains(slice []string, elem string) bool {
+ for _, s := range slice {
+ if elem == s {
+ return true
+ }
+ }
+ return false
+}
From 30e86631865df6f3989add87a2d6ee378637f1ce Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 16:20:05 +0100
Subject: [PATCH 018/732] Added new api fields
---
episode.go | 15 +++++++++++----
season.go | 44 +++++++++++++++++++++++++++-----------------
2 files changed, 38 insertions(+), 21 deletions(-)
diff --git a/episode.go b/episode.go
index 247967f..712f8cb 100644
--- a/episode.go
+++ b/episode.go
@@ -12,10 +12,17 @@ type Episode struct {
siteCache map[string]interface{}
- ID string `json:"id"`
- SeriesID string `json:"series_id"`
- SeriesTitle string `json:"series_title"`
- SeasonNumber int `json:"season_number"`
+ ID string `json:"id"`
+ ChannelID string `json:"channel_id"`
+
+ SeriesID string `json:"series_id"`
+ SeriesTitle string `json:"series_title"`
+ SeriesSlugTitle string `json:"series_slug_title"`
+
+ SeasonID string `json:"season_id"`
+ SeasonTitle string `json:"season_title"`
+ SeasonSlugTitle string `json:"season_slug_title"`
+ SeasonNumber int `json:"season_number"`
Episode string `json:"episode"`
EpisodeNumber int `json:"episode_number"`
diff --git a/season.go b/season.go
index 0ec0802..9021af7 100644
--- a/season.go
+++ b/season.go
@@ -9,24 +9,34 @@ import (
type Season struct {
crunchy *Crunchyroll
- ID string `json:"id"`
- Title string `json:"title"`
- SlugTitle string `json:"slug_title"`
- SeriesID string `json:"series_id"`
- SeasonNumber int `json:"season_number"`
- IsComplete bool `json:"is_complete"`
- Description string `json:"description"`
- Keywords []string `json:"keywords"`
- SeasonTags []string `json:"season_tags"`
- IsMature bool `json:"is_mature"`
- MatureBlocked bool `json:"mature_blocked"`
- IsSubbed bool `json:"is_subbed"`
- IsDubbed bool `json:"is_dubbed"`
- IsSimulcast bool `json:"is_simulcast"`
- SeoTitle string `json:"seo_title"`
- SeoDescription string `json:"seo_description"`
+ ID string `json:"id"`
+ ChannelID string `json:"channel_id"`
- Language LOCALE
+ Title string `json:"title"`
+ SlugTitle string `json:"slug_title"`
+
+ SeriesID string `json:"series_id"`
+ SeasonNumber int `json:"season_number"`
+
+ IsComplete bool `json:"is_complete"`
+
+ Description string `json:"description"`
+ Keywords []string `json:"keywords"`
+ SeasonTags []string `json:"season_tags"`
+ IsMature bool `json:"is_mature"`
+ MatureBlocked bool `json:"mature_blocked"`
+ IsSubbed bool `json:"is_subbed"`
+ IsDubbed bool `json:"is_dubbed"`
+ IsSimulcast bool `json:"is_simulcast"`
+
+ SeoTitle string `json:"seo_title"`
+ SeoDescription string `json:"seo_description"`
+
+ AvailabilityNotes string `json:"availability_notes"`
+
+ // the locales are always empty, idk why this may change in the future
+ AudioLocales []LOCALE
+ SubtitleLocales []LOCALE
}
// SeasonFromID returns a season by its api id
From effd160dc3895ad4bccf6ad3f4d2dc37fb223009 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 17 Jan 2022 19:40:33 +0100
Subject: [PATCH 019/732] Moved logger to separate file
---
cmd/crunchyroll-go/cmd/logger.go | 199 +++++++++++++++++++++++++++++++
cmd/crunchyroll-go/cmd/utils.go | 190 -----------------------------
2 files changed, 199 insertions(+), 190 deletions(-)
create mode 100644 cmd/crunchyroll-go/cmd/logger.go
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
new file mode 100644
index 0000000..1079dc6
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -0,0 +1,199 @@
+package cmd
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "runtime"
+ "strings"
+ "sync"
+ "time"
+)
+
+type progress struct {
+ status bool
+ message string
+}
+
+type logger struct {
+ DebugLog *log.Logger
+ InfoLog *log.Logger
+ ErrLog *log.Logger
+
+ devView bool
+
+ progressWG sync.Mutex
+ progress chan progress
+}
+
+func newLogger(debug, info, err bool, color bool) *logger {
+ debugLog, infoLog, errLog := log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0)
+
+ debugColor, infoColor, errColor := "", "", ""
+ if color && runtime.GOOS != "windows" {
+ debugColor, infoColor, errColor = "\033[95m", "\033[96m", "\033[31m"
+ }
+
+ if debug {
+ debugLog.SetOutput(&loggerWriter{original: os.Stdout, color: debugColor})
+ }
+ if info {
+ infoLog.SetOutput(&loggerWriter{original: os.Stdout, color: infoColor})
+ }
+ if err {
+ errLog.SetOutput(&loggerWriter{original: os.Stdout, color: errColor})
+ }
+
+ if debug {
+ debugLog = log.New(debugLog.Writer(), "[debug] ", 0)
+ infoLog = log.New(infoLog.Writer(), "[info] ", 0)
+ errLog = log.New(errLog.Writer(), "[err] ", 0)
+ }
+
+ return &logger{
+ DebugLog: debugLog,
+ InfoLog: infoLog,
+ ErrLog: errLog,
+
+ devView: debug,
+ }
+}
+
+func (l *logger) Empty() {
+ if !l.devView && l.InfoLog.Writer() != io.Discard {
+ fmt.Println()
+ }
+}
+
+func (l *logger) StartProgress(message string) {
+ if l.devView {
+ l.InfoLog.Println(message)
+ return
+ }
+ l.progress = make(chan progress)
+
+ go func() {
+ states := []string{"-", "\\", "|", "/"}
+ for i := 0; ; i++ {
+ l.progressWG.Lock()
+ select {
+ case p := <-l.progress:
+ // clearing the last line
+ fmt.Printf("\r%s\r", strings.Repeat(" ", len(l.InfoLog.Prefix())+len(message)+2))
+ if p.status {
+ successTag := "✔"
+ if runtime.GOOS == "windows" {
+ successTag = "~"
+ }
+ l.InfoLog.Printf("%s %s", successTag, p.message)
+ } else {
+ errorTag := "✘"
+ if runtime.GOOS == "windows" {
+ errorTag = "!"
+ }
+ l.ErrLog.Printf("%s %s", errorTag, p.message)
+ }
+ l.progress = nil
+ l.progressWG.Unlock()
+ return
+ default:
+ if i%10 == 0 {
+ fmt.Printf("\r%s%s %s", l.InfoLog.Prefix(), states[i/10%4], message)
+ }
+ time.Sleep(35 * time.Millisecond)
+ l.progressWG.Unlock()
+ }
+ }
+ }()
+}
+
+func (l *logger) StartProgressf(message string, a ...interface{}) {
+ l.StartProgress(fmt.Sprintf(message, a...))
+}
+
+func (l *logger) EndProgress(successful bool, message string) {
+ if l.devView {
+ if successful {
+ l.InfoLog.Print(message)
+ } else {
+ l.ErrLog.Print(message)
+ }
+ return
+ } else if l.progress != nil {
+ l.progress <- progress{
+ status: successful,
+ message: message,
+ }
+ }
+}
+
+func (l *logger) EndProgressf(successful bool, message string, a ...interface{}) {
+ l.EndProgress(successful, fmt.Sprintf(message, a...))
+}
+
+func (l *logger) Debugln(v ...interface{}) {
+ l.print(0, v...)
+}
+
+func (l *logger) Debugf(message string, a ...interface{}) {
+ l.print(0, fmt.Sprintf(message, a...))
+}
+
+func (l *logger) Infoln(v ...interface{}) {
+ l.print(1, v...)
+}
+
+func (l *logger) Infof(message string, a ...interface{}) {
+ l.print(1, fmt.Sprintf(message, a...))
+}
+
+func (l *logger) Errln(v ...interface{}) {
+ l.print(2, v...)
+}
+
+func (l *logger) Errf(message string, a ...interface{}) {
+ l.print(2, fmt.Sprintf(message, a...))
+}
+
+func (l *logger) Fatalln(v ...interface{}) {
+ l.print(2, v...)
+ os.Exit(1)
+}
+
+func (l *logger) Fatalf(message string, a ...interface{}) {
+ l.print(2, fmt.Sprintf(message, a...))
+ os.Exit(1)
+}
+
+func (l *logger) print(level int, v ...interface{}) {
+ if l.progress != nil {
+ l.progressWG.Lock()
+ defer l.progressWG.Unlock()
+ fmt.Print("\r")
+ }
+
+ switch level {
+ case 0:
+ l.DebugLog.Print(v...)
+ case 1:
+ l.InfoLog.Print(v...)
+ case 2:
+ l.ErrLog.Print(v...)
+ }
+}
+
+type loggerWriter struct {
+ io.Writer
+
+ original io.Writer
+ color string
+}
+
+func (lw *loggerWriter) Write(p []byte) (n int, err error) {
+ if lw.color != "" {
+ p = append([]byte(lw.color), p...)
+ p = append(p, []byte("\033[0m")...)
+ }
+ return lw.original.Write(p)
+}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 5f799ca..3c357f9 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -4,9 +4,7 @@ import (
"fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/ByteDream/crunchyroll-go/utils"
- "io"
"io/ioutil"
- "log"
"net/http"
"net/url"
"os"
@@ -16,199 +14,11 @@ import (
"runtime"
"strconv"
"strings"
- "sync"
"time"
)
var sessionIDPath = filepath.Join(os.TempDir(), ".crunchy")
-type progress struct {
- status bool
- message string
-}
-
-type logger struct {
- DebugLog *log.Logger
- InfoLog *log.Logger
- ErrLog *log.Logger
-
- devView bool
-
- progressWG sync.Mutex
- progress chan progress
-}
-
-func newLogger(debug, info, err bool, color bool) *logger {
- debugLog, infoLog, errLog := log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0)
-
- debugColor, infoColor, errColor := "", "", ""
- if color && runtime.GOOS != "windows" {
- debugColor, infoColor, errColor = "\033[95m", "\033[96m", "\033[31m"
- }
-
- if debug {
- debugLog.SetOutput(&loggerWriter{original: os.Stdout, color: debugColor})
- }
- if info {
- infoLog.SetOutput(&loggerWriter{original: os.Stdout, color: infoColor})
- }
- if err {
- errLog.SetOutput(&loggerWriter{original: os.Stdout, color: errColor})
- }
-
- if debug {
- debugLog = log.New(debugLog.Writer(), "[debug] ", 0)
- infoLog = log.New(infoLog.Writer(), "[info] ", 0)
- errLog = log.New(errLog.Writer(), "[err] ", 0)
- }
-
- return &logger{
- DebugLog: debugLog,
- InfoLog: infoLog,
- ErrLog: errLog,
-
- devView: debug,
- }
-}
-
-func (l *logger) Empty() {
- if !l.devView && l.InfoLog.Writer() != io.Discard {
- fmt.Println()
- }
-}
-
-func (l *logger) StartProgress(message string) {
- if l.devView {
- l.InfoLog.Println(message)
- return
- }
- l.progress = make(chan progress)
-
- go func() {
- states := []string{"-", "\\", "|", "/"}
- for i := 0; ; i++ {
- l.progressWG.Lock()
- select {
- case p := <-l.progress:
- // clearing the last line
- fmt.Printf("\r%s\r", strings.Repeat(" ", len(l.InfoLog.Prefix())+len(message)+2))
- if p.status {
- successTag := "✔"
- if runtime.GOOS == "windows" {
- successTag = "~"
- }
- l.InfoLog.Printf("%s %s", successTag, p.message)
- } else {
- errorTag := "✘"
- if runtime.GOOS == "windows" {
- errorTag = "!"
- }
- l.ErrLog.Printf("%s %s", errorTag, p.message)
- }
- l.progress = nil
- l.progressWG.Unlock()
- return
- default:
- if i%10 == 0 {
- fmt.Printf("\r%s%s %s", l.InfoLog.Prefix(), states[i/10%4], message)
- }
- time.Sleep(35 * time.Millisecond)
- l.progressWG.Unlock()
- }
- }
- }()
-}
-
-func (l *logger) StartProgressf(message string, a ...interface{}) {
- l.StartProgress(fmt.Sprintf(message, a...))
-}
-
-func (l *logger) EndProgress(successful bool, message string) {
- if l.devView {
- if successful {
- l.InfoLog.Print(message)
- } else {
- l.ErrLog.Print(message)
- }
- return
- } else if l.progress != nil {
- l.progress <- progress{
- status: successful,
- message: message,
- }
- }
-}
-
-func (l *logger) EndProgressf(successful bool, message string, a ...interface{}) {
- l.EndProgress(successful, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Debugln(v ...interface{}) {
- l.print(0, v...)
-}
-
-func (l *logger) Debugf(message string, a ...interface{}) {
- l.print(0, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Infoln(v ...interface{}) {
- l.print(1, v...)
-}
-
-func (l *logger) Infof(message string, a ...interface{}) {
- l.print(1, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Errln(v ...interface{}) {
- l.print(2, v...)
-}
-
-func (l *logger) Errf(message string, a ...interface{}) {
- l.print(2, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Fatalln(v ...interface{}) {
- l.print(2, v...)
- os.Exit(1)
-}
-
-func (l *logger) Fatalf(message string, a ...interface{}) {
- l.print(2, fmt.Sprintf(message, a...))
- os.Exit(1)
-}
-
-func (l *logger) print(level int, v ...interface{}) {
- if l.progress != nil {
- l.progressWG.Lock()
- defer l.progressWG.Unlock()
- fmt.Print("\r")
- }
-
- switch level {
- case 0:
- l.DebugLog.Print(v...)
- case 1:
- l.InfoLog.Print(v...)
- case 2:
- l.ErrLog.Print(v...)
- }
-}
-
-type loggerWriter struct {
- io.Writer
-
- original io.Writer
- color string
-}
-
-func (lw *loggerWriter) Write(p []byte) (n int, err error) {
- if lw.color != "" {
- p = append([]byte(lw.color), p...)
- p = append(p, []byte("\033[0m")...)
- }
- return lw.original.Write(p)
-}
-
// systemLocale receives the system locale
// https://stackoverflow.com/questions/51829386/golang-get-system-language/51831590#51831590
func systemLocale() crunchyroll.LOCALE {
From 674b0e0de3b5482722a08be72b83bf42e39741ef Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 18 Jan 2022 23:02:01 +0100
Subject: [PATCH 020/732] Added development notice
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index 66eb204..b19ab6f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+**This branch is under highly development, so it may contain errors which are making compiling not possible**
+
# crunchyroll-go
A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api.
From 21619fe33301d07d21262e29f24d5c2fc5c62450 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 18 Jan 2022 23:46:07 +0100
Subject: [PATCH 021/732] Removed deprecated functions
---
cmd/crunchyroll-go/cmd/login.go | 6 +++---
crunchyroll.go | 8 --------
format.go | 10 ----------
3 files changed, 3 insertions(+), 21 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index fb4b85c..c2f07e0 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -30,7 +30,7 @@ func init() {
}
func loginCredentials(email, password string) error {
- out.Debugln("Logging in via credentials")
+ out.Debug("Logging in via credentials")
session, err := crunchyroll.LoginWithCredentials(email, password, locale, client)
if err != nil {
return err
@@ -40,11 +40,11 @@ func loginCredentials(email, password string) error {
func loginSessionID(sessionID string, alreadyChecked bool) error {
if !alreadyChecked {
- out.Debugln("Logging in via session id")
+ out.Debug("Logging in via session id")
if _, err := crunchyroll.LoginWithSessionID(sessionID, locale, client); err != nil {
return err
}
}
- out.Infoln("Due to security reasons, you have to login again on the next reboot")
+ out.Info("Due to security reasons, you have to login again on the next reboot")
return ioutil.WriteFile(sessionIDPath, []byte(sessionID), 0777)
}
diff --git a/crunchyroll.go b/crunchyroll.go
index 457df0d..3d5adf4 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -325,14 +325,6 @@ func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Epi
return matchingEpisodes, nil
}
-// MatchEpisode tries to extract the crunchyroll series name and title out of the given url
-//
-// Deprecated: Use ParseEpisodeURL instead
-func MatchEpisode(url string) (seriesName, title string, ok bool) {
- seriesName, title, _, _, ok = ParseEpisodeURL(url)
- return
-}
-
// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url
func ParseVideoURL(url string) (seriesName string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/?$`)
diff --git a/format.go b/format.go
index 7fbdb58..fb388e1 100644
--- a/format.go
+++ b/format.go
@@ -37,16 +37,6 @@ type Format struct {
Subtitles []*Subtitle
}
-// Download calls DownloadGoroutines with 4 goroutines.
-// See DownloadGoroutines for more details
-//
-// Deprecated: Use DownloadGoroutines instead
-func (f *Format) Download(output *os.File, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File, err error) error) error {
- return f.DownloadGoroutines(output, 4, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- return onSegmentDownload(segment, current, total, file, nil)
- })
-}
-
// DownloadGoroutines downloads the format to the given output file (as .ts file).
// See Format.DownloadSegments for more information
func (f *Format) DownloadGoroutines(output *os.File, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
From 0e8738a3044468041dc2bbd1a837c6a90914eb7a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Feb 2022 13:25:38 +0100
Subject: [PATCH 022/732] Fixed nil map error
---
utils/walk.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/utils/walk.go b/utils/walk.go
index 4eb9ff3..f5e96d3 100644
--- a/utils/walk.go
+++ b/utils/walk.go
@@ -30,7 +30,7 @@ type EpisodeWalker struct {
// Urls to seasons and episodes are support, normal as well as beta urls
func (ew EpisodeWalker) WalkURLs(urls []string) error {
var episodeIds, seasonIds, seriesNames []string
- var episodeNames map[string][]string
+ episodeNames := make(map[string][]string)
for _, url := range urls {
if episodeId, ok := crunchyroll.ParseBetaEpisodeURL(url); ok && !(ew.CheckDuplicates && sliceContains(episodeIds, episodeId)) {
From a5d9696c9cc50ea0be0c84085939f69f572f9da3 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 7 Feb 2022 14:07:35 +0100
Subject: [PATCH 023/732] Added caching
---
crunchyroll.go | 18 ++++++++++++++++++
episode.go | 16 +++++++++++++++-
season.go | 9 +++++++++
stream.go | 10 ++++++++++
video.go | 20 ++++++++++++++++++++
5 files changed, 72 insertions(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 3d5adf4..8c402d6 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -51,6 +51,9 @@ type Crunchyroll struct {
ExternalID string
MaturityRating string
}
+
+ // If cache is true, internal caching is enabled
+ cache bool
}
// LoginWithCredentials logs in via crunchyroll email and password
@@ -86,6 +89,7 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
Client: client,
Locale: locale,
SessionID: sessionID,
+ cache: true,
}
var endpoint string
var err error
@@ -231,6 +235,20 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
return resp, err
}
+// IsCaching returns if data gets cached or not.
+// See SetCaching for more information
+func (c *Crunchyroll) IsCaching() bool {
+ return c.cache
+}
+
+// SetCaching enables or disables internal caching of requests made.
+// Caching is enabled by default.
+// If it is disabled the already cached data still gets called.
+// The best way to prevent this is to create a complete new Crunchyroll struct
+func (c *Crunchyroll) SetCaching(caching bool) {
+ c.cache = caching
+}
+
// Search searches a query and returns all found series and movies within the given limit
func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
diff --git a/episode.go b/episode.go
index 712f8cb..36a8c34 100644
--- a/episode.go
+++ b/episode.go
@@ -10,6 +10,8 @@ import (
type Episode struct {
crunchy *Crunchyroll
+ children []*Stream
+
siteCache map[string]interface{}
ID string `json:"id"`
@@ -134,7 +136,11 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
// Streams returns all streams which are available for the episode
func (e *Episode) Streams() ([]*Stream, error) {
- return fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ if e.children != nil {
+ return e.children, nil
+ }
+
+ streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
e.crunchy.Config.CountryCode,
e.crunchy.Config.MaturityRating,
e.crunchy.Config.Channel,
@@ -143,4 +149,12 @@ func (e *Episode) Streams() ([]*Stream, error) {
e.crunchy.Config.Signature,
e.crunchy.Config.Policy,
e.crunchy.Config.KeyPairID))
+ if err != nil {
+ return nil, err
+ }
+
+ if e.crunchy.cache {
+ e.children = streams
+ }
+ return streams, nil
}
diff --git a/season.go b/season.go
index 9021af7..16bc6d6 100644
--- a/season.go
+++ b/season.go
@@ -9,6 +9,8 @@ import (
type Season struct {
crunchy *Crunchyroll
+ children []*Episode
+
ID string `json:"id"`
ChannelID string `json:"channel_id"`
@@ -69,6 +71,10 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
// Episodes returns all episodes which are available for the season
func (s *Season) Episodes() (episodes []*Episode, err error) {
+ if s.children != nil {
+ return s.children, nil
+ }
+
resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
s.crunchy.Config.CountryCode,
s.crunchy.Config.MaturityRating,
@@ -101,5 +107,8 @@ func (s *Season) Episodes() (episodes []*Episode, err error) {
episodes = append(episodes, episode)
}
+ if s.crunchy.cache {
+ s.children = episodes
+ }
return
}
diff --git a/stream.go b/stream.go
index 59ab3c9..5fe0f96 100644
--- a/stream.go
+++ b/stream.go
@@ -11,6 +11,8 @@ import (
type Stream struct {
crunchy *Crunchyroll
+ children []*Format
+
HardsubLocale LOCALE
AudioLocale LOCALE
Subtitles []*Subtitle
@@ -35,6 +37,10 @@ func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
// Formats returns all formats which are available for the stream
func (s *Stream) Formats() ([]*Format, error) {
+ if s.children != nil {
+ return s.children, nil
+ }
+
resp, err := s.crunchy.Client.Get(s.streamURL)
if err != nil {
return nil, err
@@ -57,6 +63,10 @@ func (s *Stream) Formats() ([]*Format, error) {
Subtitles: s.Subtitles,
})
}
+
+ if s.crunchy.cache {
+ s.children = formats
+ }
return formats, nil
}
diff --git a/video.go b/video.go
index c39a4d0..1b83c90 100644
--- a/video.go
+++ b/video.go
@@ -38,6 +38,8 @@ type Movie struct {
crunchy *Crunchyroll
+ children []*MovieListing
+
// not generated when calling MovieFromID
MovieListingMetadata struct {
AvailabilityNotes string `json:"availability_notes"`
@@ -95,6 +97,10 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
// Beside the normal movie, sometimes movie previews are returned too, but you can try to get the actual movie
// by sorting the returning MovieListing slice with the utils.MovieListingByDuration interface
func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
+ if m.children != nil {
+ return m.children, nil
+ }
+
resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
m.crunchy.Config.CountryCode,
m.crunchy.Config.MaturityRating,
@@ -120,6 +126,10 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
}
movieListings = append(movieListings, movieListing)
}
+
+ if m.crunchy.cache {
+ m.children = movieListings
+ }
return movieListings, nil
}
@@ -129,6 +139,8 @@ type Series struct {
crunchy *Crunchyroll
+ children []*Season
+
PromoDescription string `json:"promo_description"`
PromoTitle string `json:"promo_title"`
@@ -179,6 +191,10 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
// Seasons returns all seasons of a series
func (s *Series) Seasons() (seasons []*Season, err error) {
+ if s.children != nil {
+ return s.children, nil
+ }
+
resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
s.crunchy.Config.CountryCode,
s.crunchy.Config.MaturityRating,
@@ -204,5 +220,9 @@ func (s *Series) Seasons() (seasons []*Season, err error) {
}
seasons = append(seasons, season)
}
+
+ if s.crunchy.cache {
+ s.children = seasons
+ }
return
}
From 7dd74e793acce3f8e77cf370ea6bb6269ba00158 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 7 Feb 2022 14:09:22 +0100
Subject: [PATCH 024/732] Removed site cache
---
episode.go | 23 +++--------------------
1 file changed, 3 insertions(+), 20 deletions(-)
diff --git a/episode.go b/episode.go
index 36a8c34..5a34366 100644
--- a/episode.go
+++ b/episode.go
@@ -12,8 +12,6 @@ type Episode struct {
children []*Stream
- siteCache map[string]interface{}
-
ID string `json:"id"`
ChannelID string `json:"channel_id"`
@@ -110,28 +108,13 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// AudioLocale returns the audio locale of the episode.
// Every episode in a season (should) have the same audio locale,
-// so if you want to get the audio locale of a season, just call this method on the first episode of the season.
-// Otherwise, if you call this function on every episode it will cause a massive delay and redundant network
-// overload since it calls an api endpoint every time
+// so if you want to get the audio locale of a season, just call this method on the first episode of the season
func (e *Episode) AudioLocale() (LOCALE, error) {
- resp, err := e.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- e.crunchy.Config.CountryCode,
- e.crunchy.Config.MaturityRating,
- e.crunchy.Config.Channel,
- e.StreamID,
- e.crunchy.Locale,
- e.crunchy.Config.Signature,
- e.crunchy.Config.Policy,
- e.crunchy.Config.KeyPairID))
+ streams, err := e.Streams()
if err != nil {
return "", err
}
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
- e.siteCache = jsonBody
-
- return LOCALE(jsonBody["audio_locale"].(string)), nil
+ return streams[0].AudioLocale, nil
}
// Streams returns all streams which are available for the episode
From 9c9a6f497fac675f578194476203ae56ec7298ed Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 7 Feb 2022 18:51:16 +0100
Subject: [PATCH 025/732] Added new download options
---
downloader.go | 186 ++++++++++++++++++++++++++++++++++++++++++
format.go | 218 ++++++++++++++------------------------------------
2 files changed, 247 insertions(+), 157 deletions(-)
create mode 100644 downloader.go
diff --git a/downloader.go b/downloader.go
new file mode 100644
index 0000000..882904d
--- /dev/null
+++ b/downloader.go
@@ -0,0 +1,186 @@
+package crunchyroll
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "fmt"
+ "github.com/grafov/m3u8"
+ "io/ioutil"
+ "math"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+type Downloader struct {
+ // Filename is the filename of the output file
+ Filename string
+ // TempDir is the directory where the temporary files should be stored
+ TempDir string
+ // If IgnoreExisting is true, existing Filename's and TempDir's may be
+ // overwritten or deleted
+ IgnoreExisting bool
+ // If DeleteTempAfter is true, the temp directory gets deleted afterwards
+ DeleteTempAfter bool
+
+ // Goroutines is the number of goroutines to download segments with
+ Goroutines int
+
+ // A method to call when a segment was downloaded
+ OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
+
+ // If FFmpeg is true, ffmpeg will used to merge and convert files
+ FFmpeg bool
+}
+
+func NewDownloader(filename string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
+ tmp, _ := os.MkdirTemp("", "crunchy_")
+
+ return Downloader{
+ Filename: filename,
+ TempDir: tmp,
+ DeleteTempAfter: true,
+ Goroutines: goroutines,
+ OnSegmentDownload: onSegmentDownload,
+ }
+}
+
+// download downloads every mpeg transport stream segment to a given directory (more information below).
+// After every segment download onSegmentDownload will be called with:
+// the downloaded segment, the current position, the total size of segments to download, the file where the segment content was written to an error (if occurred).
+// The filename is always .ts
+//
+// Short explanation:
+// The actual crunchyroll video is split up in multiple segments (or video files) which have to be downloaded and merged after to generate a single video file.
+// And this function just downloads each of this segment into the given directory.
+// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
+func download(format *Format, tempDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
+ resp, err := format.crunchy.Client.Get(format.Video.URI)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ // reads the m3u8 file
+ playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
+ if err != nil {
+ return err
+ }
+ // extracts the segments from the playlist
+ var segments []*m3u8.MediaSegment
+ for _, segment := range playlist.(*m3u8.MediaPlaylist).Segments {
+ // some segments are nil, so they have to be filtered out
+ if segment != nil {
+ segments = append(segments, segment)
+ }
+ }
+
+ var wg sync.WaitGroup
+ chunkSize := int(math.Ceil(float64(len(segments)) / float64(goroutines)))
+
+ // when a onSegmentDownload call returns an error, this channel will be set to true and stop all goroutines
+ quit := make(chan bool)
+
+ // receives the decrypt block and iv from the first segment.
+ // in my tests, only the first segment has specified this data, so the decryption data from this first segments will be used in every other segment too
+ block, iv, err := getCrypt(format, segments[0])
+ if err != nil {
+ return err
+ }
+
+ var total int32
+ for i := 0; i < len(segments); i += chunkSize {
+ wg.Add(1)
+ end := i + chunkSize
+ if end > len(segments) {
+ end = len(segments)
+ }
+ i := i
+
+ go func() {
+ for j, segment := range segments[i:end] {
+ select {
+ case <-quit:
+ break
+ default:
+ var file *os.File
+ k := 1
+ for ; k < 4; k++ {
+ file, err = downloadSegment(format, segment, filepath.Join(tempDir, fmt.Sprintf("%d.ts", i+j)), block, iv)
+ if err == nil {
+ break
+ }
+ // sleep if an error occurs. very useful because sometimes the connection times out
+ time.Sleep(5 * time.Duration(k) * time.Second)
+ }
+ if k == 4 {
+ quit <- true
+ return
+ }
+ if onSegmentDownload != nil {
+ if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), len(segments), file); err != nil {
+ quit <- true
+ file.Close()
+ return
+ }
+ }
+ file.Close()
+ }
+ }
+ wg.Done()
+ }()
+ }
+ wg.Wait()
+
+ select {
+ case <-quit:
+ return err
+ default:
+ return nil
+ }
+}
+
+// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block block and a iv byte sequence
+func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
+ var resp *http.Response
+
+ resp, err = format.crunchy.Client.Get(segment.Key.URI)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+ key, err := ioutil.ReadAll(resp.Body)
+
+ block, err = aes.NewCipher(key)
+ if err != nil {
+ return nil, nil, err
+ }
+ iv = []byte(segment.Key.IV)
+ if len(iv) == 0 {
+ iv = key
+ }
+
+ return block, iv, nil
+}
+
+// downloadSegment downloads a segment, decrypts it and names it after the given index
+func downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
+ // every segment is aes-128 encrypted and has to be decrypted when downloaded
+ content, err := decryptSegment(format.crunchy.Client, segment, block, iv)
+ if err != nil {
+ return nil, err
+ }
+
+ file, err := os.Create(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer file.Close()
+ if _, err = file.Write(content); err != nil {
+ return nil, err
+ }
+
+ return file, nil
+}
diff --git a/format.go b/format.go
index fb388e1..49a67ba 100644
--- a/format.go
+++ b/format.go
@@ -2,29 +2,24 @@ package crunchyroll
import (
"bufio"
- "crypto/aes"
- "crypto/cipher"
"fmt"
"github.com/grafov/m3u8"
"io/ioutil"
- "math"
- "net/http"
"os"
+ "os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
- "sync"
- "sync/atomic"
- "time"
)
+type FormatType string
+
const (
EPISODE FormatType = "episodes"
MOVIE = "movies"
)
-type FormatType string
type Format struct {
crunchy *Crunchyroll
@@ -37,166 +32,50 @@ type Format struct {
Subtitles []*Subtitle
}
-// DownloadGoroutines downloads the format to the given output file (as .ts file).
-// See Format.DownloadSegments for more information
-func (f *Format) DownloadGoroutines(output *os.File, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
- downloadDir, err := os.MkdirTemp("", "crunchy_")
- if err != nil {
- return err
+func (f *Format) Download(downloader Downloader) error {
+ if _, err := os.Stat(downloader.Filename); err == nil && !downloader.IgnoreExisting {
+ return fmt.Errorf("file %s already exists", downloader.Filename)
}
- defer os.RemoveAll(downloadDir)
-
- if err := f.DownloadSegments(downloadDir, goroutines, onSegmentDownload); err != nil {
- return err
- }
-
- return f.mergeSegments(downloadDir, output)
-}
-
-// DownloadSegments downloads every mpeg transport stream segment to a given directory (more information below).
-// After every segment download onSegmentDownload will be called with:
-// the downloaded segment, the current position, the total size of segments to download, the file where the segment content was written to an error (if occurred).
-// The filename is always .ts
-//
-// Short explanation:
-// The actual crunchyroll video is split up in multiple segments (or video files) which have to be downloaded and merged after to generate a single video file.
-// And this function just downloads each of this segment into the given directory.
-// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
-func (f *Format) DownloadSegments(outputDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
- resp, err := f.crunchy.Client.Get(f.Video.URI)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- // reads the m3u8 file
- playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
- if err != nil {
- return err
- }
- // extracts the segments from the playlist
- var segments []*m3u8.MediaSegment
- for _, segment := range playlist.(*m3u8.MediaPlaylist).Segments {
- // some segments are nil, so they have to be filtered out
- if segment != nil {
- segments = append(segments, segment)
+ if _, err := os.Stat(downloader.TempDir); err == nil && !downloader.IgnoreExisting {
+ content, err := os.ReadDir(downloader.TempDir)
+ if err != nil {
+ return err
}
- }
-
- var wg sync.WaitGroup
- chunkSize := int(math.Ceil(float64(len(segments)) / float64(goroutines)))
-
- // when a onSegmentDownload call returns an error, this channel will be set to true and stop all goroutines
- quit := make(chan bool)
-
- // receives the decrypt block and iv from the first segment.
- // in my tests, only the first segment has specified this data, so the decryption data from this first segments will be used in every other segment too
- block, iv, err := f.getCrypt(segments[0])
- if err != nil {
- return err
- }
-
- var total int32
- for i := 0; i < len(segments); i += chunkSize {
- wg.Add(1)
- end := i + chunkSize
- if end > len(segments) {
- end = len(segments)
+ if len(content) > 0 {
+ return fmt.Errorf("directory %s is not empty", downloader.Filename)
}
- i := i
-
- go func() {
- for j, segment := range segments[i:end] {
- select {
- case <-quit:
- break
- default:
- var file *os.File
- k := 1
- for ; k < 4; k++ {
- file, err = f.downloadSegment(segment, filepath.Join(outputDir, fmt.Sprintf("%d.ts", i+j)), block, iv)
- if err == nil {
- break
- }
- // sleep if an error occurs. very useful because sometimes the connection times out
- time.Sleep(5 * time.Duration(k) * time.Second)
- }
- if k == 4 {
- quit <- true
- return
- }
- if onSegmentDownload != nil {
- if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), len(segments), file); err != nil {
- quit <- true
- file.Close()
- return
- }
- }
- file.Close()
- }
- }
- wg.Done()
- }()
- }
- wg.Wait()
-
- select {
- case <-quit:
+ } else if err != nil && os.IsNotExist(err) {
+ if err := os.Mkdir(downloader.TempDir, 0755); err != nil {
+ return err
+ }
+ } else {
return err
- default:
- return nil
+ }
+
+ if err := download(f, downloader.TempDir, downloader.Goroutines, downloader.OnSegmentDownload); err != nil {
+ return err
+ }
+
+ if downloader.FFmpeg {
+ return mergeSegmentsFFmpeg(downloader.TempDir, downloader.Filename)
+ } else {
+ return mergeSegments(downloader.TempDir, downloader.Filename)
}
}
-// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block block and a iv byte sequence
-func (f *Format) getCrypt(segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
- var resp *http.Response
-
- resp, err = f.crunchy.Client.Get(segment.Key.URI)
+// mergeSegments reads every file in tempDir and writes their content to the outputFile.
+// The given output file gets created or overwritten if already existing
+func mergeSegments(tempDir string, outputFile string) error {
+ dir, err := os.ReadDir(tempDir)
if err != nil {
- return nil, nil, err
+ return err
}
- defer resp.Body.Close()
- key, err := ioutil.ReadAll(resp.Body)
-
- block, err = aes.NewCipher(key)
+ file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
- return nil, nil, err
- }
- iv = []byte(segment.Key.IV)
- if len(iv) == 0 {
- iv = key
- }
-
- return block, iv, nil
-}
-
-// downloadSegment downloads a segment, decrypts it and names it after the given index
-func (f *Format) downloadSegment(segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
- // every segment is aes-128 encrypted and has to be decrypted when downloaded
- content, err := decryptSegment(f.crunchy.Client, segment, block, iv)
- if err != nil {
- return nil, err
- }
-
- file, err := os.Create(filename)
- if err != nil {
- return nil, err
+ return err
}
defer file.Close()
- if _, err = file.Write(content); err != nil {
- return nil, err
- }
-
- return file, nil
-}
-
-// mergeSegments reads every file in tempPath and writes their content to output
-func (f *Format) mergeSegments(tempPath string, output *os.File) error {
- dir, err := os.ReadDir(tempPath)
- if err != nil {
- return err
- }
- writer := bufio.NewWriter(output)
+ writer := bufio.NewWriter(file)
defer writer.Flush()
// sort the directory files after their numeric names
@@ -213,7 +92,7 @@ func (f *Format) mergeSegments(tempPath string, output *os.File) error {
})
for _, file := range dir {
- bodyAsBytes, err := ioutil.ReadFile(filepath.Join(tempPath, file.Name()))
+ bodyAsBytes, err := ioutil.ReadFile(filepath.Join(tempDir, file.Name()))
if err != nil {
return err
}
@@ -223,3 +102,28 @@ func (f *Format) mergeSegments(tempPath string, output *os.File) error {
}
return nil
}
+
+// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
+// with ffmpeg (https://ffmpeg.org/).
+// The given output file gets created or overwritten if already existing
+func mergeSegmentsFFmpeg(tempDir string, outputFile string) error {
+ dir, err := os.ReadDir(tempDir)
+ if err != nil {
+ return err
+ }
+ f, err := os.CreateTemp("", "*.txt")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(f.Name())
+ for i := 0; i < len(dir); i++ {
+ fmt.Fprintf(f, "file '%s.ts'\n", filepath.Join(tempDir, strconv.Itoa(i)))
+ }
+ cmd := exec.Command("ffmpeg",
+ "-f", "concat",
+ "-safe", "0",
+ "-i", f.Name(),
+ "-c", "copy",
+ outputFile)
+ return cmd.Run()
+}
From af2824f2168057e8df9e7ed467131764cf66f2d8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 7 Feb 2022 18:51:28 +0100
Subject: [PATCH 026/732] Removed walk utility
---
utils/walk.go | 210 --------------------------------------------------
1 file changed, 210 deletions(-)
delete mode 100644 utils/walk.go
diff --git a/utils/walk.go b/utils/walk.go
deleted file mode 100644
index f5e96d3..0000000
--- a/utils/walk.go
+++ /dev/null
@@ -1,210 +0,0 @@
-package utils
-
-import (
- "fmt"
- "github.com/ByteDream/crunchyroll-go"
-)
-
-// EpisodeWalker is an easy-to-use struct which walks down given urls
-// and triggers functions where you can perform further actions with
-// the delivered objects
-type EpisodeWalker struct {
- // The Crunchyroll instance to perform all actions on
- Crunchyroll *crunchyroll.Crunchyroll
-
- // If CheckDuplicates is true, duplicated urls, seasons and episodes
- // are filtered out and the values given in OnSeason for example is
- // always unique
- CheckDuplicates bool
-
- // OnUrl gets called when an url is parsed.
- // The error is generally only nil when the url is invalid
- OnUrl func(url string, err error) error
- // OnSeason gets called when a season was parsed
- OnSeason func(season *crunchyroll.Season, err error) error
- // OnEpisode gets called when a season was parsed
- OnEpisode func(episode *crunchyroll.Episode, err error) error
-}
-
-// WalkURLs walks through all urls.
-// Urls to seasons and episodes are support, normal as well as beta urls
-func (ew EpisodeWalker) WalkURLs(urls []string) error {
- var episodeIds, seasonIds, seriesNames []string
- episodeNames := make(map[string][]string)
-
- for _, url := range urls {
- if episodeId, ok := crunchyroll.ParseBetaEpisodeURL(url); ok && !(ew.CheckDuplicates && sliceContains(episodeIds, episodeId)) {
- episodeIds = append(episodeIds, episodeId)
- } else if seasonId, ok := crunchyroll.ParseBetaSeriesURL(url); ok && !(ew.CheckDuplicates && sliceContains(seasonIds, seasonId)) {
- seasonIds = append(seasonIds, seasonId)
- } else if seriesName, title, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
- if eps, ok := episodeNames[seriesName]; ok {
- if !ew.CheckDuplicates || !sliceContains(eps, title) {
- eps = append(eps, title)
- }
- } else {
- episodeNames[seriesName] = []string{title}
- }
- } else if seriesName, ok := crunchyroll.ParseVideoURL(url); ok && !(ew.CheckDuplicates && sliceContains(seriesNames, seriesName)) {
- seriesNames = append(seriesNames, seriesName)
- } else {
- err := fmt.Errorf("invalid url %s", url)
- if ew.OnUrl != nil {
- if err = ew.OnUrl(url, err); err != nil {
- return err
- }
- continue
- } else {
- return err
- }
- }
-
- if ew.OnUrl != nil {
- if err := ew.OnUrl(url, nil); err != nil {
- return err
- }
- }
- }
-
- for _, name := range seriesNames {
- video, err := ew.Crunchyroll.FindVideoByName(name)
- if err != nil {
- return err
- }
- // in all cases i've ever tested video was a series - even
- // if it was listed as a movie on the crunchyroll website.
- // i just hope no error occurs here :)
- seasons, err := video.(*crunchyroll.Series).Seasons()
- if err != nil {
- return err
- }
- for _, season := range seasons {
- if ew.CheckDuplicates {
- if sliceContains(seasonIds, season.ID) {
- continue
- }
- seasonIds = append(seasonIds, season.ID)
- }
- if ew.OnSeason != nil {
- if err := ew.OnSeason(season, nil); err != nil {
- return err
- }
- }
- }
- }
-
- if err := ew.walkEpisodeIds(episodeIds); err != nil {
- return err
- } else if err := ew.walkSeasonIds(seasonIds); err != nil {
- return err
- } else if err := ew.walkEpisodeNames(episodeNames); err != nil {
- return err
- }
- return nil
-}
-
-func (ew EpisodeWalker) walkEpisodeIds(episodeIds []string) error {
- var episodeIdsCheck []string
-
- for _, id := range episodeIds {
- if ew.CheckDuplicates {
- if sliceContains(episodeIdsCheck, id) {
- continue
- }
- episodeIdsCheck = append(episodeIdsCheck, id)
- }
-
- episode, err := crunchyroll.EpisodeFromID(ew.Crunchyroll, id)
- if ew.OnEpisode != nil {
- if err = ew.OnEpisode(episode, err); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func (ew EpisodeWalker) walkSeasonIds(seasonIds []string) error {
- var episodeIdsCheck []string
-
- for _, id := range seasonIds {
- season, err := crunchyroll.SeasonFromID(ew.Crunchyroll, id)
- if ew.OnSeason != nil {
- if err = ew.OnSeason(season, err); err != nil {
- return err
- }
- } else if err != nil {
- return err
- }
- eps, err := season.Episodes()
- if err != nil {
- return err
- }
- for _, ep := range eps {
- if ew.CheckDuplicates {
- if sliceContains(episodeIdsCheck, ep.ID) {
- continue
- }
- episodeIdsCheck = append(episodeIdsCheck, ep.ID)
- }
-
- if ew.OnEpisode != nil {
- if err = ew.OnEpisode(ep, nil); err != nil {
- return err
- }
- }
- }
- }
- return nil
-}
-
-func (ew EpisodeWalker) walkEpisodeNames(episodeNames map[string][]string) error {
- var episodeIdsCheck []string
-
- for seriesName, episodeName := range episodeNames {
- video, err := ew.Crunchyroll.FindVideoByName(seriesName)
- if err != nil {
- return err
- }
- series := video.(*crunchyroll.Series)
- seasons, err := series.Seasons()
- if err != nil {
- return err
- }
- for _, season := range seasons {
- eps, err := season.Episodes()
- if err != nil {
- return err
- }
- for _, ep := range eps {
- for _, name := range episodeName {
- if ep.SlugTitle == name {
- if ew.OnEpisode != nil {
- if ew.CheckDuplicates {
- if sliceContains(episodeIdsCheck, ep.ID) {
- continue
- }
- episodeIdsCheck = append(episodeIdsCheck, ep.ID)
- }
-
- if err = ew.OnEpisode(ep, nil); err != nil {
- return err
- }
- }
- break
- }
- }
- }
- }
- }
- return nil
-}
-
-func sliceContains(slice []string, elem string) bool {
- for _, s := range slice {
- if elem == s {
- return true
- }
- }
- return false
-}
From 0cbcad2c403eac53b98ffc150abf6b8cfd1a4b58 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 7 Feb 2022 19:17:00 +0100
Subject: [PATCH 027/732] Added utility to work with urls
---
utils/url.go | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 utils/url.go
diff --git a/utils/url.go b/utils/url.go
new file mode 100644
index 0000000..5181358
--- /dev/null
+++ b/utils/url.go
@@ -0,0 +1,82 @@
+package utils
+
+import (
+ "fmt"
+ "github.com/ByteDream/crunchyroll-go"
+)
+
+// ExtractEpisodesFromUrl extracts all episodes from an url.
+// If audio is not empty, the episodes gets filtered after the given locale
+func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio crunchyroll.LOCALE) ([]*crunchyroll.Episode, error) {
+ series, episodes, err := ParseUrl(crunchy, url)
+ if err != nil {
+ return nil, err
+ }
+
+ var eps []*crunchyroll.Episode
+
+ if series != nil {
+ seasons, err := series.Seasons()
+ if err != nil {
+ return nil, err
+ }
+ for _, season := range seasons {
+ if audio != "" {
+ if audioLocale, err := season.AudioLocale(); err != nil {
+ return nil, err
+ } else if audioLocale != audio {
+ continue
+ }
+ }
+ e, err := season.Episodes()
+ if err != nil {
+ return nil, err
+ }
+ eps = append(eps, e...)
+ }
+ } else if episodes != nil {
+ for _, episode := range episodes {
+ if audio == "" {
+ eps = append(eps, episode)
+ } else if audioLocale, err := episode.AudioLocale(); err != nil {
+ return nil, err
+ } else if audioLocale == audio {
+ eps = append(eps, episode)
+ }
+ }
+ }
+
+ return eps, nil
+}
+
+// ParseUrl parses the given url into a series or episode.
+// The returning episode is a slice because non-beta urls have the same episode with different languages
+func ParseUrl(crunchy *crunchyroll.Crunchyroll, url string) (*crunchyroll.Series, []*crunchyroll.Episode, error) {
+ if seriesId, ok := crunchyroll.ParseBetaSeriesURL(url); ok {
+ series, err := crunchyroll.SeriesFromID(crunchy, seriesId)
+ if err != nil {
+ return nil, nil, err
+ }
+ return series, nil, nil
+ } else if episodeId, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
+ episode, err := crunchyroll.EpisodeFromID(crunchy, episodeId)
+ if err != nil {
+ return nil, nil, err
+ }
+ return nil, []*crunchyroll.Episode{episode}, nil
+ } else if seriesName, ok := crunchyroll.ParseVideoURL(url); ok {
+ video, err := crunchy.FindVideoByName(seriesName)
+ if err != nil {
+ return nil, nil, err
+ }
+ return video.(*crunchyroll.Series), nil, nil
+ } else if seriesName, title, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ episodes, err := crunchy.FindEpisodeByName(seriesName, title)
+ if err != nil {
+ return nil, nil, err
+ }
+ return nil, episodes, nil
+ } else {
+ return nil, nil, fmt.Errorf("invalid url %s", url)
+ }
+}
From 92fe5782d1592ca520689765e5eeda6dada35dcc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 10 Feb 2022 23:08:28 +0100
Subject: [PATCH 028/732] Added function to get specific format by resolution
and subtitle
---
episode.go | 38 ++++++++++++++++++++++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/episode.go b/episode.go
index 5a34366..84756d0 100644
--- a/episode.go
+++ b/episode.go
@@ -117,6 +117,44 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil
}
+func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
+ streams, err := e.Streams()
+ if err != nil {
+ return nil, err
+ }
+ var foundStream *Stream
+ for _, stream := range streams {
+ if hardsub && stream.HardsubLocale == subtitle || stream.HardsubLocale == "" && subtitle == "" {
+ foundStream = stream
+ break
+ } else if !hardsub {
+ for _, subtitle := range stream.Subtitles {
+ if subtitle.Locale == subtitle.Locale {
+ foundStream = stream
+ break
+ }
+ }
+ if foundStream != nil {
+ break
+ }
+ }
+ }
+
+ if foundStream == nil {
+ return nil, fmt.Errorf("no matching stream found")
+ }
+ formats, err := foundStream.Formats()
+ if err != nil {
+ return nil, err
+ }
+ for _, format := range formats {
+ if format.Video.Resolution == resolution {
+ return format, nil
+ }
+ }
+ return nil, fmt.Errorf("no matching resolution found")
+}
+
// Streams returns all streams which are available for the episode
func (e *Episode) Streams() ([]*Stream, error) {
if e.children != nil {
From 1c3de9ea935bb79021809f7f2f0c6559457ff5e0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 10 Feb 2022 23:09:11 +0100
Subject: [PATCH 029/732] Added function to get audio locale
---
season.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/season.go b/season.go
index 16bc6d6..438236d 100644
--- a/season.go
+++ b/season.go
@@ -69,6 +69,14 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
return season, nil
}
+func (s *Season) AudioLocale() (LOCALE, error) {
+ episodes, err := s.Episodes()
+ if err != nil {
+ return "", err
+ }
+ return episodes[0].AudioLocale()
+}
+
// Episodes returns all episodes which are available for the season
func (s *Season) Episodes() (episodes []*Episode, err error) {
if s.children != nil {
From c557486089d9f510a8c5ed19b244e5367563e1db Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 13:15:21 +0100
Subject: [PATCH 030/732] Added 'worst' and 'best' resolution filter to
GetFormat(...)
---
episode.go | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/episode.go b/episode.go
index 84756d0..6392054 100644
--- a/episode.go
+++ b/episode.go
@@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"regexp"
+ "strconv"
+ "strings"
"time"
)
@@ -147,11 +149,33 @@ func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*
if err != nil {
return nil, err
}
+ var res *Format
for _, format := range formats {
+ if resolution == "worst" || resolution == "best" {
+ curSplitRes := strings.SplitN(format.Video.Resolution, "x", 1)
+ curResX, _ := strconv.Atoi(curSplitRes[0])
+ curResY, _ := strconv.Atoi(curSplitRes[1])
+
+ resSplitRes := strings.SplitN(res.Video.Resolution, "x", 1)
+ resResX, _ := strconv.Atoi(resSplitRes[0])
+ resResY, _ := strconv.Atoi(resSplitRes[1])
+
+ if resolution == "worst" && curResX+curResY < resResX+resResY {
+ res = format
+ } else if resolution == "best" && curResX+curResY > resResX+resResY {
+ res = format
+ }
+ }
+
if format.Video.Resolution == resolution {
return format, nil
}
}
+
+ if res != nil {
+ return res, nil
+ }
+
return nil, fmt.Errorf("no matching resolution found")
}
From 0a4b9ec96e2a803b69521caa0b06e2b895b1f5ad Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 15:01:05 +0100
Subject: [PATCH 031/732] Added context to downloader and moved method from
utils to downloader
---
downloader.go | 60 +++++++++++++++++++++++++++++++++++++++++++++------
utils.go | 32 ---------------------------
2 files changed, 53 insertions(+), 39 deletions(-)
diff --git a/downloader.go b/downloader.go
index 882904d..8e4d1a1 100644
--- a/downloader.go
+++ b/downloader.go
@@ -1,6 +1,7 @@
package crunchyroll
import (
+ "context"
"crypto/aes"
"crypto/cipher"
"fmt"
@@ -23,9 +24,14 @@ type Downloader struct {
// If IgnoreExisting is true, existing Filename's and TempDir's may be
// overwritten or deleted
IgnoreExisting bool
- // If DeleteTempAfter is true, the temp directory gets deleted afterwards
+ // If DeleteTempAfter is true, the temp directory gets deleted afterwards.
+ // Note that in case of a hard signal exit (os.Interrupt, ...) the directory
+ // will NOT be deleted. In such situations try to catch the signal and
+ // cancel Context
DeleteTempAfter bool
+ Context context.Context
+
// Goroutines is the number of goroutines to download segments with
Goroutines int
@@ -43,6 +49,7 @@ func NewDownloader(filename string, goroutines int, onSegmentDownload func(segme
Filename: filename,
TempDir: tmp,
DeleteTempAfter: true,
+ Context: context.Background(),
Goroutines: goroutines,
OnSegmentDownload: onSegmentDownload,
}
@@ -57,7 +64,7 @@ func NewDownloader(filename string, goroutines int, onSegmentDownload func(segme
// The actual crunchyroll video is split up in multiple segments (or video files) which have to be downloaded and merged after to generate a single video file.
// And this function just downloads each of this segment into the given directory.
// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
-func download(format *Format, tempDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
+func download(context context.Context, format *Format, tempDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
resp, err := format.crunchy.Client.Get(format.Video.URI)
if err != nil {
return err
@@ -100,15 +107,19 @@ func download(format *Format, tempDir string, goroutines int, onSegmentDownload
i := i
go func() {
+ defer wg.Done()
+
for j, segment := range segments[i:end] {
select {
+ case <-context.Done():
+ return
case <-quit:
- break
+ return
default:
var file *os.File
k := 1
for ; k < 4; k++ {
- file, err = downloadSegment(format, segment, filepath.Join(tempDir, fmt.Sprintf("%d.ts", i+j)), block, iv)
+ file, err = downloadSegment(context, format, segment, filepath.Join(tempDir, fmt.Sprintf("%d.ts", i+j)), block, iv)
if err == nil {
break
}
@@ -129,12 +140,13 @@ func download(format *Format, tempDir string, goroutines int, onSegmentDownload
file.Close()
}
}
- wg.Done()
}()
}
wg.Wait()
select {
+ case <-context.Done():
+ return context.Err()
case <-quit:
return err
default:
@@ -166,9 +178,9 @@ func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, i
}
// downloadSegment downloads a segment, decrypts it and names it after the given index
-func downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
+func downloadSegment(context context.Context, format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
// every segment is aes-128 encrypted and has to be decrypted when downloaded
- content, err := decryptSegment(format.crunchy.Client, segment, block, iv)
+ content, err := decryptSegment(context, format.crunchy.Client, segment, block, iv)
if err != nil {
return nil, err
}
@@ -184,3 +196,37 @@ func downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string
return file, nil
}
+
+// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25
+func decryptSegment(context context.Context, client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
+ req, err := http.NewRequest(http.MethodGet, segment.URI, nil)
+ if err != nil {
+ return nil, err
+ }
+ req.WithContext(context)
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ raw, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ blockMode := cipher.NewCBCDecrypter(block, iv[:block.BlockSize()])
+ decrypted := make([]byte, len(raw))
+ blockMode.CryptBlocks(decrypted, raw)
+ raw = pkcs5UnPadding(decrypted)
+
+ return raw, nil
+}
+
+// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47
+func pkcs5UnPadding(origData []byte) []byte {
+ length := len(origData)
+ unPadding := int(origData[length-1])
+ return origData[:(length - unPadding)]
+}
diff --git a/utils.go b/utils.go
index 5983a60..a3d4191 100644
--- a/utils.go
+++ b/utils.go
@@ -1,11 +1,7 @@
package crunchyroll
import (
- "crypto/cipher"
"encoding/json"
- "github.com/grafov/m3u8"
- "io/ioutil"
- "net/http"
)
func decodeMapToStruct(m interface{}, s interface{}) error {
@@ -16,34 +12,6 @@ func decodeMapToStruct(m interface{}, s interface{}) error {
return json.Unmarshal(jsonBody, s)
}
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25
-func decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
- resp, err := client.Get(segment.URI)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- raw, err := ioutil.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- blockMode := cipher.NewCBCDecrypter(block, iv[:block.BlockSize()])
- decrypted := make([]byte, len(raw))
- blockMode.CryptBlocks(decrypted, raw)
- raw = pkcs5UnPadding(decrypted)
-
- return raw, nil
-}
-
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47
-func pkcs5UnPadding(origData []byte) []byte {
- length := len(origData)
- unPadding := int(origData[length-1])
- return origData[:(length - unPadding)]
-}
-
func regexGroups(parsed [][]string, subexpNames ...string) map[string]string {
groups := map[string]string{}
for _, match := range parsed {
From d5fc8824cf63af97eadc2d25191e4a892c9f2f4c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 15:03:53 +0100
Subject: [PATCH 032/732] Implemented downloader context
---
format.go | 36 ++++++++++++++++++++++++++++++------
1 file changed, 30 insertions(+), 6 deletions(-)
diff --git a/format.go b/format.go
index 49a67ba..f3d548f 100644
--- a/format.go
+++ b/format.go
@@ -2,6 +2,7 @@ package crunchyroll
import (
"bufio"
+ "context"
"fmt"
"github.com/grafov/m3u8"
"io/ioutil"
@@ -52,20 +53,23 @@ func (f *Format) Download(downloader Downloader) error {
return err
}
- if err := download(f, downloader.TempDir, downloader.Goroutines, downloader.OnSegmentDownload); err != nil {
+ if downloader.DeleteTempAfter {
+ defer os.RemoveAll(downloader.TempDir)
+ }
+ if err := download(downloader.Context, f, downloader.TempDir, downloader.Goroutines, downloader.OnSegmentDownload); err != nil {
return err
}
if downloader.FFmpeg {
- return mergeSegmentsFFmpeg(downloader.TempDir, downloader.Filename)
+ return mergeSegmentsFFmpeg(downloader.Context, downloader.TempDir, downloader.Filename)
} else {
- return mergeSegments(downloader.TempDir, downloader.Filename)
+ return mergeSegments(downloader.Context, downloader.TempDir, downloader.Filename)
}
}
// mergeSegments reads every file in tempDir and writes their content to the outputFile.
// The given output file gets created or overwritten if already existing
-func mergeSegments(tempDir string, outputFile string) error {
+func mergeSegments(context context.Context, tempDir string, outputFile string) error {
dir, err := os.ReadDir(tempDir)
if err != nil {
return err
@@ -92,6 +96,12 @@ func mergeSegments(tempDir string, outputFile string) error {
})
for _, file := range dir {
+ select {
+ case <-context.Done():
+ return context.Err()
+ default:
+ }
+
bodyAsBytes, err := ioutil.ReadFile(filepath.Join(tempDir, file.Name()))
if err != nil {
return err
@@ -106,7 +116,7 @@ func mergeSegments(tempDir string, outputFile string) error {
// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
// with ffmpeg (https://ffmpeg.org/).
// The given output file gets created or overwritten if already existing
-func mergeSegmentsFFmpeg(tempDir string, outputFile string) error {
+func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile string) error {
dir, err := os.ReadDir(tempDir)
if err != nil {
return err
@@ -125,5 +135,19 @@ func mergeSegmentsFFmpeg(tempDir string, outputFile string) error {
"-i", f.Name(),
"-c", "copy",
outputFile)
- return cmd.Run()
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+
+ cmdChan := make(chan error)
+ go func() {
+ cmdChan <- cmd.Wait()
+ }()
+
+ select {
+ case err := <-cmdChan:
+ return err
+ case <-context.Done():
+ return context.Err()
+ }
}
From 8544a49cab2646d80c1288cec7e05a78f13a9147 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 16:02:32 +0100
Subject: [PATCH 033/732] Updated context usage and description
---
downloader.go | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/downloader.go b/downloader.go
index 8e4d1a1..7471a82 100644
--- a/downloader.go
+++ b/downloader.go
@@ -30,6 +30,12 @@ type Downloader struct {
// cancel Context
DeleteTempAfter bool
+ // Context to control the download process with.
+ // There is a tiny delay when canceling the context and the actual stop of the
+ // process. So it is not recommend stopping the program immediately after calling
+ // the cancel function. It's better when canceling it and then exit the program
+ // when Format.Download throws an error. See the signal handling section in
+ // cmd/crunchyroll-go/cmd/download.go for an example
Context context.Context
// Goroutines is the number of goroutines to download segments with
@@ -42,14 +48,16 @@ type Downloader struct {
FFmpeg bool
}
-func NewDownloader(filename string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
+// NewDownloader creates a downloader with default settings which should
+// fit the most needs
+func NewDownloader(context context.Context, filename string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
tmp, _ := os.MkdirTemp("", "crunchy_")
return Downloader{
Filename: filename,
TempDir: tmp,
DeleteTempAfter: true,
- Context: context.Background(),
+ Context: context,
Goroutines: goroutines,
OnSegmentDownload: onSegmentDownload,
}
@@ -65,7 +73,13 @@ func NewDownloader(filename string, goroutines int, onSegmentDownload func(segme
// And this function just downloads each of this segment into the given directory.
// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
func download(context context.Context, format *Format, tempDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
- resp, err := format.crunchy.Client.Get(format.Video.URI)
+ req, err := http.NewRequest(http.MethodGet, format.Video.URI, nil)
+ if err != nil {
+ return err
+ }
+ req.WithContext(context)
+
+ resp, err := format.crunchy.Client.Do(req)
if err != nil {
return err
}
From 1b0124e385986a375423ceeeb227cea133ab9a04 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 20:31:50 +0100
Subject: [PATCH 034/732] Fixed array length and nil panic
---
episode.go | 9 +++++++--
utils/sort.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 56 insertions(+), 4 deletions(-)
diff --git a/episode.go b/episode.go
index 6392054..07b8616 100644
--- a/episode.go
+++ b/episode.go
@@ -152,11 +152,16 @@ func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*
var res *Format
for _, format := range formats {
if resolution == "worst" || resolution == "best" {
- curSplitRes := strings.SplitN(format.Video.Resolution, "x", 1)
+ if res == nil {
+ res = format
+ continue
+ }
+
+ curSplitRes := strings.SplitN(format.Video.Resolution, "x", 2)
curResX, _ := strconv.Atoi(curSplitRes[0])
curResY, _ := strconv.Atoi(curSplitRes[1])
- resSplitRes := strings.SplitN(res.Video.Resolution, "x", 1)
+ resSplitRes := strings.SplitN(res.Video.Resolution, "x", 2)
resResX, _ := strconv.Atoi(resSplitRes[0])
resResY, _ := strconv.Atoi(resSplitRes[1])
diff --git a/utils/sort.go b/utils/sort.go
index 628af64..bf9fb77 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -2,10 +2,45 @@ package utils
import (
"github.com/ByteDream/crunchyroll-go"
+ "sort"
"strconv"
"strings"
)
+// SortEpisodesBySeason sorts the given episodes by their seasons
+func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode {
+ sortMap := map[string]map[int][]*crunchyroll.Episode{}
+
+ for _, episode := range episodes {
+ if _, ok := sortMap[episode.SeriesID]; !ok {
+ sortMap[episode.SeriesID] = map[int][]*crunchyroll.Episode{}
+ }
+ if _, ok := sortMap[episode.SeriesID][episode.SeasonNumber]; !ok {
+ sortMap[episode.SeriesID][episode.SeasonNumber] = make([]*crunchyroll.Episode, 0)
+ }
+ sortMap[episode.SeriesID][episode.SeasonNumber] = append(sortMap[episode.SeriesID][episode.SeasonNumber], episode)
+ }
+
+ var eps [][]*crunchyroll.Episode
+ for _, series := range sortMap {
+ keys := make([]int, len(series))
+ for seriesNumber := range series {
+ keys = append(keys, seriesNumber)
+ }
+ sort.Ints(keys)
+
+ for _, key := range keys {
+ es := series[key]
+ if len(es) > 0 {
+ sort.Sort(EpisodesByNumber(es))
+ eps = append(eps, es)
+ }
+ }
+ }
+
+ return eps
+}
+
// MovieListingsByDuration sorts movie listings by their duration
type MovieListingsByDuration []*crunchyroll.MovieListing
@@ -32,6 +67,18 @@ func (ebd EpisodesByDuration) Less(i, j int) bool {
return ebd[i].DurationMS < ebd[j].DurationMS
}
+type EpisodesByNumber []*crunchyroll.Episode
+
+func (ebn EpisodesByNumber) Len() int {
+ return len(ebn)
+}
+func (ebn EpisodesByNumber) Swap(i, j int) {
+ ebn[i], ebn[j] = ebn[j], ebn[i]
+}
+func (ebn EpisodesByNumber) Less(i, j int) bool {
+ return ebn[i].EpisodeNumber < ebn[j].EpisodeNumber
+}
+
// FormatsByResolution sorts formats after their resolution
type FormatsByResolution []*crunchyroll.Format
@@ -42,11 +89,11 @@ func (fbr FormatsByResolution) Swap(i, j int) {
fbr[i], fbr[j] = fbr[j], fbr[i]
}
func (fbr FormatsByResolution) Less(i, j int) bool {
- iSplitRes := strings.Split(fbr[i].Video.Resolution, "x")
+ iSplitRes := strings.SplitN(fbr[i].Video.Resolution, "x", 2)
iResX, _ := strconv.Atoi(iSplitRes[0])
iResY, _ := strconv.Atoi(iSplitRes[1])
- jSplitRes := strings.Split(fbr[j].Video.Resolution, "x")
+ jSplitRes := strings.SplitN(fbr[j].Video.Resolution, "x", 2)
jResX, _ := strconv.Atoi(jSplitRes[0])
jResY, _ := strconv.Atoi(jSplitRes[1])
From a6858556b55036f133a0d5b4a7c85933a1ab4236 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 20:36:41 +0100
Subject: [PATCH 035/732] Removed subtitle by locale
---
utils/locale.go | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/utils/locale.go b/utils/locale.go
index 708d82f..3d00648 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -54,17 +54,3 @@ func LocaleLanguage(locale crunchyroll.LOCALE) string {
return ""
}
}
-
-// SubtitleByLocale returns the subtitle of a crunchyroll.Format by its locale.
-// Check the second ok return value if the format has this subtitle
-func SubtitleByLocale(format *crunchyroll.Format, locale crunchyroll.LOCALE) (subtitle *crunchyroll.Subtitle, ok bool) {
- if format.Subtitles == nil {
- return
- }
- for _, sub := range format.Subtitles {
- if sub.Locale == locale {
- return sub, true
- }
- }
- return
-}
From 3382eca3bdc4ad0ce2219caa4690db90f31c1b12 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 13 Feb 2022 23:09:13 +0100
Subject: [PATCH 036/732] Removed redesign notice
---
cmd/crunchyroll-go/main.go | 2 --
1 file changed, 2 deletions(-)
diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go
index 0b13e39..efc6a1e 100644
--- a/cmd/crunchyroll-go/main.go
+++ b/cmd/crunchyroll-go/main.go
@@ -1,7 +1,5 @@
package main
-// the cli will be redesigned soon
-
import (
"github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd"
)
From 77c0ae64b4eb5452c3d4de4845a7fcc2e93b3457 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 17 Feb 2022 17:10:30 +0100
Subject: [PATCH 037/732] Added docs to SortEpisodesBySeason
---
utils/sort.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/utils/sort.go b/utils/sort.go
index bf9fb77..760d19c 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -7,7 +7,8 @@ import (
"strings"
)
-// SortEpisodesBySeason sorts the given episodes by their seasons
+// SortEpisodesBySeason sorts the given episodes by their seasons.
+// Note that the same episodes just with different audio locales will cause problems
func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode {
sortMap := map[string]map[int][]*crunchyroll.Episode{}
From 22525973c7af879077d501fe6c2d24fa94cc0956 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 17 Feb 2022 18:27:14 +0100
Subject: [PATCH 038/732] Renamed Download to Save and changed parameter to
io.Writer
---
subtitle.go | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/subtitle.go b/subtitle.go
index 6214031..06ec9aa 100644
--- a/subtitle.go
+++ b/subtitle.go
@@ -2,7 +2,6 @@ package crunchyroll
import (
"io"
- "os"
)
type Subtitle struct {
@@ -13,13 +12,13 @@ type Subtitle struct {
Format string `json:"format"`
}
-func (s Subtitle) Download(file *os.File) error {
+func (s Subtitle) Save(writer io.Writer) error {
resp, err := s.crunchy.Client.Get(s.URL)
if err != nil {
return err
}
defer resp.Body.Close()
- _, err = io.Copy(file, resp.Body)
+ _, err = io.Copy(writer, resp.Body)
return err
}
From e7b5a7bf301b155596a3c60441d9a645cdce7efd Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 17 Feb 2022 20:23:17 +0100
Subject: [PATCH 039/732] Updated download ffmpeg behavior
---
downloader.go | 23 ++++++++++++++++++++---
format.go | 22 ++++++++++++++++------
2 files changed, 36 insertions(+), 9 deletions(-)
diff --git a/downloader.go b/downloader.go
index 7471a82..9cca3c4 100644
--- a/downloader.go
+++ b/downloader.go
@@ -43,9 +43,15 @@ type Downloader struct {
// A method to call when a segment was downloaded
OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
+ // If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at
+ // once. Normally (because of the use of goroutines while downloading) multiple could get
+ // called simultaneously
+ LockOnSegmentDownload bool
- // If FFmpeg is true, ffmpeg will used to merge and convert files
- FFmpeg bool
+ // If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files.
+ // The given opts will be used as ffmpeg parameters while merging.
+ // Some opts are already used, see mergeSegmentsFFmpeg in format.go for more details
+ FFmpegOpts []string
}
// NewDownloader creates a downloader with default settings which should
@@ -72,7 +78,7 @@ func NewDownloader(context context.Context, filename string, goroutines int, onS
// The actual crunchyroll video is split up in multiple segments (or video files) which have to be downloaded and merged after to generate a single video file.
// And this function just downloads each of this segment into the given directory.
// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
-func download(context context.Context, format *Format, tempDir string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
+func download(context context.Context, format *Format, tempDir string, goroutines int, lockOnSegmentDownload bool, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
req, err := http.NewRequest(http.MethodGet, format.Video.URI, nil)
if err != nil {
return err
@@ -99,6 +105,7 @@ func download(context context.Context, format *Format, tempDir string, goroutine
}
var wg sync.WaitGroup
+ var lock sync.Mutex
chunkSize := int(math.Ceil(float64(len(segments)) / float64(goroutines)))
// when a onSegmentDownload call returns an error, this channel will be set to true and stop all goroutines
@@ -145,11 +152,21 @@ func download(context context.Context, format *Format, tempDir string, goroutine
return
}
if onSegmentDownload != nil {
+ if lockOnSegmentDownload {
+ lock.Lock()
+ }
+
if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), len(segments), file); err != nil {
quit <- true
+ if lockOnSegmentDownload {
+ lock.Unlock()
+ }
file.Close()
return
}
+ if lockOnSegmentDownload {
+ lock.Unlock()
+ }
}
file.Close()
}
diff --git a/format.go b/format.go
index f3d548f..b1b21a4 100644
--- a/format.go
+++ b/format.go
@@ -56,12 +56,12 @@ func (f *Format) Download(downloader Downloader) error {
if downloader.DeleteTempAfter {
defer os.RemoveAll(downloader.TempDir)
}
- if err := download(downloader.Context, f, downloader.TempDir, downloader.Goroutines, downloader.OnSegmentDownload); err != nil {
+ if err := download(downloader.Context, f, downloader.TempDir, downloader.Goroutines, downloader.LockOnSegmentDownload, downloader.OnSegmentDownload); err != nil {
return err
}
- if downloader.FFmpeg {
- return mergeSegmentsFFmpeg(downloader.Context, downloader.TempDir, downloader.Filename)
+ if downloader.FFmpegOpts != nil {
+ return mergeSegmentsFFmpeg(downloader.Context, downloader.TempDir, downloader.Filename, downloader.FFmpegOpts)
} else {
return mergeSegments(downloader.Context, downloader.TempDir, downloader.Filename)
}
@@ -116,7 +116,7 @@ func mergeSegments(context context.Context, tempDir string, outputFile string) e
// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
// with ffmpeg (https://ffmpeg.org/).
// The given output file gets created or overwritten if already existing
-func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile string) error {
+func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile string, opts []string) error {
dir, err := os.ReadDir(tempDir)
if err != nil {
return err
@@ -129,12 +129,21 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
for i := 0; i < len(dir); i++ {
fmt.Fprintf(f, "file '%s.ts'\n", filepath.Join(tempDir, strconv.Itoa(i)))
}
- cmd := exec.Command("ffmpeg",
+
+ // predefined options ... custom options ... predefined output filename
+ command := []string{
"-f", "concat",
"-safe", "0",
"-i", f.Name(),
"-c", "copy",
- outputFile)
+ }
+ if opts != nil {
+ command = append(command, opts...)
+ }
+ command = append(command, outputFile)
+
+ cmd := exec.Command("ffmpeg",
+ command...)
if err := cmd.Start(); err != nil {
return err
}
@@ -148,6 +157,7 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
case err := <-cmdChan:
return err
case <-context.Done():
+ cmd.Process.Kill()
return context.Err()
}
}
From 18ce16b3bd1c61c5f0b0034caac915e7787eed2d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 18 Feb 2022 14:51:08 +0100
Subject: [PATCH 040/732] How tf did this work before
---
utils/sort.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/utils/sort.go b/utils/sort.go
index 760d19c..1f9092a 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -24,7 +24,7 @@ func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Epis
var eps [][]*crunchyroll.Episode
for _, series := range sortMap {
- keys := make([]int, len(series))
+ var keys []int
for seriesNumber := range series {
keys = append(keys, seriesNumber)
}
From 3dfc69e2d98ac960c493cfd7e9441db3398e89b5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Feb 2022 13:16:58 +0100
Subject: [PATCH 041/732] Fixed wrong return and added stderr error output when
merging with ffmpeg
---
format.go | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/format.go b/format.go
index b1b21a4..516fb71 100644
--- a/format.go
+++ b/format.go
@@ -2,6 +2,7 @@ package crunchyroll
import (
"bufio"
+ "bytes"
"context"
"fmt"
"github.com/grafov/m3u8"
@@ -37,7 +38,14 @@ func (f *Format) Download(downloader Downloader) error {
if _, err := os.Stat(downloader.Filename); err == nil && !downloader.IgnoreExisting {
return fmt.Errorf("file %s already exists", downloader.Filename)
}
- if _, err := os.Stat(downloader.TempDir); err == nil && !downloader.IgnoreExisting {
+ if _, err := os.Stat(downloader.TempDir); err != nil {
+ if os.IsNotExist(err) {
+ err = os.Mkdir(downloader.TempDir, 0755)
+ }
+ if err != nil {
+ return err
+ }
+ } else if !downloader.IgnoreExisting {
content, err := os.ReadDir(downloader.TempDir)
if err != nil {
return err
@@ -45,12 +53,6 @@ func (f *Format) Download(downloader Downloader) error {
if len(content) > 0 {
return fmt.Errorf("directory %s is not empty", downloader.Filename)
}
- } else if err != nil && os.IsNotExist(err) {
- if err := os.Mkdir(downloader.TempDir, 0755); err != nil {
- return err
- }
- } else {
- return err
}
if downloader.DeleteTempAfter {
@@ -142,8 +144,11 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
}
command = append(command, outputFile)
+ var errBuf bytes.Buffer
cmd := exec.Command("ffmpeg",
command...)
+ cmd.Stderr = &errBuf
+
if err := cmd.Start(); err != nil {
return err
}
@@ -154,8 +159,8 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
}()
select {
- case err := <-cmdChan:
- return err
+ case <-cmdChan:
+ return fmt.Errorf(errBuf.String())
case <-context.Done():
cmd.Process.Kill()
return context.Err()
From 685dbc622e5c35b17d0323de168bd686e97e533c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Feb 2022 15:05:45 +0100
Subject: [PATCH 042/732] More ffmpeg merge fixes
---
format.go | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/format.go b/format.go
index 516fb71..104f1e9 100644
--- a/format.go
+++ b/format.go
@@ -123,17 +123,19 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
if err != nil {
return err
}
- f, err := os.CreateTemp("", "*.txt")
+ f, err := os.Create(filepath.Join(tempDir, "list.txt"))
if err != nil {
return err
}
- defer os.Remove(f.Name())
- for i := 0; i < len(dir); i++ {
+ // -1 is the list.txt file
+ for i := 0; i < len(dir)-1; i++ {
fmt.Fprintf(f, "file '%s.ts'\n", filepath.Join(tempDir, strconv.Itoa(i)))
}
+ f.Close()
// predefined options ... custom options ... predefined output filename
command := []string{
+ "-y",
"-f", "concat",
"-safe", "0",
"-i", f.Name(),
@@ -153,14 +155,21 @@ func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile str
return err
}
- cmdChan := make(chan error)
+ cmdChan := make(chan error, 1)
go func() {
cmdChan <- cmd.Wait()
}()
select {
- case <-cmdChan:
- return fmt.Errorf(errBuf.String())
+ case err = <-cmdChan:
+ if err != nil {
+ if errBuf.Len() > 0 {
+ return fmt.Errorf(errBuf.String())
+ } else {
+ return err
+ }
+ }
+ return nil
case <-context.Done():
cmd.Process.Kill()
return context.Err()
From 758b9b59c82808f56c46595ba8aeac87ae0d9865 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Feb 2022 15:19:26 +0100
Subject: [PATCH 043/732] Added InitVideo() function to initialize
Format.Video.Chunklist
---
format.go | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/format.go b/format.go
index 104f1e9..9dfaf3e 100644
--- a/format.go
+++ b/format.go
@@ -34,6 +34,28 @@ type Format struct {
Subtitles []*Subtitle
}
+// InitVideo initializes the Format.Video completely.
+// The Format.Video.Chunklist pointer is, by default, nil because an additional
+// request must be made to receive its content. The request is not made when
+// initializing a Format struct because it would probably cause an intense overhead
+// since Format.Video.Chunklist is only used sometimes
+func (f *Format) InitVideo() error {
+ if f.Video.Chunklist == nil {
+ resp, err := f.crunchy.Client.Get(f.Video.URI)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
+ if err != nil {
+ return err
+ }
+ f.Video.Chunklist = playlist.(*m3u8.MediaPlaylist)
+ }
+ return nil
+}
+
func (f *Format) Download(downloader Downloader) error {
if _, err := os.Stat(downloader.Filename); err == nil && !downloader.IgnoreExisting {
return fmt.Errorf("file %s already exists", downloader.Filename)
From 86368bf98555b105416eb818d80369fdac1a4ad4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Feb 2022 15:36:11 +0100
Subject: [PATCH 044/732] Implemented Format.InitVideo()
---
downloader.go | 37 ++++++++-----------------------------
1 file changed, 8 insertions(+), 29 deletions(-)
diff --git a/downloader.go b/downloader.go
index 9cca3c4..828f684 100644
--- a/downloader.go
+++ b/downloader.go
@@ -79,58 +79,37 @@ func NewDownloader(context context.Context, filename string, goroutines int, onS
// And this function just downloads each of this segment into the given directory.
// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
func download(context context.Context, format *Format, tempDir string, goroutines int, lockOnSegmentDownload bool, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
- req, err := http.NewRequest(http.MethodGet, format.Video.URI, nil)
- if err != nil {
+ if err := format.InitVideo(); err != nil {
return err
}
- req.WithContext(context)
-
- resp, err := format.crunchy.Client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- // reads the m3u8 file
- playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
- if err != nil {
- return err
- }
- // extracts the segments from the playlist
- var segments []*m3u8.MediaSegment
- for _, segment := range playlist.(*m3u8.MediaPlaylist).Segments {
- // some segments are nil, so they have to be filtered out
- if segment != nil {
- segments = append(segments, segment)
- }
- }
var wg sync.WaitGroup
var lock sync.Mutex
- chunkSize := int(math.Ceil(float64(len(segments)) / float64(goroutines)))
+ chunkSize := int(math.Ceil(float64(format.Video.Chunklist.Count()) / float64(goroutines)))
// when a onSegmentDownload call returns an error, this channel will be set to true and stop all goroutines
quit := make(chan bool)
// receives the decrypt block and iv from the first segment.
// in my tests, only the first segment has specified this data, so the decryption data from this first segments will be used in every other segment too
- block, iv, err := getCrypt(format, segments[0])
+ block, iv, err := getCrypt(format, format.Video.Chunklist.Segments[0])
if err != nil {
return err
}
var total int32
- for i := 0; i < len(segments); i += chunkSize {
+ for i := 0; i < int(format.Video.Chunklist.Count()); i += chunkSize {
wg.Add(1)
end := i + chunkSize
- if end > len(segments) {
- end = len(segments)
+ if end > int(format.Video.Chunklist.Count()) {
+ end = int(format.Video.Chunklist.Count())
}
i := i
go func() {
defer wg.Done()
- for j, segment := range segments[i:end] {
+ for j, segment := range format.Video.Chunklist.Segments[i:end] {
select {
case <-context.Done():
return
@@ -156,7 +135,7 @@ func download(context context.Context, format *Format, tempDir string, goroutine
lock.Lock()
}
- if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), len(segments), file); err != nil {
+ if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), int(format.Video.Chunklist.Count()), file); err != nil {
quit <- true
if lockOnSegmentDownload {
lock.Unlock()
From 3285d458bef1db71ba5cc0cf3615b4933ce22418 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Feb 2022 16:59:57 +0100
Subject: [PATCH 045/732] Fixed always true subtitle locale comparison in
GetFormat(...)
---
episode.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/episode.go b/episode.go
index 07b8616..12d21b9 100644
--- a/episode.go
+++ b/episode.go
@@ -130,8 +130,8 @@ func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*
foundStream = stream
break
} else if !hardsub {
- for _, subtitle := range stream.Subtitles {
- if subtitle.Locale == subtitle.Locale {
+ for _, streamSubtitle := range stream.Subtitles {
+ if streamSubtitle.Locale == subtitle {
foundStream = stream
break
}
From bd19b3408802ff1b3799a5df7b9caf9526b627e2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 23 Feb 2022 22:32:20 +0100
Subject: [PATCH 046/732] Added sort function for subtitles by their locale
---
utils/sort.go | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/utils/sort.go b/utils/sort.go
index 1f9092a..16ce41a 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -100,3 +100,15 @@ func (fbr FormatsByResolution) Less(i, j int) bool {
return iResX+iResY < jResX+jResY
}
+
+type SubtitlesByLocale []*crunchyroll.Subtitle
+
+func (sbl SubtitlesByLocale) Len() int {
+ return len(sbl)
+}
+func (sbl SubtitlesByLocale) Swap(i, j int) {
+ sbl[i], sbl[j] = sbl[j], sbl[i]
+}
+func (sbl SubtitlesByLocale) Less(i, j int) bool {
+ return sbl[i].Locale < sbl[j].Locale
+}
From 777738a1a1473f35255eff5f552d954c07c184a8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 24 Feb 2022 21:22:58 +0100
Subject: [PATCH 047/732] Changed spanish latin america and arabic locale &
added portuguese
---
crunchyroll.go | 5 +++--
utils/locale.go | 7 +++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 8c402d6..baa2947 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -18,14 +18,15 @@ type LOCALE string
const (
JP LOCALE = "ja-JP"
US = "en-US"
- LA = "es-LA"
+ LA = "es-419"
ES = "es-ES"
FR = "fr-FR"
+ PT = "pt-PT"
BR = "pt-BR"
IT = "it-IT"
DE = "de-DE"
RU = "ru-RU"
- ME = "ar-ME"
+ AR = "ar-SA"
)
type Crunchyroll struct {
diff --git a/utils/locale.go b/utils/locale.go
index 3d00648..8d78912 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -10,11 +10,12 @@ var AllLocales = []crunchyroll.LOCALE{
crunchyroll.LA,
crunchyroll.ES,
crunchyroll.FR,
+ crunchyroll.PT,
crunchyroll.BR,
crunchyroll.IT,
crunchyroll.DE,
crunchyroll.RU,
- crunchyroll.ME,
+ crunchyroll.AR,
}
// ValidateLocale validates if the given locale actually exist
@@ -40,6 +41,8 @@ func LocaleLanguage(locale crunchyroll.LOCALE) string {
return "Spanish (Spain)"
case crunchyroll.FR:
return "French"
+ case crunchyroll.PT:
+ return "Portuguese (Europe)"
case crunchyroll.BR:
return "Portuguese (Brazil)"
case crunchyroll.IT:
@@ -48,7 +51,7 @@ func LocaleLanguage(locale crunchyroll.LOCALE) string {
return "German"
case crunchyroll.RU:
return "Russian"
- case crunchyroll.ME:
+ case crunchyroll.AR:
return "Arabic"
default:
return ""
From b4ba50d5a4f3ff24511560a6a5b789901b6562bb Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 24 Feb 2022 21:24:11 +0100
Subject: [PATCH 048/732] Added xz dependency
---
go.mod | 1 +
go.sum | 2 ++
2 files changed, 3 insertions(+)
diff --git a/go.mod b/go.mod
index ee64a5c..a69da4f 100644
--- a/go.mod
+++ b/go.mod
@@ -5,4 +5,5 @@ go 1.16
require (
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.2.1
+ github.com/ulikunitz/xz v0.5.6
)
diff --git a/go.sum b/go.sum
index d4baaf3..fe25b3a 100644
--- a/go.sum
+++ b/go.sum
@@ -218,6 +218,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
+github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
From ceec69ef659b89b0c539e2ded0e338f3fa825bd0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 24 Feb 2022 22:09:55 +0100
Subject: [PATCH 049/732] Better audio filtering
---
utils/url.go | 53 ++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 41 insertions(+), 12 deletions(-)
diff --git a/utils/url.go b/utils/url.go
index 5181358..9bdec77 100644
--- a/utils/url.go
+++ b/utils/url.go
@@ -7,7 +7,7 @@ import (
// ExtractEpisodesFromUrl extracts all episodes from an url.
// If audio is not empty, the episodes gets filtered after the given locale
-func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio crunchyroll.LOCALE) ([]*crunchyroll.Episode, error) {
+func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio ...crunchyroll.LOCALE) ([]*crunchyroll.Episode, error) {
series, episodes, err := ParseUrl(crunchy, url)
if err != nil {
return nil, err
@@ -21,10 +21,20 @@ func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio
return nil, err
}
for _, season := range seasons {
- if audio != "" {
- if audioLocale, err := season.AudioLocale(); err != nil {
+ if audio != nil {
+ locale, err := season.AudioLocale()
+ if err != nil {
return nil, err
- } else if audioLocale != audio {
+ }
+
+ var found bool
+ for _, l := range audio {
+ if locale == l {
+ found = true
+ break
+ }
+ }
+ if !found {
continue
}
}
@@ -35,15 +45,34 @@ func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio
eps = append(eps, e...)
}
} else if episodes != nil {
- for _, episode := range episodes {
- if audio == "" {
- eps = append(eps, episode)
- } else if audioLocale, err := episode.AudioLocale(); err != nil {
- return nil, err
- } else if audioLocale == audio {
- eps = append(eps, episode)
- }
+ if audio == nil {
+ return episodes, nil
}
+
+ for _, episode := range episodes {
+ locale, err := episode.AudioLocale()
+ if err != nil {
+ return nil, err
+ }
+ if audio != nil {
+ var found bool
+ for _, l := range audio {
+ if locale == l {
+ found = true
+ break
+ }
+ }
+ if !found {
+ continue
+ }
+ }
+
+ eps = append(eps, episode)
+ }
+ }
+
+ if len(eps) == 0 {
+ return nil, fmt.Errorf("could not find any matching episode")
}
return eps, nil
From 79d55a5d3bcb7661e1139969aec56a82371597b5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 24 Feb 2022 23:11:04 +0100
Subject: [PATCH 050/732] Tests are overrated
---
crunchyroll_test.go | 115 --------------------------------------------
1 file changed, 115 deletions(-)
delete mode 100644 crunchyroll_test.go
diff --git a/crunchyroll_test.go b/crunchyroll_test.go
deleted file mode 100644
index 7009fed..0000000
--- a/crunchyroll_test.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package crunchyroll
-
-import (
- "github.com/grafov/m3u8"
- "net/http"
- "os"
- "testing"
-)
-
-var (
- email = os.Getenv("EMAIL")
- password = os.Getenv("PASSWORD")
- sessionID = os.Getenv("SESSION_ID")
-
- crunchy *Crunchyroll
- season *Season
- episode *Episode
- stream *Stream
-)
-
-func TestLogin(t *testing.T) {
- var err error
- if email != "" && password != "" {
- crunchy, err = LoginWithCredentials(email, password, DE, http.DefaultClient)
- if err != nil {
- t.Error(err)
- }
- t.Logf("Logged in with email and password\nAuth: %s %s\nSession id: %s",
- crunchy.Config.TokenType, crunchy.Config.AccessToken, crunchy.SessionID)
- } else if sessionID != "" {
- crunchy, err = LoginWithSessionID(sessionID, DE, http.DefaultClient)
- if err != nil {
- t.Error(err)
- }
- t.Logf("Logged in with session id\nAuth: %s %s\nSession id: %s",
- crunchy.Config.TokenType, crunchy.Config.AccessToken, crunchy.SessionID)
- } else {
- t.Skipf("email and / or password and session id environtment variables are not set, skipping login. All following test may fail also")
- }
-}
-
-func TestCrunchy_Search(t *testing.T) {
- series, movies, err := crunchy.Search("movie", 20)
- if err != nil {
- t.Error(err)
- }
- t.Logf("Found %d series and %d movie(s) for search query `movie`", len(series), len(movies))
-}
-
-func TestSeries_Seasons(t *testing.T) {
- video, err := crunchy.FindVideo("https://www.crunchyroll.com/darling-in-the-franxx")
- if err != nil {
- t.Error(err)
- }
- series := video.(*Series)
- seasons, err := series.Seasons()
- if err != nil {
- t.Error(err)
- }
- if len(seasons) > 0 {
- season = seasons[4]
- } else {
- t.Logf("%s has no seasons, some future test will fail", series.Title)
- }
- t.Logf("Found %d seasons for series %s", len(seasons), series.Title)
-}
-
-func TestCrunchyroll_FindEpisode(t *testing.T) {
- episodes, err := crunchy.FindEpisode("https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575")
- if err != nil {
- t.Error(err)
- }
- t.Logf("Found %d episodes for episode %s", len(episodes), "https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575")
-}
-
-func TestSeason_Episodes(t *testing.T) {
- episodes, err := season.Episodes()
- if err != nil {
- t.Error(err)
- }
- if len(episodes) > 0 {
- episode = episodes[0]
- } else {
- t.Logf("%s has no episodes, some future test will fail", season.Title)
- }
- t.Logf("Found %d episodes for season %s", len(episodes), season.Title)
-}
-
-func TestEpisode_Streams(t *testing.T) {
- streams, err := episode.Streams()
- if err != nil {
- t.Error(err)
- }
- if len(streams) > 0 {
- stream = streams[0]
- } else {
- t.Logf("%s has no streams, some future test will fail", season.Title)
- }
- t.Logf("Found %d streams for episode %s", len(streams), season.Title)
-}
-
-func TestFormat_Download(t *testing.T) {
- formats, err := stream.Formats()
- if err != nil {
- t.Error(err)
- }
- file, err := os.Create("test")
- if err != nil {
- t.Error(err)
- }
- formats[0].DownloadGoroutines(file, 4, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- t.Logf("Downloaded %.2f%% (%d/%d)", float32(current)/float32(total)*100, current, total)
- return nil
- })
-}
From 9fbb3266aaa7e8603a6564a8d32fb5bb19919ab5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 14:36:11 +0100
Subject: [PATCH 051/732] Removed xz dependency
---
go.mod | 8 +-
go.sum | 270 ++++++++++++++++++++++++++++++++++++++++++++++++---------
2 files changed, 237 insertions(+), 41 deletions(-)
diff --git a/go.mod b/go.mod
index a69da4f..f5c3ea4 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,10 @@ go 1.16
require (
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.2.1
- github.com/ulikunitz/xz v0.5.6
+ github.com/spf13/cobra v1.3.0
+)
+
+require (
+ github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
)
diff --git a/go.sum b/go.sum
index fe25b3a..a1bf13c 100644
--- a/go.sum
+++ b/go.sum
@@ -18,6 +18,15 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -26,7 +35,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@@ -39,23 +48,46 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
+github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -64,19 +96,32 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
+github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@@ -85,6 +130,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -102,6 +148,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -115,10 +162,12 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -130,87 +179,139 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
+github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
+github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
+github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
+github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
+github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
+github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
+github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
+github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
+github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
+github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
+github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
+github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw=
-github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
+github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -218,16 +319,17 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
-github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
+github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
+github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
+github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -235,16 +337,20 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -280,10 +386,11 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -291,9 +398,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -316,6 +425,9 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -327,7 +439,12 @@ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -341,21 +458,30 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -374,12 +500,27 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -388,6 +529,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -397,7 +540,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -405,9 +547,9 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -441,7 +583,11 @@ golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -467,7 +613,17 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
+google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -515,7 +671,29 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -535,7 +713,15 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -548,12 +734,18 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From ef4b24f068a140dd28f5d0c8cd90c4841d64e989 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 16:38:56 +0100
Subject: [PATCH 052/732] Removed color flag and changed error output format
---
cmd/crunchyroll-go/cmd/root.go | 25 ++++++++++---------------
1 file changed, 10 insertions(+), 15 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 6f8a4e6..db2cba6 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -5,20 +5,17 @@ import (
"github.com/spf13/cobra"
"net/http"
"os"
- "runtime"
"runtime/debug"
)
var (
client *http.Client
- locale crunchyroll.LOCALE
crunchy *crunchyroll.Crunchyroll
- out = newLogger(false, true, true, colorFlag)
+ out = newLogger(false, true, true)
quietFlag bool
verboseFlag bool
proxyFlag string
- colorFlag bool
)
var rootCmd = &cobra.Command{
@@ -26,9 +23,9 @@ var rootCmd = &cobra.Command{
Short: "Download crunchyroll videos with ease",
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
if verboseFlag {
- out = newLogger(true, true, true, colorFlag)
+ out = newLogger(true, true, true)
} else if quietFlag {
- out = newLogger(false, false, false, false)
+ out = newLogger(false, false, false)
}
out.DebugLog.Printf("Executing `%s` command with %d arg(s)\n", cmd.Name(), len(args))
@@ -42,23 +39,21 @@ 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().BoolVar(&colorFlag, "color", false, "Colored output. Only available on not windows systems")
}
func Execute() {
rootCmd.CompletionOptions.DisableDefaultCmd = true
defer func() {
if r := recover(); r != nil {
- out.Errln(r)
- // change color to red
- if colorFlag && runtime.GOOS != "windows" {
- out.ErrLog.SetOutput(&loggerWriter{original: out.ErrLog.Writer(), color: "\033[31m"})
+ if out.IsDev() {
+ out.Err(string(debug.Stack()))
+ } else {
+ out.Err("Unexpected error: %v", r)
}
- out.Debugln(string(debug.Stack()))
- os.Exit(2)
+ os.Exit(1)
}
}()
- if err := rootCmd.Execute(); err != nil {
- out.Fatalln(err)
+ if rootCmd.Execute() != nil {
+ os.Exit(1)
}
}
From e785cb580b895435c1c363cd579041d517bb51ea Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 16:41:12 +0100
Subject: [PATCH 053/732] Fixed system locale receiving and added download
progress, download (format) information, filename generator, url episodes
extractor
---
cmd/crunchyroll-go/cmd/utils.go | 220 ++++++++++++++++++++++++++++----
1 file changed, 195 insertions(+), 25 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 3c357f9..809e982 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -11,37 +11,48 @@ import (
"os/exec"
"path"
"path/filepath"
+ "reflect"
"runtime"
"strconv"
"strings"
+ "sync"
"time"
)
var sessionIDPath = filepath.Join(os.TempDir(), ".crunchy")
+var (
+ invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
+ invalidLinuxChars = []string{"/"}
+)
+
// systemLocale receives the system locale
// https://stackoverflow.com/questions/51829386/golang-get-system-language/51831590#51831590
func systemLocale() crunchyroll.LOCALE {
if runtime.GOOS != "windows" {
if lang, ok := os.LookupEnv("LANG"); ok {
- return localeToLOCALE(strings.ReplaceAll(strings.Split(lang, ".")[0], "_", "-"))
+ prefix := strings.Split(lang, "_")[0]
+ suffix := strings.Split(strings.Split(lang, ".")[0], "_")[1]
+ l := crunchyroll.LOCALE(fmt.Sprintf("%s-%s", prefix, suffix))
+ if !utils.ValidateLocale(l) {
+ 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 {
- return localeToLOCALE(strings.Trim(string(output), "\r\n"))
+ if output, err := cmd.Output(); err == nil {
+ l := crunchyroll.LOCALE(strings.Trim(string(output), "\r\n"))
+ if !utils.ValidateLocale(l) {
+ out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
+ l = crunchyroll.US
+ }
+ return l
}
}
- return localeToLOCALE("en-US")
-}
-
-func localeToLOCALE(locale string) crunchyroll.LOCALE {
- if l := crunchyroll.LOCALE(locale); utils.ValidateLocale(l) {
- return l
- } else {
- out.Errf("%s is not a supported locale, using %s as fallback\n", locale, crunchyroll.US)
- return crunchyroll.US
- }
+ out.Err("Failed to get locale, using %s", crunchyroll.US)
+ return crunchyroll.US
}
func allLocalesAsStrings() (locales []string) {
@@ -55,7 +66,7 @@ func createOrDefaultClient(proxy string) (*http.Client, error) {
if proxy == "" {
return http.DefaultClient, nil
} else {
- out.Infof("Using custom proxy %s\n", proxy)
+ out.Info("Using custom proxy %s", proxy)
proxyURL, err := url.Parse(proxy)
if err != nil {
return nil, err
@@ -86,7 +97,8 @@ func freeFileName(filename string) (string, bool) {
func loadSessionID() (string, error) {
if _, stat := os.Stat(sessionIDPath); os.IsNotExist(stat) {
- out.Fatalf("To use this command, login first. Type `%s login -h` to get help\n", os.Args[0])
+ out.Err("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
+ os.Exit(1)
}
body, err := ioutil.ReadFile(sessionIDPath)
if err != nil {
@@ -96,35 +108,34 @@ func loadSessionID() (string, error) {
}
func loadCrunchy() {
- out.StartProgress("Logging in")
+ out.SetProgress("Logging in")
sessionID, err := loadSessionID()
if err == nil {
- if crunchy, err = crunchyroll.LoginWithSessionID(sessionID, locale, client); err != nil {
- out.EndProgress(false, err.Error())
+ if crunchy, err = crunchyroll.LoginWithSessionID(sessionID, systemLocale(), client); err != nil {
+ out.StopProgress(err.Error())
os.Exit(1)
}
} else {
- out.EndProgress(false, err.Error())
+ out.StopProgress(err.Error())
os.Exit(1)
}
- out.EndProgress(true, "Logged in")
- out.Debugf("Logged in with session id %s\n", sessionID)
+ out.StopProgress("Logged in")
+ out.Debug("Logged in with session id %s", sessionID)
}
func hasFFmpeg() bool {
- cmd := exec.Command("ffmpeg", "-h")
- return cmd.Run() == nil
+ return exec.Command("ffmpeg", "-h").Run() == nil
}
func terminalWidth() int {
if runtime.GOOS != "windows" {
cmd := exec.Command("stty", "size")
cmd.Stdin = os.Stdin
- out, err := cmd.Output()
+ res, err := cmd.Output()
if err != nil {
return 60
}
- width, err := strconv.Atoi(strings.Split(strings.ReplaceAll(string(out), "\n", ""), " ")[1])
+ width, err := strconv.Atoi(strings.Split(strings.ReplaceAll(string(res), "\n", ""), " ")[1])
if err != nil {
return 60
}
@@ -132,3 +143,162 @@ func terminalWidth() int {
}
return 60
}
+
+func generateFilename(name, directory string) string {
+ if runtime.GOOS != "windows" {
+ for _, char := range invalidLinuxChars {
+ strings.ReplaceAll(name, char, "")
+ }
+ out.Debug("Replaced invalid characters (not windows)")
+ } else {
+ for _, char := range invalidWindowsChars {
+ strings.ReplaceAll(name, char, "")
+ }
+ out.Debug("Replaced invalid characters (windows)")
+ }
+
+ if directory != "" {
+ name = filepath.Join(directory, name)
+ }
+
+ filename, changed := freeFileName(name)
+ if changed {
+ out.Info("File %s already exists, changing name to %s", name, filename)
+ }
+
+ return filename
+}
+
+func extractEpisodes(url string, locales ...crunchyroll.LOCALE) [][]*crunchyroll.Episode {
+ final := make([][]*crunchyroll.Episode, len(locales))
+ episodes, err := utils.ExtractEpisodesFromUrl(crunchy, url, locales...)
+ if err != nil {
+ out.Err("Failed to get episodes: %v", err)
+ os.Exit(1)
+ }
+
+ // fetch all episodes and sort them by their locale
+ var wg sync.WaitGroup
+ for _, episode := range episodes {
+ episode := episode
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ audioLocale, err := episode.AudioLocale()
+ if err != nil {
+ out.Err("Failed to get audio locale: %v", err)
+ os.Exit(1)
+ }
+
+ for i, locale := range locales {
+ if locale == audioLocale {
+ final[i] = append(final[i], episode)
+ }
+ }
+ }()
+ }
+ wg.Wait()
+
+ return final
+}
+
+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"`
+ 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
+ }
+ }
+ 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
+ progressWidth := float32(terminalWidth() - (12 + len(dp.Prefix) + len(dp.Message)) - (len(fmt.Sprint(dp.Total)))*2)
+
+ repeatCount := int(percentage / (float32(100) / progressWidth))
+ // it can be lower than zero when the terminal is very tiny
+ if repeatCount < 0 {
+ repeatCount = 0
+ }
+ progressPercentage := (strings.Repeat("=", repeatCount) + ">")[1:]
+
+ fmt.Printf("\r%s%s [%-"+fmt.Sprint(progressWidth)+"s]%4d%% %8d/%d", dp.Prefix, msg, progressPercentage, int(percentage), dp.Current, dp.Total)
+}
From 252762f410925e92205a61b387b677df4de82874 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 17:26:03 +0100
Subject: [PATCH 054/732] Fixed logger to properly delete current line before
overwriting
---
cmd/crunchyroll-go/cmd/logger.go | 213 +++++++++++++------------------
1 file changed, 89 insertions(+), 124 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index 1079dc6..d9c7248 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -1,19 +1,18 @@
package cmd
import (
+ "context"
"fmt"
"io"
"log"
"os"
- "runtime"
"strings"
- "sync"
"time"
)
type progress struct {
- status bool
message string
+ stop bool
}
type logger struct {
@@ -23,26 +22,21 @@ type logger struct {
devView bool
- progressWG sync.Mutex
- progress chan progress
+ progress chan progress
+ done chan interface{}
}
-func newLogger(debug, info, err bool, color bool) *logger {
- debugLog, infoLog, errLog := log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0), log.New(io.Discard, "=> ", 0)
-
- debugColor, infoColor, errColor := "", "", ""
- if color && runtime.GOOS != "windows" {
- debugColor, infoColor, errColor = "\033[95m", "\033[96m", "\033[31m"
- }
+func newLogger(debug, info, err bool) *logger {
+ debugLog, infoLog, errLog := log.New(io.Discard, "➞ ", 0), log.New(io.Discard, "➞ ", 0), log.New(io.Discard, "➞ ", 0)
if debug {
- debugLog.SetOutput(&loggerWriter{original: os.Stdout, color: debugColor})
+ debugLog.SetOutput(os.Stdout)
}
if info {
- infoLog.SetOutput(&loggerWriter{original: os.Stdout, color: infoColor})
+ infoLog.SetOutput(os.Stdout)
}
if err {
- errLog.SetOutput(&loggerWriter{original: os.Stdout, color: errColor})
+ errLog.SetOutput(os.Stderr)
}
if debug {
@@ -60,140 +54,111 @@ func newLogger(debug, info, err bool, color bool) *logger {
}
}
+func (l *logger) IsDev() bool {
+ return l.devView
+}
+
+func (l *logger) IsQuiet() bool {
+ return l.DebugLog.Writer() == io.Discard && l.InfoLog.Writer() == io.Discard && l.ErrLog.Writer() == io.Discard
+}
+
+func (l *logger) Debug(format string, v ...interface{}) {
+ l.DebugLog.Printf(format, v...)
+}
+
+func (l *logger) Info(format string, v ...interface{}) {
+ l.InfoLog.Printf(format, v...)
+}
+
+func (l *logger) Err(format string, v ...interface{}) {
+ l.ErrLog.Printf(format, v...)
+}
+
func (l *logger) Empty() {
- if !l.devView && l.InfoLog.Writer() != io.Discard {
- fmt.Println()
+ if l.InfoLog.Writer() != io.Discard {
+ fmt.Println("")
}
}
-func (l *logger) StartProgress(message string) {
- if l.devView {
- l.InfoLog.Println(message)
+func (l *logger) SetProgress(format string, v ...interface{}) {
+ if out.InfoLog.Writer() == io.Discard {
return
}
+
+ message := fmt.Sprintf(format, v...)
+
+ if l.progress != nil {
+ l.progress <- progress{
+ message: message,
+ stop: false,
+ }
+ return
+ }
+
l.progress = make(chan progress)
+ l.done = make(chan interface{})
go func() {
states := []string{"-", "\\", "|", "/"}
+ var count int
+
for i := 0; ; i++ {
- l.progressWG.Lock()
+ ctx, cancel := context.WithTimeout(context.Background(), 35*time.Millisecond)
select {
case p := <-l.progress:
- // clearing the last line
- fmt.Printf("\r%s\r", strings.Repeat(" ", len(l.InfoLog.Prefix())+len(message)+2))
- if p.status {
- successTag := "✔"
- if runtime.GOOS == "windows" {
- successTag = "~"
+ cancel()
+
+ if p.stop {
+ if !l.devView {
+ fmt.Printf("\r" + strings.Repeat(" ", 2+len(message)))
+ fmt.Printf("\r➞ %s\n", p.message)
+ } else {
+ l.Debug(message)
}
- l.InfoLog.Printf("%s %s", successTag, p.message)
+
+ l.progress = nil
+
+ if count > 0 {
+ fmt.Printf("↳ %s\n", p.message)
+ }
+
+ l.done <- nil
+ return
} else {
- errorTag := "✘"
- if runtime.GOOS == "windows" {
- errorTag = "!"
+ if !l.devView {
+ fmt.Printf("\r↓ %s\n", message)
+ } else {
+ l.Debug(message)
}
- l.ErrLog.Printf("%s %s", errorTag, p.message)
+
+ l.progress = make(chan progress)
+ count++
+
+ if !l.devView {
+ fmt.Printf("\r" + strings.Repeat(" ", 2+len(message)))
+ fmt.Printf("\r➞ %s\n", p.message)
+ } else {
+ l.Debug(p.message)
+ }
+ message = p.message
}
- l.progress = nil
- l.progressWG.Unlock()
- return
- default:
- if i%10 == 0 {
- fmt.Printf("\r%s%s %s", l.InfoLog.Prefix(), states[i/10%4], message)
+ case <-ctx.Done():
+ if !l.devView && i%10 == 0 {
+ fmt.Printf("\r%s %s", states[i/10%4], message)
}
- time.Sleep(35 * time.Millisecond)
- l.progressWG.Unlock()
}
}
}()
}
-func (l *logger) StartProgressf(message string, a ...interface{}) {
- l.StartProgress(fmt.Sprintf(message, a...))
-}
-
-func (l *logger) EndProgress(successful bool, message string) {
- if l.devView {
- if successful {
- l.InfoLog.Print(message)
- } else {
- l.ErrLog.Print(message)
- }
+func (l *logger) StopProgress(format string, v ...interface{}) {
+ if out.InfoLog.Writer() == io.Discard {
return
- } else if l.progress != nil {
- l.progress <- progress{
- status: successful,
- message: message,
- }
- }
-}
-
-func (l *logger) EndProgressf(successful bool, message string, a ...interface{}) {
- l.EndProgress(successful, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Debugln(v ...interface{}) {
- l.print(0, v...)
-}
-
-func (l *logger) Debugf(message string, a ...interface{}) {
- l.print(0, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Infoln(v ...interface{}) {
- l.print(1, v...)
-}
-
-func (l *logger) Infof(message string, a ...interface{}) {
- l.print(1, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Errln(v ...interface{}) {
- l.print(2, v...)
-}
-
-func (l *logger) Errf(message string, a ...interface{}) {
- l.print(2, fmt.Sprintf(message, a...))
-}
-
-func (l *logger) Fatalln(v ...interface{}) {
- l.print(2, v...)
- os.Exit(1)
-}
-
-func (l *logger) Fatalf(message string, a ...interface{}) {
- l.print(2, fmt.Sprintf(message, a...))
- os.Exit(1)
-}
-
-func (l *logger) print(level int, v ...interface{}) {
- if l.progress != nil {
- l.progressWG.Lock()
- defer l.progressWG.Unlock()
- fmt.Print("\r")
}
- switch level {
- case 0:
- l.DebugLog.Print(v...)
- case 1:
- l.InfoLog.Print(v...)
- case 2:
- l.ErrLog.Print(v...)
+ l.progress <- progress{
+ message: fmt.Sprintf(format, v...),
+ stop: true,
}
-}
-
-type loggerWriter struct {
- io.Writer
-
- original io.Writer
- color string
-}
-
-func (lw *loggerWriter) Write(p []byte) (n int, err error) {
- if lw.color != "" {
- p = append([]byte(lw.color), p...)
- p = append(p, []byte("\033[0m")...)
- }
- return lw.original.Write(p)
+ <-l.done
}
From 733f4a97ead8d18a4104d07daa25dbe87bc7e1dd Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 17:42:18 +0100
Subject: [PATCH 055/732] Added support for storing login credential
permanently
---
cmd/crunchyroll-go/cmd/login.go | 64 +++++++++++++++++++++++----------
cmd/crunchyroll-go/cmd/utils.go | 57 ++++++++++++++++++-----------
2 files changed, 81 insertions(+), 40 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index c2f07e0..f15bc5b 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -1,13 +1,20 @@
package cmd
import (
+ "fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/spf13/cobra"
"io/ioutil"
+ "os"
+ "os/user"
+ "path/filepath"
+ "runtime"
)
var (
- sessionIDFlag bool
+ loginSessionIDFlag bool
+
+ loginPersistentFlag bool
)
var loginCmd = &cobra.Command{
@@ -15,36 +22,55 @@ var loginCmd = &cobra.Command{
Short: "Login to crunchyroll",
Args: cobra.RangeArgs(1, 2),
- RunE: func(cmd *cobra.Command, args []string) error {
- if sessionIDFlag {
- return loginSessionID(args[0], false)
+ Run: func(cmd *cobra.Command, args []string) {
+ if loginSessionIDFlag {
+ loginSessionID(args[0])
} else {
- return loginCredentials(args[0], args[1])
+ loginCredentials(args[0], args[1])
}
},
}
func init() {
+ loginCmd.Flags().BoolVar(&loginSessionIDFlag, "session-id", false, "Use a session id to login instead of username and password")
+
+ loginCmd.Flags().BoolVar(&loginPersistentFlag, "persistent", false, "If the given credential should be stored persistent")
+
rootCmd.AddCommand(loginCmd)
- loginCmd.Flags().BoolVar(&sessionIDFlag, "session-id", false, "session id")
}
-func loginCredentials(email, password string) error {
+func loginCredentials(user, password string) error {
out.Debug("Logging in via credentials")
- session, err := crunchyroll.LoginWithCredentials(email, password, locale, client)
- if err != nil {
- return err
+ if _, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(), client); err != nil {
+ out.Err(err.Error())
+ os.Exit(1)
}
- return loginSessionID(session.SessionID, true)
+
+ return ioutil.WriteFile(loginStorePath(), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600)
}
-func loginSessionID(sessionID string, alreadyChecked bool) error {
- if !alreadyChecked {
- out.Debug("Logging in via session id")
- if _, err := crunchyroll.LoginWithSessionID(sessionID, locale, client); err != nil {
- return err
- }
+func loginSessionID(sessionID string) error {
+ out.Debug("Logging in via session id")
+ if _, err := crunchyroll.LoginWithSessionID(sessionID, systemLocale(), client); err != nil {
+ out.Err(err.Error())
+ os.Exit(1)
}
- out.Info("Due to security reasons, you have to login again on the next reboot")
- return ioutil.WriteFile(sessionIDPath, []byte(sessionID), 0777)
+
+ return ioutil.WriteFile(loginStorePath(), []byte(sessionID), 0600)
+}
+
+func loginStorePath() string {
+ path := filepath.Join(os.TempDir(), ".crunchy")
+ if loginPersistentFlag {
+ if runtime.GOOS != "windows" {
+ usr, _ := user.Current()
+ path = filepath.Join(usr.HomeDir, ".config/crunchyroll-go")
+ }
+
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", path)
+ } else {
+ out.Info("Due to security reasons, you have to login again on the next reboot")
+ }
+
+ return path
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 809e982..4a9e748 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -4,11 +4,11 @@ import (
"fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/ByteDream/crunchyroll-go/utils"
- "io/ioutil"
"net/http"
"net/url"
"os"
"os/exec"
+ "os/user"
"path"
"path/filepath"
"reflect"
@@ -19,8 +19,6 @@ import (
"time"
)
-var sessionIDPath = filepath.Join(os.TempDir(), ".crunchy")
-
var (
invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
invalidLinuxChars = []string{"/"}
@@ -95,32 +93,49 @@ func freeFileName(filename string) (string, bool) {
return filename, j != 0
}
-func loadSessionID() (string, error) {
- if _, stat := os.Stat(sessionIDPath); os.IsNotExist(stat) {
- out.Err("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
- os.Exit(1)
- }
- body, err := ioutil.ReadFile(sessionIDPath)
- if err != nil {
- return "", err
- }
- return strings.ReplaceAll(string(body), "\n", ""), nil
-}
-
func loadCrunchy() {
out.SetProgress("Logging in")
- sessionID, err := loadSessionID()
- if err == nil {
- if crunchy, err = crunchyroll.LoginWithSessionID(sessionID, systemLocale(), client); err != nil {
+
+ files := []string{filepath.Join(os.TempDir(), ".crunchy")}
+
+ if runtime.GOOS != "windows" {
+ usr, _ := user.Current()
+ files = append(files, filepath.Join(usr.HomeDir, ".config/crunchyroll-go"))
+ }
+
+ var body []byte
+ var err error
+ for _, file := range files {
+ if _, err = os.Stat(file); os.IsNotExist(err) {
+ continue
+ }
+ body, err = os.ReadFile(file)
+ break
+ }
+ if body == nil {
+ out.Err("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
+ os.Exit(1)
+ } else if err != nil {
+ out.Err("Failed to read login information: %v", err)
+ os.Exit(1)
+ }
+
+ split := strings.SplitN(string(body), "\n", 2)
+ if len(split) == 1 || split[2] == "" {
+ if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
}
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
} else {
- out.StopProgress(err.Error())
- os.Exit(1)
+ if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(), client); err != nil {
+ out.StopProgress(err.Error())
+ os.Exit(1)
+ }
+ out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
}
+
out.StopProgress("Logged in")
- out.Debug("Logged in with session id %s", sessionID)
}
func hasFFmpeg() bool {
From 647eb075e592807d6b58411a757c81d657b01b94 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 17:42:47 +0100
Subject: [PATCH 056/732] Renamed parameter in LoginWithCredentials
---
crunchyroll.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index baa2947..8c456a4 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -57,8 +57,8 @@ type Crunchyroll struct {
cache bool
}
-// LoginWithCredentials logs in via crunchyroll email and password
-func LoginWithCredentials(email string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
+// LoginWithCredentials logs in via crunchyroll username or email and password
+func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s",
"LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI")
sessResp, err := client.Get(sessionIDEndpoint)
@@ -76,7 +76,7 @@ func LoginWithCredentials(email string, password string, locale LOCALE, client *
loginEndpoint := "https://api.crunchyroll.com/login.0.json"
authValues := url.Values{}
authValues.Set("session_id", sessionID)
- authValues.Set("account", email)
+ authValues.Set("account", user)
authValues.Set("password", password)
client.Post(loginEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(authValues.Encode()))
From 5219f1841709902f411e2fa4dbcacb3c2dd94098 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Feb 2022 17:56:11 +0100
Subject: [PATCH 057/732] Fixed array index out of bounds error when logging in
---
cmd/crunchyroll-go/cmd/utils.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 4a9e748..508e2e8 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -121,7 +121,7 @@ func loadCrunchy() {
}
split := strings.SplitN(string(body), "\n", 2)
- if len(split) == 1 || split[2] == "" {
+ if len(split) == 1 || split[1] == "" {
if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
From d6347a6b6ba3b7500888d8b00483f5bfb76493df Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 2 Mar 2022 18:56:28 +0100
Subject: [PATCH 058/732] Moved url parsing methods from utils to standard
library
---
utils/url.go => url.go | 31 +++++++++++++++----------------
1 file changed, 15 insertions(+), 16 deletions(-)
rename utils/url.go => url.go (62%)
diff --git a/utils/url.go b/url.go
similarity index 62%
rename from utils/url.go
rename to url.go
index 9bdec77..5abf370 100644
--- a/utils/url.go
+++ b/url.go
@@ -1,19 +1,18 @@
-package utils
+package crunchyroll
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go"
)
// ExtractEpisodesFromUrl extracts all episodes from an url.
// If audio is not empty, the episodes gets filtered after the given locale
-func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio ...crunchyroll.LOCALE) ([]*crunchyroll.Episode, error) {
- series, episodes, err := ParseUrl(crunchy, url)
+func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Episode, error) {
+ series, episodes, err := c.ParseUrl(url)
if err != nil {
return nil, err
}
- var eps []*crunchyroll.Episode
+ var eps []*Episode
if series != nil {
seasons, err := series.Seasons()
@@ -80,27 +79,27 @@ func ExtractEpisodesFromUrl(crunchy *crunchyroll.Crunchyroll, url string, audio
// ParseUrl parses the given url into a series or episode.
// The returning episode is a slice because non-beta urls have the same episode with different languages
-func ParseUrl(crunchy *crunchyroll.Crunchyroll, url string) (*crunchyroll.Series, []*crunchyroll.Episode, error) {
- if seriesId, ok := crunchyroll.ParseBetaSeriesURL(url); ok {
- series, err := crunchyroll.SeriesFromID(crunchy, seriesId)
+func (c *Crunchyroll) ParseUrl(url string) (*Series, []*Episode, error) {
+ if seriesId, ok := ParseBetaSeriesURL(url); ok {
+ series, err := SeriesFromID(c, seriesId)
if err != nil {
return nil, nil, err
}
return series, nil, nil
- } else if episodeId, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- episode, err := crunchyroll.EpisodeFromID(crunchy, episodeId)
+ } else if episodeId, ok := ParseBetaEpisodeURL(url); ok {
+ episode, err := EpisodeFromID(c, episodeId)
if err != nil {
return nil, nil, err
}
- return nil, []*crunchyroll.Episode{episode}, nil
- } else if seriesName, ok := crunchyroll.ParseVideoURL(url); ok {
- video, err := crunchy.FindVideoByName(seriesName)
+ return nil, []*Episode{episode}, nil
+ } else if seriesName, ok := ParseVideoURL(url); ok {
+ video, err := c.FindVideoByName(seriesName)
if err != nil {
return nil, nil, err
}
- return video.(*crunchyroll.Series), nil, nil
- } else if seriesName, title, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
- episodes, err := crunchy.FindEpisodeByName(seriesName, title)
+ return video.(*Series), nil, nil
+ } else if seriesName, title, _, _, ok := ParseEpisodeURL(url); ok {
+ episodes, err := c.FindEpisodeByName(seriesName, title)
if err != nil {
return nil, nil, err
}
From 6fb6b3c03ce4fc29022ccc40138a9193a2561416 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:01:25 +0100
Subject: [PATCH 059/732] Moved all download related stuff to downloader.go and
fixed some errors in it
---
downloader.go | 295 ++++++++++++++++++++++++++++++++++++++------------
format.go | 152 +-------------------------
2 files changed, 230 insertions(+), 217 deletions(-)
diff --git a/downloader.go b/downloader.go
index 828f684..d0bdf7c 100644
--- a/downloader.go
+++ b/downloader.go
@@ -1,29 +1,53 @@
package crunchyroll
import (
+ "bytes"
"context"
"crypto/aes"
"crypto/cipher"
"fmt"
"github.com/grafov/m3u8"
+ "io"
"io/ioutil"
"math"
"net/http"
"os"
+ "os/exec"
"path/filepath"
+ "regexp"
"sync"
"sync/atomic"
"time"
)
+var ffmpegInfoPattern = regexp.MustCompile(`Output #0, (.+),`)
+
+// NewDownloader creates a downloader with default settings which should
+// fit the most needs
+func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
+ tmp, _ := os.MkdirTemp("", "crunchy_")
+
+ return Downloader{
+ Writer: writer,
+ TempDir: tmp,
+ DeleteTempAfter: true,
+ Context: context,
+ Goroutines: goroutines,
+ OnSegmentDownload: onSegmentDownload,
+ }
+}
+
+// Downloader is used to download Format's
type Downloader struct {
- // Filename is the filename of the output file
- Filename string
- // TempDir is the directory where the temporary files should be stored
+ // The output is all written to Writer
+ Writer io.Writer
+
+ // TempDir is the directory where the temporary segment files should be stored.
+ // The files will be placed directly into the root of the directory.
+ // If empty a random temporary directory on the system's default tempdir
+ // will be created.
+ // If the directory does not exist, it will be created
TempDir string
- // If IgnoreExisting is true, existing Filename's and TempDir's may be
- // overwritten or deleted
- IgnoreExisting bool
// If DeleteTempAfter is true, the temp directory gets deleted afterwards.
// Note that in case of a hard signal exit (os.Interrupt, ...) the directory
// will NOT be deleted. In such situations try to catch the signal and
@@ -41,7 +65,10 @@ type Downloader struct {
// Goroutines is the number of goroutines to download segments with
Goroutines int
- // A method to call when a segment was downloaded
+ // A method to call when a segment was downloaded.
+ // Note that the segments are downloaded asynchronously (depending on the count of
+ // Goroutines) and the function gets called asynchronously too, so for example it is
+ // first called on segment 1, then segment 254, then segment 3 and so on
OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
// If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at
// once. Normally (because of the use of goroutines while downloading) multiple could get
@@ -50,51 +77,177 @@ type Downloader struct {
// If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files.
// The given opts will be used as ffmpeg parameters while merging.
- // Some opts are already used, see mergeSegmentsFFmpeg in format.go for more details
+ //
+ // If Writer is *os.File and -f (which sets the output format) is not specified, the output
+ // format will be retrieved by its file ending. If this is not the case and -f is not given,
+ // the output format will be mpegts / mpeg transport stream.
+ // Execute 'ffmpeg -muxers' to see all available output formats.
FFmpegOpts []string
}
-// NewDownloader creates a downloader with default settings which should
-// fit the most needs
-func NewDownloader(context context.Context, filename string, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
- tmp, _ := os.MkdirTemp("", "crunchy_")
-
- return Downloader{
- Filename: filename,
- TempDir: tmp,
- DeleteTempAfter: true,
- Context: context,
- Goroutines: goroutines,
- OnSegmentDownload: onSegmentDownload,
- }
-}
-
-// download downloads every mpeg transport stream segment to a given directory (more information below).
-// After every segment download onSegmentDownload will be called with:
-// the downloaded segment, the current position, the total size of segments to download, the file where the segment content was written to an error (if occurred).
-// The filename is always .ts
-//
-// Short explanation:
-// The actual crunchyroll video is split up in multiple segments (or video files) which have to be downloaded and merged after to generate a single video file.
-// And this function just downloads each of this segment into the given directory.
-// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
-func download(context context.Context, format *Format, tempDir string, goroutines int, lockOnSegmentDownload bool, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) error {
+// download's the given format
+func (d Downloader) download(format *Format) error {
if err := format.InitVideo(); err != nil {
return err
}
+ if _, err := os.Stat(d.TempDir); os.IsNotExist(err) {
+ if err := os.Mkdir(d.TempDir, 0700); err != nil {
+ return err
+ }
+ }
+ if d.DeleteTempAfter {
+ defer os.RemoveAll(d.TempDir)
+ }
+
+ files, err := d.downloadSegments(format)
+ if err != nil {
+ return err
+ }
+ if d.FFmpegOpts == nil {
+ return d.mergeSegments(files)
+ } else {
+ return d.mergeSegmentsFFmpeg(files)
+ }
+}
+
+// mergeSegments reads every file in tempDir and writes their content to Downloader.Writer.
+// The given output file gets created or overwritten if already existing
+func (d Downloader) mergeSegments(files []string) error {
+ for _, file := range files {
+ select {
+ case <-d.Context.Done():
+ return d.Context.Err()
+ default:
+ f, err := os.Open(file)
+ if err != nil {
+ return err
+ }
+ if _, err = io.Copy(d.Writer, f); err != nil {
+ f.Close()
+ return err
+ }
+ f.Close()
+ }
+ }
+ return nil
+}
+
+// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
+// with ffmpeg (https://ffmpeg.org/).
+// The given output file gets created or overwritten if already existing
+func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
+ list, err := os.Create(filepath.Join(d.TempDir, "list.txt"))
+ if err != nil {
+ return err
+ }
+
+ for _, file := range files {
+ if _, err = fmt.Fprintf(list, "file '%s'\n", file); err != nil {
+ list.Close()
+ return err
+ }
+ }
+ list.Close()
+
+ // predefined options ... custom options ... predefined output filename
+ command := []string{
+ "-y",
+ "-f", "concat",
+ "-safe", "0",
+ "-i", list.Name(),
+ "-c", "copy",
+ }
+ if d.FFmpegOpts != nil {
+ command = append(command, d.FFmpegOpts...)
+ var found bool
+ for _, opts := range d.FFmpegOpts {
+ if opts == "-f" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ if file, ok := d.Writer.(*os.File); ok {
+ var outBuf bytes.Buffer
+ infoCmd := exec.Command("ffmpeg", file.Name())
+ infoCmd.Stderr = &outBuf
+
+ if infoCmd.Run(); err != nil {
+ return err
+ }
+ if parsed := ffmpegInfoPattern.FindStringSubmatch(outBuf.String()); parsed != nil {
+ command = append(command, "-f", parsed[1])
+ }
+ } else {
+ command = append(command, "-f", "mpegts")
+ }
+ }
+ }
+ command = append(command, "pipe:1")
+
+ var errBuf bytes.Buffer
+ cmd := exec.Command("ffmpeg",
+ command...)
+ cmd.Stderr = &errBuf
+ // io.Copy may be better but this uses less code so ¯\_(ツ)_/¯
+ cmd.Stdout = d.Writer
+
+ if err = cmd.Start(); err != nil {
+ return err
+ }
+
+ cmdChan := make(chan error, 1)
+ go func() {
+ cmdChan <- cmd.Wait()
+ }()
+
+ select {
+ case err = <-cmdChan:
+ if err != nil {
+ if errBuf.Len() > 0 {
+ return fmt.Errorf(errBuf.String())
+ } else {
+ return err
+ }
+ }
+ return nil
+ case <-d.Context.Done():
+ cmd.Process.Kill()
+ return d.Context.Err()
+ }
+}
+
+// downloadSegments downloads every mpeg transport stream segment to a given
+// directory (more information below).
+// After every segment download onSegmentDownload will be called with:
+// the downloaded segment, the current position, the total size of segments to download,
+// the file where the segment content was written to an error (if occurred).
+// The filename is always .ts
+//
+// Short explanation:
+// The actual crunchyroll video is split up in multiple segments (or video files) which
+// have to be downloaded and merged after to generate a single video file.
+// And this function just downloads each of this segment into the given directory.
+// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
+func (d Downloader) downloadSegments(format *Format) ([]string, error) {
+ if err := format.InitVideo(); err != nil {
+ return nil, err
+ }
+
var wg sync.WaitGroup
var lock sync.Mutex
- chunkSize := int(math.Ceil(float64(format.Video.Chunklist.Count()) / float64(goroutines)))
+ chunkSize := int(math.Ceil(float64(format.Video.Chunklist.Count()) / float64(d.Goroutines)))
- // when a onSegmentDownload call returns an error, this channel will be set to true and stop all goroutines
- quit := make(chan bool)
+ // when a onSegmentDownload call returns an error, this context will be set cancelled and stop all goroutines
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
// receives the decrypt block and iv from the first segment.
// in my tests, only the first segment has specified this data, so the decryption data from this first segments will be used in every other segment too
block, iv, err := getCrypt(format, format.Video.Chunklist.Segments[0])
if err != nil {
- return err
+ return nil, err
}
var total int32
@@ -111,39 +264,42 @@ func download(context context.Context, format *Format, tempDir string, goroutine
for j, segment := range format.Video.Chunklist.Segments[i:end] {
select {
- case <-context.Done():
- return
- case <-quit:
+ case <-d.Context.Done():
+ case <-ctx.Done():
return
default:
var file *os.File
- k := 1
- for ; k < 4; k++ {
- file, err = downloadSegment(context, format, segment, filepath.Join(tempDir, fmt.Sprintf("%d.ts", i+j)), block, iv)
+ for k := 0; k < 3; k++ {
+ filename := filepath.Join(d.TempDir, fmt.Sprintf("%d.ts", i+j))
+ file, err = d.downloadSegment(format, segment, filename, block, iv)
if err == nil {
break
}
- // sleep if an error occurs. very useful because sometimes the connection times out
- time.Sleep(5 * time.Duration(k) * time.Second)
+ if k == 2 {
+ cancel()
+ return
+ }
+ select {
+ case <-d.Context.Done():
+ case <-ctx.Done():
+ return
+ case <-time.After(5 * time.Duration(k) * time.Second):
+ // sleep if an error occurs. very useful because sometimes the connection times out
+ }
}
- if k == 4 {
- quit <- true
- return
- }
- if onSegmentDownload != nil {
- if lockOnSegmentDownload {
+ if d.OnSegmentDownload != nil {
+ if d.LockOnSegmentDownload {
lock.Lock()
}
- if err = onSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), int(format.Video.Chunklist.Count()), file); err != nil {
- quit <- true
- if lockOnSegmentDownload {
+ if err = d.OnSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), int(format.Video.Chunklist.Count()), file); err != nil {
+ if d.LockOnSegmentDownload {
lock.Unlock()
}
file.Close()
return
}
- if lockOnSegmentDownload {
+ if d.LockOnSegmentDownload {
lock.Unlock()
}
}
@@ -155,16 +311,21 @@ func download(context context.Context, format *Format, tempDir string, goroutine
wg.Wait()
select {
- case <-context.Done():
- return context.Err()
- case <-quit:
- return err
+ case <-d.Context.Done():
+ return nil, d.Context.Err()
+ case <-ctx.Done():
+ return nil, err
default:
- return nil
+ var files []string
+ for i := 0; i < int(total); i++ {
+ files = append(files, filepath.Join(d.TempDir, fmt.Sprintf("%d.ts", i)))
+ }
+
+ return files, nil
}
}
-// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block block and a iv byte sequence
+// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence
func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
var resp *http.Response
@@ -188,9 +349,9 @@ func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, i
}
// downloadSegment downloads a segment, decrypts it and names it after the given index
-func downloadSegment(context context.Context, format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
+func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
// every segment is aes-128 encrypted and has to be decrypted when downloaded
- content, err := decryptSegment(context, format.crunchy.Client, segment, block, iv)
+ content, err := d.decryptSegment(format.crunchy.Client, segment, block, iv)
if err != nil {
return nil, err
}
@@ -208,12 +369,12 @@ func downloadSegment(context context.Context, format *Format, segment *m3u8.Medi
}
// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25
-func decryptSegment(context context.Context, client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
+func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
req, err := http.NewRequest(http.MethodGet, segment.URI, nil)
if err != nil {
return nil, err
}
- req.WithContext(context)
+ req.WithContext(d.Context)
resp, err := client.Do(req)
if err != nil {
@@ -229,13 +390,13 @@ func decryptSegment(context context.Context, client *http.Client, segment *m3u8.
blockMode := cipher.NewCBCDecrypter(block, iv[:block.BlockSize()])
decrypted := make([]byte, len(raw))
blockMode.CryptBlocks(decrypted, raw)
- raw = pkcs5UnPadding(decrypted)
+ raw = d.pkcs5UnPadding(decrypted)
return raw, nil
}
// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47
-func pkcs5UnPadding(origData []byte) []byte {
+func (d Downloader) pkcs5UnPadding(origData []byte) []byte {
length := len(origData)
unPadding := int(origData[length-1])
return origData[:(length - unPadding)]
diff --git a/format.go b/format.go
index 9dfaf3e..ec94c16 100644
--- a/format.go
+++ b/format.go
@@ -1,18 +1,7 @@
package crunchyroll
import (
- "bufio"
- "bytes"
- "context"
- "fmt"
"github.com/grafov/m3u8"
- "io/ioutil"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
)
type FormatType string
@@ -56,144 +45,7 @@ func (f *Format) InitVideo() error {
return nil
}
+// Download downloads the Format with the via Downloader specified options
func (f *Format) Download(downloader Downloader) error {
- if _, err := os.Stat(downloader.Filename); err == nil && !downloader.IgnoreExisting {
- return fmt.Errorf("file %s already exists", downloader.Filename)
- }
- if _, err := os.Stat(downloader.TempDir); err != nil {
- if os.IsNotExist(err) {
- err = os.Mkdir(downloader.TempDir, 0755)
- }
- if err != nil {
- return err
- }
- } else if !downloader.IgnoreExisting {
- content, err := os.ReadDir(downloader.TempDir)
- if err != nil {
- return err
- }
- if len(content) > 0 {
- return fmt.Errorf("directory %s is not empty", downloader.Filename)
- }
- }
-
- if downloader.DeleteTempAfter {
- defer os.RemoveAll(downloader.TempDir)
- }
- if err := download(downloader.Context, f, downloader.TempDir, downloader.Goroutines, downloader.LockOnSegmentDownload, downloader.OnSegmentDownload); err != nil {
- return err
- }
-
- if downloader.FFmpegOpts != nil {
- return mergeSegmentsFFmpeg(downloader.Context, downloader.TempDir, downloader.Filename, downloader.FFmpegOpts)
- } else {
- return mergeSegments(downloader.Context, downloader.TempDir, downloader.Filename)
- }
-}
-
-// mergeSegments reads every file in tempDir and writes their content to the outputFile.
-// The given output file gets created or overwritten if already existing
-func mergeSegments(context context.Context, tempDir string, outputFile string) error {
- dir, err := os.ReadDir(tempDir)
- if err != nil {
- return err
- }
- file, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY, 0755)
- if err != nil {
- return err
- }
- defer file.Close()
- writer := bufio.NewWriter(file)
- defer writer.Flush()
-
- // sort the directory files after their numeric names
- sort.Slice(dir, func(i, j int) bool {
- iNum, err := strconv.Atoi(strings.Split(dir[i].Name(), ".")[0])
- if err != nil {
- return false
- }
- jNum, err := strconv.Atoi(strings.Split(dir[j].Name(), ".")[0])
- if err != nil {
- return false
- }
- return iNum < jNum
- })
-
- for _, file := range dir {
- select {
- case <-context.Done():
- return context.Err()
- default:
- }
-
- bodyAsBytes, err := ioutil.ReadFile(filepath.Join(tempDir, file.Name()))
- if err != nil {
- return err
- }
- if _, err = writer.Write(bodyAsBytes); err != nil {
- return err
- }
- }
- return nil
-}
-
-// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
-// with ffmpeg (https://ffmpeg.org/).
-// The given output file gets created or overwritten if already existing
-func mergeSegmentsFFmpeg(context context.Context, tempDir string, outputFile string, opts []string) error {
- dir, err := os.ReadDir(tempDir)
- if err != nil {
- return err
- }
- f, err := os.Create(filepath.Join(tempDir, "list.txt"))
- if err != nil {
- return err
- }
- // -1 is the list.txt file
- for i := 0; i < len(dir)-1; i++ {
- fmt.Fprintf(f, "file '%s.ts'\n", filepath.Join(tempDir, strconv.Itoa(i)))
- }
- f.Close()
-
- // predefined options ... custom options ... predefined output filename
- command := []string{
- "-y",
- "-f", "concat",
- "-safe", "0",
- "-i", f.Name(),
- "-c", "copy",
- }
- if opts != nil {
- command = append(command, opts...)
- }
- command = append(command, outputFile)
-
- var errBuf bytes.Buffer
- cmd := exec.Command("ffmpeg",
- command...)
- cmd.Stderr = &errBuf
-
- if err := cmd.Start(); err != nil {
- return err
- }
-
- cmdChan := make(chan error, 1)
- go func() {
- cmdChan <- cmd.Wait()
- }()
-
- select {
- case err = <-cmdChan:
- if err != nil {
- if errBuf.Len() > 0 {
- return fmt.Errorf(errBuf.String())
- } else {
- return err
- }
- }
- return nil
- case <-context.Done():
- cmd.Process.Kill()
- return context.Err()
- }
+ return downloader.download(f)
}
From 5f5ec3858529821c0cf72a66d372274da3bd6091 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:04:27 +0100
Subject: [PATCH 060/732] Added function to sort episode by audio
---
utils/sort.go | 39 +++++++++++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/utils/sort.go b/utils/sort.go
index 16ce41a..90e05c7 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -5,6 +5,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
)
// SortEpisodesBySeason sorts the given episodes by their seasons.
@@ -42,6 +43,44 @@ func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Epis
return eps
}
+// SortEpisodesByAudio sort the given episodes by their audio locale
+func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCALE][]*crunchyroll.Episode, error) {
+ eps := map[crunchyroll.LOCALE][]*crunchyroll.Episode{}
+
+ errChan := make(chan error)
+
+ var wg sync.WaitGroup
+ var lock sync.Mutex
+ for _, episode := range episodes {
+ episode := episode
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ audioLocale, err := episode.AudioLocale()
+ if err != nil {
+ errChan <- err
+ return
+ }
+ lock.Lock()
+ defer lock.Unlock()
+
+ if _, ok := eps[audioLocale]; !ok {
+ eps[audioLocale] = make([]*crunchyroll.Episode, 0)
+ }
+ eps[audioLocale] = append(eps[audioLocale], episode)
+ }()
+ }
+ go func() {
+ wg.Wait()
+ errChan <- nil
+ }()
+
+ if err := <-errChan; err != nil {
+ return nil, err
+ }
+ return eps, nil
+}
+
// MovieListingsByDuration sorts movie listings by their duration
type MovieListingsByDuration []*crunchyroll.MovieListing
From e855c16a2f87d837afcb06203ba334f18edf1951 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:08:12 +0100
Subject: [PATCH 061/732] New progress bar calculate method and using utils
implementation for audio sorting
---
cmd/crunchyroll-go/cmd/utils.go | 44 ++++++++++++++-------------------
1 file changed, 19 insertions(+), 25 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 508e2e8..839611b 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -186,33 +186,20 @@ func generateFilename(name, directory string) string {
func extractEpisodes(url string, locales ...crunchyroll.LOCALE) [][]*crunchyroll.Episode {
final := make([][]*crunchyroll.Episode, len(locales))
- episodes, err := utils.ExtractEpisodesFromUrl(crunchy, url, locales...)
+ episodes, err := crunchy.ExtractEpisodesFromUrl(url, locales...)
if err != nil {
out.Err("Failed to get episodes: %v", err)
os.Exit(1)
}
- // fetch all episodes and sort them by their locale
- var wg sync.WaitGroup
- for _, episode := range episodes {
- episode := episode
- wg.Add(1)
- go func() {
- defer wg.Done()
- audioLocale, err := episode.AudioLocale()
- if err != nil {
- out.Err("Failed to get audio locale: %v", err)
- os.Exit(1)
- }
-
- for i, locale := range locales {
- if locale == audioLocale {
- final[i] = append(final[i], episode)
- }
- }
- }()
+ localeSorted, err := utils.SortEpisodesByAudio(episodes)
+ if err != nil {
+ out.Err("Failed to get audio locale: %v", err)
+ os.Exit(1)
+ }
+ for i, locale := range locales {
+ final[i] = append(final[i], localeSorted[locale]...)
}
- wg.Wait()
return final
}
@@ -306,14 +293,21 @@ func (dp *DownloadProgress) update(msg string, permanent bool) {
}
percentage := float32(dp.Current) / float32(dp.Total) * 100
- progressWidth := float32(terminalWidth() - (12 + len(dp.Prefix) + len(dp.Message)) - (len(fmt.Sprint(dp.Total)))*2)
- repeatCount := int(percentage / (float32(100) / progressWidth))
+ 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) + ">")[1:]
+ progressPercentage := strings.Repeat("=", repeatCount)
+ if dp.Current != dp.Total {
+ progressPercentage += ">"
+ }
- fmt.Printf("\r%s%s [%-"+fmt.Sprint(progressWidth)+"s]%4d%% %8d/%d", dp.Prefix, msg, progressPercentage, int(percentage), dp.Current, dp.Total)
+ fmt.Printf("\r%s%-"+fmt.Sprint(progressWidth)+"s%s", pre, progressPercentage, post)
}
From b9b9654d2cee86c39b58927ff547d7012b2471e2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:09:39 +0100
Subject: [PATCH 062/732] Renamed persistent credentials file and restricted
login message for windows users
---
cmd/crunchyroll-go/cmd/login.go | 4 ++--
cmd/crunchyroll-go/cmd/utils.go | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index f15bc5b..fa55020 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -64,11 +64,11 @@ func loginStorePath() string {
if loginPersistentFlag {
if runtime.GOOS != "windows" {
usr, _ := user.Current()
- path = filepath.Join(usr.HomeDir, ".config/crunchyroll-go")
+ path = filepath.Join(usr.HomeDir, ".config/crunchy")
}
out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", path)
- } else {
+ } else if runtime.GOOS != "windows" {
out.Info("Due to security reasons, you have to login again on the next reboot")
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 839611b..09b4a2f 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -100,7 +100,7 @@ func loadCrunchy() {
if runtime.GOOS != "windows" {
usr, _ := user.Current()
- files = append(files, filepath.Join(usr.HomeDir, ".config/crunchyroll-go"))
+ files = append(files, filepath.Join(usr.HomeDir, ".config/crunchy"))
}
var body []byte
From d42206b8f92fc86c0cde1deb15c2dc59458405a5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:10:16 +0100
Subject: [PATCH 063/732] Added documentation for GetFormat
---
episode.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/episode.go b/episode.go
index 12d21b9..6ab330b 100644
--- a/episode.go
+++ b/episode.go
@@ -119,6 +119,7 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil
}
+// GetFormat returns the format which matches the given resolution and subtitle locale
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
streams, err := e.Streams()
if err != nil {
From b1e5718c8d7428f3a9db04f491c96ef0473dee33 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 01:10:38 +0100
Subject: [PATCH 064/732] Added exit logging function
---
cmd/crunchyroll-go/cmd/logger.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index d9c7248..cf15f57 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -74,6 +74,10 @@ func (l *logger) Err(format string, v ...interface{}) {
l.ErrLog.Printf(format, v...)
}
+func (l *logger) Exit(format string, v ...interface{}) {
+ fmt.Fprintln(l.ErrLog.Writer(), fmt.Sprintf(format, v...))
+}
+
func (l *logger) Empty() {
if l.InfoLog.Writer() != io.Discard {
fmt.Println("")
From a378c93d68d6ed9e0c3647b0289016100f51c031 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 02:41:23 +0100
Subject: [PATCH 065/732] Made some structs private
---
cmd/crunchyroll-go/cmd/utils.go | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 09b4a2f..b9bc2d0 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -204,7 +204,7 @@ func extractEpisodes(url string, locales ...crunchyroll.LOCALE) [][]*crunchyroll
return final
}
-type FormatInformation struct {
+type formatInformation struct {
// the format to download
format *crunchyroll.Format
@@ -221,7 +221,7 @@ type FormatInformation struct {
Subtitle crunchyroll.LOCALE `json:"subtitle"`
}
-func (fi FormatInformation) Format(source string) string {
+func (fi formatInformation) Format(source string) string {
fields := reflect.TypeOf(fi)
values := reflect.ValueOf(fi)
@@ -246,7 +246,7 @@ func (fi FormatInformation) Format(source string) string {
return source
}
-type DownloadProgress struct {
+type downloadProgress struct {
Prefix string
Message string
@@ -259,15 +259,15 @@ type DownloadProgress struct {
lock sync.Mutex
}
-func (dp *DownloadProgress) Update() {
+func (dp *downloadProgress) Update() {
dp.update("", false)
}
-func (dp *DownloadProgress) UpdateMessage(msg string, permanent bool) {
+func (dp *downloadProgress) UpdateMessage(msg string, permanent bool) {
dp.update(msg, permanent)
}
-func (dp *DownloadProgress) update(msg string, permanent bool) {
+func (dp *downloadProgress) update(msg string, permanent bool) {
if dp.Quiet {
return
}
From 600eaeeecac7ec6e353d360e684b391b3fce9181 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 6 Mar 2022 16:04:44 +0100
Subject: [PATCH 066/732] Print empty line only if dev view is not enabled
---
cmd/crunchyroll-go/cmd/logger.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index cf15f57..22e4582 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -79,7 +79,7 @@ func (l *logger) Exit(format string, v ...interface{}) {
}
func (l *logger) Empty() {
- if l.InfoLog.Writer() != io.Discard {
+ if !l.devView && l.InfoLog.Writer() != io.Discard {
fmt.Println("")
}
}
From 60fed00b0e93158c939af9019454d543cbd44de2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 14 Mar 2022 23:28:52 +0100
Subject: [PATCH 067/732] Added .tar.* file ending
---
cmd/crunchyroll-go/cmd/utils.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index b9bc2d0..208b1d5 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"os/user"
- "path"
"path/filepath"
"reflect"
"runtime"
@@ -81,8 +80,13 @@ func createOrDefaultClient(proxy string) (*http.Client, error) {
}
func freeFileName(filename string) (string, bool) {
- ext := path.Ext(filename)
+ 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) {
From b1945d672d47a83b442d0ce556766bafbca1b15a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 14 Mar 2022 23:41:55 +0100
Subject: [PATCH 068/732] Changed filename processing when merging with ffmpeg
---
downloader.go | 92 +++++++++++++++++++++++----------------------------
1 file changed, 41 insertions(+), 51 deletions(-)
diff --git a/downloader.go b/downloader.go
index d0bdf7c..48460c3 100644
--- a/downloader.go
+++ b/downloader.go
@@ -14,14 +14,12 @@ import (
"os"
"os/exec"
"path/filepath"
- "regexp"
+ "strings"
"sync"
"sync/atomic"
"time"
)
-var ffmpegInfoPattern = regexp.MustCompile(`Output #0, (.+),`)
-
// NewDownloader creates a downloader with default settings which should
// fit the most needs
func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
@@ -160,62 +158,54 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
}
if d.FFmpegOpts != nil {
command = append(command, d.FFmpegOpts...)
- var found bool
+ }
+
+ var tmpfile string
+ if _, ok := d.Writer.(*io.PipeWriter); ok {
+ if file, ok := d.Writer.(*os.File); ok {
+ tmpfile = filepath.Base(file.Name())
+ }
+ }
+ if filepath.Ext(tmpfile) == "" {
+ // checks if the -f flag is set (overwrites the output format)
+ var hasF bool
for _, opts := range d.FFmpegOpts {
- if opts == "-f" {
- found = true
+ if strings.TrimSpace(opts) == "-f" {
+ hasF = true
break
}
}
- if !found {
- if file, ok := d.Writer.(*os.File); ok {
- var outBuf bytes.Buffer
- infoCmd := exec.Command("ffmpeg", file.Name())
- infoCmd.Stderr = &outBuf
-
- if infoCmd.Run(); err != nil {
- return err
- }
- if parsed := ffmpegInfoPattern.FindStringSubmatch(outBuf.String()); parsed != nil {
- command = append(command, "-f", parsed[1])
- }
- } else {
- command = append(command, "-f", "mpegts")
- }
- }
- }
- command = append(command, "pipe:1")
-
- var errBuf bytes.Buffer
- cmd := exec.Command("ffmpeg",
- command...)
- cmd.Stderr = &errBuf
- // io.Copy may be better but this uses less code so ¯\_(ツ)_/¯
- cmd.Stdout = d.Writer
-
- if err = cmd.Start(); err != nil {
- return err
- }
-
- cmdChan := make(chan error, 1)
- go func() {
- cmdChan <- cmd.Wait()
- }()
-
- select {
- case err = <-cmdChan:
- if err != nil {
- if errBuf.Len() > 0 {
- return fmt.Errorf(errBuf.String())
- } else {
+ if !hasF {
+ command = append(command, "-f", "matroska")
+ f, err := os.CreateTemp(d.TempDir, "")
+ if err != nil {
return err
}
+ f.Close()
+ tmpfile = f.Name()
}
- return nil
- case <-d.Context.Done():
- cmd.Process.Kill()
- return d.Context.Err()
}
+ command = append(command, tmpfile)
+
+ var errBuf bytes.Buffer
+ cmd := exec.CommandContext(d.Context, "ffmpeg",
+ command...)
+ cmd.Stderr = &errBuf
+
+ if err = cmd.Run(); err != nil {
+ if errBuf.Len() > 0 {
+ return fmt.Errorf(errBuf.String())
+ } else {
+ return err
+ }
+ }
+ file, err := os.Open(tmpfile)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ _, err = io.Copy(d.Writer, file)
+ return err
}
// downloadSegments downloads every mpeg transport stream segment to a given
From 4d538dfc0c0dae0eb0e333375f0aa70bec3ffc8e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 14 Mar 2022 23:42:28 +0100
Subject: [PATCH 069/732] Added actual error message to recover error output
---
cmd/crunchyroll-go/cmd/root.go | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index db2cba6..2fe3a61 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -1,6 +1,7 @@
package cmd
import (
+ "context"
"github.com/ByteDream/crunchyroll-go"
"github.com/spf13/cobra"
"net/http"
@@ -21,6 +22,10 @@ var (
var rootCmd = &cobra.Command{
Use: "crunchyroll",
Short: "Download crunchyroll videos with ease",
+
+ SilenceErrors: true,
+ SilenceUsage: true,
+
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
if verboseFlag {
out = newLogger(true, true, true)
@@ -46,14 +51,17 @@ func Execute() {
defer func() {
if r := recover(); r != nil {
if out.IsDev() {
- out.Err(string(debug.Stack()))
+ out.Err("%v: %s", r, debug.Stack())
} else {
out.Err("Unexpected error: %v", r)
}
os.Exit(1)
}
}()
- if rootCmd.Execute() != nil {
+ if err := rootCmd.Execute(); err != nil {
+ if err != context.Canceled {
+ out.Exit("An error occurred: %v", err)
+ }
os.Exit(1)
}
}
From aa1d72868b457e83adbe1c45557c11107577c81e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 13:05:57 +0100
Subject: [PATCH 070/732] Fixed debug stop message
---
cmd/crunchyroll-go/cmd/logger.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index 22e4582..45ddd15 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -117,7 +117,7 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
fmt.Printf("\r" + strings.Repeat(" ", 2+len(message)))
fmt.Printf("\r➞ %s\n", p.message)
} else {
- l.Debug(message)
+ l.Debug(p.message)
}
l.progress = nil
From 3ee5f7a538b1f51c4cc051586148737890b3b64a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 13:41:39 +0100
Subject: [PATCH 071/732] Added ids to ...FromID functions (how did this work
before??)
---
episode.go | 1 +
movie_listing.go | 1 +
season.go | 1 +
video.go | 2 ++
4 files changed, 5 insertions(+)
diff --git a/episode.go b/episode.go
index 6ab330b..d9814cb 100644
--- a/episode.go
+++ b/episode.go
@@ -94,6 +94,7 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
episode := &Episode{
crunchy: crunchy,
+ ID: id,
}
if err := decodeMapToStruct(jsonBody, episode); err != nil {
return nil, err
diff --git a/movie_listing.go b/movie_listing.go
index cab4e9d..9646c18 100644
--- a/movie_listing.go
+++ b/movie_listing.go
@@ -56,6 +56,7 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
movieListing := &MovieListing{
crunchy: crunchy,
+ ID: id,
}
if err = decodeMapToStruct(jsonBody, movieListing); err != nil {
return nil, err
diff --git a/season.go b/season.go
index 438236d..c9a17da 100644
--- a/season.go
+++ b/season.go
@@ -61,6 +61,7 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
season := &Season{
crunchy: crunchy,
+ ID: id,
}
if err := decodeMapToStruct(jsonBody, season); err != nil {
return nil, err
diff --git a/video.go b/video.go
index 1b83c90..f543df1 100644
--- a/video.go
+++ b/video.go
@@ -86,6 +86,7 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
movieListing := &Movie{
crunchy: crunchy,
}
+ movieListing.ID = id
if err = decodeMapToStruct(jsonBody, movieListing); err != nil {
return nil, err
}
@@ -182,6 +183,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
series := &Series{
crunchy: crunchy,
}
+ series.ID = id
if err = decodeMapToStruct(jsonBody, series); err != nil {
return nil, err
}
From 1d6b930d9a2cf82eb14c60a53a48654529d7a430 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 18:35:05 +0100
Subject: [PATCH 072/732] Added pattern to filter episodes from (series) urls
---
cmd/crunchyroll-go/cmd/utils.go | 77 ++++++++++++++++++++++++++++++---
1 file changed, 71 insertions(+), 6 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 208b1d5..d244e58 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -11,6 +11,7 @@ import (
"os/user"
"path/filepath"
"reflect"
+ "regexp"
"runtime"
"strconv"
"strings"
@@ -23,6 +24,8 @@ var (
invalidLinuxChars = []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() crunchyroll.LOCALE {
@@ -188,24 +191,86 @@ func generateFilename(name, directory string) string {
return filename
}
-func extractEpisodes(url string, locales ...crunchyroll.LOCALE) [][]*crunchyroll.Episode {
+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 {
- out.Err("Failed to get episodes: %v", err)
- os.Exit(1)
+ 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 {
+ continue
+ } else if toSeason != -1 && episode.SeasonNumber > toSeason {
+ continue
+ } else if fromEpisode != -1 && episode.EpisodeNumber < fromEpisode {
+ continue
+ } else if 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 {
- out.Err("Failed to get audio locale: %v", err)
- os.Exit(1)
+ 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
+ return final, nil
}
type formatInformation struct {
From 52a8f81356468be1233047decd9617e4b99bbcbe Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 22:02:06 +0100
Subject: [PATCH 073/732] Added wiki notice
---
cmd/crunchyroll-go/cmd/root.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 2fe3a61..3442597 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -21,7 +21,7 @@ var (
var rootCmd = &cobra.Command{
Use: "crunchyroll",
- Short: "Download crunchyroll videos with ease",
+ Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",
SilenceErrors: true,
SilenceUsage: true,
From 16fcf08f3474e7bacd16091a30d3b72807850223 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 22:13:28 +0100
Subject: [PATCH 074/732] Replaced ioutil package with io
---
cmd/crunchyroll-go/cmd/login.go | 5 ++---
crunchyroll.go | 8 ++++----
downloader.go | 5 ++---
3 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index fa55020..3bd4004 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -4,7 +4,6 @@ import (
"fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/spf13/cobra"
- "io/ioutil"
"os"
"os/user"
"path/filepath"
@@ -46,7 +45,7 @@ func loginCredentials(user, password string) error {
os.Exit(1)
}
- return ioutil.WriteFile(loginStorePath(), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600)
+ return os.WriteFile(loginStorePath(), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600)
}
func loginSessionID(sessionID string) error {
@@ -56,7 +55,7 @@ func loginSessionID(sessionID string) error {
os.Exit(1)
}
- return ioutil.WriteFile(loginStorePath(), []byte(sessionID), 0600)
+ return os.WriteFile(loginStorePath(), []byte(sessionID), 0600)
}
func loginStorePath() string {
diff --git a/crunchyroll.go b/crunchyroll.go
index 8c456a4..f59fb30 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -5,7 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/url"
"regexp"
@@ -68,7 +68,7 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
defer sessResp.Body.Close()
var data map[string]interface{}
- body, _ := ioutil.ReadAll(sessResp.Body)
+ body, _ := io.ReadAll(sessResp.Body)
json.Unmarshal(body, &data)
sessionID := data["data"].(map[string]interface{})["session_id"].(string)
@@ -211,7 +211,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
resp, err := c.Client.Do(req)
if err == nil {
- bodyAsBytes, _ := ioutil.ReadAll(resp.Body)
+ bodyAsBytes, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return nil, &AccessError{
@@ -231,7 +231,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
}
}
}
- resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyAsBytes))
+ resp.Body = io.NopCloser(bytes.NewBuffer(bodyAsBytes))
}
return resp, err
}
diff --git a/downloader.go b/downloader.go
index 48460c3..8c0278b 100644
--- a/downloader.go
+++ b/downloader.go
@@ -8,7 +8,6 @@ import (
"fmt"
"github.com/grafov/m3u8"
"io"
- "io/ioutil"
"math"
"net/http"
"os"
@@ -324,7 +323,7 @@ func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, i
return nil, nil, err
}
defer resp.Body.Close()
- key, err := ioutil.ReadAll(resp.Body)
+ key, err := io.ReadAll(resp.Body)
block, err = aes.NewCipher(key)
if err != nil {
@@ -372,7 +371,7 @@ func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegme
}
defer resp.Body.Close()
- raw, err := ioutil.ReadAll(resp.Body)
+ raw, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
From 73591cd75c35ccb3419d94cf91c96c0c9c9c8238 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 15 Mar 2022 22:50:48 +0100
Subject: [PATCH 075/732] Fixed episode filter not working properly
---
cmd/crunchyroll-go/cmd/utils.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index d244e58..9a61280 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -241,13 +241,13 @@ func extractEpisodes(url string, locales ...crunchyroll.LOCALE) ([][]*crunchyrol
tmpEps := make([]*crunchyroll.Episode, 0)
for _, episode := range episodes {
- if fromSeason != -1 && episode.SeasonNumber < fromSeason {
+ if fromSeason != -1 && (episode.SeasonNumber < fromSeason || (fromEpisode != -1 && episode.EpisodeNumber < fromEpisode)) {
continue
- } else if toSeason != -1 && episode.SeasonNumber > toSeason {
+ } else if fromSeason == -1 && fromEpisode != -1 && fromEpisode < episode.EpisodeNumber {
continue
- } else if fromEpisode != -1 && episode.EpisodeNumber < fromEpisode {
+ } else if toSeason != -1 && (episode.SeasonNumber > toSeason || (toEpisode != -1 && episode.EpisodeNumber > toEpisode)) {
continue
- } else if toEpisode != -1 && episode.EpisodeNumber > toEpisode {
+ } else if toSeason == -1 && toEpisode != -1 && episode.EpisodeNumber > toEpisode {
continue
} else {
tmpEps = append(tmpEps, episode)
From c822beca53bb54c2a28eb9c9a4770c8f75f69564 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 16 Mar 2022 17:25:35 +0100
Subject: [PATCH 076/732] Fixed episode filter episode number checking not
working properly
---
cmd/crunchyroll-go/cmd/utils.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 9a61280..d1d010d 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -243,7 +243,7 @@ func extractEpisodes(url string, locales ...crunchyroll.LOCALE) ([][]*crunchyrol
for _, episode := range episodes {
if fromSeason != -1 && (episode.SeasonNumber < fromSeason || (fromEpisode != -1 && episode.EpisodeNumber < fromEpisode)) {
continue
- } else if fromSeason == -1 && fromEpisode != -1 && fromEpisode < episode.EpisodeNumber {
+ } else if fromSeason == -1 && fromEpisode != -1 && episode.EpisodeNumber < fromEpisode {
continue
} else if toSeason != -1 && (episode.SeasonNumber > toSeason || (toEpisode != -1 && episode.EpisodeNumber > toEpisode)) {
continue
From cddb6ce03352bbfec5a6cfd15a3dcd39109ad0b0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 16 Mar 2022 20:48:40 +0100
Subject: [PATCH 077/732] Fixed not working invalid character replacing in file
name
---
cmd/crunchyroll-go/cmd/utils.go | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index d1d010d..f5ed9c1 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -20,8 +20,9 @@ import (
)
var (
- invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
- invalidLinuxChars = []string{"/"}
+ // ahh i love windows :)))
+ invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
+ invalidNotWindowsChars = []string{"/"}
)
var urlFilter = regexp.MustCompile(`(S(\d+))?(E(\d+))?((-)(S(\d+))?(E(\d+))?)?(,|$)`)
@@ -168,13 +169,13 @@ func terminalWidth() int {
func generateFilename(name, directory string) string {
if runtime.GOOS != "windows" {
- for _, char := range invalidLinuxChars {
- strings.ReplaceAll(name, char, "")
+ for _, char := range invalidNotWindowsChars {
+ name = strings.ReplaceAll(name, char, "")
}
out.Debug("Replaced invalid characters (not windows)")
} else {
for _, char := range invalidWindowsChars {
- strings.ReplaceAll(name, char, "")
+ name = strings.ReplaceAll(name, char, "")
}
out.Debug("Replaced invalid characters (windows)")
}
From 8525b913b80641f7a29448782694e8eff9a8e7ba Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Mar 2022 13:07:09 +0100
Subject: [PATCH 078/732] More FFmpeg optimizations when writer is a file
---
downloader.go | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/downloader.go b/downloader.go
index 8c0278b..d455f89 100644
--- a/downloader.go
+++ b/downloader.go
@@ -160,9 +160,9 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
}
var tmpfile string
- if _, ok := d.Writer.(*io.PipeWriter); ok {
+ if _, ok := d.Writer.(*io.PipeWriter); !ok {
if file, ok := d.Writer.(*os.File); ok {
- tmpfile = filepath.Base(file.Name())
+ tmpfile = file.Name()
}
}
if filepath.Ext(tmpfile) == "" {
@@ -198,12 +198,14 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
return err
}
}
- file, err := os.Open(tmpfile)
- if err != nil {
- return err
+ if f, ok := d.Writer.(*os.File); !ok || f.Name() != tmpfile {
+ file, err := os.Open(tmpfile)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ _, err = io.Copy(d.Writer, file)
}
- defer file.Close()
- _, err = io.Copy(d.Writer, file)
return err
}
From 668c6737506dbfbe1a7713de5889f263b1ca4f9a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Mar 2022 16:14:12 +0100
Subject: [PATCH 079/732] Added season name for output formatting
---
cmd/crunchyroll-go/cmd/utils.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index f5ed9c1..186f152 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -283,6 +283,7 @@ type formatInformation struct {
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"`
From dafa2f4d70ac832435d53e657ce852e1fe5a9498 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Mar 2022 17:08:39 +0100
Subject: [PATCH 080/732] Just some style changes
---
cmd/crunchyroll-go/cmd/login.go | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 3bd4004..38ae68e 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -11,9 +11,9 @@ import (
)
var (
- loginSessionIDFlag bool
-
loginPersistentFlag bool
+
+ loginSessionIDFlag bool
)
var loginCmd = &cobra.Command{
@@ -31,9 +31,15 @@ var loginCmd = &cobra.Command{
}
func init() {
- loginCmd.Flags().BoolVar(&loginSessionIDFlag, "session-id", false, "Use a session id to login instead of username and password")
+ loginCmd.Flags().BoolVar(&loginPersistentFlag,
+ "persistent",
+ false,
+ "If the given credential should be stored persistent")
- loginCmd.Flags().BoolVar(&loginPersistentFlag, "persistent", false, "If the given credential should be stored persistent")
+ loginCmd.Flags().BoolVar(&loginSessionIDFlag,
+ "session-id",
+ false,
+ "Use a session id to login instead of username and password")
rootCmd.AddCommand(loginCmd)
}
From e9fdd8fd437df2a1e7c66335fb0ba9a16cc2215c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Mar 2022 17:15:44 +0100
Subject: [PATCH 081/732] Added ability to format directory names too
---
cmd/crunchyroll-go/cmd/utils.go | 33 +++++++++++++++------------------
1 file changed, 15 insertions(+), 18 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 186f152..37f642c 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -167,23 +167,7 @@ func terminalWidth() int {
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)")
- }
-
- if directory != "" {
- name = filepath.Join(directory, name)
- }
-
+func generateFilename(name string) string {
filename, changed := freeFileName(name)
if changed {
out.Info("File %s already exists, changing name to %s", name, filename)
@@ -292,7 +276,7 @@ type formatInformation struct {
Subtitle crunchyroll.LOCALE `json:"subtitle"`
}
-func (fi formatInformation) Format(source string) string {
+func (fi formatInformation) Format(source string, removeInvalidChars bool) string {
fields := reflect.TypeOf(fi)
values := reflect.ValueOf(fi)
@@ -311,6 +295,19 @@ func (fi formatInformation) Format(source string) string {
valueAsString = "no " + valueAsString
}
}
+
+ if removeInvalidChars {
+ if runtime.GOOS != "windows" {
+ for _, char := range invalidNotWindowsChars {
+ valueAsString = strings.ReplaceAll(valueAsString, char, "")
+ }
+ } else {
+ for _, char := range invalidWindowsChars {
+ valueAsString = strings.ReplaceAll(valueAsString, char, "")
+ }
+ }
+ }
+
source = strings.ReplaceAll(source, "{"+fields.Field(i).Tag.Get("json")+"}", valueAsString)
}
From 31e03c1e089646d04e080a162d86cc8dc0260324 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 20 Mar 2022 17:34:13 +0100
Subject: [PATCH 082/732] Dependency updates
---
go.mod | 7 +-
go.sum | 754 +--------------------------------------------------------
2 files changed, 3 insertions(+), 758 deletions(-)
diff --git a/go.mod b/go.mod
index f5c3ea4..d3e701e 100644
--- a/go.mod
+++ b/go.mod
@@ -4,10 +4,5 @@ go 1.16
require (
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.3.0
-)
-
-require (
- github.com/inconshreveable/mousetrap v1.0.0 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
+ github.com/spf13/cobra v1.4.0
)
diff --git a/go.sum b/go.sum
index a1bf13c..34693ca 100644
--- a/go.sum
+++ b/go.sum
@@ -1,762 +1,12 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
-cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
-cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
-cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
-cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
-cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
-cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
-cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
-github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
-github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
-github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
-github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=
-github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
-github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
-github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=
-github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
-github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
-github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=
-github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=
-github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
-github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
-github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
-github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
-github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
-github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
-github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.3.0 h1:R7cSvGu+Vv+qX0gW5R/85dx2kmmJT5z5NM8ifdYjdn0=
-github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=
-github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
+github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
-github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8=
-github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
-google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
-google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
-google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
-google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
-google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
-google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
-google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
-google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
-google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
-google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
-google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
-google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
-google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
-google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
From 98f5da3bf367a570a582b03f821c48d870e17e5b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 10:11:33 +0100
Subject: [PATCH 083/732] Reverted and extended generateFilename function
---
cmd/crunchyroll-go/cmd/utils.go | 40 ++++++++++++++++++++-------------
1 file changed, 25 insertions(+), 15 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 37f642c..032d066 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -21,7 +21,7 @@ import (
var (
// ahh i love windows :)))
- invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
+ invalidWindowsChars = []string{"\\", "<", ">", ":", "\"", "/", "|", "?", "*"}
invalidNotWindowsChars = []string{"/"}
)
@@ -167,7 +167,29 @@ func terminalWidth() int {
return 60
}
-func generateFilename(name string) string {
+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, "")
+ }
+ // this needs only to be done on windows lol :)
+ if directory != "" {
+ for _, char := range invalidWindowsChars[1:] {
+ directory = strings.ReplaceAll(directory, char, "")
+ }
+ }
+ out.Debug("Replaced invalid characters (windows)")
+ }
+
+ if directory != "" {
+ name = filepath.Join(directory, name)
+ }
+
filename, changed := freeFileName(name)
if changed {
out.Info("File %s already exists, changing name to %s", name, filename)
@@ -276,7 +298,7 @@ type formatInformation struct {
Subtitle crunchyroll.LOCALE `json:"subtitle"`
}
-func (fi formatInformation) Format(source string, removeInvalidChars bool) string {
+func (fi formatInformation) Format(source string) string {
fields := reflect.TypeOf(fi)
values := reflect.ValueOf(fi)
@@ -296,18 +318,6 @@ func (fi formatInformation) Format(source string, removeInvalidChars bool) strin
}
}
- if removeInvalidChars {
- if runtime.GOOS != "windows" {
- for _, char := range invalidNotWindowsChars {
- valueAsString = strings.ReplaceAll(valueAsString, char, "")
- }
- } else {
- for _, char := range invalidWindowsChars {
- valueAsString = strings.ReplaceAll(valueAsString, char, "")
- }
- }
- }
-
source = strings.ReplaceAll(source, "{"+fields.Field(i).Tag.Get("json")+"}", valueAsString)
}
From 588d402c631d0f62a91a4fe628d4778a6fe347c5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 14:30:04 +0100
Subject: [PATCH 084/732] Just some style corrections
---
downloader.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/downloader.go b/downloader.go
index d455f89..48c0295 100644
--- a/downloader.go
+++ b/downloader.go
@@ -89,7 +89,7 @@ func (d Downloader) download(format *Format) error {
}
if _, err := os.Stat(d.TempDir); os.IsNotExist(err) {
- if err := os.Mkdir(d.TempDir, 0700); err != nil {
+ if err = os.Mkdir(d.TempDir, 0700); err != nil {
return err
}
}
@@ -267,12 +267,14 @@ func (d Downloader) downloadSegments(format *Format) ([]string, error) {
break
}
if k == 2 {
+ file.Close()
cancel()
return
}
select {
case <-d.Context.Done():
case <-ctx.Done():
+ file.Close()
return
case <-time.After(5 * time.Duration(k) * time.Second):
// sleep if an error occurs. very useful because sometimes the connection times out
From a6c14cb6c9e62a5518eb07daac5bce8fdbb6cb8e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 16:40:00 +0100
Subject: [PATCH 085/732] Reworked logger progress
---
cmd/crunchyroll-go/cmd/logger.go | 78 ++++++++++++++++----------------
1 file changed, 39 insertions(+), 39 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index 45ddd15..750ffc9 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -1,12 +1,12 @@
package cmd
import (
- "context"
"fmt"
"io"
"log"
"os"
"strings"
+ "sync"
"time"
)
@@ -24,6 +24,7 @@ type logger struct {
progress chan progress
done chan interface{}
+ lock sync.Mutex
}
func newLogger(debug, info, err bool) *logger {
@@ -87,69 +88,66 @@ func (l *logger) Empty() {
func (l *logger) SetProgress(format string, v ...interface{}) {
if out.InfoLog.Writer() == io.Discard {
return
- }
-
- message := fmt.Sprintf(format, v...)
-
- if l.progress != nil {
- l.progress <- progress{
- message: message,
- stop: false,
- }
+ } else if l.devView {
+ l.Debug(format, v...)
return
}
- l.progress = make(chan progress)
- l.done = make(chan interface{})
+ initialMessage := fmt.Sprintf(format, v...)
+
+ p := progress{
+ message: initialMessage,
+ }
+
+ l.lock.Lock()
+ if l.done != nil {
+ l.progress <- p
+ return
+ } else {
+ l.progress = make(chan progress, 1)
+ l.progress <- p
+ l.done = make(chan interface{})
+ }
go func() {
states := []string{"-", "\\", "|", "/"}
+
var count int
for i := 0; ; i++ {
- ctx, cancel := context.WithTimeout(context.Background(), 35*time.Millisecond)
select {
case p := <-l.progress:
- cancel()
-
if p.stop {
- if !l.devView {
- fmt.Printf("\r" + strings.Repeat(" ", 2+len(message)))
- fmt.Printf("\r➞ %s\n", p.message)
+ fmt.Printf("\r" + strings.Repeat(" ", 2+len(initialMessage)))
+ if count > 1 {
+ fmt.Printf("\r↳ %s\n", p.message)
} else {
- l.Debug(p.message)
+ fmt.Printf("\r➞ %s\n", p.message)
}
+ if l.done != nil {
+ l.done <- nil
+ }
l.progress = nil
- if count > 0 {
- fmt.Printf("↳ %s\n", p.message)
- }
-
- l.done <- nil
+ l.lock.Unlock()
return
} else {
- if !l.devView {
- fmt.Printf("\r↓ %s\n", message)
- } else {
- l.Debug(message)
+ if count > 0 {
+ fmt.Printf("\r↓ %s\n", p.message)
}
+ l.progress = make(chan progress, 1)
- l.progress = make(chan progress)
count++
- if !l.devView {
- fmt.Printf("\r" + strings.Repeat(" ", 2+len(message)))
- fmt.Printf("\r➞ %s\n", p.message)
- } else {
- l.Debug(p.message)
- }
- message = p.message
+ fmt.Printf("\r%s %s", states[i/10%4], initialMessage)
+ l.lock.Unlock()
}
- case <-ctx.Done():
- if !l.devView && i%10 == 0 {
- fmt.Printf("\r%s %s", states[i/10%4], message)
+ default:
+ if i%10 == 0 {
+ fmt.Printf("\r%s %s", states[i/10%4], initialMessage)
}
+ time.Sleep(35 * time.Millisecond)
}
}
}()
@@ -160,9 +158,11 @@ func (l *logger) StopProgress(format string, v ...interface{}) {
return
}
+ l.lock.Lock()
l.progress <- progress{
message: fmt.Sprintf(format, v...),
stop: true,
}
<-l.done
+ l.done = nil
}
From ac7904668f212cfadb58fb98248648c8aa775bc5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 16:55:27 +0100
Subject: [PATCH 086/732] Added context to the crunchyroll struct
---
crunchyroll.go | 4 ++++
downloader.go | 3 +--
subtitle.go | 8 +++++++-
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index f59fb30..82cd118 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -2,6 +2,7 @@ package crunchyroll
import (
"bytes"
+ "context"
"encoding/json"
"errors"
"fmt"
@@ -32,6 +33,8 @@ const (
type Crunchyroll struct {
// Client is the http.Client to perform all requests over
Client *http.Client
+ // Context can be used to stop requests with Client and is context.Background by default
+ Context context.Context
// Locale specifies in which language all results should be returned / requested
Locale LOCALE
// SessionID is the crunchyroll session id which was used for authentication
@@ -88,6 +91,7 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
crunchy := &Crunchyroll{
Client: client,
+ Context: context.Background(),
Locale: locale,
SessionID: sessionID,
cache: true,
diff --git a/downloader.go b/downloader.go
index 48c0295..be625b3 100644
--- a/downloader.go
+++ b/downloader.go
@@ -363,11 +363,10 @@ func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment,
// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25
func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
- req, err := http.NewRequest(http.MethodGet, segment.URI, nil)
+ req, err := http.NewRequestWithContext(d.Context, http.MethodGet, segment.URI, nil)
if err != nil {
return nil, err
}
- req.WithContext(d.Context)
resp, err := client.Do(req)
if err != nil {
diff --git a/subtitle.go b/subtitle.go
index 06ec9aa..6a33e14 100644
--- a/subtitle.go
+++ b/subtitle.go
@@ -2,6 +2,7 @@ package crunchyroll
import (
"io"
+ "net/http"
)
type Subtitle struct {
@@ -13,7 +14,12 @@ type Subtitle struct {
}
func (s Subtitle) Save(writer io.Writer) error {
- resp, err := s.crunchy.Client.Get(s.URL)
+ req, err := http.NewRequestWithContext(s.crunchy.Context, http.MethodGet, s.URL, nil)
+ if err != nil {
+ return err
+ }
+
+ resp, err := s.crunchy.Client.Do(req)
if err != nil {
return err
}
From f6f51799d774c2f0089ef9c64412d22be6de6c99 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 17:11:01 +0100
Subject: [PATCH 087/732] Extended checking if error is context cancelling
---
cmd/crunchyroll-go/cmd/root.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 3442597..25c8270 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -7,6 +7,7 @@ import (
"net/http"
"os"
"runtime/debug"
+ "strings"
)
var (
@@ -59,7 +60,7 @@ func Execute() {
}
}()
if err := rootCmd.Execute(); err != nil {
- if err != context.Canceled {
+ if !strings.HasSuffix(err.Error(), context.Canceled.Error()) {
out.Exit("An error occurred: %v", err)
}
os.Exit(1)
From d090a5a1daa781be9aac83950a8318267ccd5317 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 19:36:19 +0100
Subject: [PATCH 088/732] Windows things
---
cmd/crunchyroll-go/cmd/logger.go | 32 +++++++++++++++++++++++++++-----
1 file changed, 27 insertions(+), 5 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index 750ffc9..8c549f6 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -5,11 +5,31 @@ import (
"io"
"log"
"os"
+ "runtime"
"strings"
"sync"
"time"
)
+var prefix, progressDown, progressDownFinish string
+
+func initPrefixBecauseWindowsSucksBallsHard() {
+ // dear windows user, please change to a good OS, linux in the best case.
+ // MICROSHIT DOES NOT GET IT DONE TO SHOW THE SYMBOLS IN THE ELSE CLAUSE
+ // CORRECTLY. NOT IN THE CMD NOR POWERSHELL. WHY TF, IT IS ONE OF THE MOST
+ // PROFITABLE COMPANIES ON THIS PLANET AND CANNOT SHOW A PROPER UTF-8 SYMBOL
+ // IN THEIR OWN PRODUCT WHICH GETS USED MILLION TIMES A DAY
+ if runtime.GOOS == "windows" {
+ prefix = "=>"
+ progressDown = "|"
+ progressDownFinish = "->"
+ } else {
+ prefix = "➞"
+ progressDown = "↓"
+ progressDownFinish = "↳"
+ }
+}
+
type progress struct {
message string
stop bool
@@ -28,7 +48,9 @@ type logger struct {
}
func newLogger(debug, info, err bool) *logger {
- debugLog, infoLog, errLog := log.New(io.Discard, "➞ ", 0), log.New(io.Discard, "➞ ", 0), log.New(io.Discard, "➞ ", 0)
+ initPrefixBecauseWindowsSucksBallsHard()
+
+ debugLog, infoLog, errLog := log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0)
if debug {
debugLog.SetOutput(os.Stdout)
@@ -118,11 +140,11 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
select {
case p := <-l.progress:
if p.stop {
- fmt.Printf("\r" + strings.Repeat(" ", 2+len(initialMessage)))
+ fmt.Printf("\r" + strings.Repeat(" ", len(prefix)+len(initialMessage)))
if count > 1 {
- fmt.Printf("\r↳ %s\n", p.message)
+ fmt.Printf("\r%s %s\n", progressDownFinish, p.message)
} else {
- fmt.Printf("\r➞ %s\n", p.message)
+ fmt.Printf("\r%s %s\n", prefix, p.message)
}
if l.done != nil {
@@ -134,7 +156,7 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
return
} else {
if count > 0 {
- fmt.Printf("\r↓ %s\n", p.message)
+ fmt.Printf("\r%s %s\n", progressDown, p.message)
}
l.progress = make(chan progress, 1)
From ba8054b611fd1140530888c9c59b3d5c2c280fa1 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 19:36:39 +0100
Subject: [PATCH 089/732] Optimized filename generation
---
cmd/crunchyroll-go/cmd/utils.go | 24 +++++++++++++-----------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 032d066..7af5b3e 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -177,20 +177,10 @@ func generateFilename(name, directory string) string {
for _, char := range invalidWindowsChars {
name = strings.ReplaceAll(name, char, "")
}
- // this needs only to be done on windows lol :)
- if directory != "" {
- for _, char := range invalidWindowsChars[1:] {
- directory = strings.ReplaceAll(directory, char, "")
- }
- }
out.Debug("Replaced invalid characters (windows)")
}
- if directory != "" {
- name = filepath.Join(directory, name)
- }
-
- filename, changed := freeFileName(name)
+ filename, changed := freeFileName(filepath.Join(directory, name))
if changed {
out.Info("File %s already exists, changing name to %s", name, filename)
}
@@ -318,6 +308,18 @@ func (fi formatInformation) Format(source string) string {
}
}
+ 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)
}
From 83c9ae4927877a1cbdf71f142a0fcd5365930979 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 21 Mar 2022 20:45:38 +0100
Subject: [PATCH 090/732] Made verbose output optional when receiving system
locale
---
cmd/crunchyroll-go/cmd/login.go | 4 ++--
cmd/crunchyroll-go/cmd/utils.go | 18 ++++++++++++------
2 files changed, 14 insertions(+), 8 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 38ae68e..1303516 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -46,7 +46,7 @@ func init() {
func loginCredentials(user, password string) error {
out.Debug("Logging in via credentials")
- if _, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(), client); err != nil {
+ if _, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client); err != nil {
out.Err(err.Error())
os.Exit(1)
}
@@ -56,7 +56,7 @@ func loginCredentials(user, password string) error {
func loginSessionID(sessionID string) error {
out.Debug("Logging in via session id")
- if _, err := crunchyroll.LoginWithSessionID(sessionID, systemLocale(), client); err != nil {
+ if _, err := crunchyroll.LoginWithSessionID(sessionID, systemLocale(false), client); err != nil {
out.Err(err.Error())
os.Exit(1)
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 7af5b3e..7008cf7 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -29,14 +29,16 @@ 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() crunchyroll.LOCALE {
+func systemLocale(verbose bool) crunchyroll.LOCALE {
if runtime.GOOS != "windows" {
if lang, ok := os.LookupEnv("LANG"); ok {
prefix := strings.Split(lang, "_")[0]
suffix := strings.Split(strings.Split(lang, ".")[0], "_")[1]
l := crunchyroll.LOCALE(fmt.Sprintf("%s-%s", prefix, suffix))
if !utils.ValidateLocale(l) {
- out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
+ if verbose {
+ out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
+ }
l = crunchyroll.US
}
return l
@@ -46,13 +48,17 @@ func systemLocale() crunchyroll.LOCALE {
if output, err := cmd.Output(); err == nil {
l := crunchyroll.LOCALE(strings.Trim(string(output), "\r\n"))
if !utils.ValidateLocale(l) {
- out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
+ if verbose {
+ out.Err("%s is not a supported locale, using %s as fallback", l, crunchyroll.US)
+ }
l = crunchyroll.US
}
return l
}
}
- out.Err("Failed to get locale, using %s", crunchyroll.US)
+ if verbose {
+ out.Err("Failed to get locale, using %s", crunchyroll.US)
+ }
return crunchyroll.US
}
@@ -130,13 +136,13 @@ func loadCrunchy() {
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
- if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(), client); err != nil {
+ if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
}
out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
} else {
- if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(), client); err != nil {
+ if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
}
From c1b3cd7b0a4f0c7fd9484d863a28054926f136b4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 22 Mar 2022 13:10:42 +0100
Subject: [PATCH 091/732] Fixed logger softlock
---
cmd/crunchyroll-go/cmd/logger.go | 3 +++
cmd/crunchyroll-go/cmd/utils.go | 4 ++--
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/cmd/crunchyroll-go/cmd/logger.go
index 8c549f6..83bc214 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/cmd/crunchyroll-go/cmd/logger.go
@@ -178,6 +178,9 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
func (l *logger) StopProgress(format string, v ...interface{}) {
if out.InfoLog.Writer() == io.Discard {
return
+ } else if l.devView {
+ l.Debug(format, v...)
+ return
}
l.lock.Lock()
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 7008cf7..398b457 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -127,10 +127,10 @@ func loadCrunchy() {
break
}
if body == nil {
- out.Err("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
+ out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
os.Exit(1)
} else if err != nil {
- out.Err("Failed to read login information: %v", err)
+ out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
}
From 893be7dd5306c594ba0ec9cb82b0b212959eff66 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 22 Mar 2022 19:22:42 +0100
Subject: [PATCH 092/732] Made free filename silent by default and changed
renaming behavior
---
cmd/crunchyroll-go/cmd/utils.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 398b457..bf70564 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -102,7 +102,7 @@ func freeFileName(filename string) (string, bool) {
if _, stat := os.Stat(filename); stat != nil && !os.IsExist(stat) {
break
}
- filename = fmt.Sprintf("%s (%d)%s", base, j, ext)
+ filename = fmt.Sprintf("%s (%d)%s", base, j+1, ext)
}
return filename, j != 0
}
@@ -188,7 +188,7 @@ func generateFilename(name, directory string) string {
filename, changed := freeFileName(filepath.Join(directory, name))
if changed {
- out.Info("File %s already exists, changing name to %s", name, filename)
+ out.Debug("File `%s` already exists, changing name to `%s`", filepath.Base(name), filepath.Base(filename))
}
return filename
From f65cef84ca65917720ff3aff6f59df7eba7dd0c1 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 22 Mar 2022 19:39:33 +0100
Subject: [PATCH 093/732] Rewrote complete download command
---
cmd/crunchyroll-go/cmd/download.go | 974 ++++++++---------------------
1 file changed, 266 insertions(+), 708 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 0465f78..a3d78a8 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -1,40 +1,31 @@
package cmd
import (
- "encoding/json"
+ "context"
"fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/ByteDream/crunchyroll-go/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"os"
- "os/exec"
"os/signal"
- "path"
"path/filepath"
- "reflect"
- "regexp"
"runtime"
"sort"
"strconv"
"strings"
- "syscall"
)
var (
- audioFlag string
- subtitleFlag string
- noHardsubFlag bool
- onlySubFlag bool
+ downloadAudioFlag string
+ downloadSubtitleFlag string
- directoryFlag string
- outputFlag string
+ downloadDirectoryFlag string
+ downloadOutputFlag string
- resolutionFlag string
+ downloadResolutionFlag string
- alternativeProgressFlag bool
-
- goroutinesFlag int
+ downloadGoroutinesFlag int
)
var getCmd = &cobra.Command{
@@ -42,721 +33,288 @@ var getCmd = &cobra.Command{
Short: "Download a video",
Args: cobra.MinimumNArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ out.Debug("Validating arguments")
+
+ if filepath.Ext(downloadOutputFlag) != ".ts" {
+ if !hasFFmpeg() {
+ 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)
+ } else {
+ out.Debug("Custom file ending '%s' (ffmpeg is installed)", filepath.Ext(downloadOutputFlag))
+ }
+ }
+
+ if !utils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
+ return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(allLocalesAsStrings(), ", "))
+ } else if downloadSubtitleFlag != "" && !utils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
+ return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(allLocalesAsStrings(), ", "))
+ }
+ out.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
+
+ switch downloadResolutionFlag {
+ case "1080p", "720p", "480p", "360p", "240p":
+ intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
+ downloadResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(downloadResolutionFlag, "p"))
+ case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
+ default:
+ return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)
+ }
+ out.Debug("Using resolution '%s'", downloadResolutionFlag)
+
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
loadCrunchy()
- sig := make(chan os.Signal)
- signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
- go func() {
- <-sig
- if cleanupPath != "" {
- os.RemoveAll(cleanupPath)
- }
- os.Exit(1)
- }()
-
- download(args)
+ return download(args)
},
}
-var (
- invalidWindowsChars = []string{"<", ">", ":", "\"", "/", "|", "\\", "?", "*"}
- invalidLinuxChars = []string{"/"}
-)
-
-var cleanupPath string
-
func init() {
- rootCmd.AddCommand(getCmd)
- getCmd.Flags().StringVarP(&audioFlag, "audio", "a", "", "The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
- getCmd.Flags().StringVarP(&subtitleFlag, "subtitle", "s", "", "The locale of the subtitle. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
- getCmd.Flags().BoolVar(&noHardsubFlag, "no-hardsub", false, "Same as '-s', but the subtitles are not stored in the video itself, but in a separate file")
- getCmd.Flags().BoolVar(&onlySubFlag, "only-sub", false, "Downloads only the subtitles without the corresponding video")
+ getCmd.Flags().StringVarP(&downloadAudioFlag, "audio",
+ "a",
+ string(systemLocale(false)),
+ "The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
+ getCmd.Flags().StringVarP(&downloadSubtitleFlag,
+ "subtitle",
+ "s",
+ "",
+ "The locale of the subtitle. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
cwd, _ := os.Getwd()
- getCmd.Flags().StringVarP(&directoryFlag, "directory", "d", cwd, "The directory to download the file to")
- getCmd.Flags().StringVarP(&outputFlag, "output", "o", "{title}.ts", "Name of the output file\n"+
- "If you use the following things in the name, the will get replaced\n"+
- "\t{title} » Title of the video\n"+
- "\t{series_name} » Name of the series\n"+
- "\t{season_title} » Title of the season\n"+
- "\t{season_number} » Number of the season\n"+
- "\t{episode_number} » Number of the episode\n"+
- "\t{resolution} » Resolution of the video\n"+
- "\t{fps} » Frame Rate of the video\n"+
- "\t{audio} » Audio locale of the video\n"+
- "\t{subtitle} » Subtitle locale of the video\n")
+ getCmd.Flags().StringVarP(&downloadDirectoryFlag,
+ "directory",
+ "d",
+ cwd,
+ "The directory to download the file(s) into")
+ getCmd.Flags().StringVarP(&downloadOutputFlag,
+ "output",
+ "o",
+ "{title}.ts",
+ "Name of the output file. "+
+ "If you use the following things in the name, the will get replaced:\n"+
+ "\t{title} » Title of the video\n"+
+ "\t{series_name} » Name of the series\n"+
+ "\t{season_name} » Name of the season\n"+
+ "\t{season_number} » Number of the season\n"+
+ "\t{episode_number} » Number of the episode\n"+
+ "\t{resolution} » Resolution of the video\n"+
+ "\t{fps} » Frame Rate of the video\n"+
+ "\t{audio} » Audio locale of the video\n"+
+ "\t{subtitle} » Subtitle locale of the video")
- getCmd.Flags().StringVarP(&resolutionFlag, "resolution", "r", "best", "The video resolution. Can either be specified via the pixels, the abbreviation for pixels, or 'common-use' words\n"+
- "\tAvailable pixels: 1920x1080, 1280x720, 640x480, 480x360, 426x240\n"+
- "\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
- "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)\n")
+ getCmd.Flags().StringVarP(&downloadResolutionFlag,
+ "resolution",
+ "r",
+ "best",
+ "The video resolution. Can either be specified via the pixels, the abbreviation for pixels, or 'common-use' words\n"+
+ "\tAvailable pixels: 1920x1080, 1280x720, 640x480, 480x360, 428x240\n"+
+ "\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
+ "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
- getCmd.Flags().BoolVar(&alternativeProgressFlag, "alternative-progress", false, "Shows an alternative, not so user-friendly progress instead of the progress bar")
+ getCmd.Flags().IntVarP(&downloadGoroutinesFlag,
+ "goroutines",
+ "g",
+ runtime.NumCPU(),
+ "Sets how many parallel segment downloads should be used")
- // TODO: Rename this to something understandable (for "normal" users)
- getCmd.Flags().IntVarP(&goroutinesFlag, "goroutines", "g", 4, "Sets how many parallel segment downloads should be used")
+ rootCmd.AddCommand(getCmd)
}
-type episodeInformation struct {
- Format *crunchyroll.Format
- Title string
- URL string
- SeriesTitle string
- SeasonNum int
- EpisodeNum int
- AllSubtitles []*crunchyroll.Subtitle
-}
-
-type information struct {
- Title string `json:"title"`
- SeriesName string `json:"series_name"`
- SeasonNumber int `json:"season_number"`
- EpisodeNumber int `json:"episode_number"`
- OriginalURL string `json:"original_url"`
- DownloadURL string `json:"download_url"`
- Resolution string `json:"resolution"`
- FPS float64 `json:"fps"`
- Audio crunchyroll.LOCALE `json:"audio"`
- Subtitle crunchyroll.LOCALE `json:"subtitle"`
- Hardsub bool `json:"hardsub"`
-}
-
-func download(urls []string) {
- switch path.Ext(outputFlag) {
- case ".ts":
- // checks if only subtitles should be downloaded and if so, if the output flag has the default value
- if onlySubFlag && outputFlag == "{title}.ts" {
- outputFlag = "{title}.ass"
- }
- break
- case ".ass":
- if !onlySubFlag {
- break
- }
- fallthrough
- default:
- if !hasFFmpeg() {
- out.Fatalf("The file ending for the output file (%s) is not `.ts`. "+
- "Install ffmpeg (https://ffmpeg.org/download.html) use other media file endings (e.g. `.mp4`)\n", outputFlag)
- }
- }
- allEpisodes, total, successes := parseURLs(urls)
- out.Infof("%d of %d episodes could be parsed\n", successes, total)
-
- out.Empty()
- if len(allEpisodes) == 0 {
- out.Fatalf("Nothing to download, aborting\n")
- }
- if onlySubFlag {
- out.Infof("Downloads (only subtitles):")
- } else {
- out.Infof("Downloads:")
- }
- for i, episode := range allEpisodes {
- video := episode.Format.Video
- if onlySubFlag && subtitleFlag == "" {
- out.Infof("\t%d. %s » %spx, %.2f FPS (%s S%02dE%02d)\n",
- i+1, episode.Title, video.Resolution, video.FrameRate, episode.SeriesTitle, episode.SeasonNum, episode.EpisodeNum)
- } else {
- out.Infof("\t%d. %s » %spx, %.2f FPS, %s audio (%s S%02dE%02d)\n",
- i+1, episode.Title, video.Resolution, video.FrameRate, utils.LocaleLanguage(episode.Format.AudioLocale), episode.SeriesTitle, episode.SeasonNum, episode.EpisodeNum)
- }
- }
- out.Empty()
-
- if fileInfo, stat := os.Stat(directoryFlag); os.IsNotExist(stat) {
- if err := os.MkdirAll(directoryFlag, 0777); err != nil {
- out.Fatalf("Failed to create directory which was given from the `-d`/`--directory` flag: %s\n", err)
- }
- } else if !fileInfo.IsDir() {
- out.Fatalf("%s (given from the `-d`/`--directory` flag) is not a directory\n", directoryFlag)
- }
-
- var success int
- for _, episode := range allEpisodes {
- var subtitle crunchyroll.LOCALE
- if subtitleFlag != "" {
- subtitle = localeToLOCALE(subtitleFlag)
- }
- info := information{
- Title: episode.Title,
- SeriesName: episode.SeriesTitle,
- SeasonNumber: episode.SeasonNum,
- EpisodeNumber: episode.EpisodeNum,
- OriginalURL: episode.URL,
- DownloadURL: episode.Format.Video.URI,
- Resolution: episode.Format.Video.Resolution,
- FPS: episode.Format.Video.FrameRate,
- Audio: episode.Format.AudioLocale,
- Subtitle: subtitle,
- }
-
- if verboseFlag {
- fmtOptionsBytes, err := json.Marshal(info)
- if err != nil {
- fmtOptionsBytes = make([]byte, 0)
- }
- out.Debugf("Information (json): %s\n", string(fmtOptionsBytes))
- }
-
- filename := outputFlag
-
- fields := reflect.TypeOf(info)
- values := reflect.ValueOf(info)
- for i := 0; i < fields.NumField(); i++ {
- field := fields.Field(i)
- value := values.Field(i)
-
- var valueAsString string
- switch value.Kind() {
- case reflect.String:
- valueAsString = value.String()
- case reflect.Int:
- valueAsString = strconv.Itoa(int(value.Int()))
- if len(valueAsString) == 1 {
- valueAsString = "0" + valueAsString
- }
- case reflect.Float64:
- valueAsString = strconv.FormatFloat(value.Float(), 'f', 2, 64)
- case reflect.Bool:
- if value.Bool() {
- valueAsString = field.Tag.Get("json")
- } else {
- valueAsString = fmt.Sprintf("no %s", field.Tag.Get("json"))
- }
- }
-
- filename = strings.ReplaceAll(filename, "{"+field.Tag.Get("json")+"}", valueAsString)
- }
-
- invalidChars := invalidLinuxChars
- if runtime.GOOS == "windows" {
- invalidChars = invalidWindowsChars
- }
-
- // replaces all the invalid characters
- for _, char := range invalidChars {
- filename = strings.ReplaceAll(filename, char, "")
- }
-
- if onlySubFlag {
- var found bool
- if subtitleFlag == "" {
- for _, formatSubtitle := range episode.AllSubtitles {
- ext := path.Ext(filename)
- base := strings.TrimSuffix(filename, ext)
-
- originalSubtitleFilename := fmt.Sprintf("%s_%s%s", base, formatSubtitle.Locale, ext)
- subtitleFilename, changed := freeFileName(originalSubtitleFilename)
- if changed {
- out.Infof("The file %s already exist, renaming the download file to %s", originalSubtitleFilename, subtitleFilename)
- }
- file, err := os.Create(subtitleFilename)
- if err != nil {
- out.Errf("Failed to open subtitle file for locale %s: %v", formatSubtitle.Locale, err)
- continue
- }
- if err = formatSubtitle.Download(file); err != nil {
- out.Errf("Error while downloading %s subtitles: %s", formatSubtitle.Locale, err)
- continue
- }
- found = true
- }
- } else {
- for _, formatSubtitle := range episode.Format.Subtitles {
- if formatSubtitle.Locale == subtitle {
- file, err := os.Create(filename)
- if err != nil {
- out.Errf("Failed to open file %s: %v", filename, err)
- break
- }
- if err = formatSubtitle.Download(file); err != nil {
- out.Errf("Error while downloading subtitles: %v", err)
- break
- }
- found = true
- break
- }
- }
- }
- if found {
- out.Infof("Downloaded subtitles for %s", episode.Title)
- success++
- }
- } else {
- if downloadFormat(episode.Format, episode.AllSubtitles, filename, info) {
- success++
- }
- out.Empty()
- }
- }
-
- if onlySubFlag {
- out.Infof("Downloaded all %d out of %d video subtitles\n", success, len(allEpisodes))
- } else {
- out.Infof("Downloaded %d out of %d videos\n", success, len(allEpisodes))
- }
-}
-
-func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successes int) {
- videoDupes := map[string]utils.VideoStructure{}
-
- betaUrl := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com*`)
-
+func download(urls []string) error {
for i, url := range urls {
- out.StartProgressf("Parsing url %d", i+1)
-
- var localTotal, localSuccesses int
-
- var err error
- var video utils.VideoStructure
- var episode *crunchyroll.Episode
- if betaUrl.MatchString(url) {
- if episodeid, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- episode, err = crunchyroll.EpisodeFromID(crunchy, episodeid)
- } else if seriesid, ok := crunchyroll.ParseBetaSeriesURL(url); ok {
- var vid crunchyroll.Video
- vid, err = crunchyroll.SeriesFromID(crunchy, seriesid)
-
- switch vid.(type) {
- case *crunchyroll.Series:
- seasons, err := video.(*crunchyroll.Series).Seasons()
- if err != nil {
- out.EndProgressf(false, "Failed to get seasons for url %s: %s\n", url, err)
- continue
- }
- video = utils.NewSeasonStructure(seasons).EpisodeStructure
- if err := video.(*utils.EpisodeStructure).InitAll(); err != nil {
- out.EndProgressf(false, "Failed to initialize series for url %s\n", url)
- continue
- }
- case *crunchyroll.Movie:
- movieListings, err := video.(*crunchyroll.Movie).MovieListing()
- if err != nil {
- out.EndProgressf(false, "Failed to get movie listing for url %s\n", url)
- continue
- }
- video = utils.NewMovieListingStructure(movieListings)
- if err := video.(*utils.MovieListingStructure).InitAll(); err != nil {
- out.EndProgressf(false, "Failed to initialize movie for url %s\n", url)
- continue
- }
- }
- }
- } else {
- var seriesName string
- var ok bool
- if seriesName, _, _, _, ok = crunchyroll.ParseEpisodeURL(url); !ok {
- seriesName, ok = crunchyroll.MatchVideo(url)
- }
-
- if ok {
- dupe, ok := videoDupes[seriesName]
- if !ok {
- var vid crunchyroll.Video
- vid, err = crunchy.FindVideo(fmt.Sprintf("https://www.crunchyroll.com/%s", seriesName))
-
- switch vid.(type) {
- case *crunchyroll.Series:
- seasons, err := vid.(*crunchyroll.Series).Seasons()
- if err != nil {
- out.EndProgressf(false, "Failed to get seasons for url %s: %s\n", url, err)
- continue
- }
- dupe = utils.NewSeasonStructure(seasons).EpisodeStructure
- if err := dupe.(*utils.EpisodeStructure).InitAll(); err != nil {
- out.EndProgressf(false, "Failed to initialize series for url %s\n", url)
- continue
- }
- case *crunchyroll.Movie:
- movieListings, err := vid.(*crunchyroll.Movie).MovieListing()
- if err != nil {
- out.EndProgressf(false, "Failed to get movie listing for url %s\n", url)
- continue
- }
- dupe = utils.NewMovieListingStructure(movieListings)
- if err := dupe.(*utils.MovieListingStructure).InitAll(); err != nil {
- out.EndProgressf(false, "Failed to initialize movie for url %s\n", url)
- continue
- }
- }
- }
- video = dupe
- } else {
- err = fmt.Errorf("")
- }
- }
-
+ out.SetProgress("Parsing url %d", i+1)
+ episodes, err := downloadExtractEpisodes(url)
if err != nil {
- out.EndProgressf(false, "URL %d seems to be invalid\n", i+1)
- } else if episode != nil {
- epstruct := utils.NewEpisodeStructure([]*crunchyroll.Episode{episode})
+ out.StopProgress("Failed to parse url %d", i+1)
+ return err
+ }
+ out.StopProgress("Parsed url %d", i+1)
- if err = epstruct.InitAll(); err != nil {
- out.EndProgressf(false, "Could not init url %d, skipping\n", i+1)
- } else if ep := parseEpisodes(epstruct, url); ep.Format != nil {
- allEpisodes = append(allEpisodes, ep)
- localSuccesses++
- } else {
- out.EndProgressf(false, "Could not parse url %d, skipping\n", i+1)
+ for _, season := range episodes {
+ out.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
+
+ for j, info := range season {
+ out.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
+ j+1,
+ info.Title,
+ info.Resolution,
+ info.FPS,
+ info.SeasonNumber,
+ info.EpisodeNumber)
}
- localTotal++
- } else if video != nil {
- if _, ok := crunchyroll.MatchVideo(url); ok {
- out.Debugf("Parsed url %d as video\n", i+1)
- var parsed []episodeInformation
- parsed, localTotal, localSuccesses = parseVideo(video, url)
- allEpisodes = append(allEpisodes, parsed...)
- } else if _, _, _, _, ok = crunchyroll.ParseEpisodeURL(url); ok {
- out.Debugf("Parsed url %d as episode\n", i+1)
- if episode := parseEpisodes(video.(*utils.EpisodeStructure), url); episode.Format != nil {
- allEpisodes = append(allEpisodes, episode)
- localSuccesses++
- } else {
- out.EndProgressf(false, "Could not parse url %d, skipping\n", i+1)
+ }
+ out.Empty()
+
+ for _, season := range episodes {
+ for _, info := range season {
+ dir := info.Format(downloadDirectoryFlag)
+ if _, err = os.Stat(dir); os.IsNotExist(err) {
+ if err = os.MkdirAll(dir, 0777); err != nil {
+ return fmt.Errorf("error while creating directory: %v", err)
+ }
}
- localTotal++
- } else {
- out.EndProgressf(false, "Could not parse url %d, skipping\n", i+1)
- continue
- }
- } else {
- out.EndProgressf(false, "URL %d seems to be invalid\n", i+1)
- }
-
- out.EndProgressf(true, "Parsed url %d with %d successes and %d fails\n", i+1, localSuccesses, localTotal-localSuccesses)
-
- total += localTotal
- successes += localSuccesses
- }
- return
-}
-
-func parseVideo(videoStructure utils.VideoStructure, url string) (episodeInformations []episodeInformation, total, successes int) {
- var orderedFormats [][]*crunchyroll.Format
-
- switch videoStructure.(type) {
- case *utils.EpisodeStructure:
- orderedFormats, _ = videoStructure.(*utils.EpisodeStructure).OrderFormatsByEpisodeNumber()
- case *utils.MovieListingStructure:
- unorderedFormats, _ := videoStructure.(*utils.MovieListingStructure).Formats()
- orderedFormats = append(orderedFormats, unorderedFormats)
- }
-
- out.Debugf("Found %d different episodes\n", len(orderedFormats))
-
- for _, formats := range orderedFormats {
- if formats == nil {
- continue
- }
- total++
-
- var title string
- switch videoStructure.(type) {
- case *utils.EpisodeStructure:
- episode, _ := videoStructure.(*utils.EpisodeStructure).GetEpisodeByFormat(formats[0])
- title = episode.Title
- case *utils.MovieListingStructure:
- movieListing, _ := videoStructure.(*utils.MovieListingStructure).GetMovieListingByFormat(formats[0])
- title = movieListing.Title
- }
-
- if format := findFormat(formats, title); format != nil {
- info := episodeInformation{Format: format, URL: url}
- switch videoStructure.(type) {
- case *utils.EpisodeStructure:
- episode, _ := videoStructure.(*utils.EpisodeStructure).GetEpisodeByFormat(format)
- info.Title = episode.Title
- info.SeriesTitle = episode.SeriesTitle
- info.SeasonNum = episode.SeasonNumber
- info.EpisodeNum = episode.EpisodeNumber
- case *utils.MovieListingStructure:
- movieListing, _ := videoStructure.(*utils.MovieListingStructure).GetMovieListingByFormat(format)
- info.Title = movieListing.Title
- info.SeriesTitle = movieListing.Title
- info.SeasonNum, info.EpisodeNum = 1, 1
- }
-
- for _, audioFormat := range formats {
- if audioFormat.AudioLocale == crunchyroll.JP {
- info.AllSubtitles = audioFormat.Subtitles
- break
- }
- }
-
- episodeInformations = append(episodeInformations, info)
- out.Debugf("Successful parsed %s\n", title)
- }
- successes++
- }
-
- return
-}
-
-func parseEpisodes(episodeStructure *utils.EpisodeStructure, url string) episodeInformation {
- episode, _ := episodeStructure.GetEpisodeByURL(url)
- ordered, _ := episodeStructure.OrderFormatsByEpisodeNumber()
-
- var subtitles []*crunchyroll.Subtitle
- formats := ordered[episode.EpisodeNumber]
- for _, format := range formats {
- if format.AudioLocale == crunchyroll.JP {
- subtitles = format.Subtitles
- break
- }
- }
-
- out.Debugf("Found %d formats\n", len(formats))
- if format := findFormat(formats, episode.Title); format != nil {
- episode, _ = episodeStructure.GetEpisodeByFormat(format)
- out.Debugf("Found matching episode %s\n", episode.Title)
- return episodeInformation{
- Format: format,
- AllSubtitles: subtitles,
- Title: episode.Title,
- URL: url,
- SeriesTitle: episode.SeriesTitle,
- SeasonNum: episode.SeasonNumber,
- EpisodeNum: episode.EpisodeNumber,
- }
- }
- return episodeInformation{}
-}
-
-func findFormat(formats []*crunchyroll.Format, name string) (format *crunchyroll.Format) {
- formatStructure := utils.NewFormatStructure(formats)
-
- // if the only sub flag is given the japanese format gets returned because it has all subtitles available
- if onlySubFlag {
- jpFormat, _ := formatStructure.FilterFormatsByAudio(crunchyroll.JP)
- return jpFormat[0]
- }
-
- var audioLocale, subtitleLocale crunchyroll.LOCALE
-
- if audioFlag != "" {
- audioLocale = localeToLOCALE(audioFlag)
- } else {
- audioLocale = systemLocale()
- }
- if subtitleFlag != "" {
- subtitleLocale = localeToLOCALE(subtitleFlag)
- }
-
- formats, _ = formatStructure.FilterFormatsByLocales(audioLocale, subtitleLocale, !noHardsubFlag)
- if formats == nil {
- if audioFlag == "" {
- out.Errf("Failed to find episode with '%s' audio and '%s' subtitles, tying with %s audio\n", audioLocale, subtitleLocale, strings.ToLower(utils.LocaleLanguage(crunchyroll.JP)))
- audioLocale = crunchyroll.JP
- formats, _ = formatStructure.FilterFormatsByLocales(audioLocale, subtitleLocale, !noHardsubFlag)
- }
- if formats == nil && subtitleFlag == "" {
- out.Errf("Failed to find episode with '%s' audio and '%s' subtitles, tying with %s subtitle\n", audioLocale, subtitleLocale, strings.ToLower(utils.LocaleLanguage(systemLocale())))
- subtitleLocale = systemLocale()
- formats, _ = formatStructure.FilterFormatsByLocales(audioLocale, subtitleLocale, !noHardsubFlag)
- }
- if formats == nil {
- out.Errf("Could not find matching video with '%s' audio and '%s' subtitles for %s. Try to change the '--audio' and / or '--subtitle' flag\n", audioLocale, subtitleLocale, name)
- return nil
- }
- }
- if resolutionFlag == "best" || resolutionFlag == "" {
- sort.Sort(sort.Reverse(utils.FormatsByResolution(formats)))
- format = formats[0]
- } else if resolutionFlag == "worst" {
- sort.Sort(utils.FormatsByResolution(formats))
- format = formats[0]
- } else if strings.HasSuffix(resolutionFlag, "p") {
- for _, f := range formats {
- if strings.Split(f.Video.Resolution, "x")[1] == strings.TrimSuffix(resolutionFlag, "p") {
- format = f
- break
- }
- }
- } else if strings.Contains(resolutionFlag, "x") {
- for _, f := range formats {
- if f.Video.Resolution == resolutionFlag {
- format = f
- break
- }
- }
- }
- if format == nil {
- out.Errf("Failed to get video with resolution '%s'\n", resolutionFlag)
- }
-
- subtitleFlag = string(subtitleLocale)
- return
-}
-
-func downloadFormat(format *crunchyroll.Format, subtitles []*crunchyroll.Subtitle, outFile string, info information) bool {
- oldOutFile := outFile
- outFile, changed := freeFileName(outFile)
- ext := path.Ext(outFile)
- out.Debugf("Download filename: %s\n", outFile)
- if changed {
- out.Errf("The file %s already exist, renaming the download file to %s\n", oldOutFile, outFile)
- }
- if ext != ".ts" {
- if !hasFFmpeg() {
- out.Fatalf("The file ending for the output file (%s) is not `.ts`. "+
- "Install ffmpeg (https://ffmpeg.org/download.html) use other media file endings (e.g. `.mp4`)\n", outFile)
- }
- out.Debugf("File will be converted via ffmpeg")
- }
- var subtitleFilename string
- if noHardsubFlag {
- subtitle, ok := utils.SubtitleByLocale(format, info.Subtitle)
- if !ok {
- out.Errf("Failed to get %s subtitles\n", info.Subtitle)
- return false
- }
- subtitleFilename, _ = freeFileName(fmt.Sprintf("%s.%s", strings.TrimSuffix(outFile, ext), subtitle.Format))
- out.Debugf("Subtitles will be saved as '%s'\n", subtitleFilename)
- }
-
- out.Infof("Downloading '%s' (%s) as '%s'\n", info.Title, info.OriginalURL, outFile)
- out.Infof("Series: %s\n", info.SeriesName)
- out.Infof("Season & Episode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
- out.Infof("Audio: %s\n", info.Audio)
- out.Infof("Subtitle: %s\n", info.Subtitle)
- out.Infof("Hardsub: %v\n", format.Hardsub != "")
- out.Infof("Resolution: %s\n", info.Resolution)
- out.Infof("FPS: %.2f\n", info.FPS)
-
- var err error
- if ext == ".ts" {
- var file *os.File
- file, err = os.Create(outFile)
- defer file.Close()
- if err != nil {
- out.Errf("Could not create file '%s' to download episode '%s' (%s): %s, skipping\n", outFile, info.Title, info.OriginalURL, err)
- return false
- }
-
- err = format.DownloadGoroutines(file, goroutinesFlag, downloadProgress)
- // newline to avoid weird output
- fmt.Println()
- } else {
- var tempDir string
- tempDir, err = os.MkdirTemp("", "crunchy_")
- if err != nil {
- out.Errln("Failed to create temp download dir. Skipping")
- return false
- }
-
- var segmentCount int
- err = format.DownloadSegments(tempDir, goroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- segmentCount++
- return downloadProgress(segment, current, total, file)
- })
- // newline to avoid weird output
- fmt.Println()
-
- f, _ := os.CreateTemp("", "*.txt")
- for i := 0; i < segmentCount; i++ {
- fmt.Fprintf(f, "file '%s.ts'\n", filepath.Join(tempDir, strconv.Itoa(i)))
- }
- defer os.Remove(f.Name())
- f.Close()
-
- args := []string{
- "-f", "concat",
- "-safe", "0",
- "-i", f.Name(),
- }
- if ext == ".mkv" && subtitleFlag == "" {
- // this saves all subtitles into a mkv file. see https://github.com/ByteDream/crunchyroll-go/issues/5 for some details
-
- ffmpegInput := make([]string, 0)
- ffmpegMap := []string{"-map", "0"}
- ffmpegMetadata := make([]string, 0)
- for i, subtitle := range subtitles {
- subtitleFilepath := filepath.Join(cleanupPath, fmt.Sprintf("%s.%s", subtitle.Locale, subtitle.Format))
-
- var file *os.File
- file, err = os.Create(subtitleFilepath)
+ file, err := os.Create(generateFilename(info.Format(downloadOutputFlag), dir))
if err != nil {
- out.Errf("Could not create file to download %s subtitles to: %v", subtitle.Locale, err)
- continue
+ return fmt.Errorf("failed to create output file: %v", err)
}
- if err = subtitle.Download(file); err != nil {
- out.Errf("Failed to download subtitles: %s", err)
- continue
+
+ if err = downloadInfo(info, file); err != nil {
+ file.Close()
+ os.Remove(file.Name())
+ return err
}
- ffmpegInput = append(ffmpegInput, "-i", subtitleFilepath)
- ffmpegMap = append(ffmpegMap, "-map", strconv.Itoa(i+1))
- ffmpegMetadata = append(ffmpegMetadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("language=%s", strings.Split(string(subtitle.Locale), "-")[0]))
+ file.Close()
}
-
- args = append(args, ffmpegInput...)
- args = append(args, ffmpegMap...)
- args = append(args, ffmpegMetadata...)
- }
- args = append(args, "-c", "copy", outFile)
-
- cmd := exec.Command("ffmpeg", args...)
- err = cmd.Run()
- }
- os.RemoveAll(cleanupPath)
- cleanupPath = ""
-
- if err != nil {
- out.Errf("Failed to download video, skipping: %v", err)
- } else {
- if info.Subtitle == "" {
- out.Infof("Downloaded '%s' as '%s' with %s audio locale\n", info.Title, outFile, strings.ToLower(utils.LocaleLanguage(info.Audio)))
- } else {
- out.Infof("Downloaded '%s' as '%s' with %s audio locale and %s subtitle locale\n", info.Title, outFile, strings.ToLower(utils.LocaleLanguage(info.Audio)), strings.ToLower(utils.LocaleLanguage(info.Subtitle)))
- if subtitleFilename != "" {
- file, err := os.Create(subtitleFilename)
- if err != nil {
- out.Errf("Failed to download subtitles: %s\n", err)
- return false
- } else {
- subtitle, ok := utils.SubtitleByLocale(format, info.Subtitle)
- if !ok {
- out.Errf("Failed to get %s subtitles\n", info.Subtitle)
- return false
- }
- if err := subtitle.Download(file); err != nil {
- out.Errf("Failed to download subtitles: %s\n", err)
- return false
- }
- out.Infof("Downloaded '%s' subtitles to '%s'\n", info.Subtitle, subtitleFilename)
- }
- }
- }
- }
-
- return true
-}
-
-func downloadProgress(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- if cleanupPath == "" {
- cleanupPath = path.Dir(file.Name())
- }
-
- if !quietFlag {
- percentage := float32(current) / float32(total) * 100
- if alternativeProgressFlag {
- out.Infof("Downloading %d/%d (%.2f%%) » %s", current, total, percentage, segment.URI)
- } else {
- progressWidth := float32(terminalWidth() - (14 + len(out.InfoLog.Prefix())) - (len(fmt.Sprint(total)))*2)
-
- repeatCount := int(percentage / (float32(100) / progressWidth))
- // it can be lower than zero when the terminal is very tiny
- if repeatCount < 0 {
- repeatCount = 0
- }
-
- // alternative:
- // progressPercentage := strings.Repeat("█", repeatCount)
- progressPercentage := (strings.Repeat("=", repeatCount) + ">")[1:]
-
- fmt.Printf("\r%s[%-"+fmt.Sprint(progressWidth)+"s]%4d%% %8d/%d", out.InfoLog.Prefix(), progressPercentage, int(percentage), current, total)
}
}
return nil
}
+
+func downloadInfo(info formatInformation, file *os.File) error {
+ out.Debug("Entering season %d, episode %d", info.SeasonNumber, info.EpisodeNumber)
+
+ if err := info.format.InitVideo(); err != nil {
+ return fmt.Errorf("error while initializing the video: %v", err)
+ }
+
+ downloadProgress := &downloadProgress{
+ Prefix: out.InfoLog.Prefix(),
+ Message: "Downloading video",
+ // number of segments a video has +2 is for merging and the success message
+ Total: int(info.format.Video.Chunklist.Count()) + 2,
+ Dev: out.IsDev(),
+ Quiet: out.IsQuiet(),
+ }
+ if out.IsDev() {
+ downloadProgress.Prefix = out.DebugLog.Prefix()
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ downloader := crunchyroll.NewDownloader(ctx, file, downloadGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
+ // check if the context was cancelled.
+ // must be done in to not print any progress messages if ctrl+c was pressed
+ if ctx.Err() != nil {
+ return nil
+ }
+
+ if out.IsDev() {
+ downloadProgress.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
+ } else {
+ downloadProgress.Update()
+ }
+
+ if current == total {
+ downloadProgress.UpdateMessage("Merging segments", false)
+ }
+ return nil
+ })
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt)
+ go func() {
+ select {
+ case <-sig:
+ signal.Stop(sig)
+ out.Exit("Exiting... (may take a few seconds)")
+ out.Exit("To force exit press ctrl+c (again)")
+ cancel()
+ // 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
+ // directory where the segments are downloaded to will not be deleted
+ case <-ctx.Done():
+ // this is just here to end the goroutine and prevent it from running forever without a reason
+ }
+ }()
+ out.Debug("Set up signal catcher")
+
+ out.Info("Downloading episode `%s` to `%s`", info.Title, filepath.Base(file.Name()))
+ out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
+ out.Info("\tAudio: %s", info.Audio)
+ out.Info("\tSubtitle: %s", info.Subtitle)
+ out.Info("\tResolution: %spx", info.Resolution)
+ out.Info("\tFPS: %.2f", info.FPS)
+ if err := info.format.Download(downloader); err != nil {
+ return fmt.Errorf("error while downloading: %v", err)
+ }
+
+ downloadProgress.UpdateMessage("Download finished", false)
+
+ signal.Stop(sig)
+ out.Debug("Stopped signal catcher")
+
+ out.Empty()
+ out.Empty()
+
+ return nil
+}
+
+func downloadExtractEpisodes(url string) ([][]formatInformation, error) {
+ episodes, err := extractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
+ if err != nil {
+ return nil, err
+ }
+ japanese := episodes[0]
+ custom := episodes[1]
+
+ sort.Sort(utils.EpisodesByNumber(japanese))
+ sort.Sort(utils.EpisodesByNumber(custom))
+
+ var errMessages []string
+
+ var final []*crunchyroll.Episode
+ if len(japanese) == 0 || len(japanese) == len(custom) {
+ final = custom
+ } else {
+ for _, jp := range japanese {
+ before := len(final)
+ for _, episode := range custom {
+ if jp.SeasonNumber == episode.SeasonNumber && jp.EpisodeNumber == episode.EpisodeNumber {
+ final = append(final, episode)
+ }
+ }
+ if before == len(final) {
+ errMessages = append(errMessages, fmt.Sprintf("%s has no %s audio, using %s as fallback", jp.Title, crunchyroll.LOCALE(downloadAudioFlag), crunchyroll.JP))
+ final = append(final, jp)
+ }
+ }
+ }
+
+ if len(errMessages) > 10 {
+ for _, msg := range errMessages[:10] {
+ out.SetProgress(msg)
+ }
+ out.SetProgress("... and %d more", len(errMessages)-10)
+ } else {
+ for _, msg := range errMessages {
+ out.SetProgress(msg)
+ }
+ }
+
+ var infoFormat [][]formatInformation
+ for _, season := range utils.SortEpisodesBySeason(final) {
+ tmpFormatInformation := make([]formatInformation, 0)
+ for _, episode := range season {
+ format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
+ if err != nil {
+ return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
+ }
+ tmpFormatInformation = append(tmpFormatInformation, formatInformation{
+ format: format,
+
+ Title: episode.Title,
+ SeriesName: episode.SeriesTitle,
+ SeasonName: episode.SeasonTitle,
+ SeasonNumber: episode.SeasonNumber,
+ EpisodeNumber: episode.EpisodeNumber,
+ Resolution: format.Video.Resolution,
+ FPS: format.Video.FrameRate,
+ Audio: format.AudioLocale,
+ })
+ }
+ infoFormat = append(infoFormat, tmpFormatInformation)
+ }
+ return infoFormat, nil
+}
From b2393530391e933f17f89f77d0ec9f001116c418 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 22 Mar 2022 19:39:43 +0100
Subject: [PATCH 094/732] Added new archive command
---
cmd/crunchyroll-go/cmd/archive.go | 798 ++++++++++++++++++++++++++++++
1 file changed, 798 insertions(+)
create mode 100644 cmd/crunchyroll-go/cmd/archive.go
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
new file mode 100644
index 0000000..17072eb
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -0,0 +1,798 @@
+package cmd
+
+import (
+ "archive/tar"
+ "archive/zip"
+ "bufio"
+ "bytes"
+ "compress/gzip"
+ "context"
+ "fmt"
+ "github.com/ByteDream/crunchyroll-go"
+ "github.com/ByteDream/crunchyroll-go/utils"
+ "github.com/grafov/m3u8"
+ "github.com/spf13/cobra"
+ "io"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ archiveLanguagesFlag []string
+
+ archiveDirectoryFlag string
+ archiveOutputFlag string
+
+ archiveMergeFlag string
+
+ archiveCompressFlag string
+
+ archiveResolutionFlag string
+
+ archiveGoroutinesFlag int
+)
+
+var archiveCmd = &cobra.Command{
+ Use: "archive",
+ Short: "Stores the given videos with all subtitles and multiple audios in a .mkv file",
+ Args: cobra.MinimumNArgs(1),
+
+ PreRunE: func(cmd *cobra.Command, args []string) error {
+ out.Debug("Validating arguments")
+
+ if !hasFFmpeg() {
+ return fmt.Errorf("ffmpeg is needed to run this command correctly")
+ }
+ out.Debug("FFmpeg detected")
+
+ if filepath.Ext(archiveOutputFlag) != ".mkv" {
+ return fmt.Errorf("currently only matroska / .mkv files are supported")
+ }
+
+ for _, locale := range archiveLanguagesFlag {
+ if !utils.ValidateLocale(crunchyroll.LOCALE(locale)) {
+ // if locale is 'all', match all known locales
+ if locale == "all" {
+ archiveLanguagesFlag = allLocalesAsStrings()
+ break
+ }
+ return fmt.Errorf("%s is not a valid locale. Choose from: %s", locale, strings.Join(allLocalesAsStrings(), ", "))
+ }
+ }
+ out.Debug("Using following audio locales: %s", strings.Join(archiveLanguagesFlag, ", "))
+
+ var found bool
+ for _, mode := range []string{"auto", "audio", "video"} {
+ if archiveMergeFlag == mode {
+ out.Debug("Using %s merge behavior", archiveMergeFlag)
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("'%s' is no valid merge flag. Use 'auto', 'audio' or 'video'", archiveMergeFlag)
+ }
+
+ if archiveCompressFlag != "" {
+ found = false
+ for _, algo := range []string{".tar", ".tar.gz", ".tgz", ".zip"} {
+ if strings.HasSuffix(archiveCompressFlag, algo) {
+ out.Debug("Using %s compression", algo)
+ found = true
+ break
+ }
+ }
+ if !found {
+ return fmt.Errorf("'%s' is no valid compress algorithm. Valid algorithms / file endings are '.tar', '.tar.gz', '.zip'",
+ archiveCompressFlag)
+ }
+ }
+
+ switch archiveResolutionFlag {
+ case "1080p", "720p", "480p", "360p", "240p":
+ intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
+ archiveResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(archiveResolutionFlag, "p"))
+ case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
+ default:
+ return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)
+ }
+ out.Debug("Using resolution '%s'", archiveResolutionFlag)
+
+ return nil
+ },
+ RunE: func(cmd *cobra.Command, args []string) error {
+ loadCrunchy()
+
+ return archive(args)
+ },
+}
+
+func init() {
+ archiveCmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
+ "language",
+ "l",
+ []string{string(systemLocale(false)), string(crunchyroll.JP)},
+ "Audio locale which should be downloaded. Can be used multiple times")
+
+ cwd, _ := os.Getwd()
+ archiveCmd.Flags().StringVarP(&archiveDirectoryFlag,
+ "directory",
+ "d",
+ cwd,
+ "The directory to store the files into")
+ archiveCmd.Flags().StringVarP(&archiveOutputFlag,
+ "output",
+ "o",
+ "{title}.mkv",
+ "Name of the output file. If you use the following things in the name, the will get replaced:\n"+
+ "\t{title} » Title of the video\n"+
+ "\t{series_name} » Name of the series\n"+
+ "\t{season_name} » Name of the season\n"+
+ "\t{season_number} » Number of the season\n"+
+ "\t{episode_number} » Number of the episode\n"+
+ "\t{resolution} » Resolution of the video\n"+
+ "\t{fps} » Frame Rate of the video\n"+
+ "\t{audio} » Audio locale of the video\n"+
+ "\t{subtitle} » Subtitle locale of the video")
+
+ archiveCmd.Flags().StringVarP(&archiveMergeFlag,
+ "merge",
+ "m",
+ "auto",
+ "Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'")
+
+ archiveCmd.Flags().StringVarP(&archiveCompressFlag,
+ "compress",
+ "c",
+ "",
+ "If is set, all output will be compresses into an archive (every url generates a new one). "+
+ "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")
+
+ archiveCmd.Flags().StringVarP(&archiveResolutionFlag,
+ "resolution",
+ "r",
+ "best",
+ "The video resolution. Can either be specified via the pixels, the abbreviation for pixels, or 'common-use' words\n"+
+ "\tAvailable pixels: 1920x1080, 1280x720, 640x480, 480x360, 428x240\n"+
+ "\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
+ "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
+
+ archiveCmd.Flags().IntVarP(&archiveGoroutinesFlag,
+ "goroutines",
+ "g",
+ runtime.NumCPU(),
+ "Number of parallel segment downloads")
+
+ rootCmd.AddCommand(archiveCmd)
+}
+
+func archive(urls []string) error {
+ for i, url := range urls {
+ out.SetProgress("Parsing url %d", i+1)
+ episodes, err := archiveExtractEpisodes(url)
+ if err != nil {
+ out.StopProgress("Failed to parse url %d", i+1)
+ return err
+ }
+ out.StopProgress("Parsed url %d", i+1)
+
+ var compressFile *os.File
+ var c compress
+
+ if archiveCompressFlag != "" {
+ compressFile, err = os.Create(generateFilename(archiveCompressFlag, ""))
+ if err != nil {
+ return fmt.Errorf("failed to create archive file: %v", err)
+ }
+ if strings.HasSuffix(archiveCompressFlag, ".tar") {
+ c = newTarCompress(compressFile)
+ } else if strings.HasSuffix(archiveCompressFlag, ".tar.gz") || strings.HasSuffix(archiveCompressFlag, ".tgz") {
+ c = newGzipCompress(compressFile)
+ } else if strings.HasSuffix(archiveCompressFlag, ".zip") {
+ c = newZipCompress(compressFile)
+ }
+ }
+
+ for _, season := range episodes {
+ out.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
+
+ for j, info := range season {
+ out.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
+ j+1,
+ info.Title,
+ info.Resolution,
+ info.FPS,
+ info.SeasonNumber,
+ info.EpisodeNumber)
+ }
+ }
+ out.Empty()
+
+ for _, season := range episodes {
+ for _, info := range season {
+ var filename string
+ var writeCloser io.WriteCloser
+ if c != nil {
+ filename = info.Format(archiveOutputFlag)
+ writeCloser, err = c.NewFile(info)
+ if err != nil {
+ return fmt.Errorf("failed to pre generate new archive file: %v", err)
+ }
+ } else {
+ dir := info.Format(downloadDirectoryFlag)
+ if _, err = os.Stat(dir); os.IsNotExist(err) {
+ if err = os.MkdirAll(dir, 0777); err != nil {
+ return fmt.Errorf("error while creating directory: %v", err)
+ }
+ }
+ filename = generateFilename(info.Format(archiveOutputFlag), dir)
+ writeCloser, err = os.Create(filename)
+ if err != nil {
+ return fmt.Errorf("failed to create new file: %v", err)
+ }
+ }
+
+ if err = archiveInfo(info, writeCloser, filename); err != nil {
+ writeCloser.Close()
+ if f, ok := writeCloser.(*os.File); ok {
+ os.Remove(f.Name())
+ } else {
+ c.Close()
+ compressFile.Close()
+ os.RemoveAll(compressFile.Name())
+ }
+ return err
+ }
+
+ writeCloser.Close()
+ }
+ }
+ if c != nil {
+ c.Close()
+ }
+ if compressFile != nil {
+ compressFile.Close()
+ }
+ }
+ return nil
+}
+
+func archiveInfo(info 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))
+
+ downloadProgress, err := createArchiveProgress(info)
+ if err != nil {
+ return fmt.Errorf("error while setting up downloader: %v", err)
+ }
+
+ rootFile, err := os.CreateTemp("", fmt.Sprintf("%s_*.ts", strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))))
+ if err != nil {
+ return fmt.Errorf("failed to create temp file: %v", err)
+ }
+ defer os.Remove(rootFile.Name())
+ defer rootFile.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ downloader := crunchyroll.NewDownloader(ctx, rootFile, downloadGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
+ // check if the context was cancelled.
+ // must be done in to not print any progress messages if ctrl+c was pressed
+ if ctx.Err() != nil {
+ return nil
+ }
+
+ if out.IsDev() {
+ downloadProgress.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
+ } else {
+ downloadProgress.Update()
+ }
+
+ if current == total {
+ downloadProgress.UpdateMessage("Merging segments", false)
+ }
+ return nil
+ })
+
+ sig := make(chan os.Signal, 1)
+ signal.Notify(sig, os.Interrupt)
+ go func() {
+ select {
+ case <-sig:
+ signal.Stop(sig)
+ out.Exit("Exiting... (may take a few seconds)")
+ out.Exit("To force exit press ctrl+c (again)")
+ cancel()
+ // 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
+ // directory where the segments are downloaded to will not be deleted
+ case <-ctx.Done():
+ // this is just here to end the goroutine and prevent it from running forever without a reason
+ }
+ }()
+ out.Debug("Set up signal catcher")
+
+ var additionalDownloaderOpts []string
+ var mergeMessage string
+ switch archiveMergeFlag {
+ case "auto":
+ additionalDownloaderOpts = []string{"-vn"}
+ for _, format := range info.additionalFormats {
+ if format.Video.Bandwidth != info.format.Video.Bandwidth {
+ // revoke the changed FFmpegOpts above
+ additionalDownloaderOpts = []string{}
+ break
+ }
+ }
+ if len(additionalDownloaderOpts) > 0 {
+ mergeMessage = "merging audio for additional formats"
+ } else {
+ mergeMessage = "merging video for additional formats"
+ }
+ case "audio":
+ additionalDownloaderOpts = []string{"-vn"}
+ mergeMessage = "merging audio for additional formats"
+ case "video":
+ mergeMessage = "merging video for additional formats"
+ }
+
+ out.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
+ out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
+ out.Info("\tAudio: %s", info.Audio)
+ out.Info("\tSubtitle: %s", info.Subtitle)
+ out.Info("\tResolution: %spx", info.Resolution)
+ out.Info("\tFPS: %.2f", info.FPS)
+
+ var videoFiles, audioFiles, subtitleFiles []string
+ defer func() {
+ for _, f := range append(append(videoFiles, audioFiles...), subtitleFiles...) {
+ os.RemoveAll(f)
+ }
+ }()
+
+ var f []string
+ if f, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.format); err != nil {
+ if err != ctx.Err() {
+ return fmt.Errorf("error while downloading: %v", err)
+ }
+ return err
+ }
+ videoFiles = append(videoFiles, f[0])
+
+ if len(additionalDownloaderOpts) == 0 {
+ var videos []string
+ downloader.FFmpegOpts = additionalDownloaderOpts
+ if videos, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.additionalFormats...); err != nil {
+ return fmt.Errorf("error while downloading additional videos: %v", err)
+ }
+ downloader.FFmpegOpts = []string{}
+ videoFiles = append(videoFiles, videos...)
+ } else {
+ var audios []string
+ if audios, err = archiveDownloadVideos(downloader, filepath.Base(filename), false, info.additionalFormats...); err != nil {
+ return fmt.Errorf("error while downloading additional videos: %v", err)
+ }
+ audioFiles = append(audioFiles, audios...)
+ }
+
+ sort.Sort(utils.SubtitlesByLocale(info.format.Subtitles))
+ if len(archiveLanguagesFlag) > 0 && archiveLanguagesFlag[0] != "all" {
+ for j, language := range archiveLanguagesFlag {
+ locale := crunchyroll.LOCALE(language)
+ for k, subtitle := range info.format.Subtitles {
+ if subtitle.Locale == locale {
+ info.format.Subtitles = append(info.format.Subtitles[:k], info.format.Subtitles[k+1:]...)
+ info.format.Subtitles = append(info.format.Subtitles[:j], append([]*crunchyroll.Subtitle{subtitle}, info.format.Subtitles[j:]...)...)
+ break
+ }
+ }
+ }
+ }
+
+ var subtitles []string
+ if subtitles, err = archiveDownloadSubtitles(filepath.Base(filename), info.format.Subtitles...); err != nil {
+ return fmt.Errorf("error while downloading subtitles: %v", err)
+ }
+ subtitleFiles = append(subtitleFiles, subtitles...)
+
+ if err = archiveFFmpeg(ctx, writeCloser, videoFiles, audioFiles, subtitleFiles); err != nil {
+ return fmt.Errorf("failed to merge files: %v", err)
+ }
+
+ downloadProgress.UpdateMessage("Download finished", false)
+
+ signal.Stop(sig)
+ out.Debug("Stopped signal catcher")
+
+ out.Empty()
+ out.Empty()
+
+ return nil
+}
+
+func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
+ var progressCount int
+ if err := info.format.InitVideo(); err != nil {
+ return nil, fmt.Errorf("error while initializing a video: %v", err)
+ }
+ // + number of segments a video has +1 is for merging
+ progressCount += int(info.format.Video.Chunklist.Count()) + 1
+ for _, f := range info.additionalFormats {
+ if f == info.format {
+ continue
+ }
+
+ if err := f.InitVideo(); err != nil {
+ return nil, err
+ }
+ // + number of segments a video has +1 is for merging
+ progressCount += int(f.Video.Chunklist.Count()) + 1
+ }
+
+ downloadProgress := &downloadProgress{
+ Prefix: out.InfoLog.Prefix(),
+ Message: "Downloading video",
+ // number of segments a video +1 is for the success message
+ Total: progressCount + 1,
+ Dev: out.IsDev(),
+ Quiet: out.IsQuiet(),
+ }
+ if out.IsDev() {
+ downloadProgress.Prefix = out.DebugLog.Prefix()
+ }
+
+ return downloadProgress, nil
+}
+
+func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, video bool, formats ...*crunchyroll.Format) ([]string, error) {
+ var files []string
+
+ for _, format := range formats {
+ var name string
+ if video {
+ name = fmt.Sprintf("%s_%s_video_*.ts", filename, format.AudioLocale)
+ } else {
+ name = fmt.Sprintf("%s_%s_audio_*.aac", filename, format.AudioLocale)
+ }
+
+ f, err := os.CreateTemp("", name)
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, f.Name())
+
+ downloader.Writer = f
+ if err = format.Download(downloader); err != nil {
+ f.Close()
+ for _, file := range files {
+ os.Remove(file)
+ }
+ return nil, err
+ }
+ f.Close()
+
+ out.Debug("Downloaded '%s' video", format.AudioLocale)
+ }
+
+ return files, nil
+}
+
+func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitle) ([]string, error) {
+ var files []string
+
+ for _, subtitle := range subtitles {
+ f, err := os.CreateTemp("", fmt.Sprintf("%s_%s_subtitle_*.ass", filename, subtitle.Locale))
+ if err != nil {
+ return nil, err
+ }
+ files = append(files, f.Name())
+
+ if err := subtitle.Save(f); err != nil {
+ f.Close()
+ for _, file := range files {
+ os.Remove(file)
+ }
+ return nil, err
+ }
+ f.Close()
+
+ out.Debug("Downloaded '%s' subtitles", subtitle.Locale)
+ }
+
+ return files, nil
+}
+
+func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, subtitleFiles []string) error {
+ var input, maps, metadata []string
+ re := regexp.MustCompile(`(?m)_([a-z]{2}-([A-Z]{2}|[0-9]{3}))_(video|audio|subtitle)`)
+
+ for i, video := range videoFiles {
+ input = append(input, "-i", video)
+ maps = append(maps, "-map", strconv.Itoa(i))
+ locale := crunchyroll.LOCALE(re.FindStringSubmatch(video)[1])
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("language=%s", utils.LocaleLanguage(locale)))
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", utils.LocaleLanguage(locale)))
+ }
+
+ for i, audio := range audioFiles {
+ input = append(input, "-i", audio)
+ maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
+ locale := crunchyroll.LOCALE(re.FindStringSubmatch(audio)[1])
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", utils.LocaleLanguage(locale)))
+ }
+
+ for i, subtitle := range subtitleFiles {
+ input = append(input, "-i", subtitle)
+ maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)+len(audioFiles)))
+ locale := crunchyroll.LOCALE(re.FindStringSubmatch(subtitle)[1])
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:s:%d", i), fmt.Sprintf("title=%s", utils.LocaleLanguage(locale)))
+ }
+
+ commandOptions := []string{"-y"}
+ commandOptions = append(commandOptions, input...)
+ commandOptions = append(commandOptions, maps...)
+ commandOptions = append(commandOptions, metadata...)
+ // we have to create a temporary file here because it must be seekable
+ // for ffmpeg.
+ // ffmpeg could write to dst too, but this would require to re-encode
+ // the audio which results in much higher time and resource consumption
+ // (0-1 second with the temp file, ~20 seconds with re-encoding on my system)
+ file, err := os.CreateTemp("", "")
+ if err != nil {
+ return err
+ }
+ file.Close()
+ defer os.Remove(file.Name())
+
+ commandOptions = append(commandOptions, "-c", "copy", "-f", "matroska", file.Name())
+
+ // just a little nicer debug output to copy and paste the ffmpeg for debug reasons
+ if out.IsDev() {
+ var debugOptions []string
+
+ for _, option := range commandOptions {
+ if strings.HasPrefix(option, "title=") {
+ debugOptions = append(debugOptions, "title=\""+strings.TrimPrefix(option, "title=")+"\"")
+ } else if strings.HasPrefix(option, "language=") {
+ debugOptions = append(debugOptions, "language=\""+strings.TrimPrefix(option, "language=")+"\"")
+ } else if strings.Contains(option, " ") {
+ debugOptions = append(debugOptions, "\""+option+"\"")
+ } else {
+ debugOptions = append(debugOptions, option)
+ }
+ }
+ out.Debug("FFmpeg merge command: ffmpeg %s", strings.Join(debugOptions, " "))
+ }
+
+ var errBuf bytes.Buffer
+ cmd := exec.CommandContext(ctx, "ffmpeg", commandOptions...)
+ cmd.Stderr = &errBuf
+ if err = cmd.Run(); err != nil {
+ return fmt.Errorf(errBuf.String())
+ }
+
+ file, err = os.Open(file.Name())
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ _, err = bufio.NewWriter(dst).ReadFrom(file)
+ return err
+}
+
+func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
+ var hasJapanese bool
+ languagesAsLocale := []crunchyroll.LOCALE{crunchyroll.JP}
+ for _, language := range archiveLanguagesFlag {
+ locale := crunchyroll.LOCALE(language)
+ if locale == crunchyroll.JP {
+ hasJapanese = true
+ } else {
+ languagesAsLocale = append(languagesAsLocale, locale)
+ }
+ }
+
+ episodes, err := extractEpisodes(url, languagesAsLocale...)
+ if err != nil {
+ return nil, err
+ }
+
+ if !hasJapanese && len(episodes[1:]) == 0 {
+ return nil, fmt.Errorf("no episodes found")
+ }
+
+ for i, eps := range episodes {
+ if len(eps) == 0 {
+ out.SetProgress("%s has no matching episodes", languagesAsLocale[i])
+ } 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]))
+ }
+ }
+
+ if !hasJapanese {
+ episodes = episodes[1:]
+ }
+
+ eps := make(map[int]map[int]*formatInformation)
+ for _, lang := range episodes {
+ for _, season := range utils.SortEpisodesBySeason(lang) {
+ if _, ok := eps[season[0].SeasonNumber]; !ok {
+ eps[season[0].SeasonNumber] = map[int]*formatInformation{}
+ }
+ for _, episode := range season {
+ format, err := episode.GetFormat(archiveResolutionFlag, "", false)
+ if err != nil {
+ return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
+ }
+
+ if _, ok := eps[episode.SeasonNumber][episode.EpisodeNumber]; !ok {
+ eps[episode.SeasonNumber][episode.EpisodeNumber] = &formatInformation{
+ format: format,
+ additionalFormats: make([]*crunchyroll.Format, 0),
+
+ Title: episode.Title,
+ SeriesName: episode.SeriesTitle,
+ SeasonName: episode.SeasonTitle,
+ SeasonNumber: episode.SeasonNumber,
+ EpisodeNumber: episode.EpisodeNumber,
+ Resolution: format.Video.Resolution,
+ FPS: format.Video.FrameRate,
+ Audio: format.AudioLocale,
+ }
+ } else {
+ eps[episode.SeasonNumber][episode.EpisodeNumber].additionalFormats = append(eps[episode.SeasonNumber][episode.EpisodeNumber].additionalFormats, format)
+ }
+ }
+ }
+ }
+
+ var infoFormat [][]formatInformation
+ for _, e := range eps {
+ var tmpFormatInfo []formatInformation
+
+ var keys []int
+ for episodeNumber := range e {
+ keys = append(keys, episodeNumber)
+ }
+ sort.Ints(keys)
+
+ for _, key := range keys {
+ tmpFormatInfo = append(tmpFormatInfo, *e[key])
+ }
+
+ infoFormat = append(infoFormat, tmpFormatInfo)
+ }
+
+ 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
+}
From c367636d96f923cdc074dc86df566e79024c061e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 22 Mar 2022 19:39:55 +0100
Subject: [PATCH 095/732] v2 update
---
Makefile | 11 +-
README.md | 311 +++++++++++++----------------------------------
crunchyroll-go.1 | 120 +++++++++++++-----
3 files changed, 177 insertions(+), 265 deletions(-)
diff --git a/Makefile b/Makefile
index 8a26748..0744c35 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=1.2.4
+VERSION=2.0.0
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
@@ -21,13 +21,10 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/man/man1/crunchyroll-go.1
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
-test:
- go test -v .
-
release:
- cd cmd/crunchyroll-go && GOOS=linux GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_linux
- cd cmd/crunchyroll-go && GOOS=windows GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_windows.exe
- cd cmd/crunchyroll-go && GOOS=darwin GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_darwin
+ cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_linux
+ cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_windows.exe
+ cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_darwin
strip cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index b19ab6f..709c3ae 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,8 @@
-**This branch is under highly development, so it may contain errors which are making compiling not possible**
+Version 2 is out 🥳, see all the changes.
# crunchyroll-go
-A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api.
-
-**You surely need a crunchyroll premium account to get full (api) access.**
+A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api. To use it, you need a crunchyroll premium account to for full (api) access.
@@ -32,20 +30,22 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
•
Library 📚
•
- Credits 🙏
- •
- Notice 🗒️
+ Disclaimer ☝️
•
License ⚖
-## 🖥️ CLI
+# 🖥️ CLI
-#### ✨ Features
-- Download single videos and entire series from [crunchyroll](https://www.crunchyroll.com)
+## ✨ Features
-#### Get the executable
-- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below
+- Download single videos and entire series from [crunchyroll](https://www.crunchyroll.com).
+- Archive episode or seasons in an `.mkv` file with multiple subtitles and audios and compress them to gzip or zip files.
+- Specify a range which episodes to download from an anime.
+
+## 💾 Get the executable
+
+- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below:
- [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
- [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
- [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
@@ -54,22 +54,26 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
$ yay -S crunchyroll-go
```
- 🛠 Build it yourself
- - use `make` (requires `go` to be installed):
+ - use `make` (requires `go` to be installed):
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go
$ make && sudo make install
```
- - use `go`:
+ - use `go`:
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go/cmd/crunchyroll-go
$ go build -o crunchy
```
-### 📝 Examples
+## 📝 Examples
+
+_Before reading_: Because of the huge functionality not all cases can be covered in the README.
+Make sure to check the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli), further usages and options are described there.
+
+### Login
-#### Login
Before you can do something, you have to login first.
This can be performed via crunchyroll account email and password.
@@ -82,11 +86,9 @@ or via session id
$ crunchy login --session-id 8e9gs135defhga790dvrf2i0eris8gts
```
-#### Download
+### Download
-**With the cli you can download single videos or entire series.**
-
-By default the cli tries to download the episode with your system language as audio.
+By default, the cli tries to download the episode with your system language as audio.
If no streams with your system language are available, the video will be downloaded with japanese audio and hardsubbed subtitles in your system language.
**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
@@ -95,7 +97,6 @@ $ crunchy download https://www.crunchyroll.com/darling-in-the-franxx/episode-1-a
```
With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
-
```
$ crunchy download -r best https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
@@ -105,250 +106,106 @@ The file is by default saved as a `.ts` (mpeg transport stream) file.
With the `-o` flag, you can change the name (and file ending) of the output file.
So if you want to save it as, for example, `mp4` file, just name it `whatever.mp4`.
**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
-
```
$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have.
Type `crunchy help download` to see all available locales.
-
```
$ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/darling-in-the-franxx
```
##### Flags
-- `--audio` » forces audio of the video(s)
-- `--subtitle` » forces subtitle of the video(s)
-- `--no-hardsub` » forces that the subtitles are stored as a separate file and are not directly embedded into the video
-- `--only-sub` » downloads only the subtitles without the corresponding video
-- `-d`, `--directory` » directory to download the video(s) to
-- `-o`, `--output` » name of the output file
+The following flags can be (optional) passed to modify the [download](#download) process.
-- `-r`, `--resolution` » the resolution of the video(s). `best` for best resolution, `worst` for worst
+| Short | Extended | Description |
+|-------|----------------|--------------------------------------------------------------------------------|
+| `-a` | `--audio` | Forces audio of the video(s). |
+| `-s` | `--subtitle` | Forces subtitle of the video(s). |
+| `-d` | `--directory` | Directory to download the video(s) to. |
+| `-o` | `--output` | Name of the output file. |
+| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
+| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
-- `--alternative-progress` » shows an alternative, not so user-friendly progress instead of the progress bar
+### Archive
-- `-g`, `--goroutines` » sets how many parallel segment downloads should be used
+Archive works just like [download](#download). It downloads the given videos as `.mkv` files and stores all (soft) subtitles in it.
+Default audio locales are japanese and your system language (if available) but you can set more or less with the `--language` flag.
+
+Archive a file
+```shell
+$ crunchy archive https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+```
+
+Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
+```shell
+$ crunchy archive -c "ditf.tar.gz" https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx
+```
+
+##### Flags
+
+The following flags can be (optional) passed to modify the [archive](#archive) process.
+
+| Short | Extended | Description |
+|-------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
+| `-d` | `--directory` | Directory to download the video(s) to. |
+| `-o` | `--output` | Name of the output file. |
+| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli#archive) for more information. |
+| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
+| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
+| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
+
+### Help
-#### Help
- General help
- ```
+ ```shell
$ crunchy help
```
- Login help
- ```
+ ```shell
$ crunchy help login
```
- Download help
- ```
+ ```shell
$ crunchy help download
```
-#### Global flags
+- Archive help
+ ```shell
+ $ crunchy help archive
+ ```
+
+### Global flags
+
These flags you can use across every sub-command
-- `-q`, `--quiet` » disables all output
-- `-v`, `--verbose` » shows additional debug output
-- `--color` » adds color to the output (works only on not windows systems)
+| Flag | Description |
+|------|------------------------------------------------------|
+| `-q` | Disables all output. |
+| `-v` | Shows additional debug output. |
+| `-p` | Use a proxy to hide your ip / redirect your traffic. |
-- `-p`, `--proxy` » use a proxy to hide your ip / redirect your traffic
+# 📚 Library
-- `-l`, `--locale` » the language to display video specific things like the title. default is your system language
-
-## 📚 Library
Download the library via `go get`
-
-```
+```shell
$ go get github.com/ByteDream/crunchyroll-go
```
-### 📝 Examples
-```go
-func main() {
- // login with credentials
- crunchy, err := crunchyroll.LoginWithCredentials("user@example.com", "password", crunchyroll.US, http.DefaultClient)
- if err != nil {
- panic(err)
- }
+The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go).
- // finds a series or movie by a crunchyroll link
- video, err := crunchy.FindVideo("https://www.crunchyroll.com/darling-in-the-franxx")
- if err != nil {
- panic(err)
- }
+Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library).
- series := video.(*crunchyroll.Series)
- seasons, err := series.Seasons()
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d seasons for series %s\n", len(seasons), series.Title)
+# ☝️ Disclaimer
- // search `Darling` and return 20 results
- series, movies, err := crunchy.Search("Darling", 20)
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d series and %d movies for query `Darling`\n", len(series), len(movies))
-}
-```
+This tool is **ONLY** meant to be used for private purposes.
+To use this tool you need crunchyroll premium anyway, so there is no reason why rip and share the episodes.
-```go
-func main() {
- crunchy, err := crunchyroll.LoginWithSessionID("8e9gs135defhga790dvrf2i0eris8gts", crunchyroll.US, http.DefaultClient)
- if err != nil {
- panic(err)
- }
-
- // returns an episode slice with all episodes which are matching the given url.
- // the episodes in the returning slice differs from the underlying streams, but are all pointing to the first ditf episode
- episodes, err := crunchy.FindEpisode("https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575")
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d episodes\n", len(episodes))
-}
-```
-
-Structure
-
-Because of the apis structure, it can lead very fast much redundant code for simple tasks, like getting all episodes
-with japanese audio and german subtitle. For this case and some other, the api has a utility called `Structure` in its utils.
-
-```go
-func main() {
- crunchy, err := crunchyroll.LoginWithCredentials("user@example.com", "password", crunchyroll.US, http.DefaultClient)
- if err != nil {
- panic(err)
- }
-
- // search `Darling` and return 20 results (series and movies) or less
- series, movies, err := crunchy.Search("Darling", 20)
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d series and %d movies for search query `Darling`\n", len(series), len(movies))
-
- seasons, err := series[0].Seasons()
- if err != nil {
- panic(err)
- }
-
- // in the crunchyroll.utils package, you find some structs which can be used to simplify tasks.
- // you can recursively search all underlying content
- seriesStructure := utils.NewSeasonStructure(seasons)
-
- // this returns every format of all the above given seasons
- formats, err := seriesStructure.Formats()
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d formats\n", len(formats))
-
- filteredFormats, err := seriesStructure.FilterFormatsByLocales(crunchyroll.JP, crunchyroll.DE, true)
- if err != nil {
- panic(err)
- }
- fmt.Printf("Found %d formats with japanese audio and hardsubbed german subtitles\n", len(filteredFormats))
-
- // reverse sorts the formats after their resolution by calling a sort type which is also defined in the api utils
- // and stores the format with the highest resolution in a variable
- sort.Sort(sort.Reverse(utils.FormatsByResolution(filteredFormats)))
- format := formats[0]
- // get the episode from which the format is a child
- episode, err := seriesStructure.FilterEpisodeByFormat(format)
- if err != nil {
- panic(err)
- }
-
- file, err := os.Create(fmt.Sprintf("%s.ts", episode.Title))
- if err != nil {
- panic(err)
- }
-
- // download the format to the file
- if err := format.DownloadGoroutines(file, 4, nil); err != nil {
- panic(err)
- }
- fmt.Printf("Downloaded %s with %s resolution and %.2f fps as %s\n", episode.Title, format.Video.Resolution, format.Video.FPS, file.Name())
-
- // for more useful structure function just let your IDE's autocomplete make its thing
-}
-```
-
-As you can see in the example above, most of the `crunchyroll.utils` Structure functions are returning errors. There is
-a build-in functionality with are avoiding causing the most errors and let you safely ignore them as well.
-**Note that errors still can appear**
-
-```go
-func main() {
- crunchy, err := crunchyroll.LoginWithCredentials("user@example.com", "password", crunchyroll.US, http.DefaultClient)
- if err != nil {
- panic(err)
- }
-
- foundEpisodes, err := crunchy.FindEpisode("https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575")
- if err != nil {
- panic(err)
- }
- episodeStructure := utils.NewEpisodeStructure(foundEpisodes)
-
- // this function recursively calls all api endpoints, receives everything and stores it in memory,
- // so that after executing this, no more request to the crunchyroll server has to be made.
- // note that it could cause much network load while running this method.
- //
- // you should check the InitAllState before, because InitAll could have been already called or
- // another function has the initialization as side effect and re-initializing everything
- // will change every pointer in the struct which can cause massive problems afterwards.
- if !episodeStructure.InitAllState() {
- if err := episodeStructure.InitAll(); err != nil {
- panic(err)
- }
- }
-
- formats, _ := episodeStructure.Formats()
- streams, _ := episodeStructure.Streams()
- episodes, _ := episodeStructure.Episodes()
- fmt.Printf("Initialized %d formats, %d streams and %d episodes\n", len(formats), len(streams), len(episodes))
-}
-```
-
-### Tests
-You can also run test to see if the api works correctly.
-Before doing this, make sure to either set your crunchyroll email and password or sessions as environment variable.
-The email variable has to be named `EMAIL` and the password variable `PASSWORD`. If you want to use your session id, the variable must be named `SESSION_ID`.
-
-You can run the test via `make`
-```
-$ make test
-```
-
-or via `go` directly
-```
-$ go test .
-```
-
-# 🙏 Credits
-
-### [Kamyroll-Python](https://github.com/hyugogirubato/Kamyroll-Python)
-- Extracted all api endpoints and the login process from this
-
-### [m3u8](https://github.com/oopsguy/m3u8)
-- Decrypting mpeg stream files
-
-### All libraries
-- [m3u8](https://github.com/grafov/m3u8) (not the m3u8 library from above) » mpeg stream info library
-- [cobra](https://github.com/spf13/cobra) » cli library
-
-# 🗒️ Notice
-
-Sometimes the download stops without a reason on linux and does not go further. In this case the `tmpfs` / `/tmp` directory may be full. Execute `df /tmp` to see how much of the space is used.
-
-I would really appreciate if someone rewrites the complete cli. I'm not satisfied with it's current structure but at the moment I have no time and no desire to do it myself.
+**The responsibility for what happens to the downloaded videos lies entirely with the user who downloaded them.**
# ⚖ License
-This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file
-for more details.
+This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file for more details.
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 40c7f83..4054554 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -1,16 +1,18 @@
-.TH crunchyroll-go 1 "13 September 2021" "crunchyroll-go" "Crunchyroll Downloader"
+.TH crunchyroll-go 1 "21 March 2022" "crunchyroll-go" "Crunchyroll Downloader"
.SH NAME
crunchyroll-go - A cli for downloading videos and entire series from crunchyroll.
.SH SYNOPSIS
-crunchyroll-go [\fB-h\fR] [\fB--color\fR] [\fB-p\fR \fIPROXY\fR] [\fB-q\fR] [\fB-v\fR]
+crunchyroll-go [\fB-h\fR] [\fB-p\fR \fIPROXY\fR] [\fB-q\fR] [\fB-v\fR]
.br
crunchyroll-go help
.br
-crunchyroll-go login [\fB--session-id\fR \fISESSION_ID\fR] [\fIemail\fR, \fIpassword\fR]
+crunchyroll-go login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR] [\fIusername\fR, \fIpassword\fR]
.br
-crunchyroll-go download [\fB--alternative-progress\fR] [\fB-a\fR \fILOCALE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB--no-hardsub\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-s\fR \fILOCALE\fR] \fIURL…\fR
+crunchyroll-go download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
+.br
+crunchyroll-go archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.SH DESCRIPTION
.TP
@@ -28,10 +30,6 @@ This options can be passed to every action.
Shows help.
.TP
-\fB--color\fR
-Shows the output in different colors which will make it easier to differ the output.
-.TP
-
\fB-p, --proxy PROXY\fR
Sets a proxy through which all traffic will be routed.
.TP
@@ -43,42 +41,40 @@ Disables all output.
\fB-v, --verbose\fR
Shows verbose output.
-.SH LOGIN OPTIONS
-This options can only be used when calling the \fIlogin\fR action.
+.SH LOGIN COMMAND
+This command logs in to crunchyroll and stores the session id or credentials on the drive. This needs to be done before calling other commands since they need a valid login to operate.
+.TP
+
+\fB--persistent\fR
+Stores the given credentials permanent on the drive. The *nix path for it is $HOME/.config/crunchy.
+.br
+NOTE: The credentials are stored in plain text and if you not use \fB--session-id\fR your credentials are used (if you not use the \fB--persistent\fR flag only a session id gets stored regardless if you login with username/password or a session id).
.TP
\fB--session-id SESSION_ID\fR
-Login via a session id (which can be extracted from a crunchyroll browser cookie) instead of using email and password.
+Login via a session id (which can be extracted from a crunchyroll browser cookie) instead of using username and password.
-.SH DOWNLOAD OPTIONS
-This options can only be used when calling the \fIdownload\fR action.
+.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.
.TP
-\fB--alternative-progress\fR
-Shows an alternative, not so user-friendly progress instead of the progress bar which contains more information.
+\fB-a, --audio AUDIO\fR
+Forces to download videos with the given audio locale. If no video with this audio locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
.TP
-\fB-a, --audio LOCALE\fR
-Forces to download videos with the given audio locale. If no video with this audio locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-LA, es-ES, fr-FR, pt-BR, it-IT, de-DE, ru-RU, ar-ME.
+\fB-s, --subtitle SUBTITLE\fR
+Forces to download the videos with subtitles in the given locale / language. If no video with this subtitle locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
.TP
\fB-d, --directory DIRECTORY\fR
The directory to download all files to.
.TP
-\fB--no-hardsub\fR
-Same as '-s', but the subtitles are not stored in the video itself, but in a separate file.
-.TP
-
-\fB--only-sub\fR
-Downloads only the subtitles without the corresponding video.
-.TP
-
\fB-o, --output OUTPUT\fR
Name of the output file. Formatting is also supported, so if the name contains one or more of the following things, they will get replaced.
{title} » Title of the video.
{series_name} » Name of the series.
- {season_title} » Title of the season.
+ {season_name} » Name of the season.
{season_number} » Number of the season.
{episode_number} » Number of the episode.
{resolution} » Resolution of the video.
@@ -94,12 +90,66 @@ The video resolution. Can either be specified via the pixels (e.g. 1920x1080), t
Available common-use words: best (best available resolution), worst (worst available resolution).
.TP
-\fB-s, --subtitle LOCALE\fR
-Forces to download the videos with subtitles in the given locale / language. If no video with this subtitle locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-LA, es-ES, fr-FR, pt-BR, it-IT, de-DE, ru-RU, ar-ME.
+\fB-g, --goroutines GOROUTINES\fR
+Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
+
+.SH ARCHIVE COMMAND
+This command behaves like \fBdownload\fR besides the fact that it requires \fIffmpeg\fR and stores the output only to .mkv files.
+.TP
+
+\fB-l, --language LANGUAGE\fR
+Audio locales which should be downloaded. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
+.TP
+
+\fB-d, --directory DIRECTORY\fR
+The directory to download all files to.
+.TP
+
+\fB-o, --output OUTPUT\fR
+Name of the output file. Formatting is also supported, so if the name contains one or more of the following things, they will get replaced.
+ {title} » Title of the video.
+ {series_name} » Name of the series.
+ {season_name} » Name of the season.
+ {season_number} » Number of the season.
+ {episode_number} » Number of the episode.
+ {resolution} » Resolution of the video.
+ {fps} » Frame Rate of the video.
+ {audio} » Audio locale of the video.
+ {subtitle} » Subtitle locale of the video.
+.TP
+
+\fB-m, --merge MERGE BEHAVIOR\fR
+Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. \fB--audio\fR stores one video and only the audio of all other languages, \fBvideo\fR stores all videos of the given languages and their audio, \fBauto\fR (which is the default) only behaves like video if the length of two videos are different (and only for the two videos), else like audio.
+.TP
+
+\fB-c, --compress COMPRESS\fR
+If is set, all output will be compresses into an archive (every url generates a new one). 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.
+Just like \fB--output\fR the name can be formatted. But the only option available here is \fI{series_name}\fR.
+.TP
+
+\fB-r, resolution RESOLUTION\fR
+The video resolution. Can either be specified via the pixels (e.g. 1920x1080), the abbreviation for pixels (e.g. 1080p) or "common-use" words (e.g. best).
+ Available pixels: 1920x1080, 1280x720, 640x480, 480x360, 426x240.
+ Available abbreviations: 1080p, 720p, 480p, 360p, 240p.
+ Available common-use words: best (best available resolution), worst (worst available resolution).
.TP
\fB-g, --goroutines GOROUTINES\fR
-Sets the number of parallel downloads for the segments the final video is made of.
+Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
+
+.SH URL OPTIONS
+If you want to download only specific episode of a series, you could either pass every single episode url to the downloader (which is fine for 1 - 3 episodes) or use filtering.
+It works pretty simple, just put a specific pattern surrounded by square brackets at the end of the url from the anime you want to download. A season and / or episode as well as a range from where to where episodes should be downloaded can be specified.
+Use the list below to get a better overview what is possible
+ ...[E5] - Download the fifth episode.
+ ...[S1] - Download the full first season.
+ ...[-S2] - Download all seasons up to and including season 2.
+ ...[S3E4-] - Download all episodes from and including season 3, episode 4.
+ ...[S1E4-S3] - Download all episodes from and including season 1, episode 4, until and including season 3.
+
+In practise, it would look like this: \fIhttps://beta.crunchyroll.com/series/12345678/example[S1E5-S3E2]\fR.
+
+The \fBS\fR, followed by the number indicates the season number, \fBE\fR, followed by the number indicates an episode number. It doesn't matter if \fBS\fR, \fBE\fR or both are missing. Theoretically \fB[-]\fR is a valid pattern too. Note that \fBS\fR must always stay before \fBE\fR when used.
.SH EXAMPLES
Login via crunchyroll account email and password.
@@ -116,7 +166,15 @@ $ crunchyroll-go download -o "darling.mp4" -r 720p https://www.crunchyroll.com/d
Download a episode with japanese audio and american subtitles.
.br
-$ crunchyroll-go download -a ja-JP -s en-US https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchyroll-go download -a ja-JP -s en-US https://www.crunchyroll.com/darling-in-the-franxx[E3-E5]
+
+Stores the episode in a .mkv file.
+.br
+$ crunchyroll-go archive https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+
+Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
+.br
+$ crunchyroll-go archive -c "ditf.tar.gz" https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx[E1-E2]
.SH BUGS
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
@@ -127,7 +185,7 @@ ByteDream
Source: https://github.com/ByteDream/crunchyroll-go
.SH COPYRIGHT
-Copyright (C) 2021 ByteDream
+Copyright (C) 2022 ByteDream
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
From 449cc3789636b33539ba5ef2168b924471e727a2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 23 Mar 2022 08:46:47 +0100
Subject: [PATCH 096/732] Made second binary in install to symbolic link and
added clean target
---
Makefile | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 0744c35..7f2444a 100644
--- a/Makefile
+++ b/Makefile
@@ -9,9 +9,12 @@ build:
cd cmd/crunchyroll-go && go build -o $(BINARY_NAME)
mv cmd/crunchyroll-go/$(BINARY_NAME) .
+clean:
+ rm $(BINARY_NAME)
+
install:
install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchyroll-go
- cp -f $(DESTDIR)$(PREFIX)/bin/crunchyroll-go $(DESTDIR)$(PREFIX)/bin/crunchy
+ ln -sf $(DESTDIR)$(PREFIX)/bin/crunchyroll-go $(DESTDIR)$(PREFIX)/bin/crunchy
install -Dm644 crunchyroll-go.1 $(DESTDIR)$(PREFIX)/share/man/man1/crunchyroll-go.1
install -Dm644 LICENSE $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
From 23bb9fdbd6b8ade7b8d7424c4cfc1811a3357ea2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 18:03:46 +0100
Subject: [PATCH 097/732] Fix terminal width calculating causing error on some
systems
---
cmd/crunchyroll-go/cmd/utils.go | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index bf70564..f76a2bd 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -164,7 +164,13 @@ func terminalWidth() int {
if err != nil {
return 60
}
- width, err := strconv.Atoi(strings.Split(strings.ReplaceAll(string(res), "\n", ""), " ")[1])
+ // 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
}
From bb0e51deeec802a671fb646405ebf16f72eb8c12 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 19:08:43 +0100
Subject: [PATCH 098/732] Fix subtitle sorting
---
utils/sort.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/utils/sort.go b/utils/sort.go
index 90e05c7..82eb2b4 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -149,5 +149,5 @@ func (sbl SubtitlesByLocale) Swap(i, j int) {
sbl[i], sbl[j] = sbl[j], sbl[i]
}
func (sbl SubtitlesByLocale) Less(i, j int) bool {
- return sbl[i].Locale < sbl[j].Locale
+ return LocaleLanguage(sbl[i].Locale) < LocaleLanguage(sbl[j].Locale)
}
From 851dab7aac9cdf62e94b05bacdc75c71b45a90a8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 19:09:44 +0100
Subject: [PATCH 099/732] Add sorting for locales as string
---
cmd/crunchyroll-go/cmd/utils.go | 2 ++
1 file changed, 2 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index f76a2bd..b4afb73 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -13,6 +13,7 @@ import (
"reflect"
"regexp"
"runtime"
+ "sort"
"strconv"
"strings"
"sync"
@@ -66,6 +67,7 @@ func allLocalesAsStrings() (locales []string) {
for _, locale := range utils.AllLocales {
locales = append(locales, string(locale))
}
+ sort.Strings(locales)
return
}
From 8e791f7e8b506b0aa14edc96a46681395e954044 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 19:10:02 +0100
Subject: [PATCH 100/732] Fix unnecessary subtitle removing (#18) and add
option to enable custom subtitle sorting
---
cmd/crunchyroll-go/cmd/archive.go | 16 +++++++++-------
1 file changed, 9 insertions(+), 7 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 17072eb..f94d20a 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -385,13 +385,15 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
}
sort.Sort(utils.SubtitlesByLocale(info.format.Subtitles))
- if len(archiveLanguagesFlag) > 0 && archiveLanguagesFlag[0] != "all" {
- for j, language := range archiveLanguagesFlag {
- locale := crunchyroll.LOCALE(language)
- for k, subtitle := range info.format.Subtitles {
- if subtitle.Locale == locale {
- info.format.Subtitles = append(info.format.Subtitles[:k], info.format.Subtitles[k+1:]...)
- info.format.Subtitles = append(info.format.Subtitles[:j], append([]*crunchyroll.Subtitle{subtitle}, info.format.Subtitles[j:]...)...)
+
+ sortSubtitles, _ := strconv.ParseBool(os.Getenv("SORT_SUBTITLES"))
+ if sortSubtitles && len(archiveLanguagesFlag) > 0 {
+ // this sort the subtitle locales after the languages which were specified
+ // with the `archiveLanguagesFlag` flag
+ for _, language := range archiveLanguagesFlag {
+ for i, subtitle := range info.format.Subtitles {
+ if subtitle.Locale == crunchyroll.LOCALE(language) {
+ info.format.Subtitles = append([]*crunchyroll.Subtitle{subtitle}, append(info.format.Subtitles[:i], info.format.Subtitles[i+1:]...)...)
break
}
}
From 36ecbb0cd7d9f5fc3c4f26e011308102a6772259 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 19:33:14 +0100
Subject: [PATCH 101/732] Add newline on download progress failure
---
cmd/crunchyroll-go/cmd/archive.go | 5 +++++
cmd/crunchyroll-go/cmd/download.go | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index f94d20a..a36f806 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -274,6 +274,11 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
if err != nil {
return fmt.Errorf("error while setting up downloader: %v", err)
}
+ defer func() {
+ if downloadProgress.Total != downloadProgress.Current {
+ fmt.Println()
+ }
+ }()
rootFile, err := os.CreateTemp("", fmt.Sprintf("%s_*.ts", strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))))
if err != nil {
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index a3d78a8..bbf9a3e 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -190,6 +190,11 @@ func downloadInfo(info formatInformation, file *os.File) error {
if out.IsDev() {
downloadProgress.Prefix = out.DebugLog.Prefix()
}
+ defer func() {
+ if downloadProgress.Total != downloadProgress.Current {
+ fmt.Println()
+ }
+ }()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
From def5dc417ce44b761a9bf0c99d263434f847ca26 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 20:14:33 +0100
Subject: [PATCH 102/732] Version 2.0.1
---
Makefile | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Makefile b/Makefile
index 7f2444a..5ba784c 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.0
+VERSION=2.0.1
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
@@ -10,7 +10,7 @@ build:
mv cmd/crunchyroll-go/$(BINARY_NAME) .
clean:
- rm $(BINARY_NAME)
+ rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
install:
install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchyroll-go
From 6b3635aef3c997dab4bdc495dcdc36bd4b734d57 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 26 Mar 2022 20:30:02 +0100
Subject: [PATCH 103/732] Made symbolic link to second binary in install target
relative
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 5ba784c..7991824 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,7 @@ clean:
install:
install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchyroll-go
- ln -sf $(DESTDIR)$(PREFIX)/bin/crunchyroll-go $(DESTDIR)$(PREFIX)/bin/crunchy
+ ln -sf ./crunchyroll-go $(DESTDIR)$(PREFIX)/bin/crunchy
install -Dm644 crunchyroll-go.1 $(DESTDIR)$(PREFIX)/share/man/man1/crunchyroll-go.1
install -Dm644 LICENSE $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
From 051cad45370a6de16759bbe45f6f4eea740fd0ee Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Mar 2022 19:48:19 +0200
Subject: [PATCH 104/732] Fix download and goroutines flag not working with
archive (#19)
---
cmd/crunchyroll-go/cmd/archive.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index a36f806..cd18349 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -229,7 +229,7 @@ func archive(urls []string) error {
return fmt.Errorf("failed to pre generate new archive file: %v", err)
}
} else {
- dir := info.Format(downloadDirectoryFlag)
+ dir := info.Format(archiveDirectoryFlag)
if _, err = os.Stat(dir); os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("error while creating directory: %v", err)
@@ -289,7 +289,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
- downloader := crunchyroll.NewDownloader(ctx, rootFile, downloadGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
+ downloader := crunchyroll.NewDownloader(ctx, rootFile, archiveGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
// check if the context was cancelled.
// must be done in to not print any progress messages if ctrl+c was pressed
if ctx.Err() != nil {
From e4d075c8557e9024a597eeab0e86f6bf47297475 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Mar 2022 19:56:09 +0200
Subject: [PATCH 105/732] Fix download not converting into other media formats
if specified
---
cmd/crunchyroll-go/cmd/download.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index bbf9a3e..4832e68 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -216,6 +216,9 @@ func downloadInfo(info formatInformation, file *os.File) error {
}
return nil
})
+ if hasFFmpeg() {
+ downloader.FFmpegOpts = make([]string, 0)
+ }
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
From e0069a10e09287ba553846b950c5f7d6b3a73b34 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Mar 2022 19:57:19 +0200
Subject: [PATCH 106/732] Rename variables
---
cmd/crunchyroll-go/cmd/archive.go | 18 +++++++++---------
cmd/crunchyroll-go/cmd/download.go | 14 +++++++-------
2 files changed, 16 insertions(+), 16 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index cd18349..aefc85c 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -270,12 +270,12 @@ func archive(urls []string) error {
func archiveInfo(info 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))
- downloadProgress, err := createArchiveProgress(info)
+ dp, err := createArchiveProgress(info)
if err != nil {
return fmt.Errorf("error while setting up downloader: %v", err)
}
defer func() {
- if downloadProgress.Total != downloadProgress.Current {
+ if dp.Total != dp.Current {
fmt.Println()
}
}()
@@ -297,13 +297,13 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
}
if out.IsDev() {
- downloadProgress.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 {
- downloadProgress.Update()
+ dp.Update()
}
if current == total {
- downloadProgress.UpdateMessage("Merging segments", false)
+ dp.UpdateMessage("Merging segments", false)
}
return nil
})
@@ -415,7 +415,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
return fmt.Errorf("failed to merge files: %v", err)
}
- downloadProgress.UpdateMessage("Download finished", false)
+ dp.UpdateMessage("Download finished", false)
signal.Stop(sig)
out.Debug("Stopped signal catcher")
@@ -445,7 +445,7 @@ func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
progressCount += int(f.Video.Chunklist.Count()) + 1
}
- downloadProgress := &downloadProgress{
+ dp := &downloadProgress{
Prefix: out.InfoLog.Prefix(),
Message: "Downloading video",
// number of segments a video +1 is for the success message
@@ -454,10 +454,10 @@ func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
Quiet: out.IsQuiet(),
}
if out.IsDev() {
- downloadProgress.Prefix = out.DebugLog.Prefix()
+ dp.Prefix = out.DebugLog.Prefix()
}
- return downloadProgress, nil
+ return dp, nil
}
func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, video bool, formats ...*crunchyroll.Format) ([]string, error) {
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 4832e68..30d764a 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -179,7 +179,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
return fmt.Errorf("error while initializing the video: %v", err)
}
- downloadProgress := &downloadProgress{
+ dp := &downloadProgress{
Prefix: out.InfoLog.Prefix(),
Message: "Downloading video",
// number of segments a video has +2 is for merging and the success message
@@ -188,10 +188,10 @@ func downloadInfo(info formatInformation, file *os.File) error {
Quiet: out.IsQuiet(),
}
if out.IsDev() {
- downloadProgress.Prefix = out.DebugLog.Prefix()
+ dp.Prefix = out.DebugLog.Prefix()
}
defer func() {
- if downloadProgress.Total != downloadProgress.Current {
+ if dp.Total != dp.Current {
fmt.Println()
}
}()
@@ -206,13 +206,13 @@ func downloadInfo(info formatInformation, file *os.File) error {
}
if out.IsDev() {
- downloadProgress.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 {
- downloadProgress.Update()
+ dp.Update()
}
if current == total {
- downloadProgress.UpdateMessage("Merging segments", false)
+ dp.UpdateMessage("Merging segments", false)
}
return nil
})
@@ -248,7 +248,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
return fmt.Errorf("error while downloading: %v", err)
}
- downloadProgress.UpdateMessage("Download finished", false)
+ dp.UpdateMessage("Download finished", false)
signal.Stop(sig)
out.Debug("Stopped signal catcher")
From d27fc672889f97b245a953597017e890dc1025dd Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Mar 2022 20:02:51 +0200
Subject: [PATCH 107/732] Remove empty output on last download
---
cmd/crunchyroll-go/cmd/archive.go | 10 ++++++----
cmd/crunchyroll-go/cmd/download.go | 9 ++++++---
2 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index aefc85c..95091d0 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -218,8 +218,8 @@ func archive(urls []string) error {
}
out.Empty()
- for _, season := range episodes {
- for _, info := range season {
+ for j, season := range episodes {
+ for k, info := range season {
var filename string
var writeCloser io.WriteCloser
if c != nil {
@@ -253,8 +253,11 @@ func archive(urls []string) error {
}
return err
}
-
writeCloser.Close()
+
+ if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
+ out.Empty()
+ }
}
}
if c != nil {
@@ -420,7 +423,6 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
signal.Stop(sig)
out.Debug("Stopped signal catcher")
- out.Empty()
out.Empty()
return nil
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 30d764a..4fbac96 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -147,8 +147,8 @@ func download(urls []string) error {
}
out.Empty()
- for _, season := range episodes {
- for _, info := range season {
+ for j, season := range episodes {
+ for k, info := range season {
dir := info.Format(downloadDirectoryFlag)
if _, err = os.Stat(dir); os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0777); err != nil {
@@ -166,6 +166,10 @@ func download(urls []string) error {
return err
}
file.Close()
+
+ if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
+ out.Empty()
+ }
}
}
}
@@ -253,7 +257,6 @@ func downloadInfo(info formatInformation, file *os.File) error {
signal.Stop(sig)
out.Debug("Stopped signal catcher")
- out.Empty()
out.Empty()
return nil
From d34fd10516f0834f672bb509af1eff49b544dc83 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Mar 2022 20:05:40 +0200
Subject: [PATCH 108/732] Refactoring
---
README.md | 39 +++++++++++++++++++++------------------
1 file changed, 21 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 709c3ae..64c7cd9 100644
--- a/README.md
+++ b/README.md
@@ -46,21 +46,21 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
## 💾 Get the executable
- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```
$ yay -S crunchyroll-go
```
- 🛠 Build it yourself
- - use `make` (requires `go` to be installed):
+ - use `make` (requires `go` to be installed):
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go
$ make && sudo make install
```
- - use `go`:
+ - use `go`:
```
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go/cmd/crunchyroll-go
@@ -69,27 +69,27 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
## 📝 Examples
-_Before reading_: Because of the huge functionality not all cases can be covered in the README.
-Make sure to check the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli), further usages and options are described there.
+_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli), further usages and options are described there.
### Login
Before you can do something, you have to login first.
This can be performed via crunchyroll account email and password.
+
```
$ crunchy login user@example.com password
```
or via session id
+
```
$ crunchy login --session-id 8e9gs135defhga790dvrf2i0eris8gts
```
### Download
-By default, the cli tries to download the episode with your system language as audio.
-If no streams with your system language are available, the video will be downloaded with japanese audio and hardsubbed subtitles in your system language.
+By default, the cli tries to download the episode with your system language as audio. If no streams with your system language are available, the video will be downloaded with japanese audio and hardsubbed subtitles in your system language.
**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
```
@@ -97,21 +97,22 @@ $ crunchy download https://www.crunchyroll.com/darling-in-the-franxx/episode-1-a
```
With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
+
```
$ crunchy download -r best https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
The file is by default saved as a `.ts` (mpeg transport stream) file.
-`.ts` files may can't be played or are looking very weird (it depends on the video player you are using).
-With the `-o` flag, you can change the name (and file ending) of the output file.
-So if you want to save it as, for example, `mp4` file, just name it `whatever.mp4`.
+`.ts` files may can't be played or are looking very weird (it depends on the video player you are using). With the `-o` flag, you can change the name (and file ending) of the output file. So if you want to save it as, for example, `mp4`
+file, just name it `whatever.mp4`.
**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
+
```
$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
-With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have.
-Type `crunchy help download` to see all available locales.
+With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have. Type `crunchy help download` to see all available locales.
+
```
$ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/darling-in-the-franxx
```
@@ -131,15 +132,17 @@ The following flags can be (optional) passed to modify the [download](#download)
### Archive
-Archive works just like [download](#download). It downloads the given videos as `.mkv` files and stores all (soft) subtitles in it.
-Default audio locales are japanese and your system language (if available) but you can set more or less with the `--language` flag.
+Archive works just like [download](#download). It downloads the given videos as `.mkv` files and stores all (soft) subtitles in it. Default audio locales are japanese and your system language (if available) but you can set more or less with
+the `--language` flag.
Archive a file
+
```shell
$ crunchy archive https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
+
```shell
$ crunchy archive -c "ditf.tar.gz" https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx
```
@@ -191,6 +194,7 @@ These flags you can use across every sub-command
# 📚 Library
Download the library via `go get`
+
```shell
$ go get github.com/ByteDream/crunchyroll-go
```
@@ -201,8 +205,7 @@ Examples how to use the library and some features of it are described in the [wi
# ☝️ Disclaimer
-This tool is **ONLY** meant to be used for private purposes.
-To use this tool you need crunchyroll premium anyway, so there is no reason why rip and share the episodes.
+This tool is **ONLY** meant to be used for private purposes. To use this tool you need crunchyroll premium anyway, so there is no reason why rip and share the episodes.
**The responsibility for what happens to the downloaded videos lies entirely with the user who downloaded them.**
From 8dcbced9c78068e937e06573daa87c5797006fbe Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 2 Apr 2022 20:16:45 +0200
Subject: [PATCH 109/732] Version 2.0.2
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 7991824..8a21b56 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.1
+VERSION=2.0.2
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From cda7bc9d35ad79f36f2fe75637a73ca49f8b1e8d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 22:01:10 +0200
Subject: [PATCH 110/732] Add login file fallback and session id caching if
logging in with credentials
---
cmd/crunchyroll-go/cmd/utils.go | 58 ++++++++++++++++++++-------------
1 file changed, 35 insertions(+), 23 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index b4afb73..f7a95ad 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -119,39 +119,51 @@ func loadCrunchy() {
files = append(files, filepath.Join(usr.HomeDir, ".config/crunchy"))
}
- var body []byte
var err error
for _, file := range files {
if _, err = os.Stat(file); os.IsNotExist(err) {
+ err = nil
continue
}
- body, err = os.ReadFile(file)
- break
- }
- if body == nil {
- out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
- os.Exit(1)
- } else 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 crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
+ var body []byte
+ if body, err = os.ReadFile(file); err != nil {
+ out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
+ } else if body == nil {
+ continue
}
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+
+ split := strings.SplitN(string(body), "\n", 2)
+ if len(split) == 1 || split[1] == "" {
+ if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err == nil {
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+ }
+ } else {
+ if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
+ continue
+ }
+ out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
+ if runtime.GOOS != "windows" {
+ // the session id is written to a temp file to reduce the amount of re-logging in.
+ // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600); err != nil {
+ out.StopProgress("Failed to write session id to temp file")
+ os.Exit(1)
+ }
+ out.Debug("Wrote session id to temp file")
+ }
+ }
+
+ out.StopProgress("Logged in")
+ return
+ }
+ if err != nil {
+ out.StopProgress(err.Error())
} else {
- if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
- os.Exit(1)
- }
- out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
+ out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
}
- out.StopProgress("Logged in")
+ os.Exit(1)
}
func hasFFmpeg() bool {
From 02bd33ef596d3e818ddc8dfdfee855ab2824f9a9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 22:14:11 +0200
Subject: [PATCH 111/732] Deprecate pure crunchyroll classic url functions
---
crunchyroll.go | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 82cd118..3717eb3 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -348,7 +348,11 @@ func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Epi
return matchingEpisodes, nil
}
-// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url
+// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url.
+//
+// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaSeriesURL
+// if possible since beta url are always safe to use.
+// The method will stay in the library until only beta urls are supported by crunchyroll itself.
func ParseVideoURL(url string) (seriesName string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/?$`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
@@ -364,7 +368,11 @@ func ParseVideoURL(url string) (seriesName string, ok bool) {
// ParseEpisodeURL tries to extract the crunchyroll series name, title, episode number and web id out of the given crunchyroll url
// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
-// the episode number will be 235
+// the episode number will be 235.
+//
+// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaEpisodeURL
+// if possible since beta url are always safe to use.
+// The method will stay in the library until only beta urls are supported by crunchyroll itself.
func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, webId int, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P.+)-(?P\d+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
From db30b9eadcab09b484b5c3379723cd75a0e6bea5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 22:16:45 +0200
Subject: [PATCH 112/732] Add notice when downloading via cli and no episodes
could be found
---
cmd/crunchyroll-go/cmd/archive.go | 2 ++
cmd/crunchyroll-go/cmd/download.go | 2 ++
2 files changed, 4 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 95091d0..9f5df94 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -182,6 +182,8 @@ func archive(urls []string) error {
episodes, err := archiveExtractEpisodes(url)
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
+ out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
return err
}
out.StopProgress("Parsed url %d", i+1)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 4fbac96..f53b3be 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -128,6 +128,8 @@ func download(urls []string) error {
episodes, err := downloadExtractEpisodes(url)
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
+ out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
return err
}
out.StopProgress("Parsed url %d", i+1)
From 253d8712c81b3389aa2aafac14334455c9a7e708 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 22:34:45 +0200
Subject: [PATCH 113/732] Add /videos suffix support for classic series url
---
crunchyroll.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 3717eb3..d34ba81 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -354,7 +354,7 @@ func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Epi
// if possible since beta url are always safe to use.
// The method will stay in the library until only beta urls are supported by crunchyroll itself.
func ParseVideoURL(url string) (seriesName string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/?$`)
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)(/videos)?/?$`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
groups := regexGroups(urlMatch, pattern.SubexpNames()...)
seriesName = groups["series"]
From e2f42493ac23e90334d5ce164f3bb2ddf6dfd020 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 22:38:06 +0200
Subject: [PATCH 114/732] Fix credential file overwrite with session id
---
cmd/crunchyroll-go/cmd/utils.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index f7a95ad..4d3faad 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -143,7 +143,7 @@ func loadCrunchy() {
continue
}
out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
- if runtime.GOOS != "windows" {
+ if file != filepath.Join(os.TempDir(), ".crunchy") {
// the session id is written to a temp file to reduce the amount of re-logging in.
// it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600); err != nil {
From 2e9ce3cf52628b49171da4c36754aa340867a295 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 15 Apr 2022 23:45:09 +0200
Subject: [PATCH 115/732] Deprecated find video and optimized find episode (as
result of #22)
---
crunchyroll.go | 28 ++++++++++++++++++----------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index d34ba81..cc21b8b 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -306,6 +306,10 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
// FindVideoByName finds a Video (Season or Movie) by its name.
// Use this in combination with ParseVideoURL and hand over the corresponding results
// to this function.
+//
+// Deprecated: Use Search instead. The first result sometimes isn't the correct one
+// so this function is inaccurate in some cases.
+// See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information.
func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
s, m, err := c.Search(seriesName, 1)
if err != nil {
@@ -324,27 +328,31 @@ func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
// Use this in combination with ParseEpisodeURL and hand over the corresponding results
// to this function.
func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Episode, error) {
- video, err := c.FindVideoByName(seriesName)
- if err != nil {
- return nil, err
- }
- seasons, err := video.(*Series).Seasons()
+ series, _, err := c.Search(seriesName, 5)
if err != nil {
return nil, err
}
var matchingEpisodes []*Episode
- for _, season := range seasons {
- episodes, err := season.Episodes()
+ for _, s := range series {
+ seasons, err := s.Seasons()
if err != nil {
return nil, err
}
- for _, episode := range episodes {
- if episode.SlugTitle == episodeTitle {
- matchingEpisodes = append(matchingEpisodes, episode)
+
+ for _, season := range seasons {
+ episodes, err := season.Episodes()
+ if err != nil {
+ return nil, err
+ }
+ for _, episode := range episodes {
+ if episode.SlugTitle == episodeTitle {
+ matchingEpisodes = append(matchingEpisodes, episode)
+ }
}
}
}
+
return matchingEpisodes, nil
}
From 3617955bc5e83293ec9a5217a5d544b74e666455 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 16 Apr 2022 00:17:36 +0200
Subject: [PATCH 116/732] Fix typos & add more comments
---
README.md | 2 +-
cmd/crunchyroll-go/cmd/archive.go | 8 ++++----
cmd/crunchyroll-go/cmd/utils.go | 2 +-
crunchyroll.go | 30 +++++++++++++++---------------
downloader.go | 30 +++++++++++++++---------------
episode.go | 10 ++++++----
error.go | 2 +-
format.go | 7 ++++---
movie_listing.go | 8 +++++---
season.go | 6 ++++--
stream.go | 7 ++++---
subtitle.go | 2 ++
url.go | 4 ++--
utils/locale.go | 5 +++--
utils/sort.go | 12 +++++++-----
video.go | 15 ++++++++-------
16 files changed, 82 insertions(+), 68 deletions(-)
diff --git a/README.md b/README.md
index 64c7cd9..8bfa193 100644
--- a/README.md
+++ b/README.md
@@ -73,7 +73,7 @@ _Before reading_: Because of the huge functionality not all cases can be covered
### Login
-Before you can do something, you have to login first.
+Before you can do something, you have to log in first.
This can be performed via crunchyroll account email and password.
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 9f5df94..23c2c68 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -728,7 +728,7 @@ func (tc *tarCompress) Close() error {
err = tc.dst.Close()
if err != nil && err2 != nil {
- // best way to show double errors at once that i've found
+ // 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
@@ -750,11 +750,11 @@ func (tc *tarCompress) NewFile(information formatInformation) (io.WriteCloser, e
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
+ // 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
+ // 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()),
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 4d3faad..42fe7de 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -403,7 +403,7 @@ func (dp *downloadProgress) update(msg string, permanent bool) {
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
+ // 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
diff --git a/crunchyroll.go b/crunchyroll.go
index cc21b8b..d6d2744 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -13,7 +13,7 @@ import (
"strconv"
)
-// LOCALE represents a locale / language
+// LOCALE represents a locale / language.
type LOCALE string
const (
@@ -31,16 +31,16 @@ const (
)
type Crunchyroll struct {
- // Client is the http.Client to perform all requests over
+ // Client is the http.Client to perform all requests over.
Client *http.Client
- // Context can be used to stop requests with Client and is context.Background by default
+ // Context can be used to stop requests with Client and is context.Background by default.
Context context.Context
- // Locale specifies in which language all results should be returned / requested
+ // Locale specifies in which language all results should be returned / requested.
Locale LOCALE
- // SessionID is the crunchyroll session id which was used for authentication
+ // SessionID is the crunchyroll session id which was used for authentication.
SessionID string
- // Config stores parameters which are needed by some api calls
+ // Config stores parameters which are needed by some api calls.
Config struct {
TokenType string
AccessToken string
@@ -56,11 +56,11 @@ type Crunchyroll struct {
MaturityRating string
}
- // If cache is true, internal caching is enabled
+ // If cache is true, internal caching is enabled.
cache bool
}
-// LoginWithCredentials logs in via crunchyroll username or email and password
+// LoginWithCredentials logs in via crunchyroll username or email and password.
func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s",
"LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI")
@@ -87,7 +87,7 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
}
// LoginWithSessionID logs in via a crunchyroll session id.
-// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com
+// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com.
func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
crunchy := &Crunchyroll{
Client: client,
@@ -205,7 +205,7 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return crunchy, nil
}
-// request is a base function which handles api requests
+// request is a base function which handles api requests.
func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
if err != nil {
@@ -241,7 +241,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
}
// IsCaching returns if data gets cached or not.
-// See SetCaching for more information
+// See SetCaching for more information.
func (c *Crunchyroll) IsCaching() bool {
return c.cache
}
@@ -249,12 +249,12 @@ func (c *Crunchyroll) IsCaching() bool {
// SetCaching enables or disables internal caching of requests made.
// Caching is enabled by default.
// If it is disabled the already cached data still gets called.
-// The best way to prevent this is to create a complete new Crunchyroll struct
+// The best way to prevent this is to create a complete new Crunchyroll struct.
func (c *Crunchyroll) SetCaching(caching bool) {
c.cache = caching
}
-// Search searches a query and returns all found series and movies within the given limit
+// Search searches a query and returns all found series and movies within the given limit.
func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
query, limit, c.Locale)
@@ -397,7 +397,7 @@ func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, w
return
}
-// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season
+// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season.
func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
@@ -408,7 +408,7 @@ func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
return
}
-// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode
+// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode.
func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P\w+).*`)
if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
diff --git a/downloader.go b/downloader.go
index be625b3..74546da 100644
--- a/downloader.go
+++ b/downloader.go
@@ -20,7 +20,7 @@ import (
)
// NewDownloader creates a downloader with default settings which should
-// fit the most needs
+// fit the most needs.
func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
tmp, _ := os.MkdirTemp("", "crunchy_")
@@ -43,12 +43,12 @@ type Downloader struct {
// The files will be placed directly into the root of the directory.
// If empty a random temporary directory on the system's default tempdir
// will be created.
- // If the directory does not exist, it will be created
+ // If the directory does not exist, it will be created.
TempDir string
// If DeleteTempAfter is true, the temp directory gets deleted afterwards.
// Note that in case of a hard signal exit (os.Interrupt, ...) the directory
// will NOT be deleted. In such situations try to catch the signal and
- // cancel Context
+ // cancel Context.
DeleteTempAfter bool
// Context to control the download process with.
@@ -56,7 +56,7 @@ type Downloader struct {
// process. So it is not recommend stopping the program immediately after calling
// the cancel function. It's better when canceling it and then exit the program
// when Format.Download throws an error. See the signal handling section in
- // cmd/crunchyroll-go/cmd/download.go for an example
+ // cmd/crunchyroll-go/cmd/download.go for an example.
Context context.Context
// Goroutines is the number of goroutines to download segments with
@@ -65,11 +65,11 @@ type Downloader struct {
// A method to call when a segment was downloaded.
// Note that the segments are downloaded asynchronously (depending on the count of
// Goroutines) and the function gets called asynchronously too, so for example it is
- // first called on segment 1, then segment 254, then segment 3 and so on
+ // first called on segment 1, then segment 254, then segment 3 and so on.
OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
// If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at
// once. Normally (because of the use of goroutines while downloading) multiple could get
- // called simultaneously
+ // called simultaneously.
LockOnSegmentDownload bool
// If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files.
@@ -82,7 +82,7 @@ type Downloader struct {
FFmpegOpts []string
}
-// download's the given format
+// download's the given format.
func (d Downloader) download(format *Format) error {
if err := format.InitVideo(); err != nil {
return err
@@ -109,7 +109,7 @@ func (d Downloader) download(format *Format) error {
}
// mergeSegments reads every file in tempDir and writes their content to Downloader.Writer.
-// The given output file gets created or overwritten if already existing
+// The given output file gets created or overwritten if already existing.
func (d Downloader) mergeSegments(files []string) error {
for _, file := range files {
select {
@@ -132,7 +132,7 @@ func (d Downloader) mergeSegments(files []string) error {
// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
// with ffmpeg (https://ffmpeg.org/).
-// The given output file gets created or overwritten if already existing
+// The given output file gets created or overwritten if already existing.
func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
list, err := os.Create(filepath.Join(d.TempDir, "list.txt"))
if err != nil {
@@ -214,13 +214,13 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
// After every segment download onSegmentDownload will be called with:
// the downloaded segment, the current position, the total size of segments to download,
// the file where the segment content was written to an error (if occurred).
-// The filename is always .ts
+// The filename is always .ts.
//
// Short explanation:
// The actual crunchyroll video is split up in multiple segments (or video files) which
// have to be downloaded and merged after to generate a single video file.
// And this function just downloads each of this segment into the given directory.
-// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information
+// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information.
func (d Downloader) downloadSegments(format *Format) ([]string, error) {
if err := format.InitVideo(); err != nil {
return nil, err
@@ -318,7 +318,7 @@ func (d Downloader) downloadSegments(format *Format) ([]string, error) {
}
}
-// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence
+// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence.
func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
var resp *http.Response
@@ -341,7 +341,7 @@ func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, i
return block, iv, nil
}
-// downloadSegment downloads a segment, decrypts it and names it after the given index
+// downloadSegment downloads a segment, decrypts it and names it after the given index.
func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
// every segment is aes-128 encrypted and has to be decrypted when downloaded
content, err := d.decryptSegment(format.crunchy.Client, segment, block, iv)
@@ -361,7 +361,7 @@ func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment,
return file, nil
}
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25
+// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25.
func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
req, err := http.NewRequestWithContext(d.Context, http.MethodGet, segment.URI, nil)
if err != nil {
@@ -387,7 +387,7 @@ func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegme
return raw, nil
}
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47
+// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47.
func (d Downloader) pkcs5UnPadding(origData []byte) []byte {
length := len(origData)
unPadding := int(origData[length-1])
diff --git a/episode.go b/episode.go
index d9814cb..a25844c 100644
--- a/episode.go
+++ b/episode.go
@@ -9,6 +9,7 @@ import (
"time"
)
+// Episode contains all information about an episode.
type Episode struct {
crunchy *Crunchyroll
@@ -74,7 +75,7 @@ type Episode struct {
StreamID string
}
-// EpisodeFromID returns an episode by its api id
+// EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -111,7 +112,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// AudioLocale returns the audio locale of the episode.
// Every episode in a season (should) have the same audio locale,
-// so if you want to get the audio locale of a season, just call this method on the first episode of the season
+// so if you want to get the audio locale of a season, just call
+// this method on the first episode of the season.
func (e *Episode) AudioLocale() (LOCALE, error) {
streams, err := e.Streams()
if err != nil {
@@ -120,7 +122,7 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil
}
-// GetFormat returns the format which matches the given resolution and subtitle locale
+// GetFormat returns the format which matches the given resolution and subtitle locale.
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
streams, err := e.Streams()
if err != nil {
@@ -186,7 +188,7 @@ func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*
return nil, fmt.Errorf("no matching resolution found")
}
-// Streams returns all streams which are available for the episode
+// Streams returns all streams which are available for the episode.
func (e *Episode) Streams() ([]*Stream, error) {
if e.children != nil {
return e.children, nil
diff --git a/error.go b/error.go
index 79ce1c0..b3e887f 100644
--- a/error.go
+++ b/error.go
@@ -3,7 +3,7 @@ package crunchyroll
import "fmt"
// AccessError is an error which will be returned when some special sort of api request fails.
-// See Crunchyroll.request when the error gets used
+// See Crunchyroll.request when the error gets used.
type AccessError struct {
error
diff --git a/format.go b/format.go
index ec94c16..6f17bf7 100644
--- a/format.go
+++ b/format.go
@@ -11,11 +11,12 @@ const (
MOVIE = "movies"
)
+// Format contains detailed information about an episode video stream.
type Format struct {
crunchy *Crunchyroll
ID string
- // FormatType represents if the format parent is an episode or a movie
+ // FormatType represents if the format parent is an episode or a movie.
FormatType FormatType
Video *m3u8.Variant
AudioLocale LOCALE
@@ -27,7 +28,7 @@ type Format struct {
// The Format.Video.Chunklist pointer is, by default, nil because an additional
// request must be made to receive its content. The request is not made when
// initializing a Format struct because it would probably cause an intense overhead
-// since Format.Video.Chunklist is only used sometimes
+// since Format.Video.Chunklist is only used sometimes.
func (f *Format) InitVideo() error {
if f.Video.Chunklist == nil {
resp, err := f.crunchy.Client.Get(f.Video.URI)
@@ -45,7 +46,7 @@ func (f *Format) InitVideo() error {
return nil
}
-// Download downloads the Format with the via Downloader specified options
+// Download downloads the Format with the via Downloader specified options.
func (f *Format) Download(downloader Downloader) error {
return downloader.download(f)
}
diff --git a/movie_listing.go b/movie_listing.go
index 9646c18..63d7fab 100644
--- a/movie_listing.go
+++ b/movie_listing.go
@@ -5,6 +5,8 @@ import (
"fmt"
)
+// MovieListing contains information about something which is called
+// movie listing. I don't know what this means thb.
type MovieListing struct {
crunchy *Crunchyroll
@@ -36,7 +38,7 @@ type MovieListing struct {
AvailabilityNotes string `json:"availability_notes"`
}
-// MovieListingFromID returns a movie listing by its api id
+// MovieListingFromID returns a movie listing by its api id.
func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -65,7 +67,7 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
return movieListing, nil
}
-// AudioLocale is same as Episode.AudioLocale
+// AudioLocale is same as Episode.AudioLocale.
func (ml *MovieListing) AudioLocale() (LOCALE, error) {
resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
ml.crunchy.Config.CountryCode,
@@ -86,7 +88,7 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) {
return LOCALE(jsonBody["audio_locale"].(string)), nil
}
-// Streams returns all streams which are available for the movie listing
+// Streams returns all streams which are available for the movie listing.
func (ml *MovieListing) Streams() ([]*Stream, error) {
return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
ml.crunchy.Config.CountryCode,
diff --git a/season.go b/season.go
index c9a17da..825a816 100644
--- a/season.go
+++ b/season.go
@@ -6,6 +6,7 @@ import (
"regexp"
)
+// Season contains information about an anime season.
type Season struct {
crunchy *Crunchyroll
@@ -41,7 +42,7 @@ type Season struct {
SubtitleLocales []LOCALE
}
-// SeasonFromID returns a season by its api id
+// SeasonFromID returns a season by its api id.
func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -70,6 +71,7 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
return season, nil
}
+// AudioLocale returns the audio locale of the season.
func (s *Season) AudioLocale() (LOCALE, error) {
episodes, err := s.Episodes()
if err != nil {
@@ -78,7 +80,7 @@ func (s *Season) AudioLocale() (LOCALE, error) {
return episodes[0].AudioLocale()
}
-// Episodes returns all episodes which are available for the season
+// Episodes returns all episodes which are available for the season.
func (s *Season) Episodes() (episodes []*Episode, err error) {
if s.children != nil {
return s.children, nil
diff --git a/stream.go b/stream.go
index 5fe0f96..d8957e6 100644
--- a/stream.go
+++ b/stream.go
@@ -8,6 +8,7 @@ import (
"regexp"
)
+// Stream contains information about all available video stream of an episode.
type Stream struct {
crunchy *Crunchyroll
@@ -22,7 +23,7 @@ type Stream struct {
streamURL string
}
-// StreamsFromID returns a stream by its api id
+// StreamsFromID returns a stream by its api id.
func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -35,7 +36,7 @@ func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
crunchy.Config.KeyPairID))
}
-// Formats returns all formats which are available for the stream
+// Formats returns all formats which are available for the stream.
func (s *Stream) Formats() ([]*Format, error) {
if s.children != nil {
return s.children, nil
@@ -70,7 +71,7 @@ func (s *Stream) Formats() ([]*Format, error) {
return formats, nil
}
-// fromVideoStreams returns all streams which are accessible via the endpoint
+// fromVideoStreams returns all streams which are accessible via the endpoint.
func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) {
resp, err := crunchy.request(endpoint)
if err != nil {
diff --git a/subtitle.go b/subtitle.go
index 6a33e14..164b56e 100644
--- a/subtitle.go
+++ b/subtitle.go
@@ -5,6 +5,7 @@ import (
"net/http"
)
+// Subtitle contains the information about a video subtitle.
type Subtitle struct {
crunchy *Crunchyroll
@@ -13,6 +14,7 @@ type Subtitle struct {
Format string `json:"format"`
}
+// Save writes the subtitle to the given io.Writer.
func (s Subtitle) Save(writer io.Writer) error {
req, err := http.NewRequestWithContext(s.crunchy.Context, http.MethodGet, s.URL, nil)
if err != nil {
diff --git a/url.go b/url.go
index 5abf370..32603fc 100644
--- a/url.go
+++ b/url.go
@@ -5,7 +5,7 @@ import (
)
// ExtractEpisodesFromUrl extracts all episodes from an url.
-// If audio is not empty, the episodes gets filtered after the given locale
+// If audio is not empty, the episodes gets filtered after the given locale.
func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Episode, error) {
series, episodes, err := c.ParseUrl(url)
if err != nil {
@@ -78,7 +78,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
// ParseUrl parses the given url into a series or episode.
-// The returning episode is a slice because non-beta urls have the same episode with different languages
+// The returning episode is a slice because non-beta urls have the same episode with different languages.
func (c *Crunchyroll) ParseUrl(url string) (*Series, []*Episode, error) {
if seriesId, ok := ParseBetaSeriesURL(url); ok {
series, err := SeriesFromID(c, seriesId)
diff --git a/utils/locale.go b/utils/locale.go
index 8d78912..111496b 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -4,6 +4,7 @@ import (
"github.com/ByteDream/crunchyroll-go"
)
+// AllLocales is an array of all available locales.
var AllLocales = []crunchyroll.LOCALE{
crunchyroll.JP,
crunchyroll.US,
@@ -18,7 +19,7 @@ var AllLocales = []crunchyroll.LOCALE{
crunchyroll.AR,
}
-// ValidateLocale validates if the given locale actually exist
+// ValidateLocale validates if the given locale actually exist.
func ValidateLocale(locale crunchyroll.LOCALE) bool {
for _, l := range AllLocales {
if l == locale {
@@ -28,7 +29,7 @@ func ValidateLocale(locale crunchyroll.LOCALE) bool {
return false
}
-// LocaleLanguage returns the country by its locale
+// LocaleLanguage returns the country by its locale.
func LocaleLanguage(locale crunchyroll.LOCALE) string {
switch locale {
case crunchyroll.JP:
diff --git a/utils/sort.go b/utils/sort.go
index 82eb2b4..d4c7925 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -9,7 +9,7 @@ import (
)
// SortEpisodesBySeason sorts the given episodes by their seasons.
-// Note that the same episodes just with different audio locales will cause problems
+// Note that the same episodes just with different audio locales will cause problems.
func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode {
sortMap := map[string]map[int][]*crunchyroll.Episode{}
@@ -43,7 +43,7 @@ func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Epis
return eps
}
-// SortEpisodesByAudio sort the given episodes by their audio locale
+// SortEpisodesByAudio sort the given episodes by their audio locale.
func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCALE][]*crunchyroll.Episode, error) {
eps := map[crunchyroll.LOCALE][]*crunchyroll.Episode{}
@@ -81,7 +81,7 @@ func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCAL
return eps, nil
}
-// MovieListingsByDuration sorts movie listings by their duration
+// MovieListingsByDuration sorts movie listings by their duration.
type MovieListingsByDuration []*crunchyroll.MovieListing
func (mlbd MovieListingsByDuration) Len() int {
@@ -94,7 +94,7 @@ func (mlbd MovieListingsByDuration) Less(i, j int) bool {
return mlbd[i].DurationMS < mlbd[j].DurationMS
}
-// EpisodesByDuration sorts episodes by their duration
+// EpisodesByDuration sorts episodes by their duration.
type EpisodesByDuration []*crunchyroll.Episode
func (ebd EpisodesByDuration) Len() int {
@@ -107,6 +107,7 @@ func (ebd EpisodesByDuration) Less(i, j int) bool {
return ebd[i].DurationMS < ebd[j].DurationMS
}
+// EpisodesByNumber sorts episodes after their episode number.
type EpisodesByNumber []*crunchyroll.Episode
func (ebn EpisodesByNumber) Len() int {
@@ -119,7 +120,7 @@ func (ebn EpisodesByNumber) Less(i, j int) bool {
return ebn[i].EpisodeNumber < ebn[j].EpisodeNumber
}
-// FormatsByResolution sorts formats after their resolution
+// FormatsByResolution sorts formats after their resolution.
type FormatsByResolution []*crunchyroll.Format
func (fbr FormatsByResolution) Len() int {
@@ -140,6 +141,7 @@ func (fbr FormatsByResolution) Less(i, j int) bool {
return iResX+iResY < jResX+jResY
}
+// SubtitlesByLocale sorts subtitles after their locale.
type SubtitlesByLocale []*crunchyroll.Subtitle
func (sbl SubtitlesByLocale) Len() int {
diff --git a/video.go b/video.go
index f543df1..00b7734 100644
--- a/video.go
+++ b/video.go
@@ -30,8 +30,10 @@ type video struct {
} `json:"images"`
}
+// Video is the base for Movie and Season.
type Video interface{}
+// Movie contains information about a movie.
type Movie struct {
video
Video
@@ -40,7 +42,7 @@ type Movie struct {
children []*MovieListing
- // not generated when calling MovieFromID
+ // not generated when calling MovieFromID.
MovieListingMetadata struct {
AvailabilityNotes string `json:"availability_notes"`
AvailableOffline bool `json:"available_offline"`
@@ -65,7 +67,7 @@ type Movie struct {
}
}
-// MovieFromID returns a movie by its api id
+// MovieFromID returns a movie by its api id.
func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -95,8 +97,6 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
}
// MovieListing returns all videos corresponding with the movie.
-// Beside the normal movie, sometimes movie previews are returned too, but you can try to get the actual movie
-// by sorting the returning MovieListing slice with the utils.MovieListingByDuration interface
func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
if m.children != nil {
return m.children, nil
@@ -134,6 +134,7 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
return movieListings, nil
}
+// Series contains information about an anime series.
type Series struct {
video
Video
@@ -156,13 +157,13 @@ type Series struct {
MatureRatings []string `json:"mature_ratings"`
SeasonCount int `json:"season_count"`
- // not generated when calling SeriesFromID
+ // not generated when calling SeriesFromID.
SearchMetadata struct {
Score float64 `json:"score"`
}
}
-// SeriesFromID returns a series by its api id
+// SeriesFromID returns a series by its api id.
func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
crunchy.Config.CountryCode,
@@ -191,7 +192,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
return series, nil
}
-// Seasons returns all seasons of a series
+// Seasons returns all seasons of a series.
func (s *Series) Seasons() (seasons []*Season, err error) {
if s.children != nil {
return s.children, nil
From 598e460e6cd14dee602300e778f96f6a558ddfb7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 16 Apr 2022 01:07:56 +0200
Subject: [PATCH 117/732] Add custom useragent for cli request
---
Makefile | 13 +++++-------
cmd/crunchyroll-go/cmd/root.go | 13 ++++++++++--
cmd/crunchyroll-go/cmd/utils.go | 37 +++++++++++++++++++++++++++------
3 files changed, 47 insertions(+), 16 deletions(-)
diff --git a/Makefile b/Makefile
index 8a21b56..8f42f36 100644
--- a/Makefile
+++ b/Makefile
@@ -6,8 +6,7 @@ DESTDIR=
PREFIX=/usr
build:
- cd cmd/crunchyroll-go && go build -o $(BINARY_NAME)
- mv cmd/crunchyroll-go/$(BINARY_NAME) .
+ go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -25,10 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
release:
- cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_linux
- cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_windows.exe
- cd cmd/crunchyroll-go && CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o $(VERSION_BINARY_NAME)_darwin
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
- strip cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_linux
-
- mv cmd/crunchyroll-go/$(VERSION_BINARY_NAME)_* .
+ strip $(VERSION_BINARY_NAME)_linux
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 25c8270..c06df04 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -2,6 +2,7 @@ package cmd
import (
"context"
+ "fmt"
"github.com/ByteDream/crunchyroll-go"
"github.com/spf13/cobra"
"net/http"
@@ -10,6 +11,8 @@ import (
"strings"
)
+var Version = "development"
+
var (
client *http.Client
crunchy *crunchyroll.Crunchyroll
@@ -17,7 +20,10 @@ var (
quietFlag bool
verboseFlag bool
- proxyFlag string
+
+ proxyFlag string
+
+ useragentFlag string
)
var rootCmd = &cobra.Command{
@@ -36,7 +42,7 @@ var rootCmd = &cobra.Command{
out.DebugLog.Printf("Executing `%s` command with %d arg(s)\n", cmd.Name(), len(args))
- client, err = createOrDefaultClient(proxyFlag)
+ client, err = createOrDefaultClient(proxyFlag, useragentFlag)
return
},
}
@@ -44,7 +50,10 @@ var rootCmd = &cobra.Command{
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("crunchyroll-go/%s", Version), "Useragent to do all request with")
}
func Execute() {
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 42fe7de..7589010 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -71,7 +71,23 @@ func allLocalesAsStrings() (locales []string) {
return
}
-func createOrDefaultClient(proxy string) (*http.Client, error) {
+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 {
@@ -80,12 +96,21 @@ func createOrDefaultClient(proxy string) (*http.Client, error) {
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: &http.Transport{
- DisableCompression: true,
- Proxy: http.ProxyURL(proxyURL),
- },
- Timeout: 30 * time.Second,
+ Transport: rt,
+ Timeout: 30 * time.Second,
}
return client, nil
}
From b524a1a7dd99071b01fced9b8c4dbab5ede1852c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 16 Apr 2022 01:08:18 +0200
Subject: [PATCH 118/732] Version 2.1.0
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 8f42f36..bbff663 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.0.2
+VERSION=2.1.0
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From 9ccd1ed93cd8bae97ae95594eff406aab68990e3 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 16 Apr 2022 01:27:59 +0200
Subject: [PATCH 119/732] Create dependabot.yml
---
.github/dependabot.yml | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .github/dependabot.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..3938344
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "daily"
From a98eb56fbad77f4c666f9270282dc004cda07f9a Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 16 Apr 2022 01:59:34 +0200
Subject: [PATCH 120/732] Create ci.yml
---
.github/workflows/ci.yml | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..0ae3bc7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v3
+ with:
+ go-version: 1.16
+
+ - name: Build
+ run: go build -v cmd/crunchyroll-go/main.go
+
+ - name: Test
+ run: go test -v cmd/crunchyroll-go/main.go
From 543b9c36681a1b6a4d9a1065c6db658e7e535118 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 16 Apr 2022 02:02:10 +0200
Subject: [PATCH 121/732] Update ci.yml
---
.github/workflows/ci.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 0ae3bc7..7887e0c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,3 +1,5 @@
+name: CI
+
on:
push:
branches: [ master ]
@@ -5,7 +7,7 @@ on:
branches: [ master ]
jobs:
- build:
+ test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
From 4b5b18773093f7a17262316f1a32a8c365b09720 Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sun, 17 Apr 2022 18:57:42 +0200
Subject: [PATCH 122/732] Add version to cli root command
---
cmd/crunchyroll-go/cmd/root.go | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index c06df04..3a8a6b6 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -27,8 +27,9 @@ var (
)
var rootCmd = &cobra.Command{
- Use: "crunchyroll",
- Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",
+ Use: "crunchyroll",
+ Version: Version,
+ Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",
SilenceErrors: true,
SilenceUsage: true,
From 6385457c10110f1f006f6ea2c0c5a32258d56341 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 17 Apr 2022 21:28:53 +0200
Subject: [PATCH 123/732] Add --version command description (#24)
---
crunchyroll-go.1 | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 4054554..2fd5437 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -40,6 +40,10 @@ Disables all output.
\fB-v, --verbose\fR
Shows verbose output.
+.TP
+
+\fB--version\fR
+Shows the current cli version.
.SH LOGIN COMMAND
This command logs in to crunchyroll and stores the session id or credentials on the drive. This needs to be done before calling other commands since they need a valid login to operate.
From 2d08e985383ad0a61db5b60ec7b74dfc5fb0008d Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 17 Apr 2022 21:33:20 +0200
Subject: [PATCH 124/732] Add ci badge
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index 8bfa193..d6d8b6e 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,9 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
+
+
+
From 580ea74902baecf126402ae10765ff0f8cf7e8fb Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 18 Apr 2022 10:24:56 +0200
Subject: [PATCH 125/732] Update ci badge
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index d6d8b6e..b56657b 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
-
+
From fd3d945c3afee236a17cd9e218d83c940be18fb8 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 18 Apr 2022 15:43:50 +0200
Subject: [PATCH 126/732] Create codeql-analysis.yml
---
.github/workflows/codeql-analysis.yml | 70 +++++++++++++++++++++++++++
1 file changed, 70 insertions(+)
create mode 100644 .github/workflows/codeql-analysis.yml
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..b7d4dfe
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,70 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ branches: [ master ]
+ schedule:
+ - cron: '40 3 * * 2'
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'go' ]
+ # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+ # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v2
+ with:
+ languages: ${{ matrix.language }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+ # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v2
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ #- run: |
+ # make bootstrap
+ # make release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v2
From be3d33744fafd6b195008cc8b6d2a5a00a7f166a Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Mon, 18 Apr 2022 16:56:27 +0200
Subject: [PATCH 127/732] Rename getCmd variable to downloadCmd
---
cmd/crunchyroll-go/cmd/download.go | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index f53b3be..55b4b1f 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -28,7 +28,7 @@ var (
downloadGoroutinesFlag int
)
-var getCmd = &cobra.Command{
+var downloadCmd = &cobra.Command{
Use: "download",
Short: "Download a video",
Args: cobra.MinimumNArgs(1),
@@ -72,23 +72,23 @@ var getCmd = &cobra.Command{
}
func init() {
- getCmd.Flags().StringVarP(&downloadAudioFlag, "audio",
+ downloadCmd.Flags().StringVarP(&downloadAudioFlag, "audio",
"a",
string(systemLocale(false)),
"The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
- getCmd.Flags().StringVarP(&downloadSubtitleFlag,
+ downloadCmd.Flags().StringVarP(&downloadSubtitleFlag,
"subtitle",
"s",
"",
"The locale of the subtitle. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
cwd, _ := os.Getwd()
- getCmd.Flags().StringVarP(&downloadDirectoryFlag,
+ downloadCmd.Flags().StringVarP(&downloadDirectoryFlag,
"directory",
"d",
cwd,
"The directory to download the file(s) into")
- getCmd.Flags().StringVarP(&downloadOutputFlag,
+ downloadCmd.Flags().StringVarP(&downloadOutputFlag,
"output",
"o",
"{title}.ts",
@@ -104,7 +104,7 @@ func init() {
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
- getCmd.Flags().StringVarP(&downloadResolutionFlag,
+ downloadCmd.Flags().StringVarP(&downloadResolutionFlag,
"resolution",
"r",
"best",
@@ -113,13 +113,13 @@ func init() {
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
- getCmd.Flags().IntVarP(&downloadGoroutinesFlag,
+ downloadCmd.Flags().IntVarP(&downloadGoroutinesFlag,
"goroutines",
"g",
runtime.NumCPU(),
"Sets how many parallel segment downloads should be used")
- rootCmd.AddCommand(getCmd)
+ rootCmd.AddCommand(downloadCmd)
}
func download(urls []string) error {
From 58bf54996496a6e98b241967ba50222c3a2ed595 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Tue, 19 Apr 2022 21:32:19 +0200
Subject: [PATCH 128/732] Update README.md
---
README.md | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index b56657b..6e205c4 100644
--- a/README.md
+++ b/README.md
@@ -53,18 +53,19 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
- [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
- [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
- ```
+ ```shell
$ yay -S crunchyroll-go
```
-- 🛠 Build it yourself
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 mac):
- use `make` (requires `go` to be installed):
- ```
+ ```shell
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go
- $ make && sudo make install
+ $ make
+ $ sudo make install # <- only if you want to install it on your system
```
- use `go`:
- ```
+ ```shell
$ git clone https://github.com/ByteDream/crunchyroll-go
$ cd crunchyroll-go/cmd/crunchyroll-go
$ go build -o crunchy
@@ -80,13 +81,13 @@ Before you can do something, you have to log in first.
This can be performed via crunchyroll account email and password.
-```
+```shell
$ crunchy login user@example.com password
```
or via session id
-```
+```shell
$ crunchy login --session-id 8e9gs135defhga790dvrf2i0eris8gts
```
@@ -95,13 +96,13 @@ $ crunchy login --session-id 8e9gs135defhga790dvrf2i0eris8gts
By default, the cli tries to download the episode with your system language as audio. If no streams with your system language are available, the video will be downloaded with japanese audio and hardsubbed subtitles in your system language.
**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
-```
+```shell
$ crunchy download https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
-```
+```shell
$ crunchy download -r best https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
@@ -110,13 +111,13 @@ The file is by default saved as a `.ts` (mpeg transport stream) file.
file, just name it `whatever.mp4`.
**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
-```
+```shell
$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
```
With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have. Type `crunchy help download` to see all available locales.
-```
+```shell
$ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/darling-in-the-franxx
```
From 68aa7e903fee57e1d9ba572a77e7f983de99c791 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Fri, 22 Apr 2022 12:30:33 +0200
Subject: [PATCH 129/732] Update go build instructions
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6e205c4..8ffc2b5 100644
--- a/README.md
+++ b/README.md
@@ -67,8 +67,8 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
- use `go`:
```shell
$ git clone https://github.com/ByteDream/crunchyroll-go
- $ cd crunchyroll-go/cmd/crunchyroll-go
- $ go build -o crunchy
+ $ cd crunchyroll-go
+ $ go build -o crunchy cmd/crunchyroll-go/main.go
```
## 📝 Examples
From 06a541421039a50682af40e4e4975dfa1ec9f6c9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 26 Apr 2022 08:34:33 +0200
Subject: [PATCH 130/732] Add video, audio and subtitle as locale (#26)
---
cmd/crunchyroll-go/cmd/archive.go | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 23c2c68..deb17ad 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -530,21 +530,25 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
input = append(input, "-i", video)
maps = append(maps, "-map", strconv.Itoa(i))
locale := crunchyroll.LOCALE(re.FindStringSubmatch(video)[1])
- metadata = append(metadata, fmt.Sprintf("-metadata:s:v:%d", i), fmt.Sprintf("language=%s", utils.LocaleLanguage(locale)))
- metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", utils.LocaleLanguage(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: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)))
}
for i, audio := range audioFiles {
input = append(input, "-i", audio)
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
locale := crunchyroll.LOCALE(re.FindStringSubmatch(audio)[1])
- metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", utils.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("title=%s", utils.LocaleLanguage(locale)))
}
for i, subtitle := range subtitleFiles {
input = append(input, "-i", subtitle)
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)+len(audioFiles)))
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("title=%s", utils.LocaleLanguage(locale)))
}
From 980c28a754609e28c60db249168abb56ef12f239 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 26 Apr 2022 08:34:50 +0200
Subject: [PATCH 131/732] Add scoop installer instructions
---
README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/README.md b/README.md
index 8ffc2b5..a477171 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,11 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
```shell
$ yay -S crunchyroll-go
```
+ - On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+- ```shell
+ $ scoop bucket add extra # <- in case you haven't added the extra repository already
+ $ scoop install crunchyroll-go
+ ```
- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 mac):
- use `make` (requires `go` to be installed):
```shell
From c9ad097d8521a7463019293ec572090923495022 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Tue, 26 Apr 2022 08:42:01 +0200
Subject: [PATCH 132/732] Fix typo
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index a477171..014da47 100644
--- a/README.md
+++ b/README.md
@@ -56,8 +56,8 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
```shell
$ yay -S crunchyroll-go
```
- - On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
-- ```shell
+- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+ ```shell
$ scoop bucket add extra # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
From 1e865adaa5e6bd7430a37b4d72b6330cdf470750 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Tue, 26 Apr 2022 08:53:02 +0200
Subject: [PATCH 133/732] Typo fix in scoop install
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 014da47..255f9d9 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
```
- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
- $ scoop bucket add extra # <- in case you haven't added the extra repository already
+ $ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 mac):
From 0c92fc0989641d75491c8e722b4726fc472620e3 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 27 Apr 2022 10:53:40 +0200
Subject: [PATCH 134/732] Deactivate subtitles by default (#26)
---
cmd/crunchyroll-go/cmd/archive.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index deb17ad..6fc0aba 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -568,7 +568,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
file.Close()
defer os.Remove(file.Name())
- commandOptions = append(commandOptions, "-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
if out.IsDev() {
From 48595f25fa34a3bb7ae01e82191b59e4c8b00060 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 28 Apr 2022 10:21:02 +0200
Subject: [PATCH 135/732] Update debug print variable
---
cmd/crunchyroll-go/cmd/root.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 3a8a6b6..c8830b9 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -41,7 +41,7 @@ var rootCmd = &cobra.Command{
out = newLogger(false, false, false)
}
- out.DebugLog.Printf("Executing `%s` command with %d arg(s)\n", cmd.Name(), len(args))
+ out.Debug("Executing `%s` command with %d arg(s)", cmd.Name(), len(args))
client, err = createOrDefaultClient(proxyFlag, useragentFlag)
return
From 037df1d16f0febc3bc6e66e4a5921c72d7e87971 Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:31:01 +0200
Subject: [PATCH 136/732] handle session queries not being valid
---
crunchyroll.go | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index d6d2744..5d9031d 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -70,6 +70,10 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
}
defer sessResp.Body.Close()
+ if sessResp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Failed to start session: %s", sessResp.Status)
+ }
+
var data map[string]interface{}
body, _ := io.ReadAll(sessResp.Body)
json.Unmarshal(body, &data)
@@ -109,6 +113,11 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, err
}
defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("Failed to start session: %s", resp.Status)
+ }
+
json.NewDecoder(resp.Body).Decode(&jsonBody)
if _, ok := jsonBody["message"]; ok {
return nil, errors.New("invalid session id")
From db47eeb11c6f30dec008a5802deb9129dc09bea5 Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:34:17 +0200
Subject: [PATCH 137/732] handle login with creds errors
---
crunchyroll.go | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 5d9031d..57548cc 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -71,7 +71,7 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
defer sessResp.Body.Close()
if sessResp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("Failed to start session: %s", sessResp.Status)
+ return nil, fmt.Errorf("failed to start session for credentials login: %s", sessResp.Status)
}
var data map[string]interface{}
@@ -85,7 +85,15 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
authValues.Set("session_id", sessionID)
authValues.Set("account", user)
authValues.Set("password", password)
- client.Post(loginEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(authValues.Encode()))
+ loginResp, err := client.Post(loginEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(authValues.Encode()))
+ if err != nil {
+ return nil, err
+ }
+ defer loginResp.Body.Close()
+
+ if loginResp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status)
+ }
return LoginWithSessionID(sessionID, locale, client)
}
From 353f425bbfeeb9c6b74ff3cc93954309a95ae418 Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:36:45 +0200
Subject: [PATCH 138/732] typo fix
---
crunchyroll.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 57548cc..0d03440 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -123,7 +123,7 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("Failed to start session: %s", resp.Status)
+ return nil, fmt.Errorf("failed to start session: %s", resp.Status)
}
json.NewDecoder(resp.Body).Decode(&jsonBody)
From aa3f8e1b341a39f2a78cf8d5522afd721ecd97d6 Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:39:11 +0200
Subject: [PATCH 139/732] handle json parsing errors
---
crunchyroll.go | 24 ++++++++++++++++++------
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 0d03440..8ae0932 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -76,7 +76,9 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
var data map[string]interface{}
body, _ := io.ReadAll(sessResp.Body)
- json.Unmarshal(body, &data)
+ if err = json.Unmarshal(body, &data); err != nil {
+ return nil, fmt.Errorf("failed to parse start session with credentials response: %w", err)
+ }
sessionID := data["data"].(map[string]interface{})["session_id"].(string)
@@ -126,7 +128,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, fmt.Errorf("failed to start session: %s", resp.Status)
}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
+ }
if _, ok := jsonBody["message"]; ok {
return nil, errors.New("invalid session id")
}
@@ -178,7 +182,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, err
}
defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'token' response: %w", err)
+ }
crunchy.Config.TokenType = jsonBody["token_type"].(string)
crunchy.Config.AccessToken = jsonBody["access_token"].(string)
@@ -189,7 +195,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, err
}
defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'index' response: %w", err)
+ }
cms := jsonBody["cms"].(map[string]interface{})
crunchy.Config.Policy = cms["policy"].(string)
@@ -203,7 +211,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, err
}
defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
+ }
crunchy.Config.AccountID = jsonBody["account_id"].(string)
crunchy.Config.ExternalID = jsonBody["external_id"].(string)
@@ -215,7 +225,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, err
}
defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
+ }
crunchy.Config.MaturityRating = jsonBody["maturity_rating"].(string)
From 187a0c8817e6ecfe446712bb3f9db4e040776bd1 Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:43:43 +0200
Subject: [PATCH 140/732] add missing defer
---
crunchyroll.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index 8ae0932..bf20ed8 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -244,6 +244,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
resp, err := c.Client.Do(req)
if err == nil {
+ defer resp.Body.Close()
bodyAsBytes, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
From 362708cf3584c192988a1f37eb659ff3b30271fe Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:45:00 +0200
Subject: [PATCH 141/732] handle json unmarshall error
---
crunchyroll.go | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index bf20ed8..ece4da1 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -295,7 +295,9 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
defer resp.Body.Close()
var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'search' response: %w", err)
+ }
for _, item := range jsonBody["items"].([]interface{}) {
item := item.(map[string]interface{})
From 413949797c8ed1f1b75df3515a70d05143b9e5ad Mon Sep 17 00:00:00 2001
From: Hekmon
Date: Fri, 29 Apr 2022 11:49:23 +0200
Subject: [PATCH 142/732] avoid copy error to be shadowed
---
downloader.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/downloader.go b/downloader.go
index 74546da..0001347 100644
--- a/downloader.go
+++ b/downloader.go
@@ -199,8 +199,8 @@ func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
}
}
if f, ok := d.Writer.(*os.File); !ok || f.Name() != tmpfile {
- file, err := os.Open(tmpfile)
- if err != nil {
+ var file *os.File
+ if file, err = os.Open(tmpfile); err != nil {
return err
}
defer file.Close()
From 1f1f6849dcdb2f0217c841d340c7c5f91c81d93a Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Fri, 29 Apr 2022 15:50:10 +0200
Subject: [PATCH 143/732] Fix typo
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 255f9d9..7bdef2b 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 mac):
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
```shell
$ git clone https://github.com/ByteDream/crunchyroll-go
From a49e65e1514fde0dd2b0071a887a95fea5f8d8b8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 30 Apr 2022 21:25:40 +0200
Subject: [PATCH 144/732] Enable autosuggestions but hide it from commands
---
cmd/crunchyroll-go/cmd/root.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index c8830b9..5ac91b5 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -58,7 +58,7 @@ func init() {
}
func Execute() {
- rootCmd.CompletionOptions.DisableDefaultCmd = true
+ rootCmd.CompletionOptions.HiddenDefaultCmd = true
defer func() {
if r := recover(); r != nil {
if out.IsDev() {
From 80b0784f50773fadaa0835128ec2e603e6f57ed2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 1 May 2022 13:25:43 +0200
Subject: [PATCH 145/732] Update module to v2
---
cmd/crunchyroll-go/cmd/archive.go | 4 ++--
cmd/crunchyroll-go/cmd/download.go | 4 ++--
cmd/crunchyroll-go/cmd/login.go | 2 +-
cmd/crunchyroll-go/cmd/root.go | 2 +-
cmd/crunchyroll-go/cmd/utils.go | 4 ++--
cmd/crunchyroll-go/main.go | 2 +-
go.mod | 2 +-
utils/locale.go | 2 +-
utils/sort.go | 2 +-
9 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 6fc0aba..6f3f6a0 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -8,8 +8,8 @@ import (
"compress/gzip"
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go"
- "github.com/ByteDream/crunchyroll-go/utils"
+ "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v2/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 55b4b1f..1ce202c 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -3,8 +3,8 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go"
- "github.com/ByteDream/crunchyroll-go/utils"
+ "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v2/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"os"
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 1303516..5bef2a9 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -2,7 +2,7 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go"
+ "github.com/ByteDream/crunchyroll-go/v2"
"github.com/spf13/cobra"
"os"
"os/user"
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 5ac91b5..14f0c0e 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go"
+ "github.com/ByteDream/crunchyroll-go/v2"
"github.com/spf13/cobra"
"net/http"
"os"
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 7589010..136fe28 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -2,8 +2,8 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go"
- "github.com/ByteDream/crunchyroll-go/utils"
+ "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v2/utils"
"net/http"
"net/url"
"os"
diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go
index efc6a1e..a502afb 100644
--- a/cmd/crunchyroll-go/main.go
+++ b/cmd/crunchyroll-go/main.go
@@ -1,7 +1,7 @@
package main
import (
- "github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd"
+ "github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd"
)
func main() {
diff --git a/go.mod b/go.mod
index d3e701e..7a4bf42 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/ByteDream/crunchyroll-go
+module github.com/ByteDream/crunchyroll-go/v2
go 1.16
diff --git a/utils/locale.go b/utils/locale.go
index 111496b..537b165 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go"
+ "github.com/ByteDream/crunchyroll-go/v2"
)
// AllLocales is an array of all available locales.
diff --git a/utils/sort.go b/utils/sort.go
index d4c7925..a44717d 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go"
+ "github.com/ByteDream/crunchyroll-go/v2"
"sort"
"strconv"
"strings"
From ddfd2b44a10e53012827127ff2e4e8d778ad5259 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 1 May 2022 14:03:17 +0200
Subject: [PATCH 146/732] Update library get option to v2
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7bdef2b..cb657b6 100644
--- a/README.md
+++ b/README.md
@@ -205,7 +205,7 @@ These flags you can use across every sub-command
Download the library via `go get`
```shell
-$ go get github.com/ByteDream/crunchyroll-go
+$ go get github.com/ByteDream/crunchyroll-go/v2
```
The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go).
From a590da82318db2027f80438780366fa20b68f5a1 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 1 May 2022 14:04:06 +0200
Subject: [PATCH 147/732] Version 2.2.0
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index bbff663..a73003a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.1.0
+VERSION=2.2.0
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From ad703fb98515aab5f418eb091f3b597fb82c2566 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 1 May 2022 17:12:29 +0200
Subject: [PATCH 148/732] Update documentation url
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cb657b6..c58e2ea 100644
--- a/README.md
+++ b/README.md
@@ -208,7 +208,7 @@ Download the library via `go get`
$ go get github.com/ByteDream/crunchyroll-go/v2
```
-The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go).
+The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v2).
Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library).
From df441049005196566da39031bf29ae383df827eb Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 4 May 2022 08:17:33 +0200
Subject: [PATCH 149/732] Update example urls to their beta equivalents
---
README.md | 12 ++++++------
crunchyroll-go.1 | 10 +++++-----
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index c58e2ea..23054d0 100644
--- a/README.md
+++ b/README.md
@@ -102,13 +102,13 @@ By default, the cli tries to download the episode with your system language as a
**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
```shell
-$ crunchy download https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchy download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
```shell
-$ crunchy download -r best https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchy download -r best https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
The file is by default saved as a `.ts` (mpeg transport stream) file.
@@ -117,13 +117,13 @@ file, just name it `whatever.mp4`.
**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
```shell
-$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have. Type `crunchy help download` to see all available locales.
```shell
-$ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/darling-in-the-franxx
+$ crunchy download --audio ja-JP --subtitle de-DE https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
```
##### Flags
@@ -147,13 +147,13 @@ the `--language` flag.
Archive a file
```shell
-$ crunchy archive https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchy archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
```shell
-$ crunchy archive -c "ditf.tar.gz" https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx
+$ crunchy archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
```
##### Flags
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 2fd5437..558aa23 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -162,23 +162,23 @@ $ crunchyroll-go login user@example.com 12345678
Download a episode normally. Your system locale will be used for the video's audio.
.br
-$ crunchyroll-go download https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchyroll-go download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with 720p and name it to 'darling.mp4'. Note that you need \fBffmpeg\fR to save files which do not have '.ts' as file extension.
.br
-$ crunchyroll-go download -o "darling.mp4" -r 720p https://www.crunchyroll.com/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchyroll-go download -o "darling.mp4" -r 720p https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with japanese audio and american subtitles.
.br
-$ crunchyroll-go download -a ja-JP -s en-US https://www.crunchyroll.com/darling-in-the-franxx[E3-E5]
+$ crunchyroll-go download -a ja-JP -s en-US https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
Stores the episode in a .mkv file.
.br
-$ crunchyroll-go archive https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx/episode-1-alone-and-lonesome-759575
+$ crunchyroll-go archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
.br
-$ crunchyroll-go archive -c "ditf.tar.gz" https://www.crunchyroll.com/darling-in-the-franxx/darling-in-the-franxx[E1-E2]
+$ crunchyroll-go archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
.SH BUGS
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
From 7db4ca6b93e0b0dbacf51e549ac6d760fa324537 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 6 May 2022 11:22:32 +0200
Subject: [PATCH 150/732] Fix typos
---
downloader.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/downloader.go b/downloader.go
index 0001347..05622cc 100644
--- a/downloader.go
+++ b/downloader.go
@@ -36,7 +36,7 @@ func NewDownloader(context context.Context, writer io.Writer, goroutines int, on
// Downloader is used to download Format's
type Downloader struct {
- // The output is all written to Writer
+ // The output is all written to Writer.
Writer io.Writer
// TempDir is the directory where the temporary segment files should be stored.
@@ -59,7 +59,7 @@ type Downloader struct {
// cmd/crunchyroll-go/cmd/download.go for an example.
Context context.Context
- // Goroutines is the number of goroutines to download segments with
+ // Goroutines is the number of goroutines to download segments with.
Goroutines int
// A method to call when a segment was downloaded.
@@ -82,7 +82,7 @@ type Downloader struct {
FFmpegOpts []string
}
-// download's the given format.
+// download downloads the given format.
func (d Downloader) download(format *Format) error {
if err := format.InitVideo(); err != nil {
return err
From 5e3636015b33da6f88b72fb4b4d9b5135b76d666 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 7 May 2022 10:46:25 +0200
Subject: [PATCH 151/732] Remove AccessError error struct
---
crunchyroll.go | 11 ++---------
error.go | 21 ---------------------
2 files changed, 2 insertions(+), 30 deletions(-)
delete mode 100644 error.go
diff --git a/crunchyroll.go b/crunchyroll.go
index ece4da1..6b1854f 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -248,21 +248,14 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
bodyAsBytes, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
- return nil, &AccessError{
- URL: endpoint,
- Body: bodyAsBytes,
- }
+ return nil, fmt.Errorf("invalid access token")
} else {
var errStruct struct {
Message string `json:"message"`
}
json.NewDecoder(bytes.NewBuffer(bodyAsBytes)).Decode(&errStruct)
if errStruct.Message != "" {
- return nil, &AccessError{
- URL: endpoint,
- Body: bodyAsBytes,
- Message: errStruct.Message,
- }
+ return nil, fmt.Errorf(errStruct.Message)
}
}
resp.Body = io.NopCloser(bytes.NewBuffer(bodyAsBytes))
diff --git a/error.go b/error.go
deleted file mode 100644
index b3e887f..0000000
--- a/error.go
+++ /dev/null
@@ -1,21 +0,0 @@
-package crunchyroll
-
-import "fmt"
-
-// AccessError is an error which will be returned when some special sort of api request fails.
-// See Crunchyroll.request when the error gets used.
-type AccessError struct {
- error
-
- URL string
- Body []byte
- Message string
-}
-
-func (ae *AccessError) Error() string {
- if ae.Message == "" {
- return fmt.Sprintf("Access token invalid for url %s\nBody: %s", ae.URL, string(ae.Body))
- } else {
- return ae.Message
- }
-}
From ed8e63c26880c630bd1b03d62d46ed076f7f5afc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 8 May 2022 11:06:37 +0200
Subject: [PATCH 152/732] Fix locale panic (#29)
---
cmd/crunchyroll-go/cmd/utils.go | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 136fe28..29a48f7 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -33,9 +33,13 @@ var urlFilter = regexp.MustCompile(`(S(\d+))?(E(\d+))?((-)(S(\d+))?(E(\d+))?)?(,
func systemLocale(verbose bool) crunchyroll.LOCALE {
if runtime.GOOS != "windows" {
if lang, ok := os.LookupEnv("LANG"); ok {
- prefix := strings.Split(lang, "_")[0]
- suffix := strings.Split(strings.Split(lang, ".")[0], "_")[1]
- l := crunchyroll.LOCALE(fmt.Sprintf("%s-%s", prefix, suffix))
+ 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)
From 1c1dd5ec4b759e57deecbc6a41dcd7fba127846c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 8 May 2022 11:07:00 +0200
Subject: [PATCH 153/732] Version 2.2.1
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index a73003a..eef19a4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.2.0
+VERSION=2.2.1
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From 11b0f2b48baad073694100f596f4f9bef5f32806 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 8 May 2022 15:40:36 +0200
Subject: [PATCH 154/732] Fix typos
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 23054d0..c83493c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-Version 2 is out 🥳, see all the changes.
+Version 2 is out 🥳, see all the changes .
# crunchyroll-go
@@ -192,7 +192,7 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
### Global flags
-These flags you can use across every sub-command
+These flags you can use across every sub-command:
| Flag | Description |
|------|------------------------------------------------------|
From e7e106f74d7ddbf054ba14f92e381a2b390753fa Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 8 May 2022 19:14:16 +0200
Subject: [PATCH 155/732] Fix version not set when building with make
---
Makefile | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/Makefile b/Makefile
index eef19a4..d23fd11 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ DESTDIR=
PREFIX=/usr
build:
- go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
+ go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -24,8 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
release:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
strip $(VERSION_BINARY_NAME)_linux
From 3ee53c0cab7629c09f76ce56da8520a35515a833 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 13 May 2022 18:38:38 +0200
Subject: [PATCH 156/732] Bump go version to 1.18
---
go.mod | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index 7a4bf42..9a38468 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,13 @@
module github.com/ByteDream/crunchyroll-go/v2
-go 1.16
+go 1.18
require (
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.4.0
)
+
+require (
+ github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+)
From 901bbf0706e22ee37ee4f940b274f40b412261b7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 13 May 2022 19:22:17 +0200
Subject: [PATCH 157/732] Add update command
---
cmd/crunchyroll-go/cmd/update.go | 134 +++++++++++++++++++++++++++++++
crunchyroll-go.1 | 9 +++
2 files changed, 143 insertions(+)
create mode 100644 cmd/crunchyroll-go/cmd/update.go
diff --git a/cmd/crunchyroll-go/cmd/update.go b/cmd/crunchyroll-go/cmd/update.go
new file mode 100644
index 0000000..4c7aaad
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/update.go
@@ -0,0 +1,134 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/spf13/cobra"
+ "io"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+)
+
+var (
+ updateInstallFlag bool
+)
+
+var updateCmd = &cobra.Command{
+ Use: "update",
+ Short: "Check if updates are available",
+ Args: cobra.MaximumNArgs(0),
+
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return update()
+ },
+}
+
+func init() {
+ updateCmd.Flags().BoolVarP(&updateInstallFlag,
+ "install",
+ "i",
+ false,
+ "If set and a new version is available, the new version gets installed")
+
+ rootCmd.AddCommand(updateCmd)
+}
+
+func update() error {
+ var release map[string]interface{}
+
+ resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchyroll-go/releases/latest")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ if err = json.NewDecoder(resp.Body).Decode(&release); err != nil {
+ return err
+ }
+ releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
+
+ if Version == "development" {
+ out.Info("Development version, update service not available")
+ return nil
+ }
+
+ latestRelease := strings.SplitN(releaseVersion, ".", 4)
+ if len(latestRelease) != 3 {
+ return fmt.Errorf("latest tag name (%s) is not parsable", releaseVersion)
+ }
+
+ internalVersion := strings.SplitN(Version, ".", 4)
+ if len(internalVersion) != 3 {
+ return fmt.Errorf("internal version (%s) is not parsable", Version)
+ }
+
+ var hasUpdate bool
+ for i := 0; i < 3; i++ {
+ if latestRelease[i] < internalVersion[i] {
+ out.Info("Local version (%s) is newer than version in latest release (%s)", Version, releaseVersion)
+ return nil
+ } else if latestRelease[i] > internalVersion[i] {
+ hasUpdate = true
+ }
+ }
+
+ if !hasUpdate {
+ out.Info("Version is up-to-date")
+ return nil
+ }
+
+ out.Info("A new version is available (%s). Installed version is %s: https://github.com/ByteDream/crunchyroll-go/releases/tag/v%s", releaseVersion, Version, releaseVersion)
+
+ if updateInstallFlag {
+ if runtime.GOARCH != "amd64" {
+ return fmt.Errorf("invalid architecture found (%s), only amd64 is currently supported for automatic updating. "+
+ "You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOARCH)
+ }
+
+ var downloadFile string
+ switch runtime.GOOS {
+ case "linux":
+ yayCommand := exec.Command("pacman -Q crunchyroll-go")
+ if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
+ out.Info("crunchyroll-go was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
+ return nil
+ }
+ downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
+ case "darwin":
+ downloadFile = fmt.Sprintf("crunchy-v%s_darwin", releaseVersion)
+ case "windows":
+ downloadFile = fmt.Sprintf("crunchy-v%s_windows.exe", releaseVersion)
+ default:
+ return fmt.Errorf("invalid operation system found (%s), only linux, windows and darwin / macos are currently supported. "+
+ "You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOOS)
+ }
+
+ out.SetProgress("Updating executable %s", os.Args[0])
+
+ perms, err := os.Stat(os.Args[0])
+ if err != nil {
+ return err
+ }
+ os.Remove(os.Args[0])
+ executeFile, err := os.OpenFile(os.Args[0], os.O_CREATE|os.O_WRONLY, perms.Mode())
+ if err != nil {
+ return err
+ }
+ defer executeFile.Close()
+
+ resp, err := client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchyroll-go/releases/download/v%s/%s", releaseVersion, downloadFile))
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if _, err = io.Copy(executeFile, resp.Body); err != nil {
+ return err
+ }
+
+ out.StopProgress("Updated executable %s", os.Args[0])
+ }
+
+ return nil
+}
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 558aa23..576c291 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -13,6 +13,8 @@ crunchyroll-go login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR]
crunchyroll-go download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.br
crunchyroll-go archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
+.br
+crunchyroll-go update [\fB-i\fR \fIINSTALL\fR]
.SH DESCRIPTION
.TP
@@ -141,6 +143,13 @@ The video resolution. Can either be specified via the pixels (e.g. 1920x1080), t
\fB-g, --goroutines GOROUTINES\fR
Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
+.SH UPDATE COMMAND
+Checks if a newer version is available.
+.TP
+
+\fB-i, --install INSTALL\fR
+If given, the command tries to update the executable with the newer version (if a newer is available).
+
.SH URL OPTIONS
If you want to download only specific episode of a series, you could either pass every single episode url to the downloader (which is fine for 1 - 3 episodes) or use filtering.
It works pretty simple, just put a specific pattern surrounded by square brackets at the end of the url from the anime you want to download. A season and / or episode as well as a range from where to where episodes should be downloaded can be specified.
From f046b68371ae8c2f8dbe671a5166136d8dcb25a3 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 13 May 2022 19:34:28 +0200
Subject: [PATCH 158/732] Update workflows running on every branch
---
.github/workflows/ci.yml | 6 +-----
.github/workflows/codeql-analysis.yml | 3 ---
2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7887e0c..44c7ac6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,10 +1,6 @@
name: CI
-on:
- push:
- branches: [ master ]
- pull_request:
- branches: [ master ]
+on: [ push, pull_request ]
jobs:
test:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index b7d4dfe..e0f5470 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,10 +13,7 @@ name: "CodeQL"
on:
push:
- branches: [ master ]
pull_request:
- # The branches below must be a subset of the branches above
- branches: [ master ]
schedule:
- cron: '40 3 * * 2'
From 192a85afb8ae1c0ffcbf3a4b80c52a4bf3f73043 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 13 May 2022 19:53:16 +0200
Subject: [PATCH 159/732] Use fmt.Errorf instead of errors.New & new invalid
session id error message
---
crunchyroll.go | 9 ++++-----
stream.go | 3 +--
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 6b1854f..00d3123 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"io"
"net/http"
@@ -131,15 +130,15 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
}
- if _, ok := jsonBody["message"]; ok {
- return nil, errors.New("invalid session id")
+ if isError, ok := jsonBody["error"]; ok && isError.(bool) {
+ return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"])
}
data := jsonBody["data"].(map[string]interface{})
crunchy.Config.CountryCode = data["country_code"].(string)
user := data["user"]
if user == nil {
- return nil, errors.New("invalid session id, user is not logged in")
+ return nil, fmt.Errorf("invalid session id, user is not logged in")
}
if user.(map[string]interface{})["premium"] == "" {
crunchy.Config.Premium = false
@@ -346,7 +345,7 @@ func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
} else if len(m) > 0 {
return m[0], nil
}
- return nil, errors.New("no series or movie could be found")
+ return nil, fmt.Errorf("no series or movie could be found")
}
// FindEpisodeByName finds an episode by its crunchyroll series name and episode title.
diff --git a/stream.go b/stream.go
index d8957e6..7505a24 100644
--- a/stream.go
+++ b/stream.go
@@ -2,7 +2,6 @@ package crunchyroll
import (
"encoding/json"
- "errors"
"fmt"
"github.com/grafov/m3u8"
"regexp"
@@ -83,7 +82,7 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
if len(jsonBody) == 0 {
// this may get thrown when the crunchyroll account has just a normal account and not one with premium
- return nil, errors.New("no stream available")
+ return nil, fmt.Errorf("no stream available")
}
audioLocale := jsonBody["audio_locale"].(string)
From 62735cf07cb8740db61fa886ddb02fb82e766952 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 19:28:05 +0200
Subject: [PATCH 160/732] Change request url for some request & regex
---
crunchyroll.go | 17 ++++++++---------
episode.go | 12 ++++--------
movie_listing.go | 18 ++++++------------
season.go | 16 +++++++---------
stream.go | 8 +++-----
video.go | 24 ++++++++----------------
6 files changed, 36 insertions(+), 59 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 6b1854f..60b4320 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -11,6 +11,7 @@ import (
"net/url"
"regexp"
"strconv"
+ "strings"
)
// LOCALE represents a locale / language.
@@ -33,7 +34,7 @@ const (
type Crunchyroll struct {
// Client is the http.Client to perform all requests over.
Client *http.Client
- // Context can be used to stop requests with Client and is context.Background by default.
+ // Context can be used to stop requests with Client.
Context context.Context
// Locale specifies in which language all results should be returned / requested.
Locale LOCALE
@@ -45,9 +46,10 @@ type Crunchyroll struct {
TokenType string
AccessToken string
+ Bucket string
+
CountryCode string
Premium bool
- Channel string
Policy string
Signature string
KeyPairID string
@@ -141,13 +143,7 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
if user == nil {
return nil, errors.New("invalid session id, user is not logged in")
}
- if user.(map[string]interface{})["premium"] == "" {
- crunchy.Config.Premium = false
- crunchy.Config.Channel = "-"
- } else {
- crunchy.Config.Premium = true
- crunchy.Config.Channel = "crunchyroll"
- }
+ crunchy.Config.Premium = user.(map[string]interface{})["premium"] != ""
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -200,6 +196,9 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
}
cms := jsonBody["cms"].(map[string]interface{})
+ // / is trimmed so that urls which require it must be in .../{bucket}/... like format.
+ // this just looks cleaner
+ crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
crunchy.Config.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
diff --git a/episode.go b/episode.go
index a25844c..3c9d38f 100644
--- a/episode.go
+++ b/episode.go
@@ -77,10 +77,8 @@ type Episode struct {
// EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -194,10 +192,8 @@ func (e *Episode) Streams() ([]*Stream, error) {
return e.children, nil
}
- streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- e.crunchy.Config.CountryCode,
- e.crunchy.Config.MaturityRating,
- e.crunchy.Config.Channel,
+ streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ e.crunchy.Config.Bucket,
e.StreamID,
e.crunchy.Locale,
e.crunchy.Config.Signature,
diff --git a/movie_listing.go b/movie_listing.go
index 63d7fab..80b7b46 100644
--- a/movie_listing.go
+++ b/movie_listing.go
@@ -40,10 +40,8 @@ type MovieListing struct {
// MovieListingFromID returns a movie listing by its api id.
func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -69,10 +67,8 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
// AudioLocale is same as Episode.AudioLocale.
func (ml *MovieListing) AudioLocale() (LOCALE, error) {
- resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- ml.crunchy.Config.CountryCode,
- ml.crunchy.Config.MaturityRating,
- ml.crunchy.Config.Channel,
+ resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ ml.crunchy.Config.Bucket,
ml.ID,
ml.crunchy.Locale,
ml.crunchy.Config.Signature,
@@ -90,10 +86,8 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) {
// Streams returns all streams which are available for the movie listing.
func (ml *MovieListing) Streams() ([]*Stream, error) {
- return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- ml.crunchy.Config.CountryCode,
- ml.crunchy.Config.MaturityRating,
- ml.crunchy.Config.Channel,
+ return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ ml.crunchy.Config.Bucket,
ml.ID,
ml.crunchy.Locale,
ml.crunchy.Config.Signature,
diff --git a/season.go b/season.go
index 825a816..b83d214 100644
--- a/season.go
+++ b/season.go
@@ -44,10 +44,8 @@ type Season struct {
// SeasonFromID returns a season by its api id.
func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
- resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -86,10 +84,8 @@ func (s *Season) Episodes() (episodes []*Episode, err error) {
return s.children, nil
}
- resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- s.crunchy.Config.CountryCode,
- s.crunchy.Config.MaturityRating,
- s.crunchy.Config.Channel,
+ resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ s.crunchy.Config.Bucket,
s.ID,
s.crunchy.Locale,
s.crunchy.Config.Signature,
@@ -111,8 +107,10 @@ func (s *Season) Episodes() (episodes []*Episode, err error) {
}
if episode.Playback != "" {
streamHref := item.(map[string]interface{})["__links__"].(map[string]interface{})["streams"].(map[string]interface{})["href"].(string)
- if match := regexp.MustCompile(`(?m)^/cms/v2/\S+videos/(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 {
+ if match := regexp.MustCompile(`(?m)(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 {
episode.StreamID = match[0][1]
+ } else {
+ fmt.Println()
}
}
episodes = append(episodes, episode)
diff --git a/stream.go b/stream.go
index d8957e6..b1193ed 100644
--- a/stream.go
+++ b/stream.go
@@ -25,10 +25,8 @@ type Stream struct {
// StreamsFromID returns a stream by its api id.
func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
- return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -105,7 +103,7 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
var id string
var formatType FormatType
href := jsonBody["__links__"].(map[string]interface{})["resource"].(map[string]interface{})["href"].(string)
- if match := regexp.MustCompile(`(?sm)^/cms/v2/\S+/crunchyroll/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 {
+ if match := regexp.MustCompile(`(?sm)/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 {
formatType = FormatType(match[0][1])
id = match[0][2]
}
diff --git a/video.go b/video.go
index 00b7734..8a7ee76 100644
--- a/video.go
+++ b/video.go
@@ -69,10 +69,8 @@ type Movie struct {
// MovieFromID returns a movie by its api id.
func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -102,10 +100,8 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
return m.children, nil
}
- resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- m.crunchy.Config.CountryCode,
- m.crunchy.Config.MaturityRating,
- m.crunchy.Config.Channel,
+ resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ m.crunchy.Config.Bucket,
m.ID,
m.crunchy.Locale,
m.crunchy.Config.Signature,
@@ -165,10 +161,8 @@ type Series struct {
// SeriesFromID returns a series by its api id.
func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.CountryCode,
- crunchy.Config.MaturityRating,
- crunchy.Config.Channel,
+ resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ crunchy.Config.Bucket,
id,
crunchy.Locale,
crunchy.Config.Signature,
@@ -198,10 +192,8 @@ func (s *Series) Seasons() (seasons []*Season, err error) {
return s.children, nil
}
- resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- s.crunchy.Config.CountryCode,
- s.crunchy.Config.MaturityRating,
- s.crunchy.Config.Channel,
+ resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
+ s.crunchy.Config.Bucket,
s.ID,
s.crunchy.Locale,
s.crunchy.Config.Signature,
From f51bdeaec7dcc05c4927b377ec113e1dbefc41c6 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 20:03:52 +0200
Subject: [PATCH 161/732] Add error return in some login failure cases (#30)
---
crunchyroll.go | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index 60b4320..d3b1ce8 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -97,6 +97,13 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
if loginResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status)
+ } else {
+ var loginRespBody map[string]interface{}
+ json.NewDecoder(loginResp.Body).Decode(&loginRespBody)
+
+ if loginRespBody["error"].(bool) {
+ return nil, fmt.Errorf("an unexpected login error occoured: %s", loginRespBody["message"])
+ }
}
return LoginWithSessionID(sessionID, locale, client)
From afa975c459947f613ea950ae2927d596918530ca Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 21:21:35 +0200
Subject: [PATCH 162/732] Add session id always cached in temp directory (to
prevent #30)
---
cmd/crunchyroll-go/cmd/login.go | 61 +++++++++++++++++--------
cmd/crunchyroll-go/cmd/utils.go | 81 ++++++++++++++++-----------------
2 files changed, 79 insertions(+), 63 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 5bef2a9..c9fc923 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -5,9 +5,7 @@ import (
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/spf13/cobra"
"os"
- "os/user"
"path/filepath"
- "runtime"
)
var (
@@ -21,11 +19,11 @@ var loginCmd = &cobra.Command{
Short: "Login to crunchyroll",
Args: cobra.RangeArgs(1, 2),
- Run: func(cmd *cobra.Command, args []string) {
+ RunE: func(cmd *cobra.Command, args []string) error {
if loginSessionIDFlag {
- loginSessionID(args[0])
+ return loginSessionID(args[0])
} else {
- loginCredentials(args[0], args[1])
+ return loginCredentials(args[0], args[1])
}
},
}
@@ -46,12 +44,31 @@ func init() {
func loginCredentials(user, password string) error {
out.Debug("Logging in via credentials")
- if _, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client); err != nil {
- out.Err(err.Error())
- os.Exit(1)
+ c, err := crunchyroll.LoginWithCredentials(user, password, systemLocale(false), client)
+ if err != nil {
+ return err
}
- return os.WriteFile(loginStorePath(), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600)
+ if loginPersistentFlag {
+ if configDir, err := os.UserConfigDir(); err != nil {
+ return fmt.Errorf("could not save credentials persistent: %w", err)
+ } else {
+ os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
+ if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600); err != nil {
+ return err
+ }
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
+ }
+ }
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 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 {
@@ -61,21 +78,25 @@ func loginSessionID(sessionID string) error {
os.Exit(1)
}
- return os.WriteFile(loginStorePath(), []byte(sessionID), 0600)
-}
-
-func loginStorePath() string {
- path := filepath.Join(os.TempDir(), ".crunchy")
+ var err error
if loginPersistentFlag {
- if runtime.GOOS != "windows" {
- usr, _ := user.Current()
- path = filepath.Join(usr.HomeDir, ".config/crunchy")
+ if configDir, err := os.UserConfigDir(); err != nil {
+ return fmt.Errorf("could not save credentials persistent: %w", err)
+ } else {
+ os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
+ if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(sessionID), 0600); err != nil {
+ return err
+ }
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
+ }
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(sessionID), 0600); err != nil {
+ return err
+ }
- out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", path)
- } else if runtime.GOOS != "windows" {
+ if !loginPersistentFlag {
out.Info("Due to security reasons, you have to login again on the next reboot")
}
- return path
+ return nil
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 29a48f7..cafd99d 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -8,7 +8,6 @@ import (
"net/url"
"os"
"os/exec"
- "os/user"
"path/filepath"
"reflect"
"regexp"
@@ -141,57 +140,53 @@ func freeFileName(filename string) (string, bool) {
func loadCrunchy() {
out.SetProgress("Logging in")
- files := []string{filepath.Join(os.TempDir(), ".crunchy")}
-
- if runtime.GOOS != "windows" {
- usr, _ := user.Current()
- files = append(files, filepath.Join(usr.HomeDir, ".config/crunchy"))
- }
-
- var err error
- for _, file := range files {
- if _, err = os.Stat(file); os.IsNotExist(err) {
- err = nil
- continue
- }
- var body []byte
- if body, err = os.ReadFile(file); err != nil {
- out.StopProgress("Failed to read login information: %v", err)
- os.Exit(1)
- } else if body == nil {
- continue
- }
-
- split := strings.SplitN(string(body), "\n", 2)
- if len(split) == 1 || split[1] == "" {
- if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err == nil {
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+ if configDir, err := os.UserConfigDir(); err == nil {
+ persistentFilePath := filepath.Join(configDir, "crunchyroll-go", "crunchy")
+ if _, statErr := os.Stat(persistentFilePath); statErr == nil {
+ body, err := os.ReadFile(persistentFilePath)
+ if err != nil {
+ out.StopProgress("Failed to read login information: %v", err)
+ os.Exit(1)
}
- } else {
- if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
- continue
- }
- out.Debug("Logged in with username '%s' and password '%s'. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0], split[1])
- if file != filepath.Join(os.TempDir(), ".crunchy") {
- // the session id is written to a temp file to reduce the amount of re-logging in.
- // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
- if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600); err != nil {
- out.StopProgress("Failed to write session id to temp file")
+ split := strings.SplitN(string(body), "\n", 2)
+ if len(split) == 1 || split[1] == "" {
+ split[0] = url.QueryEscape(split[0])
+ if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
+ out.StopProgress(err.Error())
os.Exit(1)
}
- out.Debug("Wrote session id to temp file")
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+ } else {
+ if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
+ out.StopProgress(err.Error())
+ os.Exit(1)
+ }
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.SessionID)
+ // the session id is written to a temp file to reduce the amount of re-logging in.
+ // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
+ os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600)
}
+ return
+ }
+ }
+
+ 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.LoginWithSessionID(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
+ out.StopProgress(err.Error())
+ os.Exit(1)
}
- out.StopProgress("Logged in")
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
return
}
- if err != nil {
- out.StopProgress(err.Error())
- } else {
- out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
- }
+ out.StopProgress("To use this command, login first. Type `%s login -h` to get help", os.Args[0])
os.Exit(1)
}
From 5b3466d06d555639aa27f3c13f54ac50b6e1740d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 22:06:44 +0200
Subject: [PATCH 163/732] Add stream not available with non-premium error
notice
---
stream.go | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/stream.go b/stream.go
index b1193ed..0fa54b9 100644
--- a/stream.go
+++ b/stream.go
@@ -80,8 +80,12 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
json.NewDecoder(resp.Body).Decode(&jsonBody)
if len(jsonBody) == 0 {
- // this may get thrown when the crunchyroll account has just a normal account and not one with premium
- return nil, errors.New("no stream available")
+ // this may get thrown when the crunchyroll account is just a normal account and not one with premium
+ if !crunchy.Config.Premium {
+ return nil, fmt.Errorf("no stream available, this might be the result of using a non-premium account")
+ } else {
+ return nil, errors.New("no stream available")
+ }
}
audioLocale := jsonBody["audio_locale"].(string)
From 6c476df24effcad9d6297c453535ce7f9bcf72db Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 22:07:44 +0200
Subject: [PATCH 164/732] Set beta url notice only if account is premium
---
cmd/crunchyroll-go/cmd/archive.go | 6 ++++--
cmd/crunchyroll-go/cmd/download.go | 6 ++++--
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 6f3f6a0..63e5d5a 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -182,8 +182,10 @@ func archive(urls []string) error {
episodes, err := archiveExtractEpisodes(url)
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
- out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ if 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, " +
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ }
return err
}
out.StopProgress("Parsed url %d", i+1)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 1ce202c..1254048 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -128,8 +128,10 @@ func download(urls []string) error {
episodes, err := downloadExtractEpisodes(url)
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
- out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ if 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, " +
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ }
return err
}
out.StopProgress("Parsed url %d", i+1)
From 0fa829828f987ce9b825f04f300c73b142438373 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 22:42:31 +0200
Subject: [PATCH 165/732] Fix 1080p, 720p, ... not working
---
cmd/crunchyroll-go/cmd/archive.go | 10 +++++++---
cmd/crunchyroll-go/cmd/download.go | 8 ++++++--
2 files changed, 13 insertions(+), 5 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 6f3f6a0..6dcf61b 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -13,6 +13,7 @@ import (
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
+ "math"
"os"
"os/exec"
"os/signal"
@@ -98,9 +99,12 @@ var archiveCmd = &cobra.Command{
}
switch archiveResolutionFlag {
- case "1080p", "720p", "480p", "360p", "240p":
- intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
- archiveResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(archiveResolutionFlag, "p"))
+ case "1080p", "720p", "480p", "360p":
+ intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
+ archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
+ case "240p":
+ // 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
+ archiveResolutionFlag = "428x240"
case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
default:
return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 1ce202c..92b6027 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -7,6 +7,7 @@ import (
"github.com/ByteDream/crunchyroll-go/v2/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
+ "math"
"os"
"os/signal"
"path/filepath"
@@ -53,9 +54,12 @@ var downloadCmd = &cobra.Command{
out.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
switch downloadResolutionFlag {
- case "1080p", "720p", "480p", "360p", "240p":
+ case "1080p", "720p", "480p", "360p":
intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
- downloadResolutionFlag = fmt.Sprintf("%dx%s", int(intRes*(16/9)), strings.TrimSuffix(downloadResolutionFlag, "p"))
+ downloadResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
+ case "240p":
+ // 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
+ downloadResolutionFlag = "428x240"
case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
default:
return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)
From b70bf9902b1b51ff5aa943f2059cdda8c673cdeb Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 11:01:49 +0200
Subject: [PATCH 166/732] Add error return in some login failure cases (#30)
---
crunchyroll.go | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index 00d3123..1dafee7 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -94,6 +94,13 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
if loginResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status)
+ } else {
+ var loginRespBody map[string]interface{}
+ json.NewDecoder(loginResp.Body).Decode(&loginRespBody)
+
+ if loginRespBody["error"].(bool) {
+ return nil, fmt.Errorf("an unexpected login error occoured: %s", loginRespBody["message"])
+ }
}
return LoginWithSessionID(sessionID, locale, client)
From ea3506b7f6f7f63676df453b8927a97fcc7bf7b4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 11:11:18 +0200
Subject: [PATCH 167/732] Fix logging in message shown for too long
---
cmd/crunchyroll-go/cmd/utils.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index cafd99d..c799165 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -166,6 +166,7 @@ func loadCrunchy() {
// it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600)
}
+ out.StopProgress("Logged in")
return
}
}
@@ -183,6 +184,8 @@ func loadCrunchy() {
}
out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
+
+ out.StopProgress("Logged in")
return
}
From b92eddc5d242435b453d4fea1a2ecba32838725b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 11:14:35 +0200
Subject: [PATCH 168/732] Add temporary session id lookup on login first
---
cmd/crunchyroll-go/cmd/utils.go | 35 ++++++++++++++++-----------------
1 file changed, 17 insertions(+), 18 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index c799165..2a632ff 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -140,6 +140,23 @@ func freeFileName(filename string) (string, bool) {
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.LoginWithSessionID(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
+ out.Debug("Failed to login with temp session id: %w", err)
+ } else {
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
+
+ out.StopProgress("Logged in")
+ return
+ }
+ }
+
if configDir, err := os.UserConfigDir(); err == nil {
persistentFilePath := filepath.Join(configDir, "crunchyroll-go", "crunchy")
if _, statErr := os.Stat(persistentFilePath); statErr == nil {
@@ -171,24 +188,6 @@ func loadCrunchy() {
}
}
- 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.LoginWithSessionID(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
- os.Exit(1)
- }
-
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
-
- 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)
}
From 00ea7635eb23bca6757d105507ba308befab7aa7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 11:18:27 +0200
Subject: [PATCH 169/732] Version 2.2.2
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index d23fd11..3dd5c3e 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.2.1
+VERSION=2.2.2
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From 5d732123d97efac57ebf62fd7be0f98eac50f8c0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 11:35:21 +0200
Subject: [PATCH 170/732] Change cli name from crunchyroll to crunchyroll-go
---
cmd/crunchyroll-go/cmd/root.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 14f0c0e..82ee133 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -27,7 +27,7 @@ var (
)
var rootCmd = &cobra.Command{
- Use: "crunchyroll",
+ Use: "crunchyroll-go",
Version: Version,
Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",
From 0ffae4ddda15c72020343837367c6ea9be829dd9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 22:24:21 +0200
Subject: [PATCH 171/732] Add available function to check if episode streams
are available
---
episode.go | 7 +++++++
url.go | 5 +++++
utils/sort.go | 3 +++
3 files changed, 15 insertions(+)
diff --git a/episode.go b/episode.go
index 3c9d38f..7d477ce 100644
--- a/episode.go
+++ b/episode.go
@@ -112,6 +112,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// Every episode in a season (should) have the same audio locale,
// so if you want to get the audio locale of a season, just call
// this method on the first episode of the season.
+// Will fail if no streams are available, thus use Available to
+// prevent any misleading errors.
func (e *Episode) AudioLocale() (LOCALE, error) {
streams, err := e.Streams()
if err != nil {
@@ -120,6 +122,11 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil
}
+// Available returns if downloadable streams for this episodes are available.
+func (e *Episode) Available() bool {
+ return e.crunchy.Config.Premium || !e.IsPremiumOnly
+}
+
// GetFormat returns the format which matches the given resolution and subtitle locale.
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
streams, err := e.Streams()
diff --git a/url.go b/url.go
index 32603fc..c84a3b9 100644
--- a/url.go
+++ b/url.go
@@ -49,6 +49,11 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
for _, episode := range episodes {
+ // if no episode streams are available, calling episode.AudioLocale
+ // will result in an unwanted error
+ if !episode.Available() {
+ continue
+ }
locale, err := episode.AudioLocale()
if err != nil {
return nil, err
diff --git a/utils/sort.go b/utils/sort.go
index a44717d..e06946c 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -52,6 +52,9 @@ func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCAL
var wg sync.WaitGroup
var lock sync.Mutex
for _, episode := range episodes {
+ if !episode.Available() {
+ continue
+ }
episode := episode
wg.Add(1)
go func() {
From 43be2eee146550f9934231bbd1525cf735e41ab1 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 16 May 2022 22:25:33 +0200
Subject: [PATCH 172/732] Fix typo & add audio locale todo for non-premium
accounts
---
season.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/season.go b/season.go
index b83d214..f5ec08e 100644
--- a/season.go
+++ b/season.go
@@ -37,7 +37,7 @@ type Season struct {
AvailabilityNotes string `json:"availability_notes"`
- // the locales are always empty, idk why this may change in the future
+ // the locales are always empty, idk why, this may change in the future
AudioLocales []LOCALE
SubtitleLocales []LOCALE
}
@@ -71,6 +71,7 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
// AudioLocale returns the audio locale of the season.
func (s *Season) AudioLocale() (LOCALE, error) {
+ // TODO: Add a function like Episode.Available to prevent this from returning an unwanted error when the account is non-premium
episodes, err := s.Episodes()
if err != nil {
return "", err
From f635bf1a2e1783e88fec015a25627d58c5698108 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 21:47:17 +0200
Subject: [PATCH 173/732] Add available function to check if season streams are
available
---
episode.go | 4 ++--
season.go | 12 +++++++++++-
url.go | 1 +
3 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/episode.go b/episode.go
index 7d477ce..3a2e403 100644
--- a/episode.go
+++ b/episode.go
@@ -112,8 +112,8 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
// Every episode in a season (should) have the same audio locale,
// so if you want to get the audio locale of a season, just call
// this method on the first episode of the season.
-// Will fail if no streams are available, thus use Available to
-// prevent any misleading errors.
+// Will fail if no streams are available, thus use Episode.Available
+// to prevent any misleading errors.
func (e *Episode) AudioLocale() (LOCALE, error) {
streams, err := e.Streams()
if err != nil {
diff --git a/season.go b/season.go
index f5ec08e..0bb8623 100644
--- a/season.go
+++ b/season.go
@@ -70,8 +70,9 @@ func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
}
// AudioLocale returns the audio locale of the season.
+// Will fail if no streams are available, thus use Season.Available
+// to prevent any misleading errors.
func (s *Season) AudioLocale() (LOCALE, error) {
- // TODO: Add a function like Episode.Available to prevent this from returning an unwanted error when the account is non-premium
episodes, err := s.Episodes()
if err != nil {
return "", err
@@ -79,6 +80,15 @@ func (s *Season) AudioLocale() (LOCALE, error) {
return episodes[0].AudioLocale()
}
+// Available returns if downloadable streams for this season are available.
+func (s *Season) Available() (bool, error) {
+ episodes, err := s.Episodes()
+ if err != nil {
+ return false, err
+ }
+ return episodes[0].Available(), nil
+}
+
// Episodes returns all episodes which are available for the season.
func (s *Season) Episodes() (episodes []*Episode, err error) {
if s.children != nil {
diff --git a/url.go b/url.go
index c84a3b9..0f5bb25 100644
--- a/url.go
+++ b/url.go
@@ -21,6 +21,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
for _, season := range seasons {
if audio != nil {
+
locale, err := season.AudioLocale()
if err != nil {
return nil, err
From 7be803d485ca88994b1169b24e6c5adf88236c36 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 21:54:31 +0200
Subject: [PATCH 174/732] Add extended error message if account is non-premium
---
url.go | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/url.go b/url.go
index 0f5bb25..86874eb 100644
--- a/url.go
+++ b/url.go
@@ -13,6 +13,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
var eps []*Episode
+ var notAvailableContinue bool
if series != nil {
seasons, err := series.Seasons()
@@ -21,6 +22,12 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
for _, season := range seasons {
if audio != nil {
+ if available, err := season.Available(); err != nil {
+ return nil, err
+ } else if !available {
+ notAvailableContinue = true
+ continue
+ }
locale, err := season.AudioLocale()
if err != nil {
@@ -53,6 +60,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
// if no episode streams are available, calling episode.AudioLocale
// will result in an unwanted error
if !episode.Available() {
+ notAvailableContinue = true
continue
}
locale, err := episode.AudioLocale()
@@ -77,7 +85,11 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
}
if len(eps) == 0 {
- return nil, fmt.Errorf("could not find any matching episode")
+ if notAvailableContinue {
+ return nil, fmt.Errorf("could not find any matching episode which is accessable with an non-premium account")
+ } else {
+ return nil, fmt.Errorf("could not find any matching episode")
+ }
}
return eps, nil
From d4e095a576f1617082ad9952b626c916800e5382 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 21:56:29 +0200
Subject: [PATCH 175/732] Fix typo
---
url.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/url.go b/url.go
index 86874eb..95f3951 100644
--- a/url.go
+++ b/url.go
@@ -86,7 +86,7 @@ func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Ep
if len(eps) == 0 {
if notAvailableContinue {
- return nil, fmt.Errorf("could not find any matching episode which is accessable with an non-premium account")
+ return nil, fmt.Errorf("could not find any matching episode which is accessable with a non-premium account")
} else {
return nil, fmt.Errorf("could not find any matching episode")
}
From 608e03bc11925c9bb78ad6a0b4efbf4137a34c6e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 18 May 2022 22:21:49 +0200
Subject: [PATCH 176/732] Add better update output
---
cmd/crunchyroll-go/cmd/update.go | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/update.go b/cmd/crunchyroll-go/cmd/update.go
index 4c7aaad..c8512e6 100644
--- a/cmd/crunchyroll-go/cmd/update.go
+++ b/cmd/crunchyroll-go/cmd/update.go
@@ -63,10 +63,12 @@ func update() error {
return fmt.Errorf("internal version (%s) is not parsable", Version)
}
+ out.Info("Installed version is %s", Version)
+
var hasUpdate bool
for i := 0; i < 3; i++ {
if latestRelease[i] < internalVersion[i] {
- out.Info("Local version (%s) is newer than version in latest release (%s)", Version, releaseVersion)
+ out.Info("Local version is newer than version in latest release (%s)", releaseVersion)
return nil
} else if latestRelease[i] > internalVersion[i] {
hasUpdate = true
@@ -78,7 +80,7 @@ func update() error {
return nil
}
- out.Info("A new version is available (%s). Installed version is %s: https://github.com/ByteDream/crunchyroll-go/releases/tag/v%s", releaseVersion, Version, releaseVersion)
+ out.Info("A new version is available (%s): https://github.com/ByteDream/crunchyroll-go/releases/tag/v%s", releaseVersion, releaseVersion)
if updateInstallFlag {
if runtime.GOARCH != "amd64" {
From a4ec163275ab6fb6961cc6e208d959ee02399a80 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 20 May 2022 22:57:07 +0200
Subject: [PATCH 177/732] Add basic encrypted login credentials support
---
cmd/crunchyroll-go/cmd/login.go | 75 ++++++++++++++++++++++++++++---
cmd/crunchyroll-go/cmd/unix.go | 48 ++++++++++++++++++++
cmd/crunchyroll-go/cmd/utils.go | 52 ++++++++++++++++++---
cmd/crunchyroll-go/cmd/windows.go | 41 +++++++++++++++++
4 files changed, 205 insertions(+), 11 deletions(-)
create mode 100644 cmd/crunchyroll-go/cmd/unix.go
create mode 100644 cmd/crunchyroll-go/cmd/windows.go
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index c9fc923..565dac0 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -1,15 +1,22 @@
package cmd
import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/spf13/cobra"
+ "io"
"os"
"path/filepath"
)
var (
loginPersistentFlag bool
+ loginEncryptFlag bool
loginSessionIDFlag bool
)
@@ -33,6 +40,10 @@ func init() {
"persistent",
false,
"If the given credential should be stored persistent")
+ loginCmd.Flags().BoolVar(&loginEncryptFlag,
+ "encrypt",
+ false,
+ "Encrypt the given credentials (won't do anything if --session-id is given)")
loginCmd.Flags().BoolVar(&loginSessionIDFlag,
"session-id",
@@ -49,20 +60,74 @@ func loginCredentials(user, password string) error {
return err
}
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); 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, "crunchyroll-go"), 0755)
- if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(fmt.Sprintf("%s\n%s", user, password)), 0600); err != nil {
+ if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), credentials, 0600); err != nil {
return err
}
- out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
+ if !loginEncryptFlag {
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
+ }
}
}
- if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil {
- return err
- }
if !loginPersistentFlag {
out.Info("Due to security reasons, you have to login again on the next reboot")
diff --git a/cmd/crunchyroll-go/cmd/unix.go b/cmd/crunchyroll-go/cmd/unix.go
new file mode 100644
index 0000000..962088f
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/unix.go
@@ -0,0 +1,48 @@
+//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
+
+package cmd
+
+import (
+ "bufio"
+ "os"
+ "os/exec"
+ "syscall"
+)
+
+// https://github.com/bgentry/speakeasy/blob/master/speakeasy_unix.go
+var stty string
+
+func init() {
+ var err error
+ if stty, err = exec.LookPath("stty"); err != nil {
+ panic(err)
+ }
+}
+
+func readLineSilent() ([]byte, error) {
+ pid, err := setEcho(false)
+ if err != nil {
+ return nil, err
+ }
+ defer setEcho(true)
+
+ syscall.Wait4(pid, nil, 0, nil)
+
+ l, _, err := bufio.NewReader(os.Stdin).ReadLine()
+ return l, err
+}
+
+func setEcho(on bool) (pid int, err error) {
+ fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}
+
+ if on {
+ pid, err = syscall.ForkExec(stty, []string{"stty", "echo"}, &syscall.ProcAttr{Files: fds})
+ } else {
+ pid, err = syscall.ForkExec(stty, []string{"stty", "-echo"}, &syscall.ProcAttr{Files: fds})
+ }
+
+ if err != nil {
+ return 0, err
+ }
+ return
+}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 2a632ff..d7eba3d 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -1,6 +1,9 @@
package cmd
import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha256"
"fmt"
"github.com/ByteDream/crunchyroll-go/v2"
"github.com/ByteDream/crunchyroll-go/v2/utils"
@@ -167,13 +170,49 @@ func loadCrunchy() {
}
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
- split[0] = url.QueryEscape(split[0])
- if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
- os.Exit(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)
+ } else {
+ split[0] = url.QueryEscape(split[0])
+ if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
+ out.StopProgress(err.Error())
+ os.Exit(1)
+ }
+ out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
}
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
- } else {
+ }
+
+ 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)
@@ -183,6 +222,7 @@ func loadCrunchy() {
// it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600)
}
+
out.StopProgress("Logged in")
return
}
diff --git a/cmd/crunchyroll-go/cmd/windows.go b/cmd/crunchyroll-go/cmd/windows.go
new file mode 100644
index 0000000..d6eecb1
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/windows.go
@@ -0,0 +1,41 @@
+//go:build windows
+
+package cmd
+
+import (
+ "bufio"
+ "os"
+ "syscall"
+)
+
+// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
+func readLineSilent() ([]byte, error) {
+ var oldMode uint32
+
+ if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
+ return nil, err
+ }
+
+ newMode := oldMode &^ 0x0004
+
+ err := setConsoleMode(syscall.Stdin, newMode)
+ defer setConsoleMode(syscall.Stdin, oldMode)
+
+ if err != nil {
+ return nil, err
+ }
+
+ l, _, err := bufio.NewReader(os.Stdin).ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ return l, err
+}
+
+func setConsoleMode(console syscall.Handle, mode uint32) error {
+ dll := syscall.MustLoadDLL("kernel32")
+ proc := dll.MustFindProc("SetConsoleMode")
+ _, _, err := proc.Call(uintptr(console), uintptr(mode))
+
+ return err
+}
From 4d65d2f2dff842b1708f3907bd6c5a7c0076d8fc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 20 May 2022 23:05:38 +0200
Subject: [PATCH 178/732] Bump CI go version to 1.18
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44c7ac6..7c54dd3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
- go-version: 1.16
+ go-version: 1.18
- name: Build
run: go build -v cmd/crunchyroll-go/main.go
From b78d6a7871f4f177fdeb6729a2a011a3f48b7747 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 21 May 2022 00:29:03 +0200
Subject: [PATCH 179/732] Change default Makefile version variable to
development
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 3dd5c3e..9b96538 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-VERSION=2.2.2
+VERSION=development
BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
From eb2414d0120bd1067b9324d4b4c31d4dcea9a568 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 21 May 2022 21:38:45 +0200
Subject: [PATCH 180/732] Update module version to v3
---
Makefile | 8 ++++----
README.md | 4 ++--
cmd/crunchyroll-go/cmd/archive.go | 4 ++--
cmd/crunchyroll-go/cmd/download.go | 4 ++--
cmd/crunchyroll-go/cmd/login.go | 2 +-
cmd/crunchyroll-go/cmd/root.go | 2 +-
cmd/crunchyroll-go/cmd/utils.go | 4 ++--
cmd/crunchyroll-go/main.go | 2 +-
go.mod | 2 +-
utils/locale.go | 2 +-
utils/sort.go | 2 +-
11 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/Makefile b/Makefile
index 9b96538..a747c1d 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ DESTDIR=
PREFIX=/usr
build:
- go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
+ go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -24,8 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
release:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
strip $(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index c83493c..f875018 100644
--- a/README.md
+++ b/README.md
@@ -205,10 +205,10 @@ These flags you can use across every sub-command:
Download the library via `go get`
```shell
-$ go get github.com/ByteDream/crunchyroll-go/v2
+$ go get github.com/ByteDream/crunchyroll-go/v3
```
-The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v2).
+The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v3).
Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library).
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 6dcf61b..d4a7e48 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -8,8 +8,8 @@ import (
"compress/gzip"
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/ByteDream/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 92b6027..3dddf48 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -3,8 +3,8 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/ByteDream/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"math"
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index c9fc923..4bee3b0 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -2,7 +2,7 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v3"
"github.com/spf13/cobra"
"os"
"path/filepath"
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 82ee133..02873f1 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v3"
"github.com/spf13/cobra"
"net/http"
"os"
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 2a632ff..2fddd60 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -2,8 +2,8 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/ByteDream/crunchyroll-go/v3/utils"
"net/http"
"net/url"
"os"
diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go
index a502afb..0ced8aa 100644
--- a/cmd/crunchyroll-go/main.go
+++ b/cmd/crunchyroll-go/main.go
@@ -1,7 +1,7 @@
package main
import (
- "github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd"
+ "github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd"
)
func main() {
diff --git a/go.mod b/go.mod
index 9a38468..5c64ddf 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/ByteDream/crunchyroll-go/v2
+module github.com/ByteDream/crunchyroll-go/v3
go 1.18
diff --git a/utils/locale.go b/utils/locale.go
index 537b165..85a8650 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v3"
)
// AllLocales is an array of all available locales.
diff --git a/utils/sort.go b/utils/sort.go
index a44717d..eacb96b 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/ByteDream/crunchyroll-go/v3"
"sort"
"strconv"
"strings"
From 2d28991a70e80d2c92f2b4d091bd0aa88ca00a6f Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 21 May 2022 21:41:25 +0200
Subject: [PATCH 181/732] Remove v2 release notice
---
README.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/README.md b/README.md
index c83493c..b8d48fa 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,3 @@
-Version 2 is out 🥳, see all the changes .
-
# crunchyroll-go
A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api. To use it, you need a crunchyroll premium account to for full (api) access.
From 382d19ee94e15e4415aa26da98ccb2fc1d84d863 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 11:17:08 +0200
Subject: [PATCH 182/732] Fix login not working with session id
---
crunchyroll.go | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 1dafee7..dcfc375 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -10,6 +10,7 @@ import (
"net/url"
"regexp"
"strconv"
+ "strings"
)
// LOCALE represents a locale / language.
@@ -143,17 +144,6 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
data := jsonBody["data"].(map[string]interface{})
crunchy.Config.CountryCode = data["country_code"].(string)
- user := data["user"]
- if user == nil {
- return nil, fmt.Errorf("invalid session id, user is not logged in")
- }
- if user.(map[string]interface{})["premium"] == "" {
- crunchy.Config.Premium = false
- crunchy.Config.Channel = "-"
- } else {
- crunchy.Config.Premium = true
- crunchy.Config.Channel = "crunchyroll"
- }
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -206,6 +196,13 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
}
cms := jsonBody["cms"].(map[string]interface{})
+ if strings.Contains(cms["bucket"].(string), "crunchyroll") {
+ crunchy.Config.Premium = true
+ crunchy.Config.Channel = "crunchyroll"
+ } else {
+ crunchy.Config.Premium = false
+ crunchy.Config.Channel = "-"
+ }
crunchy.Config.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
From 638689ee327f4b5df4571baa0afd9763b222a9ee Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 15:13:15 +0200
Subject: [PATCH 183/732] Add new login method & deprecated login with session
id
---
crunchyroll.go | 298 +++++++++++++++++++++++++------------------------
1 file changed, 151 insertions(+), 147 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 1dafee7..0aa4a2e 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -10,6 +10,7 @@ import (
"net/url"
"regexp"
"strconv"
+ "strings"
)
// LOCALE represents a locale / language.
@@ -36,14 +37,17 @@ type Crunchyroll struct {
Context context.Context
// Locale specifies in which language all results should be returned / requested.
Locale LOCALE
- // SessionID is the crunchyroll session id which was used for authentication.
- SessionID string
+ // EtpRt is the crunchyroll beta equivalent to a session id (prior SessionID field in
+ // this struct in v2 and below).
+ EtpRt string
// Config stores parameters which are needed by some api calls.
Config struct {
TokenType string
AccessToken string
+ Bucket string
+
CountryCode string
Premium bool
Channel string
@@ -59,101 +63,38 @@ type Crunchyroll struct {
cache bool
}
-// LoginWithCredentials logs in via crunchyroll username or email and password.
-func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s",
- "LNDJgOit5yaRIWN", "com.crunchyroll.windows.desktop", "Az2srGnChW65fuxYz2Xxl1GcZQgtGgI")
- sessResp, err := client.Get(sessionIDEndpoint)
- if err != nil {
- return nil, err
- }
- defer sessResp.Body.Close()
-
- if sessResp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("failed to start session for credentials login: %s", sessResp.Status)
- }
-
- var data map[string]interface{}
- body, _ := io.ReadAll(sessResp.Body)
- if err = json.Unmarshal(body, &data); err != nil {
- return nil, fmt.Errorf("failed to parse start session with credentials response: %w", err)
- }
-
- sessionID := data["data"].(map[string]interface{})["session_id"].(string)
-
- loginEndpoint := "https://api.crunchyroll.com/login.0.json"
- authValues := url.Values{}
- authValues.Set("session_id", sessionID)
- authValues.Set("account", user)
- authValues.Set("password", password)
- loginResp, err := client.Post(loginEndpoint, "application/x-www-form-urlencoded", bytes.NewBufferString(authValues.Encode()))
- if err != nil {
- return nil, err
- }
- defer loginResp.Body.Close()
-
- if loginResp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status)
- } else {
- var loginRespBody map[string]interface{}
- json.NewDecoder(loginResp.Body).Decode(&loginRespBody)
-
- if loginRespBody["error"].(bool) {
- return nil, fmt.Errorf("an unexpected login error occoured: %s", loginRespBody["message"])
- }
- }
-
- return LoginWithSessionID(sessionID, locale, client)
+type loginResponse struct {
+ AccessToken string `json:"access_token"`
+ ExpiresIn int `json:"expires_in"`
+ TokenType string `json:"token_type"`
+ Scope string `json:"scope"`
+ Country string `json:"country"`
+ AccountID string `json:"account_id"`
}
-// LoginWithSessionID logs in via a crunchyroll session id.
-// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com.
-func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- crunchy := &Crunchyroll{
- Client: client,
- Context: context.Background(),
- Locale: locale,
- SessionID: sessionID,
- cache: true,
- }
- var endpoint string
- var err error
- var resp *http.Response
- var jsonBody map[string]interface{}
+// LoginWithCredentials logs in via crunchyroll username or email and password.
+func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
+ endpoint := "https://beta-api.crunchyroll.com/auth/v1/token"
+ values := url.Values{}
+ values.Set("username", user)
+ values.Set("password", password)
+ values.Set("grant_type", "password")
- // start session
- endpoint = fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?session_id=%s",
- sessionID)
- resp, err = client.Get(endpoint)
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(values.Encode()))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Authorization", "Basic aHJobzlxM2F3dnNrMjJ1LXRzNWE6cHROOURteXRBU2Z6QjZvbXVsSzh6cUxzYTczVE1TY1k=")
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ resp, err := request(req, client)
if err != nil {
return nil, err
}
defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("failed to start session: %s", resp.Status)
- }
-
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
- }
- if isError, ok := jsonBody["error"]; ok && isError.(bool) {
- return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"])
- }
- data := jsonBody["data"].(map[string]interface{})
-
- crunchy.Config.CountryCode = data["country_code"].(string)
- user := data["user"]
- if user == nil {
- return nil, fmt.Errorf("invalid session id, user is not logged in")
- }
- if user.(map[string]interface{})["premium"] == "" {
- crunchy.Config.Premium = false
- crunchy.Config.Channel = "-"
- } else {
- crunchy.Config.Premium = true
- crunchy.Config.Channel = "crunchyroll"
- }
+ var loginResp loginResponse
+ json.NewDecoder(resp.Body).Decode(&loginResp)
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -163,83 +104,164 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
}
}
- // token
- endpoint = "https://beta-api.crunchyroll.com/auth/v1/token"
+ return postLogin(loginResp, etpRt, locale, client)
+}
+
+// LoginWithSessionID logs in via a crunchyroll session id.
+// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com.
+//
+// Deprecated: Login via session id caused some trouble in the past (e.g. #15 or #30) which resulted in
+// login not working. Use LoginWithEtpRt instead. EtpRt practically the crunchyroll beta equivalent to
+// a session id.
+// The method will stay in the library until session id login is removed completely or login with it
+// does not work for a longer period of time.
+func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
+ endpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?session_id=%s",
+ sessionID)
+ resp, err := client.Get(endpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]any
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ var etpRt string
+ for _, cookie := range resp.Cookies() {
+ if cookie.Name == "etp_rt" {
+ etpRt = cookie.Value
+ break
+ }
+ }
+
+ return LoginWithEtpRt(etpRt, locale, client)
+}
+
+// LoginWithEtpRt logs in via the crunchyroll etp rt cookie. This cookie is the crunchyroll beta
+// equivalent to the classic session id.
+// The etp_rt cookie is automatically set when visiting https://beta.crunchyroll.com. Note that you
+// need a crunchyroll account to access it.
+func LoginWithEtpRt(etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
+ endpoint := "https://beta-api.crunchyroll.com/auth/v1/token"
grantType := url.Values{}
grantType.Set("grant_type", "etp_rt_cookie")
- authRequest, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(grantType.Encode()))
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(grantType.Encode()))
if err != nil {
return nil, err
}
- authRequest.Header.Add("Authorization", "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6")
- authRequest.Header.Add("Content-Type", "application/x-www-form-urlencoded")
- authRequest.AddCookie(&http.Cookie{
- Name: "session_id",
- Value: sessionID,
- })
- authRequest.AddCookie(&http.Cookie{
+ req.Header.Add("Authorization", "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6")
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.AddCookie(&http.Cookie{
Name: "etp_rt",
Value: etpRt,
})
+ resp, err := request(req, client)
+ if err != nil {
+ return nil, err
+ }
- resp, err = client.Do(authRequest)
+ var loginResp loginResponse
+ json.NewDecoder(resp.Body).Decode(&loginResp)
+
+ return postLogin(loginResp, etpRt, locale, client)
+}
+
+func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
+ crunchy := &Crunchyroll{
+ Client: client,
+ Context: context.Background(),
+ Locale: locale,
+ EtpRt: etpRt,
+ cache: true,
+ }
+
+ crunchy.Config.TokenType = loginResp.TokenType
+ crunchy.Config.AccessToken = loginResp.AccessToken
+ crunchy.Config.AccountID = loginResp.AccountID
+
+ var jsonBody map[string]any
+
+ endpoint := "https://beta-api.crunchyroll.com/index/v2"
+ resp, err := crunchy.request(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'token' response: %w", err)
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+ cms := jsonBody["cms"].(map[string]any)
+ crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
+ if strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") {
+ crunchy.Config.Premium = true
+ crunchy.Config.Channel = "crunchyroll"
+ } else {
+ crunchy.Config.Premium = false
+ crunchy.Config.Channel = "-"
}
- crunchy.Config.TokenType = jsonBody["token_type"].(string)
- crunchy.Config.AccessToken = jsonBody["access_token"].(string)
-
- // index
- endpoint = "https://beta-api.crunchyroll.com/index/v2"
- resp, err = crunchy.request(endpoint)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'index' response: %w", err)
- }
- cms := jsonBody["cms"].(map[string]interface{})
-
crunchy.Config.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
- // me
endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me"
resp, err = crunchy.request(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
- }
-
- crunchy.Config.AccountID = jsonBody["account_id"].(string)
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
crunchy.Config.ExternalID = jsonBody["external_id"].(string)
- //profile
endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me/profile"
resp, err = crunchy.request(endpoint)
if err != nil {
return nil, err
}
defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
- }
-
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
crunchy.Config.MaturityRating = jsonBody["maturity_rating"].(string)
return crunchy, nil
}
+func request(req *http.Request, client *http.Client) (*http.Response, error) {
+ resp, err := client.Do(req)
+ if err == nil {
+ var buf bytes.Buffer
+ io.Copy(&buf, resp.Body)
+ defer resp.Body.Close()
+ defer func() {
+ resp.Body = io.NopCloser(&buf)
+ }()
+
+ if buf.Len() != 0 {
+ var errMap map[string]any
+
+ if err = json.Unmarshal(buf.Bytes(), &errMap); err != nil {
+ return nil, fmt.Errorf("invalid json response: %w", err)
+ }
+
+ if val, ok := errMap["error"]; ok {
+ if errorAsString, ok := val.(string); ok {
+ if code, ok := errMap["code"].(string); ok {
+ return nil, fmt.Errorf("error for endpoint %s (%d): %s - %s", req.URL.String(), resp.StatusCode, errorAsString, code)
+ }
+ return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, errorAsString)
+ } else if errorAsBool, ok := val.(bool); ok && errorAsBool {
+ if msg, ok := errMap["message"].(string); ok {
+ return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, msg)
+ }
+ }
+ }
+ }
+
+ if resp.StatusCode >= 400 {
+ return nil, fmt.Errorf("error for endpoint %s: %s", req.URL.String(), resp.Status)
+ }
+ }
+ return resp, err
+}
+
// request is a base function which handles api requests.
func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, endpoint, nil)
@@ -248,25 +270,7 @@ func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
}
req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken))
- resp, err := c.Client.Do(req)
- if err == nil {
- defer resp.Body.Close()
- bodyAsBytes, _ := io.ReadAll(resp.Body)
- defer resp.Body.Close()
- if resp.StatusCode == http.StatusUnauthorized {
- return nil, fmt.Errorf("invalid access token")
- } else {
- var errStruct struct {
- Message string `json:"message"`
- }
- json.NewDecoder(bytes.NewBuffer(bodyAsBytes)).Decode(&errStruct)
- if errStruct.Message != "" {
- return nil, fmt.Errorf(errStruct.Message)
- }
- }
- resp.Body = io.NopCloser(bytes.NewBuffer(bodyAsBytes))
- }
- return resp, err
+ return request(req, c.Client)
}
// IsCaching returns if data gets cached or not.
From c94ce0fb59dddb29bcbce4ad7e1e5b2dc1814b84 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 15:58:28 +0200
Subject: [PATCH 184/732] Fix country code not set
---
crunchyroll.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index 0aa4a2e..06a28ec 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -180,6 +180,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
crunchy.Config.TokenType = loginResp.TokenType
crunchy.Config.AccessToken = loginResp.AccessToken
crunchy.Config.AccountID = loginResp.AccountID
+ crunchy.Config.CountryCode = loginResp.Country
var jsonBody map[string]any
From 0780a2a2bceb42d83fb533540de247718585e6a9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 15:59:00 +0200
Subject: [PATCH 185/732] Change login type from session id to etp rt
---
cmd/crunchyroll-go/cmd/login.go | 46 ++++++++++++++++++++++++++++++---
cmd/crunchyroll-go/cmd/utils.go | 14 +++++-----
2 files changed, 49 insertions(+), 11 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 4bee3b0..28a784e 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -12,6 +12,7 @@ var (
loginPersistentFlag bool
loginSessionIDFlag bool
+ loginEtpRtFlag bool
)
var loginCmd = &cobra.Command{
@@ -22,6 +23,8 @@ var loginCmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
if loginSessionIDFlag {
return loginSessionID(args[0])
+ } else if loginEtpRtFlag {
+ return loginEtpRt(args[0])
} else {
return loginCredentials(args[0], args[1])
}
@@ -38,6 +41,10 @@ func init() {
"session-id",
false,
"Use a session id to login instead of username and password")
+ loginCmd.Flags().BoolVar(&loginEtpRtFlag,
+ "etp-rt",
+ false,
+ "Use a etp rt cookie to login instead of username and password")
rootCmd.AddCommand(loginCmd)
}
@@ -60,7 +67,7 @@ func loginCredentials(user, password string) error {
out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
- if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil {
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
return err
}
@@ -73,7 +80,38 @@ func loginCredentials(user, password string) error {
func loginSessionID(sessionID string) error {
out.Debug("Logging in via session id")
- if _, err := crunchyroll.LoginWithSessionID(sessionID, systemLocale(false), client); err != nil {
+ var c *crunchyroll.Crunchyroll
+ var err error
+ if c, err = crunchyroll.LoginWithSessionID(sessionID, systemLocale(false), client); err != nil {
+ out.Err(err.Error())
+ os.Exit(1)
+ }
+
+ if loginPersistentFlag {
+ if configDir, err := os.UserConfigDir(); err != nil {
+ return fmt.Errorf("could not save credentials persistent: %w", err)
+ } else {
+ os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
+ if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(c.EtpRt), 0600); err != nil {
+ return err
+ }
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
+ }
+ }
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
+ return err
+ }
+
+ if !loginPersistentFlag {
+ out.Info("Due to security reasons, you have to login again on the next reboot")
+ }
+
+ return nil
+}
+
+func loginEtpRt(etpRt string) error {
+ out.Debug("Logging in via etp rt")
+ if _, err := crunchyroll.LoginWithEtpRt(etpRt, systemLocale(false), client); err != nil {
out.Err(err.Error())
os.Exit(1)
}
@@ -84,13 +122,13 @@ func loginSessionID(sessionID string) error {
return fmt.Errorf("could not save credentials persistent: %w", err)
} else {
os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
- if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(sessionID), 0600); err != nil {
+ if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(etpRt), 0600); err != nil {
return err
}
out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
- if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(sessionID), 0600); err != nil {
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(etpRt), 0600); err != nil {
return err
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 2fddd60..86691ab 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -147,10 +147,10 @@ func loadCrunchy() {
out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
}
- if crunchy, err = crunchyroll.LoginWithSessionID(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
- out.Debug("Failed to login with temp session id: %w", err)
+ if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
+ out.Debug("Failed to login with temp etp rt: %w", err)
} else {
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
+ out.Debug("Logged in with etp rt %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
out.StopProgress("Logged in")
return
@@ -168,20 +168,20 @@ func loadCrunchy() {
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
split[0] = url.QueryEscape(split[0])
- if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
+ if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
}
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+ out.Debug("Logged in with etp rt %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
} else {
if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
}
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.SessionID)
+ out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.EtpRt)
// the session id is written to a temp file to reduce the amount of re-logging in.
// it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
- os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.SessionID), 0600)
+ os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.EtpRt), 0600)
}
out.StopProgress("Logged in")
return
From b4ba8c45996eeb3fce5484cd7af23199b868bff6 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 16:00:08 +0200
Subject: [PATCH 186/732] Bump go CI version to 1.18
---
.github/workflows/ci.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 44c7ac6..7c54dd3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,7 +11,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v3
with:
- go-version: 1.16
+ go-version: 1.18
- name: Build
run: go build -v cmd/crunchyroll-go/main.go
From 15373ed7d6b689e900ba3c44cf35376048bed679 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 27 May 2022 16:03:37 +0200
Subject: [PATCH 187/732] Fix typo
---
cmd/crunchyroll-go/cmd/utils.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 86691ab..aa98399 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -179,7 +179,7 @@ func loadCrunchy() {
os.Exit(1)
}
out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.EtpRt)
- // the session id is written to a temp file to reduce the amount of re-logging in.
+ // 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)
}
From 08c46e50bb1cdc6bc23dc73efb94fd5dc0c1cc6a Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Fri, 27 May 2022 19:05:35 +0200
Subject: [PATCH 188/732] Add new endpoints
---
category.go | 43 +++++++
crunchyroll.go | 300 +++++++++++++++++++++++++++++++++++++++++++++++++
news.go | 19 ++++
simulcast.go | 13 +++
utils.go | 48 ++++++++
5 files changed, 423 insertions(+)
create mode 100644 category.go
create mode 100644 news.go
create mode 100644 simulcast.go
diff --git a/category.go b/category.go
new file mode 100644
index 0000000..58855fc
--- /dev/null
+++ b/category.go
@@ -0,0 +1,43 @@
+package crunchyroll
+
+// Category contains all information about a category.
+type Category struct {
+ Category string `json:"tenant_category"`
+
+ SubCategories []struct {
+ Category string `json:"tenant_category"`
+ ParentCategory string `json:"parent_category"`
+
+ Localization struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Locale LOCALE `json:"locale"`
+ } `json:"localization"`
+
+ Slug string `json:"slug"`
+ } `json:"sub_categories"`
+
+ Images struct {
+ Background []struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"background"`
+
+ Low []struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"low"`
+ } `json:"images"`
+
+ Localization struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Locale LOCALE `json:"locale"`
+ } `json:"localization"`
+
+ Slug string `json:"slug"`
+}
diff --git a/crunchyroll.go b/crunchyroll.go
index dcfc375..a81f373 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -30,6 +30,23 @@ const (
AR = "ar-SA"
)
+// SortOrder represents a sort order.
+type SortOrder string
+
+const (
+ POPULARITY SortOrder = "popularity"
+ NEWLYADDED = "newly_added"
+ ALPHABETICAL = "alphabetical"
+)
+
+// MediaType represents a media type.
+type MediaType string
+
+const (
+ SERIES MediaType = "series"
+ MOVIELISTING = "movie_listing"
+)
+
type Crunchyroll struct {
// Client is the http.Client to perform all requests over.
Client *http.Client
@@ -60,6 +77,30 @@ type Crunchyroll struct {
cache bool
}
+// BrowseOptions represents options for browsing the crunchyroll catalog.
+type BrowseOptions struct {
+ // Categories specifies the categories of the results.
+ Categories []string `param:"categories"`
+
+ // IsDubbed specifies whether the results should be dubbed.
+ IsDubbed bool `param:"is_dubbed"`
+
+ // IsSubbed specifies whether the results should be subbed.
+ IsSubbed bool `param:"is_subbed"`
+
+ // SimulcastID specifies a particular simulcast season in which the results have been aired.
+ SimulcastID string `param:"season_tag"`
+
+ // SortBy specifies how the results should be sorted.
+ SortBy SortOrder `param:"sort_by"`
+
+ // Start specifies the index from which the results should be returned.
+ Start uint `param:"start"`
+
+ // Type specifies the media type of the results.
+ Type MediaType `param:"type"`
+}
+
// LoginWithCredentials logs in via crunchyroll username or email and password.
func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
sessionIDEndpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?version=1.0&access_token=%s&device_type=%s&device_id=%s",
@@ -446,3 +487,262 @@ func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
}
return
}
+
+// Browse browses the crunchyroll catalog filtered by the specified options and returns all found series and movies within the given limit.
+func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m []*Movie, err error) {
+ query, err := encodeStructToQueryValues(options)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s",
+ query, limit, c.Locale)
+ resp, err := c.request(browseEndpoint)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'browse' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+
+ return s, m, nil
+}
+
+// Categories returns all available categories and possible subcategories.
+func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
+ tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
+ includeSubcategories, c.Locale)
+ resp, err := c.request(tenantCategoriesEndpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'tenant_categories' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ category := &Category{}
+ if err := decodeMapToStruct(item, category); err != nil {
+ return nil, err
+ }
+
+ ca = append(ca, category)
+ }
+
+ return ca, nil
+}
+
+// Simulcasts returns all available simulcast seasons for the current locale.
+func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
+ seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale)
+ resp, err := c.request(seasonListEndpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'season_list' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ simulcast := &Simulcast{}
+ if err := decodeMapToStruct(item, simulcast); err != nil {
+ return nil, err
+ }
+
+ s = append(s, simulcast)
+ }
+
+ return s, nil
+}
+
+// News returns the top and latest news from crunchyroll for the current locale within the given limits.
+func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*LatestNews, err error) {
+ newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
+ topLimit, latestLimit, c.Locale)
+ resp, err := c.request(newsFeedEndpoint)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'news_feed' response: %w", err)
+ }
+
+ topNews := jsonBody["top_news"].(map[string]interface{})
+ for _, item := range topNews["items"].([]interface{}) {
+ topNews := &TopNews{}
+ if err := decodeMapToStruct(item, topNews); err != nil {
+ return nil, nil, err
+ }
+
+ t = append(t, topNews)
+ }
+
+ latestNews := jsonBody["latest_news"].(map[string]interface{})
+ for _, item := range latestNews["items"].([]interface{}) {
+ latestNews := &LatestNews{}
+ if err := decodeMapToStruct(item, latestNews); err != nil {
+ return nil, nil, err
+ }
+
+ l = append(l, latestNews)
+ }
+
+ return t, l, nil
+}
+
+// Recommendations returns series and movie recommendations from crunchyroll based on your account within the given limit.
+func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
+ recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
+ c.Config.AccountID, limit, c.Locale)
+ resp, err := c.request(recommendationsEndpoint)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'recommendations' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+
+ return s, m, nil
+}
+
+// UpNext returns the next episodes that you can continue watching based on your account within the given limit.
+func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
+ upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
+ c.Config.AccountID, limit, c.Locale)
+ resp, err := c.request(upNextAccountEndpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'up_next_account' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ panel := item.(map[string]interface{})["panel"]
+
+ episode := &Episode{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(panel, episode); err != nil {
+ return nil, err
+ }
+
+ e = append(e, episode)
+ }
+
+ return e, nil
+}
+
+// SimilarTo returns similar series and movies to the one specified by id within the given limits.
+func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) {
+ similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
+ c.Config.AccountID, id, limit, c.Locale)
+ resp, err := c.request(similarToEndpoint)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'similar_to' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+
+ return s, m, nil
+}
diff --git a/news.go b/news.go
new file mode 100644
index 0000000..3985cb4
--- /dev/null
+++ b/news.go
@@ -0,0 +1,19 @@
+package crunchyroll
+
+// News contains all information about a news.
+type News struct {
+ Title string `json:"title"`
+ Link string `json:"link"`
+ Image string `json:"image"`
+ Creator string `json:"creator"`
+ PublishDate string `json:"publish_date"`
+ Description string `json:"description"`
+}
+
+type TopNews struct {
+ News
+}
+
+type LatestNews struct {
+ News
+}
diff --git a/simulcast.go b/simulcast.go
new file mode 100644
index 0000000..d02f348
--- /dev/null
+++ b/simulcast.go
@@ -0,0 +1,13 @@
+package crunchyroll
+
+// Simulcast contains all information about a simulcast season.
+type Simulcast struct {
+ ID string `json:"id"`
+
+ Localization struct {
+ Title string `json:"title"`
+
+ // appears to be always an empty string.
+ Description string `json:"description"`
+ } `json:"localization"`
+}
diff --git a/utils.go b/utils.go
index a3d4191..672772f 100644
--- a/utils.go
+++ b/utils.go
@@ -2,6 +2,10 @@ package crunchyroll
import (
"encoding/json"
+ "fmt"
+ "net/url"
+ "reflect"
+ "strings"
)
func decodeMapToStruct(m interface{}, s interface{}) error {
@@ -23,3 +27,47 @@ func regexGroups(parsed [][]string, subexpNames ...string) map[string]string {
}
return groups
}
+
+func encodeStructToQueryValues(s interface{}) (string, error) {
+ values := make(url.Values)
+ v := reflect.ValueOf(s)
+
+ for i := 0; i < v.Type().NumField(); i++ {
+
+ // don't include parameters with default or without values in the query to avoid corruption of the API response.
+ if isEmptyValue(v.Field(i)) {
+ continue
+ }
+
+ key := v.Type().Field(i).Tag.Get("param")
+ var val string
+
+ if v.Field(i).Kind() == reflect.Slice {
+ var items []string
+
+ for _, i := range v.Field(i).Interface().([]string) {
+ items = append(items, i)
+ }
+
+ val = strings.Join(items, ",")
+ } else {
+ val = fmt.Sprint(v.Field(i).Interface())
+ }
+
+ values.Add(key, val)
+ }
+
+ return values.Encode(), nil
+}
+
+func isEmptyValue(v reflect.Value) bool {
+ switch v.Kind() {
+ case reflect.Slice, reflect.String:
+ return v.Len() == 0
+ case reflect.Bool:
+ return !v.Bool()
+ case reflect.Uint:
+ return v.Uint() == 0
+ }
+ return false
+}
From cf3559698582ee2589570fc59f4b67797b7c9554 Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sat, 28 May 2022 19:55:56 +0200
Subject: [PATCH 189/732] Add watch history
---
crunchyroll.go | 48 +++++++++++++++++++++++++++++++++++++++++++-----
episode.go | 12 ++++++++++++
news.go | 8 --------
3 files changed, 55 insertions(+), 13 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index a81f373..aefce1a 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -591,7 +591,7 @@ func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
}
// News returns the top and latest news from crunchyroll for the current locale within the given limits.
-func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*LatestNews, err error) {
+func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) {
newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
topLimit, latestLimit, c.Locale)
resp, err := c.request(newsFeedEndpoint)
@@ -607,7 +607,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*
topNews := jsonBody["top_news"].(map[string]interface{})
for _, item := range topNews["items"].([]interface{}) {
- topNews := &TopNews{}
+ topNews := &News{}
if err := decodeMapToStruct(item, topNews); err != nil {
return nil, nil, err
}
@@ -617,7 +617,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*TopNews, l []*
latestNews := jsonBody["latest_news"].(map[string]interface{})
for _, item := range latestNews["items"].([]interface{}) {
- latestNews := &LatestNews{}
+ latestNews := &News{}
if err := decodeMapToStruct(item, latestNews); err != nil {
return nil, nil, err
}
@@ -672,7 +672,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err
return s, m, nil
}
-// UpNext returns the next episodes that you can continue watching based on your account within the given limit.
+// UpNext returns the episodes that are up next based on your account within the given limit.
func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
c.Config.AccountID, limit, c.Locale)
@@ -703,7 +703,7 @@ func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
return e, nil
}
-// SimilarTo returns similar series and movies to the one specified by id within the given limits.
+// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limits.
func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) {
similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
c.Config.AccountID, id, limit, c.Locale)
@@ -746,3 +746,41 @@ func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie,
return s, m, nil
}
+
+// WatchHistory returns the history of watched episodes based on your account from the given page with the given size.
+func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
+ watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
+ c.Config.AccountID, page, size, c.Locale)
+ resp, err := c.request(watchHistoryEndpoint)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'watch-history' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ panel := item.(map[string]interface{})["panel"]
+
+ episode := &Episode{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(panel, episode); err != nil {
+ return nil, err
+ }
+
+ historyEpisode := &HistoryEpisode{
+ Episode: episode,
+ }
+ if err := decodeMapToStruct(item, historyEpisode); err != nil {
+ return nil, err
+ }
+
+ e = append(e, historyEpisode)
+ }
+
+ return e, nil
+}
diff --git a/episode.go b/episode.go
index a25844c..07f6705 100644
--- a/episode.go
+++ b/episode.go
@@ -75,6 +75,18 @@ type Episode struct {
StreamID string
}
+// HistoryEpisode contains additional information about an episode if the account has watched or started to watch the episode.
+type HistoryEpisode struct {
+ *Episode
+
+ ID string `json:"id"`
+ DatePlayed string `json:"date_played"`
+ ParentID string `json:"parent_id"`
+ ParentType MediaType `json:"parent_type"`
+ Playhead uint `json:"playhead"`
+ FullyWatched bool `json:"fully_watched"`
+}
+
// EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
diff --git a/news.go b/news.go
index 3985cb4..d90dd65 100644
--- a/news.go
+++ b/news.go
@@ -9,11 +9,3 @@ type News struct {
PublishDate string `json:"publish_date"`
Description string `json:"description"`
}
-
-type TopNews struct {
- News
-}
-
-type LatestNews struct {
- News
-}
From 7897da3baf200a77f9b9f876e7a7da9981d77e05 Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sun, 29 May 2022 00:21:17 +0200
Subject: [PATCH 190/732] Add account and update comments
---
account.go | 27 ++++++++++++++++++
crunchyroll.go | 74 ++++++++++++++++++++++++++++++++++++--------------
2 files changed, 80 insertions(+), 21 deletions(-)
create mode 100644 account.go
diff --git a/account.go b/account.go
new file mode 100644
index 0000000..f6f87f1
--- /dev/null
+++ b/account.go
@@ -0,0 +1,27 @@
+package crunchyroll
+
+// Account contains information about a crunchyroll account.
+type Account struct {
+ AccountID string `json:"account_id"`
+ ExternalID string `json:"external_id"`
+ EmailVerified bool `json:"email_verified"`
+ Created string `json:"created"`
+
+ Avatar string `json:"avatar"`
+ CrBetaOptIn bool `json:"cr_beta_opt_in"`
+ Email string `json:"email"`
+ MatureContentFlagManga string `json:"mature_content_flag_manga"`
+ MaturityRating string `json:"maturity_rating"`
+ OptOutAndroidInAppMarketing bool `json:"opt_out_android_in_app_marketing"`
+ OptOutFreeTrials bool `json:"opt_out_free_trials"`
+ OptOutNewMediaQueueUpdates bool `json:"opt_out_new_media_queue_updates"`
+ OptOutNewsletters bool `json:"opt_out_newsletters"`
+ OptOutPmUpdates bool `json:"opt_out_pm_updates"`
+ OptOutPromotionalUpdates bool `json:"opt_out_promotional_updates"`
+ OptOutQueueUpdates bool `json:"opt_out_queue_updates"`
+ OptOutStoreDeals bool `json:"opt_out_store_deals"`
+ PreferredCommunicationLanguage LOCALE `json:"preferred_communication_language"`
+ PreferredContentSubtitleLanguage LOCALE `json:"preferred_content_subtitle_language"`
+ QaUser bool `json:"qa_user"`
+ Username string `json:"username"`
+}
diff --git a/crunchyroll.go b/crunchyroll.go
index aefce1a..4e5a436 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -30,15 +30,6 @@ const (
AR = "ar-SA"
)
-// SortOrder represents a sort order.
-type SortOrder string
-
-const (
- POPULARITY SortOrder = "popularity"
- NEWLYADDED = "newly_added"
- ALPHABETICAL = "alphabetical"
-)
-
// MediaType represents a media type.
type MediaType string
@@ -47,6 +38,15 @@ const (
MOVIELISTING = "movie_listing"
)
+// SortType represents a sort type.
+type SortType string
+
+const (
+ POPULARITY SortType = "popularity"
+ NEWLYADDED = "newly_added"
+ ALPHABETICAL = "alphabetical"
+)
+
type Crunchyroll struct {
// Client is the http.Client to perform all requests over.
Client *http.Client
@@ -79,25 +79,25 @@ type Crunchyroll struct {
// BrowseOptions represents options for browsing the crunchyroll catalog.
type BrowseOptions struct {
- // Categories specifies the categories of the results.
+ // Categories specifies the categories of the entries.
Categories []string `param:"categories"`
- // IsDubbed specifies whether the results should be dubbed.
+ // IsDubbed specifies whether the entries should be dubbed.
IsDubbed bool `param:"is_dubbed"`
- // IsSubbed specifies whether the results should be subbed.
+ // IsSubbed specifies whether the entries should be subbed.
IsSubbed bool `param:"is_subbed"`
- // SimulcastID specifies a particular simulcast season in which the results have been aired.
- SimulcastID string `param:"season_tag"`
+ // Simulcast specifies a particular simulcast season by id in which the entries have been aired.
+ Simulcast string `param:"season_tag"`
- // SortBy specifies how the results should be sorted.
- SortBy SortOrder `param:"sort_by"`
+ // Sort specifies how the entries should be sorted.
+ Sort SortType `param:"sort_by"`
- // Start specifies the index from which the results should be returned.
+ // Start specifies the index from which the entries should be returned.
Start uint `param:"start"`
- // Type specifies the media type of the results.
+ // Type specifies the media type of the entries.
Type MediaType `param:"type"`
}
@@ -672,7 +672,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err
return s, m, nil
}
-// UpNext returns the episodes that are up next based on your account within the given limit.
+// UpNext returns the episodes that are up next based on the currently logged in account within the given limit.
func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
c.Config.AccountID, limit, c.Locale)
@@ -703,7 +703,7 @@ func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
return e, nil
}
-// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limits.
+// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limit.
func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) {
similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
c.Config.AccountID, id, limit, c.Locale)
@@ -747,7 +747,7 @@ func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie,
return s, m, nil
}
-// WatchHistory returns the history of watched episodes based on your account from the given page with the given size.
+// WatchHistory returns the history of watched episodes based on the currently logged in account from the given page with the given size.
func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
c.Config.AccountID, page, size, c.Locale)
@@ -784,3 +784,35 @@ func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, e
return e, nil
}
+
+// Account returns information about the currently logged in crunchyroll account.
+func (c *Crunchyroll) Account() (*Account, error) {
+ resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
+ }
+
+ resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
+ }
+
+ account := &Account{}
+
+ if err := decodeMapToStruct(jsonBody, account); err != nil {
+ return nil, err
+ }
+
+ return account, nil
+}
From acc6c63ebd3f65e2e325018f20217e4433e7982b Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sun, 29 May 2022 01:42:24 +0200
Subject: [PATCH 191/732] Fix comment
---
crunchyroll.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 4e5a436..5ee3a97 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -628,7 +628,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*New
return t, l, nil
}
-// Recommendations returns series and movie recommendations from crunchyroll based on your account within the given limit.
+// Recommendations returns series and movie recommendations from crunchyroll based on the currently logged in account within the given limit.
func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
c.Config.AccountID, limit, c.Locale)
From b53256ca3fde95e834c4019b37eb230f7881713e Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 29 May 2022 10:10:48 +0200
Subject: [PATCH 192/732] Use Account struct directly instead of maps
---
crunchyroll.go | 13 ++++---------
1 file changed, 4 insertions(+), 9 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 5ee3a97..d20dd20 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -793,8 +793,9 @@ func (c *Crunchyroll) Account() (*Account, error) {
}
defer resp.Body.Close()
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ account := &Account{}
+
+ if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
}
@@ -804,15 +805,9 @@ func (c *Crunchyroll) Account() (*Account, error) {
}
defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
}
- account := &Account{}
-
- if err := decodeMapToStruct(jsonBody, account); err != nil {
- return nil, err
- }
-
return account, nil
}
From 29343d1c6f8326332f4bba57c49de543fd63c2ae Mon Sep 17 00:00:00 2001
From: IchBinLeoon <50367411+IchBinLeoon@users.noreply.github.com>
Date: Sun, 29 May 2022 15:08:38 +0200
Subject: [PATCH 193/732] Add requested changes
---
account.go | 10 ++++++----
episode.go | 3 +--
utils.go | 27 +++++++++++++--------------
3 files changed, 20 insertions(+), 20 deletions(-)
diff --git a/account.go b/account.go
index f6f87f1..d22eba5 100644
--- a/account.go
+++ b/account.go
@@ -1,11 +1,13 @@
package crunchyroll
+import "time"
+
// Account contains information about a crunchyroll account.
type Account struct {
- AccountID string `json:"account_id"`
- ExternalID string `json:"external_id"`
- EmailVerified bool `json:"email_verified"`
- Created string `json:"created"`
+ AccountID string `json:"account_id"`
+ ExternalID string `json:"external_id"`
+ EmailVerified bool `json:"email_verified"`
+ Created time.Time `json:"created"`
Avatar string `json:"avatar"`
CrBetaOptIn bool `json:"cr_beta_opt_in"`
diff --git a/episode.go b/episode.go
index 07f6705..de38c65 100644
--- a/episode.go
+++ b/episode.go
@@ -79,8 +79,7 @@ type Episode struct {
type HistoryEpisode struct {
*Episode
- ID string `json:"id"`
- DatePlayed string `json:"date_played"`
+ DatePlayed time.Time `json:"date_played"`
ParentID string `json:"parent_id"`
ParentType MediaType `json:"parent_type"`
Playhead uint `json:"playhead"`
diff --git a/utils.go b/utils.go
index 672772f..d32d39b 100644
--- a/utils.go
+++ b/utils.go
@@ -35,8 +35,19 @@ func encodeStructToQueryValues(s interface{}) (string, error) {
for i := 0; i < v.Type().NumField(); i++ {
// don't include parameters with default or without values in the query to avoid corruption of the API response.
- if isEmptyValue(v.Field(i)) {
- continue
+ switch v.Field(i).Kind() {
+ case reflect.Slice, reflect.String:
+ if v.Field(i).Len() == 0 {
+ continue
+ }
+ case reflect.Bool:
+ if !v.Field(i).Bool() {
+ continue
+ }
+ case reflect.Uint:
+ if v.Field(i).Uint() == 0 {
+ continue
+ }
}
key := v.Type().Field(i).Tag.Get("param")
@@ -59,15 +70,3 @@ func encodeStructToQueryValues(s interface{}) (string, error) {
return values.Encode(), nil
}
-
-func isEmptyValue(v reflect.Value) bool {
- switch v.Kind() {
- case reflect.Slice, reflect.String:
- return v.Len() == 0
- case reflect.Bool:
- return !v.Bool()
- case reflect.Uint:
- return v.Uint() == 0
- }
- return false
-}
From 6581a5bd0f14f0601c2e4b84478bad84df74998d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 29 May 2022 16:10:37 +0200
Subject: [PATCH 194/732] Fix typo
---
cmd/crunchyroll-go/cmd/utils.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index aa98399..0a89921 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -148,9 +148,9 @@ func loadCrunchy() {
os.Exit(1)
}
if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
- out.Debug("Failed to login with temp etp rt: %w", err)
+ out.Debug("Failed to login with temp etp rt cookie: %w", err)
} else {
- out.Debug("Logged in with etp rt %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
+ out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
out.StopProgress("Logged in")
return
@@ -172,7 +172,7 @@ func loadCrunchy() {
out.StopProgress(err.Error())
os.Exit(1)
}
- out.Debug("Logged in with etp rt %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
+ out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
} else {
if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
From 2471042d02b743e9925493e2b69e78c237a70ecb Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 30 May 2022 08:56:30 +0200
Subject: [PATCH 195/732] Change error formatter for non Errorf
---
cmd/crunchyroll-go/cmd/utils.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 0a89921..5c4a36a 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -148,7 +148,7 @@ func loadCrunchy() {
os.Exit(1)
}
if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
- out.Debug("Failed to login with temp etp rt cookie: %w", err)
+ 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)
From 1c37c3e699e033f2eedf41ca8326c125ef826319 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 30 May 2022 12:08:52 +0200
Subject: [PATCH 196/732] Add method parameter to internal request function
---
crunchyroll.go | 12 ++++++------
episode.go | 3 ++-
movie_listing.go | 5 +++--
season.go | 3 ++-
stream.go | 3 ++-
video.go | 9 +++++----
6 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 06a28ec..bdffa27 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -185,7 +185,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
var jsonBody map[string]any
endpoint := "https://beta-api.crunchyroll.com/index/v2"
- resp, err := crunchy.request(endpoint)
+ resp, err := crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -205,7 +205,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me"
- resp, err = crunchy.request(endpoint)
+ resp, err = crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -214,7 +214,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
crunchy.Config.ExternalID = jsonBody["external_id"].(string)
endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me/profile"
- resp, err = crunchy.request(endpoint)
+ resp, err = crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -264,8 +264,8 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
}
// request is a base function which handles api requests.
-func (c *Crunchyroll) request(endpoint string) (*http.Response, error) {
- req, err := http.NewRequest(http.MethodGet, endpoint, nil)
+func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, error) {
+ req, err := http.NewRequest(method, endpoint, nil)
if err != nil {
return nil, err
}
@@ -292,7 +292,7 @@ func (c *Crunchyroll) SetCaching(caching bool) {
func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
query, limit, c.Locale)
- resp, err := c.request(searchEndpoint)
+ resp, err := c.request(searchEndpoint, http.MethodGet)
if err != nil {
return nil, nil, err
}
diff --git a/episode.go b/episode.go
index a25844c..e9dd16a 100644
--- a/episode.go
+++ b/episode.go
@@ -3,6 +3,7 @@ package crunchyroll
import (
"encoding/json"
"fmt"
+ "net/http"
"regexp"
"strconv"
"strings"
@@ -85,7 +86,7 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
crunchy.Locale,
crunchy.Config.Signature,
crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
+ crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
diff --git a/movie_listing.go b/movie_listing.go
index 63d7fab..110f67a 100644
--- a/movie_listing.go
+++ b/movie_listing.go
@@ -3,6 +3,7 @@ package crunchyroll
import (
"encoding/json"
"fmt"
+ "net/http"
)
// MovieListing contains information about something which is called
@@ -48,7 +49,7 @@ func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error)
crunchy.Locale,
crunchy.Config.Signature,
crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
+ crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
@@ -77,7 +78,7 @@ func (ml *MovieListing) AudioLocale() (LOCALE, error) {
ml.crunchy.Locale,
ml.crunchy.Config.Signature,
ml.crunchy.Config.Policy,
- ml.crunchy.Config.KeyPairID))
+ ml.crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return "", err
}
diff --git a/season.go b/season.go
index 825a816..f5513f5 100644
--- a/season.go
+++ b/season.go
@@ -3,6 +3,7 @@ package crunchyroll
import (
"encoding/json"
"fmt"
+ "net/http"
"regexp"
)
@@ -94,7 +95,7 @@ func (s *Season) Episodes() (episodes []*Episode, err error) {
s.crunchy.Locale,
s.crunchy.Config.Signature,
s.crunchy.Config.Policy,
- s.crunchy.Config.KeyPairID))
+ s.crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
diff --git a/stream.go b/stream.go
index 7505a24..00060ef 100644
--- a/stream.go
+++ b/stream.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"github.com/grafov/m3u8"
+ "net/http"
"regexp"
)
@@ -72,7 +73,7 @@ func (s *Stream) Formats() ([]*Format, error) {
// fromVideoStreams returns all streams which are accessible via the endpoint.
func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) {
- resp, err := crunchy.request(endpoint)
+ resp, err := crunchy.request(endpoint, http.MethodGet)
if err != nil {
return nil, err
}
diff --git a/video.go b/video.go
index 00b7734..365f954 100644
--- a/video.go
+++ b/video.go
@@ -3,6 +3,7 @@ package crunchyroll
import (
"encoding/json"
"fmt"
+ "net/http"
)
type video struct {
@@ -77,7 +78,7 @@ func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
crunchy.Locale,
crunchy.Config.Signature,
crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
+ crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
@@ -110,7 +111,7 @@ func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
m.crunchy.Locale,
m.crunchy.Config.Signature,
m.crunchy.Config.Policy,
- m.crunchy.Config.KeyPairID))
+ m.crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
@@ -173,7 +174,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
crunchy.Locale,
crunchy.Config.Signature,
crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
+ crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
@@ -206,7 +207,7 @@ func (s *Series) Seasons() (seasons []*Season, err error) {
s.crunchy.Locale,
s.crunchy.Config.Signature,
s.crunchy.Config.Policy,
- s.crunchy.Config.KeyPairID))
+ s.crunchy.Config.KeyPairID), http.MethodGet)
if err != nil {
return nil, err
}
From 3a7ec02598ea1f857b51a51307b5036a0742db0c Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 30 May 2022 12:24:54 +0200
Subject: [PATCH 197/732] Resolve merge conflicts
---
crunchyroll.go | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 268562d..e5c4a9d 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -169,15 +169,13 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
return nil, fmt.Errorf("failed to start session: %s", resp.Status)
}
+ var jsonBody map[string]any
if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
}
if isError, ok := jsonBody["error"]; ok && isError.(bool) {
return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"])
}
- data := jsonBody["data"].(map[string]interface{})
-
- crunchy.Config.CountryCode = data["country_code"].(string)
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -243,6 +241,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&jsonBody)
+
cms := jsonBody["cms"].(map[string]any)
crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
if strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") {
@@ -261,7 +260,6 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
crunchy.Config.Channel = "-"
}
- cms := jsonBody["cms"].(map[string]interface{})
crunchy.Config.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
@@ -526,7 +524,7 @@ func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m
browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s",
query, limit, c.Locale)
- resp, err := c.request(browseEndpoint)
+ resp, err := c.request(browseEndpoint, http.MethodGet)
if err != nil {
return nil, nil, err
}
@@ -570,7 +568,7 @@ func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m
func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
includeSubcategories, c.Locale)
- resp, err := c.request(tenantCategoriesEndpoint)
+ resp, err := c.request(tenantCategoriesEndpoin, http.MethodGet)
if err != nil {
return nil, err
}
@@ -623,7 +621,7 @@ func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) {
newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
topLimit, latestLimit, c.Locale)
- resp, err := c.request(newsFeedEndpoint)
+ resp, err := c.request(newsFeedEndpoint, http.MethodGet)
if err != nil {
return nil, nil, err
}
From 35b1cbbdb42328d848fbef5ff3d6f157f9923297 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 30 May 2022 12:27:20 +0200
Subject: [PATCH 198/732] Fix typo
---
crunchyroll.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index e5c4a9d..5154c9c 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -568,7 +568,7 @@ func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m
func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
includeSubcategories, c.Locale)
- resp, err := c.request(tenantCategoriesEndpoin, http.MethodGet)
+ resp, err := c.request(tenantCategoriesEndpoint, http.MethodGet)
if err != nil {
return nil, err
}
From 38fe521d55a8377697c03e86490443688fb9295d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 30 May 2022 12:32:13 +0200
Subject: [PATCH 199/732] Resolve more merge conflicts which GitHub didn't save
lul
---
crunchyroll.go | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 5154c9c..38b889b 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -241,7 +241,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
}
defer resp.Body.Close()
json.NewDecoder(resp.Body).Decode(&jsonBody)
-
+
cms := jsonBody["cms"].(map[string]any)
crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
if strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") {
@@ -259,7 +259,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
crunchy.Config.Premium = false
crunchy.Config.Channel = "-"
}
-
+
crunchy.Config.Policy = cms["policy"].(string)
crunchy.Config.Signature = cms["signature"].(string)
crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
@@ -594,7 +594,7 @@ func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err
// Simulcasts returns all available simulcast seasons for the current locale.
func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale)
- resp, err := c.request(seasonListEndpoint)
+ resp, err := c.request(seasonListEndpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -659,7 +659,7 @@ func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*New
func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(recommendationsEndpoint)
+ resp, err := c.request(recommendationsEndpoint, http.MethodGet)
if err != nil {
return nil, nil, err
}
@@ -703,7 +703,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err
func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(upNextAccountEndpoint)
+ resp, err := c.request(upNextAccountEndpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -734,7 +734,7 @@ func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) {
similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
c.Config.AccountID, id, limit, c.Locale)
- resp, err := c.request(similarToEndpoint)
+ resp, err := c.request(similarToEndpoint, http.MethodGet)
if err != nil {
return nil, nil, err
}
@@ -778,7 +778,7 @@ func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie,
func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
c.Config.AccountID, page, size, c.Locale)
- resp, err := c.request(watchHistoryEndpoint)
+ resp, err := c.request(watchHistoryEndpoint, http.MethodGet)
if err != nil {
return nil, err
}
@@ -814,7 +814,7 @@ func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, e
// Account returns information about the currently logged in crunchyroll account.
func (c *Crunchyroll) Account() (*Account, error) {
- resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me")
+ resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
if err != nil {
return nil, err
}
@@ -826,7 +826,7 @@ func (c *Crunchyroll) Account() (*Account, error) {
return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
}
- resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile")
+ resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile", http.MethodGet)
if err != nil {
return nil, err
}
From b9ff56c111e3a5aa1cfa7f730d04a173e3524210 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 31 May 2022 17:01:03 +0200
Subject: [PATCH 200/732] Extend encrypt flag description
---
cmd/crunchyroll-go/cmd/login.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 565dac0..1c47340 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -43,7 +43,7 @@ func init() {
loginCmd.Flags().BoolVar(&loginEncryptFlag,
"encrypt",
false,
- "Encrypt the given credentials (won't do anything if --session-id is given)")
+ "Encrypt the given credentials (won't do anything if --session-id is given or --persistent is not given)")
loginCmd.Flags().BoolVar(&loginSessionIDFlag,
"session-id",
From ce29e31164ba25396cbcbb3b619723e5ffa283d4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 31 May 2022 17:04:39 +0200
Subject: [PATCH 201/732] Add encrypt flag notice when use login without it
---
cmd/crunchyroll-go/cmd/login.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 1c47340..d31179e 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -124,7 +124,8 @@ func loginCredentials(user, password string) error {
return err
}
if !loginEncryptFlag {
- out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s)", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
+ out.Info("The login information will be stored permanently UNENCRYPTED on your drive (%s). "+
+ "To encrypt it, use the `--encrypt` flag", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
}
From 51b5e7b2ffba9bfd62f736441ab1b5eb8218570c Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 6 Jun 2022 21:23:29 +0200
Subject: [PATCH 202/732] Add bug issue templates
---
.github/ISSUE_TEMPLATE/bug-report.md | 36 ++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
new file mode 100644
index 0000000..3cbda16
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -0,0 +1,36 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Bug target**
+Set a `x` between the square brackets if your bug occurred while using the CLI or the library
+
+- [ ] CLI (command line client)
+- [ ] Library
+
+**To Reproduce**
+Steps to reproduce the behavior:
+```
+$ crunchy ...
+```
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Client (please complete the following information):**
+ - OS: [e.g. Windows]
+ - Version [e.g. v2.2.2]
+
+**Additional context**
+Add any other context about the problem here.
From c963af1f11c2585494372243d31a7c508c466247 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Wed, 8 Jun 2022 14:10:46 +0200
Subject: [PATCH 203/732] Why github
---
cmd/crunchyroll-go/cmd/utils.go | 3 ---
1 file changed, 3 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 7fe498f..dd1254c 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -170,7 +170,6 @@ func loadCrunchy() {
}
split := strings.SplitN(string(body), "\n", 2)
if len(split) == 1 || split[1] == "" {
-<<<<<<< v3/feature/encrypted-credentials
if strings.HasPrefix(split[0], "aes:") {
encrypted := body[4:]
@@ -214,7 +213,6 @@ func loadCrunchy() {
}
if len(split) == 2 {
-=======
split[0] = url.QueryEscape(split[0])
if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
@@ -222,7 +220,6 @@ func loadCrunchy() {
}
out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
} else {
->>>>>>> next/v3
if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
out.StopProgress(err.Error())
os.Exit(1)
From 735136077eeb1cbd916d5c95627898b90525237e Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Wed, 8 Jun 2022 14:12:30 +0200
Subject: [PATCH 204/732] I removed it but ok github if you want i do it a
second time, no problem
---
cmd/crunchyroll-go/cmd/login.go | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index bfef794..0c00100 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -135,13 +135,10 @@ func loginCredentials(user, password string) error {
"To encrypt it, use the `--encrypt` flag", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
- }
-<<<<<<< v3/feature/encrypted-credentials
-=======
+
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
return err
}
->>>>>>> next/v3
if !loginPersistentFlag {
out.Info("Due to security reasons, you have to login again on the next reboot")
From 69d2e10362929c415ecc050ac73535eedc7defdf Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 8 Jun 2022 14:18:20 +0200
Subject: [PATCH 205/732] I fucking hate it
---
cmd/crunchyroll-go/cmd/login.go | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index 0c00100..d8cc5c3 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -67,10 +67,6 @@ func loginCredentials(user, password string) error {
return err
}
- if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.SessionID), 0600); err != nil {
- return err
- }
-
if loginPersistentFlag {
if configDir, err := os.UserConfigDir(); err != nil {
return fmt.Errorf("could not save credentials persistent: %w", err)
@@ -135,7 +131,8 @@ func loginCredentials(user, password string) error {
"To encrypt it, use the `--encrypt` flag", filepath.Join(configDir, "crunchyroll-go", "crunchy"))
}
}
-
+ }
+
if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(c.EtpRt), 0600); err != nil {
return err
}
From dcdde6749e971aa0dbc1ee104d9fbf123883c217 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 9 Jun 2022 09:56:32 +0200
Subject: [PATCH 206/732] Add info command
---
cmd/crunchyroll-go/cmd/info.go | 40 ++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100644 cmd/crunchyroll-go/cmd/info.go
diff --git a/cmd/crunchyroll-go/cmd/info.go b/cmd/crunchyroll-go/cmd/info.go
new file mode 100644
index 0000000..f5ed995
--- /dev/null
+++ b/cmd/crunchyroll-go/cmd/info.go
@@ -0,0 +1,40 @@
+package cmd
+
+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
+}
From 31d3065e7bcc30ac6693254be4ae4a68570894ce Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 9 Jun 2022 10:31:16 +0200
Subject: [PATCH 207/732] Fix weird code which was probably caused by a
wonderful merge
---
cmd/crunchyroll-go/cmd/utils.go | 29 +++++++++++------------------
1 file changed, 11 insertions(+), 18 deletions(-)
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index dd1254c..12d3d4e 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -150,7 +150,7 @@ func loadCrunchy() {
out.StopProgress("Failed to read login information: %v", err)
os.Exit(1)
}
- if crunchy, err = crunchyroll.LoginWithEtpRt(url.QueryEscape(string(body)), systemLocale(true), client); err != nil {
+ if crunchy, err = crunchyroll.LoginWithEtpRt(string(body), systemLocale(true), client); err != nil {
out.Debug("Failed to login with temp etp rt cookie: %v", err)
} 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)
@@ -202,34 +202,27 @@ func loadCrunchy() {
os.Exit(1)
}
split = strings.SplitN(string(b), "\n", 2)
- } else {
- split[0] = url.QueryEscape(split[0])
- if crunchy, err = crunchyroll.LoginWithSessionID(split[0], systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
- os.Exit(1)
- }
- out.Debug("Logged in with session id %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
}
}
if len(split) == 2 {
- split[0] = url.QueryEscape(split[0])
+ 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])
- } else {
- if crunchy, err = crunchyroll.LoginWithCredentials(split[0], split[1], systemLocale(true), client); err != nil {
- out.StopProgress(err.Error())
- os.Exit(1)
- }
- out.Debug("Logged in with etp rt cookie %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", crunchy.EtpRt)
- // the etp rt is written to a temp file to reduce the amount of re-logging in.
- // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
- os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.EtpRt), 0600)
}
+ // the etp rt is written to a temp file to reduce the amount of re-logging in.
+ // it seems like that crunchyroll has also a little cooldown if a user logs in too often in a short time
+ os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.EtpRt), 0600)
+
out.StopProgress("Logged in")
return
}
From 137f3779ea2e2182ab1c15ed35c89fd931587732 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 10 Jun 2022 16:12:58 +0200
Subject: [PATCH 208/732] Fix merge issues again, I love this shit
---
crunchyroll.go | 41 +++++++++++++----------------------------
stream.go | 2 +-
2 files changed, 14 insertions(+), 29 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 73cba57..be5af03 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -110,6 +111,9 @@ type loginResponse struct {
Scope string `json:"scope"`
Country string `json:"country"`
AccountID string `json:"account_id"`
+
+ Error bool `json:"error"`
+ Message string `json:"message"`
}
// LoginWithCredentials logs in via crunchyroll username or email and password.
@@ -133,16 +137,13 @@ func LoginWithCredentials(user string, password string, locale LOCALE, client *h
}
defer resp.Body.Close()
- if loginResp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("failed to auth with credentials: %s", loginResp.Status)
- } else {
- var loginRespBody map[string]interface{}
- json.NewDecoder(loginResp.Body).Decode(&loginRespBody)
-
- if loginRespBody["error"].(bool) {
- return nil, fmt.Errorf("an unexpected login error occoured: %s", loginRespBody["message"])
- }
- }
+ var loginResp loginResponse
+ json.NewDecoder(resp.Body).Decode(&loginResp)
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("failed to auth with credentials: %s", resp.Status)
+ } else if loginResp.Error {
+ return nil, fmt.Errorf("an unexpected login error occoured: %s", loginResp.Message)
+ }
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -180,18 +181,16 @@ func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*
if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
}
-
+
if isError, ok := jsonBody["error"]; ok && isError.(bool) {
return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"])
}
data := jsonBody["data"].(map[string]interface{})
- crunchy.Config.CountryCode = data["country_code"].(string)
user := data["user"]
if user == nil {
return nil, errors.New("invalid session id, user is not logged in")
}
- crunchy.Config.Premium = user.(map[string]interface{})["premium"] != ""
var etpRt string
for _, cookie := range resp.Cookies() {
@@ -260,21 +259,7 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
cms := jsonBody["cms"].(map[string]any)
crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
- if strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll") {
- crunchy.Config.Premium = true
- crunchy.Config.Channel = "crunchyroll"
- } else {
- crunchy.Config.Premium = false
- crunchy.Config.Channel = "-"
- }
-
- if strings.Contains(cms["bucket"].(string), "crunchyroll") {
- crunchy.Config.Premium = true
- crunchy.Config.Channel = "crunchyroll"
- } else {
- crunchy.Config.Premium = false
- crunchy.Config.Channel = "-"
- }
+ crunchy.Config.Premium = strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll")
// / is trimmed so that urls which require it must be in .../{bucket}/... like format.
// this just looks cleaner
diff --git a/stream.go b/stream.go
index 042d5f7..e5ce54f 100644
--- a/stream.go
+++ b/stream.go
@@ -84,7 +84,7 @@ func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream,
if !crunchy.Config.Premium {
return nil, fmt.Errorf("no stream available, this might be the result of using a non-premium account")
} else {
- return nil, errors.New("no stream available")
+ return nil, fmt.Errorf("no stream available")
}
}
From 8d69b7775b621eb65957b489b524439539b0ac7b Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 12 Jun 2022 14:23:13 +0200
Subject: [PATCH 209/732] Add name poll notice
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index b8d48fa..10d6c99 100644
--- a/README.md
+++ b/README.md
@@ -36,6 +36,8 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
License ⚖
+_There is currently a [poll](https://github.com/ByteDream/crunchyroll-go/issues/39) going on how to name the CLI in the next version. Please have a moment and participate to it._
+
# 🖥️ CLI
## ✨ Features
From c5f2b55f346d2e45a6cf09ffa8750c915a180970 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 00:22:38 +0200
Subject: [PATCH 210/732] Add watchlist endpoint, add new request method &
change SortType name and consts
---
crunchyroll.go | 93 +++++++++++++++++++++++++++++++++++++++++++----
episode.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++----
video.go | 21 +++++++++++
3 files changed, 199 insertions(+), 12 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 38b889b..7618fdf 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -38,13 +38,28 @@ const (
MOVIELISTING = "movie_listing"
)
-// SortType represents a sort type.
-type SortType string
+// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after.
+type BrowseSortType string
const (
- POPULARITY SortType = "popularity"
- NEWLYADDED = "newly_added"
- ALPHABETICAL = "alphabetical"
+ BROWSESORTPOPULARITY BrowseSortType = "popularity"
+ BROWSESORTNEWLYADDED = "newly_added"
+ BROWSESORTALPHABETICAL = "alphabetical"
+)
+
+// WatchlistLanguageType represents a filter type to filter Crunchyroll.WatchList entries after.
+type WatchlistLanguageType int
+
+const (
+ WATCHLISTLANGUAGESUBBED WatchlistLanguageType = iota + 1
+ WATCHLISTLANGUAGEDUBBED
+)
+
+type WatchlistContentType string
+
+const (
+ WATCHLISTCONTENTSERIES WatchlistContentType = "series"
+ WATCHLISTCONTENTMOVIES = "movie_listing"
)
type Crunchyroll struct {
@@ -95,7 +110,7 @@ type BrowseOptions struct {
Simulcast string `param:"season_tag"`
// Sort specifies how the entries should be sorted.
- Sort SortType `param:"sort_by"`
+ Sort BrowseSortType `param:"sort_by"`
// Start specifies the index from which the entries should be returned.
Start uint `param:"start"`
@@ -329,6 +344,10 @@ func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, e
if err != nil {
return nil, err
}
+ return c.requestFull(req)
+}
+
+func (c *Crunchyroll) requestFull(req *http.Request) (*http.Response, error) {
req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken))
return request(req, c.Client)
@@ -812,6 +831,68 @@ func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, e
return e, nil
}
+type WatchlistOptions struct {
+ // OrderAsc specified whether the results should be order ascending or descending.
+ OrderAsc bool
+
+ // OnlyFavorites specifies whether only episodes which are marked as favorite should be returned.
+ OnlyFavorites bool
+
+ // LanguageType specifies whether returning episodes should be only subbed or dubbed.
+ LanguageType WatchlistLanguageType
+
+ // ContentType specified whether returning videos should only be series episodes or movies.
+ // But tbh all movies I've searched on crunchy were flagged as series too, so this
+ // parameter is kinda useless.
+ ContentType WatchlistContentType
+}
+
+func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) {
+ values := url.Values{}
+ if options.OrderAsc {
+ values.Set("order", "asc")
+ } else {
+ values.Set("order", "desc")
+ }
+ if options.OnlyFavorites {
+ values.Set("only_favorites", "true")
+ }
+ switch options.LanguageType {
+ case WATCHLISTLANGUAGESUBBED:
+ values.Set("is_subbed", "true")
+ case WATCHLISTLANGUAGEDUBBED:
+ values.Set("is_dubbed", "true")
+ }
+ values.Set("n", strconv.Itoa(int(limit)))
+ values.Set("locale", string(c.Locale))
+
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/%s/watchlist?%s", c.Config.AccountID, values.Encode())
+ resp, err := c.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ var watchlistEntries []*WatchlistEntry
+ if err := decodeMapToStruct(jsonBody["items"], &watchlistEntries); err != nil {
+ return nil, err
+ }
+
+ for _, entry := range watchlistEntries {
+ switch entry.Panel.Type {
+ case WATCHLISTENTRYEPISODE:
+ entry.Panel.EpisodeMetadata.crunchy = c
+ case WATCHLISTENTRYSERIES:
+ entry.Panel.SeriesMetadata.crunchy = c
+ }
+ }
+
+ return watchlistEntries, nil
+}
+
// Account returns information about the currently logged in crunchyroll account.
func (c *Crunchyroll) Account() (*Account, error) {
resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
diff --git a/episode.go b/episode.go
index 0405e0e..e99bd12 100644
--- a/episode.go
+++ b/episode.go
@@ -1,6 +1,7 @@
package crunchyroll
import (
+ "bytes"
"encoding/json"
"fmt"
"net/http"
@@ -39,11 +40,14 @@ type Episode struct {
NextEpisodeID string `json:"next_episode_id"`
NextEpisodeTitle string `json:"next_episode_title"`
- HDFlag bool `json:"hd_flag"`
- IsMature bool `json:"is_mature"`
- MatureBlocked bool `json:"mature_blocked"`
+ HDFlag bool `json:"hd_flag"`
+ MaturityRatings []string `json:"maturity_ratings"`
+ IsMature bool `json:"is_mature"`
+ MatureBlocked bool `json:"mature_blocked"`
- EpisodeAirDate time.Time `json:"episode_air_date"`
+ EpisodeAirDate time.Time `json:"episode_air_date"`
+ FreeAvailableDate time.Time `json:"free_available_date"`
+ PremiumAvailableDate time.Time `json:"premium_available_date"`
IsSubbed bool `json:"is_subbed"`
IsDubbed bool `json:"is_dubbed"`
@@ -52,8 +56,9 @@ type Episode struct {
SeoDescription string `json:"seo_description"`
SeasonTags []string `json:"season_tags"`
- AvailableOffline bool `json:"available_offline"`
- Slug string `json:"slug"`
+ AvailableOffline bool `json:"available_offline"`
+ MediaType MediaType `json:"media_type"`
+ Slug string `json:"slug"`
Images struct {
Thumbnail [][]struct {
@@ -87,6 +92,62 @@ type HistoryEpisode struct {
FullyWatched bool `json:"fully_watched"`
}
+// WatchlistEntryType specifies which type a watchlist entry has.
+type WatchlistEntryType string
+
+const (
+ WATCHLISTENTRYEPISODE = "episode"
+ WATCHLISTENTRYSERIES = "series"
+)
+
+// WatchlistEntry contains information about an entry on the watchlist.
+type WatchlistEntry struct {
+ Panel struct {
+ Title string `json:"title"`
+ PromoTitle string `json:"promo_title"`
+ Slug string `json:"slug"`
+ Playback string `json:"playback"`
+ PromoDescription string `json:"promo_description"`
+ Images struct {
+ Thumbnail [][]struct {
+ Height int `json:"height"`
+ Source string `json:"source"`
+ Type string `json:"type"`
+ Width int `json:"width"`
+ } `json:"thumbnail"`
+ PosterTall [][]struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"poster_tall"`
+ PosterWide [][]struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"poster_wide"`
+ } `json:"images"`
+ ID string `json:"id"`
+ Description string `json:"description"`
+ ChannelID string `json:"channel_id"`
+ Type WatchlistEntryType `json:"type"`
+ ExternalID string `json:"external_id"`
+ SlugTitle string `json:"slug_title"`
+ // not null if Type is WATCHLISTENTRYEPISODE
+ EpisodeMetadata *Episode `json:"episode_metadata"`
+ // not null if Type is WATCHLISTENTRYSERIES
+ SeriesMetadata *Series `json:"series_metadata"`
+ }
+
+ New bool `json:"new"`
+ NewContent bool `json:"new_content"`
+ IsFavorite bool `json:"is_favorite"`
+ NeverWatched bool `json:"never_watched"`
+ CompleteStatus bool `json:"complete_status"`
+ Playahead uint `json:"playahead"`
+}
+
// EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
@@ -122,6 +183,30 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
return episode, nil
}
+// AddToWatchlist adds the current episode to the watchlist.
+// There is currently a bug, or as I like to say in context of the crunchyroll api, feature,
+// that only series and not individual episode can be added to the watchlist. Even though
+// I somehow got an episode to my watchlist on the crunchyroll website, it never worked with the
+// api here. So this function actually adds the whole series to the watchlist.
+func (e *Episode) AddToWatchlist() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", e.crunchy.Config.AccountID, e.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"content_id": e.SeriesID})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = e.crunchy.requestFull(req)
+ return err
+}
+
+// RemoveFromWatchlist removes the current episode from the watchlist.
+func (e *Episode) RemoveFromWatchlist() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", e.crunchy.Config.AccountID, e.SeriesID, e.crunchy.Locale)
+ _, err := e.crunchy.request(endpoint, http.MethodDelete)
+ return err
+}
+
// AudioLocale returns the audio locale of the episode.
// Every episode in a season (should) have the same audio locale,
// so if you want to get the audio locale of a season, just call
diff --git a/video.go b/video.go
index 365f954..42efacd 100644
--- a/video.go
+++ b/video.go
@@ -1,6 +1,7 @@
package crunchyroll
import (
+ "bytes"
"encoding/json"
"fmt"
"net/http"
@@ -193,6 +194,26 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
return series, nil
}
+// AddToWatchlist adds the current episode to the watchlist.
+func (s *Series) AddToWatchlist() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", s.crunchy.Config.AccountID, s.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"content_id": s.ID})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = s.crunchy.requestFull(req)
+ return err
+}
+
+// RemoveFromWatchlist removes the current episode from the watchlist.
+func (s *Series) RemoveFromWatchlist() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", s.crunchy.Config.AccountID, s.ID, s.crunchy.Locale)
+ _, err := s.crunchy.request(endpoint, http.MethodDelete)
+ return err
+}
+
// Seasons returns all seasons of a series.
func (s *Series) Seasons() (seasons []*Season, err error) {
if s.children != nil {
From 5709012dfe5f625426ba28b99b52720a6ff32b8f Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 00:36:27 +0200
Subject: [PATCH 211/732] Add custom error for internal request
---
crunchyroll.go | 10 +++++-----
episode.go | 9 +++++----
error.go | 17 +++++++++++++++++
video.go | 2 ++
4 files changed, 29 insertions(+), 9 deletions(-)
create mode 100644 error.go
diff --git a/crunchyroll.go b/crunchyroll.go
index 7618fdf..4c29ab4 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -314,25 +314,25 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
var errMap map[string]any
if err = json.Unmarshal(buf.Bytes(), &errMap); err != nil {
- return nil, fmt.Errorf("invalid json response: %w", err)
+ return nil, &RequestError{Response: resp, Message: fmt.Sprintf("invalid json response: %w", err)}
}
if val, ok := errMap["error"]; ok {
if errorAsString, ok := val.(string); ok {
if code, ok := errMap["code"].(string); ok {
- return nil, fmt.Errorf("error for endpoint %s (%d): %s - %s", req.URL.String(), resp.StatusCode, errorAsString, code)
+ return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", errorAsString, code)}
}
- return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, errorAsString)
+ return nil, &RequestError{Response: resp, Message: errorAsString}
} else if errorAsBool, ok := val.(bool); ok && errorAsBool {
if msg, ok := errMap["message"].(string); ok {
- return nil, fmt.Errorf("error for endpoint %s (%d): %s", req.URL.String(), resp.StatusCode, msg)
+ return nil, &RequestError{Response: resp, Message: msg}
}
}
}
}
if resp.StatusCode >= 400 {
- return nil, fmt.Errorf("error for endpoint %s: %s", req.URL.String(), resp.Status)
+ return nil, &RequestError{Response: resp, Message: resp.Status}
}
}
return resp, err
diff --git a/episode.go b/episode.go
index e99bd12..d0e9b14 100644
--- a/episode.go
+++ b/episode.go
@@ -184,10 +184,10 @@ func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
}
// AddToWatchlist adds the current episode to the watchlist.
-// There is currently a bug, or as I like to say in context of the crunchyroll api, feature,
-// that only series and not individual episode can be added to the watchlist. Even though
-// I somehow got an episode to my watchlist on the crunchyroll website, it never worked with the
-// api here. So this function actually adds the whole series to the watchlist.
+// Will return an RequestError with the response status code of 409 if the series was already on the watchlist before.
+// There is currently a bug, or as I like to say in context of the crunchyroll api, feature, that only series and not
+// individual episode can be added to the watchlist. Even though I somehow got an episode to my watchlist on the
+// crunchyroll website, it never worked with the api here. So this function actually adds the whole series to the watchlist.
func (e *Episode) AddToWatchlist() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", e.crunchy.Config.AccountID, e.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"content_id": e.SeriesID})
@@ -201,6 +201,7 @@ func (e *Episode) AddToWatchlist() error {
}
// RemoveFromWatchlist removes the current episode from the watchlist.
+// Will return an RequestError with the response status code of 404 if the series was not on the watchlist before.
func (e *Episode) RemoveFromWatchlist() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", e.crunchy.Config.AccountID, e.SeriesID, e.crunchy.Locale)
_, err := e.crunchy.request(endpoint, http.MethodDelete)
diff --git a/error.go b/error.go
new file mode 100644
index 0000000..b4f7329
--- /dev/null
+++ b/error.go
@@ -0,0 +1,17 @@
+package crunchyroll
+
+import (
+ "fmt"
+ "net/http"
+)
+
+type RequestError struct {
+ error
+
+ Response *http.Response
+ Message string
+}
+
+func (re *RequestError) String() string {
+ return fmt.Sprintf("error for endpoint %s (%d): %s", re.Response.Request.URL.String(), re.Response.StatusCode, re.Message)
+}
diff --git a/video.go b/video.go
index 42efacd..3e7d7cf 100644
--- a/video.go
+++ b/video.go
@@ -195,6 +195,7 @@ func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
}
// AddToWatchlist adds the current episode to the watchlist.
+// Will return an RequestError with the response status code of 409 if the series was already on the watchlist before.
func (s *Series) AddToWatchlist() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", s.crunchy.Config.AccountID, s.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"content_id": s.ID})
@@ -208,6 +209,7 @@ func (s *Series) AddToWatchlist() error {
}
// RemoveFromWatchlist removes the current episode from the watchlist.
+// Will return an RequestError with the response status code of 404 if the series was not on the watchlist before.
func (s *Series) RemoveFromWatchlist() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", s.crunchy.Config.AccountID, s.ID, s.crunchy.Locale)
_, err := s.crunchy.request(endpoint, http.MethodDelete)
From f9792aa84781b893c9b589202caacd7325cdb347 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 00:54:30 +0200
Subject: [PATCH 212/732] Add extra file for common in different places used
elements
---
common.go | 39 +++++++++++++++++++++++++++++++++++++++
episode.go | 38 +-------------------------------------
2 files changed, 40 insertions(+), 37 deletions(-)
create mode 100644 common.go
diff --git a/common.go b/common.go
new file mode 100644
index 0000000..91e1312
--- /dev/null
+++ b/common.go
@@ -0,0 +1,39 @@
+package crunchyroll
+
+type Panel struct {
+ Title string `json:"title"`
+ PromoTitle string `json:"promo_title"`
+ Slug string `json:"slug"`
+ Playback string `json:"playback"`
+ PromoDescription string `json:"promo_description"`
+ Images struct {
+ Thumbnail [][]struct {
+ Height int `json:"height"`
+ Source string `json:"source"`
+ Type string `json:"type"`
+ Width int `json:"width"`
+ } `json:"thumbnail"`
+ PosterTall [][]struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"poster_tall"`
+ PosterWide [][]struct {
+ Width int `json:"width"`
+ Height int `json:"height"`
+ Type string `json:"type"`
+ Source string `json:"source"`
+ } `json:"poster_wide"`
+ } `json:"images"`
+ ID string `json:"id"`
+ Description string `json:"description"`
+ ChannelID string `json:"channel_id"`
+ Type WatchlistEntryType `json:"type"`
+ ExternalID string `json:"external_id"`
+ SlugTitle string `json:"slug_title"`
+ // not null if Type is WATCHLISTENTRYEPISODE
+ EpisodeMetadata *Episode `json:"episode_metadata"`
+ // not null if Type is WATCHLISTENTRYSERIES
+ SeriesMetadata *Series `json:"series_metadata"`
+}
diff --git a/episode.go b/episode.go
index d0e9b14..f97de31 100644
--- a/episode.go
+++ b/episode.go
@@ -102,43 +102,7 @@ const (
// WatchlistEntry contains information about an entry on the watchlist.
type WatchlistEntry struct {
- Panel struct {
- Title string `json:"title"`
- PromoTitle string `json:"promo_title"`
- Slug string `json:"slug"`
- Playback string `json:"playback"`
- PromoDescription string `json:"promo_description"`
- Images struct {
- Thumbnail [][]struct {
- Height int `json:"height"`
- Source string `json:"source"`
- Type string `json:"type"`
- Width int `json:"width"`
- } `json:"thumbnail"`
- PosterTall [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"poster_tall"`
- PosterWide [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"poster_wide"`
- } `json:"images"`
- ID string `json:"id"`
- Description string `json:"description"`
- ChannelID string `json:"channel_id"`
- Type WatchlistEntryType `json:"type"`
- ExternalID string `json:"external_id"`
- SlugTitle string `json:"slug_title"`
- // not null if Type is WATCHLISTENTRYEPISODE
- EpisodeMetadata *Episode `json:"episode_metadata"`
- // not null if Type is WATCHLISTENTRYSERIES
- SeriesMetadata *Series `json:"series_metadata"`
- }
+ Panel Panel `json:"panel"`
New bool `json:"new"`
NewContent bool `json:"new_content"`
From 8ddb436fac31628aa016e29600fe0cef4f523f61 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 01:03:10 +0200
Subject: [PATCH 213/732] Move WatchlistEntry struct to own file
---
episode.go | 12 ------------
watchlist.go | 13 +++++++++++++
2 files changed, 13 insertions(+), 12 deletions(-)
create mode 100644 watchlist.go
diff --git a/episode.go b/episode.go
index f97de31..7f48e93 100644
--- a/episode.go
+++ b/episode.go
@@ -100,18 +100,6 @@ const (
WATCHLISTENTRYSERIES = "series"
)
-// WatchlistEntry contains information about an entry on the watchlist.
-type WatchlistEntry struct {
- Panel Panel `json:"panel"`
-
- New bool `json:"new"`
- NewContent bool `json:"new_content"`
- IsFavorite bool `json:"is_favorite"`
- NeverWatched bool `json:"never_watched"`
- CompleteStatus bool `json:"complete_status"`
- Playahead uint `json:"playahead"`
-}
-
// EpisodeFromID returns an episode by its api id.
func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/%s/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
diff --git a/watchlist.go b/watchlist.go
new file mode 100644
index 0000000..73fbb3d
--- /dev/null
+++ b/watchlist.go
@@ -0,0 +1,13 @@
+package crunchyroll
+
+// WatchlistEntry contains information about an entry on the watchlist.
+type WatchlistEntry struct {
+ Panel Panel `json:"panel"`
+
+ New bool `json:"new"`
+ NewContent bool `json:"new_content"`
+ IsFavorite bool `json:"is_favorite"`
+ NeverWatched bool `json:"never_watched"`
+ CompleteStatus bool `json:"complete_status"`
+ Playahead uint `json:"playahead"`
+}
From aa088cb318afd60deed167356a2d8938cacccad9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 01:42:57 +0200
Subject: [PATCH 214/732] Fix error printing caused panic
---
error.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/error.go b/error.go
index b4f7329..9bc2ae6 100644
--- a/error.go
+++ b/error.go
@@ -12,6 +12,6 @@ type RequestError struct {
Message string
}
-func (re *RequestError) String() string {
+func (re *RequestError) Error() string {
return fmt.Sprintf("error for endpoint %s (%d): %s", re.Response.Request.URL.String(), re.Response.StatusCode, re.Message)
}
From 72484c78af9ec33060e7f5c5b8f64f8625683e69 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 01:56:39 +0200
Subject: [PATCH 215/732] Add crunchylists endpoint
---
crunchylists.go | 148 ++++++++++++++++++++++++++++++++++++++++++++++++
crunchyroll.go | 19 +++++++
2 files changed, 167 insertions(+)
create mode 100644 crunchylists.go
diff --git a/crunchylists.go b/crunchylists.go
new file mode 100644
index 0000000..1117dd7
--- /dev/null
+++ b/crunchylists.go
@@ -0,0 +1,148 @@
+package crunchyroll
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+type CrunchyLists struct {
+ crunchy *Crunchyroll
+
+ Items []*CrunchyListPreview `json:"items"`
+ TotalPublic int `json:"total_public"`
+ TotalPrivate int `json:"total_private"`
+ MaxPrivate int `json:"max_private"`
+}
+
+func (cl *CrunchyLists) Create(name string) (*CrunchyList, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", cl.crunchy.Config.AccountID, cl.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"title": name})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ resp, err := cl.crunchy.requestFull(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ return CrunchyListFromID(cl.crunchy, jsonBody["list_id"].(string))
+}
+
+type CrunchyListPreview struct {
+ crunchy *Crunchyroll
+
+ ListID string `json:"list_id"`
+ IsPublic bool `json:"is_public"`
+ Total int `json:"total"`
+ ModifiedAt time.Time `json:"modified_at"`
+ Title string `json:"title"`
+}
+
+func (clp *CrunchyListPreview) CrunchyList() (*CrunchyList, error) {
+ return CrunchyListFromID(clp.crunchy, clp.ListID)
+}
+
+func CrunchyListFromID(crunchy *Crunchyroll, id string) (*CrunchyList, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", crunchy.Config.AccountID, id, crunchy.Locale)
+ resp, err := crunchy.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ crunchyList := &CrunchyList{
+ crunchy: crunchy,
+ ID: id,
+ }
+ if err := json.NewDecoder(resp.Body).Decode(crunchyList); err != nil {
+ return nil, err
+ }
+ for _, item := range crunchyList.Items {
+ item.crunchy = crunchy
+ }
+ return crunchyList, nil
+}
+
+type CrunchyList struct {
+ crunchy *Crunchyroll
+
+ ID string `json:"id"`
+
+ Max int `json:"max"`
+ Total int `json:"total"`
+ Title string `json:"title"`
+ IsPublic bool `json:"is_public"`
+ ModifiedAt time.Time `json:"modified_at"`
+ Items []*CrunchyListItem `json:"items"`
+}
+
+func (cl *CrunchyList) AddSeries(series *Series) error {
+ return cl.AddSeriesFromID(series.ID)
+}
+
+func (cl *CrunchyList) AddSeriesFromID(id string) error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"content_id": id})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = cl.crunchy.requestFull(req)
+ return err
+}
+
+func (cl *CrunchyList) RemoveSeries(series *Series) error {
+ return cl.RemoveSeriesFromID(series.ID)
+}
+
+func (cl *CrunchyList) RemoveSeriesFromID(id string) error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, id, cl.crunchy.Locale)
+ _, err := cl.crunchy.request(endpoint, http.MethodDelete)
+ return err
+}
+
+func (cl *CrunchyList) Delete() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
+ _, err := cl.crunchy.request(endpoint, http.MethodDelete)
+ return err
+}
+
+func (cl *CrunchyList) Rename(name string) error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"title": name})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = cl.crunchy.requestFull(req)
+ if err == nil {
+ cl.Title = name
+ }
+ return err
+}
+
+type CrunchyListItem struct {
+ crunchy *Crunchyroll
+
+ ListID string `json:"list_id"`
+ ID string `json:"id"`
+ ModifiedAt time.Time `json:"modified_at"`
+ Panel Panel `json:"panel"`
+}
+
+func (cli *CrunchyListItem) Remove() error {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s", cli.crunchy.Config.AccountID, cli.ListID, cli.ID)
+ _, err := cli.crunchy.request(endpoint, http.MethodDelete)
+ return err
+}
diff --git a/crunchyroll.go b/crunchyroll.go
index 4c29ab4..b9593f3 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -893,6 +893,25 @@ func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*Watchl
return watchlistEntries, nil
}
+func (c *Crunchyroll) CrunchyLists() (*CrunchyLists, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
+ resp, err := c.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ crunchyLists := &CrunchyLists{
+ crunchy: c,
+ }
+ json.NewDecoder(resp.Body).Decode(crunchyLists)
+ for _, item := range crunchyLists.Items {
+ item.crunchy = c
+ }
+
+ return crunchyLists, nil
+}
+
// Account returns information about the currently logged in crunchyroll account.
func (c *Crunchyroll) Account() (*Account, error) {
resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
From 9f6a225caf58dc9b8f35133cc2c9fb4a1af71c94 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 02:00:58 +0200
Subject: [PATCH 216/732] Add better error output
---
crunchyroll.go | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index b9593f3..3d07252 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -328,6 +328,11 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
return nil, &RequestError{Response: resp, Message: msg}
}
}
+ } else if _, ok := errMap["code"]; ok {
+ if errContext, ok := errMap["context"]; ok {
+ errField := errContext.([]any)[0].(map[string]any)
+ return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", errField["code"].(string), errField["field"].(string))}
+ }
}
}
From c86595d2c6f3a6d07d7798814b074beba13c47c2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 02:28:03 +0200
Subject: [PATCH 217/732] Add image to common structs
---
common.go | 28 ++++++++++------------------
episode.go | 7 +------
movie_listing.go | 7 +------
video.go | 14 ++------------
4 files changed, 14 insertions(+), 42 deletions(-)
diff --git a/common.go b/common.go
index 91e1312..b76a8fe 100644
--- a/common.go
+++ b/common.go
@@ -1,5 +1,12 @@
package crunchyroll
+type Image struct {
+ Height int `json:"height"`
+ Source string `json:"source"`
+ Type string `json:"type"`
+ Width int `json:"width"`
+}
+
type Panel struct {
Title string `json:"title"`
PromoTitle string `json:"promo_title"`
@@ -7,24 +14,9 @@ type Panel struct {
Playback string `json:"playback"`
PromoDescription string `json:"promo_description"`
Images struct {
- Thumbnail [][]struct {
- Height int `json:"height"`
- Source string `json:"source"`
- Type string `json:"type"`
- Width int `json:"width"`
- } `json:"thumbnail"`
- PosterTall [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"poster_tall"`
- PosterWide [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"poster_wide"`
+ Thumbnail [][]Image `json:"thumbnail"`
+ PosterTall [][]Image `json:"poster_tall"`
+ PosterWide [][]Image `json:"poster_wide"`
} `json:"images"`
ID string `json:"id"`
Description string `json:"description"`
diff --git a/episode.go b/episode.go
index 7f48e93..7d9caac 100644
--- a/episode.go
+++ b/episode.go
@@ -61,12 +61,7 @@ type Episode struct {
Slug string `json:"slug"`
Images struct {
- Thumbnail [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"thumbnail"`
+ Thumbnail [][]Image `json:"thumbnail"`
} `json:"images"`
DurationMS int `json:"duration_ms"`
diff --git a/movie_listing.go b/movie_listing.go
index 110f67a..81cb48d 100644
--- a/movie_listing.go
+++ b/movie_listing.go
@@ -19,12 +19,7 @@ type MovieListing struct {
Description string `json:"description"`
Images struct {
- Thumbnail [][]struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"thumbnail"`
+ Thumbnail [][]Image `json:"thumbnail"`
} `json:"images"`
DurationMS int `json:"duration_ms"`
diff --git a/video.go b/video.go
index 3e7d7cf..3a67f5c 100644
--- a/video.go
+++ b/video.go
@@ -17,18 +17,8 @@ type video struct {
SlugTitle string `json:"slug_title"`
Images struct {
- PosterTall [][]struct {
- Height int `json:"height"`
- Source string `json:"source"`
- Type string `json:"type"`
- Width int `json:"width"`
- } `json:"poster_tall"`
- PosterWide [][]struct {
- Height int `json:"height"`
- Source string `json:"source"`
- Type string `json:"type"`
- Width int `json:"width"`
- } `json:"poster_wide"`
+ PosterTall [][]Image `json:"poster_tall"`
+ PosterWide [][]Image `json:"poster_wide"`
} `json:"images"`
}
From 475dc34f7af0ab57de8def3b0489a2030f9983f9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 13:31:29 +0200
Subject: [PATCH 218/732] Fix error handling caused panic
---
crunchyroll.go | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 3d07252..0544c70 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -331,7 +331,11 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
} else if _, ok := errMap["code"]; ok {
if errContext, ok := errMap["context"]; ok {
errField := errContext.([]any)[0].(map[string]any)
- return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", errField["code"].(string), errField["field"].(string))}
+ var code string
+ if code, ok = errField["message"].(string); !ok {
+ code = errField["code"].(string)
+ }
+ return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", code, errField["field"].(string))}
}
}
}
From 0c93893627e4f0906f9de4c9158514bf2fc06016 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 13:38:41 +0200
Subject: [PATCH 219/732] Fix error handling caused panic (again)
---
crunchyroll.go | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 0544c70..8b4fad8 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -329,13 +329,15 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
}
}
} else if _, ok := errMap["code"]; ok {
- if errContext, ok := errMap["context"]; ok {
- errField := errContext.([]any)[0].(map[string]any)
+ if errContext, ok := errMap["context"].([]any); ok && len(errContext) > 0 {
+ errField := errContext[0].(map[string]any)
var code string
if code, ok = errField["message"].(string); !ok {
code = errField["code"].(string)
}
return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", code, errField["field"].(string))}
+ } else if errMessage, ok := errMap["message"].(string); ok {
+ return nil, &RequestError{Response: resp, Message: errMessage}
}
}
}
From cee34105327856a37b5619c67615b1b2cb6a8445 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 19 Jun 2022 14:43:46 +0200
Subject: [PATCH 220/732] Add comment endpoint
---
comment.go | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++
episode.go | 80 ++++++++++++++++
utils.go | 12 +++
3 files changed, 354 insertions(+)
create mode 100644 comment.go
diff --git a/comment.go b/comment.go
new file mode 100644
index 0000000..4dba10d
--- /dev/null
+++ b/comment.go
@@ -0,0 +1,262 @@
+package crunchyroll
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+type Comment struct {
+ crunchy *Crunchyroll
+
+ EpisodeID string `json:"episode_id"`
+
+ CommentID string `json:"comment_id"`
+ DomainID string `json:"domain_id"`
+
+ GuestbookKey string `json:"guestbook_key"`
+
+ User struct {
+ UserKey string `json:"user_key"`
+ UserAttributes struct {
+ Username string `json:"username"`
+ Avatar struct {
+ Locked []Image `json:"locked"`
+ Unlocked []Image `json:"unlocked"`
+ } `json:"avatar"`
+ } `json:"user_attributes"`
+ UserFlags []any `json:"user_flags"`
+ } `json:"user"`
+
+ Message string `json:"message"`
+ ParentCommentID int `json:"parent_comment_id"`
+
+ Locale LOCALE `json:"locale"`
+
+ UserVotes []string `json:"user_votes"`
+ Flags []string `json:"flags"`
+ Votes struct {
+ Inappropriate int `json:"inappropriate"`
+ Like int `json:"like"`
+ Spoiler int `json:"spoiler"`
+ } `json:"votes"`
+
+ DeleteReason any `json:"delete_reason"`
+
+ Created time.Time `json:"created"`
+ Modified time.Time `json:"modified"`
+
+ IsOwner bool `json:"is_owner"`
+ RepliesCount int `json:"replies_count"`
+}
+
+// Delete deleted the current comment. Works only if the user has written the comment.
+func (c *Comment) Delete() error {
+ if !c.IsOwner {
+ return fmt.Errorf("cannot delete, user is not the comment author")
+ }
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
+ resp, err := c.crunchy.request(endpoint, http.MethodDelete)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ // the api returns a new comment object when modifying it.
+ // hopefully this does not change
+ json.NewDecoder(resp.Body).Decode(c)
+
+ return nil
+}
+
+// MarkAsSpoiler marks the current comment as spoiler. Works only if the user has written the comment,
+// and it isn't already marked as spoiler.
+func (c *Comment) MarkAsSpoiler() error {
+ if !c.IsOwner {
+ return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
+ } else if c.markedAs("spoiler") {
+ return fmt.Errorf("comment is already marked as spoiler")
+ }
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
+ body, _ := json.Marshal(map[string][]string{"add": {"spoiler"}})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ resp, err := c.crunchy.requestFull(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ json.NewDecoder(resp.Body).Decode(c)
+
+ return nil
+}
+
+// UnmarkAsSpoiler unmarks the current comment as spoiler. Works only if the user has written the comment,
+// and it is already marked as spoiler.
+func (c *Comment) UnmarkAsSpoiler() error {
+ if !c.IsOwner {
+ return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
+ } else if !c.markedAs("spoiler") {
+ return fmt.Errorf("comment is not marked as spoiler")
+ }
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
+ body, _ := json.Marshal(map[string][]string{"remove": {"spoiler"}})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ resp, err := c.crunchy.requestFull(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ json.NewDecoder(resp.Body).Decode(c)
+
+ return nil
+}
+
+// Like likes the comment. Works only if the user hasn't already liked it.
+func (c *Comment) Like() error {
+ if err := c.vote("like", "liked"); err != nil {
+ return err
+ }
+ c.Votes.Like += 1
+
+ return nil
+}
+
+// RemoveLike removes the like from the comment. Works only if the user has liked it.
+func (c *Comment) RemoveLike() error {
+ if err := c.unVote("like", "liked"); err != nil {
+ return err
+ }
+ c.Votes.Like -= 1
+
+ return nil
+}
+
+// Reply replies to the current comment.
+func (c *Comment) Reply(message string, spoiler bool) (*Comment, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", c.EpisodeID, c.crunchy.Locale)
+ var flags []string
+ if spoiler {
+ flags = append(flags, "spoiler")
+ }
+ body, _ := json.Marshal(map[string]any{"locale": string(c.crunchy.Locale), "message": message, "flags": flags, "parent_id": c.CommentID})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ resp, err := c.crunchy.requestFull(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ reply := &Comment{}
+ if err = json.NewDecoder(resp.Body).Decode(reply); err != nil {
+ return nil, err
+ }
+
+ return reply, nil
+}
+
+// Replies shows all replies to the current comment.
+func (c *Comment) Replies(page uint, size uint) ([]*Comment, error) {
+ if c.RepliesCount == 0 {
+ return []*Comment{}, nil
+ }
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/replies?page_size=%d&page=%d&locale=%s", c.EpisodeID, c.CommentID, size, page, c.Locale)
+ resp, err := c.crunchy.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]any
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ var comments []*Comment
+ if err = decodeMapToStruct(jsonBody["items"].([]any), &comments); err != nil {
+ return nil, err
+ }
+ return comments, nil
+}
+
+// Report reports the comment. Only works if the comment hasn't been reported yet.
+func (c *Comment) Report() error {
+ return c.vote("inappropriate", "reported")
+}
+
+// UnreportComment removes the report request from the comment. Only works if the user
+// has reported the comment.
+func (c *Comment) UnreportComment() error {
+ return c.unVote("inappropriate", "reported")
+}
+
+// FlagAsSpoiler sends a request to the user (and / or crunchyroll?) to mark the comment
+// as spoiler. Only works if the comment hasn't been flagged as spoiler yet.
+func (c *Comment) FlagAsSpoiler() error {
+ return c.vote("spoiler", "spoiler")
+}
+
+// UnflagAsSpoiler rewokes the request to the user (and / or crunchyroll?) to mark the
+// comment as spoiler. Only works if the user has flagged the comment as spoiler.
+func (c *Comment) UnflagAsSpoiler() error {
+ return c.unVote("spoiler", "spoiler")
+}
+
+func (c *Comment) markedAs(voteType string) bool {
+ for _, userVote := range c.UserVotes {
+ if userVote == voteType {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *Comment) vote(voteType, readableName string) error {
+ if c.markedAs(voteType) {
+ return fmt.Errorf("comment is already marked as %s", readableName)
+ }
+
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
+ body, _ := json.Marshal(map[string]string{"vote_type": voteType})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = c.crunchy.requestFull(req)
+ if err != nil {
+ return err
+ }
+ c.UserVotes = append(c.UserVotes, voteType)
+
+ return nil
+}
+
+func (c *Comment) unVote(voteType, readableName string) error {
+ for i, userVote := range c.UserVotes {
+ if userVote == voteType {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?vote_type=%s&locale=%s", c.EpisodeID, c.CommentID, voteType, c.crunchy.Locale)
+ _, err := c.crunchy.request(endpoint, http.MethodDelete)
+ if err != nil {
+ return err
+ }
+ c.UserVotes = append(c.UserVotes[:i], c.UserVotes[i+1:]...)
+ return nil
+ }
+ }
+
+ return fmt.Errorf("comment is not marked as %s", readableName)
+}
diff --git a/episode.go b/episode.go
index 7d9caac..9e20510 100644
--- a/episode.go
+++ b/episode.go
@@ -167,6 +167,86 @@ func (e *Episode) AudioLocale() (LOCALE, error) {
return streams[0].AudioLocale, nil
}
+// Comment creates a new comment under the episode.
+func (e *Episode) Comment(message string, spoiler bool) (*Comment, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", e.ID, e.crunchy.Locale)
+ var flags []string
+ if spoiler {
+ flags = append(flags, "spoiler")
+ }
+ body, _ := json.Marshal(map[string]any{"locale": string(e.crunchy.Locale), "flags": flags, "message": message})
+ req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ resp, err := e.crunchy.requestFull(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ c := &Comment{
+ crunchy: e.crunchy,
+ EpisodeID: e.ID,
+ }
+ if err = json.NewDecoder(resp.Body).Decode(c); err != nil {
+ return nil, err
+ }
+
+ return c, nil
+}
+
+// CommentsOrderType represents a sort type to sort Episode.Comments after.
+type CommentsOrderType string
+
+const (
+ CommentsOrderAsc CommentsOrderType = "asc"
+ CommentsOrderDesc = "desc"
+)
+
+type CommentsSortType string
+
+const (
+ CommentsSortPopular CommentsSortType = "popular"
+ CommentsSortDate = "date"
+)
+
+type CommentsOptions struct {
+ // Order specified the order how the comments should be returned.
+ Order CommentsOrderType `json:"order"`
+
+ // Sort specified after which key the comments should be sorted.
+ Sort CommentsSortType `json:"sort"`
+}
+
+// Comments returns comments under the given episode.
+func (e *Episode) Comments(options CommentsOptions, page uint, size uint) (c []*Comment, err error) {
+ options, err = structDefaults(CommentsOptions{Order: CommentsOrderDesc, Sort: CommentsSortPopular}, options)
+ if err != nil {
+ return nil, err
+ }
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?page=%d&page_size=%d&order=%s&sort=%s&locale=%s", e.ID, page, size, options.Order, options.Sort, e.crunchy.Locale)
+ resp, err := e.crunchy.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]any
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ if err = decodeMapToStruct(jsonBody["items"].([]any), &c); err != nil {
+ return nil, err
+ }
+ for _, comment := range c {
+ comment.crunchy = e.crunchy
+ comment.EpisodeID = e.ID
+ }
+
+ return
+}
+
// GetFormat returns the format which matches the given resolution and subtitle locale.
func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
streams, err := e.Streams()
diff --git a/utils.go b/utils.go
index d32d39b..a665040 100644
--- a/utils.go
+++ b/utils.go
@@ -1,6 +1,7 @@
package crunchyroll
import (
+ "bytes"
"encoding/json"
"fmt"
"net/url"
@@ -70,3 +71,14 @@ func encodeStructToQueryValues(s interface{}) (string, error) {
return values.Encode(), nil
}
+
+func structDefaults[T any](defaultStruct T, customStruct T) (T, error) {
+ rawDefaultStruct, err := json.Marshal(defaultStruct)
+ if err != nil {
+ return *new(T), err
+ }
+ if err = json.NewDecoder(bytes.NewBuffer(rawDefaultStruct)).Decode(&customStruct); err != nil {
+ return *new(T), err
+ }
+ return customStruct, nil
+}
From 715ade831ca0387e5d45c447201e86bab5389552 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 20 Jun 2022 10:22:17 +0200
Subject: [PATCH 221/732] Add more account and profile endpoints
---
account.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++++-
crunchyroll.go | 4 +-
2 files changed, 117 insertions(+), 3 deletions(-)
diff --git a/account.go b/account.go
index d22eba5..87a6ef8 100644
--- a/account.go
+++ b/account.go
@@ -1,9 +1,16 @@
package crunchyroll
-import "time"
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "time"
+)
// Account contains information about a crunchyroll account.
type Account struct {
+ crunchy *Crunchyroll
+
AccountID string `json:"account_id"`
ExternalID string `json:"external_id"`
EmailVerified bool `json:"email_verified"`
@@ -25,5 +32,110 @@ type Account struct {
PreferredCommunicationLanguage LOCALE `json:"preferred_communication_language"`
PreferredContentSubtitleLanguage LOCALE `json:"preferred_content_subtitle_language"`
QaUser bool `json:"qa_user"`
- Username string `json:"username"`
+
+ Username string `json:"username"`
+ Wallpaper *Wallpaper `json:"wallpaper"`
+}
+
+// UpdateEmailLanguage sets in which language emails should be received.
+func (a *Account) UpdateEmailLanguage(language LOCALE) error {
+ err := a.updatePreferences("preferred_communication_language", string(language))
+ if err == nil {
+ a.PreferredCommunicationLanguage = language
+ }
+ return err
+}
+
+// UpdateVideoSubtitleLanguage sets in which language default subtitles should be shown
+func (a *Account) UpdateVideoSubtitleLanguage(language LOCALE) error {
+ err := a.updatePreferences("preferred_content_subtitle_language", string(language))
+ if err == nil {
+ a.PreferredContentSubtitleLanguage = language
+ }
+ return err
+}
+
+// UpdateMatureVideoContent sets if mature video content / 18+ content should be shown
+func (a *Account) UpdateMatureVideoContent(enabled bool) error {
+ if enabled {
+ return a.updatePreferences("maturity_rating", "M3")
+ } else {
+ return a.updatePreferences("maturity_rating", "M2")
+ }
+}
+
+// UpdateMatureMangaContent sets if mature manga content / 18+ content should be shown
+func (a *Account) UpdateMatureMangaContent(enabled bool) error {
+ if enabled {
+ return a.updatePreferences("mature_content_flag_manga", "1")
+ } else {
+ return a.updatePreferences("mature_content_flag_manga", "0")
+ }
+}
+
+func (a *Account) updatePreferences(name, value string) error {
+ endpoint := "https://beta.crunchyroll.com/accounts/v1/me/profile"
+ body, _ := json.Marshal(map[string]string{name: value})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = a.crunchy.requestFull(req)
+ return err
+}
+
+// ChangePassword changes the password for the current account.
+func (a *Account) ChangePassword(currentPassword, newPassword string) error {
+ endpoint := "https://beta.crunchyroll.com/accounts/v1/me/credentials"
+ body, _ := json.Marshal(map[string]string{"accountId": a.AccountID, "current_password": currentPassword, "new_password": newPassword})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = a.crunchy.requestFull(req)
+ return err
+}
+
+// ChangeEmail changes the email address for the current account.
+func (a *Account) ChangeEmail(currentPassword, newEmail string) error {
+ endpoint := "https://beta.crunchyroll.com/accounts/v1/me/credentials"
+ body, _ := json.Marshal(map[string]string{"current_password": currentPassword, "email": newEmail})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Content-Type", "application/json")
+ _, err = a.crunchy.requestFull(req)
+ return err
+}
+
+// AvailableWallpapers returns all available wallpapers which can be set as profile wallpaper.
+func (a *Account) AvailableWallpapers() (w []*Wallpaper, err error) {
+ endpoint := "https://beta.crunchyroll.com/assets/v1/wallpaper"
+ resp, err := a.crunchy.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]any
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ err = decodeMapToStruct(jsonBody["items"].([]any), &w)
+ return
+}
+
+// ChangeWallpaper changes the profile wallpaper of the current user. Use AvailableWallpapers
+// to get all available ones.
+func (a *Account) ChangeWallpaper(wallpaper *Wallpaper) error {
+ endpoint := "https://beta.crunchyroll.com/accounts/v1/me/profile"
+ body, _ := json.Marshal(map[string]string{"wallpaper": string(*wallpaper)})
+ req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
+ if err != nil {
+ return err
+ }
+ _, err = a.crunchy.requestFull(req)
+ return err
}
diff --git a/crunchyroll.go b/crunchyroll.go
index 8b4fad8..704003d 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -931,7 +931,9 @@ func (c *Crunchyroll) Account() (*Account, error) {
}
defer resp.Body.Close()
- account := &Account{}
+ account := &Account{
+ crunchy: c,
+ }
if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
From 256c97c2b748ed8e871a713b7d4451c9f56b55d2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 20 Jun 2022 10:24:01 +0200
Subject: [PATCH 222/732] Fix Wallpaper type not found
---
wallpaper.go | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 wallpaper.go
diff --git a/wallpaper.go b/wallpaper.go
new file mode 100644
index 0000000..f64cedf
--- /dev/null
+++ b/wallpaper.go
@@ -0,0 +1,4 @@
+package crunchyroll
+
+// Wallpaper contains a wallpaper name which can be set via Account.ChangeWallpaper.
+type Wallpaper string
From d1859b4c25c3f7f627b50f03f0f5202da681144a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 21 Jun 2022 17:43:12 +0200
Subject: [PATCH 223/732] Change const names to make them more readable
---
crunchyroll.go | 26 +++++++++++++-------------
episode.go | 4 ++--
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 704003d..5de3ab2 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -34,32 +34,32 @@ const (
type MediaType string
const (
- SERIES MediaType = "series"
- MOVIELISTING = "movie_listing"
+ MediaTypeSeries MediaType = "series"
+ MediaTypeMovie = "movie_listing"
)
// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after.
type BrowseSortType string
const (
- BROWSESORTPOPULARITY BrowseSortType = "popularity"
- BROWSESORTNEWLYADDED = "newly_added"
- BROWSESORTALPHABETICAL = "alphabetical"
+ BrowseSortPopularity BrowseSortType = "popularity"
+ BrowseSortNewlyAdded = "newly_added"
+ BrowseSortAlphabetical = "alphabetical"
)
// WatchlistLanguageType represents a filter type to filter Crunchyroll.WatchList entries after.
type WatchlistLanguageType int
const (
- WATCHLISTLANGUAGESUBBED WatchlistLanguageType = iota + 1
- WATCHLISTLANGUAGEDUBBED
+ WatchlistLanguageSubbed WatchlistLanguageType = iota + 1
+ WatchlistLanguageDubbed
)
type WatchlistContentType string
const (
- WATCHLISTCONTENTSERIES WatchlistContentType = "series"
- WATCHLISTCONTENTMOVIES = "movie_listing"
+ WatchlistContentSeries WatchlistContentType = "series"
+ WatchlistContentMovies = "movie_listing"
)
type Crunchyroll struct {
@@ -869,9 +869,9 @@ func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*Watchl
values.Set("only_favorites", "true")
}
switch options.LanguageType {
- case WATCHLISTLANGUAGESUBBED:
+ case WatchlistLanguageSubbed:
values.Set("is_subbed", "true")
- case WATCHLISTLANGUAGEDUBBED:
+ case WatchlistLanguageDubbed:
values.Set("is_dubbed", "true")
}
values.Set("n", strconv.Itoa(int(limit)))
@@ -894,9 +894,9 @@ func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*Watchl
for _, entry := range watchlistEntries {
switch entry.Panel.Type {
- case WATCHLISTENTRYEPISODE:
+ case WatchlistEntryEpisode:
entry.Panel.EpisodeMetadata.crunchy = c
- case WATCHLISTENTRYSERIES:
+ case WatchlistEntrySeries:
entry.Panel.SeriesMetadata.crunchy = c
}
}
diff --git a/episode.go b/episode.go
index 9e20510..b02280d 100644
--- a/episode.go
+++ b/episode.go
@@ -91,8 +91,8 @@ type HistoryEpisode struct {
type WatchlistEntryType string
const (
- WATCHLISTENTRYEPISODE = "episode"
- WATCHLISTENTRYSERIES = "series"
+ WatchlistEntryEpisode = "episode"
+ WatchlistEntrySeries = "series"
)
// EpisodeFromID returns an episode by its api id.
From ec872d8c86dbfca2757f8ccaa0fa754e21c13703 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 21 Jun 2022 21:15:49 +0200
Subject: [PATCH 224/732] Move functions into their own, separate files & add
docs
---
account.go | 30 +++
category.go | 48 +++-
crunchylists.go | 19 ++
crunchyroll.go | 624 -----------------------------------------------
news.go | 46 +++-
parse.go | 69 ++++++
search.go | 193 +++++++++++++++
simulcast.go | 32 +++
suggestions.go | 82 +++++++
video.go | 43 ++++
watch_history.go | 45 ++++
watchlist.go | 88 +++++++
12 files changed, 681 insertions(+), 638 deletions(-)
create mode 100644 parse.go
create mode 100644 search.go
create mode 100644 suggestions.go
create mode 100644 watch_history.go
diff --git a/account.go b/account.go
index 87a6ef8..f2d6a70 100644
--- a/account.go
+++ b/account.go
@@ -3,10 +3,40 @@ package crunchyroll
import (
"bytes"
"encoding/json"
+ "fmt"
"net/http"
"time"
)
+// Account returns information about the currently logged in crunchyroll account.
+func (c *Crunchyroll) Account() (*Account, error) {
+ resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ account := &Account{
+ crunchy: c,
+ }
+
+ if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
+ return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
+ }
+
+ resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile", http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
+ return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
+ }
+
+ return account, nil
+}
+
// Account contains information about a crunchyroll account.
type Account struct {
crunchy *Crunchyroll
diff --git a/category.go b/category.go
index 58855fc..eccefff 100644
--- a/category.go
+++ b/category.go
@@ -1,5 +1,38 @@
package crunchyroll
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// Categories returns all available categories and possible subcategories.
+func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
+ tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
+ includeSubcategories, c.Locale)
+ resp, err := c.request(tenantCategoriesEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'tenant_categories' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ category := &Category{}
+ if err := decodeMapToStruct(item, category); err != nil {
+ return nil, err
+ }
+
+ ca = append(ca, category)
+ }
+
+ return ca, nil
+}
+
// Category contains all information about a category.
type Category struct {
Category string `json:"tenant_category"`
@@ -18,19 +51,8 @@ type Category struct {
} `json:"sub_categories"`
Images struct {
- Background []struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"background"`
-
- Low []struct {
- Width int `json:"width"`
- Height int `json:"height"`
- Type string `json:"type"`
- Source string `json:"source"`
- } `json:"low"`
+ Background []Image `json:"background"`
+ Low []Image `json:"low"`
} `json:"images"`
Localization struct {
diff --git a/crunchylists.go b/crunchylists.go
index 1117dd7..a2a7a1f 100644
--- a/crunchylists.go
+++ b/crunchylists.go
@@ -8,6 +8,25 @@ import (
"time"
)
+func (c *Crunchyroll) CrunchyLists() (*CrunchyLists, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
+ resp, err := c.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ crunchyLists := &CrunchyLists{
+ crunchy: c,
+ }
+ json.NewDecoder(resp.Body).Decode(crunchyLists)
+ for _, item := range crunchyLists.Items {
+ item.crunchy = c
+ }
+
+ return crunchyLists, nil
+}
+
type CrunchyLists struct {
crunchy *Crunchyroll
diff --git a/crunchyroll.go b/crunchyroll.go
index 5de3ab2..a83415c 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -8,8 +8,6 @@ import (
"io"
"net/http"
"net/url"
- "regexp"
- "strconv"
"strings"
)
@@ -38,30 +36,6 @@ const (
MediaTypeMovie = "movie_listing"
)
-// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after.
-type BrowseSortType string
-
-const (
- BrowseSortPopularity BrowseSortType = "popularity"
- BrowseSortNewlyAdded = "newly_added"
- BrowseSortAlphabetical = "alphabetical"
-)
-
-// WatchlistLanguageType represents a filter type to filter Crunchyroll.WatchList entries after.
-type WatchlistLanguageType int
-
-const (
- WatchlistLanguageSubbed WatchlistLanguageType = iota + 1
- WatchlistLanguageDubbed
-)
-
-type WatchlistContentType string
-
-const (
- WatchlistContentSeries WatchlistContentType = "series"
- WatchlistContentMovies = "movie_listing"
-)
-
type Crunchyroll struct {
// Client is the http.Client to perform all requests over.
Client *http.Client
@@ -95,30 +69,6 @@ type Crunchyroll struct {
cache bool
}
-// BrowseOptions represents options for browsing the crunchyroll catalog.
-type BrowseOptions struct {
- // Categories specifies the categories of the entries.
- Categories []string `param:"categories"`
-
- // IsDubbed specifies whether the entries should be dubbed.
- IsDubbed bool `param:"is_dubbed"`
-
- // IsSubbed specifies whether the entries should be subbed.
- IsSubbed bool `param:"is_subbed"`
-
- // Simulcast specifies a particular simulcast season by id in which the entries have been aired.
- Simulcast string `param:"season_tag"`
-
- // Sort specifies how the entries should be sorted.
- Sort BrowseSortType `param:"sort_by"`
-
- // Start specifies the index from which the entries should be returned.
- Start uint `param:"start"`
-
- // Type specifies the media type of the entries.
- Type MediaType `param:"type"`
-}
-
type loginResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
@@ -377,577 +327,3 @@ func (c *Crunchyroll) IsCaching() bool {
func (c *Crunchyroll) SetCaching(caching bool) {
c.cache = caching
}
-
-// Search searches a query and returns all found series and movies within the given limit.
-func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
- searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
- query, limit, c.Locale)
- resp, err := c.request(searchEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'search' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- item := item.(map[string]interface{})
- if item["total"].(float64) > 0 {
- switch item["type"] {
- case "series":
- for _, series := range item["items"].([]interface{}) {
- series2 := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(series, series2); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(series.(map[string]interface{})["series_metadata"].(map[string]interface{}), series2); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series2)
- }
- case "movie_listing":
- for _, movie := range item["items"].([]interface{}) {
- movie2 := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(movie, movie2); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie2)
- }
- }
- }
- }
-
- return s, m, nil
-}
-
-// FindVideoByName finds a Video (Season or Movie) by its name.
-// Use this in combination with ParseVideoURL and hand over the corresponding results
-// to this function.
-//
-// Deprecated: Use Search instead. The first result sometimes isn't the correct one
-// so this function is inaccurate in some cases.
-// See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information.
-func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
- s, m, err := c.Search(seriesName, 1)
- if err != nil {
- return nil, err
- }
-
- if len(s) > 0 {
- return s[0], nil
- } else if len(m) > 0 {
- return m[0], nil
- }
- return nil, fmt.Errorf("no series or movie could be found")
-}
-
-// FindEpisodeByName finds an episode by its crunchyroll series name and episode title.
-// Use this in combination with ParseEpisodeURL and hand over the corresponding results
-// to this function.
-func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Episode, error) {
- series, _, err := c.Search(seriesName, 5)
- if err != nil {
- return nil, err
- }
-
- var matchingEpisodes []*Episode
- for _, s := range series {
- seasons, err := s.Seasons()
- if err != nil {
- return nil, err
- }
-
- for _, season := range seasons {
- episodes, err := season.Episodes()
- if err != nil {
- return nil, err
- }
- for _, episode := range episodes {
- if episode.SlugTitle == episodeTitle {
- matchingEpisodes = append(matchingEpisodes, episode)
- }
- }
- }
- }
-
- return matchingEpisodes, nil
-}
-
-// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url.
-//
-// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaSeriesURL
-// if possible since beta url are always safe to use.
-// The method will stay in the library until only beta urls are supported by crunchyroll itself.
-func ParseVideoURL(url string) (seriesName string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)(/videos)?/?$`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seriesName = groups["series"]
-
- if seriesName != "" {
- ok = true
- }
- }
- return
-}
-
-// ParseEpisodeURL tries to extract the crunchyroll series name, title, episode number and web id out of the given crunchyroll url
-// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
-// the episode number will be 235.
-//
-// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaEpisodeURL
-// if possible since beta url are always safe to use.
-// The method will stay in the library until only beta urls are supported by crunchyroll itself.
-func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, webId int, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P.+)-(?P\d+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seriesName = groups["series"]
- episodeNumber, _ = strconv.Atoi(groups["number"])
- title = groups["title"]
- webId, _ = strconv.Atoi(groups["webId"])
-
- if seriesName != "" && title != "" && webId != 0 {
- ok = true
- }
- }
- return
-}
-
-// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season.
-func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P\w+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seasonId = groups["seasonId"]
- ok = true
- }
- return
-}
-
-// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode.
-func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P\w+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- episodeId = groups["episodeId"]
- ok = true
- }
- return
-}
-
-// Browse browses the crunchyroll catalog filtered by the specified options and returns all found series and movies within the given limit.
-func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m []*Movie, err error) {
- query, err := encodeStructToQueryValues(options)
- if err != nil {
- return nil, nil, err
- }
-
- browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s",
- query, limit, c.Locale)
- resp, err := c.request(browseEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'browse' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case "series":
- series := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series)
- case "movie_listing":
- movie := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
-
- return s, m, nil
-}
-
-// Categories returns all available categories and possible subcategories.
-func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
- tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
- includeSubcategories, c.Locale)
- resp, err := c.request(tenantCategoriesEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'tenant_categories' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- category := &Category{}
- if err := decodeMapToStruct(item, category); err != nil {
- return nil, err
- }
-
- ca = append(ca, category)
- }
-
- return ca, nil
-}
-
-// Simulcasts returns all available simulcast seasons for the current locale.
-func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
- seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale)
- resp, err := c.request(seasonListEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'season_list' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- simulcast := &Simulcast{}
- if err := decodeMapToStruct(item, simulcast); err != nil {
- return nil, err
- }
-
- s = append(s, simulcast)
- }
-
- return s, nil
-}
-
-// News returns the top and latest news from crunchyroll for the current locale within the given limits.
-func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) {
- newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
- topLimit, latestLimit, c.Locale)
- resp, err := c.request(newsFeedEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'news_feed' response: %w", err)
- }
-
- topNews := jsonBody["top_news"].(map[string]interface{})
- for _, item := range topNews["items"].([]interface{}) {
- topNews := &News{}
- if err := decodeMapToStruct(item, topNews); err != nil {
- return nil, nil, err
- }
-
- t = append(t, topNews)
- }
-
- latestNews := jsonBody["latest_news"].(map[string]interface{})
- for _, item := range latestNews["items"].([]interface{}) {
- latestNews := &News{}
- if err := decodeMapToStruct(item, latestNews); err != nil {
- return nil, nil, err
- }
-
- l = append(l, latestNews)
- }
-
- return t, l, nil
-}
-
-// Recommendations returns series and movie recommendations from crunchyroll based on the currently logged in account within the given limit.
-func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
- recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
- c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(recommendationsEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'recommendations' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case "series":
- series := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series)
- case "movie_listing":
- movie := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
-
- return s, m, nil
-}
-
-// UpNext returns the episodes that are up next based on the currently logged in account within the given limit.
-func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
- upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
- c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(upNextAccountEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'up_next_account' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- panel := item.(map[string]interface{})["panel"]
-
- episode := &Episode{
- crunchy: c,
- }
- if err := decodeMapToStruct(panel, episode); err != nil {
- return nil, err
- }
-
- e = append(e, episode)
- }
-
- return e, nil
-}
-
-// SimilarTo returns similar series and movies according to crunchyroll to the one specified by id within the given limit.
-func (c *Crunchyroll) SimilarTo(id string, limit uint) (s []*Series, m []*Movie, err error) {
- similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
- c.Config.AccountID, id, limit, c.Locale)
- resp, err := c.request(similarToEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'similar_to' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case "series":
- series := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series)
- case "movie_listing":
- movie := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
-
- return s, m, nil
-}
-
-// WatchHistory returns the history of watched episodes based on the currently logged in account from the given page with the given size.
-func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
- watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
- c.Config.AccountID, page, size, c.Locale)
- resp, err := c.request(watchHistoryEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'watch-history' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- panel := item.(map[string]interface{})["panel"]
-
- episode := &Episode{
- crunchy: c,
- }
- if err := decodeMapToStruct(panel, episode); err != nil {
- return nil, err
- }
-
- historyEpisode := &HistoryEpisode{
- Episode: episode,
- }
- if err := decodeMapToStruct(item, historyEpisode); err != nil {
- return nil, err
- }
-
- e = append(e, historyEpisode)
- }
-
- return e, nil
-}
-
-type WatchlistOptions struct {
- // OrderAsc specified whether the results should be order ascending or descending.
- OrderAsc bool
-
- // OnlyFavorites specifies whether only episodes which are marked as favorite should be returned.
- OnlyFavorites bool
-
- // LanguageType specifies whether returning episodes should be only subbed or dubbed.
- LanguageType WatchlistLanguageType
-
- // ContentType specified whether returning videos should only be series episodes or movies.
- // But tbh all movies I've searched on crunchy were flagged as series too, so this
- // parameter is kinda useless.
- ContentType WatchlistContentType
-}
-
-func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) {
- values := url.Values{}
- if options.OrderAsc {
- values.Set("order", "asc")
- } else {
- values.Set("order", "desc")
- }
- if options.OnlyFavorites {
- values.Set("only_favorites", "true")
- }
- switch options.LanguageType {
- case WatchlistLanguageSubbed:
- values.Set("is_subbed", "true")
- case WatchlistLanguageDubbed:
- values.Set("is_dubbed", "true")
- }
- values.Set("n", strconv.Itoa(int(limit)))
- values.Set("locale", string(c.Locale))
-
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/%s/watchlist?%s", c.Config.AccountID, values.Encode())
- resp, err := c.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- var watchlistEntries []*WatchlistEntry
- if err := decodeMapToStruct(jsonBody["items"], &watchlistEntries); err != nil {
- return nil, err
- }
-
- for _, entry := range watchlistEntries {
- switch entry.Panel.Type {
- case WatchlistEntryEpisode:
- entry.Panel.EpisodeMetadata.crunchy = c
- case WatchlistEntrySeries:
- entry.Panel.SeriesMetadata.crunchy = c
- }
- }
-
- return watchlistEntries, nil
-}
-
-func (c *Crunchyroll) CrunchyLists() (*CrunchyLists, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
- resp, err := c.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- crunchyLists := &CrunchyLists{
- crunchy: c,
- }
- json.NewDecoder(resp.Body).Decode(crunchyLists)
- for _, item := range crunchyLists.Items {
- item.crunchy = c
- }
-
- return crunchyLists, nil
-}
-
-// Account returns information about the currently logged in crunchyroll account.
-func (c *Crunchyroll) Account() (*Account, error) {
- resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- account := &Account{
- crunchy: c,
- }
-
- if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
- return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
- }
-
- resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile", http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
- return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
- }
-
- return account, nil
-}
diff --git a/news.go b/news.go
index d90dd65..074dc0e 100644
--- a/news.go
+++ b/news.go
@@ -1,6 +1,50 @@
package crunchyroll
-// News contains all information about a news.
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// News returns the top and latest news from crunchyroll for the current locale within the given limits.
+func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) {
+ newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
+ topLimit, latestLimit, c.Locale)
+ resp, err := c.request(newsFeedEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'news_feed' response: %w", err)
+ }
+
+ topNews := jsonBody["top_news"].(map[string]interface{})
+ for _, item := range topNews["items"].([]interface{}) {
+ topNews := &News{}
+ if err := decodeMapToStruct(item, topNews); err != nil {
+ return nil, nil, err
+ }
+
+ t = append(t, topNews)
+ }
+
+ latestNews := jsonBody["latest_news"].(map[string]interface{})
+ for _, item := range latestNews["items"].([]interface{}) {
+ latestNews := &News{}
+ if err := decodeMapToStruct(item, latestNews); err != nil {
+ return nil, nil, err
+ }
+
+ l = append(l, latestNews)
+ }
+
+ return t, l, nil
+}
+
+// News contains all information about news.
type News struct {
Title string `json:"title"`
Link string `json:"link"`
diff --git a/parse.go b/parse.go
new file mode 100644
index 0000000..392e971
--- /dev/null
+++ b/parse.go
@@ -0,0 +1,69 @@
+package crunchyroll
+
+import (
+ "regexp"
+ "strconv"
+)
+
+// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season.
+func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P\w+).*`)
+ if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
+ groups := regexGroups(urlMatch, pattern.SubexpNames()...)
+ seasonId = groups["seasonId"]
+ ok = true
+ }
+ return
+}
+
+// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode.
+func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P\w+).*`)
+ if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
+ groups := regexGroups(urlMatch, pattern.SubexpNames()...)
+ episodeId = groups["episodeId"]
+ ok = true
+ }
+ return
+}
+
+// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url.
+//
+// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaSeriesURL
+// if possible since beta url are always safe to use.
+// The method will stay in the library until only beta urls are supported by crunchyroll itself.
+func ParseVideoURL(url string) (seriesName string, ok bool) {
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)(/videos)?/?$`)
+ if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
+ groups := regexGroups(urlMatch, pattern.SubexpNames()...)
+ seriesName = groups["series"]
+
+ if seriesName != "" {
+ ok = true
+ }
+ }
+ return
+}
+
+// ParseEpisodeURL tries to extract the crunchyroll series name, title, episode number and web id out of the given crunchyroll url
+// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
+// the episode number will be 235.
+//
+// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaEpisodeURL
+// if possible since beta url are always safe to use.
+// The method will stay in the library until only beta urls are supported by crunchyroll itself.
+func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, webId int, ok bool) {
+ pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P.+)-(?P\d+).*`)
+ if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
+ groups := regexGroups(urlMatch, pattern.SubexpNames()...)
+ seriesName = groups["series"]
+ episodeNumber, _ = strconv.Atoi(groups["number"])
+ title = groups["title"]
+ webId, _ = strconv.Atoi(groups["webId"])
+
+ if seriesName != "" && title != "" && webId != 0 {
+ ok = true
+ }
+ }
+ return
+}
diff --git a/search.go b/search.go
new file mode 100644
index 0000000..d7f6e76
--- /dev/null
+++ b/search.go
@@ -0,0 +1,193 @@
+package crunchyroll
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after.
+type BrowseSortType string
+
+const (
+ BrowseSortPopularity BrowseSortType = "popularity"
+ BrowseSortNewlyAdded = "newly_added"
+ BrowseSortAlphabetical = "alphabetical"
+)
+
+// BrowseOptions represents options for browsing the crunchyroll catalog.
+type BrowseOptions struct {
+ // Categories specifies the categories of the entries.
+ Categories []string `param:"categories"`
+
+ // IsDubbed specifies whether the entries should be dubbed.
+ IsDubbed bool `param:"is_dubbed"`
+
+ // IsSubbed specifies whether the entries should be subbed.
+ IsSubbed bool `param:"is_subbed"`
+
+ // Simulcast specifies a particular simulcast season by id in which the entries have been aired.
+ Simulcast string `param:"season_tag"`
+
+ // Sort specifies how the entries should be sorted.
+ Sort BrowseSortType `param:"sort_by"`
+
+ // Start specifies the index from which the entries should be returned.
+ Start uint `param:"start"`
+
+ // Type specifies the media type of the entries.
+ Type MediaType `param:"type"`
+}
+
+// Browse browses the crunchyroll catalog filtered by the specified options and returns all found series and movies within the given limit.
+func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m []*Movie, err error) {
+ query, err := encodeStructToQueryValues(options)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s",
+ query, limit, c.Locale)
+ resp, err := c.request(browseEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'browse' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+
+ return s, m, nil
+}
+
+// FindVideoByName finds a Video (Season or Movie) by its name.
+// Use this in combination with ParseVideoURL and hand over the corresponding results
+// to this function.
+//
+// Deprecated: Use Search instead. The first result sometimes isn't the correct one
+// so this function is inaccurate in some cases.
+// See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information.
+func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
+ s, m, err := c.Search(seriesName, 1)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(s) > 0 {
+ return s[0], nil
+ } else if len(m) > 0 {
+ return m[0], nil
+ }
+ return nil, fmt.Errorf("no series or movie could be found")
+}
+
+// FindEpisodeByName finds an episode by its crunchyroll series name and episode title.
+// Use this in combination with ParseEpisodeURL and hand over the corresponding results
+// to this function.
+func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Episode, error) {
+ series, _, err := c.Search(seriesName, 5)
+ if err != nil {
+ return nil, err
+ }
+
+ var matchingEpisodes []*Episode
+ for _, s := range series {
+ seasons, err := s.Seasons()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, season := range seasons {
+ episodes, err := season.Episodes()
+ if err != nil {
+ return nil, err
+ }
+ for _, episode := range episodes {
+ if episode.SlugTitle == episodeTitle {
+ matchingEpisodes = append(matchingEpisodes, episode)
+ }
+ }
+ }
+ }
+
+ return matchingEpisodes, nil
+}
+
+// Search searches a query and returns all found series and movies within the given limit.
+func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
+ searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
+ query, limit, c.Locale)
+ resp, err := c.request(searchEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'search' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ item := item.(map[string]interface{})
+ if item["total"].(float64) > 0 {
+ switch item["type"] {
+ case "series":
+ for _, series := range item["items"].([]interface{}) {
+ series2 := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(series, series2); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(series.(map[string]interface{})["series_metadata"].(map[string]interface{}), series2); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series2)
+ }
+ case "movie_listing":
+ for _, movie := range item["items"].([]interface{}) {
+ movie2 := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(movie, movie2); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie2)
+ }
+ }
+ }
+ }
+
+ return s, m, nil
+}
diff --git a/simulcast.go b/simulcast.go
index d02f348..e77d2f8 100644
--- a/simulcast.go
+++ b/simulcast.go
@@ -1,5 +1,37 @@
package crunchyroll
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// Simulcasts returns all available simulcast seasons for the current locale.
+func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
+ seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale)
+ resp, err := c.request(seasonListEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'season_list' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ simulcast := &Simulcast{}
+ if err := decodeMapToStruct(item, simulcast); err != nil {
+ return nil, err
+ }
+
+ s = append(s, simulcast)
+ }
+
+ return s, nil
+}
+
// Simulcast contains all information about a simulcast season.
type Simulcast struct {
ID string `json:"id"`
diff --git a/suggestions.go b/suggestions.go
new file mode 100644
index 0000000..d6bd770
--- /dev/null
+++ b/suggestions.go
@@ -0,0 +1,82 @@
+package crunchyroll
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// Recommendations returns series and movie recommendations from crunchyroll based on the currently logged in account within the given limit.
+func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
+ recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
+ c.Config.AccountID, limit, c.Locale)
+ resp, err := c.request(recommendationsEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'recommendations' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ s = append(s, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+
+ return s, m, nil
+}
+
+// UpNext returns the episodes that are up next based on the currently logged in account within the given limit.
+func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
+ upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
+ c.Config.AccountID, limit, c.Locale)
+ resp, err := c.request(upNextAccountEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'up_next_account' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ panel := item.(map[string]interface{})["panel"]
+
+ episode := &Episode{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(panel, episode); err != nil {
+ return nil, err
+ }
+
+ e = append(e, episode)
+ }
+
+ return e, nil
+}
diff --git a/video.go b/video.go
index 3a67f5c..2d76499 100644
--- a/video.go
+++ b/video.go
@@ -206,6 +206,49 @@ func (s *Series) RemoveFromWatchlist() error {
return err
}
+// Similar returns similar series and movies to the current series within the given limit.
+func (s *Series) Similar(limit uint) (ss []*Series, m []*Movie, err error) {
+ similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
+ s.crunchy.Config.AccountID, s.ID, limit, s.crunchy.Locale)
+ resp, err := s.crunchy.request(similarToEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, nil, fmt.Errorf("failed to parse 'similar_to' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ switch item.(map[string]interface{})["type"] {
+ case "series":
+ series := &Series{
+ crunchy: s.crunchy,
+ }
+ if err := decodeMapToStruct(item, series); err != nil {
+ return nil, nil, err
+ }
+ if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
+ return nil, nil, err
+ }
+
+ ss = append(ss, series)
+ case "movie_listing":
+ movie := &Movie{
+ crunchy: s.crunchy,
+ }
+ if err := decodeMapToStruct(item, movie); err != nil {
+ return nil, nil, err
+ }
+
+ m = append(m, movie)
+ }
+ }
+ return
+}
+
// Seasons returns all seasons of a series.
func (s *Series) Seasons() (seasons []*Season, err error) {
if s.children != nil {
diff --git a/watch_history.go b/watch_history.go
new file mode 100644
index 0000000..d9ce265
--- /dev/null
+++ b/watch_history.go
@@ -0,0 +1,45 @@
+package crunchyroll
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+// WatchHistory returns the history of watched episodes based on the currently logged in account from the given page with the given size.
+func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
+ watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
+ c.Config.AccountID, page, size, c.Locale)
+ resp, err := c.request(watchHistoryEndpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
+ return nil, fmt.Errorf("failed to parse 'watch-history' response: %w", err)
+ }
+
+ for _, item := range jsonBody["items"].([]interface{}) {
+ panel := item.(map[string]interface{})["panel"]
+
+ episode := &Episode{
+ crunchy: c,
+ }
+ if err := decodeMapToStruct(panel, episode); err != nil {
+ return nil, err
+ }
+
+ historyEpisode := &HistoryEpisode{
+ Episode: episode,
+ }
+ if err := decodeMapToStruct(item, historyEpisode); err != nil {
+ return nil, err
+ }
+
+ e = append(e, historyEpisode)
+ }
+
+ return e, nil
+}
diff --git a/watchlist.go b/watchlist.go
index 73fbb3d..b4db34d 100644
--- a/watchlist.go
+++ b/watchlist.go
@@ -1,5 +1,93 @@
package crunchyroll
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strconv"
+)
+
+// WatchlistLanguageType represents a filter type to filter Crunchyroll.Watchlist entries after sub or dub.
+type WatchlistLanguageType int
+
+const (
+ WatchlistLanguageSubbed WatchlistLanguageType = iota + 1
+ WatchlistLanguageDubbed
+)
+
+// WatchlistContentType represents a filter type to filter Crunchyroll.Watchlist entries if they're series or movies.
+type WatchlistContentType string
+
+const (
+ WatchlistContentSeries WatchlistContentType = "series"
+ WatchlistContentMovies = "movie_listing"
+)
+
+// WatchlistOptions represents options for receiving the user watchlist.
+type WatchlistOptions struct {
+ // OrderAsc specified whether the results should be order ascending or descending.
+ OrderAsc bool
+
+ // OnlyFavorites specifies whether only episodes which are marked as favorite should be returned.
+ OnlyFavorites bool
+
+ // LanguageType specifies whether returning episodes should be only subbed or dubbed.
+ LanguageType WatchlistLanguageType
+
+ // ContentType specified whether returning videos should only be series episodes or movies.
+ // But tbh all movies I've searched on crunchy were flagged as series too, so this
+ // parameter is kinda useless.
+ ContentType WatchlistContentType
+}
+
+// Watchlist returns the watchlist entries for the currently logged in user.
+func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) {
+ values := url.Values{}
+ if options.OrderAsc {
+ values.Set("order", "asc")
+ } else {
+ values.Set("order", "desc")
+ }
+ if options.OnlyFavorites {
+ values.Set("only_favorites", "true")
+ }
+ switch options.LanguageType {
+ case WatchlistLanguageSubbed:
+ values.Set("is_subbed", "true")
+ case WatchlistLanguageDubbed:
+ values.Set("is_dubbed", "true")
+ }
+ values.Set("n", strconv.Itoa(int(limit)))
+ values.Set("locale", string(c.Locale))
+
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/%s/watchlist?%s", c.Config.AccountID, values.Encode())
+ resp, err := c.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var jsonBody map[string]interface{}
+ json.NewDecoder(resp.Body).Decode(&jsonBody)
+
+ var watchlistEntries []*WatchlistEntry
+ if err := decodeMapToStruct(jsonBody["items"], &watchlistEntries); err != nil {
+ return nil, err
+ }
+
+ for _, entry := range watchlistEntries {
+ switch entry.Panel.Type {
+ case WatchlistEntryEpisode:
+ entry.Panel.EpisodeMetadata.crunchy = c
+ case WatchlistEntrySeries:
+ entry.Panel.SeriesMetadata.crunchy = c
+ }
+ }
+
+ return watchlistEntries, nil
+}
+
// WatchlistEntry contains information about an entry on the watchlist.
type WatchlistEntry struct {
Panel Panel `json:"panel"`
From f71846628daf690ea7dd0a9ab32760658fd155e9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 21 Jun 2022 22:02:18 +0200
Subject: [PATCH 225/732] Refactor crunchyroll.go
---
crunchyroll.go | 125 +++++++++++++++++++++++++------------------------
1 file changed, 63 insertions(+), 62 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index a83415c..214cbe4 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -36,39 +36,6 @@ const (
MediaTypeMovie = "movie_listing"
)
-type Crunchyroll struct {
- // Client is the http.Client to perform all requests over.
- Client *http.Client
- // Context can be used to stop requests with Client and is context.Background by default.
- Context context.Context
- // Locale specifies in which language all results should be returned / requested.
- Locale LOCALE
- // EtpRt is the crunchyroll beta equivalent to a session id (prior SessionID field in
- // this struct in v2 and below).
- EtpRt string
-
- // Config stores parameters which are needed by some api calls.
- Config struct {
- TokenType string
- AccessToken string
-
- Bucket string
-
- CountryCode string
- Premium bool
- Channel string
- Policy string
- Signature string
- KeyPairID string
- AccountID string
- ExternalID string
- MaturityRating string
- }
-
- // If cache is true, internal caching is enabled.
- cache bool
-}
-
type loginResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
@@ -250,6 +217,69 @@ func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *htt
return crunchy, nil
}
+type Crunchyroll struct {
+ // Client is the http.Client to perform all requests over.
+ Client *http.Client
+ // Context can be used to stop requests with Client and is context.Background by default.
+ Context context.Context
+ // Locale specifies in which language all results should be returned / requested.
+ Locale LOCALE
+ // EtpRt is the crunchyroll beta equivalent to a session id (prior SessionID field in
+ // this struct in v2 and below).
+ EtpRt string
+
+ // Config stores parameters which are needed by some api calls.
+ Config struct {
+ TokenType string
+ AccessToken string
+
+ Bucket string
+
+ CountryCode string
+ Premium bool
+ Channel string
+ Policy string
+ Signature string
+ KeyPairID string
+ AccountID string
+ ExternalID string
+ MaturityRating string
+ }
+
+ // If cache is true, internal caching is enabled.
+ cache bool
+}
+
+// IsCaching returns if data gets cached or not.
+// See SetCaching for more information.
+func (c *Crunchyroll) IsCaching() bool {
+ return c.cache
+}
+
+// SetCaching enables or disables internal caching of requests made.
+// Caching is enabled by default.
+// If it is disabled the already cached data still gets called.
+// The best way to prevent this is to create a complete new Crunchyroll struct.
+func (c *Crunchyroll) SetCaching(caching bool) {
+ c.cache = caching
+}
+
+// request is a base function which handles simple api requests.
+func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, error) {
+ req, err := http.NewRequest(method, endpoint, nil)
+ if err != nil {
+ return nil, err
+ }
+ return c.requestFull(req)
+}
+
+// requestFull is a base function which handles full user controlled api requests.
+func (c *Crunchyroll) requestFull(req *http.Request) (*http.Response, error) {
+ req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken))
+
+ return request(req, c.Client)
+}
+
func request(req *http.Request, client *http.Client) (*http.Response, error) {
resp, err := client.Do(req)
if err == nil {
@@ -298,32 +328,3 @@ func request(req *http.Request, client *http.Client) (*http.Response, error) {
}
return resp, err
}
-
-// request is a base function which handles api requests.
-func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, error) {
- req, err := http.NewRequest(method, endpoint, nil)
- if err != nil {
- return nil, err
- }
- return c.requestFull(req)
-}
-
-func (c *Crunchyroll) requestFull(req *http.Request) (*http.Response, error) {
- req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken))
-
- return request(req, c.Client)
-}
-
-// IsCaching returns if data gets cached or not.
-// See SetCaching for more information.
-func (c *Crunchyroll) IsCaching() bool {
- return c.cache
-}
-
-// SetCaching enables or disables internal caching of requests made.
-// Caching is enabled by default.
-// If it is disabled the already cached data still gets called.
-// The best way to prevent this is to create a complete new Crunchyroll struct.
-func (c *Crunchyroll) SetCaching(caching bool) {
- c.cache = caching
-}
From 2067c50937497fadf540107e5502ee005e9433b8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 21 Jun 2022 22:04:22 +0200
Subject: [PATCH 226/732] Add logout endpoint
---
crunchyroll.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/crunchyroll.go b/crunchyroll.go
index 214cbe4..9c786c6 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -250,6 +250,16 @@ type Crunchyroll struct {
cache bool
}
+// Logout logs the user out which invalidates the current session.
+// You have to call a login method again and create a new Crunchyroll instance
+// if you want to perform any further actions since this instance is not usable
+// anymore after calling this.
+func (c *Crunchyroll) Logout() error {
+ endpoint := "https://crunchyroll.com/logout"
+ _, err := c.request(endpoint, http.MethodGet)
+ return err
+}
+
// IsCaching returns if data gets cached or not.
// See SetCaching for more information.
func (c *Crunchyroll) IsCaching() bool {
From 810f3ae12ea332d83a550a561d3495ed7334105c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 22 Jun 2022 01:35:26 +0000
Subject: [PATCH 227/732] Bump github.com/spf13/cobra from 1.4.0 to 1.5.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.4.0...v1.5.0)
---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
go.mod | 2 +-
go.sum | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/go.mod b/go.mod
index 9a38468..99877f2 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.18
require (
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.4.0
+ github.com/spf13/cobra v1.5.0
)
require (
diff --git a/go.sum b/go.sum
index 34693ca..aa512f1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,11 @@
-github.com/cpuguy83/go-md2man/v2 v2.0.1/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/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
+github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
From bfad0caa9a95fc93b663f6897b25acbf0e596f38 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 13:52:10 +0200
Subject: [PATCH 228/732] Update email language and video sub language setting
function names
---
account.go | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/account.go b/account.go
index f2d6a70..cb45a77 100644
--- a/account.go
+++ b/account.go
@@ -67,8 +67,8 @@ type Account struct {
Wallpaper *Wallpaper `json:"wallpaper"`
}
-// UpdateEmailLanguage sets in which language emails should be received.
-func (a *Account) UpdateEmailLanguage(language LOCALE) error {
+// UpdatePreferredEmailLanguage sets in which language emails should be received.
+func (a *Account) UpdatePreferredEmailLanguage(language LOCALE) error {
err := a.updatePreferences("preferred_communication_language", string(language))
if err == nil {
a.PreferredCommunicationLanguage = language
@@ -76,8 +76,8 @@ func (a *Account) UpdateEmailLanguage(language LOCALE) error {
return err
}
-// UpdateVideoSubtitleLanguage sets in which language default subtitles should be shown
-func (a *Account) UpdateVideoSubtitleLanguage(language LOCALE) error {
+// UpdatePreferredVideoSubtitleLanguage sets in which language default subtitles should be shown
+func (a *Account) UpdatePreferredVideoSubtitleLanguage(language LOCALE) error {
err := a.updatePreferences("preferred_content_subtitle_language", string(language))
if err == nil {
a.PreferredContentSubtitleLanguage = language
From a283ba724785d537c56ce3967dd0f1b2fe489290 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 14:27:15 +0200
Subject: [PATCH 229/732] Add functions to get wallpaper urls
---
wallpaper.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/wallpaper.go b/wallpaper.go
index f64cedf..da7a032 100644
--- a/wallpaper.go
+++ b/wallpaper.go
@@ -1,4 +1,14 @@
package crunchyroll
+import "fmt"
+
// Wallpaper contains a wallpaper name which can be set via Account.ChangeWallpaper.
type Wallpaper string
+
+func (w *Wallpaper) TinyUrl() string {
+ return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/360x115/%s", *w)
+}
+
+func (w *Wallpaper) BigUrl() string {
+ return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/1920x400/%s", *w)
+}
From 28070bd32dbd8d5f1c126836ce130b5d1a79323a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 16:44:34 +0200
Subject: [PATCH 230/732] Add function to check if a comment is marked as
spoiler
---
comment.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/comment.go b/comment.go
index 4dba10d..47005cf 100644
--- a/comment.go
+++ b/comment.go
@@ -71,6 +71,16 @@ func (c *Comment) Delete() error {
return nil
}
+// IsSpoiler returns if the comment is marked as spoiler or not.
+func (c *Comment) IsSpoiler() bool {
+ for _, flag := range c.Flags {
+ if flag == "spoiler" {
+ return true
+ }
+ }
+ return false
+}
+
// MarkAsSpoiler marks the current comment as spoiler. Works only if the user has written the comment,
// and it isn't already marked as spoiler.
func (c *Comment) MarkAsSpoiler() error {
From ead1db2be8f7bc46c9ce60e7be24acb946a626e0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 16:50:48 +0200
Subject: [PATCH 231/732] Add function to check if a comment is liked by the
logged-in user
---
comment.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/comment.go b/comment.go
index 47005cf..762b57b 100644
--- a/comment.go
+++ b/comment.go
@@ -143,6 +143,16 @@ func (c *Comment) Like() error {
return nil
}
+// Liked returns if the user has liked the comment.
+func (c *Comment) Liked() bool {
+ for _, flag := range c.Flags {
+ if flag == "liked" {
+ return true
+ }
+ }
+ return false
+}
+
// RemoveLike removes the like from the comment. Works only if the user has liked it.
func (c *Comment) RemoveLike() error {
if err := c.unVote("like", "liked"); err != nil {
From 9919a48e9ae52ea6050537f5a4adca9723b8e4b6 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 16:57:49 +0200
Subject: [PATCH 232/732] Change 'UnreportComment' to 'RemoveReport'
---
comment.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/comment.go b/comment.go
index 762b57b..811433f 100644
--- a/comment.go
+++ b/comment.go
@@ -217,9 +217,9 @@ func (c *Comment) Report() error {
return c.vote("inappropriate", "reported")
}
-// UnreportComment removes the report request from the comment. Only works if the user
+// RemoveReport removes the report request from the comment. Only works if the user
// has reported the comment.
-func (c *Comment) UnreportComment() error {
+func (c *Comment) RemoveReport() error {
return c.unVote("inappropriate", "reported")
}
From f03287856b3f509f9ac1fbe68fe9580deef13a8b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 16:59:04 +0200
Subject: [PATCH 233/732] Add function to check if comment is reported
---
comment.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/comment.go b/comment.go
index 811433f..98c400c 100644
--- a/comment.go
+++ b/comment.go
@@ -217,6 +217,10 @@ func (c *Comment) Report() error {
return c.vote("inappropriate", "reported")
}
+func (c *Comment) IsReported() bool {
+ return c.markedAs("reported")
+}
+
// RemoveReport removes the report request from the comment. Only works if the user
// has reported the comment.
func (c *Comment) RemoveReport() error {
From 0521895f11e9cbafe49585f1fe65f637f888036c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 16:59:54 +0200
Subject: [PATCH 234/732] Add function to check if comment is flagged as
spoiler
---
comment.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/comment.go b/comment.go
index 98c400c..dc68201 100644
--- a/comment.go
+++ b/comment.go
@@ -233,6 +233,10 @@ func (c *Comment) FlagAsSpoiler() error {
return c.vote("spoiler", "spoiler")
}
+func (c *Comment) IsFlaggedAsSpoiler() bool {
+ return c.markedAs("spoiler")
+}
+
// UnflagAsSpoiler rewokes the request to the user (and / or crunchyroll?) to mark the
// comment as spoiler. Only works if the user has flagged the comment as spoiler.
func (c *Comment) UnflagAsSpoiler() error {
From 4cfcc11e206cda21ffd3ff7c7413bac088a5cdc6 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 17:00:46 +0200
Subject: [PATCH 235/732] Simplified 'Liked'
---
comment.go | 7 +------
1 file changed, 1 insertion(+), 6 deletions(-)
diff --git a/comment.go b/comment.go
index dc68201..333ec4b 100644
--- a/comment.go
+++ b/comment.go
@@ -145,12 +145,7 @@ func (c *Comment) Like() error {
// Liked returns if the user has liked the comment.
func (c *Comment) Liked() bool {
- for _, flag := range c.Flags {
- if flag == "liked" {
- return true
- }
- }
- return false
+ return c.markedAs("liked")
}
// RemoveLike removes the like from the comment. Works only if the user has liked it.
From 14491ce6c954cdcd486ccc4876027ee003da5aee Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 17:01:08 +0200
Subject: [PATCH 236/732] Rename 'markedAs' to 'votedAs'
---
comment.go | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/comment.go b/comment.go
index 333ec4b..d58fade 100644
--- a/comment.go
+++ b/comment.go
@@ -86,7 +86,7 @@ func (c *Comment) IsSpoiler() bool {
func (c *Comment) MarkAsSpoiler() error {
if !c.IsOwner {
return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
- } else if c.markedAs("spoiler") {
+ } else if c.votedAs("spoiler") {
return fmt.Errorf("comment is already marked as spoiler")
}
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
@@ -112,7 +112,7 @@ func (c *Comment) MarkAsSpoiler() error {
func (c *Comment) UnmarkAsSpoiler() error {
if !c.IsOwner {
return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
- } else if !c.markedAs("spoiler") {
+ } else if !c.votedAs("spoiler") {
return fmt.Errorf("comment is not marked as spoiler")
}
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
@@ -145,7 +145,7 @@ func (c *Comment) Like() error {
// Liked returns if the user has liked the comment.
func (c *Comment) Liked() bool {
- return c.markedAs("liked")
+ return c.votedAs("liked")
}
// RemoveLike removes the like from the comment. Works only if the user has liked it.
@@ -213,7 +213,7 @@ func (c *Comment) Report() error {
}
func (c *Comment) IsReported() bool {
- return c.markedAs("reported")
+ return c.votedAs("reported")
}
// RemoveReport removes the report request from the comment. Only works if the user
@@ -229,7 +229,7 @@ func (c *Comment) FlagAsSpoiler() error {
}
func (c *Comment) IsFlaggedAsSpoiler() bool {
- return c.markedAs("spoiler")
+ return c.votedAs("spoiler")
}
// UnflagAsSpoiler rewokes the request to the user (and / or crunchyroll?) to mark the
@@ -238,7 +238,7 @@ func (c *Comment) UnflagAsSpoiler() error {
return c.unVote("spoiler", "spoiler")
}
-func (c *Comment) markedAs(voteType string) bool {
+func (c *Comment) votedAs(voteType string) bool {
for _, userVote := range c.UserVotes {
if userVote == voteType {
return true
@@ -248,7 +248,7 @@ func (c *Comment) markedAs(voteType string) bool {
}
func (c *Comment) vote(voteType, readableName string) error {
- if c.markedAs(voteType) {
+ if c.votedAs(voteType) {
return fmt.Errorf("comment is already marked as %s", readableName)
}
From e6172cdf90685e5f363c984f77301a34c2dc5076 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 17:16:47 +0200
Subject: [PATCH 237/732] Change watchlist option order type and content type
---
watchlist.go | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/watchlist.go b/watchlist.go
index b4db34d..6fa0bc0 100644
--- a/watchlist.go
+++ b/watchlist.go
@@ -16,18 +16,17 @@ const (
WatchlistLanguageDubbed
)
-// WatchlistContentType represents a filter type to filter Crunchyroll.Watchlist entries if they're series or movies.
-type WatchlistContentType string
+type WatchlistOrderType string
const (
- WatchlistContentSeries WatchlistContentType = "series"
- WatchlistContentMovies = "movie_listing"
+ WatchlistOrderAsc = "asc"
+ WatchlistOrderDesc = "desc"
)
// WatchlistOptions represents options for receiving the user watchlist.
type WatchlistOptions struct {
- // OrderAsc specified whether the results should be order ascending or descending.
- OrderAsc bool
+ // Order specified whether the results should be order ascending or descending.
+ Order WatchlistOrderType
// OnlyFavorites specifies whether only episodes which are marked as favorite should be returned.
OnlyFavorites bool
@@ -38,17 +37,16 @@ type WatchlistOptions struct {
// ContentType specified whether returning videos should only be series episodes or movies.
// But tbh all movies I've searched on crunchy were flagged as series too, so this
// parameter is kinda useless.
- ContentType WatchlistContentType
+ ContentType MediaType
}
// Watchlist returns the watchlist entries for the currently logged in user.
func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) {
values := url.Values{}
- if options.OrderAsc {
- values.Set("order", "asc")
- } else {
- values.Set("order", "desc")
+ if options.Order == "" {
+ options.Order = WatchlistOrderDesc
}
+ values.Set("order", string(options.Order))
if options.OnlyFavorites {
values.Set("only_favorites", "true")
}
From 1a4abdc4d817f3a95ca09ee7e249a905ead3ac32 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 17:31:26 +0200
Subject: [PATCH 238/732] Made 'l' in crunchylist lowercase and made
CrunchylistFromID priave
---
crunchylists.go | 86 ++++++++++++++++++++++++-------------------------
1 file changed, 43 insertions(+), 43 deletions(-)
diff --git a/crunchylists.go b/crunchylists.go
index a2a7a1f..b23ae00 100644
--- a/crunchylists.go
+++ b/crunchylists.go
@@ -8,7 +8,7 @@ import (
"time"
)
-func (c *Crunchyroll) CrunchyLists() (*CrunchyLists, error) {
+func (c *Crunchyroll) Crunchylists() (*Crunchylists, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
resp, err := c.request(endpoint, http.MethodGet)
if err != nil {
@@ -16,27 +16,27 @@ func (c *Crunchyroll) CrunchyLists() (*CrunchyLists, error) {
}
defer resp.Body.Close()
- crunchyLists := &CrunchyLists{
+ crunchylists := &Crunchylists{
crunchy: c,
}
- json.NewDecoder(resp.Body).Decode(crunchyLists)
- for _, item := range crunchyLists.Items {
+ json.NewDecoder(resp.Body).Decode(crunchylists)
+ for _, item := range crunchylists.Items {
item.crunchy = c
}
- return crunchyLists, nil
+ return crunchylists, nil
}
-type CrunchyLists struct {
+type Crunchylists struct {
crunchy *Crunchyroll
- Items []*CrunchyListPreview `json:"items"`
+ Items []*CrunchylistPreview `json:"items"`
TotalPublic int `json:"total_public"`
TotalPrivate int `json:"total_private"`
MaxPrivate int `json:"max_private"`
}
-func (cl *CrunchyLists) Create(name string) (*CrunchyList, error) {
+func (cl *Crunchylists) Create(name string) (*Crunchylist, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", cl.crunchy.Config.AccountID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"title": name})
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
@@ -53,10 +53,10 @@ func (cl *CrunchyLists) Create(name string) (*CrunchyList, error) {
var jsonBody map[string]interface{}
json.NewDecoder(resp.Body).Decode(&jsonBody)
- return CrunchyListFromID(cl.crunchy, jsonBody["list_id"].(string))
+ return crunchylistFromID(cl.crunchy, jsonBody["list_id"].(string))
}
-type CrunchyListPreview struct {
+type CrunchylistPreview struct {
crunchy *Crunchyroll
ListID string `json:"list_id"`
@@ -66,32 +66,11 @@ type CrunchyListPreview struct {
Title string `json:"title"`
}
-func (clp *CrunchyListPreview) CrunchyList() (*CrunchyList, error) {
- return CrunchyListFromID(clp.crunchy, clp.ListID)
+func (clp *CrunchylistPreview) Crunchylist() (*Crunchylist, error) {
+ return crunchylistFromID(clp.crunchy, clp.ListID)
}
-func CrunchyListFromID(crunchy *Crunchyroll, id string) (*CrunchyList, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", crunchy.Config.AccountID, id, crunchy.Locale)
- resp, err := crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- crunchyList := &CrunchyList{
- crunchy: crunchy,
- ID: id,
- }
- if err := json.NewDecoder(resp.Body).Decode(crunchyList); err != nil {
- return nil, err
- }
- for _, item := range crunchyList.Items {
- item.crunchy = crunchy
- }
- return crunchyList, nil
-}
-
-type CrunchyList struct {
+type Crunchylist struct {
crunchy *Crunchyroll
ID string `json:"id"`
@@ -101,14 +80,14 @@ type CrunchyList struct {
Title string `json:"title"`
IsPublic bool `json:"is_public"`
ModifiedAt time.Time `json:"modified_at"`
- Items []*CrunchyListItem `json:"items"`
+ Items []*CrunchylistItem `json:"items"`
}
-func (cl *CrunchyList) AddSeries(series *Series) error {
+func (cl *Crunchylist) AddSeries(series *Series) error {
return cl.AddSeriesFromID(series.ID)
}
-func (cl *CrunchyList) AddSeriesFromID(id string) error {
+func (cl *Crunchylist) AddSeriesFromID(id string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"content_id": id})
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
@@ -120,23 +99,23 @@ func (cl *CrunchyList) AddSeriesFromID(id string) error {
return err
}
-func (cl *CrunchyList) RemoveSeries(series *Series) error {
+func (cl *Crunchylist) RemoveSeries(series *Series) error {
return cl.RemoveSeriesFromID(series.ID)
}
-func (cl *CrunchyList) RemoveSeriesFromID(id string) error {
+func (cl *Crunchylist) RemoveSeriesFromID(id string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, id, cl.crunchy.Locale)
_, err := cl.crunchy.request(endpoint, http.MethodDelete)
return err
}
-func (cl *CrunchyList) Delete() error {
+func (cl *Crunchylist) Delete() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
_, err := cl.crunchy.request(endpoint, http.MethodDelete)
return err
}
-func (cl *CrunchyList) Rename(name string) error {
+func (cl *Crunchylist) Rename(name string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"title": name})
req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
@@ -151,7 +130,28 @@ func (cl *CrunchyList) Rename(name string) error {
return err
}
-type CrunchyListItem struct {
+func crunchylistFromID(crunchy *Crunchyroll, id string) (*Crunchylist, error) {
+ endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", crunchy.Config.AccountID, id, crunchy.Locale)
+ resp, err := crunchy.request(endpoint, http.MethodGet)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ crunchyList := &Crunchylist{
+ crunchy: crunchy,
+ ID: id,
+ }
+ if err := json.NewDecoder(resp.Body).Decode(crunchyList); err != nil {
+ return nil, err
+ }
+ for _, item := range crunchyList.Items {
+ item.crunchy = crunchy
+ }
+ return crunchyList, nil
+}
+
+type CrunchylistItem struct {
crunchy *Crunchyroll
ListID string `json:"list_id"`
@@ -160,7 +160,7 @@ type CrunchyListItem struct {
Panel Panel `json:"panel"`
}
-func (cli *CrunchyListItem) Remove() error {
+func (cli *CrunchylistItem) Remove() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s", cli.crunchy.Config.AccountID, cli.ListID, cli.ID)
_, err := cli.crunchy.request(endpoint, http.MethodDelete)
return err
From fa2321e9e8517ab66a55a95e0d7bfb71c495ec36 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 23 Jun 2022 17:40:29 +0200
Subject: [PATCH 239/732] Rename 'Logout' to 'InvalidateSession'
---
crunchyroll.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/crunchyroll.go b/crunchyroll.go
index 9c786c6..a8b3284 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -250,11 +250,11 @@ type Crunchyroll struct {
cache bool
}
-// Logout logs the user out which invalidates the current session.
+// InvalidateSession logs the user out which invalidates the current session.
// You have to call a login method again and create a new Crunchyroll instance
// if you want to perform any further actions since this instance is not usable
// anymore after calling this.
-func (c *Crunchyroll) Logout() error {
+func (c *Crunchyroll) InvalidateSession() error {
endpoint := "https://crunchyroll.com/logout"
_, err := c.request(endpoint, http.MethodGet)
return err
From 2569ddd1c7ed1a6c3006fd87a5bc79bff2e11661 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 24 Jun 2022 11:34:02 +0200
Subject: [PATCH 240/732] Add docs to crunchylists
---
crunchylists.go | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/crunchylists.go b/crunchylists.go
index b23ae00..a297da2 100644
--- a/crunchylists.go
+++ b/crunchylists.go
@@ -8,6 +8,7 @@ import (
"time"
)
+// Crunchylists returns a struct to control crunchylists.
func (c *Crunchyroll) Crunchylists() (*Crunchylists, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
resp, err := c.request(endpoint, http.MethodGet)
@@ -36,6 +37,7 @@ type Crunchylists struct {
MaxPrivate int `json:"max_private"`
}
+// Create creates a new crunchylist with the given name. Duplicate names for lists are allowed.
func (cl *Crunchylists) Create(name string) (*Crunchylist, error) {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", cl.crunchy.Config.AccountID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"title": name})
@@ -66,6 +68,7 @@ type CrunchylistPreview struct {
Title string `json:"title"`
}
+// Crunchylist returns the belonging Crunchylist struct.
func (clp *CrunchylistPreview) Crunchylist() (*Crunchylist, error) {
return crunchylistFromID(clp.crunchy, clp.ListID)
}
@@ -83,10 +86,12 @@ type Crunchylist struct {
Items []*CrunchylistItem `json:"items"`
}
+// AddSeries adds a series.
func (cl *Crunchylist) AddSeries(series *Series) error {
return cl.AddSeriesFromID(series.ID)
}
+// AddSeriesFromID adds a series from its id
func (cl *Crunchylist) AddSeriesFromID(id string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"content_id": id})
@@ -99,22 +104,26 @@ func (cl *Crunchylist) AddSeriesFromID(id string) error {
return err
}
+// RemoveSeries removes a series
func (cl *Crunchylist) RemoveSeries(series *Series) error {
return cl.RemoveSeriesFromID(series.ID)
}
+// RemoveSeriesFromID removes a series by its id
func (cl *Crunchylist) RemoveSeriesFromID(id string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, id, cl.crunchy.Locale)
_, err := cl.crunchy.request(endpoint, http.MethodDelete)
return err
}
+// Delete deleted the current crunchylist.
func (cl *Crunchylist) Delete() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
_, err := cl.crunchy.request(endpoint, http.MethodDelete)
return err
}
+// Rename renames the current crunchylist.
func (cl *Crunchylist) Rename(name string) error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
body, _ := json.Marshal(map[string]string{"title": name})
@@ -160,6 +169,7 @@ type CrunchylistItem struct {
Panel Panel `json:"panel"`
}
+// Remove removes the current item from its crunchylist.
func (cli *CrunchylistItem) Remove() error {
endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s", cli.crunchy.Config.AccountID, cli.ListID, cli.ID)
_, err := cli.crunchy.request(endpoint, http.MethodDelete)
From 3dcfbc0fbb5c00caad58d3309145356e21ea019b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 24 Jun 2022 11:51:12 +0200
Subject: [PATCH 241/732] Add docs to wallpaper
---
wallpaper.go | 2 ++
1 file changed, 2 insertions(+)
diff --git a/wallpaper.go b/wallpaper.go
index da7a032..e99bafb 100644
--- a/wallpaper.go
+++ b/wallpaper.go
@@ -5,10 +5,12 @@ import "fmt"
// Wallpaper contains a wallpaper name which can be set via Account.ChangeWallpaper.
type Wallpaper string
+// TinyUrl returns the url to the wallpaper in low resolution.
func (w *Wallpaper) TinyUrl() string {
return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/360x115/%s", *w)
}
+// BigUrl returns the url to the wallpaper in high resolution.
func (w *Wallpaper) BigUrl() string {
return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/1920x400/%s", *w)
}
From 79c3ba2636a188697a6161d0d68b4ba8cdedc664 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 24 Jun 2022 11:51:48 +0200
Subject: [PATCH 242/732] Refactor to use consts instead of string
---
search.go | 8 ++++----
suggestions.go | 4 ++--
video.go | 4 ++--
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/search.go b/search.go
index d7f6e76..cbbf7e9 100644
--- a/search.go
+++ b/search.go
@@ -61,7 +61,7 @@ func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m
for _, item := range jsonBody["items"].([]interface{}) {
switch item.(map[string]interface{})["type"] {
- case "series":
+ case MediaTypeSeries:
series := &Series{
crunchy: c,
}
@@ -73,7 +73,7 @@ func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m
}
s = append(s, series)
- case "movie_listing":
+ case MediaTypeMovie:
movie := &Movie{
crunchy: c,
}
@@ -160,7 +160,7 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
item := item.(map[string]interface{})
if item["total"].(float64) > 0 {
switch item["type"] {
- case "series":
+ case MediaTypeSeries:
for _, series := range item["items"].([]interface{}) {
series2 := &Series{
crunchy: c,
@@ -174,7 +174,7 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
s = append(s, series2)
}
- case "movie_listing":
+ case MediaTypeMovie:
for _, movie := range item["items"].([]interface{}) {
movie2 := &Movie{
crunchy: c,
diff --git a/suggestions.go b/suggestions.go
index d6bd770..35d7a9b 100644
--- a/suggestions.go
+++ b/suggestions.go
@@ -23,7 +23,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err
for _, item := range jsonBody["items"].([]interface{}) {
switch item.(map[string]interface{})["type"] {
- case "series":
+ case MediaTypeSeries:
series := &Series{
crunchy: c,
}
@@ -35,7 +35,7 @@ func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err
}
s = append(s, series)
- case "movie_listing":
+ case MediaTypeMovie:
movie := &Movie{
crunchy: c,
}
diff --git a/video.go b/video.go
index 2d76499..04ea5e9 100644
--- a/video.go
+++ b/video.go
@@ -223,7 +223,7 @@ func (s *Series) Similar(limit uint) (ss []*Series, m []*Movie, err error) {
for _, item := range jsonBody["items"].([]interface{}) {
switch item.(map[string]interface{})["type"] {
- case "series":
+ case MediaTypeSeries:
series := &Series{
crunchy: s.crunchy,
}
@@ -235,7 +235,7 @@ func (s *Series) Similar(limit uint) (ss []*Series, m []*Movie, err error) {
}
ss = append(ss, series)
- case "movie_listing":
+ case MediaTypeMovie:
movie := &Movie{
crunchy: s.crunchy,
}
From 0fed0f8d3be02ecd395b4ece8467e40b2b08c2c9 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 27 Jun 2022 22:31:36 +0200
Subject: [PATCH 243/732] Change license to GPL-3.0
---
LICENSE | 687 +++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 650 insertions(+), 37 deletions(-)
diff --git a/LICENSE b/LICENSE
index 36688ae..f288702 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,61 +1,674 @@
-Copyright © 2007 Free Software Foundation, Inc.
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
-Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
-This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below.
-0. Additional Definitions.
+ Preamble
-As used herein, “this License” refers to version 3 of the GNU Lesser General Public License, and the “GNU GPL” refers to version 3 of the GNU General Public License.
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
-“The Library” refers to a covered work governed by this License, other than an Application or a Combined Work as defined below.
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
-An “Application” is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library.
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
-A “Combined Work” is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the “Linked Version”.
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
-The “Minimal Corresponding Source” for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version.
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
-The “Corresponding Application Code” for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work.
-1. Exception to Section 3 of the GNU GPL.
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
-You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL.
-2. Conveying Modified Versions.
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
-If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version:
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
- a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or
- b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy.
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
-3. Object Code Incorporating Material from Library Header Files.
+ The precise terms and conditions for copying, distribution and
+modification follow.
-The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following:
+ TERMS AND CONDITIONS
- a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License.
- b) Accompany the object code with a copy of the GNU GPL and this license document.
+ 0. Definitions.
-4. Combined Works.
+ "This License" refers to version 3 of the GNU General Public License.
-You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following:
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
- a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License.
- b) Accompany the Combined Work with a copy of the GNU GPL and this license document.
- c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document.
- d) Do one of the following:
- 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.
- 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version.
- e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.)
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
-5. Combined Libraries.
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
-You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following:
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
- a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License.
- b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work.
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
-6. Revised Versions of the GNU Lesser General Public License.
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
-The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
-Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation.
+ 1. Source Code.
-If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library.
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ Copyright (C)
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+ .
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+.
From 8a3e42e4d1cb3b4447d878ee04867030d401f9b4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 27 Jun 2022 22:33:26 +0200
Subject: [PATCH 244/732] Remove library & refactor cli
---
.github/workflows/ci.yml | 4 +-
Makefile | 22 +-
README.md | 80 ++--
account.go | 171 --------
category.go | 65 ---
cmd/crunchyroll-go/main.go | 9 -
.../cmd => commands}/archive.go | 4 +-
.../cmd => commands}/download.go | 4 +-
{cmd/crunchyroll-go/cmd => commands}/info.go | 2 +-
.../crunchyroll-go/cmd => commands}/logger.go | 2 +-
{cmd/crunchyroll-go/cmd => commands}/login.go | 20 +-
{cmd/crunchyroll-go/cmd => commands}/root.go | 8 +-
{cmd/crunchyroll-go/cmd => commands}/unix.go | 2 +-
.../crunchyroll-go/cmd => commands}/update.go | 16 +-
{cmd/crunchyroll-go/cmd => commands}/utils.go | 4 +-
.../cmd => commands}/windows.go | 2 +-
comment.go | 285 -------------
common.go | 31 --
crunchyroll-go.1 => crunchy-cli.1 | 49 ++-
crunchylists.go | 177 --------
crunchyroll.go | 327 ---------------
downloader.go | 395 ------------------
episode.go | 342 ---------------
error.go | 17 -
format.go | 52 ---
go.mod | 3 +-
go.sum | 2 +
main.go | 7 +
movie_listing.go | 92 ----
news.go | 55 ---
parse.go | 69 ---
search.go | 193 ---------
season.go | 135 ------
simulcast.go | 45 --
stream.go | 129 ------
subtitle.go | 32 --
suggestions.go | 82 ----
url.go | 128 ------
utils.go | 84 ----
utils/locale.go | 60 ---
utils/sort.go | 158 -------
video.go | 280 -------------
wallpaper.go | 16 -
watch_history.go | 45 --
watchlist.go | 99 -----
45 files changed, 117 insertions(+), 3687 deletions(-)
delete mode 100644 account.go
delete mode 100644 category.go
delete mode 100644 cmd/crunchyroll-go/main.go
rename {cmd/crunchyroll-go/cmd => commands}/archive.go (99%)
rename {cmd/crunchyroll-go/cmd => commands}/download.go (99%)
rename {cmd/crunchyroll-go/cmd => commands}/info.go (98%)
rename {cmd/crunchyroll-go/cmd => commands}/logger.go (99%)
rename {cmd/crunchyroll-go/cmd => commands}/login.go (88%)
rename {cmd/crunchyroll-go/cmd => commands}/root.go (92%)
rename {cmd/crunchyroll-go/cmd => commands}/unix.go (98%)
rename {cmd/crunchyroll-go/cmd => commands}/update.go (87%)
rename {cmd/crunchyroll-go/cmd => commands}/utils.go (99%)
rename {cmd/crunchyroll-go/cmd => commands}/windows.go (97%)
delete mode 100644 comment.go
delete mode 100644 common.go
rename crunchyroll-go.1 => crunchy-cli.1 (78%)
delete mode 100644 crunchylists.go
delete mode 100644 crunchyroll.go
delete mode 100644 downloader.go
delete mode 100644 episode.go
delete mode 100644 error.go
delete mode 100644 format.go
create mode 100644 main.go
delete mode 100644 movie_listing.go
delete mode 100644 news.go
delete mode 100644 parse.go
delete mode 100644 search.go
delete mode 100644 season.go
delete mode 100644 simulcast.go
delete mode 100644 stream.go
delete mode 100644 subtitle.go
delete mode 100644 suggestions.go
delete mode 100644 url.go
delete mode 100644 utils.go
delete mode 100644 utils/locale.go
delete mode 100644 utils/sort.go
delete mode 100644 video.go
delete mode 100644 wallpaper.go
delete mode 100644 watch_history.go
delete mode 100644 watchlist.go
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c54dd3..b2409a0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,7 +14,7 @@ jobs:
go-version: 1.18
- name: Build
- run: go build -v cmd/crunchyroll-go/main.go
+ run: go build -v .
- name: Test
- run: go test -v cmd/crunchyroll-go/main.go
+ run: go test -v .
diff --git a/Makefile b/Makefile
index a747c1d..4266a91 100644
--- a/Makefile
+++ b/Makefile
@@ -6,26 +6,26 @@ DESTDIR=
PREFIX=/usr
build:
- go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
+ go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(BINARY_NAME) .
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
install:
- install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchyroll-go
- ln -sf ./crunchyroll-go $(DESTDIR)$(PREFIX)/bin/crunchy
- install -Dm644 crunchyroll-go.1 $(DESTDIR)$(PREFIX)/share/man/man1/crunchyroll-go.1
- install -Dm644 LICENSE $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
+ install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchy-cli
+ ln -sf ./crunchy-cli $(DESTDIR)$(PREFIX)/bin/crunchy
+ install -Dm644 crunchy-cli.1 $(DESTDIR)$(PREFIX)/share/man/man1/crunchy-cli.1
+ install -Dm644 LICENSE $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
uninstall:
- rm -f $(DESTDIR)$(PREFIX)/bin/crunchyroll-go
+ rm -f $(DESTDIR)$(PREFIX)/bin/crunchy-cli
rm -f $(DESTDIR)$(PREFIX)/bin/crunchy
- rm -f $(DESTDIR)$(PREFIX)/share/man/man1/crunchyroll-go.1
- rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
+ rm -f $(DESTDIR)$(PREFIX)/share/man/man1/crunchy-cli.1
+ rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
release:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
+ 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=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=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
strip $(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index 46862f0..bce05b9 100644
--- a/README.md
+++ b/README.md
@@ -1,41 +1,42 @@
-# crunchyroll-go
+# crunchy-cli
-A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api. To use it, you need a crunchyroll premium account to for full (api) access.
+A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
CLI 🖥️
•
- Library 📚
- •
Disclaimer ☝️
•
License ⚖
+_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility.
+See #39 for more information._
+
# 🖥️ CLI
## ✨ Features
@@ -46,37 +47,42 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
## 💾 Get the executable
-- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
+- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchy-cli/releases/latest) or get it from below:
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchyroll-go
```
-- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+-
+
+ On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 Mac):
- - use `make` (requires `go` to be installed):
+
+
+ Currently not working because the repo got renamed!
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
+ - use `make` (requires `go` to be installed):
```shell
- $ git clone https://github.com/ByteDream/crunchyroll-go
- $ cd crunchyroll-go
+ $ git clone https://github.com/ByteDream/crunchy-cli
+ $ cd crunchy-cli
$ make
$ sudo make install # <- only if you want to install it on your system
```
- - use `go`:
+ - use `go`:
```shell
- $ git clone https://github.com/ByteDream/crunchyroll-go
- $ cd crunchyroll-go
- $ go build -o crunchy cmd/crunchyroll-go/main.go
+ $ git clone https://github.com/ByteDream/crunchy-cli
+ $ cd crunchy-cli
+ $ go build -o crunchy .
```
## 📝 Examples
-_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli), further usages and options are described there.
+_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli), further usages and options are described there.
### Login
@@ -163,7 +169,7 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
| `-d` | `--directory` | Directory to download the video(s) to. |
| `-o` | `--output` | Name of the output file. |
-| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli#archive) for more information. |
+| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli#archive) for more information. |
| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
@@ -198,18 +204,6 @@ These flags you can use across every sub-command:
| `-v` | Shows additional debug output. |
| `-p` | Use a proxy to hide your ip / redirect your traffic. |
-# 📚 Library
-
-Download the library via `go get`
-
-```shell
-$ go get github.com/ByteDream/crunchyroll-go/v3
-```
-
-The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v3).
-
-Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library).
-
# ☝️ Disclaimer
This tool is **ONLY** meant to be used for private purposes. To use this tool you need crunchyroll premium anyway, so there is no reason why rip and share the episodes.
@@ -218,4 +212,4 @@ This tool is **ONLY** meant to be used for private purposes. To use this tool yo
# ⚖ License
-This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file for more details.
+This project is licensed under the GNU General Public License v3.0 (GPL-3.0) - see the [LICENSE](LICENSE) file for more details.
diff --git a/account.go b/account.go
deleted file mode 100644
index cb45a77..0000000
--- a/account.go
+++ /dev/null
@@ -1,171 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- "time"
-)
-
-// Account returns information about the currently logged in crunchyroll account.
-func (c *Crunchyroll) Account() (*Account, error) {
- resp, err := c.request("https://beta.crunchyroll.com/accounts/v1/me", http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- account := &Account{
- crunchy: c,
- }
-
- if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
- return nil, fmt.Errorf("failed to parse 'me' response: %w", err)
- }
-
- resp, err = c.request("https://beta.crunchyroll.com/accounts/v1/me/profile", http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- if err = json.NewDecoder(resp.Body).Decode(&account); err != nil {
- return nil, fmt.Errorf("failed to parse 'profile' response: %w", err)
- }
-
- return account, nil
-}
-
-// Account contains information about a crunchyroll account.
-type Account struct {
- crunchy *Crunchyroll
-
- AccountID string `json:"account_id"`
- ExternalID string `json:"external_id"`
- EmailVerified bool `json:"email_verified"`
- Created time.Time `json:"created"`
-
- Avatar string `json:"avatar"`
- CrBetaOptIn bool `json:"cr_beta_opt_in"`
- Email string `json:"email"`
- MatureContentFlagManga string `json:"mature_content_flag_manga"`
- MaturityRating string `json:"maturity_rating"`
- OptOutAndroidInAppMarketing bool `json:"opt_out_android_in_app_marketing"`
- OptOutFreeTrials bool `json:"opt_out_free_trials"`
- OptOutNewMediaQueueUpdates bool `json:"opt_out_new_media_queue_updates"`
- OptOutNewsletters bool `json:"opt_out_newsletters"`
- OptOutPmUpdates bool `json:"opt_out_pm_updates"`
- OptOutPromotionalUpdates bool `json:"opt_out_promotional_updates"`
- OptOutQueueUpdates bool `json:"opt_out_queue_updates"`
- OptOutStoreDeals bool `json:"opt_out_store_deals"`
- PreferredCommunicationLanguage LOCALE `json:"preferred_communication_language"`
- PreferredContentSubtitleLanguage LOCALE `json:"preferred_content_subtitle_language"`
- QaUser bool `json:"qa_user"`
-
- Username string `json:"username"`
- Wallpaper *Wallpaper `json:"wallpaper"`
-}
-
-// UpdatePreferredEmailLanguage sets in which language emails should be received.
-func (a *Account) UpdatePreferredEmailLanguage(language LOCALE) error {
- err := a.updatePreferences("preferred_communication_language", string(language))
- if err == nil {
- a.PreferredCommunicationLanguage = language
- }
- return err
-}
-
-// UpdatePreferredVideoSubtitleLanguage sets in which language default subtitles should be shown
-func (a *Account) UpdatePreferredVideoSubtitleLanguage(language LOCALE) error {
- err := a.updatePreferences("preferred_content_subtitle_language", string(language))
- if err == nil {
- a.PreferredContentSubtitleLanguage = language
- }
- return err
-}
-
-// UpdateMatureVideoContent sets if mature video content / 18+ content should be shown
-func (a *Account) UpdateMatureVideoContent(enabled bool) error {
- if enabled {
- return a.updatePreferences("maturity_rating", "M3")
- } else {
- return a.updatePreferences("maturity_rating", "M2")
- }
-}
-
-// UpdateMatureMangaContent sets if mature manga content / 18+ content should be shown
-func (a *Account) UpdateMatureMangaContent(enabled bool) error {
- if enabled {
- return a.updatePreferences("mature_content_flag_manga", "1")
- } else {
- return a.updatePreferences("mature_content_flag_manga", "0")
- }
-}
-
-func (a *Account) updatePreferences(name, value string) error {
- endpoint := "https://beta.crunchyroll.com/accounts/v1/me/profile"
- body, _ := json.Marshal(map[string]string{name: value})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = a.crunchy.requestFull(req)
- return err
-}
-
-// ChangePassword changes the password for the current account.
-func (a *Account) ChangePassword(currentPassword, newPassword string) error {
- endpoint := "https://beta.crunchyroll.com/accounts/v1/me/credentials"
- body, _ := json.Marshal(map[string]string{"accountId": a.AccountID, "current_password": currentPassword, "new_password": newPassword})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = a.crunchy.requestFull(req)
- return err
-}
-
-// ChangeEmail changes the email address for the current account.
-func (a *Account) ChangeEmail(currentPassword, newEmail string) error {
- endpoint := "https://beta.crunchyroll.com/accounts/v1/me/credentials"
- body, _ := json.Marshal(map[string]string{"current_password": currentPassword, "email": newEmail})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = a.crunchy.requestFull(req)
- return err
-}
-
-// AvailableWallpapers returns all available wallpapers which can be set as profile wallpaper.
-func (a *Account) AvailableWallpapers() (w []*Wallpaper, err error) {
- endpoint := "https://beta.crunchyroll.com/assets/v1/wallpaper"
- resp, err := a.crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]any
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- err = decodeMapToStruct(jsonBody["items"].([]any), &w)
- return
-}
-
-// ChangeWallpaper changes the profile wallpaper of the current user. Use AvailableWallpapers
-// to get all available ones.
-func (a *Account) ChangeWallpaper(wallpaper *Wallpaper) error {
- endpoint := "https://beta.crunchyroll.com/accounts/v1/me/profile"
- body, _ := json.Marshal(map[string]string{"wallpaper": string(*wallpaper)})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- _, err = a.crunchy.requestFull(req)
- return err
-}
diff --git a/category.go b/category.go
deleted file mode 100644
index eccefff..0000000
--- a/category.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// Categories returns all available categories and possible subcategories.
-func (c *Crunchyroll) Categories(includeSubcategories bool) (ca []*Category, err error) {
- tenantCategoriesEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/tenant_categories?include_subcategories=%t&locale=%s",
- includeSubcategories, c.Locale)
- resp, err := c.request(tenantCategoriesEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'tenant_categories' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- category := &Category{}
- if err := decodeMapToStruct(item, category); err != nil {
- return nil, err
- }
-
- ca = append(ca, category)
- }
-
- return ca, nil
-}
-
-// Category contains all information about a category.
-type Category struct {
- Category string `json:"tenant_category"`
-
- SubCategories []struct {
- Category string `json:"tenant_category"`
- ParentCategory string `json:"parent_category"`
-
- Localization struct {
- Title string `json:"title"`
- Description string `json:"description"`
- Locale LOCALE `json:"locale"`
- } `json:"localization"`
-
- Slug string `json:"slug"`
- } `json:"sub_categories"`
-
- Images struct {
- Background []Image `json:"background"`
- Low []Image `json:"low"`
- } `json:"images"`
-
- Localization struct {
- Title string `json:"title"`
- Description string `json:"description"`
- Locale LOCALE `json:"locale"`
- } `json:"localization"`
-
- Slug string `json:"slug"`
-}
diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go
deleted file mode 100644
index 0ced8aa..0000000
--- a/cmd/crunchyroll-go/main.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package main
-
-import (
- "github.com/ByteDream/crunchyroll-go/v3/cmd/crunchyroll-go/cmd"
-)
-
-func main() {
- cmd.Execute()
-}
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/commands/archive.go
similarity index 99%
rename from cmd/crunchyroll-go/cmd/archive.go
rename to commands/archive.go
index 0d47917..5ad6900 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/commands/archive.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"archive/tar"
@@ -188,7 +188,7 @@ func archive(urls []string) error {
out.StopProgress("Failed to parse url %d", i+1)
if 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, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/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
}
diff --git a/cmd/crunchyroll-go/cmd/download.go b/commands/download.go
similarity index 99%
rename from cmd/crunchyroll-go/cmd/download.go
rename to commands/download.go
index 74cc3f2..82b68bf 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/commands/download.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"context"
@@ -134,7 +134,7 @@ func download(urls []string) error {
out.StopProgress("Failed to parse url %d", i+1)
if 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, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/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
}
diff --git a/cmd/crunchyroll-go/cmd/info.go b/commands/info.go
similarity index 98%
rename from cmd/crunchyroll-go/cmd/info.go
rename to commands/info.go
index f5ed995..fe9693d 100644
--- a/cmd/crunchyroll-go/cmd/info.go
+++ b/commands/info.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"fmt"
diff --git a/cmd/crunchyroll-go/cmd/logger.go b/commands/logger.go
similarity index 99%
rename from cmd/crunchyroll-go/cmd/logger.go
rename to commands/logger.go
index 83bc214..fab8515 100644
--- a/cmd/crunchyroll-go/cmd/logger.go
+++ b/commands/logger.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"fmt"
diff --git a/cmd/crunchyroll-go/cmd/login.go b/commands/login.go
similarity index 88%
rename from cmd/crunchyroll-go/cmd/login.go
rename to commands/login.go
index d8cc5c3..a710bb6 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/commands/login.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"bytes"
@@ -122,13 +122,13 @@ func loginCredentials(user, password string) error {
credentials = []byte(fmt.Sprintf("%s\n%s", user, password))
}
- os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
- if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), credentials, 0600); err != nil {
+ 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, "crunchyroll-go", "crunchy"))
+ "To encrypt it, use the `--encrypt` flag", filepath.Join(configDir, "crunchy-cli", "crunchy"))
}
}
}
@@ -157,11 +157,11 @@ func loginSessionID(sessionID string) error {
if configDir, err := os.UserConfigDir(); err != nil {
return fmt.Errorf("could not save credentials persistent: %w", err)
} else {
- os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
- if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(c.EtpRt), 0600); err != nil {
+ 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, "crunchyroll-go", "crunchy"))
+ 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 {
@@ -187,11 +187,11 @@ func loginEtpRt(etpRt string) error {
if configDir, err := os.UserConfigDir(); err != nil {
return fmt.Errorf("could not save credentials persistent: %w", err)
} else {
- os.MkdirAll(filepath.Join(configDir, "crunchyroll-go"), 0755)
- if err = os.WriteFile(filepath.Join(configDir, "crunchyroll-go", "crunchy"), []byte(etpRt), 0600); err != nil {
+ 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, "crunchyroll-go", "crunchy"))
+ 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 {
diff --git a/cmd/crunchyroll-go/cmd/root.go b/commands/root.go
similarity index 92%
rename from cmd/crunchyroll-go/cmd/root.go
rename to commands/root.go
index 02873f1..ffcf35a 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/commands/root.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"context"
@@ -27,9 +27,9 @@ var (
)
var rootCmd = &cobra.Command{
- Use: "crunchyroll-go",
+ 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/crunchyroll-go/wiki",
+ 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,
@@ -54,7 +54,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&proxyFlag, "proxy", "p", "", "Proxy to use")
- rootCmd.PersistentFlags().StringVar(&useragentFlag, "useragent", fmt.Sprintf("crunchyroll-go/%s", Version), "Useragent to do all request with")
+ rootCmd.PersistentFlags().StringVar(&useragentFlag, "useragent", fmt.Sprintf("crunchy-cli/%s", Version), "Useragent to do all request with")
}
func Execute() {
diff --git a/cmd/crunchyroll-go/cmd/unix.go b/commands/unix.go
similarity index 98%
rename from cmd/crunchyroll-go/cmd/unix.go
rename to commands/unix.go
index 962088f..955695c 100644
--- a/cmd/crunchyroll-go/cmd/unix.go
+++ b/commands/unix.go
@@ -1,6 +1,6 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
-package cmd
+package commands
import (
"bufio"
diff --git a/cmd/crunchyroll-go/cmd/update.go b/commands/update.go
similarity index 87%
rename from cmd/crunchyroll-go/cmd/update.go
rename to commands/update.go
index c8512e6..fb4c1e9 100644
--- a/cmd/crunchyroll-go/cmd/update.go
+++ b/commands/update.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"encoding/json"
@@ -38,7 +38,7 @@ func init() {
func update() error {
var release map[string]interface{}
- resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchyroll-go/releases/latest")
+ resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
if err != nil {
return err
}
@@ -80,20 +80,20 @@ func update() error {
return nil
}
- out.Info("A new version is available (%s): https://github.com/ByteDream/crunchyroll-go/releases/tag/v%s", releaseVersion, releaseVersion)
+ out.Info("A new version is available (%s): https://github.com/ByteDream/crunchy-cli/releases/tag/v%s", releaseVersion, releaseVersion)
if updateInstallFlag {
if runtime.GOARCH != "amd64" {
return fmt.Errorf("invalid architecture found (%s), only amd64 is currently supported for automatic updating. "+
- "You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOARCH)
+ "You have to update manually (https://github.com/ByteDream/crunchy-cli)", runtime.GOARCH)
}
var downloadFile string
switch runtime.GOOS {
case "linux":
- yayCommand := exec.Command("pacman -Q crunchyroll-go")
+ yayCommand := exec.Command("pacman -Q crunchy-cli")
if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
- out.Info("crunchyroll-go was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
+ out.Info("crunchy-cli was probably installed via an Arch Linux AUR helper (like yay). Updating via this AUR helper is recommended")
return nil
}
downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
@@ -103,7 +103,7 @@ func update() error {
downloadFile = fmt.Sprintf("crunchy-v%s_windows.exe", releaseVersion)
default:
return fmt.Errorf("invalid operation system found (%s), only linux, windows and darwin / macos are currently supported. "+
- "You have to update manually (https://github.com/ByteDream/crunchyroll-go)", runtime.GOOS)
+ "You have to update manually (https://github.com/ByteDream/crunchy-cli", runtime.GOOS)
}
out.SetProgress("Updating executable %s", os.Args[0])
@@ -119,7 +119,7 @@ func update() error {
}
defer executeFile.Close()
- resp, err := client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchyroll-go/releases/download/v%s/%s", releaseVersion, downloadFile))
+ resp, err := client.Get(fmt.Sprintf("https://github.com/ByteDream/crunchy-cli/releases/download/v%s/%s", releaseVersion, downloadFile))
if err != nil {
return err
}
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/commands/utils.go
similarity index 99%
rename from cmd/crunchyroll-go/cmd/utils.go
rename to commands/utils.go
index 12d3d4e..486c61f 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/commands/utils.go
@@ -1,4 +1,4 @@
-package cmd
+package commands
import (
"crypto/aes"
@@ -161,7 +161,7 @@ func loadCrunchy() {
}
if configDir, err := os.UserConfigDir(); err == nil {
- persistentFilePath := filepath.Join(configDir, "crunchyroll-go", "crunchy")
+ persistentFilePath := filepath.Join(configDir, "crunchy-cli", "crunchy")
if _, statErr := os.Stat(persistentFilePath); statErr == nil {
body, err := os.ReadFile(persistentFilePath)
if err != nil {
diff --git a/cmd/crunchyroll-go/cmd/windows.go b/commands/windows.go
similarity index 97%
rename from cmd/crunchyroll-go/cmd/windows.go
rename to commands/windows.go
index d6eecb1..2ae47b5 100644
--- a/cmd/crunchyroll-go/cmd/windows.go
+++ b/commands/windows.go
@@ -1,6 +1,6 @@
//go:build windows
-package cmd
+package commands
import (
"bufio"
diff --git a/comment.go b/comment.go
deleted file mode 100644
index d58fade..0000000
--- a/comment.go
+++ /dev/null
@@ -1,285 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- "time"
-)
-
-type Comment struct {
- crunchy *Crunchyroll
-
- EpisodeID string `json:"episode_id"`
-
- CommentID string `json:"comment_id"`
- DomainID string `json:"domain_id"`
-
- GuestbookKey string `json:"guestbook_key"`
-
- User struct {
- UserKey string `json:"user_key"`
- UserAttributes struct {
- Username string `json:"username"`
- Avatar struct {
- Locked []Image `json:"locked"`
- Unlocked []Image `json:"unlocked"`
- } `json:"avatar"`
- } `json:"user_attributes"`
- UserFlags []any `json:"user_flags"`
- } `json:"user"`
-
- Message string `json:"message"`
- ParentCommentID int `json:"parent_comment_id"`
-
- Locale LOCALE `json:"locale"`
-
- UserVotes []string `json:"user_votes"`
- Flags []string `json:"flags"`
- Votes struct {
- Inappropriate int `json:"inappropriate"`
- Like int `json:"like"`
- Spoiler int `json:"spoiler"`
- } `json:"votes"`
-
- DeleteReason any `json:"delete_reason"`
-
- Created time.Time `json:"created"`
- Modified time.Time `json:"modified"`
-
- IsOwner bool `json:"is_owner"`
- RepliesCount int `json:"replies_count"`
-}
-
-// Delete deleted the current comment. Works only if the user has written the comment.
-func (c *Comment) Delete() error {
- if !c.IsOwner {
- return fmt.Errorf("cannot delete, user is not the comment author")
- }
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
- resp, err := c.crunchy.request(endpoint, http.MethodDelete)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- // the api returns a new comment object when modifying it.
- // hopefully this does not change
- json.NewDecoder(resp.Body).Decode(c)
-
- return nil
-}
-
-// IsSpoiler returns if the comment is marked as spoiler or not.
-func (c *Comment) IsSpoiler() bool {
- for _, flag := range c.Flags {
- if flag == "spoiler" {
- return true
- }
- }
- return false
-}
-
-// MarkAsSpoiler marks the current comment as spoiler. Works only if the user has written the comment,
-// and it isn't already marked as spoiler.
-func (c *Comment) MarkAsSpoiler() error {
- if !c.IsOwner {
- return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
- } else if c.votedAs("spoiler") {
- return fmt.Errorf("comment is already marked as spoiler")
- }
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
- body, _ := json.Marshal(map[string][]string{"add": {"spoiler"}})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := c.crunchy.requestFull(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- json.NewDecoder(resp.Body).Decode(c)
-
- return nil
-}
-
-// UnmarkAsSpoiler unmarks the current comment as spoiler. Works only if the user has written the comment,
-// and it is already marked as spoiler.
-func (c *Comment) UnmarkAsSpoiler() error {
- if !c.IsOwner {
- return fmt.Errorf("cannot mark as spoiler, user is not the comment author")
- } else if !c.votedAs("spoiler") {
- return fmt.Errorf("comment is not marked as spoiler")
- }
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/flags?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
- body, _ := json.Marshal(map[string][]string{"remove": {"spoiler"}})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := c.crunchy.requestFull(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- json.NewDecoder(resp.Body).Decode(c)
-
- return nil
-}
-
-// Like likes the comment. Works only if the user hasn't already liked it.
-func (c *Comment) Like() error {
- if err := c.vote("like", "liked"); err != nil {
- return err
- }
- c.Votes.Like += 1
-
- return nil
-}
-
-// Liked returns if the user has liked the comment.
-func (c *Comment) Liked() bool {
- return c.votedAs("liked")
-}
-
-// RemoveLike removes the like from the comment. Works only if the user has liked it.
-func (c *Comment) RemoveLike() error {
- if err := c.unVote("like", "liked"); err != nil {
- return err
- }
- c.Votes.Like -= 1
-
- return nil
-}
-
-// Reply replies to the current comment.
-func (c *Comment) Reply(message string, spoiler bool) (*Comment, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", c.EpisodeID, c.crunchy.Locale)
- var flags []string
- if spoiler {
- flags = append(flags, "spoiler")
- }
- body, _ := json.Marshal(map[string]any{"locale": string(c.crunchy.Locale), "message": message, "flags": flags, "parent_id": c.CommentID})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return nil, err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := c.crunchy.requestFull(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- reply := &Comment{}
- if err = json.NewDecoder(resp.Body).Decode(reply); err != nil {
- return nil, err
- }
-
- return reply, nil
-}
-
-// Replies shows all replies to the current comment.
-func (c *Comment) Replies(page uint, size uint) ([]*Comment, error) {
- if c.RepliesCount == 0 {
- return []*Comment{}, nil
- }
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/replies?page_size=%d&page=%d&locale=%s", c.EpisodeID, c.CommentID, size, page, c.Locale)
- resp, err := c.crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]any
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- var comments []*Comment
- if err = decodeMapToStruct(jsonBody["items"].([]any), &comments); err != nil {
- return nil, err
- }
- return comments, nil
-}
-
-// Report reports the comment. Only works if the comment hasn't been reported yet.
-func (c *Comment) Report() error {
- return c.vote("inappropriate", "reported")
-}
-
-func (c *Comment) IsReported() bool {
- return c.votedAs("reported")
-}
-
-// RemoveReport removes the report request from the comment. Only works if the user
-// has reported the comment.
-func (c *Comment) RemoveReport() error {
- return c.unVote("inappropriate", "reported")
-}
-
-// FlagAsSpoiler sends a request to the user (and / or crunchyroll?) to mark the comment
-// as spoiler. Only works if the comment hasn't been flagged as spoiler yet.
-func (c *Comment) FlagAsSpoiler() error {
- return c.vote("spoiler", "spoiler")
-}
-
-func (c *Comment) IsFlaggedAsSpoiler() bool {
- return c.votedAs("spoiler")
-}
-
-// UnflagAsSpoiler rewokes the request to the user (and / or crunchyroll?) to mark the
-// comment as spoiler. Only works if the user has flagged the comment as spoiler.
-func (c *Comment) UnflagAsSpoiler() error {
- return c.unVote("spoiler", "spoiler")
-}
-
-func (c *Comment) votedAs(voteType string) bool {
- for _, userVote := range c.UserVotes {
- if userVote == voteType {
- return true
- }
- }
- return false
-}
-
-func (c *Comment) vote(voteType, readableName string) error {
- if c.votedAs(voteType) {
- return fmt.Errorf("comment is already marked as %s", readableName)
- }
-
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?locale=%s", c.EpisodeID, c.CommentID, c.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"vote_type": voteType})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = c.crunchy.requestFull(req)
- if err != nil {
- return err
- }
- c.UserVotes = append(c.UserVotes, voteType)
-
- return nil
-}
-
-func (c *Comment) unVote(voteType, readableName string) error {
- for i, userVote := range c.UserVotes {
- if userVote == voteType {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments/%s/votes?vote_type=%s&locale=%s", c.EpisodeID, c.CommentID, voteType, c.crunchy.Locale)
- _, err := c.crunchy.request(endpoint, http.MethodDelete)
- if err != nil {
- return err
- }
- c.UserVotes = append(c.UserVotes[:i], c.UserVotes[i+1:]...)
- return nil
- }
- }
-
- return fmt.Errorf("comment is not marked as %s", readableName)
-}
diff --git a/common.go b/common.go
deleted file mode 100644
index b76a8fe..0000000
--- a/common.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package crunchyroll
-
-type Image struct {
- Height int `json:"height"`
- Source string `json:"source"`
- Type string `json:"type"`
- Width int `json:"width"`
-}
-
-type Panel struct {
- Title string `json:"title"`
- PromoTitle string `json:"promo_title"`
- Slug string `json:"slug"`
- Playback string `json:"playback"`
- PromoDescription string `json:"promo_description"`
- Images struct {
- Thumbnail [][]Image `json:"thumbnail"`
- PosterTall [][]Image `json:"poster_tall"`
- PosterWide [][]Image `json:"poster_wide"`
- } `json:"images"`
- ID string `json:"id"`
- Description string `json:"description"`
- ChannelID string `json:"channel_id"`
- Type WatchlistEntryType `json:"type"`
- ExternalID string `json:"external_id"`
- SlugTitle string `json:"slug_title"`
- // not null if Type is WATCHLISTENTRYEPISODE
- EpisodeMetadata *Episode `json:"episode_metadata"`
- // not null if Type is WATCHLISTENTRYSERIES
- SeriesMetadata *Series `json:"series_metadata"`
-}
diff --git a/crunchyroll-go.1 b/crunchy-cli.1
similarity index 78%
rename from crunchyroll-go.1
rename to crunchy-cli.1
index 576c291..d9b1075 100644
--- a/crunchyroll-go.1
+++ b/crunchy-cli.1
@@ -1,24 +1,24 @@
-.TH crunchyroll-go 1 "21 March 2022" "crunchyroll-go" "Crunchyroll Downloader"
+.TH crunchy-cli 1 "27 June 2022" "crunchy-cli" "Crunchyroll Cli Client"
.SH NAME
-crunchyroll-go - A cli for downloading videos and entire series from crunchyroll.
+crunchy-cli - A cli for downloading videos and entire series from crunchyroll.
.SH SYNOPSIS
-crunchyroll-go [\fB-h\fR] [\fB-p\fR \fIPROXY\fR] [\fB-q\fR] [\fB-v\fR]
+crunchy-cli [\fB-h\fR] [\fB-p\fR \fIPROXY\fR] [\fB-q\fR] [\fB-v\fR]
.br
-crunchyroll-go help
+crunchy-cli help
.br
-crunchyroll-go login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR] [\fIusername\fR, \fIpassword\fR]
+crunchy-cli login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR] [\fIusername\fR, \fIpassword\fR]
.br
-crunchyroll-go download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
+crunchy-cli download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.br
-crunchyroll-go archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
+crunchy-cli archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
.br
-crunchyroll-go update [\fB-i\fR \fIINSTALL\fR]
+crunchy-cli update [\fB-i\fR \fIINSTALL\fR]
.SH DESCRIPTION
.TP
-With \fBcrunchyroll-go\fR you can easily download video and series from crunchyroll.
+With \fBcrunchy-cli\fR you can easily download video and series from crunchyroll.
.TP
Note that you need an \fBcrunchyroll premium\fR account in order to use this tool!
@@ -167,27 +167,27 @@ The \fBS\fR, followed by the number indicates the season number, \fBE\fR, follow
.SH EXAMPLES
Login via crunchyroll account email and password.
.br
-$ crunchyroll-go login user@example.com 12345678
+$ crunchy-cli login user@example.com 12345678
Download a episode normally. Your system locale will be used for the video's audio.
.br
-$ crunchyroll-go download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with 720p and name it to 'darling.mp4'. Note that you need \fBffmpeg\fR to save files which do not have '.ts' as file extension.
.br
-$ crunchyroll-go download -o "darling.mp4" -r 720p https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli download -o "darling.mp4" -r 720p https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with japanese audio and american subtitles.
.br
-$ crunchyroll-go download -a ja-JP -s en-US https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
+$ crunchy-cli download -a ja-JP -s en-US https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
Stores the episode in a .mkv file.
.br
-$ crunchyroll-go archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
.br
-$ crunchyroll-go archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
+$ crunchy-cli archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
.SH BUGS
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
@@ -195,22 +195,21 @@ If you notice any bug or want an enhancement, feel free to create a new issue or
.SH AUTHOR
ByteDream
.br
-Source: https://github.com/ByteDream/crunchyroll-go
+Source: https://github.com/ByteDream/crunchy-cli
.SH COPYRIGHT
Copyright (C) 2022 ByteDream
-This program is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 3 of the License, or (at your option) any later version.
+This program is free software: you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3
+of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
-Lesser General Public License for more details.
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
-You should have received a copy of the GNU Lesser General Public License
-along with this program; if not, write to the Free Software Foundation,
-Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
diff --git a/crunchylists.go b/crunchylists.go
deleted file mode 100644
index a297da2..0000000
--- a/crunchylists.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- "time"
-)
-
-// Crunchylists returns a struct to control crunchylists.
-func (c *Crunchyroll) Crunchylists() (*Crunchylists, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", c.Config.AccountID, c.Locale)
- resp, err := c.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- crunchylists := &Crunchylists{
- crunchy: c,
- }
- json.NewDecoder(resp.Body).Decode(crunchylists)
- for _, item := range crunchylists.Items {
- item.crunchy = c
- }
-
- return crunchylists, nil
-}
-
-type Crunchylists struct {
- crunchy *Crunchyroll
-
- Items []*CrunchylistPreview `json:"items"`
- TotalPublic int `json:"total_public"`
- TotalPrivate int `json:"total_private"`
- MaxPrivate int `json:"max_private"`
-}
-
-// Create creates a new crunchylist with the given name. Duplicate names for lists are allowed.
-func (cl *Crunchylists) Create(name string) (*Crunchylist, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s?locale=%s", cl.crunchy.Config.AccountID, cl.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"title": name})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return nil, err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := cl.crunchy.requestFull(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- return crunchylistFromID(cl.crunchy, jsonBody["list_id"].(string))
-}
-
-type CrunchylistPreview struct {
- crunchy *Crunchyroll
-
- ListID string `json:"list_id"`
- IsPublic bool `json:"is_public"`
- Total int `json:"total"`
- ModifiedAt time.Time `json:"modified_at"`
- Title string `json:"title"`
-}
-
-// Crunchylist returns the belonging Crunchylist struct.
-func (clp *CrunchylistPreview) Crunchylist() (*Crunchylist, error) {
- return crunchylistFromID(clp.crunchy, clp.ListID)
-}
-
-type Crunchylist struct {
- crunchy *Crunchyroll
-
- ID string `json:"id"`
-
- Max int `json:"max"`
- Total int `json:"total"`
- Title string `json:"title"`
- IsPublic bool `json:"is_public"`
- ModifiedAt time.Time `json:"modified_at"`
- Items []*CrunchylistItem `json:"items"`
-}
-
-// AddSeries adds a series.
-func (cl *Crunchylist) AddSeries(series *Series) error {
- return cl.AddSeriesFromID(series.ID)
-}
-
-// AddSeriesFromID adds a series from its id
-func (cl *Crunchylist) AddSeriesFromID(id string) error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"content_id": id})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = cl.crunchy.requestFull(req)
- return err
-}
-
-// RemoveSeries removes a series
-func (cl *Crunchylist) RemoveSeries(series *Series) error {
- return cl.RemoveSeriesFromID(series.ID)
-}
-
-// RemoveSeriesFromID removes a series by its id
-func (cl *Crunchylist) RemoveSeriesFromID(id string) error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, id, cl.crunchy.Locale)
- _, err := cl.crunchy.request(endpoint, http.MethodDelete)
- return err
-}
-
-// Delete deleted the current crunchylist.
-func (cl *Crunchylist) Delete() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
- _, err := cl.crunchy.request(endpoint, http.MethodDelete)
- return err
-}
-
-// Rename renames the current crunchylist.
-func (cl *Crunchylist) Rename(name string) error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", cl.crunchy.Config.AccountID, cl.ID, cl.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"title": name})
- req, err := http.NewRequest(http.MethodPatch, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = cl.crunchy.requestFull(req)
- if err == nil {
- cl.Title = name
- }
- return err
-}
-
-func crunchylistFromID(crunchy *Crunchyroll, id string) (*Crunchylist, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s?locale=%s", crunchy.Config.AccountID, id, crunchy.Locale)
- resp, err := crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- crunchyList := &Crunchylist{
- crunchy: crunchy,
- ID: id,
- }
- if err := json.NewDecoder(resp.Body).Decode(crunchyList); err != nil {
- return nil, err
- }
- for _, item := range crunchyList.Items {
- item.crunchy = crunchy
- }
- return crunchyList, nil
-}
-
-type CrunchylistItem struct {
- crunchy *Crunchyroll
-
- ListID string `json:"list_id"`
- ID string `json:"id"`
- ModifiedAt time.Time `json:"modified_at"`
- Panel Panel `json:"panel"`
-}
-
-// Remove removes the current item from its crunchylist.
-func (cli *CrunchylistItem) Remove() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/custom-lists/%s/%s/%s", cli.crunchy.Config.AccountID, cli.ListID, cli.ID)
- _, err := cli.crunchy.request(endpoint, http.MethodDelete)
- return err
-}
diff --git a/crunchyroll.go b/crunchyroll.go
deleted file mode 100644
index c697034..0000000
--- a/crunchyroll.go
+++ /dev/null
@@ -1,327 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
-)
-
-// LOCALE represents a locale / language.
-type LOCALE string
-
-const (
- JP LOCALE = "ja-JP"
- US = "en-US"
- LA = "es-419"
- ES = "es-ES"
- FR = "fr-FR"
- PT = "pt-PT"
- BR = "pt-BR"
- IT = "it-IT"
- DE = "de-DE"
- RU = "ru-RU"
- AR = "ar-SA"
-)
-
-// MediaType represents a media type.
-type MediaType string
-
-const (
- MediaTypeSeries MediaType = "series"
- MediaTypeMovie = "movie_listing"
-)
-
-type loginResponse struct {
- AccessToken string `json:"access_token"`
- ExpiresIn int `json:"expires_in"`
- TokenType string `json:"token_type"`
- Scope string `json:"scope"`
- Country string `json:"country"`
- AccountID string `json:"account_id"`
-}
-
-// LoginWithCredentials logs in via crunchyroll username or email and password.
-func LoginWithCredentials(user string, password string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- endpoint := "https://beta-api.crunchyroll.com/auth/v1/token"
- values := url.Values{}
- values.Set("username", user)
- values.Set("password", password)
- values.Set("grant_type", "password")
-
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(values.Encode()))
- if err != nil {
- return nil, err
- }
- req.Header.Set("Authorization", "Basic aHJobzlxM2F3dnNrMjJ1LXRzNWE6cHROOURteXRBU2Z6QjZvbXVsSzh6cUxzYTczVE1TY1k=")
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-
- resp, err := request(req, client)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var loginResp loginResponse
- json.NewDecoder(resp.Body).Decode(&loginResp)
-
- var etpRt string
- for _, cookie := range resp.Cookies() {
- if cookie.Name == "etp_rt" {
- etpRt = cookie.Value
- break
- }
- }
-
- return postLogin(loginResp, etpRt, locale, client)
-}
-
-// LoginWithSessionID logs in via a crunchyroll session id.
-// Session ids are automatically generated as a cookie when visiting https://www.crunchyroll.com.
-//
-// Deprecated: Login via session id caused some trouble in the past (e.g. #15 or #30) which resulted in
-// login not working. Use LoginWithEtpRt instead. EtpRt practically the crunchyroll beta equivalent to
-// a session id.
-// The method will stay in the library until session id login is removed completely or login with it
-// does not work for a longer period of time.
-func LoginWithSessionID(sessionID string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- endpoint := fmt.Sprintf("https://api.crunchyroll.com/start_session.0.json?session_id=%s",
- sessionID)
- resp, err := client.Get(endpoint)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("failed to start session: %s", resp.Status)
- }
-
- var jsonBody map[string]any
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse start session with session id response: %w", err)
- }
- if isError, ok := jsonBody["error"]; ok && isError.(bool) {
- return nil, fmt.Errorf("invalid session id (%s): %s", jsonBody["message"].(string), jsonBody["code"])
- }
-
- var etpRt string
- for _, cookie := range resp.Cookies() {
- if cookie.Name == "etp_rt" {
- etpRt = cookie.Value
- break
- }
- }
-
- return LoginWithEtpRt(etpRt, locale, client)
-}
-
-// LoginWithEtpRt logs in via the crunchyroll etp rt cookie. This cookie is the crunchyroll beta
-// equivalent to the classic session id.
-// The etp_rt cookie is automatically set when visiting https://beta.crunchyroll.com. Note that you
-// need a crunchyroll account to access it.
-func LoginWithEtpRt(etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- endpoint := "https://beta-api.crunchyroll.com/auth/v1/token"
- grantType := url.Values{}
- grantType.Set("grant_type", "etp_rt_cookie")
-
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(grantType.Encode()))
- if err != nil {
- return nil, err
- }
- req.Header.Add("Authorization", "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6")
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- req.AddCookie(&http.Cookie{
- Name: "etp_rt",
- Value: etpRt,
- })
- resp, err := request(req, client)
- if err != nil {
- return nil, err
- }
-
- var loginResp loginResponse
- json.NewDecoder(resp.Body).Decode(&loginResp)
-
- return postLogin(loginResp, etpRt, locale, client)
-}
-
-func postLogin(loginResp loginResponse, etpRt string, locale LOCALE, client *http.Client) (*Crunchyroll, error) {
- crunchy := &Crunchyroll{
- Client: client,
- Context: context.Background(),
- Locale: locale,
- EtpRt: etpRt,
- cache: true,
- }
-
- crunchy.Config.TokenType = loginResp.TokenType
- crunchy.Config.AccessToken = loginResp.AccessToken
- crunchy.Config.AccountID = loginResp.AccountID
- crunchy.Config.CountryCode = loginResp.Country
-
- var jsonBody map[string]any
-
- endpoint := "https://beta-api.crunchyroll.com/index/v2"
- resp, err := crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- cms := jsonBody["cms"].(map[string]any)
- crunchy.Config.Premium = strings.HasSuffix(crunchy.Config.Bucket, "crunchyroll")
- // / is trimmed so that urls which require it must be in .../{bucket}/... like format.
- // this just looks cleaner
- crunchy.Config.Bucket = strings.TrimPrefix(cms["bucket"].(string), "/")
- crunchy.Config.Policy = cms["policy"].(string)
- crunchy.Config.Signature = cms["signature"].(string)
- crunchy.Config.KeyPairID = cms["key_pair_id"].(string)
-
- endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me"
- resp, err = crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
- crunchy.Config.ExternalID = jsonBody["external_id"].(string)
-
- endpoint = "https://beta-api.crunchyroll.com/accounts/v1/me/profile"
- resp, err = crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- json.NewDecoder(resp.Body).Decode(&jsonBody)
- crunchy.Config.MaturityRating = jsonBody["maturity_rating"].(string)
-
- return crunchy, nil
-}
-
-type Crunchyroll struct {
- // Client is the http.Client to perform all requests over.
- Client *http.Client
- // Context can be used to stop requests with Client and is context.Background by default.
- Context context.Context
- // Locale specifies in which language all results should be returned / requested.
- Locale LOCALE
- // EtpRt is the crunchyroll beta equivalent to a session id (prior SessionID field in
- // this struct in v2 and below).
- EtpRt string
-
- // Config stores parameters which are needed by some api calls.
- Config struct {
- TokenType string
- AccessToken string
-
- Bucket string
-
- CountryCode string
- Premium bool
- Channel string
- Policy string
- Signature string
- KeyPairID string
- AccountID string
- ExternalID string
- MaturityRating string
- }
-
- // If cache is true, internal caching is enabled.
- cache bool
-}
-
-// InvalidateSession logs the user out which invalidates the current session.
-// You have to call a login method again and create a new Crunchyroll instance
-// if you want to perform any further actions since this instance is not usable
-// anymore after calling this.
-func (c *Crunchyroll) InvalidateSession() error {
- endpoint := "https://crunchyroll.com/logout"
- _, err := c.request(endpoint, http.MethodGet)
- return err
-}
-
-// IsCaching returns if data gets cached or not.
-// See SetCaching for more information.
-func (c *Crunchyroll) IsCaching() bool {
- return c.cache
-}
-
-// SetCaching enables or disables internal caching of requests made.
-// Caching is enabled by default.
-// If it is disabled the already cached data still gets called.
-// The best way to prevent this is to create a complete new Crunchyroll struct.
-func (c *Crunchyroll) SetCaching(caching bool) {
- c.cache = caching
-}
-
-// request is a base function which handles simple api requests.
-func (c *Crunchyroll) request(endpoint string, method string) (*http.Response, error) {
- req, err := http.NewRequest(method, endpoint, nil)
- if err != nil {
- return nil, err
- }
- return c.requestFull(req)
-}
-
-// requestFull is a base function which handles full user controlled api requests.
-func (c *Crunchyroll) requestFull(req *http.Request) (*http.Response, error) {
- req.Header.Add("Authorization", fmt.Sprintf("%s %s", c.Config.TokenType, c.Config.AccessToken))
-
- return request(req, c.Client)
-}
-
-func request(req *http.Request, client *http.Client) (*http.Response, error) {
- resp, err := client.Do(req)
- if err == nil {
- var buf bytes.Buffer
- io.Copy(&buf, resp.Body)
- defer resp.Body.Close()
- defer func() {
- resp.Body = io.NopCloser(&buf)
- }()
-
- if buf.Len() != 0 {
- var errMap map[string]any
-
- if err = json.Unmarshal(buf.Bytes(), &errMap); err != nil {
- return nil, &RequestError{Response: resp, Message: fmt.Sprintf("invalid json response: %w", err)}
- }
-
- if val, ok := errMap["error"]; ok {
- if errorAsString, ok := val.(string); ok {
- if code, ok := errMap["code"].(string); ok {
- return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", errorAsString, code)}
- }
- return nil, &RequestError{Response: resp, Message: errorAsString}
- } else if errorAsBool, ok := val.(bool); ok && errorAsBool {
- if msg, ok := errMap["message"].(string); ok {
- return nil, &RequestError{Response: resp, Message: msg}
- }
- }
- } else if _, ok := errMap["code"]; ok {
- if errContext, ok := errMap["context"].([]any); ok && len(errContext) > 0 {
- errField := errContext[0].(map[string]any)
- var code string
- if code, ok = errField["message"].(string); !ok {
- code = errField["code"].(string)
- }
- return nil, &RequestError{Response: resp, Message: fmt.Sprintf("%s - %s", code, errField["field"].(string))}
- } else if errMessage, ok := errMap["message"].(string); ok {
- return nil, &RequestError{Response: resp, Message: errMessage}
- }
- }
- }
-
- if resp.StatusCode >= 400 {
- return nil, &RequestError{Response: resp, Message: resp.Status}
- }
- }
- return resp, err
-}
diff --git a/downloader.go b/downloader.go
deleted file mode 100644
index 05622cc..0000000
--- a/downloader.go
+++ /dev/null
@@ -1,395 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "context"
- "crypto/aes"
- "crypto/cipher"
- "fmt"
- "github.com/grafov/m3u8"
- "io"
- "math"
- "net/http"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- "sync/atomic"
- "time"
-)
-
-// NewDownloader creates a downloader with default settings which should
-// fit the most needs.
-func NewDownloader(context context.Context, writer io.Writer, goroutines int, onSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error) Downloader {
- tmp, _ := os.MkdirTemp("", "crunchy_")
-
- return Downloader{
- Writer: writer,
- TempDir: tmp,
- DeleteTempAfter: true,
- Context: context,
- Goroutines: goroutines,
- OnSegmentDownload: onSegmentDownload,
- }
-}
-
-// Downloader is used to download Format's
-type Downloader struct {
- // The output is all written to Writer.
- Writer io.Writer
-
- // TempDir is the directory where the temporary segment files should be stored.
- // The files will be placed directly into the root of the directory.
- // If empty a random temporary directory on the system's default tempdir
- // will be created.
- // If the directory does not exist, it will be created.
- TempDir string
- // If DeleteTempAfter is true, the temp directory gets deleted afterwards.
- // Note that in case of a hard signal exit (os.Interrupt, ...) the directory
- // will NOT be deleted. In such situations try to catch the signal and
- // cancel Context.
- DeleteTempAfter bool
-
- // Context to control the download process with.
- // There is a tiny delay when canceling the context and the actual stop of the
- // process. So it is not recommend stopping the program immediately after calling
- // the cancel function. It's better when canceling it and then exit the program
- // when Format.Download throws an error. See the signal handling section in
- // cmd/crunchyroll-go/cmd/download.go for an example.
- Context context.Context
-
- // Goroutines is the number of goroutines to download segments with.
- Goroutines int
-
- // A method to call when a segment was downloaded.
- // Note that the segments are downloaded asynchronously (depending on the count of
- // Goroutines) and the function gets called asynchronously too, so for example it is
- // first called on segment 1, then segment 254, then segment 3 and so on.
- OnSegmentDownload func(segment *m3u8.MediaSegment, current, total int, file *os.File) error
- // If LockOnSegmentDownload is true, only one OnSegmentDownload function can be called at
- // once. Normally (because of the use of goroutines while downloading) multiple could get
- // called simultaneously.
- LockOnSegmentDownload bool
-
- // If FFmpegOpts is not nil, ffmpeg will be used to merge and convert files.
- // The given opts will be used as ffmpeg parameters while merging.
- //
- // If Writer is *os.File and -f (which sets the output format) is not specified, the output
- // format will be retrieved by its file ending. If this is not the case and -f is not given,
- // the output format will be mpegts / mpeg transport stream.
- // Execute 'ffmpeg -muxers' to see all available output formats.
- FFmpegOpts []string
-}
-
-// download downloads the given format.
-func (d Downloader) download(format *Format) error {
- if err := format.InitVideo(); err != nil {
- return err
- }
-
- if _, err := os.Stat(d.TempDir); os.IsNotExist(err) {
- if err = os.Mkdir(d.TempDir, 0700); err != nil {
- return err
- }
- }
- if d.DeleteTempAfter {
- defer os.RemoveAll(d.TempDir)
- }
-
- files, err := d.downloadSegments(format)
- if err != nil {
- return err
- }
- if d.FFmpegOpts == nil {
- return d.mergeSegments(files)
- } else {
- return d.mergeSegmentsFFmpeg(files)
- }
-}
-
-// mergeSegments reads every file in tempDir and writes their content to Downloader.Writer.
-// The given output file gets created or overwritten if already existing.
-func (d Downloader) mergeSegments(files []string) error {
- for _, file := range files {
- select {
- case <-d.Context.Done():
- return d.Context.Err()
- default:
- f, err := os.Open(file)
- if err != nil {
- return err
- }
- if _, err = io.Copy(d.Writer, f); err != nil {
- f.Close()
- return err
- }
- f.Close()
- }
- }
- return nil
-}
-
-// mergeSegmentsFFmpeg reads every file in tempDir and merges their content to the outputFile
-// with ffmpeg (https://ffmpeg.org/).
-// The given output file gets created or overwritten if already existing.
-func (d Downloader) mergeSegmentsFFmpeg(files []string) error {
- list, err := os.Create(filepath.Join(d.TempDir, "list.txt"))
- if err != nil {
- return err
- }
-
- for _, file := range files {
- if _, err = fmt.Fprintf(list, "file '%s'\n", file); err != nil {
- list.Close()
- return err
- }
- }
- list.Close()
-
- // predefined options ... custom options ... predefined output filename
- command := []string{
- "-y",
- "-f", "concat",
- "-safe", "0",
- "-i", list.Name(),
- "-c", "copy",
- }
- if d.FFmpegOpts != nil {
- command = append(command, d.FFmpegOpts...)
- }
-
- var tmpfile string
- if _, ok := d.Writer.(*io.PipeWriter); !ok {
- if file, ok := d.Writer.(*os.File); ok {
- tmpfile = file.Name()
- }
- }
- if filepath.Ext(tmpfile) == "" {
- // checks if the -f flag is set (overwrites the output format)
- var hasF bool
- for _, opts := range d.FFmpegOpts {
- if strings.TrimSpace(opts) == "-f" {
- hasF = true
- break
- }
- }
- if !hasF {
- command = append(command, "-f", "matroska")
- f, err := os.CreateTemp(d.TempDir, "")
- if err != nil {
- return err
- }
- f.Close()
- tmpfile = f.Name()
- }
- }
- command = append(command, tmpfile)
-
- var errBuf bytes.Buffer
- cmd := exec.CommandContext(d.Context, "ffmpeg",
- command...)
- cmd.Stderr = &errBuf
-
- if err = cmd.Run(); err != nil {
- if errBuf.Len() > 0 {
- return fmt.Errorf(errBuf.String())
- } else {
- return err
- }
- }
- if f, ok := d.Writer.(*os.File); !ok || f.Name() != tmpfile {
- var file *os.File
- if file, err = os.Open(tmpfile); err != nil {
- return err
- }
- defer file.Close()
- _, err = io.Copy(d.Writer, file)
- }
- return err
-}
-
-// downloadSegments downloads every mpeg transport stream segment to a given
-// directory (more information below).
-// After every segment download onSegmentDownload will be called with:
-// the downloaded segment, the current position, the total size of segments to download,
-// the file where the segment content was written to an error (if occurred).
-// The filename is always .ts.
-//
-// Short explanation:
-// The actual crunchyroll video is split up in multiple segments (or video files) which
-// have to be downloaded and merged after to generate a single video file.
-// And this function just downloads each of this segment into the given directory.
-// See https://en.wikipedia.org/wiki/MPEG_transport_stream for more information.
-func (d Downloader) downloadSegments(format *Format) ([]string, error) {
- if err := format.InitVideo(); err != nil {
- return nil, err
- }
-
- var wg sync.WaitGroup
- var lock sync.Mutex
- chunkSize := int(math.Ceil(float64(format.Video.Chunklist.Count()) / float64(d.Goroutines)))
-
- // when a onSegmentDownload call returns an error, this context will be set cancelled and stop all goroutines
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- // receives the decrypt block and iv from the first segment.
- // in my tests, only the first segment has specified this data, so the decryption data from this first segments will be used in every other segment too
- block, iv, err := getCrypt(format, format.Video.Chunklist.Segments[0])
- if err != nil {
- return nil, err
- }
-
- var total int32
- for i := 0; i < int(format.Video.Chunklist.Count()); i += chunkSize {
- wg.Add(1)
- end := i + chunkSize
- if end > int(format.Video.Chunklist.Count()) {
- end = int(format.Video.Chunklist.Count())
- }
- i := i
-
- go func() {
- defer wg.Done()
-
- for j, segment := range format.Video.Chunklist.Segments[i:end] {
- select {
- case <-d.Context.Done():
- case <-ctx.Done():
- return
- default:
- var file *os.File
- for k := 0; k < 3; k++ {
- filename := filepath.Join(d.TempDir, fmt.Sprintf("%d.ts", i+j))
- file, err = d.downloadSegment(format, segment, filename, block, iv)
- if err == nil {
- break
- }
- if k == 2 {
- file.Close()
- cancel()
- return
- }
- select {
- case <-d.Context.Done():
- case <-ctx.Done():
- file.Close()
- return
- case <-time.After(5 * time.Duration(k) * time.Second):
- // sleep if an error occurs. very useful because sometimes the connection times out
- }
- }
- if d.OnSegmentDownload != nil {
- if d.LockOnSegmentDownload {
- lock.Lock()
- }
-
- if err = d.OnSegmentDownload(segment, int(atomic.AddInt32(&total, 1)), int(format.Video.Chunklist.Count()), file); err != nil {
- if d.LockOnSegmentDownload {
- lock.Unlock()
- }
- file.Close()
- return
- }
- if d.LockOnSegmentDownload {
- lock.Unlock()
- }
- }
- file.Close()
- }
- }
- }()
- }
- wg.Wait()
-
- select {
- case <-d.Context.Done():
- return nil, d.Context.Err()
- case <-ctx.Done():
- return nil, err
- default:
- var files []string
- for i := 0; i < int(total); i++ {
- files = append(files, filepath.Join(d.TempDir, fmt.Sprintf("%d.ts", i)))
- }
-
- return files, nil
- }
-}
-
-// getCrypt extracts the key and iv of a m3u8 segment and converts it into a cipher.Block and an iv byte sequence.
-func getCrypt(format *Format, segment *m3u8.MediaSegment) (block cipher.Block, iv []byte, err error) {
- var resp *http.Response
-
- resp, err = format.crunchy.Client.Get(segment.Key.URI)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
- key, err := io.ReadAll(resp.Body)
-
- block, err = aes.NewCipher(key)
- if err != nil {
- return nil, nil, err
- }
- iv = []byte(segment.Key.IV)
- if len(iv) == 0 {
- iv = key
- }
-
- return block, iv, nil
-}
-
-// downloadSegment downloads a segment, decrypts it and names it after the given index.
-func (d Downloader) downloadSegment(format *Format, segment *m3u8.MediaSegment, filename string, block cipher.Block, iv []byte) (*os.File, error) {
- // every segment is aes-128 encrypted and has to be decrypted when downloaded
- content, err := d.decryptSegment(format.crunchy.Client, segment, block, iv)
- if err != nil {
- return nil, err
- }
-
- file, err := os.Create(filename)
- if err != nil {
- return nil, err
- }
- defer file.Close()
- if _, err = file.Write(content); err != nil {
- return nil, err
- }
-
- return file, nil
-}
-
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L25.
-func (d Downloader) decryptSegment(client *http.Client, segment *m3u8.MediaSegment, block cipher.Block, iv []byte) ([]byte, error) {
- req, err := http.NewRequestWithContext(d.Context, http.MethodGet, segment.URI, nil)
- if err != nil {
- return nil, err
- }
-
- resp, err := client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- raw, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- blockMode := cipher.NewCBCDecrypter(block, iv[:block.BlockSize()])
- decrypted := make([]byte, len(raw))
- blockMode.CryptBlocks(decrypted, raw)
- raw = d.pkcs5UnPadding(decrypted)
-
- return raw, nil
-}
-
-// https://github.com/oopsguy/m3u8/blob/4150e93ec8f4f8718875a02973f5d792648ecb97/tool/crypt.go#L47.
-func (d Downloader) pkcs5UnPadding(origData []byte) []byte {
- length := len(origData)
- unPadding := int(origData[length-1])
- return origData[:(length - unPadding)]
-}
diff --git a/episode.go b/episode.go
deleted file mode 100644
index caf1e09..0000000
--- a/episode.go
+++ /dev/null
@@ -1,342 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
- "regexp"
- "strconv"
- "strings"
- "time"
-)
-
-// Episode contains all information about an episode.
-type Episode struct {
- crunchy *Crunchyroll
-
- children []*Stream
-
- ID string `json:"id"`
- ChannelID string `json:"channel_id"`
-
- SeriesID string `json:"series_id"`
- SeriesTitle string `json:"series_title"`
- SeriesSlugTitle string `json:"series_slug_title"`
-
- SeasonID string `json:"season_id"`
- SeasonTitle string `json:"season_title"`
- SeasonSlugTitle string `json:"season_slug_title"`
- SeasonNumber int `json:"season_number"`
-
- Episode string `json:"episode"`
- EpisodeNumber int `json:"episode_number"`
- SequenceNumber float64 `json:"sequence_number"`
- ProductionEpisodeID string `json:"production_episode_id"`
-
- Title string `json:"title"`
- SlugTitle string `json:"slug_title"`
- Description string `json:"description"`
- NextEpisodeID string `json:"next_episode_id"`
- NextEpisodeTitle string `json:"next_episode_title"`
-
- HDFlag bool `json:"hd_flag"`
- MaturityRatings []string `json:"maturity_ratings"`
- IsMature bool `json:"is_mature"`
- MatureBlocked bool `json:"mature_blocked"`
-
- EpisodeAirDate time.Time `json:"episode_air_date"`
- FreeAvailableDate time.Time `json:"free_available_date"`
- PremiumAvailableDate time.Time `json:"premium_available_date"`
-
- IsSubbed bool `json:"is_subbed"`
- IsDubbed bool `json:"is_dubbed"`
- IsClip bool `json:"is_clip"`
- SeoTitle string `json:"seo_title"`
- SeoDescription string `json:"seo_description"`
- SeasonTags []string `json:"season_tags"`
-
- AvailableOffline bool `json:"available_offline"`
- MediaType MediaType `json:"media_type"`
- Slug string `json:"slug"`
-
- Images struct {
- Thumbnail [][]Image `json:"thumbnail"`
- } `json:"images"`
-
- DurationMS int `json:"duration_ms"`
- IsPremiumOnly bool `json:"is_premium_only"`
- ListingID string `json:"listing_id"`
-
- SubtitleLocales []LOCALE `json:"subtitle_locales"`
- Playback string `json:"playback"`
-
- AvailabilityNotes string `json:"availability_notes"`
-
- StreamID string
-}
-
-// HistoryEpisode contains additional information about an episode if the account has watched or started to watch the episode.
-type HistoryEpisode struct {
- *Episode
-
- DatePlayed time.Time `json:"date_played"`
- ParentID string `json:"parent_id"`
- ParentType MediaType `json:"parent_type"`
- Playhead uint `json:"playhead"`
- FullyWatched bool `json:"fully_watched"`
-}
-
-// WatchlistEntryType specifies which type a watchlist entry has.
-type WatchlistEntryType string
-
-const (
- WatchlistEntryEpisode = "episode"
- WatchlistEntrySeries = "series"
-)
-
-// EpisodeFromID returns an episode by its api id.
-func EpisodeFromID(crunchy *Crunchyroll, id string) (*Episode, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes/%s?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- episode := &Episode{
- crunchy: crunchy,
- ID: id,
- }
- if err := decodeMapToStruct(jsonBody, episode); err != nil {
- return nil, err
- }
- if episode.Playback != "" {
- streamHref := jsonBody["__links__"].(map[string]interface{})["streams"].(map[string]interface{})["href"].(string)
- if match := regexp.MustCompile(`(?m)^/cms/v2/\S+videos/(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 {
- episode.StreamID = match[0][1]
- }
- }
-
- return episode, nil
-}
-
-// AddToWatchlist adds the current episode to the watchlist.
-// Will return an RequestError with the response status code of 409 if the series was already on the watchlist before.
-// There is currently a bug, or as I like to say in context of the crunchyroll api, feature, that only series and not
-// individual episode can be added to the watchlist. Even though I somehow got an episode to my watchlist on the
-// crunchyroll website, it never worked with the api here. So this function actually adds the whole series to the watchlist.
-func (e *Episode) AddToWatchlist() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", e.crunchy.Config.AccountID, e.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"content_id": e.SeriesID})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = e.crunchy.requestFull(req)
- return err
-}
-
-// RemoveFromWatchlist removes the current episode from the watchlist.
-// Will return an RequestError with the response status code of 404 if the series was not on the watchlist before.
-func (e *Episode) RemoveFromWatchlist() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", e.crunchy.Config.AccountID, e.SeriesID, e.crunchy.Locale)
- _, err := e.crunchy.request(endpoint, http.MethodDelete)
- return err
-}
-
-// AudioLocale returns the audio locale of the episode.
-// Every episode in a season (should) have the same audio locale,
-// so if you want to get the audio locale of a season, just call
-// this method on the first episode of the season.
-// Will fail if no streams are available, thus use Episode.Available
-// to prevent any misleading errors.
-func (e *Episode) AudioLocale() (LOCALE, error) {
- streams, err := e.Streams()
- if err != nil {
- return "", err
- }
- return streams[0].AudioLocale, nil
-}
-
-// Comment creates a new comment under the episode.
-func (e *Episode) Comment(message string, spoiler bool) (*Comment, error) {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?locale=%s", e.ID, e.crunchy.Locale)
- var flags []string
- if spoiler {
- flags = append(flags, "spoiler")
- }
- body, _ := json.Marshal(map[string]any{"locale": string(e.crunchy.Locale), "flags": flags, "message": message})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return nil, err
- }
- req.Header.Add("Content-Type", "application/json")
- resp, err := e.crunchy.requestFull(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- c := &Comment{
- crunchy: e.crunchy,
- EpisodeID: e.ID,
- }
- if err = json.NewDecoder(resp.Body).Decode(c); err != nil {
- return nil, err
- }
-
- return c, nil
-}
-
-// CommentsOrderType represents a sort type to sort Episode.Comments after.
-type CommentsOrderType string
-
-const (
- CommentsOrderAsc CommentsOrderType = "asc"
- CommentsOrderDesc = "desc"
-)
-
-type CommentsSortType string
-
-const (
- CommentsSortPopular CommentsSortType = "popular"
- CommentsSortDate = "date"
-)
-
-type CommentsOptions struct {
- // Order specified the order how the comments should be returned.
- Order CommentsOrderType `json:"order"`
-
- // Sort specified after which key the comments should be sorted.
- Sort CommentsSortType `json:"sort"`
-}
-
-// Comments returns comments under the given episode.
-func (e *Episode) Comments(options CommentsOptions, page uint, size uint) (c []*Comment, err error) {
- options, err = structDefaults(CommentsOptions{Order: CommentsOrderDesc, Sort: CommentsSortPopular}, options)
- if err != nil {
- return nil, err
- }
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/talkbox/guestbooks/%s/comments?page=%d&page_size=%d&order=%s&sort=%s&locale=%s", e.ID, page, size, options.Order, options.Sort, e.crunchy.Locale)
- resp, err := e.crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]any
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- if err = decodeMapToStruct(jsonBody["items"].([]any), &c); err != nil {
- return nil, err
- }
- for _, comment := range c {
- comment.crunchy = e.crunchy
- comment.EpisodeID = e.ID
- }
-
- return
-}
-
-// Available returns if downloadable streams for this episodes are available.
-func (e *Episode) Available() bool {
- return e.crunchy.Config.Premium || !e.IsPremiumOnly
-}
-
-// GetFormat returns the format which matches the given resolution and subtitle locale.
-func (e *Episode) GetFormat(resolution string, subtitle LOCALE, hardsub bool) (*Format, error) {
- streams, err := e.Streams()
- if err != nil {
- return nil, err
- }
- var foundStream *Stream
- for _, stream := range streams {
- if hardsub && stream.HardsubLocale == subtitle || stream.HardsubLocale == "" && subtitle == "" {
- foundStream = stream
- break
- } else if !hardsub {
- for _, streamSubtitle := range stream.Subtitles {
- if streamSubtitle.Locale == subtitle {
- foundStream = stream
- break
- }
- }
- if foundStream != nil {
- break
- }
- }
- }
-
- if foundStream == nil {
- return nil, fmt.Errorf("no matching stream found")
- }
- formats, err := foundStream.Formats()
- if err != nil {
- return nil, err
- }
- var res *Format
- for _, format := range formats {
- if resolution == "worst" || resolution == "best" {
- if res == nil {
- res = format
- continue
- }
-
- curSplitRes := strings.SplitN(format.Video.Resolution, "x", 2)
- curResX, _ := strconv.Atoi(curSplitRes[0])
- curResY, _ := strconv.Atoi(curSplitRes[1])
-
- resSplitRes := strings.SplitN(res.Video.Resolution, "x", 2)
- resResX, _ := strconv.Atoi(resSplitRes[0])
- resResY, _ := strconv.Atoi(resSplitRes[1])
-
- if resolution == "worst" && curResX+curResY < resResX+resResY {
- res = format
- } else if resolution == "best" && curResX+curResY > resResX+resResY {
- res = format
- }
- }
-
- if format.Video.Resolution == resolution {
- return format, nil
- }
- }
-
- if res != nil {
- return res, nil
- }
-
- return nil, fmt.Errorf("no matching resolution found")
-}
-
-// Streams returns all streams which are available for the episode.
-func (e *Episode) Streams() ([]*Stream, error) {
- if e.children != nil {
- return e.children, nil
- }
-
- streams, err := fromVideoStreams(e.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- e.crunchy.Config.Bucket,
- e.StreamID,
- e.crunchy.Locale,
- e.crunchy.Config.Signature,
- e.crunchy.Config.Policy,
- e.crunchy.Config.KeyPairID))
- if err != nil {
- return nil, err
- }
-
- if e.crunchy.cache {
- e.children = streams
- }
- return streams, nil
-}
diff --git a/error.go b/error.go
deleted file mode 100644
index 9bc2ae6..0000000
--- a/error.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package crunchyroll
-
-import (
- "fmt"
- "net/http"
-)
-
-type RequestError struct {
- error
-
- Response *http.Response
- Message string
-}
-
-func (re *RequestError) Error() string {
- return fmt.Sprintf("error for endpoint %s (%d): %s", re.Response.Request.URL.String(), re.Response.StatusCode, re.Message)
-}
diff --git a/format.go b/format.go
deleted file mode 100644
index 6f17bf7..0000000
--- a/format.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package crunchyroll
-
-import (
- "github.com/grafov/m3u8"
-)
-
-type FormatType string
-
-const (
- EPISODE FormatType = "episodes"
- MOVIE = "movies"
-)
-
-// Format contains detailed information about an episode video stream.
-type Format struct {
- crunchy *Crunchyroll
-
- ID string
- // FormatType represents if the format parent is an episode or a movie.
- FormatType FormatType
- Video *m3u8.Variant
- AudioLocale LOCALE
- Hardsub LOCALE
- Subtitles []*Subtitle
-}
-
-// InitVideo initializes the Format.Video completely.
-// The Format.Video.Chunklist pointer is, by default, nil because an additional
-// request must be made to receive its content. The request is not made when
-// initializing a Format struct because it would probably cause an intense overhead
-// since Format.Video.Chunklist is only used sometimes.
-func (f *Format) InitVideo() error {
- if f.Video.Chunklist == nil {
- resp, err := f.crunchy.Client.Get(f.Video.URI)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
- if err != nil {
- return err
- }
- f.Video.Chunklist = playlist.(*m3u8.MediaPlaylist)
- }
- return nil
-}
-
-// Download downloads the Format with the via Downloader specified options.
-func (f *Format) Download(downloader Downloader) error {
- return downloader.download(f)
-}
diff --git a/go.mod b/go.mod
index 5c64ddf..988f612 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,9 @@
-module github.com/ByteDream/crunchyroll-go/v3
+module github.com/ByteDream/crunchy-cli
go 1.18
require (
+ github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.4.0
)
diff --git a/go.sum b/go.sum
index 34693ca..31a2cd9 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+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/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..f9c3a90
--- /dev/null
+++ b/main.go
@@ -0,0 +1,7 @@
+package main
+
+import "github.com/ByteDream/crunchy-cli/commands"
+
+func main() {
+ commands.Execute()
+}
diff --git a/movie_listing.go b/movie_listing.go
deleted file mode 100644
index f9bbc21..0000000
--- a/movie_listing.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// MovieListing contains information about something which is called
-// movie listing. I don't know what this means thb.
-type MovieListing struct {
- crunchy *Crunchyroll
-
- ID string `json:"id"`
-
- Title string `json:"title"`
- Slug string `json:"slug"`
- SlugTitle string `json:"slug_title"`
- Description string `json:"description"`
-
- Images struct {
- Thumbnail [][]Image `json:"thumbnail"`
- } `json:"images"`
-
- DurationMS int `json:"duration_ms"`
- IsPremiumOnly bool `json:"is_premium_only"`
- ListeningID string `json:"listening_id"`
- IsMature bool `json:"is_mature"`
- AvailableOffline bool `json:"available_offline"`
- IsSubbed bool `json:"is_subbed"`
- IsDubbed bool `json:"is_dubbed"`
-
- Playback string `json:"playback"`
- AvailabilityNotes string `json:"availability_notes"`
-}
-
-// MovieListingFromID returns a movie listing by its api id.
-func MovieListingFromID(crunchy *Crunchyroll, id string) (*MovieListing, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movie_listing/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- movieListing := &MovieListing{
- crunchy: crunchy,
- ID: id,
- }
- if err = decodeMapToStruct(jsonBody, movieListing); err != nil {
- return nil, err
- }
-
- return movieListing, nil
-}
-
-// AudioLocale is same as Episode.AudioLocale.
-func (ml *MovieListing) AudioLocale() (LOCALE, error) {
- resp, err := ml.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- ml.crunchy.Config.Bucket,
- ml.ID,
- ml.crunchy.Locale,
- ml.crunchy.Config.Signature,
- ml.crunchy.Config.Policy,
- ml.crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return "", err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- return LOCALE(jsonBody["audio_locale"].(string)), nil
-}
-
-// Streams returns all streams which are available for the movie listing.
-func (ml *MovieListing) Streams() ([]*Stream, error) {
- return fromVideoStreams(ml.crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- ml.crunchy.Config.Bucket,
- ml.ID,
- ml.crunchy.Locale,
- ml.crunchy.Config.Signature,
- ml.crunchy.Config.Policy,
- ml.crunchy.Config.KeyPairID))
-}
diff --git a/news.go b/news.go
deleted file mode 100644
index 074dc0e..0000000
--- a/news.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// News returns the top and latest news from crunchyroll for the current locale within the given limits.
-func (c *Crunchyroll) News(topLimit uint, latestLimit uint) (t []*News, l []*News, err error) {
- newsFeedEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/news_feed?top_news_n=%d&latest_news_n=%d&locale=%s",
- topLimit, latestLimit, c.Locale)
- resp, err := c.request(newsFeedEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'news_feed' response: %w", err)
- }
-
- topNews := jsonBody["top_news"].(map[string]interface{})
- for _, item := range topNews["items"].([]interface{}) {
- topNews := &News{}
- if err := decodeMapToStruct(item, topNews); err != nil {
- return nil, nil, err
- }
-
- t = append(t, topNews)
- }
-
- latestNews := jsonBody["latest_news"].(map[string]interface{})
- for _, item := range latestNews["items"].([]interface{}) {
- latestNews := &News{}
- if err := decodeMapToStruct(item, latestNews); err != nil {
- return nil, nil, err
- }
-
- l = append(l, latestNews)
- }
-
- return t, l, nil
-}
-
-// News contains all information about news.
-type News struct {
- Title string `json:"title"`
- Link string `json:"link"`
- Image string `json:"image"`
- Creator string `json:"creator"`
- PublishDate string `json:"publish_date"`
- Description string `json:"description"`
-}
diff --git a/parse.go b/parse.go
deleted file mode 100644
index 392e971..0000000
--- a/parse.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package crunchyroll
-
-import (
- "regexp"
- "strconv"
-)
-
-// ParseBetaSeriesURL tries to extract the season id of the given crunchyroll beta url, pointing to a season.
-func ParseBetaSeriesURL(url string) (seasonId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?series/(?P\w+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seasonId = groups["seasonId"]
- ok = true
- }
- return
-}
-
-// ParseBetaEpisodeURL tries to extract the episode id of the given crunchyroll beta url, pointing to an episode.
-func ParseBetaEpisodeURL(url string) (episodeId string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?beta\.crunchyroll\.com/(\w{2}/)?watch/(?P\w+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- episodeId = groups["episodeId"]
- ok = true
- }
- return
-}
-
-// ParseVideoURL tries to extract the crunchyroll series / movie name out of the given url.
-//
-// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaSeriesURL
-// if possible since beta url are always safe to use.
-// The method will stay in the library until only beta urls are supported by crunchyroll itself.
-func ParseVideoURL(url string) (seriesName string, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)(/videos)?/?$`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seriesName = groups["series"]
-
- if seriesName != "" {
- ok = true
- }
- }
- return
-}
-
-// ParseEpisodeURL tries to extract the crunchyroll series name, title, episode number and web id out of the given crunchyroll url
-// Note that the episode number can be misleading. For example if an episode has the episode number 23.5 (slime isekai)
-// the episode number will be 235.
-//
-// Deprecated: Crunchyroll classic urls are sometimes not safe to use, use ParseBetaEpisodeURL
-// if possible since beta url are always safe to use.
-// The method will stay in the library until only beta urls are supported by crunchyroll itself.
-func ParseEpisodeURL(url string) (seriesName, title string, episodeNumber int, webId int, ok bool) {
- pattern := regexp.MustCompile(`(?m)^https?://(www\.)?crunchyroll\.com(/\w{2}(-\w{2})?)?/(?P[^/]+)/episode-(?P\d+)-(?P.+)-(?P\d+).*`)
- if urlMatch := pattern.FindAllStringSubmatch(url, -1); len(urlMatch) != 0 {
- groups := regexGroups(urlMatch, pattern.SubexpNames()...)
- seriesName = groups["series"]
- episodeNumber, _ = strconv.Atoi(groups["number"])
- title = groups["title"]
- webId, _ = strconv.Atoi(groups["webId"])
-
- if seriesName != "" && title != "" && webId != 0 {
- ok = true
- }
- }
- return
-}
diff --git a/search.go b/search.go
deleted file mode 100644
index cbbf7e9..0000000
--- a/search.go
+++ /dev/null
@@ -1,193 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// BrowseSortType represents a sort type to sort Crunchyroll.Browse items after.
-type BrowseSortType string
-
-const (
- BrowseSortPopularity BrowseSortType = "popularity"
- BrowseSortNewlyAdded = "newly_added"
- BrowseSortAlphabetical = "alphabetical"
-)
-
-// BrowseOptions represents options for browsing the crunchyroll catalog.
-type BrowseOptions struct {
- // Categories specifies the categories of the entries.
- Categories []string `param:"categories"`
-
- // IsDubbed specifies whether the entries should be dubbed.
- IsDubbed bool `param:"is_dubbed"`
-
- // IsSubbed specifies whether the entries should be subbed.
- IsSubbed bool `param:"is_subbed"`
-
- // Simulcast specifies a particular simulcast season by id in which the entries have been aired.
- Simulcast string `param:"season_tag"`
-
- // Sort specifies how the entries should be sorted.
- Sort BrowseSortType `param:"sort_by"`
-
- // Start specifies the index from which the entries should be returned.
- Start uint `param:"start"`
-
- // Type specifies the media type of the entries.
- Type MediaType `param:"type"`
-}
-
-// Browse browses the crunchyroll catalog filtered by the specified options and returns all found series and movies within the given limit.
-func (c *Crunchyroll) Browse(options BrowseOptions, limit uint) (s []*Series, m []*Movie, err error) {
- query, err := encodeStructToQueryValues(options)
- if err != nil {
- return nil, nil, err
- }
-
- browseEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/browse?%s&n=%d&locale=%s",
- query, limit, c.Locale)
- resp, err := c.request(browseEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'browse' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case MediaTypeSeries:
- series := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series)
- case MediaTypeMovie:
- movie := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
-
- return s, m, nil
-}
-
-// FindVideoByName finds a Video (Season or Movie) by its name.
-// Use this in combination with ParseVideoURL and hand over the corresponding results
-// to this function.
-//
-// Deprecated: Use Search instead. The first result sometimes isn't the correct one
-// so this function is inaccurate in some cases.
-// See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information.
-func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
- s, m, err := c.Search(seriesName, 1)
- if err != nil {
- return nil, err
- }
-
- if len(s) > 0 {
- return s[0], nil
- } else if len(m) > 0 {
- return m[0], nil
- }
- return nil, fmt.Errorf("no series or movie could be found")
-}
-
-// FindEpisodeByName finds an episode by its crunchyroll series name and episode title.
-// Use this in combination with ParseEpisodeURL and hand over the corresponding results
-// to this function.
-func (c *Crunchyroll) FindEpisodeByName(seriesName, episodeTitle string) ([]*Episode, error) {
- series, _, err := c.Search(seriesName, 5)
- if err != nil {
- return nil, err
- }
-
- var matchingEpisodes []*Episode
- for _, s := range series {
- seasons, err := s.Seasons()
- if err != nil {
- return nil, err
- }
-
- for _, season := range seasons {
- episodes, err := season.Episodes()
- if err != nil {
- return nil, err
- }
- for _, episode := range episodes {
- if episode.SlugTitle == episodeTitle {
- matchingEpisodes = append(matchingEpisodes, episode)
- }
- }
- }
- }
-
- return matchingEpisodes, nil
-}
-
-// Search searches a query and returns all found series and movies within the given limit.
-func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie, err error) {
- searchEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/search?q=%s&n=%d&type=&locale=%s",
- query, limit, c.Locale)
- resp, err := c.request(searchEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'search' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- item := item.(map[string]interface{})
- if item["total"].(float64) > 0 {
- switch item["type"] {
- case MediaTypeSeries:
- for _, series := range item["items"].([]interface{}) {
- series2 := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(series, series2); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(series.(map[string]interface{})["series_metadata"].(map[string]interface{}), series2); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series2)
- }
- case MediaTypeMovie:
- for _, movie := range item["items"].([]interface{}) {
- movie2 := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(movie, movie2); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie2)
- }
- }
- }
- }
-
- return s, m, nil
-}
diff --git a/season.go b/season.go
deleted file mode 100644
index 907122d..0000000
--- a/season.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "regexp"
-)
-
-// Season contains information about an anime season.
-type Season struct {
- crunchy *Crunchyroll
-
- children []*Episode
-
- ID string `json:"id"`
- ChannelID string `json:"channel_id"`
-
- Title string `json:"title"`
- SlugTitle string `json:"slug_title"`
-
- SeriesID string `json:"series_id"`
- SeasonNumber int `json:"season_number"`
-
- IsComplete bool `json:"is_complete"`
-
- Description string `json:"description"`
- Keywords []string `json:"keywords"`
- SeasonTags []string `json:"season_tags"`
- IsMature bool `json:"is_mature"`
- MatureBlocked bool `json:"mature_blocked"`
- IsSubbed bool `json:"is_subbed"`
- IsDubbed bool `json:"is_dubbed"`
- IsSimulcast bool `json:"is_simulcast"`
-
- SeoTitle string `json:"seo_title"`
- SeoDescription string `json:"seo_description"`
-
- AvailabilityNotes string `json:"availability_notes"`
-
- // the locales are always empty, idk why, this may change in the future
- AudioLocales []LOCALE
- SubtitleLocales []LOCALE
-}
-
-// SeasonFromID returns a season by its api id.
-func SeasonFromID(crunchy *Crunchyroll, id string) (*Season, error) {
- resp, err := crunchy.Client.Get(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- season := &Season{
- crunchy: crunchy,
- ID: id,
- }
- if err := decodeMapToStruct(jsonBody, season); err != nil {
- return nil, err
- }
-
- return season, nil
-}
-
-// AudioLocale returns the audio locale of the season.
-// Will fail if no streams are available, thus use Season.Available
-// to prevent any misleading errors.
-func (s *Season) AudioLocale() (LOCALE, error) {
- episodes, err := s.Episodes()
- if err != nil {
- return "", err
- }
- return episodes[0].AudioLocale()
-}
-
-// Available returns if downloadable streams for this season are available.
-func (s *Season) Available() (bool, error) {
- episodes, err := s.Episodes()
- if err != nil {
- return false, err
- }
- return episodes[0].Available(), nil
-}
-
-// Episodes returns all episodes which are available for the season.
-func (s *Season) Episodes() (episodes []*Episode, err error) {
- if s.children != nil {
- return s.children, nil
- }
-
- resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/episodes?season_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- s.crunchy.Config.Bucket,
- s.ID,
- s.crunchy.Locale,
- s.crunchy.Config.Signature,
- s.crunchy.Config.Policy,
- s.crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- for _, item := range jsonBody["items"].([]interface{}) {
- episode := &Episode{
- crunchy: s.crunchy,
- }
- if err = decodeMapToStruct(item, episode); err != nil {
- return nil, err
- }
- if episode.Playback != "" {
- streamHref := item.(map[string]interface{})["__links__"].(map[string]interface{})["streams"].(map[string]interface{})["href"].(string)
- if match := regexp.MustCompile(`(?m)(\w+)/streams$`).FindAllStringSubmatch(streamHref, -1); len(match) > 0 {
- episode.StreamID = match[0][1]
- } else {
- fmt.Println()
- }
- }
- episodes = append(episodes, episode)
- }
-
- if s.crunchy.cache {
- s.children = episodes
- }
- return
-}
diff --git a/simulcast.go b/simulcast.go
deleted file mode 100644
index e77d2f8..0000000
--- a/simulcast.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// Simulcasts returns all available simulcast seasons for the current locale.
-func (c *Crunchyroll) Simulcasts() (s []*Simulcast, err error) {
- seasonListEndpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/season_list?locale=%s", c.Locale)
- resp, err := c.request(seasonListEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'season_list' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- simulcast := &Simulcast{}
- if err := decodeMapToStruct(item, simulcast); err != nil {
- return nil, err
- }
-
- s = append(s, simulcast)
- }
-
- return s, nil
-}
-
-// Simulcast contains all information about a simulcast season.
-type Simulcast struct {
- ID string `json:"id"`
-
- Localization struct {
- Title string `json:"title"`
-
- // appears to be always an empty string.
- Description string `json:"description"`
- } `json:"localization"`
-}
diff --git a/stream.go b/stream.go
deleted file mode 100644
index e5ce54f..0000000
--- a/stream.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "github.com/grafov/m3u8"
- "net/http"
- "regexp"
-)
-
-// Stream contains information about all available video stream of an episode.
-type Stream struct {
- crunchy *Crunchyroll
-
- children []*Format
-
- HardsubLocale LOCALE
- AudioLocale LOCALE
- Subtitles []*Subtitle
-
- formatType FormatType
- id string
- streamURL string
-}
-
-// StreamsFromID returns a stream by its api id.
-func StreamsFromID(crunchy *Crunchyroll, id string) ([]*Stream, error) {
- return fromVideoStreams(crunchy, fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/videos/%s/streams?locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID))
-}
-
-// Formats returns all formats which are available for the stream.
-func (s *Stream) Formats() ([]*Format, error) {
- if s.children != nil {
- return s.children, nil
- }
-
- resp, err := s.crunchy.Client.Get(s.streamURL)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- playlist, _, err := m3u8.DecodeFrom(resp.Body, true)
- if err != nil {
- return nil, err
- }
- var formats []*Format
- for _, variant := range playlist.(*m3u8.MasterPlaylist).Variants {
- formats = append(formats, &Format{
- crunchy: s.crunchy,
- ID: s.id,
- FormatType: s.formatType,
- Video: variant,
- AudioLocale: s.AudioLocale,
- Hardsub: s.HardsubLocale,
- Subtitles: s.Subtitles,
- })
- }
-
- if s.crunchy.cache {
- s.children = formats
- }
- return formats, nil
-}
-
-// fromVideoStreams returns all streams which are accessible via the endpoint.
-func fromVideoStreams(crunchy *Crunchyroll, endpoint string) (streams []*Stream, err error) {
- resp, err := crunchy.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- if len(jsonBody) == 0 {
- // this may get thrown when the crunchyroll account is just a normal account and not one with premium
- if !crunchy.Config.Premium {
- return nil, fmt.Errorf("no stream available, this might be the result of using a non-premium account")
- } else {
- return nil, fmt.Errorf("no stream available")
- }
- }
-
- audioLocale := jsonBody["audio_locale"].(string)
-
- var subtitles []*Subtitle
- for _, rawSubtitle := range jsonBody["subtitles"].(map[string]interface{}) {
- subtitle := &Subtitle{
- crunchy: crunchy,
- }
- decodeMapToStruct(rawSubtitle.(map[string]interface{}), subtitle)
- subtitles = append(subtitles, subtitle)
- }
-
- for _, streamData := range jsonBody["streams"].(map[string]interface{})["adaptive_hls"].(map[string]interface{}) {
- streamData := streamData.(map[string]interface{})
-
- hardsubLocale := streamData["hardsub_locale"].(string)
-
- var id string
- var formatType FormatType
- href := jsonBody["__links__"].(map[string]interface{})["resource"].(map[string]interface{})["href"].(string)
- if match := regexp.MustCompile(`(?sm)/(\w+)/(\w+)$`).FindAllStringSubmatch(href, -1); len(match) > 0 {
- formatType = FormatType(match[0][1])
- id = match[0][2]
- }
-
- stream := &Stream{
- crunchy: crunchy,
- HardsubLocale: LOCALE(hardsubLocale),
- formatType: formatType,
- id: id,
- streamURL: streamData["url"].(string),
- AudioLocale: LOCALE(audioLocale),
- Subtitles: subtitles,
- }
-
- streams = append(streams, stream)
- }
-
- return
-}
diff --git a/subtitle.go b/subtitle.go
deleted file mode 100644
index 164b56e..0000000
--- a/subtitle.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package crunchyroll
-
-import (
- "io"
- "net/http"
-)
-
-// Subtitle contains the information about a video subtitle.
-type Subtitle struct {
- crunchy *Crunchyroll
-
- Locale LOCALE `json:"locale"`
- URL string `json:"url"`
- Format string `json:"format"`
-}
-
-// Save writes the subtitle to the given io.Writer.
-func (s Subtitle) Save(writer io.Writer) error {
- req, err := http.NewRequestWithContext(s.crunchy.Context, http.MethodGet, s.URL, nil)
- if err != nil {
- return err
- }
-
- resp, err := s.crunchy.Client.Do(req)
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- _, err = io.Copy(writer, resp.Body)
- return err
-}
diff --git a/suggestions.go b/suggestions.go
deleted file mode 100644
index 35d7a9b..0000000
--- a/suggestions.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// Recommendations returns series and movie recommendations from crunchyroll based on the currently logged in account within the given limit.
-func (c *Crunchyroll) Recommendations(limit uint) (s []*Series, m []*Movie, err error) {
- recommendationsEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/recommendations?n=%d&locale=%s",
- c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(recommendationsEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'recommendations' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case MediaTypeSeries:
- series := &Series{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- s = append(s, series)
- case MediaTypeMovie:
- movie := &Movie{
- crunchy: c,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
-
- return s, m, nil
-}
-
-// UpNext returns the episodes that are up next based on the currently logged in account within the given limit.
-func (c *Crunchyroll) UpNext(limit uint) (e []*Episode, err error) {
- upNextAccountEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/up_next_account?n=%d&locale=%s",
- c.Config.AccountID, limit, c.Locale)
- resp, err := c.request(upNextAccountEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'up_next_account' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- panel := item.(map[string]interface{})["panel"]
-
- episode := &Episode{
- crunchy: c,
- }
- if err := decodeMapToStruct(panel, episode); err != nil {
- return nil, err
- }
-
- e = append(e, episode)
- }
-
- return e, nil
-}
diff --git a/url.go b/url.go
deleted file mode 100644
index 95f3951..0000000
--- a/url.go
+++ /dev/null
@@ -1,128 +0,0 @@
-package crunchyroll
-
-import (
- "fmt"
-)
-
-// ExtractEpisodesFromUrl extracts all episodes from an url.
-// If audio is not empty, the episodes gets filtered after the given locale.
-func (c *Crunchyroll) ExtractEpisodesFromUrl(url string, audio ...LOCALE) ([]*Episode, error) {
- series, episodes, err := c.ParseUrl(url)
- if err != nil {
- return nil, err
- }
-
- var eps []*Episode
- var notAvailableContinue bool
-
- if series != nil {
- seasons, err := series.Seasons()
- if err != nil {
- return nil, err
- }
- for _, season := range seasons {
- if audio != nil {
- if available, err := season.Available(); err != nil {
- return nil, err
- } else if !available {
- notAvailableContinue = true
- continue
- }
-
- locale, err := season.AudioLocale()
- if err != nil {
- return nil, err
- }
-
- var found bool
- for _, l := range audio {
- if locale == l {
- found = true
- break
- }
- }
- if !found {
- continue
- }
- }
- e, err := season.Episodes()
- if err != nil {
- return nil, err
- }
- eps = append(eps, e...)
- }
- } else if episodes != nil {
- if audio == nil {
- return episodes, nil
- }
-
- for _, episode := range episodes {
- // if no episode streams are available, calling episode.AudioLocale
- // will result in an unwanted error
- if !episode.Available() {
- notAvailableContinue = true
- continue
- }
- locale, err := episode.AudioLocale()
- if err != nil {
- return nil, err
- }
- if audio != nil {
- var found bool
- for _, l := range audio {
- if locale == l {
- found = true
- break
- }
- }
- if !found {
- continue
- }
- }
-
- eps = append(eps, episode)
- }
- }
-
- if len(eps) == 0 {
- if notAvailableContinue {
- return nil, fmt.Errorf("could not find any matching episode which is accessable with a non-premium account")
- } else {
- return nil, fmt.Errorf("could not find any matching episode")
- }
- }
-
- return eps, nil
-}
-
-// ParseUrl parses the given url into a series or episode.
-// The returning episode is a slice because non-beta urls have the same episode with different languages.
-func (c *Crunchyroll) ParseUrl(url string) (*Series, []*Episode, error) {
- if seriesId, ok := ParseBetaSeriesURL(url); ok {
- series, err := SeriesFromID(c, seriesId)
- if err != nil {
- return nil, nil, err
- }
- return series, nil, nil
- } else if episodeId, ok := ParseBetaEpisodeURL(url); ok {
- episode, err := EpisodeFromID(c, episodeId)
- if err != nil {
- return nil, nil, err
- }
- return nil, []*Episode{episode}, nil
- } else if seriesName, ok := ParseVideoURL(url); ok {
- video, err := c.FindVideoByName(seriesName)
- if err != nil {
- return nil, nil, err
- }
- return video.(*Series), nil, nil
- } else if seriesName, title, _, _, ok := ParseEpisodeURL(url); ok {
- episodes, err := c.FindEpisodeByName(seriesName, title)
- if err != nil {
- return nil, nil, err
- }
- return nil, episodes, nil
- } else {
- return nil, nil, fmt.Errorf("invalid url %s", url)
- }
-}
diff --git a/utils.go b/utils.go
deleted file mode 100644
index a665040..0000000
--- a/utils.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/url"
- "reflect"
- "strings"
-)
-
-func decodeMapToStruct(m interface{}, s interface{}) error {
- jsonBody, err := json.Marshal(m)
- if err != nil {
- return err
- }
- return json.Unmarshal(jsonBody, s)
-}
-
-func regexGroups(parsed [][]string, subexpNames ...string) map[string]string {
- groups := map[string]string{}
- for _, match := range parsed {
- for i, content := range match {
- if subexpName := subexpNames[i]; subexpName != "" {
- groups[subexpName] = content
- }
- }
- }
- return groups
-}
-
-func encodeStructToQueryValues(s interface{}) (string, error) {
- values := make(url.Values)
- v := reflect.ValueOf(s)
-
- for i := 0; i < v.Type().NumField(); i++ {
-
- // don't include parameters with default or without values in the query to avoid corruption of the API response.
- switch v.Field(i).Kind() {
- case reflect.Slice, reflect.String:
- if v.Field(i).Len() == 0 {
- continue
- }
- case reflect.Bool:
- if !v.Field(i).Bool() {
- continue
- }
- case reflect.Uint:
- if v.Field(i).Uint() == 0 {
- continue
- }
- }
-
- key := v.Type().Field(i).Tag.Get("param")
- var val string
-
- if v.Field(i).Kind() == reflect.Slice {
- var items []string
-
- for _, i := range v.Field(i).Interface().([]string) {
- items = append(items, i)
- }
-
- val = strings.Join(items, ",")
- } else {
- val = fmt.Sprint(v.Field(i).Interface())
- }
-
- values.Add(key, val)
- }
-
- return values.Encode(), nil
-}
-
-func structDefaults[T any](defaultStruct T, customStruct T) (T, error) {
- rawDefaultStruct, err := json.Marshal(defaultStruct)
- if err != nil {
- return *new(T), err
- }
- if err = json.NewDecoder(bytes.NewBuffer(rawDefaultStruct)).Decode(&customStruct); err != nil {
- return *new(T), err
- }
- return customStruct, nil
-}
diff --git a/utils/locale.go b/utils/locale.go
deleted file mode 100644
index 85a8650..0000000
--- a/utils/locale.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package utils
-
-import (
- "github.com/ByteDream/crunchyroll-go/v3"
-)
-
-// AllLocales is an array of all available locales.
-var AllLocales = []crunchyroll.LOCALE{
- crunchyroll.JP,
- crunchyroll.US,
- crunchyroll.LA,
- crunchyroll.ES,
- crunchyroll.FR,
- crunchyroll.PT,
- crunchyroll.BR,
- crunchyroll.IT,
- crunchyroll.DE,
- crunchyroll.RU,
- crunchyroll.AR,
-}
-
-// ValidateLocale validates if the given locale actually exist.
-func ValidateLocale(locale crunchyroll.LOCALE) bool {
- for _, l := range AllLocales {
- if l == locale {
- return true
- }
- }
- return false
-}
-
-// LocaleLanguage returns the country by its locale.
-func LocaleLanguage(locale crunchyroll.LOCALE) string {
- switch locale {
- case crunchyroll.JP:
- return "Japanese"
- case crunchyroll.US:
- return "English (US)"
- case crunchyroll.LA:
- return "Spanish (Latin America)"
- case crunchyroll.ES:
- return "Spanish (Spain)"
- case crunchyroll.FR:
- return "French"
- case crunchyroll.PT:
- return "Portuguese (Europe)"
- case crunchyroll.BR:
- return "Portuguese (Brazil)"
- case crunchyroll.IT:
- return "Italian"
- case crunchyroll.DE:
- return "German"
- case crunchyroll.RU:
- return "Russian"
- case crunchyroll.AR:
- return "Arabic"
- default:
- return ""
- }
-}
diff --git a/utils/sort.go b/utils/sort.go
deleted file mode 100644
index 661eea8..0000000
--- a/utils/sort.go
+++ /dev/null
@@ -1,158 +0,0 @@
-package utils
-
-import (
- "github.com/ByteDream/crunchyroll-go/v3"
- "sort"
- "strconv"
- "strings"
- "sync"
-)
-
-// SortEpisodesBySeason sorts the given episodes by their seasons.
-// Note that the same episodes just with different audio locales will cause problems.
-func SortEpisodesBySeason(episodes []*crunchyroll.Episode) [][]*crunchyroll.Episode {
- sortMap := map[string]map[int][]*crunchyroll.Episode{}
-
- for _, episode := range episodes {
- if _, ok := sortMap[episode.SeriesID]; !ok {
- sortMap[episode.SeriesID] = map[int][]*crunchyroll.Episode{}
- }
- if _, ok := sortMap[episode.SeriesID][episode.SeasonNumber]; !ok {
- sortMap[episode.SeriesID][episode.SeasonNumber] = make([]*crunchyroll.Episode, 0)
- }
- sortMap[episode.SeriesID][episode.SeasonNumber] = append(sortMap[episode.SeriesID][episode.SeasonNumber], episode)
- }
-
- var eps [][]*crunchyroll.Episode
- for _, series := range sortMap {
- var keys []int
- for seriesNumber := range series {
- keys = append(keys, seriesNumber)
- }
- sort.Ints(keys)
-
- for _, key := range keys {
- es := series[key]
- if len(es) > 0 {
- sort.Sort(EpisodesByNumber(es))
- eps = append(eps, es)
- }
- }
- }
-
- return eps
-}
-
-// SortEpisodesByAudio sort the given episodes by their audio locale.
-func SortEpisodesByAudio(episodes []*crunchyroll.Episode) (map[crunchyroll.LOCALE][]*crunchyroll.Episode, error) {
- eps := map[crunchyroll.LOCALE][]*crunchyroll.Episode{}
-
- errChan := make(chan error)
-
- var wg sync.WaitGroup
- var lock sync.Mutex
- for _, episode := range episodes {
- if !episode.Available() {
- continue
- }
- episode := episode
- wg.Add(1)
- go func() {
- defer wg.Done()
- audioLocale, err := episode.AudioLocale()
- if err != nil {
- errChan <- err
- return
- }
- lock.Lock()
- defer lock.Unlock()
-
- if _, ok := eps[audioLocale]; !ok {
- eps[audioLocale] = make([]*crunchyroll.Episode, 0)
- }
- eps[audioLocale] = append(eps[audioLocale], episode)
- }()
- }
- go func() {
- wg.Wait()
- errChan <- nil
- }()
-
- if err := <-errChan; err != nil {
- return nil, err
- }
- return eps, nil
-}
-
-// MovieListingsByDuration sorts movie listings by their duration.
-type MovieListingsByDuration []*crunchyroll.MovieListing
-
-func (mlbd MovieListingsByDuration) Len() int {
- return len(mlbd)
-}
-func (mlbd MovieListingsByDuration) Swap(i, j int) {
- mlbd[i], mlbd[j] = mlbd[j], mlbd[i]
-}
-func (mlbd MovieListingsByDuration) Less(i, j int) bool {
- return mlbd[i].DurationMS < mlbd[j].DurationMS
-}
-
-// EpisodesByDuration sorts episodes by their duration.
-type EpisodesByDuration []*crunchyroll.Episode
-
-func (ebd EpisodesByDuration) Len() int {
- return len(ebd)
-}
-func (ebd EpisodesByDuration) Swap(i, j int) {
- ebd[i], ebd[j] = ebd[j], ebd[i]
-}
-func (ebd EpisodesByDuration) Less(i, j int) bool {
- return ebd[i].DurationMS < ebd[j].DurationMS
-}
-
-// EpisodesByNumber sorts episodes after their episode number.
-type EpisodesByNumber []*crunchyroll.Episode
-
-func (ebn EpisodesByNumber) Len() int {
- return len(ebn)
-}
-func (ebn EpisodesByNumber) Swap(i, j int) {
- ebn[i], ebn[j] = ebn[j], ebn[i]
-}
-func (ebn EpisodesByNumber) Less(i, j int) bool {
- return ebn[i].EpisodeNumber < ebn[j].EpisodeNumber
-}
-
-// FormatsByResolution sorts formats after their resolution.
-type FormatsByResolution []*crunchyroll.Format
-
-func (fbr FormatsByResolution) Len() int {
- return len(fbr)
-}
-func (fbr FormatsByResolution) Swap(i, j int) {
- fbr[i], fbr[j] = fbr[j], fbr[i]
-}
-func (fbr FormatsByResolution) Less(i, j int) bool {
- iSplitRes := strings.SplitN(fbr[i].Video.Resolution, "x", 2)
- iResX, _ := strconv.Atoi(iSplitRes[0])
- iResY, _ := strconv.Atoi(iSplitRes[1])
-
- jSplitRes := strings.SplitN(fbr[j].Video.Resolution, "x", 2)
- jResX, _ := strconv.Atoi(jSplitRes[0])
- jResY, _ := strconv.Atoi(jSplitRes[1])
-
- return iResX+iResY < jResX+jResY
-}
-
-// SubtitlesByLocale sorts subtitles after their locale.
-type SubtitlesByLocale []*crunchyroll.Subtitle
-
-func (sbl SubtitlesByLocale) Len() int {
- return len(sbl)
-}
-func (sbl SubtitlesByLocale) Swap(i, j int) {
- sbl[i], sbl[j] = sbl[j], sbl[i]
-}
-func (sbl SubtitlesByLocale) Less(i, j int) bool {
- return LocaleLanguage(sbl[i].Locale) < LocaleLanguage(sbl[j].Locale)
-}
diff --git a/video.go b/video.go
deleted file mode 100644
index 810244f..0000000
--- a/video.go
+++ /dev/null
@@ -1,280 +0,0 @@
-package crunchyroll
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-type video struct {
- ID string `json:"id"`
- ExternalID string `json:"external_id"`
-
- Description string `json:"description"`
- Title string `json:"title"`
- Slug string `json:"slug"`
- SlugTitle string `json:"slug_title"`
-
- Images struct {
- PosterTall [][]Image `json:"poster_tall"`
- PosterWide [][]Image `json:"poster_wide"`
- } `json:"images"`
-}
-
-// Video is the base for Movie and Season.
-type Video interface{}
-
-// Movie contains information about a movie.
-type Movie struct {
- video
- Video
-
- crunchy *Crunchyroll
-
- children []*MovieListing
-
- // not generated when calling MovieFromID.
- MovieListingMetadata struct {
- AvailabilityNotes string `json:"availability_notes"`
- AvailableOffline bool `json:"available_offline"`
- DurationMS int `json:"duration_ms"`
- ExtendedDescription string `json:"extended_description"`
- FirstMovieID string `json:"first_movie_id"`
- IsDubbed bool `json:"is_dubbed"`
- IsMature bool `json:"is_mature"`
- IsPremiumOnly bool `json:"is_premium_only"`
- IsSubbed bool `json:"is_subbed"`
- MatureRatings []string `json:"mature_ratings"`
- MovieReleaseYear int `json:"movie_release_year"`
- SubtitleLocales []LOCALE `json:"subtitle_locales"`
- } `json:"movie_listing_metadata"`
-
- Playback string `json:"playback"`
-
- PromoDescription string `json:"promo_description"`
- PromoTitle string `json:"promo_title"`
- SearchMetadata struct {
- Score float64 `json:"score"`
- }
-}
-
-// MovieFromID returns a movie by its api id.
-func MovieFromID(crunchy *Crunchyroll, id string) (*Movie, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies/%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- movieListing := &Movie{
- crunchy: crunchy,
- }
- movieListing.ID = id
- if err = decodeMapToStruct(jsonBody, movieListing); err != nil {
- return nil, err
- }
-
- return movieListing, nil
-}
-
-// MovieListing returns all videos corresponding with the movie.
-func (m *Movie) MovieListing() (movieListings []*MovieListing, err error) {
- if m.children != nil {
- return m.children, nil
- }
-
- resp, err := m.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- m.crunchy.Config.Bucket,
- m.ID,
- m.crunchy.Locale,
- m.crunchy.Config.Signature,
- m.crunchy.Config.Policy,
- m.crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- for _, item := range jsonBody["items"].([]interface{}) {
- movieListing := &MovieListing{
- crunchy: m.crunchy,
- }
- if err = decodeMapToStruct(item, movieListing); err != nil {
- return nil, err
- }
- movieListings = append(movieListings, movieListing)
- }
-
- if m.crunchy.cache {
- m.children = movieListings
- }
- return movieListings, nil
-}
-
-// Series contains information about an anime series.
-type Series struct {
- video
- Video
-
- crunchy *Crunchyroll
-
- children []*Season
-
- PromoDescription string `json:"promo_description"`
- PromoTitle string `json:"promo_title"`
-
- AvailabilityNotes string `json:"availability_notes"`
- EpisodeCount int `json:"episode_count"`
- ExtendedDescription string `json:"extended_description"`
- IsDubbed bool `json:"is_dubbed"`
- IsMature bool `json:"is_mature"`
- IsSimulcast bool `json:"is_simulcast"`
- IsSubbed bool `json:"is_subbed"`
- MatureBlocked bool `json:"mature_blocked"`
- MatureRatings []string `json:"mature_ratings"`
- SeasonCount int `json:"season_count"`
-
- // not generated when calling SeriesFromID.
- SearchMetadata struct {
- Score float64 `json:"score"`
- }
-}
-
-// SeriesFromID returns a series by its api id.
-func SeriesFromID(crunchy *Crunchyroll, id string) (*Series, error) {
- resp, err := crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/movies?movie_listing_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- crunchy.Config.Bucket,
- id,
- crunchy.Locale,
- crunchy.Config.Signature,
- crunchy.Config.Policy,
- crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- series := &Series{
- crunchy: crunchy,
- }
- series.ID = id
- if err = decodeMapToStruct(jsonBody, series); err != nil {
- return nil, err
- }
-
- return series, nil
-}
-
-// AddToWatchlist adds the current episode to the watchlist.
-// Will return an RequestError with the response status code of 409 if the series was already on the watchlist before.
-func (s *Series) AddToWatchlist() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s?locale=%s", s.crunchy.Config.AccountID, s.crunchy.Locale)
- body, _ := json.Marshal(map[string]string{"content_id": s.ID})
- req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBuffer(body))
- if err != nil {
- return err
- }
- req.Header.Add("Content-Type", "application/json")
- _, err = s.crunchy.requestFull(req)
- return err
-}
-
-// RemoveFromWatchlist removes the current episode from the watchlist.
-// Will return an RequestError with the response status code of 404 if the series was not on the watchlist before.
-func (s *Series) RemoveFromWatchlist() error {
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/watchlist/%s/%s?locale=%s", s.crunchy.Config.AccountID, s.ID, s.crunchy.Locale)
- _, err := s.crunchy.request(endpoint, http.MethodDelete)
- return err
-}
-
-// Similar returns similar series and movies to the current series within the given limit.
-func (s *Series) Similar(limit uint) (ss []*Series, m []*Movie, err error) {
- similarToEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/%s/similar_to?guid=%s&n=%d&locale=%s",
- s.crunchy.Config.AccountID, s.ID, limit, s.crunchy.Locale)
- resp, err := s.crunchy.request(similarToEndpoint, http.MethodGet)
- if err != nil {
- return nil, nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, nil, fmt.Errorf("failed to parse 'similar_to' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- switch item.(map[string]interface{})["type"] {
- case MediaTypeSeries:
- series := &Series{
- crunchy: s.crunchy,
- }
- if err := decodeMapToStruct(item, series); err != nil {
- return nil, nil, err
- }
- if err := decodeMapToStruct(item.(map[string]interface{})["series_metadata"].(map[string]interface{}), series); err != nil {
- return nil, nil, err
- }
-
- ss = append(ss, series)
- case MediaTypeMovie:
- movie := &Movie{
- crunchy: s.crunchy,
- }
- if err := decodeMapToStruct(item, movie); err != nil {
- return nil, nil, err
- }
-
- m = append(m, movie)
- }
- }
- return
-}
-
-// Seasons returns all seasons of a series.
-func (s *Series) Seasons() (seasons []*Season, err error) {
- if s.children != nil {
- return s.children, nil
- }
-
- resp, err := s.crunchy.request(fmt.Sprintf("https://beta-api.crunchyroll.com/cms/v2/%s/seasons?series_id=%s&locale=%s&Signature=%s&Policy=%s&Key-Pair-Id=%s",
- s.crunchy.Config.Bucket,
- s.ID,
- s.crunchy.Locale,
- s.crunchy.Config.Signature,
- s.crunchy.Config.Policy,
- s.crunchy.Config.KeyPairID), http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- for _, item := range jsonBody["items"].([]interface{}) {
- season := &Season{
- crunchy: s.crunchy,
- }
- if err = decodeMapToStruct(item, season); err != nil {
- return nil, err
- }
- seasons = append(seasons, season)
- }
-
- if s.crunchy.cache {
- s.children = seasons
- }
- return
-}
diff --git a/wallpaper.go b/wallpaper.go
deleted file mode 100644
index e99bafb..0000000
--- a/wallpaper.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package crunchyroll
-
-import "fmt"
-
-// Wallpaper contains a wallpaper name which can be set via Account.ChangeWallpaper.
-type Wallpaper string
-
-// TinyUrl returns the url to the wallpaper in low resolution.
-func (w *Wallpaper) TinyUrl() string {
- return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/360x115/%s", *w)
-}
-
-// BigUrl returns the url to the wallpaper in high resolution.
-func (w *Wallpaper) BigUrl() string {
- return fmt.Sprintf("https://static.crunchyroll.com/assets/wallpaper/1920x400/%s", *w)
-}
diff --git a/watch_history.go b/watch_history.go
deleted file mode 100644
index d9ce265..0000000
--- a/watch_history.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
-)
-
-// WatchHistory returns the history of watched episodes based on the currently logged in account from the given page with the given size.
-func (c *Crunchyroll) WatchHistory(page uint, size uint) (e []*HistoryEpisode, err error) {
- watchHistoryEndpoint := fmt.Sprintf("https://beta-api.crunchyroll.com/content/v1/watch-history/%s?page=%d&page_size=%d&locale=%s",
- c.Config.AccountID, page, size, c.Locale)
- resp, err := c.request(watchHistoryEndpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- if err = json.NewDecoder(resp.Body).Decode(&jsonBody); err != nil {
- return nil, fmt.Errorf("failed to parse 'watch-history' response: %w", err)
- }
-
- for _, item := range jsonBody["items"].([]interface{}) {
- panel := item.(map[string]interface{})["panel"]
-
- episode := &Episode{
- crunchy: c,
- }
- if err := decodeMapToStruct(panel, episode); err != nil {
- return nil, err
- }
-
- historyEpisode := &HistoryEpisode{
- Episode: episode,
- }
- if err := decodeMapToStruct(item, historyEpisode); err != nil {
- return nil, err
- }
-
- e = append(e, historyEpisode)
- }
-
- return e, nil
-}
diff --git a/watchlist.go b/watchlist.go
deleted file mode 100644
index 6fa0bc0..0000000
--- a/watchlist.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package crunchyroll
-
-import (
- "encoding/json"
- "fmt"
- "net/http"
- "net/url"
- "strconv"
-)
-
-// WatchlistLanguageType represents a filter type to filter Crunchyroll.Watchlist entries after sub or dub.
-type WatchlistLanguageType int
-
-const (
- WatchlistLanguageSubbed WatchlistLanguageType = iota + 1
- WatchlistLanguageDubbed
-)
-
-type WatchlistOrderType string
-
-const (
- WatchlistOrderAsc = "asc"
- WatchlistOrderDesc = "desc"
-)
-
-// WatchlistOptions represents options for receiving the user watchlist.
-type WatchlistOptions struct {
- // Order specified whether the results should be order ascending or descending.
- Order WatchlistOrderType
-
- // OnlyFavorites specifies whether only episodes which are marked as favorite should be returned.
- OnlyFavorites bool
-
- // LanguageType specifies whether returning episodes should be only subbed or dubbed.
- LanguageType WatchlistLanguageType
-
- // ContentType specified whether returning videos should only be series episodes or movies.
- // But tbh all movies I've searched on crunchy were flagged as series too, so this
- // parameter is kinda useless.
- ContentType MediaType
-}
-
-// Watchlist returns the watchlist entries for the currently logged in user.
-func (c *Crunchyroll) Watchlist(options WatchlistOptions, limit uint) ([]*WatchlistEntry, error) {
- values := url.Values{}
- if options.Order == "" {
- options.Order = WatchlistOrderDesc
- }
- values.Set("order", string(options.Order))
- if options.OnlyFavorites {
- values.Set("only_favorites", "true")
- }
- switch options.LanguageType {
- case WatchlistLanguageSubbed:
- values.Set("is_subbed", "true")
- case WatchlistLanguageDubbed:
- values.Set("is_dubbed", "true")
- }
- values.Set("n", strconv.Itoa(int(limit)))
- values.Set("locale", string(c.Locale))
-
- endpoint := fmt.Sprintf("https://beta.crunchyroll.com/content/v1/%s/watchlist?%s", c.Config.AccountID, values.Encode())
- resp, err := c.request(endpoint, http.MethodGet)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- var jsonBody map[string]interface{}
- json.NewDecoder(resp.Body).Decode(&jsonBody)
-
- var watchlistEntries []*WatchlistEntry
- if err := decodeMapToStruct(jsonBody["items"], &watchlistEntries); err != nil {
- return nil, err
- }
-
- for _, entry := range watchlistEntries {
- switch entry.Panel.Type {
- case WatchlistEntryEpisode:
- entry.Panel.EpisodeMetadata.crunchy = c
- case WatchlistEntrySeries:
- entry.Panel.SeriesMetadata.crunchy = c
- }
- }
-
- return watchlistEntries, nil
-}
-
-// WatchlistEntry contains information about an entry on the watchlist.
-type WatchlistEntry struct {
- Panel Panel `json:"panel"`
-
- New bool `json:"new"`
- NewContent bool `json:"new_content"`
- IsFavorite bool `json:"is_favorite"`
- NeverWatched bool `json:"never_watched"`
- CompleteStatus bool `json:"complete_status"`
- Playahead uint `json:"playahead"`
-}
From d65226252da808e9a8f892ff791332d878360809 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 27 Jun 2022 22:36:46 +0200
Subject: [PATCH 245/732] Add split notice to README
---
README.md | 80 +++++++++++++++++++++++++------------------------------
1 file changed, 36 insertions(+), 44 deletions(-)
diff --git a/README.md b/README.md
index 10d6c99..b6d960f 100644
--- a/README.md
+++ b/README.md
@@ -1,42 +1,41 @@
-# crunchyroll-go
+# crunchy-cli
-A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](https://www.crunchyroll.com) api. To use it, you need a crunchyroll premium account to for full (api) access.
+A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
CLI 🖥️
•
- Library 📚
- •
Disclaimer ☝️
•
License ⚖
-_There is currently a [poll](https://github.com/ByteDream/crunchyroll-go/issues/39) going on how to name the CLI in the next version. Please have a moment and participate to it._
+_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility.
+See [#39](https://github.com/ByteDream/crunchy-cli/issues/39) for more information._
# 🖥️ CLI
@@ -48,37 +47,42 @@ _There is currently a [poll](https://github.com/ByteDream/crunchyroll-go/issues/
## 💾 Get the executable
-- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchyroll-go/crunchy-{tag}_darwin)
+- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchy-cli/releases/latest) or get it from below:
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchyroll-go
```
-- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+-
+
+ On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchyroll-go/releases/latest) (like Raspberry Pi or M1 Mac):
- - use `make` (requires `go` to be installed):
+
+
+ Currently not working because the repo got renamed!
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
+ - use `make` (requires `go` to be installed):
```shell
- $ git clone https://github.com/ByteDream/crunchyroll-go
- $ cd crunchyroll-go
+ $ git clone https://github.com/ByteDream/crunchy-cli
+ $ cd crunchy-cli
$ make
$ sudo make install # <- only if you want to install it on your system
```
- - use `go`:
+ - use `go`:
```shell
- $ git clone https://github.com/ByteDream/crunchyroll-go
- $ cd crunchyroll-go
- $ go build -o crunchy cmd/crunchyroll-go/main.go
+ $ git clone https://github.com/ByteDream/crunchy-cli
+ $ cd crunchy-cli
+ $ go build -o crunchy .
```
## 📝 Examples
-_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli), further usages and options are described there.
+_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli), further usages and options are described there.
### Login
@@ -165,7 +169,7 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
| `-d` | `--directory` | Directory to download the video(s) to. |
| `-o` | `--output` | Name of the output file. |
-| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Cli#archive) for more information. |
+| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli#archive) for more information. |
| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
@@ -200,18 +204,6 @@ These flags you can use across every sub-command:
| `-v` | Shows additional debug output. |
| `-p` | Use a proxy to hide your ip / redirect your traffic. |
-# 📚 Library
-
-Download the library via `go get`
-
-```shell
-$ go get github.com/ByteDream/crunchyroll-go/v2
-```
-
-The documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/ByteDream/crunchyroll-go/v2).
-
-Examples how to use the library and some features of it are described in the [wiki](https://github.com/ByteDream/crunchyroll-go/wiki/Library).
-
# ☝️ Disclaimer
This tool is **ONLY** meant to be used for private purposes. To use this tool you need crunchyroll premium anyway, so there is no reason why rip and share the episodes.
@@ -220,4 +212,4 @@ This tool is **ONLY** meant to be used for private purposes. To use this tool yo
# ⚖ License
-This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file for more details.
+This project is licensed under the GNU General Public License v3.0 (GPL-3.0) - see the [LICENSE](LICENSE) file for more details.
From 781e52059145adeceb24e2dd9b764f80e3407920 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 29 Jun 2022 20:39:38 +0200
Subject: [PATCH 246/732] Update cobra version to 1.5.0
---
go.mod | 2 +-
go.sum | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/go.mod b/go.mod
index 988f612..2c0abc0 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.18
require (
github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220627201246-98185d763c0c
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.4.0
+ github.com/spf13/cobra v1.5.0
)
require (
diff --git a/go.sum b/go.sum
index 31a2cd9..626a2c5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,13 +1,13 @@
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/cpuguy83/go-md2man/v2 v2.0.1/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/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q=
-github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
+github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
+github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
From 303689ecbb1b303dde81856350991eb30d2c3b3f Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 30 Jun 2022 16:08:08 +0200
Subject: [PATCH 247/732] Move and refactor files and some more changes :3
---
Makefile | 8 +-
{commands => cli/commands/archive}/archive.go | 324 ++++--------
cli/commands/archive/compress.go | 136 +++++
.../commands/download}/download.go | 144 +++---
cli/commands/info/info.go | 40 ++
{commands => cli/commands}/logger.go | 59 ++-
cli/commands/login/login.go | 158 ++++++
{commands => cli/commands}/unix.go | 2 +-
{commands => cli/commands/update}/update.go | 35 +-
cli/commands/utils.go | 125 +++++
{commands => cli/commands}/windows.go | 2 +-
cli/root.go | 85 +++
commands/info.go | 40 --
commands/login.go | 206 --------
commands/root.go | 78 ---
commands/utils.go | 485 ------------------
crunchy-cli.1 | 2 +-
go.mod | 2 +-
go.sum | 2 +
main.go | 6 +-
utils/extract.go | 94 ++++
utils/file.go | 49 ++
utils/format.go | 63 +++
utils/http.go | 51 ++
utils/locale.go | 59 +++
utils/logger.go | 12 +
utils/save.go | 177 +++++++
utils/system.go | 7 +
utils/vars.go | 14 +
29 files changed, 1305 insertions(+), 1160 deletions(-)
rename {commands => cli/commands/archive}/archive.go (67%)
create mode 100644 cli/commands/archive/compress.go
rename {commands => cli/commands/download}/download.go (64%)
create mode 100644 cli/commands/info/info.go
rename {commands => cli/commands}/logger.go (82%)
create mode 100644 cli/commands/login/login.go
rename {commands => cli/commands}/unix.go (95%)
rename {commands => cli/commands/update}/update.go (67%)
create mode 100644 cli/commands/utils.go
rename {commands => cli/commands}/windows.go (94%)
create mode 100644 cli/root.go
delete mode 100644 commands/info.go
delete mode 100644 commands/login.go
delete mode 100644 commands/root.go
delete mode 100644 commands/utils.go
create mode 100644 utils/extract.go
create mode 100644 utils/file.go
create mode 100644 utils/format.go
create mode 100644 utils/http.go
create mode 100644 utils/locale.go
create mode 100644 utils/logger.go
create mode 100644 utils/save.go
create mode 100644 utils/system.go
create mode 100644 utils/vars.go
diff --git a/Makefile b/Makefile
index 4266a91..7836af9 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ DESTDIR=
PREFIX=/usr
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:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -24,8 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
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=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=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/commands.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
+ 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/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/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
strip $(VERSION_BINARY_NAME)_linux
diff --git a/commands/archive.go b/cli/commands/archive/archive.go
similarity index 67%
rename from commands/archive.go
rename to cli/commands/archive/archive.go
index 5ad6900..29ee836 100644
--- a/commands/archive.go
+++ b/cli/commands/archive/archive.go
@@ -1,15 +1,14 @@
-package commands
+package archive
import (
- "archive/tar"
- "archive/zip"
"bufio"
"bytes"
- "compress/gzip"
"context"
"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/utils"
+ crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
@@ -23,8 +22,6 @@ import (
"sort"
"strconv"
"strings"
- "sync"
- "time"
)
var (
@@ -42,39 +39,39 @@ var (
archiveGoroutinesFlag int
)
-var archiveCmd = &cobra.Command{
+var Cmd = &cobra.Command{
Use: "archive",
Short: "Stores the given videos with all subtitles and multiple audios in a .mkv file",
Args: cobra.MinimumNArgs(1),
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")
}
- out.Debug("FFmpeg detected")
+ utils.Log.Debug("FFmpeg detected")
if filepath.Ext(archiveOutputFlag) != ".mkv" {
return fmt.Errorf("currently only matroska / .mkv files are supported")
}
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 == "all" {
- archiveLanguagesFlag = allLocalesAsStrings()
+ archiveLanguagesFlag = utils.LocalesAsStrings()
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
for _, mode := range []string{"auto", "audio", "video"} {
if archiveMergeFlag == mode {
- out.Debug("Using %s merge behavior", archiveMergeFlag)
+ utils.Log.Debug("Using %s merge behavior", archiveMergeFlag)
found = true
break
}
@@ -87,7 +84,7 @@ var archiveCmd = &cobra.Command{
found = false
for _, algo := range []string{".tar", ".tar.gz", ".tgz", ".zip"} {
if strings.HasSuffix(archiveCompressFlag, algo) {
- out.Debug("Using %s compression", algo)
+ utils.Log.Debug("Using %s compression", algo)
found = true
break
}
@@ -100,8 +97,8 @@ var archiveCmd = &cobra.Command{
switch archiveResolutionFlag {
case "1080p", "720p", "480p", "360p":
- intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
- archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
+ intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
+ archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(archiveResolutionFlag, "p"))
case "240p":
// 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
archiveResolutionFlag = "428x240"
@@ -109,31 +106,33 @@ var archiveCmd = &cobra.Command{
default:
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
},
RunE: func(cmd *cobra.Command, args []string) error {
- loadCrunchy()
+ if err := commands.LoadCrunchy(); err != nil {
+ return err
+ }
return archive(args)
},
}
func init() {
- archiveCmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
+ Cmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
"language",
"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")
cwd, _ := os.Getwd()
- archiveCmd.Flags().StringVarP(&archiveDirectoryFlag,
+ Cmd.Flags().StringVarP(&archiveDirectoryFlag,
"directory",
"d",
cwd,
"The directory to store the files into")
- archiveCmd.Flags().StringVarP(&archiveOutputFlag,
+ Cmd.Flags().StringVarP(&archiveOutputFlag,
"output",
"o",
"{title}.mkv",
@@ -148,13 +147,13 @@ func init() {
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
- archiveCmd.Flags().StringVarP(&archiveMergeFlag,
+ Cmd.Flags().StringVarP(&archiveMergeFlag,
"merge",
"m",
"auto",
"Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'")
- archiveCmd.Flags().StringVarP(&archiveCompressFlag,
+ Cmd.Flags().StringVarP(&archiveCompressFlag,
"compress",
"c",
"",
@@ -162,7 +161,7 @@ func init() {
"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")
- archiveCmd.Flags().StringVarP(&archiveResolutionFlag,
+ Cmd.Flags().StringVarP(&archiveResolutionFlag,
"resolution",
"r",
"best",
@@ -171,51 +170,49 @@ func init() {
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
- archiveCmd.Flags().IntVarP(&archiveGoroutinesFlag,
+ Cmd.Flags().IntVarP(&archiveGoroutinesFlag,
"goroutines",
"g",
runtime.NumCPU(),
"Number of parallel segment downloads")
-
- rootCmd.AddCommand(archiveCmd)
}
func archive(urls []string) error {
for i, url := range urls {
- out.SetProgress("Parsing url %d", i+1)
+ utils.Log.SetProcess("Parsing url %d", i+1)
episodes, err := archiveExtractEpisodes(url)
if err != nil {
- out.StopProgress("Failed to parse url %d", i+1)
- if 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.StopProcess("Failed to parse url %d", i+1)
+ if utils.Crunchy.Config.Premium {
+ 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")
}
return err
}
- out.StopProgress("Parsed url %d", i+1)
+ utils.Log.StopProcess("Parsed url %d", i+1)
var compressFile *os.File
- var c compress
+ var c Compress
if archiveCompressFlag != "" {
- compressFile, err = os.Create(generateFilename(archiveCompressFlag, ""))
+ compressFile, err = os.Create(utils.GenerateFilename(archiveCompressFlag, ""))
if err != nil {
return fmt.Errorf("failed to create archive file: %v", err)
}
if strings.HasSuffix(archiveCompressFlag, ".tar") {
- c = newTarCompress(compressFile)
+ c = NewTarCompress(compressFile)
} else if strings.HasSuffix(archiveCompressFlag, ".tar.gz") || strings.HasSuffix(archiveCompressFlag, ".tgz") {
- c = newGzipCompress(compressFile)
+ c = NewGzipCompress(compressFile)
} else if strings.HasSuffix(archiveCompressFlag, ".zip") {
- c = newZipCompress(compressFile)
+ c = NewZipCompress(compressFile)
}
}
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 {
- 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,
info.Title,
info.Resolution,
@@ -224,26 +221,26 @@ func archive(urls []string) error {
info.EpisodeNumber)
}
}
- out.Empty()
+ utils.Log.Empty()
for j, season := range episodes {
for k, info := range season {
var filename string
var writeCloser io.WriteCloser
if c != nil {
- filename = info.Format(archiveOutputFlag)
+ filename = info.FormatString(archiveOutputFlag)
writeCloser, err = c.NewFile(info)
if err != nil {
return fmt.Errorf("failed to pre generate new archive file: %v", err)
}
} else {
- dir := info.Format(archiveDirectoryFlag)
+ dir := info.FormatString(archiveDirectoryFlag)
if _, err = os.Stat(dir); os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0777); err != nil {
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)
if err != nil {
return fmt.Errorf("failed to create new file: %v", err)
@@ -264,7 +261,7 @@ func archive(urls []string) error {
writeCloser.Close()
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
}
-func archiveInfo(info 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))
+func archiveInfo(info utils.FormatInformation, writeCloser io.WriteCloser, filename string) error {
+ utils.Log.Debug("Entering season %d, episode %d with %d additional formats", info.SeasonNumber, info.EpisodeNumber, len(info.AdditionalFormats))
dp, err := createArchiveProgress(info)
if err != nil {
@@ -307,7 +304,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
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)
} else {
dp.Update()
@@ -325,8 +322,8 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
select {
case <-sig:
signal.Stop(sig)
- out.Exit("Exiting... (may take a few seconds)")
- out.Exit("To force exit press ctrl+c (again)")
+ utils.Log.Err("Exiting... (may take a few seconds)")
+ utils.Log.Err("To force exit press ctrl+c (again)")
cancel()
// 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
@@ -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
}
}()
- out.Debug("Set up signal catcher")
+ utils.Log.Debug("Set up signal catcher")
var additionalDownloaderOpts []string
var mergeMessage string
switch archiveMergeFlag {
case "auto":
additionalDownloaderOpts = []string{"-vn"}
- for _, format := range info.additionalFormats {
- if format.Video.Bandwidth != info.format.Video.Bandwidth {
+ for _, format := range info.AdditionalFormats {
+ if format.Video.Bandwidth != info.Format.Video.Bandwidth {
// revoke the changed FFmpegOpts above
additionalDownloaderOpts = []string{}
break
@@ -361,12 +358,12 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
mergeMessage = "merging video for additional formats"
}
- out.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
- out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
- out.Info("\tAudio: %s", info.Audio)
- out.Info("\tSubtitle: %s", info.Subtitle)
- out.Info("\tResolution: %spx", info.Resolution)
- out.Info("\tFPS: %.2f", info.FPS)
+ utils.Log.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
+ utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
+ utils.Log.Info("\tAudio: %s", info.Audio)
+ utils.Log.Info("\tSubtitle: %s", info.Subtitle)
+ utils.Log.Info("\tResolution: %spx", info.Resolution)
+ utils.Log.Info("\tFPS: %.2f", info.FPS)
var videoFiles, audioFiles, subtitleFiles []string
defer func() {
@@ -376,7 +373,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
}()
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() {
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 {
var videos []string
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)
}
downloader.FFmpegOpts = []string{}
videoFiles = append(videoFiles, videos...)
} else {
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)
}
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"))
if sortSubtitles && len(archiveLanguagesFlag) > 0 {
// this sort the subtitle locales after the languages which were specified
// with the `archiveLanguagesFlag` flag
for _, language := range archiveLanguagesFlag {
- for i, subtitle := range info.format.Subtitles {
+ for i, subtitle := range info.Format.Subtitles {
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
}
}
@@ -417,7 +414,7 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
}
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)
}
subtitleFiles = append(subtitleFiles, subtitles...)
@@ -429,22 +426,22 @@ func archiveInfo(info formatInformation, writeCloser io.WriteCloser, filename st
dp.UpdateMessage("Download finished", false)
signal.Stop(sig)
- out.Debug("Stopped signal catcher")
+ utils.Log.Debug("Stopped signal catcher")
- out.Empty()
+ utils.Log.Empty()
return nil
}
-func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
+func createArchiveProgress(info utils.FormatInformation) (*commands.DownloadProgress, error) {
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)
}
// + number of segments a video has +1 is for merging
- progressCount += int(info.format.Video.Chunklist.Count()) + 1
- for _, f := range info.additionalFormats {
- if f == info.format {
+ progressCount += int(info.Format.Video.Chunklist.Count()) + 1
+ for _, f := range info.AdditionalFormats {
+ if f == info.Format {
continue
}
@@ -455,16 +452,16 @@ func createArchiveProgress(info formatInformation) (*downloadProgress, error) {
progressCount += int(f.Video.Chunklist.Count()) + 1
}
- dp := &downloadProgress{
- Prefix: out.InfoLog.Prefix(),
+ dp := &commands.DownloadProgress{
+ Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
Message: "Downloading video",
// number of segments a video +1 is for the success message
Total: progressCount + 1,
- Dev: out.IsDev(),
- Quiet: out.IsQuiet(),
+ Dev: utils.Log.IsDev(),
+ Quiet: utils.Log.(*commands.Logger).IsQuiet(),
}
- if out.IsDev() {
- dp.Prefix = out.DebugLog.Prefix()
+ if utils.Log.IsDev() {
+ dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
}
return dp, nil
@@ -497,7 +494,7 @@ func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, v
}
f.Close()
- out.Debug("Downloaded '%s' video", format.AudioLocale)
+ utils.Log.Debug("Downloaded '%s' video", format.AudioLocale)
}
return files, nil
@@ -522,7 +519,7 @@ func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitl
}
f.Close()
- out.Debug("Downloaded '%s' subtitles", subtitle.Locale)
+ utils.Log.Debug("Downloaded '%s' subtitles", subtitle.Locale)
}
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))
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("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("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 {
@@ -547,7 +544,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
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("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 {
@@ -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)))
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("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"}
@@ -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())
// 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
for _, option := range commandOptions {
@@ -591,7 +588,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
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
@@ -611,7 +608,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
return err
}
-func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
+func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
var hasJapanese bool
languagesAsLocale := []crunchyroll.LOCALE{crunchyroll.JP}
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 {
return nil, err
}
@@ -634,9 +631,9 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
for i, eps := range episodes {
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) {
- 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:]
}
- eps := make(map[int]map[int]*formatInformation)
+ eps := make(map[int]map[int]*utils.FormatInformation)
for _, lang := range episodes {
- for _, season := range utils.SortEpisodesBySeason(lang) {
+ for _, season := range crunchyUtils.SortEpisodesBySeason(lang) {
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 {
format, err := episode.GetFormat(archiveResolutionFlag, "", false)
@@ -657,9 +654,9 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
}
if _, ok := eps[episode.SeasonNumber][episode.EpisodeNumber]; !ok {
- eps[episode.SeasonNumber][episode.EpisodeNumber] = &formatInformation{
- format: format,
- additionalFormats: make([]*crunchyroll.Format, 0),
+ eps[episode.SeasonNumber][episode.EpisodeNumber] = &utils.FormatInformation{
+ Format: format,
+ AdditionalFormats: make([]*crunchyroll.Format, 0),
Title: episode.Title,
SeriesName: episode.SeriesTitle,
@@ -671,15 +668,15 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
Audio: format.AudioLocale,
}
} 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 {
- var tmpFormatInfo []formatInformation
+ var tmpFormatInfo []utils.FormatInformation
var keys []int
for episodeNumber := range e {
@@ -696,124 +693,3 @@ func archiveExtractEpisodes(url string) ([][]formatInformation, error) {
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
-}
diff --git a/cli/commands/archive/compress.go b/cli/commands/archive/compress.go
new file mode 100644
index 0000000..d6aa492
--- /dev/null
+++ b/cli/commands/archive/compress.go
@@ -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
+}
diff --git a/commands/download.go b/cli/commands/download/download.go
similarity index 64%
rename from commands/download.go
rename to cli/commands/download/download.go
index 82b68bf..4e0ef66 100644
--- a/commands/download.go
+++ b/cli/commands/download/download.go
@@ -1,10 +1,12 @@
-package commands
+package download
import (
"context"
"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/utils"
+ crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"math"
@@ -29,29 +31,29 @@ var (
downloadGoroutinesFlag int
)
-var downloadCmd = &cobra.Command{
+var Cmd = &cobra.Command{
Use: "download",
Short: "Download a video",
Args: cobra.MinimumNArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
- out.Debug("Validating arguments")
+ utils.Log.Debug("Validating arguments")
if filepath.Ext(downloadOutputFlag) != ".ts" {
- if !hasFFmpeg() {
+ if !utils.HasFFmpeg() {
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)
} 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)) {
- return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(allLocalesAsStrings(), ", "))
- } else if downloadSubtitleFlag != "" && !utils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
- return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(allLocalesAsStrings(), ", "))
+ if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
+ return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(utils.LocalesAsStrings(), ", "))
+ } else if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
+ 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 {
case "1080p", "720p", "480p", "360p":
@@ -64,35 +66,37 @@ var downloadCmd = &cobra.Command{
default:
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
},
RunE: func(cmd *cobra.Command, args []string) error {
- loadCrunchy()
+ if err := commands.LoadCrunchy(); err != nil {
+ return err
+ }
return download(args)
},
}
func init() {
- downloadCmd.Flags().StringVarP(&downloadAudioFlag, "audio",
+ Cmd.Flags().StringVarP(&downloadAudioFlag, "audio",
"a",
- string(systemLocale(false)),
- "The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", "))
- downloadCmd.Flags().StringVarP(&downloadSubtitleFlag,
+ string(utils.SystemLocale(false)),
+ "The locale of the audio. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
+ Cmd.Flags().StringVarP(&downloadSubtitleFlag,
"subtitle",
"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()
- downloadCmd.Flags().StringVarP(&downloadDirectoryFlag,
+ Cmd.Flags().StringVarP(&downloadDirectoryFlag,
"directory",
"d",
cwd,
"The directory to download the file(s) into")
- downloadCmd.Flags().StringVarP(&downloadOutputFlag,
+ Cmd.Flags().StringVarP(&downloadOutputFlag,
"output",
"o",
"{title}.ts",
@@ -108,7 +112,7 @@ func init() {
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
- downloadCmd.Flags().StringVarP(&downloadResolutionFlag,
+ Cmd.Flags().StringVarP(&downloadResolutionFlag,
"resolution",
"r",
"best",
@@ -117,34 +121,32 @@ func init() {
"\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
- downloadCmd.Flags().IntVarP(&downloadGoroutinesFlag,
+ Cmd.Flags().IntVarP(&downloadGoroutinesFlag,
"goroutines",
"g",
runtime.NumCPU(),
"Sets how many parallel segment downloads should be used")
-
- rootCmd.AddCommand(downloadCmd)
}
func download(urls []string) error {
for i, url := range urls {
- out.SetProgress("Parsing url %d", i+1)
+ utils.Log.SetProcess("Parsing url %d", i+1)
episodes, err := downloadExtractEpisodes(url)
if err != nil {
- out.StopProgress("Failed to parse url %d", i+1)
- if 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.StopProcess("Failed to parse url %d", i+1)
+ if utils.Crunchy.Config.Premium {
+ 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")
}
return err
}
- out.StopProgress("Parsed url %d", i+1)
+ utils.Log.StopProcess("Parsed url %d", i+1)
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 {
- 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,
info.Title,
info.Resolution,
@@ -153,17 +155,17 @@ func download(urls []string) error {
info.EpisodeNumber)
}
}
- out.Empty()
+ utils.Log.Empty()
for j, season := range episodes {
for k, info := range season {
- dir := info.Format(downloadDirectoryFlag)
+ dir := info.FormatString(downloadDirectoryFlag)
if _, err = os.Stat(dir); os.IsNotExist(err) {
if err = os.MkdirAll(dir, 0777); err != nil {
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 {
return fmt.Errorf("failed to create output file: %v", err)
}
@@ -176,7 +178,7 @@ func download(urls []string) error {
file.Close()
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
}
-func downloadInfo(info formatInformation, file *os.File) error {
- out.Debug("Entering season %d, episode %d", info.SeasonNumber, info.EpisodeNumber)
+func downloadInfo(info utils.FormatInformation, file *os.File) error {
+ 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)
}
- dp := &downloadProgress{
- Prefix: out.InfoLog.Prefix(),
+ dp := &commands.DownloadProgress{
+ Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
Message: "Downloading video",
// number of segments a video has +2 is for merging and the success message
- Total: int(info.format.Video.Chunklist.Count()) + 2,
- Dev: out.IsDev(),
- Quiet: out.IsQuiet(),
+ Total: int(info.Format.Video.Chunklist.Count()) + 2,
+ Dev: utils.Log.IsDev(),
+ Quiet: utils.Log.(*commands.Logger).IsQuiet(),
}
- if out.IsDev() {
- dp.Prefix = out.DebugLog.Prefix()
+ if utils.Log.IsDev() {
+ dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
}
defer func() {
if dp.Total != dp.Current {
@@ -217,7 +219,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
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)
} else {
dp.Update()
@@ -228,7 +230,7 @@ func downloadInfo(info formatInformation, file *os.File) error {
}
return nil
})
- if hasFFmpeg() {
+ if utils.HasFFmpeg() {
downloader.FFmpegOpts = make([]string, 0)
}
@@ -238,8 +240,8 @@ func downloadInfo(info formatInformation, file *os.File) error {
select {
case <-sig:
signal.Stop(sig)
- out.Exit("Exiting... (may take a few seconds)")
- out.Exit("To force exit press ctrl+c (again)")
+ utils.Log.Err("Exiting... (may take a few seconds)")
+ utils.Log.Err("To force exit press ctrl+c (again)")
cancel()
// 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
@@ -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
}
}()
- 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()))
- out.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
- out.Info("\tAudio: %s", info.Audio)
- out.Info("\tSubtitle: %s", info.Subtitle)
- out.Info("\tResolution: %spx", info.Resolution)
- out.Info("\tFPS: %.2f", info.FPS)
- if err := info.format.Download(downloader); err != nil {
+ utils.Log.Info("Downloading episode `%s` to `%s`", info.Title, filepath.Base(file.Name()))
+ utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
+ utils.Log.Info("\tAudio: %s", info.Audio)
+ utils.Log.Info("\tSubtitle: %s", info.Subtitle)
+ utils.Log.Info("\tResolution: %spx", info.Resolution)
+ utils.Log.Info("\tFPS: %.2f", info.FPS)
+ if err := info.Format.Download(downloader); err != nil {
return fmt.Errorf("error while downloading: %v", err)
}
dp.UpdateMessage("Download finished", false)
signal.Stop(sig)
- out.Debug("Stopped signal catcher")
+ utils.Log.Debug("Stopped signal catcher")
- out.Empty()
+ utils.Log.Empty()
return nil
}
-func downloadExtractEpisodes(url string) ([][]formatInformation, error) {
- episodes, err := extractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
+func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
+ episodes, err := utils.ExtractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
if err != nil {
return nil, err
}
japanese := episodes[0]
custom := episodes[1]
- sort.Sort(utils.EpisodesByNumber(japanese))
- sort.Sort(utils.EpisodesByNumber(custom))
+ sort.Sort(crunchyUtils.EpisodesByNumber(japanese))
+ sort.Sort(crunchyUtils.EpisodesByNumber(custom))
var errMessages []string
@@ -303,25 +305,25 @@ func downloadExtractEpisodes(url string) ([][]formatInformation, error) {
if len(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 {
for _, msg := range errMessages {
- out.SetProgress(msg)
+ utils.Log.SetProcess(msg)
}
}
- var infoFormat [][]formatInformation
- for _, season := range utils.SortEpisodesBySeason(final) {
- tmpFormatInformation := make([]formatInformation, 0)
+ var infoFormat [][]utils.FormatInformation
+ for _, season := range crunchyUtils.SortEpisodesBySeason(final) {
+ tmpFormatInformation := make([]utils.FormatInformation, 0)
for _, episode := range season {
format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
if err != nil {
return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
}
- tmpFormatInformation = append(tmpFormatInformation, formatInformation{
- format: format,
+ tmpFormatInformation = append(tmpFormatInformation, utils.FormatInformation{
+ Format: format,
Title: episode.Title,
SeriesName: episode.SeriesTitle,
diff --git a/cli/commands/info/info.go b/cli/commands/info/info.go
new file mode 100644
index 0000000..a9a6ef0
--- /dev/null
+++ b/cli/commands/info/info.go
@@ -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
+}
diff --git a/commands/logger.go b/cli/commands/logger.go
similarity index 82%
rename from commands/logger.go
rename to cli/commands/logger.go
index fab8515..2f3ae37 100644
--- a/commands/logger.go
+++ b/cli/commands/logger.go
@@ -2,6 +2,7 @@ package commands
import (
"fmt"
+ "github.com/ByteDream/crunchy-cli/utils"
"io"
"log"
"os"
@@ -35,19 +36,7 @@ type progress struct {
stop bool
}
-type logger struct {
- 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 {
+func NewLogger(debug, info, err bool) *Logger {
initPrefixBecauseWindowsSucksBallsHard()
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)
}
- return &logger{
+ return &Logger{
DebugLog: debugLog,
InfoLog: infoLog,
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
}
-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
}
-func (l *logger) Debug(format string, v ...interface{}) {
+func (l *Logger) Debug(format string, v ...interface{}) {
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...)
}
-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...)
}
-func (l *logger) Exit(format string, v ...interface{}) {
- fmt.Fprintln(l.ErrLog.Writer(), fmt.Sprintf(format, v...))
-}
-
-func (l *logger) Empty() {
+func (l *Logger) Empty() {
if !l.devView && l.InfoLog.Writer() != io.Discard {
fmt.Println("")
}
}
-func (l *logger) SetProgress(format string, v ...interface{}) {
- if out.InfoLog.Writer() == io.Discard {
+func (l *Logger) SetProcess(format string, v ...interface{}) {
+ if l.InfoLog.Writer() == io.Discard {
return
} else if l.devView {
l.Debug(format, v...)
@@ -175,8 +178,8 @@ func (l *logger) SetProgress(format string, v ...interface{}) {
}()
}
-func (l *logger) StopProgress(format string, v ...interface{}) {
- if out.InfoLog.Writer() == io.Discard {
+func (l *Logger) StopProcess(format string, v ...interface{}) {
+ if l.InfoLog.Writer() == io.Discard {
return
} else if l.devView {
l.Debug(format, v...)
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
new file mode 100644
index 0000000..31155d8
--- /dev/null
+++ b/cli/commands/login/login.go
@@ -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
+}
diff --git a/commands/unix.go b/cli/commands/unix.go
similarity index 95%
rename from commands/unix.go
rename to cli/commands/unix.go
index 955695c..ec69180 100644
--- a/commands/unix.go
+++ b/cli/commands/unix.go
@@ -19,7 +19,7 @@ func init() {
}
}
-func readLineSilent() ([]byte, error) {
+func ReadLineSilent() ([]byte, error) {
pid, err := setEcho(false)
if err != nil {
return nil, err
diff --git a/commands/update.go b/cli/commands/update/update.go
similarity index 67%
rename from commands/update.go
rename to cli/commands/update/update.go
index fb4c1e9..6587ca2 100644
--- a/commands/update.go
+++ b/cli/commands/update/update.go
@@ -1,8 +1,9 @@
-package commands
+package update
import (
"encoding/json"
"fmt"
+ "github.com/ByteDream/crunchy-cli/utils"
"github.com/spf13/cobra"
"io"
"os"
@@ -15,7 +16,7 @@ var (
updateInstallFlag bool
)
-var updateCmd = &cobra.Command{
+var Cmd = &cobra.Command{
Use: "update",
Short: "Check if updates are available",
Args: cobra.MaximumNArgs(0),
@@ -26,19 +27,17 @@ var updateCmd = &cobra.Command{
}
func init() {
- updateCmd.Flags().BoolVarP(&updateInstallFlag,
+ Cmd.Flags().BoolVarP(&updateInstallFlag,
"install",
"i",
false,
"If set and a new version is available, the new version gets installed")
-
- rootCmd.AddCommand(updateCmd)
}
func update() error {
var release map[string]interface{}
- resp, err := client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
+ resp, err := utils.Client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
if err != nil {
return err
}
@@ -48,8 +47,8 @@ func update() error {
}
releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
- if Version == "development" {
- out.Info("Development version, update service not available")
+ if utils.Version == "development" {
+ utils.Log.Info("Development version, update service not available")
return nil
}
@@ -58,17 +57,17 @@ func update() error {
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 {
- 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
for i := 0; i < 3; 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
} else if latestRelease[i] > internalVersion[i] {
hasUpdate = true
@@ -76,11 +75,11 @@ func update() error {
}
if !hasUpdate {
- out.Info("Version is up-to-date")
+ utils.Log.Info("Version is up-to-date")
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 runtime.GOARCH != "amd64" {
@@ -93,7 +92,7 @@ func update() error {
case "linux":
yayCommand := exec.Command("pacman -Q crunchy-cli")
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
}
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)
}
- out.SetProgress("Updating executable %s", os.Args[0])
+ utils.Log.SetProcess("Updating executable %s", os.Args[0])
perms, err := os.Stat(os.Args[0])
if err != nil {
@@ -119,7 +118,7 @@ func update() error {
}
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 {
return err
}
@@ -129,7 +128,7 @@ func update() error {
return err
}
- out.StopProgress("Updated executable %s", os.Args[0])
+ utils.Log.StopProcess("Updated executable %s", os.Args[0])
}
return nil
diff --git a/cli/commands/utils.go b/cli/commands/utils.go
new file mode 100644
index 0000000..924cd4d
--- /dev/null
+++ b/cli/commands/utils.go
@@ -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
+}
diff --git a/commands/windows.go b/cli/commands/windows.go
similarity index 94%
rename from commands/windows.go
rename to cli/commands/windows.go
index 2ae47b5..a9bce74 100644
--- a/commands/windows.go
+++ b/cli/commands/windows.go
@@ -9,7 +9,7 @@ import (
)
// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
-func readLineSilent() ([]byte, error) {
+func ReadLineSilent() ([]byte, error) {
var oldMode uint32
if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
diff --git a/cli/root.go b/cli/root.go
new file mode 100644
index 0000000..ebb5079
--- /dev/null
+++ b/cli/root.go
@@ -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)
+ }
+}
diff --git a/commands/info.go b/commands/info.go
deleted file mode 100644
index fe9693d..0000000
--- a/commands/info.go
+++ /dev/null
@@ -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
-}
diff --git a/commands/login.go b/commands/login.go
deleted file mode 100644
index a710bb6..0000000
--- a/commands/login.go
+++ /dev/null
@@ -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
-}
diff --git a/commands/root.go b/commands/root.go
deleted file mode 100644
index ffcf35a..0000000
--- a/commands/root.go
+++ /dev/null
@@ -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)
- }
-}
diff --git a/commands/utils.go b/commands/utils.go
deleted file mode 100644
index 486c61f..0000000
--- a/commands/utils.go
+++ /dev/null
@@ -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)
-}
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index d9b1075..ebadb9c 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -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.
.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
\fB-a, --audio AUDIO\fR
diff --git a/go.mod b/go.mod
index 2c0abc0..909378c 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/ByteDream/crunchy-cli
go 1.18
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/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 626a2c5..5603301 100644
--- a/go.sum
+++ b/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/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/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
diff --git a/main.go b/main.go
index f9c3a90..7275a13 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,9 @@
package main
-import "github.com/ByteDream/crunchy-cli/commands"
+import (
+ "github.com/ByteDream/crunchy-cli/cli"
+)
func main() {
- commands.Execute()
+ cli.Execute()
}
diff --git a/utils/extract.go b/utils/extract.go
new file mode 100644
index 0000000..ba52242
--- /dev/null
+++ b/utils/extract.go
@@ -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
+}
diff --git a/utils/file.go b/utils/file.go
new file mode 100644
index 0000000..183b06f
--- /dev/null
+++ b/utils/file.go
@@ -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
+}
diff --git a/utils/format.go b/utils/format.go
new file mode 100644
index 0000000..9642a31
--- /dev/null
+++ b/utils/format.go
@@ -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
+}
diff --git a/utils/http.go b/utils/http.go
new file mode 100644
index 0000000..f572bd9
--- /dev/null
+++ b/utils/http.go
@@ -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
+ }
+}
diff --git a/utils/locale.go b/utils/locale.go
new file mode 100644
index 0000000..e1517bb
--- /dev/null
+++ b/utils/locale.go
@@ -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
+}
diff --git a/utils/logger.go b/utils/logger.go
new file mode 100644
index 0000000..27ea344
--- /dev/null
+++ b/utils/logger.go
@@ -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)
+}
diff --git a/utils/save.go b/utils/save.go
new file mode 100644
index 0000000..43eeda7
--- /dev/null
+++ b/utils/save.go
@@ -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
+}
diff --git a/utils/system.go b/utils/system.go
new file mode 100644
index 0000000..ac97706
--- /dev/null
+++ b/utils/system.go
@@ -0,0 +1,7 @@
+package utils
+
+import "os/exec"
+
+func HasFFmpeg() bool {
+ return exec.Command("ffmpeg", "-h").Run() == nil
+}
diff --git a/utils/vars.go b/utils/vars.go
new file mode 100644
index 0000000..caca63e
--- /dev/null
+++ b/utils/vars.go
@@ -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
+)
From f1a41d6d3b66744b0f9df52dd9da712232f18cd4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Thu, 7 Jul 2022 21:21:44 +0200
Subject: [PATCH 248/732] Change name due to organization move
---
Makefile | 8 +++---
README.md | 42 +++++++++++++++---------------
cmd/crunchyroll-go/cmd/archive.go | 6 ++---
cmd/crunchyroll-go/cmd/download.go | 6 ++---
cmd/crunchyroll-go/cmd/login.go | 2 +-
cmd/crunchyroll-go/cmd/root.go | 4 +--
cmd/crunchyroll-go/cmd/utils.go | 4 +--
cmd/crunchyroll-go/main.go | 2 +-
crunchyroll-go.1 | 6 ++---
crunchyroll.go | 2 +-
go.mod | 2 +-
utils/locale.go | 4 +--
utils/sort.go | 2 +-
13 files changed, 44 insertions(+), 46 deletions(-)
diff --git a/Makefile b/Makefile
index 3dd5c3e..44662f1 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ DESTDIR=
PREFIX=/usr
build:
- go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
+ go build -ldflags "-X 'github.com/crunchy-labs/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(BINARY_NAME) cmd/crunchyroll-go/main.go
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -24,8 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchyroll-go/LICENSE
release:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
- CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe cmd/crunchyroll-go/main.go
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchyroll-go/v2/cmd/crunchyroll-go/cmd.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin cmd/crunchyroll-go/main.go
strip $(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index b6d960f..df75ec8 100644
--- a/README.md
+++ b/README.md
@@ -3,26 +3,26 @@
A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
@@ -35,7 +35,7 @@ A [go](https://golang.org) written cli client for [crunchyroll](https://www.crun
_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility.
-See [#39](https://github.com/ByteDream/crunchy-cli/issues/39) for more information._
+See [#39](https://github.com/crunchy-labs/crunchy-cli/issues/39) for more information._
# 🖥️ CLI
@@ -47,10 +47,10 @@ See [#39](https://github.com/ByteDream/crunchy-cli/issues/39) for more informati
## 💾 Get the executable
-- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchy-cli/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_darwin)
+- 📥 Download the latest binaries [here](https://github.com/crunchy-labs/crunchy-cli/releases/latest) or get it from below:
+ - [Linux (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchyroll-go
@@ -65,24 +65,24 @@ See [#39](https://github.com/ByteDream/crunchy-cli/issues/39) for more informati
Currently not working because the repo got renamed!
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
```shell
- $ git clone https://github.com/ByteDream/crunchy-cli
+ $ git clone https://github.com/crunchy-labs/crunchy-cli
$ cd crunchy-cli
$ make
$ sudo make install # <- only if you want to install it on your system
```
- use `go`:
```shell
- $ git clone https://github.com/ByteDream/crunchy-cli
+ $ git clone https://github.com/crunchy-labs/crunchy-cli
$ cd crunchy-cli
$ go build -o crunchy .
```
## 📝 Examples
-_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli), further usages and options are described there.
+_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli), further usages and options are described there.
### Login
@@ -169,7 +169,7 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
| `-d` | `--directory` | Directory to download the video(s) to. |
| `-o` | `--output` | Name of the output file. |
-| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli#archive) for more information. |
+| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#archive) for more information. |
| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
diff --git a/cmd/crunchyroll-go/cmd/archive.go b/cmd/crunchyroll-go/cmd/archive.go
index 6dcf61b..baa90e5 100644
--- a/cmd/crunchyroll-go/cmd/archive.go
+++ b/cmd/crunchyroll-go/cmd/archive.go
@@ -8,8 +8,8 @@ import (
"compress/gzip"
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
@@ -187,7 +187,7 @@ func archive(urls []string) error {
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/crunchy-labs/crunchyroll-go/issues/22 for more information")
return err
}
out.StopProgress("Parsed url %d", i+1)
diff --git a/cmd/crunchyroll-go/cmd/download.go b/cmd/crunchyroll-go/cmd/download.go
index 92b6027..063a2f7 100644
--- a/cmd/crunchyroll-go/cmd/download.go
+++ b/cmd/crunchyroll-go/cmd/download.go
@@ -3,8 +3,8 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"math"
@@ -133,7 +133,7 @@ func download(urls []string) error {
if err != nil {
out.StopProgress("Failed to parse url %d", i+1)
out.Debug("If the error says no episodes could be found but the passed url is correct and a crunchyroll classic url, " +
- "try the corresponding crunchyroll beta url instead and try again. See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information")
+ "try the corresponding crunchyroll beta url instead and try again. See https://github.com/crunchy-labs/crunchyroll-go/issues/22 for more information")
return err
}
out.StopProgress("Parsed url %d", i+1)
diff --git a/cmd/crunchyroll-go/cmd/login.go b/cmd/crunchyroll-go/cmd/login.go
index c9fc923..d5cc993 100644
--- a/cmd/crunchyroll-go/cmd/login.go
+++ b/cmd/crunchyroll-go/cmd/login.go
@@ -2,7 +2,7 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
"github.com/spf13/cobra"
"os"
"path/filepath"
diff --git a/cmd/crunchyroll-go/cmd/root.go b/cmd/crunchyroll-go/cmd/root.go
index 82ee133..8acdefe 100644
--- a/cmd/crunchyroll-go/cmd/root.go
+++ b/cmd/crunchyroll-go/cmd/root.go
@@ -3,7 +3,7 @@ package cmd
import (
"context"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
"github.com/spf13/cobra"
"net/http"
"os"
@@ -29,7 +29,7 @@ var (
var rootCmd = &cobra.Command{
Use: "crunchyroll-go",
Version: Version,
- Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/ByteDream/crunchyroll-go/wiki",
+ Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/crunchy-labs/crunchyroll-go/wiki",
SilenceErrors: true,
SilenceUsage: true,
diff --git a/cmd/crunchyroll-go/cmd/utils.go b/cmd/crunchyroll-go/cmd/utils.go
index 2a632ff..8df50f1 100644
--- a/cmd/crunchyroll-go/cmd/utils.go
+++ b/cmd/crunchyroll-go/cmd/utils.go
@@ -2,8 +2,8 @@ package cmd
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v2"
- "github.com/ByteDream/crunchyroll-go/v2/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2/utils"
"net/http"
"net/url"
"os"
diff --git a/cmd/crunchyroll-go/main.go b/cmd/crunchyroll-go/main.go
index a502afb..48a4088 100644
--- a/cmd/crunchyroll-go/main.go
+++ b/cmd/crunchyroll-go/main.go
@@ -1,7 +1,7 @@
package main
import (
- "github.com/ByteDream/crunchyroll-go/v2/cmd/crunchyroll-go/cmd"
+ "github.com/crunchy-labs/crunchyroll-go/v2/cmd/crunchyroll-go/cmd"
)
func main() {
diff --git a/crunchyroll-go.1 b/crunchyroll-go.1
index 558aa23..e6141f4 100644
--- a/crunchyroll-go.1
+++ b/crunchyroll-go.1
@@ -184,12 +184,12 @@ $ crunchyroll-go archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
.SH AUTHOR
-ByteDream
+Crunchy Labs
.br
-Source: https://github.com/ByteDream/crunchyroll-go
+Source: https://github.com/crunchy-labs/crunchyroll-go
.SH COPYRIGHT
-Copyright (C) 2022 ByteDream
+Copyright (C) 2022 Crunchy Labs
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
diff --git a/crunchyroll.go b/crunchyroll.go
index dcfc375..6f221f6 100644
--- a/crunchyroll.go
+++ b/crunchyroll.go
@@ -337,7 +337,7 @@ func (c *Crunchyroll) Search(query string, limit uint) (s []*Series, m []*Movie,
//
// Deprecated: Use Search instead. The first result sometimes isn't the correct one
// so this function is inaccurate in some cases.
-// See https://github.com/ByteDream/crunchyroll-go/issues/22 for more information.
+// See https://github.com/crunchy-labs/crunchyroll-go/issues/22 for more information.
func (c *Crunchyroll) FindVideoByName(seriesName string) (Video, error) {
s, m, err := c.Search(seriesName, 1)
if err != nil {
diff --git a/go.mod b/go.mod
index 99877f2..15cf1e1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/ByteDream/crunchyroll-go/v2
+module github.com/crunchy-labs/crunchyroll-go/v2
go 1.18
diff --git a/utils/locale.go b/utils/locale.go
index 537b165..fbedd6a 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -1,8 +1,6 @@
package utils
-import (
- "github.com/ByteDream/crunchyroll-go/v2"
-)
+import "github.com/crunchy-labs/crunchyroll-go/v2"
// AllLocales is an array of all available locales.
var AllLocales = []crunchyroll.LOCALE{
diff --git a/utils/sort.go b/utils/sort.go
index a44717d..a98f858 100644
--- a/utils/sort.go
+++ b/utils/sort.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go/v2"
+ "github.com/crunchy-labs/crunchyroll-go/v2"
"sort"
"strconv"
"strings"
From 680db83c59588b90a2de5067ae2cbe64722cb170 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 9 Jul 2022 01:03:09 +0200
Subject: [PATCH 249/732] Reactivate scoop install instructions
---
README.md | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/README.md b/README.md
index df75ec8..467b6ff 100644
--- a/README.md
+++ b/README.md
@@ -55,15 +55,11 @@ See [#39](https://github.com/crunchy-labs/crunchy-cli/issues/39) for more inform
```shell
$ yay -S crunchyroll-go
```
--
-
- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
-
-
Currently not working because the repo got renamed!
- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
From 136591061038d38d040992227d5f4e9f9fcbd3be Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 9 Jul 2022 01:25:00 +0200
Subject: [PATCH 250/732] Change discord invite link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 467b6ff..141207d 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ A [go](https://golang.org) written cli client for [crunchyroll](https://www.crun
-
+
From 5b59662e29de060fad38274621eba399b7eb06c2 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 9 Jul 2022 01:25:59 +0200
Subject: [PATCH 251/732] Change discord shield server
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 141207d..0c0b329 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ A [go](https://golang.org) written cli client for [crunchyroll](https://www.crun
-
+
From fd502446c62c2c0bd2c0dabf85e4aebcbf0964cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A7=81=E3=81=AF=E3=83=AC=E3=82=AA=E3=83=B3=E3=81=A7?=
=?UTF-8?q?=E3=81=99?= <50367411+IchBinLeoon@users.noreply.github.com>
Date: Mon, 11 Jul 2022 22:03:02 +0200
Subject: [PATCH 252/732] Fix typo
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0c0b329..6374ae0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# crunchy-cli
-A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
+A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account for full (api) access.
From 5b4c228b60cae297760f6401ad8c6ea6ebc84ebc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 25 Jul 2022 10:53:29 +0200
Subject: [PATCH 253/732] Change crunchyroll-go dependency name
---
cli/commands/archive/archive.go | 4 ++--
cli/commands/download/download.go | 4 ++--
cli/commands/info/info.go | 2 +-
cli/commands/login/login.go | 2 +-
go.mod | 2 +-
go.sum | 6 ++----
utils/extract.go | 4 ++--
utils/format.go | 2 +-
utils/locale.go | 4 ++--
utils/save.go | 2 +-
utils/vars.go | 2 +-
11 files changed, 16 insertions(+), 18 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 29ee836..99cb77f 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -7,8 +7,8 @@ import (
"fmt"
"github.com/ByteDream/crunchy-cli/cli/commands"
"github.com/ByteDream/crunchy-cli/utils"
- "github.com/ByteDream/crunchyroll-go/v3"
- crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"io"
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index 4e0ef66..2f2ec9f 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -5,8 +5,8 @@ import (
"fmt"
"github.com/ByteDream/crunchy-cli/cli/commands"
"github.com/ByteDream/crunchy-cli/utils"
- "github.com/ByteDream/crunchyroll-go/v3"
- crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
"github.com/spf13/cobra"
"math"
diff --git a/cli/commands/info/info.go b/cli/commands/info/info.go
index a9a6ef0..9b816fd 100644
--- a/cli/commands/info/info.go
+++ b/cli/commands/info/info.go
@@ -4,7 +4,7 @@ import (
"fmt"
"github.com/ByteDream/crunchy-cli/cli/commands"
"github.com/ByteDream/crunchy-cli/utils"
- crunchyUtils "github.com/ByteDream/crunchyroll-go/v3/utils"
+ crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/spf13/cobra"
)
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index 31155d8..7fb135c 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/ByteDream/crunchy-cli/cli/commands"
"github.com/ByteDream/crunchy-cli/utils"
- "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
"github.com/spf13/cobra"
"os"
)
diff --git a/go.mod b/go.mod
index 909378c..ce09935 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/ByteDream/crunchy-cli
go 1.18
require (
- github.com/ByteDream/crunchyroll-go/v3 v3.0.0-20220630135625-ed58b3fe8cc1
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 5603301..34683e1 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,6 @@
-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-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/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd h1:wxMjLwhvilxwTnYCOXmBPenMNymhC63m4aFzQ5C7ipQ=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
diff --git a/utils/extract.go b/utils/extract.go
index ba52242..60ced1d 100644
--- a/utils/extract.go
+++ b/utils/extract.go
@@ -2,8 +2,8 @@ package utils
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v3"
- "github.com/ByteDream/crunchyroll-go/v3/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"regexp"
"strconv"
"strings"
diff --git a/utils/format.go b/utils/format.go
index 9642a31..6d40142 100644
--- a/utils/format.go
+++ b/utils/format.go
@@ -2,7 +2,7 @@ package utils
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
"reflect"
"runtime"
"strings"
diff --git a/utils/locale.go b/utils/locale.go
index e1517bb..9769940 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -2,8 +2,8 @@ package utils
import (
"fmt"
- "github.com/ByteDream/crunchyroll-go/v3"
- "github.com/ByteDream/crunchyroll-go/v3/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"os"
"os/exec"
"runtime"
diff --git a/utils/save.go b/utils/save.go
index 43eeda7..ef6d0ec 100644
--- a/utils/save.go
+++ b/utils/save.go
@@ -6,7 +6,7 @@ import (
"crypto/rand"
"crypto/sha256"
"fmt"
- "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
"io"
"os"
"path/filepath"
diff --git a/utils/vars.go b/utils/vars.go
index caca63e..d2893a5 100644
--- a/utils/vars.go
+++ b/utils/vars.go
@@ -1,7 +1,7 @@
package utils
import (
- "github.com/ByteDream/crunchyroll-go/v3"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
"net/http"
)
From 2773445050c45d262d18834c9b1f9cf2394e524a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 26 Jul 2022 12:47:23 +0200
Subject: [PATCH 254/732] Change author names and links to crunchy-labs
---
Makefile | 8 +++----
README.md | 40 +++++++++++++++----------------
cli/commands/archive/archive.go | 6 ++---
cli/commands/archive/compress.go | 2 +-
cli/commands/download/download.go | 6 ++---
cli/commands/info/info.go | 4 ++--
cli/commands/logger.go | 2 +-
cli/commands/login/login.go | 4 ++--
cli/commands/update/update.go | 12 +++++-----
cli/commands/utils.go | 2 +-
cli/root.go | 16 ++++++-------
crunchy-cli.1 | 6 ++---
go.mod | 2 +-
main.go | 2 +-
14 files changed, 56 insertions(+), 56 deletions(-)
diff --git a/Makefile b/Makefile
index 7836af9..966872b 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@ DESTDIR=
PREFIX=/usr
build:
- go build -ldflags "-X 'github.com/ByteDream/crunchy-cli/utils.Version=$(VERSION)'" -o $(BINARY_NAME) .
+ go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(BINARY_NAME) .
clean:
rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
@@ -24,8 +24,8 @@ uninstall:
rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
release:
- 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/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/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
+ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux .
+ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe .
+ CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
strip $(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index bce05b9..09cd829 100644
--- a/README.md
+++ b/README.md
@@ -3,26 +3,26 @@
A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
-
-
+
+
-
-
+
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
@@ -47,10 +47,10 @@ See #39 for more information._
## 💾 Get the executable
-- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchy-cli/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/ByteDream/crunchy-cli/crunchy-{tag}_darwin)
+- 📥 Download the latest binaries [here](https://github.com/crunchy-labs/crunchy-cli/releases/latest) or get it from below:
+ - [Linux (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchyroll-go
@@ -65,24 +65,24 @@ See #39 for more information._
Currently not working because the repo got renamed!
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/ByteDream/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
+- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
```shell
- $ git clone https://github.com/ByteDream/crunchy-cli
+ $ git clone https://github.com/crunchy-labs/crunchy-cli
$ cd crunchy-cli
$ make
$ sudo make install # <- only if you want to install it on your system
```
- use `go`:
```shell
- $ git clone https://github.com/ByteDream/crunchy-cli
+ $ git clone https://github.com/crunchy-labs/crunchy-cli
$ cd crunchy-cli
$ go build -o crunchy .
```
## 📝 Examples
-_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli), further usages and options are described there.
+_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli), further usages and options are described there.
### Login
@@ -169,7 +169,7 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
| `-d` | `--directory` | Directory to download the video(s) to. |
| `-o` | `--output` | Name of the output file. |
-| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/ByteDream/crunchy-cli/wiki/Cli#archive) for more information. |
+| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#archive) for more information. |
| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 99cb77f..8e09f5e 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -5,8 +5,8 @@ import (
"bytes"
"context"
"fmt"
- "github.com/ByteDream/crunchy-cli/cli/commands"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"github.com/crunchy-labs/crunchyroll-go/v3"
crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
@@ -185,7 +185,7 @@ func archive(urls []string) error {
utils.Log.StopProcess("Failed to parse url %d", i+1)
if utils.Crunchy.Config.Premium {
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/crunchy-labs/crunchy-cli/issues/22 for more information")
}
return err
}
diff --git a/cli/commands/archive/compress.go b/cli/commands/archive/compress.go
index d6aa492..e0b9ad4 100644
--- a/cli/commands/archive/compress.go
+++ b/cli/commands/archive/compress.go
@@ -6,7 +6,7 @@ import (
"bytes"
"compress/gzip"
"fmt"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"io"
"os"
"path/filepath"
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index 2f2ec9f..2976e2d 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -3,8 +3,8 @@ package download
import (
"context"
"fmt"
- "github.com/ByteDream/crunchy-cli/cli/commands"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"github.com/crunchy-labs/crunchyroll-go/v3"
crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/grafov/m3u8"
@@ -136,7 +136,7 @@ func download(urls []string) error {
utils.Log.StopProcess("Failed to parse url %d", i+1)
if utils.Crunchy.Config.Premium {
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/crunchy-labs/crunchy-cli/issues/22 for more information")
}
return err
}
diff --git a/cli/commands/info/info.go b/cli/commands/info/info.go
index 9b816fd..db909c1 100644
--- a/cli/commands/info/info.go
+++ b/cli/commands/info/info.go
@@ -2,8 +2,8 @@ package info
import (
"fmt"
- "github.com/ByteDream/crunchy-cli/cli/commands"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/utils"
crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/spf13/cobra"
)
diff --git a/cli/commands/logger.go b/cli/commands/logger.go
index 2f3ae37..89a4717 100644
--- a/cli/commands/logger.go
+++ b/cli/commands/logger.go
@@ -2,7 +2,7 @@ package commands
import (
"fmt"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"io"
"log"
"os"
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index 7fb135c..eb1b4dc 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -3,8 +3,8 @@ package login
import (
"bytes"
"fmt"
- "github.com/ByteDream/crunchy-cli/cli/commands"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"github.com/crunchy-labs/crunchyroll-go/v3"
"github.com/spf13/cobra"
"os"
diff --git a/cli/commands/update/update.go b/cli/commands/update/update.go
index 6587ca2..831621d 100644
--- a/cli/commands/update/update.go
+++ b/cli/commands/update/update.go
@@ -3,7 +3,7 @@ package update
import (
"encoding/json"
"fmt"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"github.com/spf13/cobra"
"io"
"os"
@@ -37,7 +37,7 @@ func init() {
func update() error {
var release map[string]interface{}
- resp, err := utils.Client.Get("https://api.github.com/repos/ByteDream/crunchy-cli/releases/latest")
+ resp, err := utils.Client.Get("https://api.github.com/repos/crunchy-labs/crunchy-cli/releases/latest")
if err != nil {
return err
}
@@ -79,12 +79,12 @@ func update() error {
return nil
}
- utils.Log.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/crunchy-labs/crunchy-cli/releases/tag/v%s", releaseVersion, releaseVersion)
if updateInstallFlag {
if runtime.GOARCH != "amd64" {
return fmt.Errorf("invalid architecture found (%s), only amd64 is currently supported for automatic updating. "+
- "You have to update manually (https://github.com/ByteDream/crunchy-cli)", runtime.GOARCH)
+ "You have to update manually (https://github.com/crunchy-labs/crunchy-cli)", runtime.GOARCH)
}
var downloadFile string
@@ -102,7 +102,7 @@ func update() error {
downloadFile = fmt.Sprintf("crunchy-v%s_windows.exe", releaseVersion)
default:
return fmt.Errorf("invalid operation system found (%s), only linux, windows and darwin / macos are currently supported. "+
- "You have to update manually (https://github.com/ByteDream/crunchy-cli", runtime.GOOS)
+ "You have to update manually (https://github.com/crunchy-labs/crunchy-cli", runtime.GOOS)
}
utils.Log.SetProcess("Updating executable %s", os.Args[0])
@@ -118,7 +118,7 @@ func update() error {
}
defer executeFile.Close()
- resp, err := utils.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/crunchy-labs/crunchy-cli/releases/download/v%s/%s", releaseVersion, downloadFile))
if err != nil {
return err
}
diff --git a/cli/commands/utils.go b/cli/commands/utils.go
index 924cd4d..53e51f2 100644
--- a/cli/commands/utils.go
+++ b/cli/commands/utils.go
@@ -2,7 +2,7 @@ package commands
import (
"fmt"
- "github.com/ByteDream/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"os"
"os/exec"
"runtime"
diff --git a/cli/root.go b/cli/root.go
index ebb5079..b589a03 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -3,13 +3,13 @@ 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/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands/archive"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands/download"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands/info"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands/login"
+ "github.com/crunchy-labs/crunchy-cli/cli/commands/update"
+ "github.com/crunchy-labs/crunchy-cli/utils"
"github.com/spf13/cobra"
"os"
"runtime/debug"
@@ -28,7 +28,7 @@ var (
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",
+ Short: "Download crunchyroll videos with ease. See the wiki for details about the cli and library: https://github.com/crunchy-labs/crunchy-cli/wiki",
SilenceErrors: true,
SilenceUsage: true,
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index ebadb9c..6f20dba 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -193,12 +193,12 @@ $ crunchy-cli archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VE
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
.SH AUTHOR
-ByteDream
+Crunchy Labs Maintainers
.br
-Source: https://github.com/ByteDream/crunchy-cli
+Source: https://github.com/crunchy-labs/crunchy-cli
.SH COPYRIGHT
-Copyright (C) 2022 ByteDream
+Copyright (C) 2022 Crunchy Labs Maintainers
This program is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License
diff --git a/go.mod b/go.mod
index ce09935..37a4e42 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/ByteDream/crunchy-cli
+module github.com/crunchy-labs/crunchy-cli
go 1.18
diff --git a/main.go b/main.go
index 7275a13..72ec83b 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,7 @@
package main
import (
- "github.com/ByteDream/crunchy-cli/cli"
+ "github.com/crunchy-labs/crunchy-cli/cli"
)
func main() {
From 8942ea574bf1bf76ba59cfa3fa8e2e768625b192 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 27 Jul 2022 21:25:03 +0200
Subject: [PATCH 255/732] Add v3 notice
---
README.md | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/README.md b/README.md
index 09cd829..e8a363d 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,10 @@ A [go](https://golang.org) written cli client for [crunchyroll](https://www.crun
_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility.
See #39 for more information._
+> This tool relies on the [crunchyroll-go](https://github.com/crunchy-labs/crunchyroll-go) library to communicate with crunchyroll.
+> The library enters maintenance mode (only small fixes, no new features) with version v3 in favor of rewriting it completely in Rust.
+> **crunchy-cli** follows it (also beginning with version v3) and won't have major updates until the Rust rewrite of the library reaches a good usable state.
+
# 🖥️ CLI
## ✨ Features
From b62769ccfd90d66f4774577337e729c8bea0d940 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Wed, 27 Jul 2022 21:53:03 +0200
Subject: [PATCH 256/732] Update next version number
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index e8a363d..85e1bf5 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ See #39 for more information._
> This tool relies on the [crunchyroll-go](https://github.com/crunchy-labs/crunchyroll-go) library to communicate with crunchyroll.
> The library enters maintenance mode (only small fixes, no new features) with version v3 in favor of rewriting it completely in Rust.
-> **crunchy-cli** follows it (also beginning with version v3) and won't have major updates until the Rust rewrite of the library reaches a good usable state.
+> **crunchy-cli** follows it (with version v2.3.0) and won't have major updates until the Rust rewrite of the library reaches a good usable state.
# 🖥️ CLI
From fbb90f907953546ab5282f295a401bda9d6babc7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Fri, 29 Jul 2022 18:23:38 +0200
Subject: [PATCH 257/732] Fix info spacing
---
cli/commands/info/info.go | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/cli/commands/info/info.go b/cli/commands/info/info.go
index db909c1..650d8c7 100644
--- a/cli/commands/info/info.go
+++ b/cli/commands/info/info.go
@@ -28,13 +28,13 @@ func info() error {
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)
+ 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
}
From 81946c5092f53a37f5d4ceeca7af370bcc277fb5 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 31 Jul 2022 13:50:59 +0200
Subject: [PATCH 258/732] Remove CI badge
---
README.md | 3 ---
1 file changed, 3 deletions(-)
diff --git a/README.md b/README.md
index 85e1bf5..1147f1e 100644
--- a/README.md
+++ b/README.md
@@ -21,9 +21,6 @@ A [go](https://golang.org) written cli client for [crunchyroll](https://www.crun
-
-
-
From b5f4882601ad03c4f01e3db180b19c437cfcd3ed Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 1 Aug 2022 00:18:14 +0200
Subject: [PATCH 259/732] Update issue templates
---
.github/ISSUE_TEMPLATE/bug-report.md | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index 3cbda16..0c62d66 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -10,14 +10,8 @@ assignees: ''
**Describe the bug**
A clear and concise description of what the bug is.
-**Bug target**
-Set a `x` between the square brackets if your bug occurred while using the CLI or the library
-
-- [ ] CLI (command line client)
-- [ ] Library
-
**To Reproduce**
-Steps to reproduce the behavior:
+Steps / command to reproduce the behavior:
```
$ crunchy ...
```
From caeb734b2c03fa432e8395ff93c65d3d3f3f7a71 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 1 Aug 2022 00:41:09 +0200
Subject: [PATCH 260/732] Add login session id warning
---
cli/commands/login/login.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index eb1b4dc..20ceb6a 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -108,6 +108,7 @@ func loginCredentials(user, password string) error {
func loginSessionID(sessionID string) error {
utils.Log.Debug("Logging in via session id")
+ utils.Log.Warn("Logging in with session id is deprecated and not very reliable. Consider choosing another option (if it fails)")
var c *crunchyroll.Crunchyroll
var err error
if c, err = crunchyroll.LoginWithSessionID(sessionID, utils.SystemLocale(false), utils.Client); err != nil {
From a64981930b3e08629752490b2fc31b85e1cc2dce Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 2 Aug 2022 12:07:43 +0200
Subject: [PATCH 261/732] Add option to change temp dir
---
cli/commands/archive/archive.go | 7 +++++++
cli/commands/download/download.go | 7 +++++++
2 files changed, 14 insertions(+)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 8e09f5e..b2be970 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -29,6 +29,7 @@ var (
archiveDirectoryFlag string
archiveOutputFlag string
+ archiveTempDirFlag string
archiveMergeFlag string
@@ -146,6 +147,10 @@ func init() {
"\t{fps} » Frame Rate of the video\n"+
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
+ Cmd.Flags().StringVar(&archiveTempDirFlag,
+ "temp",
+ os.TempDir(),
+ "Directory to store temporary files in")
Cmd.Flags().StringVarP(&archiveMergeFlag,
"merge",
@@ -315,6 +320,8 @@ func archiveInfo(info utils.FormatInformation, writeCloser io.WriteCloser, filen
}
return nil
})
+ tmp, _ := os.MkdirTemp(archiveTempDirFlag, "crunchy_")
+ downloader.TempDir = tmp
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index 2976e2d..60e975e 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -25,6 +25,7 @@ var (
downloadDirectoryFlag string
downloadOutputFlag string
+ downloadTempDirFlag string
downloadResolutionFlag string
@@ -111,6 +112,10 @@ func init() {
"\t{fps} » Frame Rate of the video\n"+
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
+ Cmd.Flags().StringVar(&downloadTempDirFlag,
+ "temp",
+ os.TempDir(),
+ "Directory to store temporary files in")
Cmd.Flags().StringVarP(&downloadResolutionFlag,
"resolution",
@@ -230,6 +235,8 @@ func downloadInfo(info utils.FormatInformation, file *os.File) error {
}
return nil
})
+ tmp, _ := os.MkdirTemp(downloadTempDirFlag, "crunchy_")
+ downloader.TempDir = tmp
if utils.HasFFmpeg() {
downloader.FFmpegOpts = make([]string, 0)
}
From 6239d10d220b947cd0c0370996832b522f4d0ca0 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 8 Aug 2022 22:20:25 +0200
Subject: [PATCH 262/732] Fix crunchyroll api changes
---
cli/commands/archive/archive.go | 6 ++++
cli/commands/download/download.go | 52 +++----------------------------
utils/extract.go | 19 ++++++-----
3 files changed, 22 insertions(+), 55 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index b2be970..a15822c 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -627,6 +627,12 @@ func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
}
}
+ if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
+ return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
+ } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
+ }
+
episodes, err := utils.ExtractEpisodes(url, languagesAsLocale...)
if err != nil {
return nil, err
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index 60e975e..b7f3153 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -14,13 +14,11 @@ import (
"os/signal"
"path/filepath"
"runtime"
- "sort"
"strconv"
"strings"
)
var (
- downloadAudioFlag string
downloadSubtitleFlag string
downloadDirectoryFlag string
@@ -49,12 +47,10 @@ var Cmd = &cobra.Command{
}
}
- if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
- return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(utils.LocalesAsStrings(), ", "))
- } else if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
+ if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(utils.LocalesAsStrings(), ", "))
}
- utils.Log.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
+ utils.Log.Debug("Subtitle locale: %s", downloadSubtitleFlag)
switch downloadResolutionFlag {
case "1080p", "720p", "480p", "360p":
@@ -81,10 +77,6 @@ var Cmd = &cobra.Command{
}
func init() {
- Cmd.Flags().StringVarP(&downloadAudioFlag, "audio",
- "a",
- string(utils.SystemLocale(false)),
- "The locale of the audio. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
Cmd.Flags().StringVarP(&downloadSubtitleFlag,
"subtitle",
"s",
@@ -280,49 +272,13 @@ func downloadInfo(info utils.FormatInformation, file *os.File) error {
}
func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
- episodes, err := utils.ExtractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
+ episodes, err := utils.ExtractEpisodes(url)
if err != nil {
return nil, err
}
- japanese := episodes[0]
- custom := episodes[1]
-
- sort.Sort(crunchyUtils.EpisodesByNumber(japanese))
- sort.Sort(crunchyUtils.EpisodesByNumber(custom))
-
- var errMessages []string
-
- var final []*crunchyroll.Episode
- if len(japanese) == 0 || len(japanese) == len(custom) {
- final = custom
- } else {
- for _, jp := range japanese {
- before := len(final)
- for _, episode := range custom {
- if jp.SeasonNumber == episode.SeasonNumber && jp.EpisodeNumber == episode.EpisodeNumber {
- final = append(final, episode)
- }
- }
- if before == len(final) {
- errMessages = append(errMessages, fmt.Sprintf("%s has no %s audio, using %s as fallback", jp.Title, crunchyroll.LOCALE(downloadAudioFlag), crunchyroll.JP))
- final = append(final, jp)
- }
- }
- }
-
- if len(errMessages) > 10 {
- for _, msg := range errMessages[:10] {
- utils.Log.SetProcess(msg)
- }
- utils.Log.SetProcess("... and %d more", len(errMessages)-10)
- } else {
- for _, msg := range errMessages {
- utils.Log.SetProcess(msg)
- }
- }
var infoFormat [][]utils.FormatInformation
- for _, season := range crunchyUtils.SortEpisodesBySeason(final) {
+ for _, season := range crunchyUtils.SortEpisodesBySeason(episodes[0]) {
tmpFormatInformation := make([]utils.FormatInformation, 0)
for _, episode := range season {
format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
diff --git a/utils/extract.go b/utils/extract.go
index 60ced1d..fa2d650 100644
--- a/utils/extract.go
+++ b/utils/extract.go
@@ -28,7 +28,6 @@ func ExtractEpisodes(url string, locales ...crunchyroll.LOCALE) ([][]*crunchyrol
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)
@@ -82,12 +81,18 @@ func ExtractEpisodes(url string, locales ...crunchyroll.LOCALE) ([][]*crunchyrol
}
}
- 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]...)
+ var final [][]*crunchyroll.Episode
+ if len(locales) > 0 {
+ final = make([][]*crunchyroll.Episode, len(locales))
+ 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]...)
+ }
+ } else {
+ final = [][]*crunchyroll.Episode{episodes}
}
return final, nil
From f7a21fbfb2806779b975779afb70724333e357cc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 9 Aug 2022 01:04:46 +0200
Subject: [PATCH 263/732] Change all etp rt related stuff to refresh token
---
cli/commands/login/login.go | 22 +++++++++++-----------
go.mod | 2 +-
go.sum | 4 ++--
utils/save.go | 18 +++++++++---------
4 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index 20ceb6a..9fce181 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -14,8 +14,8 @@ var (
loginPersistentFlag bool
loginEncryptFlag bool
- loginSessionIDFlag bool
- loginEtpRtFlag bool
+ loginSessionIDFlag bool
+ loginRefreshTokenFlag bool
)
var Cmd = &cobra.Command{
@@ -26,8 +26,8 @@ var Cmd = &cobra.Command{
RunE: func(cmd *cobra.Command, args []string) error {
if loginSessionIDFlag {
return loginSessionID(args[0])
- } else if loginEtpRtFlag {
- return loginEtpRt(args[0])
+ } else if loginRefreshTokenFlag {
+ return loginRefreshToken(args[0])
} else {
return loginCredentials(args[0], args[1])
}
@@ -48,12 +48,12 @@ func init() {
"session-id",
false,
"Use a session id to login instead of username and password")
- Cmd.Flags().BoolVar(&loginEtpRtFlag,
- "etp-rt",
+ Cmd.Flags().BoolVar(&loginRefreshTokenFlag,
+ "refresh-token",
false,
- "Use a etp rt cookie to login instead of username and password")
+ "Use a refresh token to login instead of username and password. Can be obtained by copying the `etp-rt` cookie from beta.crunchyroll.com")
- Cmd.MarkFlagsMutuallyExclusive("session-id", "etp-rt")
+ Cmd.MarkFlagsMutuallyExclusive("session-id", "refresh-token")
}
func loginCredentials(user, password string) error {
@@ -132,11 +132,11 @@ func loginSessionID(sessionID string) error {
return nil
}
-func loginEtpRt(etpRt string) error {
- utils.Log.Debug("Logging in via etp rt")
+func loginRefreshToken(refreshToken string) error {
+ utils.Log.Debug("Logging in via refresh token")
var c *crunchyroll.Crunchyroll
var err error
- if c, err = crunchyroll.LoginWithEtpRt(etpRt, utils.SystemLocale(false), utils.Client); err != nil {
+ if c, err = crunchyroll.LoginWithRefreshToken(refreshToken, utils.SystemLocale(false), utils.Client); err != nil {
utils.Log.Err(err.Error())
os.Exit(1)
}
diff --git a/go.mod b/go.mod
index 37a4e42..d7f0eba 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.18
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 34683e1..225fce4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd h1:wxMjLwhvilxwTnYCOXmBPenMNymhC63m4aFzQ5C7ipQ=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220724174415-375eaf9007bd/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a h1:q5M2xmCTu2njig5rlRAd83LJDaPANmweFJjYsnxi5zM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
diff --git a/utils/save.go b/utils/save.go
index ef6d0ec..7660974 100644
--- a/utils/save.go
+++ b/utils/save.go
@@ -15,7 +15,7 @@ import (
func SaveSession(crunchy *crunchyroll.Crunchyroll) error {
file := filepath.Join(os.TempDir(), ".crunchy")
- return os.WriteFile(file, []byte(crunchy.EtpRt), 0600)
+ return os.WriteFile(file, []byte(crunchy.RefreshToken), 0600)
}
func SaveCredentialsPersistent(user, password string, encryptionKey []byte) error {
@@ -62,7 +62,7 @@ func SaveSessionPersistent(crunchy *crunchyroll.Crunchyroll) error {
if err = os.MkdirAll(filepath.Join(configDir, "crunchy-cli"), 0755); err != nil {
return err
}
- return os.WriteFile(file, []byte(crunchy.EtpRt), 0600)
+ return os.WriteFile(file, []byte(crunchy.RefreshToken), 0600)
}
func IsTempSession() bool {
@@ -118,11 +118,11 @@ func loadTempSession(file string) (*crunchyroll.Crunchyroll, error) {
if err != nil {
return nil, err
}
- crunchy, err := crunchyroll.LoginWithEtpRt(string(body), SystemLocale(true), Client)
+ crunchy, err := crunchyroll.LoginWithRefreshToken(string(body), SystemLocale(true), Client)
if err != nil {
- Log.Debug("Failed to login with temp etp rt cookie: %v", err)
+ Log.Debug("Failed to login with temp refresh token: %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)
+ Log.Debug("Logged in with refresh token %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", body)
return crunchy, nil
}
}
@@ -160,15 +160,15 @@ func loadPersistentSession(file string, encryptionKey []byte) (crunchy *crunchyr
}
Log.Debug("Logged in with credentials")
} else {
- if crunchy, err = crunchyroll.LoginWithEtpRt(split[0], SystemLocale(true), Client); err != nil {
+ if crunchy, err = crunchyroll.LoginWithRefreshToken(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])
+ Log.Debug("Logged in with refresh token %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.
+ // the refresh token 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 {
+ if err = os.WriteFile(filepath.Join(os.TempDir(), ".crunchy"), []byte(crunchy.RefreshToken), 0600); err != nil {
return nil, err
}
}
From 441ec084af98ee21f4ed447411ca045c0b64dc8b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Tue, 9 Aug 2022 01:20:34 +0200
Subject: [PATCH 264/732] Re-enable language choosing for series
---
cli/commands/download/download.go | 76 ++++++++++++++++++++++++++++---
1 file changed, 70 insertions(+), 6 deletions(-)
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index b7f3153..39273d0 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -14,11 +14,13 @@ import (
"os/signal"
"path/filepath"
"runtime"
+ "sort"
"strconv"
"strings"
)
var (
+ downloadAudioFlag string
downloadSubtitleFlag string
downloadDirectoryFlag string
@@ -47,10 +49,12 @@ var Cmd = &cobra.Command{
}
}
- if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
+ if downloadAudioFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
+ return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(utils.LocalesAsStrings(), ", "))
+ } else if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(utils.LocalesAsStrings(), ", "))
}
- utils.Log.Debug("Subtitle locale: %s", downloadSubtitleFlag)
+ utils.Log.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
switch downloadResolutionFlag {
case "1080p", "720p", "480p", "360p":
@@ -77,6 +81,10 @@ var Cmd = &cobra.Command{
}
func init() {
+ Cmd.Flags().StringVarP(&downloadAudioFlag, "audio",
+ "a",
+ "",
+ "The locale of the audio. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
Cmd.Flags().StringVarP(&downloadSubtitleFlag,
"subtitle",
"s",
@@ -272,13 +280,69 @@ func downloadInfo(info utils.FormatInformation, file *os.File) error {
}
func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
- episodes, err := utils.ExtractEpisodes(url)
- if err != nil {
- return nil, err
+ var episodes [][]*crunchyroll.Episode
+ var final []*crunchyroll.Episode
+
+ if downloadAudioFlag != "" {
+ if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
+ return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
+ } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
+ }
+
+ var err error
+ episodes, err = utils.ExtractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
+ if err != nil {
+ return nil, err
+ }
+ japanese := episodes[0]
+ custom := episodes[1]
+
+ sort.Sort(crunchyUtils.EpisodesByNumber(japanese))
+ sort.Sort(crunchyUtils.EpisodesByNumber(custom))
+
+ var errMessages []string
+
+ if len(japanese) == 0 || len(japanese) == len(custom) {
+ final = custom
+ } else {
+ for _, jp := range japanese {
+ before := len(final)
+ for _, episode := range custom {
+ if jp.SeasonNumber == episode.SeasonNumber && jp.EpisodeNumber == episode.EpisodeNumber {
+ final = append(final, episode)
+ }
+ }
+ if before == len(final) {
+ errMessages = append(errMessages, fmt.Sprintf("%s has no %s audio, using %s as fallback", jp.Title, crunchyroll.LOCALE(downloadAudioFlag), crunchyroll.JP))
+ final = append(final, jp)
+ }
+ }
+ }
+
+ if len(errMessages) > 10 {
+ for _, msg := range errMessages[:10] {
+ utils.Log.SetProcess(msg)
+ }
+ utils.Log.SetProcess("... and %d more", len(errMessages)-10)
+ } else {
+ for _, msg := range errMessages {
+ utils.Log.SetProcess(msg)
+ }
+ }
+ } else {
+ var err error
+ episodes, err = utils.ExtractEpisodes(url)
+ if err != nil {
+ return nil, err
+ } else if len(episodes) == 0 {
+ return nil, fmt.Errorf("cannot find any episode")
+ }
+ final = episodes[0]
}
var infoFormat [][]utils.FormatInformation
- for _, season := range crunchyUtils.SortEpisodesBySeason(episodes[0]) {
+ for _, season := range crunchyUtils.SortEpisodesBySeason(final) {
tmpFormatInformation := make([]utils.FormatInformation, 0)
for _, episode := range season {
format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
From ac876f674a77a030c82db589452977336721ed21 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 14 Aug 2022 13:38:42 +0200
Subject: [PATCH 265/732] Update to newer crunchyroll-go version
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index d7f0eba..cb59580 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.18
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 225fce4..d134d52 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a h1:q5M2xmCTu2njig5rlRAd83LJDaPANmweFJjYsnxi5zM=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220808230013-bb434a0fba7a/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60 h1:cvEKs8D8816yWJDXYl8V7bYLYsAcbNbGGcUZDUofwTI=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
From 4ae4345c4017e9f3287aa6143f857645f1493047 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 14 Aug 2022 13:46:39 +0200
Subject: [PATCH 266/732] Fix smartrelease host url
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 6374ae0..bb218a0 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,9 @@ See [#39](https://github.com/crunchy-labs/crunchy-cli/issues/39) for more inform
## 💾 Get the executable
- 📥 Download the latest binaries [here](https://github.com/crunchy-labs/crunchy-cli/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchyroll-go
From 416507c8a6406be5069df6975cf676ccd9318703 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 15 Aug 2022 00:27:34 +0200
Subject: [PATCH 267/732] Remove not working notice for scoop
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index bb218a0..ce53d41 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,6 @@ See [#39](https://github.com/crunchy-labs/crunchy-cli/issues/39) for more inform
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
- Currently not working because the repo got renamed!
- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
```shell
From d13e5714f8aed8feb6b880efca8287aaf7cecb9d Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 21 Aug 2022 19:40:05 +0200
Subject: [PATCH 268/732] Fix README details
---
README.md | 29 ++---------------------------
1 file changed, 2 insertions(+), 27 deletions(-)
diff --git a/README.md b/README.md
index 1147f1e..dfb3fa3 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# crunchy-cli
-A [go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account to for full (api) access.
+A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account for full access & features.
@@ -56,16 +56,11 @@ See #39 for more information._
```shell
$ yay -S crunchyroll-go
```
--
-
- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
+- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
$ scoop bucket add extras # <- in case you haven't added the extra repository already
$ scoop install crunchyroll-go
```
-
-
- Currently not working because the repo got renamed!
- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- use `make` (requires `go` to be installed):
```shell
@@ -175,26 +170,6 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
-### Help
-
-- General help
- ```shell
- $ crunchy help
- ```
-- Login help
- ```shell
- $ crunchy help login
- ```
-- Download help
- ```shell
- $ crunchy help download
- ```
-
-- Archive help
- ```shell
- $ crunchy help archive
- ```
-
### Global flags
These flags you can use across every sub-command:
From e5636df96931c2826921a6b32db15dbfcdd56a05 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 21 Aug 2022 22:07:46 +0200
Subject: [PATCH 269/732] Update crunchyroll-go version
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index cb59580..eae1163 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.18
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index d134d52..4f103cb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60 h1:cvEKs8D8816yWJDXYl8V7bYLYsAcbNbGGcUZDUofwTI=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.0-20220812161741-903599bcbe60/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1 h1:mWCNqQJb9Vf8pkM2YYgbLYTn49//7VaCMSvFK3G7l7Q=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
From f974d5296b12036f50d3a9a01b05db7708ecdb24 Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Mon, 22 Aug 2022 12:41:47 +0200
Subject: [PATCH 270/732] Remove rename issue link
---
README.md | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/README.md b/README.md
index dfb3fa3..16f9559 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,7 @@ A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crun
License ⚖
-_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility.
-See #39 for more information._
+_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility._
> This tool relies on the [crunchyroll-go](https://github.com/crunchy-labs/crunchyroll-go) library to communicate with crunchyroll.
> The library enters maintenance mode (only small fixes, no new features) with version v3 in favor of rewriting it completely in Rust.
From 0371b31dcc894d496368567fb8b8dd06288844e6 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 22 Aug 2022 13:06:05 +0200
Subject: [PATCH 271/732] Add info and update command examples
---
README.md | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 16f9559..9c22f1b 100644
--- a/README.md
+++ b/README.md
@@ -89,10 +89,10 @@ This can be performed via crunchyroll account email and password.
$ crunchy login user@example.com password
```
-or via session id
+or via refresh token / `etp_rt` cookie
```shell
-$ crunchy login --session-id 8e9gs135defhga790dvrf2i0eris8gts
+$ crunchy login --refresh-token 7578ce50-5712-3gef-b97e-01332d6b588c
```
### Download
@@ -169,6 +169,24 @@ The following flags can be (optional) passed to modify the [archive](#archive) p
| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
+
+### Info
+
+The `info` displays some information about the account which is used for the cli.
+
+```shell
+$ crunchy info
+```
+
+### Update
+
+If you want to update your local version of `crunchy-cli`, this command makes this easier.
+It checks if a new version is available and if so, updates itself.
+
+```shell
+$ crunchy update
+```
+
### Global flags
These flags you can use across every sub-command:
From afc85350ab60800ad1206542ce0deb9bb47ea47a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 22 Aug 2022 13:31:33 +0200
Subject: [PATCH 272/732] Update arch install package name
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 9c22f1b..3c7a7a0 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ _This repo was former known as **crunchyroll-go** (which still exists but now co
- [MacOS (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
- $ yay -S crunchyroll-go
+ $ yay -S crunchy-cli
```
- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
```shell
From 3f78101eb8ae190af1af7dadeb07ace490fbc598 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 3 Sep 2022 15:18:52 +0200
Subject: [PATCH 273/732] Fix unwanted video track and wrong labeled audio
track in archive (#45)
---
cli/commands/archive/archive.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index a15822c..c0a2486 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -548,10 +548,10 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
for i, audio := range audioFiles {
input = append(input, "-i", audio)
- maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)))
+ maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles))+":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("title=%s", crunchyUtils.LocaleLanguage(locale)))
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i+len(videoFiles)), fmt.Sprintf("language=%s", locale))
+ metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i+len(videoFiles)), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
}
for i, subtitle := range subtitleFiles {
From c02306ff9f2f19977bcd32ab179de070e7379c5c Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 3 Sep 2022 15:47:25 +0200
Subject: [PATCH 274/732] Bump crunchyroll-go version
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index eae1163..7152a6a 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.18
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 4f103cb..51fe7a6 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1 h1:mWCNqQJb9Vf8pkM2YYgbLYTn49//7VaCMSvFK3G7l7Q=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.1/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2 h1:PG5++Gje126/xRtzZwCowoFU1Dl3qKzFjd3lWhVXoso=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
From dc2309ab10868b7c62fbff913ad6e3a362107fec Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 3 Sep 2022 15:47:41 +0200
Subject: [PATCH 275/732] Bump locale names in docs
---
crunchy-cli.1 | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index 6f20dba..07d3422 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -65,11 +65,11 @@ A command to simply download videos. The output file is stored as a \fI.ts\fR fi
.TP
\fB-a, --audio AUDIO\fR
-Forces to download videos with the given audio locale. If no video with this audio locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
+Forces to download videos with the given audio locale. If no video with this audio locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
.TP
\fB-s, --subtitle SUBTITLE\fR
-Forces to download the videos with subtitles in the given locale / language. If no video with this subtitle locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
+Forces to download the videos with subtitles in the given locale / language. If no video with this subtitle locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
.TP
\fB-d, --directory DIRECTORY\fR
@@ -104,7 +104,7 @@ This command behaves like \fBdownload\fR besides the fact that it requires \fIff
.TP
\fB-l, --language LANGUAGE\fR
-Audio locales which should be downloaded. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA.
+Audio locales which should be downloaded. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
.TP
\fB-d, --directory DIRECTORY\fR
From d53b20717ad97e203f7f77bc237e2b9a419515f7 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sat, 3 Sep 2022 18:22:34 +0200
Subject: [PATCH 276/732] Fix video length sometimes exceeds actual episode
length (#32)
---
cli/commands/archive/archive.go | 75 +++++++++++++++++++++++++++++++++
1 file changed, 75 insertions(+)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index c0a2486..ce53b85 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -535,6 +535,10 @@ func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitl
func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, subtitleFiles []string) error {
var input, maps, metadata []string
re := regexp.MustCompile(`(?m)_([a-z]{2}-([A-Z]{2}|[0-9]{3}))_(video|audio|subtitle)`)
+ // https://github.com/crunchy-labs/crunchy-cli/issues/32
+ videoLength32Fix := regexp.MustCompile(`Duration:\s?(\d+):(\d+):(\d+).(\d+),`)
+
+ videoLength := [4]int{0, 0, 0, 0}
for i, video := range videoFiles {
input = append(input, "-i", video)
@@ -544,6 +548,27 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
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("title=%s", crunchyUtils.LocaleLanguage(locale)))
+
+ var errBuf bytes.Buffer
+ cmd := exec.CommandContext(ctx, "ffmpeg", "-i", video)
+ cmd.Stderr = &errBuf
+ cmd.Run()
+
+ matches := videoLength32Fix.FindStringSubmatch(errBuf.String())
+ hours, _ := strconv.Atoi(matches[1])
+ minutes, _ := strconv.Atoi(matches[2])
+ seconds, _ := strconv.Atoi(matches[3])
+ millis, _ := strconv.Atoi(matches[4])
+
+ if hours > videoLength[0] {
+ videoLength = [4]int{hours, minutes, seconds, millis}
+ } else if hours == videoLength[0] && minutes > videoLength[1] {
+ videoLength = [4]int{hours, minutes, seconds, millis}
+ } else if hours == videoLength[0] && minutes == videoLength[1] && seconds > videoLength[2] {
+ videoLength = [4]int{hours, minutes, seconds, millis}
+ } else if hours == videoLength[0] && minutes == videoLength[1] && seconds == videoLength[2] && millis > videoLength[3] {
+ videoLength = [4]int{hours, minutes, seconds, millis}
+ }
}
for i, audio := range audioFiles {
@@ -611,6 +636,56 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
}
defer file.Close()
+ errBuf.Reset()
+ cmd = exec.CommandContext(ctx, "ffmpeg", "-i", file.Name())
+ cmd.Stderr = &errBuf
+ cmd.Run()
+
+ matches := videoLength32Fix.FindStringSubmatch(errBuf.String())
+ hours, _ := strconv.Atoi(matches[1])
+ minutes, _ := strconv.Atoi(matches[2])
+ seconds, _ := strconv.Atoi(matches[3])
+ millis, _ := strconv.Atoi(matches[4])
+
+ var reencode bool
+ if hours > videoLength[0] {
+ reencode = true
+ } else if hours == videoLength[0] && minutes > videoLength[1] {
+ reencode = true
+ } else if hours == videoLength[0] && minutes == videoLength[1] && seconds > videoLength[2] {
+ reencode = true
+ } else if hours == videoLength[0] && minutes == videoLength[1] && seconds == videoLength[2] && millis > videoLength[3] {
+ reencode = true
+ }
+
+ // very dirty solution to https://github.com/crunchy-labs/crunchy-cli/issues/32.
+ // this might get triggered when not needed but there is currently no easy way to
+ // bypass this unwanted triggering
+ if reencode {
+ utils.Log.Debug("Reencode to short video length")
+
+ file.Close()
+
+ tmpFile, _ := os.CreateTemp("", filepath.Base(file.Name())+"-32_fix")
+ tmpFile.Close()
+
+ errBuf.Reset()
+ cmd = exec.CommandContext(ctx, "ffmpeg", "-y", "-i", file.Name(), "-c", "copy", "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]), "-f", "matroska", tmpFile.Name())
+ cmd.Stderr = &errBuf
+ if err = cmd.Run(); err != nil {
+ return fmt.Errorf(errBuf.String())
+ }
+
+ os.Remove(file.Name())
+ os.Rename(tmpFile.Name(), file.Name())
+
+ file, err = os.Open(file.Name())
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+ }
+
_, err = bufio.NewWriter(dst).ReadFrom(file)
return err
}
From 610c4e29935fcb9213c025afe5a1914329d4a6d2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 4 Sep 2022 17:12:36 +0200
Subject: [PATCH 277/732] Bump go version to 1.19
---
go.mod | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/go.mod b/go.mod
index 7152a6a..162e7a1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/crunchy-labs/crunchy-cli
-go 1.18
+go 1.19
require (
github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2
From 689bbcd9a4a3ba515edd45faf6af1d8c6de4ed27 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 4 Sep 2022 17:12:57 +0200
Subject: [PATCH 278/732] Remove go version badge
---
README.md | 3 ---
1 file changed, 3 deletions(-)
diff --git a/README.md b/README.md
index 3c7a7a0..19dba21 100644
--- a/README.md
+++ b/README.md
@@ -12,9 +12,6 @@ A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crun
-
-
-
From 027047fc7e22f5a9d439fd6fa4467ad00b3f3a3c Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sun, 4 Sep 2022 20:33:52 +0200
Subject: [PATCH 279/732] Fix binary links
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 19dba21..cbe6a37 100644
--- a/README.md
+++ b/README.md
@@ -45,9 +45,9 @@ _This repo was former known as **crunchyroll-go** (which still exists but now co
## 💾 Get the executable
- 📥 Download the latest binaries [here](https://github.com/crunchy-labs/crunchy-cli/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.crunchy-labs.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
+ - [Linux (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
+ - [Windows (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
+ - [MacOS (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
```shell
$ yay -S crunchy-cli
From 36c1423ff659f8369b227523d99b0d890520358b Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 5 Sep 2022 00:20:29 +0200
Subject: [PATCH 280/732] Fix re-encode removes video, audio and subtitle
tracks (#47)
---
cli/commands/archive/archive.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index ce53b85..63eac77 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -662,7 +662,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
// this might get triggered when not needed but there is currently no easy way to
// bypass this unwanted triggering
if reencode {
- utils.Log.Debug("Reencode to short video length")
+ utils.Log.Debug("Re-encode to short video length")
file.Close()
@@ -670,7 +670,7 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
tmpFile.Close()
errBuf.Reset()
- cmd = exec.CommandContext(ctx, "ffmpeg", "-y", "-i", file.Name(), "-c", "copy", "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]), "-f", "matroska", tmpFile.Name())
+ cmd = exec.CommandContext(ctx, "ffmpeg", "-y", "-i", file.Name(), "-c", "copy", "-map", "0", "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]), "-f", "matroska", tmpFile.Name())
cmd.Stderr = &errBuf
if err = cmd.Run(); err != nil {
return fmt.Errorf(errBuf.String())
From 62938a500ff8ef70889451c9ec7534a3f18fb2a8 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 5 Sep 2022 00:24:31 +0200
Subject: [PATCH 281/732] Disable subtitles by default on re-encode
---
cli/commands/archive/archive.go | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 63eac77..841b33d 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -670,7 +670,15 @@ func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, s
tmpFile.Close()
errBuf.Reset()
- cmd = exec.CommandContext(ctx, "ffmpeg", "-y", "-i", file.Name(), "-c", "copy", "-map", "0", "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]), "-f", "matroska", tmpFile.Name())
+ cmd = exec.CommandContext(ctx, "ffmpeg",
+ "-y",
+ "-i", file.Name(),
+ "-map", "0",
+ "-c", "copy",
+ "-disposition:s:0", "0",
+ "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]),
+ "-f", "matroska",
+ tmpFile.Name())
cmd.Stderr = &errBuf
if err = cmd.Run(); err != nil {
return fmt.Errorf(errBuf.String())
From 97dd8011379717402ecce1b1041a60a2c13cd893 Mon Sep 17 00:00:00 2001
From: LordBex
Date: Mon, 5 Sep 2022 15:40:49 +0200
Subject: [PATCH 282/732] adding subtitle flag for archive (-s, --sublang)
---
cli/commands/archive/archive.go | 52 ++++++++++++++++++++++++++++-----
1 file changed, 45 insertions(+), 7 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index ce53b85..385a13d 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -5,12 +5,6 @@ import (
"bytes"
"context"
"fmt"
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
- "github.com/grafov/m3u8"
- "github.com/spf13/cobra"
"io"
"math"
"os"
@@ -22,10 +16,18 @@ import (
"sort"
"strconv"
"strings"
+
+ "github.com/crunchy-labs/crunchy-cli/cli/commands"
+ "github.com/crunchy-labs/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
+ "github.com/grafov/m3u8"
+ "github.com/spf13/cobra"
)
var (
- archiveLanguagesFlag []string
+ archiveLanguagesFlag []string
+ archiveSubLanguagesFlag []string
archiveDirectoryFlag string
archiveOutputFlag string
@@ -69,6 +71,18 @@ var Cmd = &cobra.Command{
}
utils.Log.Debug("Using following audio locales: %s", strings.Join(archiveLanguagesFlag, ", "))
+ for _, locale := range archiveSubLanguagesFlag {
+ if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(locale)) {
+ // if locale is 'all', match all known locales
+ if locale == "all" {
+ archiveSubLanguagesFlag = utils.LocalesAsStrings()
+ break
+ }
+ return fmt.Errorf("%s is not a valid locale for Subtitels. Choose from: %s", locale, strings.Join(utils.LocalesAsStrings(), ", "))
+ }
+ }
+ utils.Log.Debug("Using following subtitels locales: %s", strings.Join(archiveSubLanguagesFlag, ", "))
+
var found bool
for _, mode := range []string{"auto", "audio", "video"} {
if archiveMergeFlag == mode {
@@ -127,12 +141,20 @@ func init() {
[]string{string(utils.SystemLocale(false)), string(crunchyroll.JP)},
"Audio locale which should be downloaded. Can be used multiple times")
+ Cmd.Flags().StringSliceVarP(&archiveSubLanguagesFlag,
+ "sublang",
+ "s",
+ []string{},
+ "Subtitles langs which should be downloaded. Can be used multiple times")
+
cwd, _ := os.Getwd()
+
Cmd.Flags().StringVarP(&archiveDirectoryFlag,
"directory",
"d",
cwd,
"The directory to store the files into")
+
Cmd.Flags().StringVarP(&archiveOutputFlag,
"output",
"o",
@@ -147,6 +169,7 @@ func init() {
"\t{fps} » Frame Rate of the video\n"+
"\t{audio} » Audio locale of the video\n"+
"\t{subtitle} » Subtitle locale of the video")
+
Cmd.Flags().StringVar(&archiveTempDirFlag,
"temp",
os.TempDir(),
@@ -507,10 +530,25 @@ func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, v
return files, nil
}
+func stringInSlice(a string, list []string) bool {
+ for _, b := range list {
+ if b == a {
+ return true
+ }
+ }
+ return false
+}
+
func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitle) ([]string, error) {
var files []string
for _, subtitle := range subtitles {
+ if len(archiveSubLanguagesFlag) > 0 {
+ if !stringInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
+ continue
+ }
+ }
+
f, err := os.CreateTemp("", fmt.Sprintf("%s_%s_subtitle_*.ass", filename, subtitle.Locale))
if err != nil {
return nil, err
From 136d970feca4eb78adbf94410fd806c3cd343f55 Mon Sep 17 00:00:00 2001
From: LordBex
Date: Mon, 5 Sep 2022 21:30:41 +0200
Subject: [PATCH 283/732] move stringInSlice to separate as ElementInSlice
---
utils/std.go | 10 ++++++++++
1 file changed, 10 insertions(+)
create mode 100644 utils/std.go
diff --git a/utils/std.go b/utils/std.go
new file mode 100644
index 0000000..75b35f1
--- /dev/null
+++ b/utils/std.go
@@ -0,0 +1,10 @@
+package utils
+
+func ElementInSlice[T comparable](check T, toCheck []T) bool {
+ for _, item := range toCheck {
+ if check == item {
+ return true
+ }
+ }
+ return false
+}
From b42c87c9f8dcae7cf17c3c05c8c7baa3dcdd6497 Mon Sep 17 00:00:00 2001
From: LordBex
Date: Mon, 5 Sep 2022 21:59:00 +0200
Subject: [PATCH 284/732] remove stringInSlice and switch to ElementInSlice
---
cli/commands/archive/archive.go | 11 +----------
1 file changed, 1 insertion(+), 10 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 8ddc423..6a7cc8c 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -530,21 +530,12 @@ func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, v
return files, nil
}
-func stringInSlice(a string, list []string) bool {
- for _, b := range list {
- if b == a {
- return true
- }
- }
- return false
-}
-
func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitle) ([]string, error) {
var files []string
for _, subtitle := range subtitles {
if len(archiveSubLanguagesFlag) > 0 {
- if !stringInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
+ if !utils.ElementInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
continue
}
}
From 3f12cbae9599f6dc16dff01ce0ba69ae66d4aa64 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 5 Sep 2022 22:54:52 +0200
Subject: [PATCH 285/732] Change -s default value
---
cli/commands/archive/archive.go | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 6a7cc8c..744635f 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -144,7 +144,7 @@ func init() {
Cmd.Flags().StringSliceVarP(&archiveSubLanguagesFlag,
"sublang",
"s",
- []string{},
+ utils.LocalesAsStrings(),
"Subtitles langs which should be downloaded. Can be used multiple times")
cwd, _ := os.Getwd()
@@ -534,10 +534,8 @@ func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitl
var files []string
for _, subtitle := range subtitles {
- if len(archiveSubLanguagesFlag) > 0 {
- if !utils.ElementInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
- continue
- }
+ if !utils.ElementInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
+ continue
}
f, err := os.CreateTemp("", fmt.Sprintf("%s_%s_subtitle_*.ass", filename, subtitle.Locale))
From 2f08aeac1abaddbe0a4a459424360ce96d83076e Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 5 Sep 2022 22:55:27 +0200
Subject: [PATCH 286/732] Add -s flag documentation
---
crunchy-cli.1 | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index 07d3422..91ba30c 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -107,6 +107,10 @@ This command behaves like \fBdownload\fR besides the fact that it requires \fIff
Audio locales which should be downloaded. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
.TP
+\fB-s, --sublang LANGUAGE\fR
+Subtitle languages to use, by default all are included. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
+.TP
+
\fB-d, --directory DIRECTORY\fR
The directory to download all files to.
.TP
From d4bef511cbc32f15b99777511ef5889f6be6af85 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 11 Sep 2022 13:02:08 +0200
Subject: [PATCH 287/732] Fix update executable path
---
cli/commands/update/update.go | 32 ++++++++++++++++++++++++--------
1 file changed, 24 insertions(+), 8 deletions(-)
diff --git a/cli/commands/update/update.go b/cli/commands/update/update.go
index 831621d..6608f26 100644
--- a/cli/commands/update/update.go
+++ b/cli/commands/update/update.go
@@ -8,6 +8,7 @@ import (
"io"
"os"
"os/exec"
+ "path"
"runtime"
"strings"
)
@@ -90,8 +91,8 @@ func update() error {
var downloadFile string
switch runtime.GOOS {
case "linux":
- yayCommand := exec.Command("pacman -Q crunchy-cli")
- if yayCommand.Run() == nil && yayCommand.ProcessState.Success() {
+ pacmanCommand := exec.Command("pacman -Q crunchy-cli")
+ if pacmanCommand.Run() == nil && pacmanCommand.ProcessState.Success() {
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
}
@@ -105,14 +106,29 @@ func update() error {
"You have to update manually (https://github.com/crunchy-labs/crunchy-cli", runtime.GOOS)
}
- utils.Log.SetProcess("Updating executable %s", os.Args[0])
+ executePath := os.Args[0]
+ var perms os.FileInfo
+ // check if the path is relative, absolute or non (if so, the executable must be in PATH)
+ if strings.HasPrefix(executePath, "."+string(os.PathSeparator)) || path.IsAbs(executePath) {
+ if perms, err = os.Stat(os.Args[0]); err != nil {
+ return err
+ }
+ } else {
+ executePath, err = exec.LookPath(os.Args[0])
+ if err != nil {
+ return err
+ }
+ if perms, err = os.Stat(executePath); err != nil {
+ return err
+ }
+ }
- perms, err := os.Stat(os.Args[0])
- if err != nil {
+ utils.Log.SetProcess("Updating executable %s", executePath)
+
+ if err = os.Remove(executePath); err != nil {
return err
}
- os.Remove(os.Args[0])
- executeFile, err := os.OpenFile(os.Args[0], os.O_CREATE|os.O_WRONLY, perms.Mode())
+ executeFile, err := os.OpenFile(executePath, os.O_CREATE|os.O_WRONLY, perms.Mode())
if err != nil {
return err
}
@@ -128,7 +144,7 @@ func update() error {
return err
}
- utils.Log.StopProcess("Updated executable %s", os.Args[0])
+ utils.Log.StopProcess("Updated executable %s", executePath)
}
return nil
From b4bc047b30c10786f4d156462220cb302979e73f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 5 Oct 2022 02:01:02 +0000
Subject: [PATCH 288/732] Bump github.com/crunchy-labs/crunchyroll-go/v3 from
3.0.2 to 3.0.3
Bumps [github.com/crunchy-labs/crunchyroll-go/v3](https://github.com/crunchy-labs/crunchyroll-go) from 3.0.2 to 3.0.3.
- [Release notes](https://github.com/crunchy-labs/crunchyroll-go/releases)
- [Changelog](https://github.com/crunchy-labs/crunchyroll-go/blob/master/news.go)
- [Commits](https://github.com/crunchy-labs/crunchyroll-go/compare/v3.0.2...v3.0.3)
---
updated-dependencies:
- dependency-name: github.com/crunchy-labs/crunchyroll-go/v3
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index 162e7a1..19fb230 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.19
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.5.0
)
diff --git a/go.sum b/go.sum
index 51fe7a6..ec82a63 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2 h1:PG5++Gje126/xRtzZwCowoFU1Dl3qKzFjd3lWhVXoso=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3 h1:hkX7iSUnGt/6Lm/M28a6bAVQNfRFeowchtIh3OOH8Bg=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
From 36bdc76a48ceb60282e52c89796228b262e3074b Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Sat, 8 Oct 2022 02:05:29 +0200
Subject: [PATCH 289/732] Update discord invite link
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cbe6a37..7605a61 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crun
-
+
From eba2417f4ed94dbfef424ea24bdbcf90271ff623 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 12 Oct 2022 01:48:15 +0000
Subject: [PATCH 290/732] Bump github.com/spf13/cobra from 1.5.0 to 1.6.0
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.5.0 to 1.6.0.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.5.0...v1.6.0)
---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
---
go.mod | 4 ++--
go.sum | 10 +++++-----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/go.mod b/go.mod
index 19fb230..b297fb0 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,10 @@ go 1.19
require (
github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.5.0
+ github.com/spf13/cobra v1.6.0
)
require (
- github.com/inconshreveable/mousetrap v1.0.0 // indirect
+ github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
diff --git a/go.sum b/go.sum
index ec82a63..1295653 100644
--- a/go.sum
+++ b/go.sum
@@ -3,12 +3,12 @@ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3 h1:hkX7iSUnGt/6Lm/M28a6bAVQNfRF
github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
+github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
-github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
+github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
+github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
From e0d100b6274537950ac4e71c5940b580ddd50d25 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Thu, 20 Oct 2022 22:02:44 +0200
Subject: [PATCH 291/732] Update dependencies and fix #59 partially
---
cli/commands/archive/archive.go | 8 +-------
cli/commands/download/download.go | 8 +-------
go.mod | 2 +-
go.sum | 4 ++--
4 files changed, 5 insertions(+), 17 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 744635f..b46ea0f 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -211,10 +211,6 @@ func archive(urls []string) error {
episodes, err := archiveExtractEpisodes(url)
if err != nil {
utils.Log.StopProcess("Failed to parse url %d", i+1)
- if utils.Crunchy.Config.Premium {
- 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/crunchy-labs/crunchy-cli/issues/22 for more information")
- }
return err
}
utils.Log.StopProcess("Parsed url %d", i+1)
@@ -737,9 +733,7 @@ func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
}
}
- if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ if _, ok := crunchyroll.ParseEpisodeURL(url); ok {
return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
}
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
index 39273d0..9102e7b 100644
--- a/cli/commands/download/download.go
+++ b/cli/commands/download/download.go
@@ -139,10 +139,6 @@ func download(urls []string) error {
episodes, err := downloadExtractEpisodes(url)
if err != nil {
utils.Log.StopProcess("Failed to parse url %d", i+1)
- if utils.Crunchy.Config.Premium {
- 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/crunchy-labs/crunchy-cli/issues/22 for more information")
- }
return err
}
utils.Log.StopProcess("Parsed url %d", i+1)
@@ -284,9 +280,7 @@ func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
var final []*crunchyroll.Episode
if downloadAudioFlag != "" {
- if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
+ if _, ok := crunchyroll.ParseEpisodeURL(url); ok {
return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
}
diff --git a/go.mod b/go.mod
index b297fb0..e04d5ae 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/crunchy-labs/crunchy-cli
go 1.19
require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3
+ github.com/crunchy-labs/crunchyroll-go/v3 v3.0.4
github.com/grafov/m3u8 v0.11.1
github.com/spf13/cobra v1.6.0
)
diff --git a/go.sum b/go.sum
index 1295653..9ca4b9c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,6 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3 h1:hkX7iSUnGt/6Lm/M28a6bAVQNfRFeowchtIh3OOH8Bg=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.3/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.4 h1:QCHlk0PEfrm7uPgLm2RNtwXED3ACKlhD9xlrlGsPhDI=
+github.com/crunchy-labs/crunchyroll-go/v3 v3.0.4/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
From dc7e5d564ea4ae7849bc2e593ab6493eb03db0f9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 25 Oct 2022 01:56:32 +0000
Subject: [PATCH 292/732] Bump github.com/spf13/cobra from 1.6.0 to 1.6.1
Bumps [github.com/spf13/cobra](https://github.com/spf13/cobra) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.6.0...v1.6.1)
---
updated-dependencies:
- dependency-name: github.com/spf13/cobra
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
go.mod | 2 +-
go.sum | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/go.mod b/go.mod
index e04d5ae..b884b5a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.19
require (
github.com/crunchy-labs/crunchyroll-go/v3 v3.0.4
github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.6.0
+ github.com/spf13/cobra v1.6.1
)
require (
diff --git a/go.sum b/go.sum
index 9ca4b9c..1881381 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
-github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
+github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
+github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
From 0572af4e07f9ec2bbefa79704a0833625095f630 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Fri, 28 Oct 2022 12:43:51 +0200
Subject: [PATCH 293/732] Change links from beta.crunchyroll.com to
www.crunchyroll.com
---
README.md | 12 ++++++------
cli/commands/login/login.go | 2 +-
crunchy-cli.1 | 12 ++++++------
3 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 7605a61..6e757e1 100644
--- a/README.md
+++ b/README.md
@@ -98,13 +98,13 @@ By default, the cli tries to download the episode with your system language as a
**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
```shell
-$ crunchy download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy download https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
```shell
-$ crunchy download -r best https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy download -r best https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
The file is by default saved as a `.ts` (mpeg transport stream) file.
@@ -113,13 +113,13 @@ file, just name it `whatever.mp4`.
**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
```shell
-$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have. Type `crunchy help download` to see all available locales.
```shell
-$ crunchy download --audio ja-JP --subtitle de-DE https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+$ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
```
##### Flags
@@ -143,13 +143,13 @@ the `--language` flag.
Archive a file
```shell
-$ crunchy archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy archive https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
```
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
```shell
-$ crunchy archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+$ crunchy archive -c "ditf.tar.gz" https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
```
##### Flags
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
index 9fce181..47a595d 100644
--- a/cli/commands/login/login.go
+++ b/cli/commands/login/login.go
@@ -51,7 +51,7 @@ func init() {
Cmd.Flags().BoolVar(&loginRefreshTokenFlag,
"refresh-token",
false,
- "Use a refresh token to login instead of username and password. Can be obtained by copying the `etp-rt` cookie from beta.crunchyroll.com")
+ "Use a refresh token to login instead of username and password. Can be obtained by copying the `etp-rt` cookie from crunchyroll.com")
Cmd.MarkFlagsMutuallyExclusive("session-id", "refresh-token")
}
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index 91ba30c..eb3f16d 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -164,7 +164,7 @@ Use the list below to get a better overview what is possible
...[S3E4-] - Download all episodes from and including season 3, episode 4.
...[S1E4-S3] - Download all episodes from and including season 1, episode 4, until and including season 3.
-In practise, it would look like this: \fIhttps://beta.crunchyroll.com/series/12345678/example[S1E5-S3E2]\fR.
+In practise, it would look like this: \fIhttps://www.crunchyroll.com/series/12345678/example[S1E5-S3E2]\fR.
The \fBS\fR, followed by the number indicates the season number, \fBE\fR, followed by the number indicates an episode number. It doesn't matter if \fBS\fR, \fBE\fR or both are missing. Theoretically \fB[-]\fR is a valid pattern too. Note that \fBS\fR must always stay before \fBE\fR when used.
@@ -175,23 +175,23 @@ $ crunchy-cli login user@example.com 12345678
Download a episode normally. Your system locale will be used for the video's audio.
.br
-$ crunchy-cli download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli download https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with 720p and name it to 'darling.mp4'. Note that you need \fBffmpeg\fR to save files which do not have '.ts' as file extension.
.br
-$ crunchy-cli download -o "darling.mp4" -r 720p https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli download -o "darling.mp4" -r 720p https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Download a episode with japanese audio and american subtitles.
.br
-$ crunchy-cli download -a ja-JP -s en-US https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
+$ crunchy-cli download -a ja-JP -s en-US https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
Stores the episode in a .mkv file.
.br
-$ crunchy-cli archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
+$ crunchy-cli archive https://www.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
.br
-$ crunchy-cli archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
+$ crunchy-cli archive -c "ditf.tar.gz" https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
.SH BUGS
If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
From 95b66c3ff51f7df23d9c4603400ce1356b917c6c Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Mon, 31 Oct 2022 21:19:24 +0100
Subject: [PATCH 294/732] Fix subtitle styling and size (#66)
---
cli/commands/archive/archive.go | 34 ++++++++++++++++++++++++++++++++-
crunchy-cli.1 | 4 ++++
2 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index b46ea0f..03ec3ac 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -40,6 +40,8 @@ var (
archiveResolutionFlag string
archiveGoroutinesFlag int
+
+ archiveNoSubtitleOptimizations bool
)
var Cmd = &cobra.Command{
@@ -203,6 +205,11 @@ func init() {
"g",
runtime.NumCPU(),
"Number of parallel segment downloads")
+
+ Cmd.Flags().BoolVar(&archiveNoSubtitleOptimizations,
+ "no-subtitle-optimizations",
+ false,
+ "Disable subtitle optimizations. See https://github.com/crunchy-labs/crunchy-cli/issues/66 for more information")
}
func archive(urls []string) error {
@@ -540,13 +547,38 @@ func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitl
}
files = append(files, f.Name())
- if err := subtitle.Save(f); err != nil {
+ buffer := &bytes.Buffer{}
+
+ if err := subtitle.Save(buffer); err != nil {
f.Close()
for _, file := range files {
os.Remove(file)
}
return nil, err
}
+
+ if !archiveNoSubtitleOptimizations {
+ buffer2 := &bytes.Buffer{}
+ var scriptInfo bool
+ for _, line := range strings.Split(buffer.String(), "\n") {
+ if scriptInfo && strings.HasPrefix(strings.TrimSpace(line), "[") {
+ buffer2.WriteString("ScaledBorderAndShadows: yes\n")
+ scriptInfo = false
+ } else if strings.TrimSpace(line) == "[Script Info]" {
+ scriptInfo = true
+ }
+ buffer2.WriteString(line + "\n")
+ }
+
+ if _, err = io.Copy(f, buffer2); err != nil {
+ return nil, err
+ }
+ } else {
+ if _, err = io.Copy(f, buffer); err != nil {
+ return nil, err
+ }
+ }
+
f.Close()
utils.Log.Debug("Downloaded '%s' subtitles", subtitle.Locale)
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index eb3f16d..3032c79 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -146,6 +146,10 @@ The video resolution. Can either be specified via the pixels (e.g. 1920x1080), t
\fB-g, --goroutines GOROUTINES\fR
Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
+.TP
+
+\fB--no-subtitle-optimizations DISABLE\fR
+Disable subtitle optimizations which caused subtitle sizing and layout issues (https://github.com/crunchy-labs/crunchy-cli/issues/66).
.SH UPDATE COMMAND
Checks if a newer version is available.
From 10617df834cf41f468eacc290f42454a781da2b7 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Mon, 31 Oct 2022 22:18:44 +0100
Subject: [PATCH 295/732] Fix archive sorting (#63)
---
cli/commands/archive/archive.go | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
index 03ec3ac..4ac4f82 100644
--- a/cli/commands/archive/archive.go
+++ b/cli/commands/archive/archive.go
@@ -824,17 +824,22 @@ func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
}
var infoFormat [][]utils.FormatInformation
- for _, e := range eps {
+ var keys []int
+ for e := range eps {
+ keys = append(keys, e)
+ }
+ sort.Ints(keys)
+
+ for _, k := range keys {
var tmpFormatInfo []utils.FormatInformation
-
- var keys []int
- for episodeNumber := range e {
- keys = append(keys, episodeNumber)
+ var kkey []int
+ for ee := range eps[k] {
+ kkey = append(kkey, ee)
}
- sort.Ints(keys)
+ sort.Ints(kkey)
- for _, key := range keys {
- tmpFormatInfo = append(tmpFormatInfo, *e[key])
+ for _, kk := range kkey {
+ tmpFormatInfo = append(tmpFormatInfo, *eps[k][kk])
}
infoFormat = append(infoFormat, tmpFormatInfo)
From 59e8793a2f4c08192af1c1dd7e3433a1ddc2e8f8 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Tue, 1 Nov 2022 22:17:19 +0100
Subject: [PATCH 296/732] Set option to modify locale used (#60)
---
cli/root.go | 14 ++++++++++++++
crunchy-cli.1 | 8 ++++++++
utils/locale.go | 4 ++++
3 files changed, 26 insertions(+)
diff --git a/cli/root.go b/cli/root.go
index b589a03..c44ca23 100644
--- a/cli/root.go
+++ b/cli/root.go
@@ -10,6 +10,8 @@ import (
"github.com/crunchy-labs/crunchy-cli/cli/commands/login"
"github.com/crunchy-labs/crunchy-cli/cli/commands/update"
"github.com/crunchy-labs/crunchy-cli/utils"
+ "github.com/crunchy-labs/crunchyroll-go/v3"
+ crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
"github.com/spf13/cobra"
"os"
"runtime/debug"
@@ -22,6 +24,8 @@ var (
proxyFlag string
+ langFlag string
+
useragentFlag string
)
@@ -40,6 +44,14 @@ var RootCmd = &cobra.Command{
utils.Log = commands.NewLogger(false, false, false)
}
+ if langFlag != "" {
+ if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(langFlag)) {
+ return fmt.Errorf("'%s' is not a valid language. Choose from %s", langFlag, strings.Join(utils.LocalesAsStrings(), ", "))
+ }
+
+ os.Setenv("CRUNCHY_LANG", langFlag)
+ }
+
utils.Log.Debug("Executing `%s` command with %d arg(s)", cmd.Name(), len(args))
utils.Client, err = utils.CreateOrDefaultClient(proxyFlag, useragentFlag)
@@ -53,6 +65,8 @@ func init() {
RootCmd.PersistentFlags().StringVarP(&proxyFlag, "proxy", "p", "", "Proxy to use")
+ RootCmd.PersistentFlags().StringVar(&langFlag, "lang", "", fmt.Sprintf("Set language to use. If not set, it's received from the system locale dynamically. Choose from: %s", strings.Join(utils.LocalesAsStrings(), ", ")))
+
RootCmd.PersistentFlags().StringVar(&useragentFlag, "useragent", fmt.Sprintf("crunchy-cli/%s", utils.Version), "Useragent to do all request with")
RootCmd.AddCommand(archive.Cmd)
diff --git a/crunchy-cli.1 b/crunchy-cli.1
index 3032c79..af697f3 100644
--- a/crunchy-cli.1
+++ b/crunchy-cli.1
@@ -44,6 +44,14 @@ Disables all output.
Shows verbose output.
.TP
+\fB--lang\fR
+Set language to use. If not set, it's received from the system locale dynamically. Choose from: ar-ME, ar-SA, de-DE, en-US, es-419, es-ES, es-LA, fr-FR, it-IT, ja-JP, pt-BR, pt-PT, ru-RU, zh-CN.
+.TP
+
+\fB--useragent\fR
+Useragent to do all request with.
+.TP
+
\fB--version\fR
Shows the current cli version.
diff --git a/utils/locale.go b/utils/locale.go
index 9769940..1e28f6a 100644
--- a/utils/locale.go
+++ b/utils/locale.go
@@ -14,6 +14,10 @@ import (
// SystemLocale receives the system locale
// https://stackoverflow.com/questions/51829386/golang-get-system-language/51831590#51831590
func SystemLocale(verbose bool) crunchyroll.LOCALE {
+ if lang, ok := os.LookupEnv("CRUNCHY_LANG"); ok {
+ return crunchyroll.LOCALE(lang)
+ }
+
if runtime.GOOS != "windows" {
if lang, ok := os.LookupEnv("LANG"); ok {
var l crunchyroll.LOCALE
From 039d7cfb81684d9de6e0311af8ff4b5c86ab1bcc Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Thu, 20 Oct 2022 18:52:08 +0200
Subject: [PATCH 297/732] Rewrite it in Rust
---
.github/dependabot.yml | 15 +-
.github/workflows/ci.yml | 20 -
.github/workflows/codeql-analysis.yml | 67 -
Cargo.lock | 1645 +++++++++++++++++++++++++
Cargo.toml | 33 +
Makefile | 31 -
README.md | 272 ++--
build.rs | 112 ++
cli/commands/archive/archive.go | 818 ------------
cli/commands/archive/compress.go | 136 --
cli/commands/download/download.go | 368 ------
cli/commands/info/info.go | 40 -
cli/commands/logger.go | 196 ---
cli/commands/login/login.go | 159 ---
cli/commands/unix.go | 48 -
cli/commands/update/update.go | 151 ---
cli/commands/utils.go | 125 --
cli/commands/windows.go | 41 -
cli/root.go | 85 --
crunchy-cli-core/Cargo.toml | 29 +
crunchy-cli-core/src/cli/archive.rs | 567 +++++++++
crunchy-cli-core/src/cli/download.rs | 452 +++++++
crunchy-cli-core/src/cli/log.rs | 197 +++
crunchy-cli-core/src/cli/login.rs | 39 +
crunchy-cli-core/src/cli/mod.rs | 5 +
crunchy-cli-core/src/cli/utils.rs | 178 +++
crunchy-cli-core/src/lib.rs | 196 +++
crunchy-cli-core/src/utils/clap.rs | 6 +
crunchy-cli-core/src/utils/context.rs | 6 +
crunchy-cli-core/src/utils/format.rs | 77 ++
crunchy-cli-core/src/utils/locale.rs | 15 +
crunchy-cli-core/src/utils/log.rs | 19 +
crunchy-cli-core/src/utils/mod.rs | 8 +
crunchy-cli-core/src/utils/os.rs | 52 +
crunchy-cli-core/src/utils/parse.rs | 170 +++
crunchy-cli-core/src/utils/sort.rs | 47 +
crunchy-cli.1 | 219 ----
go.mod | 14 -
go.sum | 14 -
main.go | 9 -
src/main.rs | 4 +
utils/extract.go | 99 --
utils/file.go | 49 -
utils/format.go | 63 -
utils/http.go | 51 -
utils/locale.go | 59 -
utils/logger.go | 12 -
utils/save.go | 177 ---
utils/std.go | 10 -
utils/system.go | 7 -
utils/vars.go | 14 -
51 files changed, 4018 insertions(+), 3208 deletions(-)
delete mode 100644 .github/workflows/ci.yml
delete mode 100644 .github/workflows/codeql-analysis.yml
create mode 100644 Cargo.lock
create mode 100644 Cargo.toml
delete mode 100644 Makefile
create mode 100644 build.rs
delete mode 100644 cli/commands/archive/archive.go
delete mode 100644 cli/commands/archive/compress.go
delete mode 100644 cli/commands/download/download.go
delete mode 100644 cli/commands/info/info.go
delete mode 100644 cli/commands/logger.go
delete mode 100644 cli/commands/login/login.go
delete mode 100644 cli/commands/unix.go
delete mode 100644 cli/commands/update/update.go
delete mode 100644 cli/commands/utils.go
delete mode 100644 cli/commands/windows.go
delete mode 100644 cli/root.go
create mode 100644 crunchy-cli-core/Cargo.toml
create mode 100644 crunchy-cli-core/src/cli/archive.rs
create mode 100644 crunchy-cli-core/src/cli/download.rs
create mode 100644 crunchy-cli-core/src/cli/log.rs
create mode 100644 crunchy-cli-core/src/cli/login.rs
create mode 100644 crunchy-cli-core/src/cli/mod.rs
create mode 100644 crunchy-cli-core/src/cli/utils.rs
create mode 100644 crunchy-cli-core/src/lib.rs
create mode 100644 crunchy-cli-core/src/utils/clap.rs
create mode 100644 crunchy-cli-core/src/utils/context.rs
create mode 100644 crunchy-cli-core/src/utils/format.rs
create mode 100644 crunchy-cli-core/src/utils/locale.rs
create mode 100644 crunchy-cli-core/src/utils/log.rs
create mode 100644 crunchy-cli-core/src/utils/mod.rs
create mode 100644 crunchy-cli-core/src/utils/os.rs
create mode 100644 crunchy-cli-core/src/utils/parse.rs
create mode 100644 crunchy-cli-core/src/utils/sort.rs
delete mode 100644 crunchy-cli.1
delete mode 100644 go.mod
delete mode 100644 go.sum
delete mode 100644 main.go
create mode 100644 src/main.rs
delete mode 100644 utils/extract.go
delete mode 100644 utils/file.go
delete mode 100644 utils/format.go
delete mode 100644 utils/http.go
delete mode 100644 utils/locale.go
delete mode 100644 utils/logger.go
delete mode 100644 utils/save.go
delete mode 100644 utils/std.go
delete mode 100644 utils/system.go
delete mode 100644 utils/vars.go
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3938344..40d73b8 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,17 @@
version: 2
updates:
- - package-ecosystem: "gomod"
+ - package-ecosystem: "cargo"
directory: "/"
schedule:
- interval: "daily"
+ interval: "weekly"
+ ignore:
+ - dependency-name: "*"
+ update-types: [ "version-update:semver-patch" ]
+
+ - package-ecosystem: "cargo"
+ directory: "/crunchy-cli-core"
+ schedule:
+ interval: "weekly"
+ ignore:
+ - dependency-name: "*"
+ update-types: [ "version-update:semver-patch" ]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
deleted file mode 100644
index b2409a0..0000000
--- a/.github/workflows/ci.yml
+++ /dev/null
@@ -1,20 +0,0 @@
-name: CI
-
-on: [ push, pull_request ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v3
-
- - name: Set up Go
- uses: actions/setup-go@v3
- with:
- go-version: 1.18
-
- - name: Build
- run: go build -v .
-
- - name: Test
- run: go test -v .
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index e0f5470..0000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,67 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
- push:
- pull_request:
- schedule:
- - cron: '40 3 * * 2'
-
-jobs:
- analyze:
- name: Analyze
- runs-on: ubuntu-latest
- permissions:
- actions: read
- contents: read
- security-events: write
-
- strategy:
- fail-fast: false
- matrix:
- language: [ 'go' ]
- # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
- # Learn more about CodeQL language support at https://git.io/codeql-language-support
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Initializes the CodeQL tools for scanning.
- - name: Initialize CodeQL
- uses: github/codeql-action/init@v2
- with:
- languages: ${{ matrix.language }}
- # If you wish to specify custom queries, you can do so here or in a config file.
- # By default, queries listed here will override any specified in a config file.
- # Prefix the list here with "+" to use these queries and those in the config file.
- # queries: ./path/to/local/query, your-org/your-repo/queries@main
-
- # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
- # If this step fails, then you should remove it and run the build manually (see below)
- - name: Autobuild
- uses: github/codeql-action/autobuild@v2
-
- # ℹ️ Command-line programs to run using the OS shell.
- # 📚 https://git.io/JvXDl
-
- # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
- # and modify them (or add more) to build your code if your project
- # uses a compiled language
-
- #- run: |
- # make bootstrap
- # make release
-
- - name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..05b2d65
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1645 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aes"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+
+[[package]]
+name = "async-channel"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-padding"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
+name = "bytes"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
+
+[[package]]
+name = "cache-padded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0fba905b035a30d25c1b585bf1171690712fbb0ad3ac47214963aa4acc36c"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "clap_mangen"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa149477df7854a1497db0def32b8a65bf98f72a14d04ac75b01938285d83420"
+dependencies = [
+ "clap",
+ "roff",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
+dependencies = [
+ "cache-padded",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crunchy-cli"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "clap",
+ "clap_complete",
+ "clap_mangen",
+ "crunchy-cli-core",
+ "tokio",
+]
+
+[[package]]
+name = "crunchy-cli-core"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "chrono",
+ "clap",
+ "crunchyroll-rs",
+ "ctrlc",
+ "dirs",
+ "isahc",
+ "log",
+ "num_cpus",
+ "regex",
+ "signal-hook",
+ "sys-locale",
+ "tempfile",
+ "terminal_size",
+ "tokio",
+]
+
+[[package]]
+name = "crunchyroll-rs"
+version = "0.1.0"
+source = "git+https://github.com/crunchy-labs/crunchyroll-rs#7aedfc6c9a91a42ef46639ba9e99adba63cd0dda"
+dependencies = [
+ "aes",
+ "cbc",
+ "chrono",
+ "crunchyroll-rs-internal",
+ "http",
+ "isahc",
+ "m3u8-rs",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smart-default",
+ "tokio",
+]
+
+[[package]]
+name = "crunchyroll-rs-internal"
+version = "0.1.0"
+source = "git+https://github.com/crunchy-labs/crunchyroll-rs#7aedfc6c9a91a42ef46639ba9e99adba63cd0dda"
+dependencies = [
+ "darling",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173"
+dependencies = [
+ "nix",
+ "winapi",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.59+curl-7.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+dependencies = [
+ "cc",
+ "libc",
+ "libnghttp2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "darling"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+
+[[package]]
+name = "futures-io"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
+
+[[package]]
+name = "futures-lite"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074"
+
+[[package]]
+name = "isahc"
+version = "1.7.0"
+source = "git+https://github.com/sagebind/isahc?rev=34f158ef#34f158ef9f87b2387bed2c81936916a29c1eaad1"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "httpdate",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libnghttp2-sys"
+version = "0.1.7+1.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "m3u8-rs"
+version = "5.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15d091887fd4a920417805283b7a838d0dcda68e8d632cd305a4439ee776d1ce"
+dependencies = [
+ "chrono",
+ "nom",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nix"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-src"
+version = "111.24.0+1.1.1s"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "polling"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "libc",
+ "log",
+ "wepoll-ffi",
+ "winapi",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "roff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316"
+
+[[package]]
+name = "rustix"
+version = "0.35.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smart-default"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sys-locale"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee"
+dependencies = [
+ "js-sys",
+ "libc",
+ "wasm-bindgen",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a"
+dependencies = [
+ "rustix",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
+dependencies = [
+ "autocfg",
+ "num_cpus",
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wepoll-ffi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..c99a2a9
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "crunchy-cli"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+default = ["static-curl"]
+
+# Embed a static curl library into the binary instead of just linking it.
+static-curl = ["crunchy-cli-core/static-curl"]
+# Embed a static openssl library into the binary instead of just linking it. If you want to compile this project against
+# musl and have openssl issues, this might solve these issues.
+static-ssl = ["crunchy-cli-core/static-ssl"]
+
+[dependencies]
+tokio = { version = "1.22", features = ["macros", "rt-multi-thread", "time"], default-features = false }
+
+crunchy-cli-core = { path = "./crunchy-cli-core" }
+
+[build-dependencies]
+chrono = "0.4"
+clap = { version = "4.0", features = ["string"] }
+clap_complete = "4.0"
+clap_mangen = "0.2"
+
+# The static-* features must be used here since build dependency features cannot be manipulated from the features
+# specified in this Cargo.toml [features].
+crunchy-cli-core = { path = "./crunchy-cli-core", features = ["static-curl", "static-ssl"] }
+
+[profile.release]
+strip = true
+opt-level = "z"
+lto = true
diff --git a/Makefile b/Makefile
deleted file mode 100644
index 966872b..0000000
--- a/Makefile
+++ /dev/null
@@ -1,31 +0,0 @@
-VERSION=development
-BINARY_NAME=crunchy
-VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)
-
-DESTDIR=
-PREFIX=/usr
-
-build:
- go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(BINARY_NAME) .
-
-clean:
- rm -f $(BINARY_NAME) $(VERSION_BINARY_NAME)_*
-
-install:
- install -Dm755 $(BINARY_NAME) $(DESTDIR)$(PREFIX)/bin/crunchy-cli
- ln -sf ./crunchy-cli $(DESTDIR)$(PREFIX)/bin/crunchy
- install -Dm644 crunchy-cli.1 $(DESTDIR)$(PREFIX)/share/man/man1/crunchy-cli.1
- install -Dm644 LICENSE $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
-
-uninstall:
- rm -f $(DESTDIR)$(PREFIX)/bin/crunchy-cli
- rm -f $(DESTDIR)$(PREFIX)/bin/crunchy
- rm -f $(DESTDIR)$(PREFIX)/share/man/man1/crunchy-cli.1
- rm -f $(DESTDIR)$(PREFIX)/share/licenses/crunchy-cli/LICENSE
-
-release:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_linux .
- CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_windows.exe .
- CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-X 'github.com/crunchy-labs/crunchy-cli/utils.Version=$(VERSION)'" -o $(VERSION_BINARY_NAME)_darwin .
-
- strip $(VERSION_BINARY_NAME)_linux
diff --git a/README.md b/README.md
index cbe6a37..2229739 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# crunchy-cli
-A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crunchyroll.com). To use it, you need a crunchyroll premium account for full access & features.
+A [Rust](https://www.rust-lang.org/) written cli client for [Crunchyroll](https://www.crunchyroll.com).
@@ -21,178 +21,202 @@ A [Go](https://golang.org) written cli client for [crunchyroll](https://www.crun
- CLI 🖥️
+ Usage 🖥️
•
Disclaimer ☝️
•
License ⚖
-_This repo was former known as **crunchyroll-go** (which still exists but now contains only the library part) but got split up into two separate repositories to provide more flexibility._
-
-> This tool relies on the [crunchyroll-go](https://github.com/crunchy-labs/crunchyroll-go) library to communicate with crunchyroll.
-> The library enters maintenance mode (only small fixes, no new features) with version v3 in favor of rewriting it completely in Rust.
-> **crunchy-cli** follows it (with version v2.3.0) and won't have major updates until the Rust rewrite of the library reaches a good usable state.
-
-# 🖥️ CLI
+> We are in no way affiliated with, maintained, authorized, sponsored, or officially associated with Crunchyroll LLC or any of its subsidiaries or affiliates.
+> The official Crunchyroll website can be found at https://crunchyroll.com/.
## ✨ Features
-- Download single videos and entire series from [crunchyroll](https://www.crunchyroll.com).
-- Archive episode or seasons in an `.mkv` file with multiple subtitles and audios and compress them to gzip or zip files.
+- Download single videos and entire series from [Crunchyroll](https://www.crunchyroll.com).
+- Archive episode or seasons in an `.mkv` file with multiple subtitles and audios.
- Specify a range which episodes to download from an anime.
## 💾 Get the executable
-- 📥 Download the latest binaries [here](https://github.com/crunchy-labs/crunchy-cli/releases/latest) or get it from below:
- - [Linux (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_linux)
- - [Windows (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_windows.exe)
- - [MacOS (x64)](https://smartrelease.bytedream.org/github/crunchy-labs/crunchy-cli/crunchy-{tag}_darwin)
-- If you use Arch btw. or any other Linux distro which is based on Arch Linux, you can download the package via the [AUR](https://aur.archlinux.org/packages/crunchyroll-go/):
- ```shell
- $ yay -S crunchy-cli
- ```
-- On Windows [scoop](https://scoop.sh/) can be used to install it (added by [@AdmnJ](https://github.com/AdmnJ)):
- ```shell
- $ scoop bucket add extras # <- in case you haven't added the extra repository already
- $ scoop install crunchyroll-go
- ```
-- 🛠 Build it yourself. Must be done if your target platform is not covered by the [provided binaries](https://github.com/crunchy-labs/crunchy-cli/releases/latest) (like Raspberry Pi or M1 Mac):
- - use `make` (requires `go` to be installed):
- ```shell
- $ git clone https://github.com/crunchy-labs/crunchy-cli
- $ cd crunchy-cli
- $ make
- $ sudo make install # <- only if you want to install it on your system
- ```
- - use `go`:
- ```shell
- $ git clone https://github.com/crunchy-labs/crunchy-cli
- $ cd crunchy-cli
- $ go build -o crunchy .
- ```
+### 📥 Download the latest binaries
-## 📝 Examples
+Checkout the [releases](https://github.com/crunchy-labs/crunchy-cli/releases) tab and get the binary from the newest release.
-_Before reading_: Because of the huge functionality not all cases can be covered in the README. Make sure to check the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli), further usages and options are described there.
+### 🛠 Build it yourself
+
+Since we do not support every platform and architecture you may have to build the project yourself.
+This requires [git](https://git-scm.com/) and [Cargo](https://doc.rust-lang.org/cargo).
+```shell
+$ git clone https://github.com/crunchy-labs/crunchy-cli
+$ cd crunchy-cli
+$ cargo build --release
+```
+After the binary has built successfully it is available in `target/release`.
+
+## 🖥️ Usage
+
+> All shown command are just examples
+
+Every command requires you to be logged in with an account.
+It doesn't matter if this account is premium or not, both works (but as free user you do not have access to premium content).
+You can pass your account via credentials (username & password) or refresh token.
+
+- Refresh Token
+ - To get the token you have to log in at [crunchyroll.com](https://www.crunchyroll.com/) and extract the `etp_rt` cookie.
+ The easiest way to get it is via a browser extension with lets you view your cookies, like [Cookie-Editor](https://cookie-editor.cgagnier.ca/) ([Firefox Store](https://addons.mozilla.org/en-US/firefox/addon/cookie-editor/); [Chrome Store](https://chrome.google.com/webstore/detail/cookie-editor/hlkenndednhfkekhgcdicdfddnkalmdm)).
+ If installed, search the `etp_rt` entry and extract the value.
+ - ```shell
+ $ crunchy --etp-rt "abcd1234-zyxw-9876-98zy-a1b2c3d4e5f6"
+ ```
+- Credentials
+ - Credentials must be provided as one single expression.
+ Username and password must be separated by a `:`.
+ - ```shell
+ $ crunchy --credentials "user:password"
+ ```
### Login
-Before you can do something, you have to log in first.
-
-This can be performed via crunchyroll account email and password.
+If you do not want to provide your credentials every time you execute a command, they can be stored permanently on disk.
+This can be done with the `login` subcommand.
```shell
-$ crunchy login user@example.com password
+$ crunchy --etp-rt "abcd1234-zyxw-9876-98zy-a1b2c3d4e5f6" login
```
-or via refresh token / `etp_rt` cookie
-
-```shell
-$ crunchy login --refresh-token 7578ce50-5712-3gef-b97e-01332d6b588c
-```
+Once set, you do not need to provide `--etp-rt` / `--credentials` anymore when using the cli.
### Download
-By default, the cli tries to download the episode with your system language as audio. If no streams with your system language are available, the video will be downloaded with japanese audio and hardsubbed subtitles in your system language.
-**If your system language is not supported, an error message will be displayed and en-US (american english) will be chosen as language.**
+**Supported urls**
+- Single episode
+ ```shell
+ $ crunchy download https://www.crunchyroll.com/watch/GRDQPM1ZY/alone-and-lonesome
+ ```
+- Episode range
+
+ If you want only specific episodes / seasons of an anime you can easily provide the series url along with a _filter_.
+ The filter has to be attached to the url. See the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter) for more information
+ ```shell
+ $ crunchy download https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1]
+ ```
+- Series
+ ```shell
+ $ crunchy download https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+
+**Options**
+- Audio language
-```shell
-$ crunchy download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-```
+ Which audio the episode(s) should be can be set via the `-a` / `--audio` flag.
+ This only works if the url points to a series since episode urls are language specific.
+ ```shell
+ $ crunchy download -a de-DE https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is your system language. If not supported by Crunchyroll, `en-US` (American English) is the default.
-With `-r best` the video(s) will have the best available resolution (mostly 1920x1080 / Full HD).
+- Subtitle language
-```shell
-$ crunchy download -r best https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-```
+ Besides the audio, it's also possible to specify which language the subtitles should have with the `-s` / `--subtitle` flag.
+ The subtitle will be hardsubbed (burned into the video) and thus, can't be turned off or on.
+ ```shell
+ $ crunchy download -s de-DE https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is no subtitle.
-The file is by default saved as a `.ts` (mpeg transport stream) file.
-`.ts` files may can't be played or are looking very weird (it depends on the video player you are using). With the `-o` flag, you can change the name (and file ending) of the output file. So if you want to save it as, for example, `mp4`
-file, just name it `whatever.mp4`.
-**You need [ffmpeg](https://ffmpeg.org) to store the video in other file formats.**
+- Output filename
-```shell
-$ crunchy download -o "daaaaaaaaaaaaaaaarling.ts" https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-```
+ You can specify the name of the output file with the `-o` / `--output` flag.
+ If you want to use any other file format than [`.ts`](https://en.wikipedia.org/wiki/MPEG_transport_stream) you need [ffmpeg](https://ffmpeg.org/).
+ ```shell
+ $ crunchy download -o "ditf.ts" https://www.crunchyroll.com/watch/GRDQPM1ZY/alone-and-lonesome
+ ```
+ Default is `{title}.ts`.
-With the `--audio` flag you can specify which audio the video should have and with `--subtitle` which subtitle it should have. Type `crunchy help download` to see all available locales.
+- Resolution
-```shell
-$ crunchy download --audio ja-JP --subtitle de-DE https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
-```
-
-##### Flags
-
-The following flags can be (optional) passed to modify the [download](#download) process.
-
-| Short | Extended | Description |
-|-------|----------------|--------------------------------------------------------------------------------|
-| `-a` | `--audio` | Forces audio of the video(s). |
-| `-s` | `--subtitle` | Forces subtitle of the video(s). |
-| `-d` | `--directory` | Directory to download the video(s) to. |
-| `-o` | `--output` | Name of the output file. |
-| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
-| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
+ The resolution for videos can be set via the `-r` / `--resolution` flag.
+ ```shell
+ $ crunchy download -r worst https://www.crunchyroll.com/watch/GRDQPM1ZY/alone-and-lonesome
+ ```
+ Default is `best`.
### Archive
-Archive works just like [download](#download). It downloads the given videos as `.mkv` files and stores all (soft) subtitles in it. Default audio locales are japanese and your system language (if available) but you can set more or less with
-the `--language` flag.
+**Supported urls**
+- Series
-Archive a file
+ Only series urls are supported since single episode urls are (audio) language locked.
+ ```shell
+ $ crunchy archive https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+
+**Options**
+- Audio languages
-```shell
-$ crunchy archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-```
+ Which audios the episode(s) should be can be set via the `-a` / `--audio` flag.
+ ```shell
+ $ crunchy archive -a ja-JP -a de-DE https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Can be used multiple times.
+ Default is your system language (if not supported by Crunchyroll, `en-US` (American English) is the default) + `ja-JP` (Japanese).
-Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
+- Subtitle languages
-```shell
-$ crunchy archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
-```
+ Besides the audio, it's also possible to specify which languages the subtitles should have with the `-s` / `--subtitle` flag.
+ ```shell
+ $ crunchy archive -s de-DE https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is all subtitles.
-##### Flags
+- Output filename
-The following flags can be (optional) passed to modify the [archive](#archive) process.
+ You can specify the name of the output file with the `-o` / `--output` flag.
+ The only supported file / container format is [`.mkv`](https://en.wikipedia.org/wiki/Matroska) since it stores / can store multiple audio, video and subtitle streams.
+ ```shell
+ $ crunchy archive -o "{title}.mkv" https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is `{title}.mkv`.
-| Short | Extended | Description |
-|-------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| `-l` | `--language` | Audio locale which should be downloaded. Can be used multiple times. |
-| `-d` | `--directory` | Directory to download the video(s) to. |
-| `-o` | `--output` | Name of the output file. |
-| `-m` | `--merge` | Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. See the [wiki](https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#archive) for more information. |
-| `-c` | `--compress` | If is set, all output will be compresses into an archive. This flag sets the name of the compressed output file and the file ending specifies the compression algorithm (gzip, tar, zip are supported). |
-| `-r` | `--resolution` | The resolution of the video(s). `best` for best resolution, `worst` for worst. |
-| `-g` | `--goroutines` | Sets how many parallel segment downloads should be used. |
+- Resolution
+ The resolution for videos can be set via the `-r` / `--resolution` flag.
+ ```shell
+ $ crunchy archive -r worst https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is `best`.
-### Info
+- Merge behavior
-The `info` displays some information about the account which is used for the cli.
+ Because of local restrictions (or other reasons) some episodes with different languages does not have the same length (e.g. when some scenes were cut out).
+ The ideal state, when multiple audios & subtitles used, would be if only one _video_ has to be stored and all other languages can be stored as audio-only.
+ But, as said, this is not always the case.
+ With the `-m` / `--merge` flag you can set what you want to do if some video lengths differ.
+ Valid options are `audio` - store one video and all other languages as audio only; `video` - store the video + audio for every language; `auto` - detect if videos differ in length: if so, behave like `video` else like `audio`.
+ Subtitles will always match to the first / primary audio and video.
+ ```shell
+ $ crunchy archive -m audio https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is `auto`.
-```shell
-$ crunchy info
-```
+- Default subtitle
-### Update
+ `--default_subtitle` set which subtitle language should be set as default / auto appear when starting the downloaded video(s).
+ ```shell
+ $ crunchy archive --default_subtitle en-US https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
+ Default is none.
-If you want to update your local version of `crunchy-cli`, this command makes this easier.
-It checks if a new version is available and if so, updates itself.
+- No subtitle optimizations
-```shell
-$ crunchy update
-```
-
-### Global flags
-
-These flags you can use across every sub-command:
-
-| Flag | Description |
-|------|------------------------------------------------------|
-| `-q` | Disables all output. |
-| `-v` | Shows additional debug output. |
-| `-p` | Use a proxy to hide your ip / redirect your traffic. |
+ Subtitles, as Crunchyroll delivers them, look weird in some video players (#66).
+ This can be fixed by adding a specific entry to the subtitles.
+ But since this entry is only a de-factor standard and not represented in the official specification of the subtitle format ([`.ass`](https://en.wikipedia.org/wiki/SubStation_Alpha)) it could cause issues with some video players (but no issue got reported so far, so it's relatively safe to use).
+ `--no_subtitle_optimizations` can disable these optimizations.
+ ```shell
+ $ crunchy archive --no_subtitle_optimizations https://www.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx
+ ```
# ☝️ Disclaimer
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..cc6a0cc
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,112 @@
+use clap::{Command, CommandFactory};
+use clap_complete::shells;
+use std::path::{Path, PathBuf};
+
+// this build file generates completions for various shells as well as manual pages
+
+fn main() -> std::io::Result<()> {
+ // do not generate anything when building non release
+ if cfg!(debug_assertions) {
+ return Ok(());
+ }
+
+ // note that we're using an anti-pattern here / violate the rust conventions. build script are
+ // not supposed to write outside of 'OUT_DIR'. to have the generated files in the build "root"
+ // (the same directory where the output binary lives) is much simpler than in 'OUT_DIR' since
+ // its nested in sub directories and is difficult to find (at least more difficult than in the
+ // build root)
+ let unconventional_out_dir =
+ std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or(std::io::ErrorKind::NotFound)?)
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .parent()
+ .unwrap()
+ .to_path_buf();
+
+ let completions_dir = exist_or_create_dir(unconventional_out_dir.join("completions"))?;
+ let manpage_dir = exist_or_create_dir(unconventional_out_dir.join("manpages"))?;
+
+ generate_completions(completions_dir)?;
+ generate_manpages(manpage_dir)?;
+
+ Ok(())
+}
+
+fn exist_or_create_dir(path: PathBuf) -> std::io::Result {
+ if !path.exists() {
+ std::fs::create_dir(path.clone())?
+ }
+ Ok(path)
+}
+
+fn generate_completions(out_dir: PathBuf) -> std::io::Result<()> {
+ let mut command: Command = crunchy_cli_core::Cli::command();
+
+ clap_complete::generate_to(
+ shells::Bash,
+ &mut command.clone(),
+ "crunchy-cli",
+ out_dir.clone(),
+ )?;
+ clap_complete::generate_to(
+ shells::Elvish,
+ &mut command.clone(),
+ "crunchy-cli",
+ out_dir.clone(),
+ )?;
+ println!(
+ "{}",
+ clap_complete::generate_to(
+ shells::Fish,
+ &mut command.clone(),
+ "crunchy-cli",
+ out_dir.clone(),
+ )?
+ .to_string_lossy()
+ );
+ clap_complete::generate_to(
+ shells::PowerShell,
+ &mut command.clone(),
+ "crunchy-cli",
+ out_dir.clone(),
+ )?;
+ clap_complete::generate_to(shells::Zsh, &mut command, "crunchy-cli", out_dir)?;
+
+ Ok(())
+}
+
+fn generate_manpages(out_dir: PathBuf) -> std::io::Result<()> {
+ fn generate_command_manpage(
+ mut command: Command,
+ base_path: &Path,
+ sub_name: &str,
+ ) -> std::io::Result<()> {
+ let (file_name, title) = if sub_name.is_empty() {
+ command = command.name("crunchy-cli");
+ ("crunchy-cli.1".to_string(), "crunchy-cli".to_string())
+ } else {
+ command = command.name(format!("crunchy-cli {}", sub_name));
+ (
+ format!("crunchy-cli-{}.1", sub_name),
+ format!("crunchy-cli-{}", sub_name),
+ )
+ };
+
+ let mut command_buf = vec![];
+ let man = clap_mangen::Man::new(command)
+ .title(title)
+ .date(chrono::Utc::now().format("%b %d, %Y").to_string());
+ man.render(&mut command_buf)?;
+
+ std::fs::write(base_path.join(file_name), command_buf)
+ }
+
+ generate_command_manpage(crunchy_cli_core::Cli::command(), &out_dir, "")?;
+ generate_command_manpage(crunchy_cli_core::Archive::command(), &out_dir, "archive")?;
+ generate_command_manpage(crunchy_cli_core::Download::command(), &out_dir, "download")?;
+ generate_command_manpage(crunchy_cli_core::Login::command(), &out_dir, "login")?;
+
+ Ok(())
+}
diff --git a/cli/commands/archive/archive.go b/cli/commands/archive/archive.go
deleted file mode 100644
index 744635f..0000000
--- a/cli/commands/archive/archive.go
+++ /dev/null
@@ -1,818 +0,0 @@
-package archive
-
-import (
- "bufio"
- "bytes"
- "context"
- "fmt"
- "io"
- "math"
- "os"
- "os/exec"
- "os/signal"
- "path/filepath"
- "regexp"
- "runtime"
- "sort"
- "strconv"
- "strings"
-
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
- "github.com/grafov/m3u8"
- "github.com/spf13/cobra"
-)
-
-var (
- archiveLanguagesFlag []string
- archiveSubLanguagesFlag []string
-
- archiveDirectoryFlag string
- archiveOutputFlag string
- archiveTempDirFlag string
-
- archiveMergeFlag string
-
- archiveCompressFlag string
-
- archiveResolutionFlag string
-
- archiveGoroutinesFlag int
-)
-
-var Cmd = &cobra.Command{
- Use: "archive",
- Short: "Stores the given videos with all subtitles and multiple audios in a .mkv file",
- Args: cobra.MinimumNArgs(1),
-
- PreRunE: func(cmd *cobra.Command, args []string) error {
- utils.Log.Debug("Validating arguments")
-
- if !utils.HasFFmpeg() {
- return fmt.Errorf("ffmpeg is needed to run this command correctly")
- }
- utils.Log.Debug("FFmpeg detected")
-
- if filepath.Ext(archiveOutputFlag) != ".mkv" {
- return fmt.Errorf("currently only matroska / .mkv files are supported")
- }
-
- for _, locale := range archiveLanguagesFlag {
- if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(locale)) {
- // if locale is 'all', match all known locales
- if locale == "all" {
- archiveLanguagesFlag = utils.LocalesAsStrings()
- break
- }
- return fmt.Errorf("%s is not a valid locale. Choose from: %s", locale, strings.Join(utils.LocalesAsStrings(), ", "))
- }
- }
- utils.Log.Debug("Using following audio locales: %s", strings.Join(archiveLanguagesFlag, ", "))
-
- for _, locale := range archiveSubLanguagesFlag {
- if !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(locale)) {
- // if locale is 'all', match all known locales
- if locale == "all" {
- archiveSubLanguagesFlag = utils.LocalesAsStrings()
- break
- }
- return fmt.Errorf("%s is not a valid locale for Subtitels. Choose from: %s", locale, strings.Join(utils.LocalesAsStrings(), ", "))
- }
- }
- utils.Log.Debug("Using following subtitels locales: %s", strings.Join(archiveSubLanguagesFlag, ", "))
-
- var found bool
- for _, mode := range []string{"auto", "audio", "video"} {
- if archiveMergeFlag == mode {
- utils.Log.Debug("Using %s merge behavior", archiveMergeFlag)
- found = true
- break
- }
- }
- if !found {
- return fmt.Errorf("'%s' is no valid merge flag. Use 'auto', 'audio' or 'video'", archiveMergeFlag)
- }
-
- if archiveCompressFlag != "" {
- found = false
- for _, algo := range []string{".tar", ".tar.gz", ".tgz", ".zip"} {
- if strings.HasSuffix(archiveCompressFlag, algo) {
- utils.Log.Debug("Using %s compression", algo)
- found = true
- break
- }
- }
- if !found {
- return fmt.Errorf("'%s' is no valid compress algorithm. Valid algorithms / file endings are '.tar', '.tar.gz', '.zip'",
- archiveCompressFlag)
- }
- }
-
- switch archiveResolutionFlag {
- case "1080p", "720p", "480p", "360p":
- intRes, _ := strconv.ParseFloat(strings.TrimSuffix(archiveResolutionFlag, "p"), 84)
- archiveResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(archiveResolutionFlag, "p"))
- case "240p":
- // 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
- archiveResolutionFlag = "428x240"
- case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
- default:
- return fmt.Errorf("'%s' is not a valid resolution", archiveResolutionFlag)
- }
- utils.Log.Debug("Using resolution '%s'", archiveResolutionFlag)
-
- return nil
- },
- RunE: func(cmd *cobra.Command, args []string) error {
- if err := commands.LoadCrunchy(); err != nil {
- return err
- }
-
- return archive(args)
- },
-}
-
-func init() {
- Cmd.Flags().StringSliceVarP(&archiveLanguagesFlag,
- "language",
- "l",
- []string{string(utils.SystemLocale(false)), string(crunchyroll.JP)},
- "Audio locale which should be downloaded. Can be used multiple times")
-
- Cmd.Flags().StringSliceVarP(&archiveSubLanguagesFlag,
- "sublang",
- "s",
- utils.LocalesAsStrings(),
- "Subtitles langs which should be downloaded. Can be used multiple times")
-
- cwd, _ := os.Getwd()
-
- Cmd.Flags().StringVarP(&archiveDirectoryFlag,
- "directory",
- "d",
- cwd,
- "The directory to store the files into")
-
- Cmd.Flags().StringVarP(&archiveOutputFlag,
- "output",
- "o",
- "{title}.mkv",
- "Name of the output file. If you use the following things in the name, the will get replaced:\n"+
- "\t{title} » Title of the video\n"+
- "\t{series_name} » Name of the series\n"+
- "\t{season_name} » Name of the season\n"+
- "\t{season_number} » Number of the season\n"+
- "\t{episode_number} » Number of the episode\n"+
- "\t{resolution} » Resolution of the video\n"+
- "\t{fps} » Frame Rate of the video\n"+
- "\t{audio} » Audio locale of the video\n"+
- "\t{subtitle} » Subtitle locale of the video")
-
- Cmd.Flags().StringVar(&archiveTempDirFlag,
- "temp",
- os.TempDir(),
- "Directory to store temporary files in")
-
- Cmd.Flags().StringVarP(&archiveMergeFlag,
- "merge",
- "m",
- "auto",
- "Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'")
-
- Cmd.Flags().StringVarP(&archiveCompressFlag,
- "compress",
- "c",
- "",
- "If is set, all output will be compresses into an archive (every url generates a new one). "+
- "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")
-
- Cmd.Flags().StringVarP(&archiveResolutionFlag,
- "resolution",
- "r",
- "best",
- "The video resolution. Can either be specified via the pixels, the abbreviation for pixels, or 'common-use' words\n"+
- "\tAvailable pixels: 1920x1080, 1280x720, 640x480, 480x360, 428x240\n"+
- "\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
- "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
-
- Cmd.Flags().IntVarP(&archiveGoroutinesFlag,
- "goroutines",
- "g",
- runtime.NumCPU(),
- "Number of parallel segment downloads")
-}
-
-func archive(urls []string) error {
- for i, url := range urls {
- utils.Log.SetProcess("Parsing url %d", i+1)
- episodes, err := archiveExtractEpisodes(url)
- if err != nil {
- utils.Log.StopProcess("Failed to parse url %d", i+1)
- if utils.Crunchy.Config.Premium {
- 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/crunchy-labs/crunchy-cli/issues/22 for more information")
- }
- return err
- }
- utils.Log.StopProcess("Parsed url %d", i+1)
-
- var compressFile *os.File
- var c Compress
-
- if archiveCompressFlag != "" {
- compressFile, err = os.Create(utils.GenerateFilename(archiveCompressFlag, ""))
- if err != nil {
- return fmt.Errorf("failed to create archive file: %v", err)
- }
- if strings.HasSuffix(archiveCompressFlag, ".tar") {
- c = NewTarCompress(compressFile)
- } else if strings.HasSuffix(archiveCompressFlag, ".tar.gz") || strings.HasSuffix(archiveCompressFlag, ".tgz") {
- c = NewGzipCompress(compressFile)
- } else if strings.HasSuffix(archiveCompressFlag, ".zip") {
- c = NewZipCompress(compressFile)
- }
- }
-
- for _, season := range episodes {
- utils.Log.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
-
- for j, info := range season {
- utils.Log.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
- j+1,
- info.Title,
- info.Resolution,
- info.FPS,
- info.SeasonNumber,
- info.EpisodeNumber)
- }
- }
- utils.Log.Empty()
-
- for j, season := range episodes {
- for k, info := range season {
- var filename string
- var writeCloser io.WriteCloser
- if c != nil {
- filename = info.FormatString(archiveOutputFlag)
- writeCloser, err = c.NewFile(info)
- if err != nil {
- return fmt.Errorf("failed to pre generate new archive file: %v", err)
- }
- } else {
- dir := info.FormatString(archiveDirectoryFlag)
- if _, err = os.Stat(dir); os.IsNotExist(err) {
- if err = os.MkdirAll(dir, 0777); err != nil {
- return fmt.Errorf("error while creating directory: %v", err)
- }
- }
- filename = utils.GenerateFilename(info.FormatString(archiveOutputFlag), dir)
- writeCloser, err = os.Create(filename)
- if err != nil {
- return fmt.Errorf("failed to create new file: %v", err)
- }
- }
-
- if err = archiveInfo(info, writeCloser, filename); err != nil {
- writeCloser.Close()
- if f, ok := writeCloser.(*os.File); ok {
- os.Remove(f.Name())
- } else {
- c.Close()
- compressFile.Close()
- os.RemoveAll(compressFile.Name())
- }
- return err
- }
- writeCloser.Close()
-
- if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
- utils.Log.Empty()
- }
- }
- }
- if c != nil {
- c.Close()
- }
- if compressFile != nil {
- compressFile.Close()
- }
- }
- return nil
-}
-
-func archiveInfo(info utils.FormatInformation, writeCloser io.WriteCloser, filename string) error {
- utils.Log.Debug("Entering season %d, episode %d with %d additional formats", info.SeasonNumber, info.EpisodeNumber, len(info.AdditionalFormats))
-
- dp, err := createArchiveProgress(info)
- if err != nil {
- return fmt.Errorf("error while setting up downloader: %v", err)
- }
- defer func() {
- if dp.Total != dp.Current {
- fmt.Println()
- }
- }()
-
- rootFile, err := os.CreateTemp("", fmt.Sprintf("%s_*.ts", strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))))
- if err != nil {
- return fmt.Errorf("failed to create temp file: %v", err)
- }
- defer os.Remove(rootFile.Name())
- defer rootFile.Close()
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- downloader := crunchyroll.NewDownloader(ctx, rootFile, archiveGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- // check if the context was cancelled.
- // must be done in to not print any progress messages if ctrl+c was pressed
- if ctx.Err() != nil {
- return nil
- }
-
- if utils.Log.IsDev() {
- dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
- } else {
- dp.Update()
- }
-
- if current == total {
- dp.UpdateMessage("Merging segments", false)
- }
- return nil
- })
- tmp, _ := os.MkdirTemp(archiveTempDirFlag, "crunchy_")
- downloader.TempDir = tmp
-
- sig := make(chan os.Signal, 1)
- signal.Notify(sig, os.Interrupt)
- go func() {
- select {
- case <-sig:
- signal.Stop(sig)
- utils.Log.Err("Exiting... (may take a few seconds)")
- utils.Log.Err("To force exit press ctrl+c (again)")
- cancel()
- // 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
- // directory where the segments are downloaded to will not be deleted
- case <-ctx.Done():
- // this is just here to end the goroutine and prevent it from running forever without a reason
- }
- }()
- utils.Log.Debug("Set up signal catcher")
-
- var additionalDownloaderOpts []string
- var mergeMessage string
- switch archiveMergeFlag {
- case "auto":
- additionalDownloaderOpts = []string{"-vn"}
- for _, format := range info.AdditionalFormats {
- if format.Video.Bandwidth != info.Format.Video.Bandwidth {
- // revoke the changed FFmpegOpts above
- additionalDownloaderOpts = []string{}
- break
- }
- }
- if len(additionalDownloaderOpts) > 0 {
- mergeMessage = "merging audio for additional formats"
- } else {
- mergeMessage = "merging video for additional formats"
- }
- case "audio":
- additionalDownloaderOpts = []string{"-vn"}
- mergeMessage = "merging audio for additional formats"
- case "video":
- mergeMessage = "merging video for additional formats"
- }
-
- utils.Log.Info("Downloading episode `%s` to `%s` (%s)", info.Title, filepath.Base(filename), mergeMessage)
- utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
- utils.Log.Info("\tAudio: %s", info.Audio)
- utils.Log.Info("\tSubtitle: %s", info.Subtitle)
- utils.Log.Info("\tResolution: %spx", info.Resolution)
- utils.Log.Info("\tFPS: %.2f", info.FPS)
-
- var videoFiles, audioFiles, subtitleFiles []string
- defer func() {
- for _, f := range append(append(videoFiles, audioFiles...), subtitleFiles...) {
- os.RemoveAll(f)
- }
- }()
-
- var f []string
- if f, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.Format); err != nil {
- if err != ctx.Err() {
- return fmt.Errorf("error while downloading: %v", err)
- }
- return err
- }
- videoFiles = append(videoFiles, f[0])
-
- if len(additionalDownloaderOpts) == 0 {
- var videos []string
- downloader.FFmpegOpts = additionalDownloaderOpts
- if videos, err = archiveDownloadVideos(downloader, filepath.Base(filename), true, info.AdditionalFormats...); err != nil {
- return fmt.Errorf("error while downloading additional videos: %v", err)
- }
- downloader.FFmpegOpts = []string{}
- videoFiles = append(videoFiles, videos...)
- } else {
- var audios []string
- if audios, err = archiveDownloadVideos(downloader, filepath.Base(filename), false, info.AdditionalFormats...); err != nil {
- return fmt.Errorf("error while downloading additional videos: %v", err)
- }
- audioFiles = append(audioFiles, audios...)
- }
-
- sort.Sort(crunchyUtils.SubtitlesByLocale(info.Format.Subtitles))
-
- sortSubtitles, _ := strconv.ParseBool(os.Getenv("SORT_SUBTITLES"))
- if sortSubtitles && len(archiveLanguagesFlag) > 0 {
- // this sort the subtitle locales after the languages which were specified
- // with the `archiveLanguagesFlag` flag
- for _, language := range archiveLanguagesFlag {
- for i, subtitle := range info.Format.Subtitles {
- if subtitle.Locale == crunchyroll.LOCALE(language) {
- info.Format.Subtitles = append([]*crunchyroll.Subtitle{subtitle}, append(info.Format.Subtitles[:i], info.Format.Subtitles[i+1:]...)...)
- break
- }
- }
- }
- }
-
- var subtitles []string
- if subtitles, err = archiveDownloadSubtitles(filepath.Base(filename), info.Format.Subtitles...); err != nil {
- return fmt.Errorf("error while downloading subtitles: %v", err)
- }
- subtitleFiles = append(subtitleFiles, subtitles...)
-
- if err = archiveFFmpeg(ctx, writeCloser, videoFiles, audioFiles, subtitleFiles); err != nil {
- return fmt.Errorf("failed to merge files: %v", err)
- }
-
- dp.UpdateMessage("Download finished", false)
-
- signal.Stop(sig)
- utils.Log.Debug("Stopped signal catcher")
-
- utils.Log.Empty()
-
- return nil
-}
-
-func createArchiveProgress(info utils.FormatInformation) (*commands.DownloadProgress, error) {
- var progressCount int
- if err := info.Format.InitVideo(); err != nil {
- return nil, fmt.Errorf("error while initializing a video: %v", err)
- }
- // + number of segments a video has +1 is for merging
- progressCount += int(info.Format.Video.Chunklist.Count()) + 1
- for _, f := range info.AdditionalFormats {
- if f == info.Format {
- continue
- }
-
- if err := f.InitVideo(); err != nil {
- return nil, err
- }
- // + number of segments a video has +1 is for merging
- progressCount += int(f.Video.Chunklist.Count()) + 1
- }
-
- dp := &commands.DownloadProgress{
- Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
- Message: "Downloading video",
- // number of segments a video +1 is for the success message
- Total: progressCount + 1,
- Dev: utils.Log.IsDev(),
- Quiet: utils.Log.(*commands.Logger).IsQuiet(),
- }
- if utils.Log.IsDev() {
- dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
- }
-
- return dp, nil
-}
-
-func archiveDownloadVideos(downloader crunchyroll.Downloader, filename string, video bool, formats ...*crunchyroll.Format) ([]string, error) {
- var files []string
-
- for _, format := range formats {
- var name string
- if video {
- name = fmt.Sprintf("%s_%s_video_*.ts", filename, format.AudioLocale)
- } else {
- name = fmt.Sprintf("%s_%s_audio_*.aac", filename, format.AudioLocale)
- }
-
- f, err := os.CreateTemp("", name)
- if err != nil {
- return nil, err
- }
- files = append(files, f.Name())
-
- downloader.Writer = f
- if err = format.Download(downloader); err != nil {
- f.Close()
- for _, file := range files {
- os.Remove(file)
- }
- return nil, err
- }
- f.Close()
-
- utils.Log.Debug("Downloaded '%s' video", format.AudioLocale)
- }
-
- return files, nil
-}
-
-func archiveDownloadSubtitles(filename string, subtitles ...*crunchyroll.Subtitle) ([]string, error) {
- var files []string
-
- for _, subtitle := range subtitles {
- if !utils.ElementInSlice(string(subtitle.Locale), archiveSubLanguagesFlag) {
- continue
- }
-
- f, err := os.CreateTemp("", fmt.Sprintf("%s_%s_subtitle_*.ass", filename, subtitle.Locale))
- if err != nil {
- return nil, err
- }
- files = append(files, f.Name())
-
- if err := subtitle.Save(f); err != nil {
- f.Close()
- for _, file := range files {
- os.Remove(file)
- }
- return nil, err
- }
- f.Close()
-
- utils.Log.Debug("Downloaded '%s' subtitles", subtitle.Locale)
- }
-
- return files, nil
-}
-
-func archiveFFmpeg(ctx context.Context, dst io.Writer, videoFiles, audioFiles, subtitleFiles []string) error {
- var input, maps, metadata []string
- re := regexp.MustCompile(`(?m)_([a-z]{2}-([A-Z]{2}|[0-9]{3}))_(video|audio|subtitle)`)
- // https://github.com/crunchy-labs/crunchy-cli/issues/32
- videoLength32Fix := regexp.MustCompile(`Duration:\s?(\d+):(\d+):(\d+).(\d+),`)
-
- videoLength := [4]int{0, 0, 0, 0}
-
- for i, video := range videoFiles {
- input = append(input, "-i", video)
- maps = append(maps, "-map", strconv.Itoa(i))
- 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("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("title=%s", crunchyUtils.LocaleLanguage(locale)))
-
- var errBuf bytes.Buffer
- cmd := exec.CommandContext(ctx, "ffmpeg", "-i", video)
- cmd.Stderr = &errBuf
- cmd.Run()
-
- matches := videoLength32Fix.FindStringSubmatch(errBuf.String())
- hours, _ := strconv.Atoi(matches[1])
- minutes, _ := strconv.Atoi(matches[2])
- seconds, _ := strconv.Atoi(matches[3])
- millis, _ := strconv.Atoi(matches[4])
-
- if hours > videoLength[0] {
- videoLength = [4]int{hours, minutes, seconds, millis}
- } else if hours == videoLength[0] && minutes > videoLength[1] {
- videoLength = [4]int{hours, minutes, seconds, millis}
- } else if hours == videoLength[0] && minutes == videoLength[1] && seconds > videoLength[2] {
- videoLength = [4]int{hours, minutes, seconds, millis}
- } else if hours == videoLength[0] && minutes == videoLength[1] && seconds == videoLength[2] && millis > videoLength[3] {
- videoLength = [4]int{hours, minutes, seconds, millis}
- }
- }
-
- for i, audio := range audioFiles {
- input = append(input, "-i", audio)
- maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles))+":1")
- locale := crunchyroll.LOCALE(re.FindStringSubmatch(audio)[1])
- metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i+len(videoFiles)), fmt.Sprintf("language=%s", locale))
- metadata = append(metadata, fmt.Sprintf("-metadata:s:a:%d", i+len(videoFiles)), fmt.Sprintf("title=%s", crunchyUtils.LocaleLanguage(locale)))
- }
-
- for i, subtitle := range subtitleFiles {
- input = append(input, "-i", subtitle)
- maps = append(maps, "-map", strconv.Itoa(i+len(videoFiles)+len(audioFiles)))
- 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("title=%s", crunchyUtils.LocaleLanguage(locale)))
- }
-
- commandOptions := []string{"-y"}
- commandOptions = append(commandOptions, input...)
- commandOptions = append(commandOptions, maps...)
- commandOptions = append(commandOptions, metadata...)
- // we have to create a temporary file here because it must be seekable
- // for ffmpeg.
- // ffmpeg could write to dst too, but this would require to re-encode
- // the audio which results in much higher time and resource consumption
- // (0-1 second with the temp file, ~20 seconds with re-encoding on my system)
- file, err := os.CreateTemp("", "")
- if err != nil {
- return err
- }
- file.Close()
- defer os.Remove(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
- if utils.Log.IsDev() {
- var debugOptions []string
-
- for _, option := range commandOptions {
- if strings.HasPrefix(option, "title=") {
- debugOptions = append(debugOptions, "title=\""+strings.TrimPrefix(option, "title=")+"\"")
- } else if strings.HasPrefix(option, "language=") {
- debugOptions = append(debugOptions, "language=\""+strings.TrimPrefix(option, "language=")+"\"")
- } else if strings.Contains(option, " ") {
- debugOptions = append(debugOptions, "\""+option+"\"")
- } else {
- debugOptions = append(debugOptions, option)
- }
- }
- utils.Log.Debug("FFmpeg merge command: ffmpeg %s", strings.Join(debugOptions, " "))
- }
-
- var errBuf bytes.Buffer
- cmd := exec.CommandContext(ctx, "ffmpeg", commandOptions...)
- cmd.Stderr = &errBuf
- if err = cmd.Run(); err != nil {
- return fmt.Errorf(errBuf.String())
- }
-
- file, err = os.Open(file.Name())
- if err != nil {
- return err
- }
- defer file.Close()
-
- errBuf.Reset()
- cmd = exec.CommandContext(ctx, "ffmpeg", "-i", file.Name())
- cmd.Stderr = &errBuf
- cmd.Run()
-
- matches := videoLength32Fix.FindStringSubmatch(errBuf.String())
- hours, _ := strconv.Atoi(matches[1])
- minutes, _ := strconv.Atoi(matches[2])
- seconds, _ := strconv.Atoi(matches[3])
- millis, _ := strconv.Atoi(matches[4])
-
- var reencode bool
- if hours > videoLength[0] {
- reencode = true
- } else if hours == videoLength[0] && minutes > videoLength[1] {
- reencode = true
- } else if hours == videoLength[0] && minutes == videoLength[1] && seconds > videoLength[2] {
- reencode = true
- } else if hours == videoLength[0] && minutes == videoLength[1] && seconds == videoLength[2] && millis > videoLength[3] {
- reencode = true
- }
-
- // very dirty solution to https://github.com/crunchy-labs/crunchy-cli/issues/32.
- // this might get triggered when not needed but there is currently no easy way to
- // bypass this unwanted triggering
- if reencode {
- utils.Log.Debug("Re-encode to short video length")
-
- file.Close()
-
- tmpFile, _ := os.CreateTemp("", filepath.Base(file.Name())+"-32_fix")
- tmpFile.Close()
-
- errBuf.Reset()
- cmd = exec.CommandContext(ctx, "ffmpeg",
- "-y",
- "-i", file.Name(),
- "-map", "0",
- "-c", "copy",
- "-disposition:s:0", "0",
- "-t", fmt.Sprintf("%02d:%02d:%02d.%d", videoLength[0], videoLength[1], videoLength[2], videoLength[3]),
- "-f", "matroska",
- tmpFile.Name())
- cmd.Stderr = &errBuf
- if err = cmd.Run(); err != nil {
- return fmt.Errorf(errBuf.String())
- }
-
- os.Remove(file.Name())
- os.Rename(tmpFile.Name(), file.Name())
-
- file, err = os.Open(file.Name())
- if err != nil {
- return err
- }
- defer file.Close()
- }
-
- _, err = bufio.NewWriter(dst).ReadFrom(file)
- return err
-}
-
-func archiveExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
- var hasJapanese bool
- languagesAsLocale := []crunchyroll.LOCALE{crunchyroll.JP}
- for _, language := range archiveLanguagesFlag {
- locale := crunchyroll.LOCALE(language)
- if locale == crunchyroll.JP {
- hasJapanese = true
- } else {
- languagesAsLocale = append(languagesAsLocale, locale)
- }
- }
-
- if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
- return nil, fmt.Errorf("archiving episodes by url is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- }
-
- episodes, err := utils.ExtractEpisodes(url, languagesAsLocale...)
- if err != nil {
- return nil, err
- }
-
- if !hasJapanese && len(episodes[1:]) == 0 {
- return nil, fmt.Errorf("no episodes found")
- }
-
- for i, eps := range episodes {
- if len(eps) == 0 {
- utils.Log.SetProcess("%s has no matching episodes", languagesAsLocale[i])
- } else if len(episodes[0]) > len(eps) {
- utils.Log.SetProcess("%s has %d less episodes than existing in japanese (%d)", languagesAsLocale[i], len(episodes[0])-len(eps), len(episodes[0]))
- }
- }
-
- if !hasJapanese {
- episodes = episodes[1:]
- }
-
- eps := make(map[int]map[int]*utils.FormatInformation)
- for _, lang := range episodes {
- for _, season := range crunchyUtils.SortEpisodesBySeason(lang) {
- if _, ok := eps[season[0].SeasonNumber]; !ok {
- eps[season[0].SeasonNumber] = map[int]*utils.FormatInformation{}
- }
- for _, episode := range season {
- format, err := episode.GetFormat(archiveResolutionFlag, "", false)
- if err != nil {
- return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
- }
-
- if _, ok := eps[episode.SeasonNumber][episode.EpisodeNumber]; !ok {
- eps[episode.SeasonNumber][episode.EpisodeNumber] = &utils.FormatInformation{
- Format: format,
- AdditionalFormats: make([]*crunchyroll.Format, 0),
-
- Title: episode.Title,
- SeriesName: episode.SeriesTitle,
- SeasonName: episode.SeasonTitle,
- SeasonNumber: episode.SeasonNumber,
- EpisodeNumber: episode.EpisodeNumber,
- Resolution: format.Video.Resolution,
- FPS: format.Video.FrameRate,
- Audio: format.AudioLocale,
- }
- } else {
- eps[episode.SeasonNumber][episode.EpisodeNumber].AdditionalFormats = append(eps[episode.SeasonNumber][episode.EpisodeNumber].AdditionalFormats, format)
- }
- }
- }
- }
-
- var infoFormat [][]utils.FormatInformation
- for _, e := range eps {
- var tmpFormatInfo []utils.FormatInformation
-
- var keys []int
- for episodeNumber := range e {
- keys = append(keys, episodeNumber)
- }
- sort.Ints(keys)
-
- for _, key := range keys {
- tmpFormatInfo = append(tmpFormatInfo, *e[key])
- }
-
- infoFormat = append(infoFormat, tmpFormatInfo)
- }
-
- return infoFormat, nil
-}
diff --git a/cli/commands/archive/compress.go b/cli/commands/archive/compress.go
deleted file mode 100644
index e0b9ad4..0000000
--- a/cli/commands/archive/compress.go
+++ /dev/null
@@ -1,136 +0,0 @@
-package archive
-
-import (
- "archive/tar"
- "archive/zip"
- "bytes"
- "compress/gzip"
- "fmt"
- "github.com/crunchy-labs/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
-}
diff --git a/cli/commands/download/download.go b/cli/commands/download/download.go
deleted file mode 100644
index 39273d0..0000000
--- a/cli/commands/download/download.go
+++ /dev/null
@@ -1,368 +0,0 @@
-package download
-
-import (
- "context"
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- crunchyUtils "github.com/crunchy-labs/crunchyroll-go/v3/utils"
- "github.com/grafov/m3u8"
- "github.com/spf13/cobra"
- "math"
- "os"
- "os/signal"
- "path/filepath"
- "runtime"
- "sort"
- "strconv"
- "strings"
-)
-
-var (
- downloadAudioFlag string
- downloadSubtitleFlag string
-
- downloadDirectoryFlag string
- downloadOutputFlag string
- downloadTempDirFlag string
-
- downloadResolutionFlag string
-
- downloadGoroutinesFlag int
-)
-
-var Cmd = &cobra.Command{
- Use: "download",
- Short: "Download a video",
- Args: cobra.MinimumNArgs(1),
-
- PreRunE: func(cmd *cobra.Command, args []string) error {
- utils.Log.Debug("Validating arguments")
-
- if filepath.Ext(downloadOutputFlag) != ".ts" {
- if !utils.HasFFmpeg() {
- 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)
- } else {
- utils.Log.Debug("Custom file ending '%s' (ffmpeg is installed)", filepath.Ext(downloadOutputFlag))
- }
- }
-
- if downloadAudioFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadAudioFlag)) {
- return fmt.Errorf("%s is not a valid audio locale. Choose from: %s", downloadAudioFlag, strings.Join(utils.LocalesAsStrings(), ", "))
- } else if downloadSubtitleFlag != "" && !crunchyUtils.ValidateLocale(crunchyroll.LOCALE(downloadSubtitleFlag)) {
- return fmt.Errorf("%s is not a valid subtitle locale. Choose from: %s", downloadSubtitleFlag, strings.Join(utils.LocalesAsStrings(), ", "))
- }
- utils.Log.Debug("Locales: audio: %s / subtitle: %s", downloadAudioFlag, downloadSubtitleFlag)
-
- switch downloadResolutionFlag {
- case "1080p", "720p", "480p", "360p":
- intRes, _ := strconv.ParseFloat(strings.TrimSuffix(downloadResolutionFlag, "p"), 84)
- downloadResolutionFlag = fmt.Sprintf("%.0fx%s", math.Ceil(intRes*(float64(16)/float64(9))), strings.TrimSuffix(downloadResolutionFlag, "p"))
- case "240p":
- // 240p would round up to 427x240 if used in the case statement above, so it has to be handled separately
- downloadResolutionFlag = "428x240"
- case "1920x1080", "1280x720", "640x480", "480x360", "428x240", "best", "worst":
- default:
- return fmt.Errorf("'%s' is not a valid resolution", downloadResolutionFlag)
- }
- utils.Log.Debug("Using resolution '%s'", downloadResolutionFlag)
-
- return nil
- },
- RunE: func(cmd *cobra.Command, args []string) error {
- if err := commands.LoadCrunchy(); err != nil {
- return err
- }
-
- return download(args)
- },
-}
-
-func init() {
- Cmd.Flags().StringVarP(&downloadAudioFlag, "audio",
- "a",
- "",
- "The locale of the audio. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
- Cmd.Flags().StringVarP(&downloadSubtitleFlag,
- "subtitle",
- "s",
- "",
- "The locale of the subtitle. Available locales: "+strings.Join(utils.LocalesAsStrings(), ", "))
-
- cwd, _ := os.Getwd()
- Cmd.Flags().StringVarP(&downloadDirectoryFlag,
- "directory",
- "d",
- cwd,
- "The directory to download the file(s) into")
- Cmd.Flags().StringVarP(&downloadOutputFlag,
- "output",
- "o",
- "{title}.ts",
- "Name of the output file. "+
- "If you use the following things in the name, the will get replaced:\n"+
- "\t{title} » Title of the video\n"+
- "\t{series_name} » Name of the series\n"+
- "\t{season_name} » Name of the season\n"+
- "\t{season_number} » Number of the season\n"+
- "\t{episode_number} » Number of the episode\n"+
- "\t{resolution} » Resolution of the video\n"+
- "\t{fps} » Frame Rate of the video\n"+
- "\t{audio} » Audio locale of the video\n"+
- "\t{subtitle} » Subtitle locale of the video")
- Cmd.Flags().StringVar(&downloadTempDirFlag,
- "temp",
- os.TempDir(),
- "Directory to store temporary files in")
-
- Cmd.Flags().StringVarP(&downloadResolutionFlag,
- "resolution",
- "r",
- "best",
- "The video resolution. Can either be specified via the pixels, the abbreviation for pixels, or 'common-use' words\n"+
- "\tAvailable pixels: 1920x1080, 1280x720, 640x480, 480x360, 428x240\n"+
- "\tAvailable abbreviations: 1080p, 720p, 480p, 360p, 240p\n"+
- "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)")
-
- Cmd.Flags().IntVarP(&downloadGoroutinesFlag,
- "goroutines",
- "g",
- runtime.NumCPU(),
- "Sets how many parallel segment downloads should be used")
-}
-
-func download(urls []string) error {
- for i, url := range urls {
- utils.Log.SetProcess("Parsing url %d", i+1)
- episodes, err := downloadExtractEpisodes(url)
- if err != nil {
- utils.Log.StopProcess("Failed to parse url %d", i+1)
- if utils.Crunchy.Config.Premium {
- 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/crunchy-labs/crunchy-cli/issues/22 for more information")
- }
- return err
- }
- utils.Log.StopProcess("Parsed url %d", i+1)
-
- for _, season := range episodes {
- utils.Log.Info("%s Season %d", season[0].SeriesName, season[0].SeasonNumber)
-
- for j, info := range season {
- utils.Log.Info("\t%d. %s » %spx, %.2f FPS (S%02dE%02d)",
- j+1,
- info.Title,
- info.Resolution,
- info.FPS,
- info.SeasonNumber,
- info.EpisodeNumber)
- }
- }
- utils.Log.Empty()
-
- for j, season := range episodes {
- for k, info := range season {
- dir := info.FormatString(downloadDirectoryFlag)
- if _, err = os.Stat(dir); os.IsNotExist(err) {
- if err = os.MkdirAll(dir, 0777); err != nil {
- return fmt.Errorf("error while creating directory: %v", err)
- }
- }
- file, err := os.Create(utils.GenerateFilename(info.FormatString(downloadOutputFlag), dir))
- if err != nil {
- return fmt.Errorf("failed to create output file: %v", err)
- }
-
- if err = downloadInfo(info, file); err != nil {
- file.Close()
- os.Remove(file.Name())
- return err
- }
- file.Close()
-
- if i != len(urls)-1 || j != len(episodes)-1 || k != len(season)-1 {
- utils.Log.Empty()
- }
- }
- }
- }
- return nil
-}
-
-func downloadInfo(info utils.FormatInformation, file *os.File) error {
- utils.Log.Debug("Entering season %d, episode %d", info.SeasonNumber, info.EpisodeNumber)
-
- if err := info.Format.InitVideo(); err != nil {
- return fmt.Errorf("error while initializing the video: %v", err)
- }
-
- dp := &commands.DownloadProgress{
- Prefix: utils.Log.(*commands.Logger).InfoLog.Prefix(),
- Message: "Downloading video",
- // number of segments a video has +2 is for merging and the success message
- Total: int(info.Format.Video.Chunklist.Count()) + 2,
- Dev: utils.Log.IsDev(),
- Quiet: utils.Log.(*commands.Logger).IsQuiet(),
- }
- if utils.Log.IsDev() {
- dp.Prefix = utils.Log.(*commands.Logger).DebugLog.Prefix()
- }
- defer func() {
- if dp.Total != dp.Current {
- fmt.Println()
- }
- }()
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- downloader := crunchyroll.NewDownloader(ctx, file, downloadGoroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
- // check if the context was cancelled.
- // must be done in to not print any progress messages if ctrl+c was pressed
- if ctx.Err() != nil {
- return nil
- }
-
- if utils.Log.IsDev() {
- dp.UpdateMessage(fmt.Sprintf("Downloading %d/%d (%.2f%%) » %s", current, total, float32(current)/float32(total)*100, segment.URI), false)
- } else {
- dp.Update()
- }
-
- if current == total {
- dp.UpdateMessage("Merging segments", false)
- }
- return nil
- })
- tmp, _ := os.MkdirTemp(downloadTempDirFlag, "crunchy_")
- downloader.TempDir = tmp
- if utils.HasFFmpeg() {
- downloader.FFmpegOpts = make([]string, 0)
- }
-
- sig := make(chan os.Signal, 1)
- signal.Notify(sig, os.Interrupt)
- go func() {
- select {
- case <-sig:
- signal.Stop(sig)
- utils.Log.Err("Exiting... (may take a few seconds)")
- utils.Log.Err("To force exit press ctrl+c (again)")
- cancel()
- // 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
- // directory where the segments are downloaded to will not be deleted
- case <-ctx.Done():
- // this is just here to end the goroutine and prevent it from running forever without a reason
- }
- }()
- utils.Log.Debug("Set up signal catcher")
-
- utils.Log.Info("Downloading episode `%s` to `%s`", info.Title, filepath.Base(file.Name()))
- utils.Log.Info("\tEpisode: S%02dE%02d", info.SeasonNumber, info.EpisodeNumber)
- utils.Log.Info("\tAudio: %s", info.Audio)
- utils.Log.Info("\tSubtitle: %s", info.Subtitle)
- utils.Log.Info("\tResolution: %spx", info.Resolution)
- utils.Log.Info("\tFPS: %.2f", info.FPS)
- if err := info.Format.Download(downloader); err != nil {
- return fmt.Errorf("error while downloading: %v", err)
- }
-
- dp.UpdateMessage("Download finished", false)
-
- signal.Stop(sig)
- utils.Log.Debug("Stopped signal catcher")
-
- utils.Log.Empty()
-
- return nil
-}
-
-func downloadExtractEpisodes(url string) ([][]utils.FormatInformation, error) {
- var episodes [][]*crunchyroll.Episode
- var final []*crunchyroll.Episode
-
- if downloadAudioFlag != "" {
- if _, ok := crunchyroll.ParseBetaEpisodeURL(url); ok {
- return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- } else if _, _, _, _, ok := crunchyroll.ParseEpisodeURL(url); ok {
- return nil, fmt.Errorf("downloading episodes by url and specifying a language is no longer supported (thx crunchyroll). use the series url instead and filter after the given episode (https://github.com/crunchy-labs/crunchy-cli/wiki/Cli#filter)")
- }
-
- var err error
- episodes, err = utils.ExtractEpisodes(url, crunchyroll.JP, crunchyroll.LOCALE(downloadAudioFlag))
- if err != nil {
- return nil, err
- }
- japanese := episodes[0]
- custom := episodes[1]
-
- sort.Sort(crunchyUtils.EpisodesByNumber(japanese))
- sort.Sort(crunchyUtils.EpisodesByNumber(custom))
-
- var errMessages []string
-
- if len(japanese) == 0 || len(japanese) == len(custom) {
- final = custom
- } else {
- for _, jp := range japanese {
- before := len(final)
- for _, episode := range custom {
- if jp.SeasonNumber == episode.SeasonNumber && jp.EpisodeNumber == episode.EpisodeNumber {
- final = append(final, episode)
- }
- }
- if before == len(final) {
- errMessages = append(errMessages, fmt.Sprintf("%s has no %s audio, using %s as fallback", jp.Title, crunchyroll.LOCALE(downloadAudioFlag), crunchyroll.JP))
- final = append(final, jp)
- }
- }
- }
-
- if len(errMessages) > 10 {
- for _, msg := range errMessages[:10] {
- utils.Log.SetProcess(msg)
- }
- utils.Log.SetProcess("... and %d more", len(errMessages)-10)
- } else {
- for _, msg := range errMessages {
- utils.Log.SetProcess(msg)
- }
- }
- } else {
- var err error
- episodes, err = utils.ExtractEpisodes(url)
- if err != nil {
- return nil, err
- } else if len(episodes) == 0 {
- return nil, fmt.Errorf("cannot find any episode")
- }
- final = episodes[0]
- }
-
- var infoFormat [][]utils.FormatInformation
- for _, season := range crunchyUtils.SortEpisodesBySeason(final) {
- tmpFormatInformation := make([]utils.FormatInformation, 0)
- for _, episode := range season {
- format, err := episode.GetFormat(downloadResolutionFlag, crunchyroll.LOCALE(downloadSubtitleFlag), true)
- if err != nil {
- return nil, fmt.Errorf("error while receiving format for %s: %v", episode.Title, err)
- }
- tmpFormatInformation = append(tmpFormatInformation, utils.FormatInformation{
- Format: format,
-
- Title: episode.Title,
- SeriesName: episode.SeriesTitle,
- SeasonName: episode.SeasonTitle,
- SeasonNumber: episode.SeasonNumber,
- EpisodeNumber: episode.EpisodeNumber,
- Resolution: format.Video.Resolution,
- FPS: format.Video.FrameRate,
- Audio: format.AudioLocale,
- })
- }
- infoFormat = append(infoFormat, tmpFormatInformation)
- }
- return infoFormat, nil
-}
diff --git a/cli/commands/info/info.go b/cli/commands/info/info.go
deleted file mode 100644
index 650d8c7..0000000
--- a/cli/commands/info/info.go
+++ /dev/null
@@ -1,40 +0,0 @@
-package info
-
-import (
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/utils"
- crunchyUtils "github.com/crunchy-labs/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
-}
diff --git a/cli/commands/logger.go b/cli/commands/logger.go
deleted file mode 100644
index 89a4717..0000000
--- a/cli/commands/logger.go
+++ /dev/null
@@ -1,196 +0,0 @@
-package commands
-
-import (
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "io"
- "log"
- "os"
- "runtime"
- "strings"
- "sync"
- "time"
-)
-
-var prefix, progressDown, progressDownFinish string
-
-func initPrefixBecauseWindowsSucksBallsHard() {
- // dear windows user, please change to a good OS, linux in the best case.
- // MICROSHIT DOES NOT GET IT DONE TO SHOW THE SYMBOLS IN THE ELSE CLAUSE
- // CORRECTLY. NOT IN THE CMD NOR POWERSHELL. WHY TF, IT IS ONE OF THE MOST
- // PROFITABLE COMPANIES ON THIS PLANET AND CANNOT SHOW A PROPER UTF-8 SYMBOL
- // IN THEIR OWN PRODUCT WHICH GETS USED MILLION TIMES A DAY
- if runtime.GOOS == "windows" {
- prefix = "=>"
- progressDown = "|"
- progressDownFinish = "->"
- } else {
- prefix = "➞"
- progressDown = "↓"
- progressDownFinish = "↳"
- }
-}
-
-type progress struct {
- message string
- stop bool
-}
-
-func NewLogger(debug, info, err bool) *Logger {
- initPrefixBecauseWindowsSucksBallsHard()
-
- debugLog, infoLog, errLog := log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0), log.New(io.Discard, prefix+" ", 0)
-
- if debug {
- debugLog.SetOutput(os.Stdout)
- }
- if info {
- infoLog.SetOutput(os.Stdout)
- }
- if err {
- errLog.SetOutput(os.Stderr)
- }
-
- if debug {
- debugLog = log.New(debugLog.Writer(), "[debug] ", 0)
- infoLog = log.New(infoLog.Writer(), "[info] ", 0)
- errLog = log.New(errLog.Writer(), "[err] ", 0)
- }
-
- return &Logger{
- DebugLog: debugLog,
- InfoLog: infoLog,
- ErrLog: errLog,
-
- devView: debug,
- }
-}
-
-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
-}
-
-func (l *Logger) IsQuiet() bool {
- return l.DebugLog.Writer() == io.Discard && l.InfoLog.Writer() == io.Discard && l.ErrLog.Writer() == io.Discard
-}
-
-func (l *Logger) Debug(format string, v ...interface{}) {
- l.DebugLog.Printf(format, v...)
-}
-
-func (l *Logger) Info(format string, v ...interface{}) {
- l.InfoLog.Printf(format, v...)
-}
-
-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...)
-}
-
-func (l *Logger) Empty() {
- if !l.devView && l.InfoLog.Writer() != io.Discard {
- fmt.Println("")
- }
-}
-
-func (l *Logger) SetProcess(format string, v ...interface{}) {
- if l.InfoLog.Writer() == io.Discard {
- return
- } else if l.devView {
- l.Debug(format, v...)
- return
- }
-
- initialMessage := fmt.Sprintf(format, v...)
-
- p := progress{
- message: initialMessage,
- }
-
- l.lock.Lock()
- if l.done != nil {
- l.progress <- p
- return
- } else {
- l.progress = make(chan progress, 1)
- l.progress <- p
- l.done = make(chan interface{})
- }
-
- go func() {
- states := []string{"-", "\\", "|", "/"}
-
- var count int
-
- for i := 0; ; i++ {
- select {
- case p := <-l.progress:
- if p.stop {
- fmt.Printf("\r" + strings.Repeat(" ", len(prefix)+len(initialMessage)))
- if count > 1 {
- fmt.Printf("\r%s %s\n", progressDownFinish, p.message)
- } else {
- fmt.Printf("\r%s %s\n", prefix, p.message)
- }
-
- if l.done != nil {
- l.done <- nil
- }
- l.progress = nil
-
- l.lock.Unlock()
- return
- } else {
- if count > 0 {
- fmt.Printf("\r%s %s\n", progressDown, p.message)
- }
- l.progress = make(chan progress, 1)
-
- count++
-
- fmt.Printf("\r%s %s", states[i/10%4], initialMessage)
- l.lock.Unlock()
- }
- default:
- if i%10 == 0 {
- fmt.Printf("\r%s %s", states[i/10%4], initialMessage)
- }
- time.Sleep(35 * time.Millisecond)
- }
- }
- }()
-}
-
-func (l *Logger) StopProcess(format string, v ...interface{}) {
- if l.InfoLog.Writer() == io.Discard {
- return
- } else if l.devView {
- l.Debug(format, v...)
- return
- }
-
- l.lock.Lock()
- l.progress <- progress{
- message: fmt.Sprintf(format, v...),
- stop: true,
- }
- <-l.done
- l.done = nil
-}
diff --git a/cli/commands/login/login.go b/cli/commands/login/login.go
deleted file mode 100644
index 9fce181..0000000
--- a/cli/commands/login/login.go
+++ /dev/null
@@ -1,159 +0,0 @@
-package login
-
-import (
- "bytes"
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- "github.com/spf13/cobra"
- "os"
-)
-
-var (
- loginPersistentFlag bool
- loginEncryptFlag bool
-
- loginSessionIDFlag bool
- loginRefreshTokenFlag 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 loginRefreshTokenFlag {
- return loginRefreshToken(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(&loginRefreshTokenFlag,
- "refresh-token",
- false,
- "Use a refresh token to login instead of username and password. Can be obtained by copying the `etp-rt` cookie from beta.crunchyroll.com")
-
- Cmd.MarkFlagsMutuallyExclusive("session-id", "refresh-token")
-}
-
-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")
- utils.Log.Warn("Logging in with session id is deprecated and not very reliable. Consider choosing another option (if it fails)")
- 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 loginRefreshToken(refreshToken string) error {
- utils.Log.Debug("Logging in via refresh token")
- var c *crunchyroll.Crunchyroll
- var err error
- if c, err = crunchyroll.LoginWithRefreshToken(refreshToken, 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
-}
diff --git a/cli/commands/unix.go b/cli/commands/unix.go
deleted file mode 100644
index ec69180..0000000
--- a/cli/commands/unix.go
+++ /dev/null
@@ -1,48 +0,0 @@
-//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos
-
-package commands
-
-import (
- "bufio"
- "os"
- "os/exec"
- "syscall"
-)
-
-// https://github.com/bgentry/speakeasy/blob/master/speakeasy_unix.go
-var stty string
-
-func init() {
- var err error
- if stty, err = exec.LookPath("stty"); err != nil {
- panic(err)
- }
-}
-
-func ReadLineSilent() ([]byte, error) {
- pid, err := setEcho(false)
- if err != nil {
- return nil, err
- }
- defer setEcho(true)
-
- syscall.Wait4(pid, nil, 0, nil)
-
- l, _, err := bufio.NewReader(os.Stdin).ReadLine()
- return l, err
-}
-
-func setEcho(on bool) (pid int, err error) {
- fds := []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()}
-
- if on {
- pid, err = syscall.ForkExec(stty, []string{"stty", "echo"}, &syscall.ProcAttr{Files: fds})
- } else {
- pid, err = syscall.ForkExec(stty, []string{"stty", "-echo"}, &syscall.ProcAttr{Files: fds})
- }
-
- if err != nil {
- return 0, err
- }
- return
-}
diff --git a/cli/commands/update/update.go b/cli/commands/update/update.go
deleted file mode 100644
index 6608f26..0000000
--- a/cli/commands/update/update.go
+++ /dev/null
@@ -1,151 +0,0 @@
-package update
-
-import (
- "encoding/json"
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/utils"
- "github.com/spf13/cobra"
- "io"
- "os"
- "os/exec"
- "path"
- "runtime"
- "strings"
-)
-
-var (
- updateInstallFlag bool
-)
-
-var Cmd = &cobra.Command{
- Use: "update",
- Short: "Check if updates are available",
- Args: cobra.MaximumNArgs(0),
-
- RunE: func(cmd *cobra.Command, args []string) error {
- return update()
- },
-}
-
-func init() {
- Cmd.Flags().BoolVarP(&updateInstallFlag,
- "install",
- "i",
- false,
- "If set and a new version is available, the new version gets installed")
-}
-
-func update() error {
- var release map[string]interface{}
-
- resp, err := utils.Client.Get("https://api.github.com/repos/crunchy-labs/crunchy-cli/releases/latest")
- if err != nil {
- return err
- }
- defer resp.Body.Close()
- if err = json.NewDecoder(resp.Body).Decode(&release); err != nil {
- return err
- }
- releaseVersion := strings.TrimPrefix(release["tag_name"].(string), "v")
-
- if utils.Version == "development" {
- utils.Log.Info("Development version, update service not available")
- return nil
- }
-
- latestRelease := strings.SplitN(releaseVersion, ".", 4)
- if len(latestRelease) != 3 {
- return fmt.Errorf("latest tag name (%s) is not parsable", releaseVersion)
- }
-
- internalVersion := strings.SplitN(utils.Version, ".", 4)
- if len(internalVersion) != 3 {
- return fmt.Errorf("internal version (%s) is not parsable", utils.Version)
- }
-
- utils.Log.Info("Installed version is %s", utils.Version)
-
- var hasUpdate bool
- for i := 0; i < 3; i++ {
- if latestRelease[i] < internalVersion[i] {
- utils.Log.Info("Local version is newer than version in latest release (%s)", releaseVersion)
- return nil
- } else if latestRelease[i] > internalVersion[i] {
- hasUpdate = true
- }
- }
-
- if !hasUpdate {
- utils.Log.Info("Version is up-to-date")
- return nil
- }
-
- utils.Log.Info("A new version is available (%s): https://github.com/crunchy-labs/crunchy-cli/releases/tag/v%s", releaseVersion, releaseVersion)
-
- if updateInstallFlag {
- if runtime.GOARCH != "amd64" {
- return fmt.Errorf("invalid architecture found (%s), only amd64 is currently supported for automatic updating. "+
- "You have to update manually (https://github.com/crunchy-labs/crunchy-cli)", runtime.GOARCH)
- }
-
- var downloadFile string
- switch runtime.GOOS {
- case "linux":
- pacmanCommand := exec.Command("pacman -Q crunchy-cli")
- if pacmanCommand.Run() == nil && pacmanCommand.ProcessState.Success() {
- 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
- }
- downloadFile = fmt.Sprintf("crunchy-v%s_linux", releaseVersion)
- case "darwin":
- downloadFile = fmt.Sprintf("crunchy-v%s_darwin", releaseVersion)
- case "windows":
- downloadFile = fmt.Sprintf("crunchy-v%s_windows.exe", releaseVersion)
- default:
- return fmt.Errorf("invalid operation system found (%s), only linux, windows and darwin / macos are currently supported. "+
- "You have to update manually (https://github.com/crunchy-labs/crunchy-cli", runtime.GOOS)
- }
-
- executePath := os.Args[0]
- var perms os.FileInfo
- // check if the path is relative, absolute or non (if so, the executable must be in PATH)
- if strings.HasPrefix(executePath, "."+string(os.PathSeparator)) || path.IsAbs(executePath) {
- if perms, err = os.Stat(os.Args[0]); err != nil {
- return err
- }
- } else {
- executePath, err = exec.LookPath(os.Args[0])
- if err != nil {
- return err
- }
- if perms, err = os.Stat(executePath); err != nil {
- return err
- }
- }
-
- utils.Log.SetProcess("Updating executable %s", executePath)
-
- if err = os.Remove(executePath); err != nil {
- return err
- }
- executeFile, err := os.OpenFile(executePath, os.O_CREATE|os.O_WRONLY, perms.Mode())
- if err != nil {
- return err
- }
- defer executeFile.Close()
-
- resp, err := utils.Client.Get(fmt.Sprintf("https://github.com/crunchy-labs/crunchy-cli/releases/download/v%s/%s", releaseVersion, downloadFile))
- if err != nil {
- return err
- }
- defer resp.Body.Close()
-
- if _, err = io.Copy(executeFile, resp.Body); err != nil {
- return err
- }
-
- utils.Log.StopProcess("Updated executable %s", executePath)
- }
-
- return nil
-}
diff --git a/cli/commands/utils.go b/cli/commands/utils.go
deleted file mode 100644
index 53e51f2..0000000
--- a/cli/commands/utils.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package commands
-
-import (
- "fmt"
- "github.com/crunchy-labs/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
-}
diff --git a/cli/commands/windows.go b/cli/commands/windows.go
deleted file mode 100644
index a9bce74..0000000
--- a/cli/commands/windows.go
+++ /dev/null
@@ -1,41 +0,0 @@
-//go:build windows
-
-package commands
-
-import (
- "bufio"
- "os"
- "syscall"
-)
-
-// https://github.com/bgentry/speakeasy/blob/master/speakeasy_windows.go
-func ReadLineSilent() ([]byte, error) {
- var oldMode uint32
-
- if err := syscall.GetConsoleMode(syscall.Stdin, &oldMode); err != nil {
- return nil, err
- }
-
- newMode := oldMode &^ 0x0004
-
- err := setConsoleMode(syscall.Stdin, newMode)
- defer setConsoleMode(syscall.Stdin, oldMode)
-
- if err != nil {
- return nil, err
- }
-
- l, _, err := bufio.NewReader(os.Stdin).ReadLine()
- if err != nil {
- return nil, err
- }
- return l, err
-}
-
-func setConsoleMode(console syscall.Handle, mode uint32) error {
- dll := syscall.MustLoadDLL("kernel32")
- proc := dll.MustFindProc("SetConsoleMode")
- _, _, err := proc.Call(uintptr(console), uintptr(mode))
-
- return err
-}
diff --git a/cli/root.go b/cli/root.go
deleted file mode 100644
index b589a03..0000000
--- a/cli/root.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package cli
-
-import (
- "context"
- "fmt"
- "github.com/crunchy-labs/crunchy-cli/cli/commands"
- "github.com/crunchy-labs/crunchy-cli/cli/commands/archive"
- "github.com/crunchy-labs/crunchy-cli/cli/commands/download"
- "github.com/crunchy-labs/crunchy-cli/cli/commands/info"
- "github.com/crunchy-labs/crunchy-cli/cli/commands/login"
- "github.com/crunchy-labs/crunchy-cli/cli/commands/update"
- "github.com/crunchy-labs/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/crunchy-labs/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)
- }
-}
diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml
new file mode 100644
index 0000000..893b278
--- /dev/null
+++ b/crunchy-cli-core/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "crunchy-cli-core"
+version = "0.1.0"
+edition = "2021"
+
+[features]
+# Embed a static curl library into the binary instead of just linking it.
+static-curl = ["crunchyroll-rs/static-curl"]
+# Embed a static openssl library into the binary instead of just linking it. If you want to compile this project against
+# musl and have openssl issues, this might solve these issues.
+static-ssl = ["crunchyroll-rs/static-ssl"]
+
+[dependencies]
+anyhow = "1.0"
+async-trait = "0.1"
+clap = { version = "4.0", features = ["derive"] }
+chrono = "0.4"
+crunchyroll-rs = { git = "https://github.com/crunchy-labs/crunchyroll-rs", default-features = false, features = ["stream", "parse"] }
+ctrlc = "3.2"
+dirs = "4.0"
+isahc = { git = "https://github.com/sagebind/isahc", rev = "34f158ef" }
+log = { version = "0.4", features = ["std"] }
+num_cpus = "1.13"
+regex = "1.6"
+signal-hook = "0.3"
+tempfile = "3.3"
+terminal_size = "0.2"
+tokio = { version = "1.21", features = ["macros", "rt-multi-thread", "time"] }
+sys-locale = "0.2"
diff --git a/crunchy-cli-core/src/cli/archive.rs b/crunchy-cli-core/src/cli/archive.rs
new file mode 100644
index 0000000..21ae13b
--- /dev/null
+++ b/crunchy-cli-core/src/cli/archive.rs
@@ -0,0 +1,567 @@
+use crate::cli::log::tab_info;
+use crate::cli::utils::{download_segments, find_resolution};
+use crate::utils::context::Context;
+use crate::utils::format::{format_string, Format};
+use crate::utils::log::progress;
+use crate::utils::os::{free_file, tempfile};
+use crate::utils::parse::{parse_url, UrlFilter};
+use crate::utils::sort::{sort_formats_after_seasons, sort_seasons_after_number};
+use crate::Execute;
+use anyhow::{bail, Result};
+use crunchyroll_rs::media::{Resolution, StreamSubtitle};
+use crunchyroll_rs::{Locale, Media, MediaCollection, Series};
+use log::{debug, error, info};
+use regex::Regex;
+use std::collections::BTreeMap;
+use std::io::Write;
+use std::path::PathBuf;
+use std::process::{Command, Stdio};
+use tempfile::TempPath;
+
+#[derive(Clone, Debug)]
+pub enum MergeBehavior {
+ Auto,
+ Audio,
+ Video,
+}
+
+fn parse_merge_behavior(s: &str) -> Result {
+ Ok(match s.to_lowercase().as_str() {
+ "auto" => MergeBehavior::Auto,
+ "audio" => MergeBehavior::Audio,
+ "video" => MergeBehavior::Video,
+ _ => return Err(format!("'{}' is not a valid merge behavior", s)),
+ })
+}
+
+#[derive(Debug, clap::Parser)]
+#[clap(about = "Archive a video")]
+#[command(arg_required_else_help(true))]
+#[command()]
+pub struct Archive {
+ #[arg(help = format!("Audio languages. Can be used multiple times. \
+ Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(long_help = format!("Audio languages. Can be used multiple times. \
+ Available languages are:\n{}", Locale::all().into_iter().map(|l| format!("{:<6} → {}", l.to_string(), l.to_human_readable())).collect::>().join("\n ")))]
+ #[arg(short, long, default_values_t = vec![crate::utils::locale::system_locale(), Locale::ja_JP])]
+ audio: Vec,
+ #[arg(help = format!("Subtitle languages. Can be used multiple times. \
+ Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(long_help = format!("Subtitle languages. Can be used multiple times. \
+ Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(short, long, default_values_t = Locale::all())]
+ subtitle: Vec,
+
+ #[arg(help = "Name of the output file")]
+ #[arg(long_help = "Name of the output file.\
+ If you use one of the following pattern they will get replaced:\n \
+ {title} → Title of the video\n \
+ {series_name} → Name of the series\n \
+ {season_name} → Name of the season\n \
+ {audio} → Audio language of the video\n \
+ {resolution} → Resolution of the video\n \
+ {season_number} → Number of the season\n \
+ {episode_number} → Number of the episode\n \
+ {series_id} → ID of the series\n \
+ {season_id} → ID of the season\n \
+ {episode_id} → ID of the episode")]
+ #[arg(short, long, default_value = "{title}.mkv")]
+ output: String,
+
+ #[arg(help = "Video resolution")]
+ #[arg(long_help = "The video resolution.\
+ Can either be specified via the pixels (e.g. 1920x1080), the abbreviation for pixels (e.g. 1080p) or 'common-use' words (e.g. best). \
+ Specifying the exact pixels is not recommended, use one of the other options instead. \
+ Crunchyroll let you choose the quality with pixel abbreviation on their clients, so you might be already familiar with the available options. \
+ The available common-use words are 'best' (choose the best resolution available) and 'worst' (worst resolution available)")]
+ #[arg(short, long, default_value = "best")]
+ #[arg(value_parser = crate::utils::clap::clap_parse_resolution)]
+ resolution: Resolution,
+
+ #[arg(
+ help = "Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio' and 'video'"
+ )]
+ #[arg(
+ long_help = "Because of local restrictions (or other reasons) some episodes with different languages does not have the same length (e.g. when some scenes were cut out). \
+ With this flag you can set the behavior when handling multiple language.
+ Valid options are 'audio' (stores one video and all other languages as audio only), 'video' (stores the video + audio for every language) and 'auto' (detects if videos differ in length: if so, behave like 'video' else like 'audio')"
+ )]
+ #[arg(short, long, default_value = "auto")]
+ #[arg(value_parser = parse_merge_behavior)]
+ merge: MergeBehavior,
+
+ #[arg(
+ help = "Set which subtitle language should be set as default / auto shown when starting a video"
+ )]
+ #[arg(long)]
+ default_subtitle: Option,
+ #[arg(help = "Disable subtitle optimizations")]
+ #[arg(
+ long_help = "By default, Crunchyroll delivers subtitles in a format which may cause issues in some video players. \
+ These issues are fixed internally by setting a flag which is not part of the official specification of the subtitle format. \
+ If you do not want this fixes or they cause more trouble than they solve (for you), it can be disabled with this flag"
+ )]
+ #[arg(long)]
+ no_subtitle_optimizations: bool,
+
+ #[arg(help = "Crunchyroll series url(s)")]
+ urls: Vec,
+}
+
+#[async_trait::async_trait(?Send)]
+impl Execute for Archive {
+ async fn execute(self, ctx: Context) -> Result<()> {
+ let mut parsed_urls = vec![];
+
+ for (i, url) in self.urls.iter().enumerate() {
+ let _progress_handler = progress!("Parsing url {}", i + 1);
+ match parse_url(&ctx.crunchy, url.clone(), true).await {
+ Ok((media_collection, url_filter)) => {
+ parsed_urls.push((media_collection, url_filter));
+ info!("Parsed url {}", i + 1)
+ }
+ Err(e) => bail!("url {} could not be parsed: {}", url, e),
+ }
+ }
+
+ for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
+ let archive_formats = match media_collection {
+ MediaCollection::Series(series) => {
+ let _progress_handler = progress!("Fetching series details");
+ formats_from_series(&self, series, &url_filter).await?
+ }
+ MediaCollection::Season(_) => bail!("Archiving a season is not supported"),
+ MediaCollection::Episode(episode) => bail!("Archiving a episode is not supported. Use url filtering instead to specify the episode (https://www.crunchyroll.com/series/{}/{}[S{}E{}])", episode.metadata.series_id, episode.metadata.series_slug_title, episode.metadata.season_number, episode.metadata.episode_number),
+ MediaCollection::MovieListing(_) => bail!("Archiving a movie listing is not supported"),
+ MediaCollection::Movie(_) => bail!("Archiving a movie is not supported")
+ };
+
+ if archive_formats.is_empty() {
+ info!("Skipping url {} (no matching episodes found)", i + 1);
+ continue;
+ }
+ info!("Loaded series information for url {}", i + 1);
+
+ if log::max_level() == log::Level::Debug {
+ let seasons = sort_formats_after_seasons(
+ archive_formats
+ .clone()
+ .into_iter()
+ .map(|(a, _)| a.get(0).unwrap().clone())
+ .collect(),
+ );
+ debug!("Series has {} seasons", seasons.len());
+ for (i, season) in seasons.into_iter().enumerate() {
+ info!("Season {} ({})", i + 1, season.get(0).unwrap().season_title);
+ for format in season {
+ info!(
+ "{}: {}px, {:.02} FPS (S{:02}E{:02})",
+ format.title,
+ format.stream.resolution,
+ format.stream.fps,
+ format.season_number,
+ format.number,
+ )
+ }
+ }
+ } else {
+ for season in sort_formats_after_seasons(
+ archive_formats
+ .clone()
+ .into_iter()
+ .map(|(a, _)| a.get(0).unwrap().clone())
+ .collect(),
+ ) {
+ let first = season.get(0).unwrap();
+ info!(
+ "{} Season {} ({})",
+ first.series_name, first.season_number, first.season_title
+ );
+
+ for (i, format) in season.into_iter().enumerate() {
+ tab_info!(
+ "{}. {} » {}px, {:.2} FPS (S{:02}E{:02})",
+ i + 1,
+ format.title,
+ format.stream.resolution,
+ format.stream.fps,
+ format.season_number,
+ format.number
+ )
+ }
+ }
+ }
+
+ for (formats, subtitles) in archive_formats {
+ let (primary, additionally) = formats.split_first().unwrap();
+
+ let mut path = PathBuf::from(&self.output);
+ path = free_file(
+ path.with_file_name(format_string(
+ if let Some(fname) = path.file_name() {
+ fname.to_str().unwrap()
+ } else {
+ "{title}.mkv"
+ }
+ .to_string(),
+ primary,
+ )),
+ )
+ .0;
+
+ info!(
+ "Downloading {} to '{}'",
+ primary.title,
+ path.to_str().unwrap()
+ );
+ tab_info!(
+ "Episode: S{:02}E{:02}",
+ primary.season_number,
+ primary.number
+ );
+ tab_info!(
+ "Audio: {} (primary), {}",
+ primary.audio,
+ additionally
+ .iter()
+ .map(|a| a.audio.to_string())
+ .collect::>()
+ .join(", ")
+ );
+ tab_info!(
+ "Subtitle: {}",
+ subtitles
+ .iter()
+ .map(|s| {
+ if let Some(default) = &self.default_subtitle {
+ if default == &s.locale {
+ return format!("{} (primary)", default);
+ }
+ }
+ s.locale.to_string()
+ })
+ .collect::>()
+ .join(", ")
+ );
+ tab_info!("Resolution: {}", primary.stream.resolution);
+ tab_info!("FPS: {:.2}", primary.stream.fps);
+
+ let mut video_paths = vec![];
+ let mut audio_paths = vec![];
+ let mut subtitle_paths = vec![];
+
+ video_paths.push((download_video(&ctx, primary, false).await?, primary));
+ for additional in additionally {
+ let only_audio = match self.merge {
+ MergeBehavior::Auto => additionally
+ .iter()
+ .all(|a| a.stream.bandwidth == primary.stream.bandwidth),
+ MergeBehavior::Audio => true,
+ MergeBehavior::Video => false,
+ };
+ let path = download_video(&ctx, additional, only_audio).await?;
+ if only_audio {
+ audio_paths.push((path, additional))
+ } else {
+ video_paths.push((path, additional))
+ }
+ }
+
+ for subtitle in subtitles {
+ subtitle_paths
+ .push((download_subtitle(&self, subtitle.clone()).await?, subtitle))
+ }
+
+ generate_mkv(&self, path, video_paths, audio_paths, subtitle_paths)?
+ }
+ }
+
+ Ok(())
+ }
+}
+
+async fn formats_from_series(
+ archive: &Archive,
+ series: Media,
+ url_filter: &UrlFilter,
+) -> Result, Vec)>> {
+ let mut seasons = series.seasons().await?;
+
+ // filter any season out which does not contain the specified audio languages
+ for season in sort_seasons_after_number(seasons.clone()) {
+ // get all locales which are specified but not present in the current iterated season and
+ // print an error saying this
+ let not_present_audio = archive
+ .audio
+ .clone()
+ .into_iter()
+ .filter(|l| !season.iter().any(|s| &s.metadata.audio_locale == l))
+ .collect::>();
+ for not_present in not_present_audio {
+ error!(
+ "Season {} of series {} is not available with {} audio",
+ season.first().unwrap().metadata.season_number,
+ series.title,
+ not_present
+ )
+ }
+
+ // remove all seasons with the wrong audio for the current iterated season number
+ seasons.retain(|s| {
+ s.metadata.season_number != season.first().unwrap().metadata.season_number
+ || archive.audio.contains(&s.metadata.audio_locale)
+ })
+ }
+
+ #[allow(clippy::type_complexity)]
+ let mut result: BTreeMap, Vec)>> =
+ BTreeMap::new();
+ for season in series.seasons().await? {
+ if !url_filter.is_season_valid(season.metadata.season_number)
+ || !archive.audio.contains(&season.metadata.audio_locale)
+ {
+ continue;
+ }
+
+ for episode in season.episodes().await? {
+ if !url_filter.is_episode_valid(
+ episode.metadata.episode_number,
+ episode.metadata.season_number,
+ ) {
+ continue;
+ }
+
+ let streams = episode.streams().await?;
+ let streaming_data = streams.streaming_data(None).await?;
+ let Some(stream) = find_resolution(streaming_data, &archive.resolution) else {
+ bail!(
+ "Resolution ({}x{}) is not available for episode {} ({}) of season {} ({}) of {}",
+ archive.resolution.width,
+ archive.resolution.height,
+ episode.metadata.episode_number,
+ episode.title,
+ episode.metadata.season_number,
+ episode.metadata.season_title,
+ episode.metadata.series_title
+ )
+ };
+
+ let (ref mut formats, _) = result
+ .entry(season.metadata.season_number)
+ .or_insert_with(BTreeMap::new)
+ .entry(episode.metadata.episode_number)
+ .or_insert_with(|| {
+ let subtitles: Vec = archive
+ .subtitle
+ .iter()
+ .filter_map(|l| streams.subtitles.get(l).cloned())
+ .collect();
+ (vec![], subtitles)
+ });
+ formats.push(Format::new_from_episode(episode, stream));
+ }
+ }
+
+ Ok(result.into_values().flat_map(|v| v.into_values()).collect())
+}
+
+async fn download_video(ctx: &Context, format: &Format, only_audio: bool) -> Result {
+ let tempfile = if only_audio {
+ tempfile(".aac")?
+ } else {
+ tempfile(".ts")?
+ };
+ let (_, path) = tempfile.into_parts();
+
+ let ffmpeg = Command::new("ffmpeg")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::null())
+ .stderr(Stdio::piped())
+ .arg("-y")
+ .args(["-f", "mpegts", "-i", "pipe:"])
+ .args(if only_audio { vec!["-vn"] } else { vec![] })
+ .arg(path.to_str().unwrap())
+ .spawn()?;
+
+ download_segments(
+ ctx,
+ &mut ffmpeg.stdin.unwrap(),
+ Some(format!("Download {}", format.audio)),
+ format.stream.segments().await?,
+ )
+ .await?;
+
+ Ok(path)
+}
+
+async fn download_subtitle(archive: &Archive, subtitle: StreamSubtitle) -> Result {
+ let tempfile = tempfile(".ass")?;
+ let (mut file, path) = tempfile.into_parts();
+
+ let mut buf = vec![];
+ subtitle.write_to(&mut buf).await?;
+ if !archive.no_subtitle_optimizations {
+ buf = fix_subtitle(buf)
+ }
+
+ file.write_all(buf.as_slice())?;
+
+ Ok(path)
+}
+
+/// Add `ScaledBorderAndShadows: yes` to subtitles; without it they look very messy on some video
+/// players. See [crunchy-labs/crunchy-cli#66](https://github.com/crunchy-labs/crunchy-cli/issues/66)
+/// for more information.
+fn fix_subtitle(raw: Vec) -> Vec {
+ let mut script_info = false;
+ let mut new = String::new();
+
+ for line in String::from_utf8_lossy(raw.as_slice()).split('\n') {
+ if line.trim().starts_with('[') && script_info {
+ new.push_str("ScaledBorderAndShadows: yes\n");
+ script_info = false
+ } else if line.trim() == "[Script Info]" {
+ script_info = true
+ }
+ new.push_str(line);
+ new.push('\n')
+ }
+
+ new.into_bytes()
+}
+
+fn generate_mkv(
+ archive: &Archive,
+ target: PathBuf,
+ video_paths: Vec<(TempPath, &Format)>,
+ audio_paths: Vec<(TempPath, &Format)>,
+ subtitle_paths: Vec<(TempPath, StreamSubtitle)>,
+) -> Result<()> {
+ let mut input = vec![];
+ let mut maps = vec![];
+ let mut metadata = vec![];
+
+ let mut video_length = (0, 0, 0, 0);
+
+ for (i, (video_path, format)) in video_paths.iter().enumerate() {
+ input.extend(["-i".to_string(), video_path.to_string_lossy().to_string()]);
+ maps.extend(["-map".to_string(), i.to_string()]);
+ metadata.extend([
+ format!("-metadata:s:v:{}", i),
+ format!("language={}", format.audio),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:v:{}", i),
+ format!("title={}", format.audio.to_human_readable()),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:a:{}", i),
+ format!("language={}", format.audio),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:a:{}", i),
+ format!("title={}", format.audio.to_human_readable()),
+ ]);
+
+ let vid_len = get_video_length(video_path.to_path_buf())?;
+ if vid_len > video_length {
+ video_length = vid_len
+ }
+ }
+ for (i, (audio_path, format)) in audio_paths.iter().enumerate() {
+ input.extend(["-i".to_string(), audio_path.to_string_lossy().to_string()]);
+ maps.extend(["-map".to_string(), (i + video_paths.len()).to_string()]);
+ metadata.extend([
+ format!("-metadata:s:a:{}", i + video_paths.len()),
+ format!("language={}", format.audio),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:a:{}", i + video_paths.len()),
+ format!("title={}", format.audio.to_human_readable()),
+ ]);
+ }
+ for (i, (subtitle_path, subtitle)) in subtitle_paths.iter().enumerate() {
+ input.extend([
+ "-i".to_string(),
+ subtitle_path.to_string_lossy().to_string(),
+ ]);
+ maps.extend([
+ "-map".to_string(),
+ (i + video_paths.len() + audio_paths.len()).to_string(),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:s:{}", i),
+ format!("language={}", subtitle.locale),
+ ]);
+ metadata.extend([
+ format!("-metadata:s:s:{}", i),
+ format!("title={}", subtitle.locale.to_human_readable()),
+ ]);
+ }
+
+ let mut command_args = vec!["-y".to_string()];
+ command_args.extend(input);
+ command_args.extend(maps);
+ command_args.extend(metadata);
+
+ // set default subtitle
+ if let Some(default_subtitle) = &archive.default_subtitle {
+ // if `--default_subtitle ` is given set the default subtitle to the given locale
+ if let Some(position) = subtitle_paths
+ .into_iter()
+ .position(|s| &s.1.locale == default_subtitle)
+ {
+ command_args.push(format!("-disposition:s:{}", position))
+ } else {
+ command_args.extend(["-disposition:s:0".to_string(), "0".to_string()])
+ }
+ } else {
+ command_args.extend(["-disposition:s:0".to_string(), "0".to_string()])
+ }
+
+ command_args.extend([
+ "-c".to_string(),
+ "copy".to_string(),
+ "-f".to_string(),
+ "matroska".to_string(),
+ target.to_string_lossy().to_string(),
+ ]);
+
+ debug!("ffmpeg {}", command_args.join(" "));
+
+ let ffmpeg = Command::new("ffmpeg")
+ .stdout(Stdio::null())
+ .stderr(Stdio::piped())
+ .args(command_args)
+ .output()?;
+ if !ffmpeg.status.success() {
+ bail!("{}", String::from_utf8_lossy(ffmpeg.stderr.as_slice()))
+ }
+
+ Ok(())
+}
+
+/// Get the length of a video. This is required because sometimes subtitles have an unnecessary entry
+/// long after the actual video ends with artificially extends the video length on some video players.
+/// To prevent this, the video length must be hard set with ffmpeg. See
+/// [crunchy-labs/crunchy-cli#32](https://github.com/crunchy-labs/crunchy-cli/issues/32) for more
+/// information.
+fn get_video_length(path: PathBuf) -> Result<(u32, u32, u32, u32)> {
+ let video_length = Regex::new(r"Duration:\s?(\d+):(\d+):(\d+).(\d+),")?;
+
+ let ffmpeg = Command::new("ffmpeg")
+ .stdout(Stdio::null())
+ .stderr(Stdio::piped())
+ .arg("-y")
+ .args(["-i", path.to_str().unwrap()])
+ .output()?;
+ let ffmpeg_output = String::from_utf8(ffmpeg.stderr)?;
+ let caps = video_length.captures(ffmpeg_output.as_str()).unwrap();
+
+ Ok((
+ caps[1].parse()?,
+ caps[2].parse()?,
+ caps[3].parse()?,
+ caps[4].parse()?,
+ ))
+}
diff --git a/crunchy-cli-core/src/cli/download.rs b/crunchy-cli-core/src/cli/download.rs
new file mode 100644
index 0000000..7f9e3ec
--- /dev/null
+++ b/crunchy-cli-core/src/cli/download.rs
@@ -0,0 +1,452 @@
+use crate::cli::log::tab_info;
+use crate::cli::utils::{download_segments, find_resolution};
+use crate::utils::context::Context;
+use crate::utils::format::{format_string, Format};
+use crate::utils::log::progress;
+use crate::utils::os::{free_file, has_ffmpeg};
+use crate::utils::parse::{parse_url, UrlFilter};
+use crate::utils::sort::{sort_formats_after_seasons, sort_seasons_after_number};
+use crate::Execute;
+use anyhow::{bail, Result};
+use crunchyroll_rs::media::{Resolution, VariantSegment};
+use crunchyroll_rs::{
+ Episode, Locale, Media, MediaCollection, Movie, MovieListing, Season, Series,
+};
+use log::{debug, error, info};
+use std::fs::File;
+use std::path::{Path, PathBuf};
+use std::process::{Command, Stdio};
+
+#[derive(Debug, clap::Parser)]
+#[clap(about = "Download a video")]
+#[command(arg_required_else_help(true))]
+pub struct Download {
+ #[arg(help = format!("Audio language. Can only be used if the provided url(s) point to a series. \
+ Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(long_help = format!("Audio language. Can only be used if the provided url(s) point to a series. \
+ Available languages are:\n{}", Locale::all().into_iter().map(|l| format!("{:<6} → {}", l.to_string(), l.to_human_readable())).collect::>().join("\n ")))]
+ #[arg(short, long, default_value_t = crate::utils::locale::system_locale())]
+ audio: Locale,
+ #[arg(help = format!("Subtitle language. Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(long_help = format!("Subtitle language. If set, the subtitle will be burned into the video and cannot be disabled. \
+ Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
+ #[arg(short, long)]
+ subtitle: Option,
+
+ #[arg(help = "Name of the output file")]
+ #[arg(long_help = "Name of the output file.\
+ If you use one of the following pattern they will get replaced:\n \
+ {title} → Title of the video\n \
+ {series_name} → Name of the series\n \
+ {season_name} → Name of the season\n \
+ {audio} → Audio language of the video\n \
+ {resolution} → Resolution of the video\n \
+ {season_number} → Number of the season\n \
+ {episode_number} → Number of the episode\n \
+ {series_id} → ID of the series\n \
+ {season_id} → ID of the season\n \
+ {episode_id} → ID of the episode")]
+ #[arg(short, long, default_value = "{title}.ts")]
+ output: String,
+
+ #[arg(help = "Video resolution")]
+ #[arg(long_help = "The video resolution.\
+ Can either be specified via the pixels (e.g. 1920x1080), the abbreviation for pixels (e.g. 1080p) or 'common-use' words (e.g. best). \
+ Specifying the exact pixels is not recommended, use one of the other options instead. \
+ Crunchyroll let you choose the quality with pixel abbreviation on their clients, so you might be already familiar with the available options. \
+ The available common-use words are 'best' (choose the best resolution available) and 'worst' (worst resolution available)")]
+ #[arg(short, long, default_value = "best")]
+ #[arg(value_parser = crate::utils::clap::clap_parse_resolution)]
+ resolution: Resolution,
+
+ #[arg(help = "Url(s) to Crunchyroll episodes or series")]
+ urls: Vec,
+}
+
+#[async_trait::async_trait(?Send)]
+impl Execute for Download {
+ async fn execute(self, ctx: Context) -> Result<()> {
+ let mut parsed_urls = vec![];
+
+ for (i, url) in self.urls.iter().enumerate() {
+ let _progress_handler = progress!("Parsing url {}", i + 1);
+ match parse_url(&ctx.crunchy, url.clone(), true).await {
+ Ok((media_collection, url_filter)) => {
+ parsed_urls.push((media_collection, url_filter));
+ info!("Parsed url {}", i + 1)
+ }
+ Err(e) => bail!("url {} could not be parsed: {}", url, e),
+ }
+ }
+
+ for (i, (media_collection, url_filter)) in parsed_urls.into_iter().enumerate() {
+ let _progress_handler = progress!("Fetching series details");
+ let formats = match media_collection {
+ MediaCollection::Series(series) => {
+ debug!("Url {} is series ({})", i + 1, series.title);
+ formats_from_series(&self, series, &url_filter).await?
+ }
+ MediaCollection::Season(season) => {
+ debug!(
+ "Url {} is season {} ({})",
+ i + 1,
+ season.metadata.season_number,
+ season.title
+ );
+ formats_from_season(&self, season, &url_filter).await?
+ }
+ MediaCollection::Episode(episode) => {
+ debug!(
+ "Url {} is episode {} ({}) of season {} ({}) of {}",
+ i + 1,
+ episode.metadata.episode_number,
+ episode.title,
+ episode.metadata.season_number,
+ episode.metadata.season_title,
+ episode.metadata.series_title
+ );
+ format_from_episode(&self, episode, &url_filter, false)
+ .await?
+ .map(|fmt| vec![fmt])
+ }
+ MediaCollection::MovieListing(movie_listing) => {
+ debug!("Url {} is movie listing ({})", i + 1, movie_listing.title);
+ format_from_movie_listing(&self, movie_listing, &url_filter).await?
+ }
+ MediaCollection::Movie(movie) => {
+ debug!("Url {} is movie ({})", i + 1, movie.title);
+ format_from_movie(&self, movie, &url_filter)
+ .await?
+ .map(|fmt| vec![fmt])
+ }
+ };
+
+ let Some(formats) = formats else {
+ info!("Skipping url {} (no matching episodes found)", i + 1);
+ continue;
+ };
+ info!("Loaded series information for url {}", i + 1);
+ drop(_progress_handler);
+
+ if log::max_level() == log::Level::Debug {
+ let seasons = sort_formats_after_seasons(formats.clone());
+ debug!("Series has {} seasons", seasons.len());
+ for (i, season) in seasons.into_iter().enumerate() {
+ info!("Season {} ({})", i + 1, season.get(0).unwrap().season_title);
+ for format in season {
+ info!(
+ "{}: {}px, {:.02} FPS (S{:02}E{:02})",
+ format.title,
+ format.stream.resolution,
+ format.stream.fps,
+ format.season_number,
+ format.number,
+ )
+ }
+ }
+ } else {
+ for season in sort_formats_after_seasons(formats.clone()) {
+ let first = season.get(0).unwrap();
+ info!(
+ "{} Season {} ({})",
+ first.series_name, first.season_number, first.season_title
+ );
+
+ for (i, format) in season.into_iter().enumerate() {
+ tab_info!(
+ "{}. {} » {}px, {:.2} FPS (S{:02}E{:02})",
+ i + 1,
+ format.title,
+ format.stream.resolution,
+ format.stream.fps,
+ format.season_number,
+ format.number
+ )
+ }
+ }
+ }
+
+ for format in formats {
+ let mut path = PathBuf::from(&self.output);
+ path = free_file(
+ path.with_file_name(format_string(
+ if let Some(fname) = path.file_name() {
+ fname.to_str().unwrap()
+ } else {
+ "{title}.ts"
+ }
+ .to_string(),
+ &format,
+ )),
+ )
+ .0;
+
+ let use_ffmpeg = if let Some(extension) = path.extension() {
+ if extension != "ts" {
+ if !has_ffmpeg() {
+ bail!(
+ "File ending is not `.ts`, ffmpeg is required to convert the video"
+ )
+ }
+ true
+ } else {
+ false
+ }
+ } else {
+ false
+ };
+
+ info!(
+ "Downloading {} to '{}'",
+ format.title,
+ path.file_name().unwrap().to_str().unwrap()
+ );
+ tab_info!("Episode: S{:02}E{:02}", format.season_number, format.number);
+ tab_info!("Audio: {}", format.audio);
+ tab_info!(
+ "Subtitles: {}",
+ self.subtitle
+ .clone()
+ .map_or("None".to_string(), |l| l.to_string())
+ );
+ tab_info!("Resolution: {}", format.stream.resolution);
+ tab_info!("FPS: {:.2}", format.stream.fps);
+
+ let segments = format.stream.segments().await?;
+
+ if use_ffmpeg {
+ download_ffmpeg(&ctx, segments, path.as_path()).await?;
+ } else if path.to_str().unwrap() == "-" {
+ let mut stdout = std::io::stdout().lock();
+ download_segments(&ctx, &mut stdout, None, segments).await?;
+ } else {
+ let mut file = File::options().create(true).write(true).open(&path)?;
+ download_segments(&ctx, &mut file, None, segments).await?
+ }
+ }
+ }
+
+ Ok(())
+ }
+}
+
+async fn download_ffmpeg(
+ ctx: &Context,
+ segments: Vec,
+ target: &Path,
+) -> Result<()> {
+ let ffmpeg = Command::new("ffmpeg")
+ .stdin(Stdio::piped())
+ .stdout(Stdio::null())
+ .stderr(Stdio::piped())
+ .arg("-y")
+ .args(["-f", "mpegts", "-i", "pipe:"])
+ .args(["-safe", "0"])
+ .args(["-c", "copy"])
+ .arg(target.to_str().unwrap())
+ .spawn()?;
+
+ download_segments(ctx, &mut ffmpeg.stdin.unwrap(), None, segments).await?;
+
+ Ok(())
+}
+
+async fn formats_from_series(
+ download: &Download,
+ series: Media,
+ url_filter: &UrlFilter,
+) -> Result>> {
+ if !series.metadata.audio_locales.is_empty()
+ && !series.metadata.audio_locales.contains(&download.audio)
+ {
+ error!(
+ "Series {} is not available with {} audio",
+ series.title, download.audio
+ );
+ return Ok(None);
+ }
+
+ let mut seasons = series.seasons().await?;
+
+ // filter any season out which does not contain the specified audio language
+ for season in sort_seasons_after_number(seasons.clone()) {
+ // check if the current iterated season has the specified audio language
+ if !season
+ .iter()
+ .any(|s| s.metadata.audio_locale == download.audio)
+ {
+ error!(
+ "Season {} of series {} is not available with {} audio",
+ season.first().unwrap().metadata.season_number,
+ series.title,
+ download.audio
+ );
+ }
+
+ // remove all seasons with the wrong audio for the current iterated season number
+ seasons.retain(|s| {
+ s.metadata.season_number != season.first().unwrap().metadata.season_number
+ || s.metadata.audio_locale == download.audio
+ })
+ }
+
+ let mut formats = vec![];
+ for season in seasons {
+ if let Some(fmts) = formats_from_season(download, season, url_filter).await? {
+ formats.extend(fmts)
+ }
+ }
+
+ Ok(some_vec_or_none(formats))
+}
+
+async fn formats_from_season(
+ download: &Download,
+ season: Media,
+ url_filter: &UrlFilter,
+) -> Result>> {
+ if season.metadata.audio_locale != download.audio {
+ error!(
+ "Season {} ({}) is not available with {} audio",
+ season.metadata.season_number, season.title, download.audio
+ );
+ return Ok(None);
+ } else if !url_filter.is_season_valid(season.metadata.season_number) {
+ return Ok(None);
+ }
+
+ let mut formats = vec![];
+
+ for episode in season.episodes().await? {
+ if let Some(fmt) = format_from_episode(download, episode, url_filter, true).await? {
+ formats.push(fmt)
+ }
+ }
+
+ Ok(some_vec_or_none(formats))
+}
+
+async fn format_from_episode(
+ download: &Download,
+ episode: Media,
+ url_filter: &UrlFilter,
+ filter_audio: bool,
+) -> Result> {
+ if filter_audio && episode.metadata.audio_locale != download.audio {
+ error!(
+ "Episode {} ({}) of season {} ({}) of {} has no {} audio",
+ episode.metadata.episode_number,
+ episode.title,
+ episode.metadata.season_number,
+ episode.metadata.season_title,
+ episode.metadata.series_title,
+ download.audio
+ );
+ return Ok(None);
+ } else if !url_filter.is_episode_valid(
+ episode.metadata.episode_number,
+ episode.metadata.season_number,
+ ) {
+ return Ok(None);
+ }
+
+ let streams = episode.streams().await?;
+ let streaming_data = if let Some(subtitle) = &download.subtitle {
+ if !streams.subtitles.keys().cloned().any(|x| &x == subtitle) {
+ error!(
+ "Episode {} ({}) of season {} ({}) of {} has no {} subtitles",
+ episode.metadata.episode_number,
+ episode.title,
+ episode.metadata.season_number,
+ episode.metadata.season_title,
+ episode.metadata.series_title,
+ subtitle
+ );
+ return Ok(None);
+ }
+ streams.streaming_data(Some(subtitle.clone())).await?
+ } else {
+ streams.streaming_data(None).await?
+ };
+
+ let Some(stream) = find_resolution(streaming_data, &download.resolution) else {
+ bail!(
+ "Resolution ({}x{}) is not available for episode {} ({}) of season {} ({}) of {}",
+ download.resolution.width,
+ download.resolution.height,
+ episode.metadata.episode_number,
+ episode.title,
+ episode.metadata.season_number,
+ episode.metadata.season_title,
+ episode.metadata.series_title
+ )
+ };
+
+ Ok(Some(Format::new_from_episode(episode, stream)))
+}
+
+async fn format_from_movie_listing(
+ download: &Download,
+ movie_listing: Media,
+ url_filter: &UrlFilter,
+) -> Result>> {
+ let mut formats = vec![];
+
+ for movie in movie_listing.movies().await? {
+ if let Some(fmt) = format_from_movie(download, movie, url_filter).await? {
+ formats.push(fmt)
+ }
+ }
+
+ Ok(some_vec_or_none(formats))
+}
+
+async fn format_from_movie(
+ download: &Download,
+ movie: Media,
+ _: &UrlFilter,
+) -> Result> {
+ let streams = movie.streams().await?;
+ let mut streaming_data = if let Some(subtitle) = &download.subtitle {
+ if !streams.subtitles.keys().cloned().any(|x| &x == subtitle) {
+ error!("Movie {} has no {} subtitles", movie.title, subtitle);
+ return Ok(None);
+ }
+ streams.streaming_data(Some(subtitle.clone())).await?
+ } else {
+ streams.streaming_data(None).await?
+ };
+
+ streaming_data.sort_by(|a, b| a.resolution.width.cmp(&b.resolution.width).reverse());
+ let stream = {
+ match download.resolution.height {
+ u64::MAX => streaming_data.into_iter().next().unwrap(),
+ u64::MIN => streaming_data.into_iter().last().unwrap(),
+ _ => {
+ if let Some(streaming_data) = streaming_data.into_iter().find(|v| {
+ download.resolution.height == u64::MAX
+ || v.resolution.height == download.resolution.height
+ }) {
+ streaming_data
+ } else {
+ bail!(
+ "Resolution ({}x{}) is not available for movie {}",
+ download.resolution.width,
+ download.resolution.height,
+ movie.title
+ )
+ }
+ }
+ }
+ };
+
+ Ok(Some(Format::new_from_movie(movie, stream)))
+}
+
+fn some_vec_or_none(v: Vec) -> Option> {
+ if v.is_empty() {
+ None
+ } else {
+ Some(v)
+ }
+}
diff --git a/crunchy-cli-core/src/cli/log.rs b/crunchy-cli-core/src/cli/log.rs
new file mode 100644
index 0000000..d4bb1b3
--- /dev/null
+++ b/crunchy-cli-core/src/cli/log.rs
@@ -0,0 +1,197 @@
+use log::{
+ set_boxed_logger, set_max_level, Level, LevelFilter, Log, Metadata, Record, SetLoggerError,
+};
+use std::io::{stdout, Write};
+use std::sync::{mpsc, Mutex};
+use std::thread;
+use std::thread::JoinHandle;
+use std::time::Duration;
+
+struct CliProgress {
+ handler: JoinHandle<()>,
+ sender: mpsc::SyncSender<(String, Level)>,
+}
+
+impl CliProgress {
+ fn new(record: &Record) -> Self {
+ let (tx, rx) = mpsc::sync_channel(1);
+
+ let init_message = format!("{}", record.args());
+ let init_level = record.level();
+ let handler = thread::spawn(move || {
+ let states = ["-", "\\", "|", "/"];
+
+ let mut old_message = init_message.clone();
+ let mut latest_info_message = init_message;
+ let mut old_level = init_level;
+ for i in 0.. {
+ let (msg, level) = match rx.try_recv() {
+ Ok(payload) => payload,
+ Err(e) => match e {
+ mpsc::TryRecvError::Empty => (old_message.clone(), old_level),
+ mpsc::TryRecvError::Disconnected => break,
+ },
+ };
+
+ // clear last line
+ // prefix (2), space (1), state (1), space (1), message(n)
+ let _ = write!(stdout(), "\r {}", " ".repeat(old_message.len()));
+
+ if old_level != level || old_message != msg {
+ if old_level <= Level::Warn {
+ let _ = writeln!(stdout(), "\r:: • {}", old_message);
+ } else if old_level == Level::Info && level <= Level::Warn {
+ let _ = writeln!(stdout(), "\r:: → {}", old_message);
+ } else if level == Level::Info {
+ latest_info_message = msg.clone();
+ }
+ }
+
+ let _ = write!(
+ stdout(),
+ "\r:: {} {}",
+ states[i / 2 % states.len()],
+ if level == Level::Info {
+ &msg
+ } else {
+ &latest_info_message
+ }
+ );
+ let _ = stdout().flush();
+
+ old_message = msg;
+ old_level = level;
+
+ thread::sleep(Duration::from_millis(100));
+ }
+
+ // clear last line
+ // prefix (2), space (1), state (1), space (1), message(n)
+ let _ = write!(stdout(), "\r {}", " ".repeat(old_message.len()));
+ let _ = writeln!(stdout(), "\r:: ✓ {}", old_message);
+ let _ = stdout().flush();
+ });
+
+ Self {
+ handler,
+ sender: tx,
+ }
+ }
+
+ fn send(&self, record: &Record) {
+ let _ = self
+ .sender
+ .send((format!("{}", record.args()), record.level()));
+ }
+
+ fn stop(self) {
+ drop(self.sender);
+ let _ = self.handler.join();
+ }
+}
+
+#[allow(clippy::type_complexity)]
+pub struct CliLogger {
+ level: LevelFilter,
+ progress: Mutex>,
+}
+
+impl Log for CliLogger {
+ fn enabled(&self, metadata: &Metadata) -> bool {
+ metadata.level() <= self.level
+ }
+
+ fn log(&self, record: &Record) {
+ if !self.enabled(record.metadata())
+ || (record.target() != "progress"
+ && record.target() != "progress_end"
+ && !record.target().starts_with("crunchy_cli"))
+ {
+ return;
+ }
+
+ if self.level >= LevelFilter::Debug {
+ self.extended(record);
+ return;
+ }
+
+ match record.target() {
+ "progress" => self.progress(record, false),
+ "progress_end" => self.progress(record, true),
+ _ => {
+ if self.progress.lock().unwrap().is_some() {
+ self.progress(record, false);
+ } else if record.level() > Level::Warn {
+ self.normal(record)
+ } else {
+ self.error(record)
+ }
+ }
+ }
+ }
+
+ fn flush(&self) {
+ let _ = stdout().flush();
+ }
+}
+
+impl CliLogger {
+ pub fn new(level: LevelFilter) -> Self {
+ Self {
+ level,
+ progress: Mutex::new(None),
+ }
+ }
+
+ pub fn init(level: LevelFilter) -> Result<(), SetLoggerError> {
+ set_max_level(level);
+ set_boxed_logger(Box::new(CliLogger::new(level)))
+ }
+
+ fn extended(&self, record: &Record) {
+ println!(
+ "[{}] {} {} ({}) {}",
+ chrono::Utc::now().format("%Y-%m-%d %H:%M:%S"),
+ record.level(),
+ // replace the 'progress' prefix if this function is invoked via 'progress!'
+ record
+ .target()
+ .replacen("progress", "crunchy_cli", 1)
+ .replacen("progress_end", "crunchy_cli", 1),
+ format!("{:?}", thread::current().id())
+ .replace("ThreadId(", "")
+ .replace(')', ""),
+ record.args()
+ )
+ }
+
+ fn normal(&self, record: &Record) {
+ println!(":: {}", record.args())
+ }
+
+ fn error(&self, record: &Record) {
+ eprintln!(":: {}", record.args())
+ }
+
+ fn progress(&self, record: &Record, stop: bool) {
+ let mut progress_option = self.progress.lock().unwrap();
+ if stop && progress_option.is_some() {
+ progress_option.take().unwrap().stop()
+ } else if let Some(p) = &*progress_option {
+ p.send(record);
+ } else {
+ *progress_option = Some(CliProgress::new(record))
+ }
+ }
+}
+
+macro_rules! tab_info {
+ ($($arg:tt)+) => {
+ if log::max_level() == log::LevelFilter::Debug {
+ info!($($arg)+)
+ } else {
+ info!("\t{}", format!($($arg)+))
+ }
+ }
+}
+pub(crate) use tab_info;
diff --git a/crunchy-cli-core/src/cli/login.rs b/crunchy-cli-core/src/cli/login.rs
new file mode 100644
index 0000000..c6d876a
--- /dev/null
+++ b/crunchy-cli-core/src/cli/login.rs
@@ -0,0 +1,39 @@
+use crate::utils::context::Context;
+use crate::Execute;
+use anyhow::bail;
+use anyhow::Result;
+use crunchyroll_rs::crunchyroll::SessionToken;
+use std::fs;
+use std::path::PathBuf;
+
+#[derive(Debug, clap::Parser)]
+#[clap(about = "Save your login credentials persistent on disk")]
+pub struct Login {
+ #[arg(help = "Remove your stored credentials (instead of save them)")]
+ #[arg(long)]
+ pub remove: bool,
+}
+
+#[async_trait::async_trait(?Send)]
+impl Execute for Login {
+ async fn execute(self, ctx: Context) -> Result<()> {
+ if let Some(login_file_path) = login_file_path() {
+ match ctx.crunchy.session_token().await {
+ SessionToken::RefreshToken(refresh_token) => Ok(fs::write(
+ login_file_path,
+ format!("refresh_token:{}", refresh_token),
+ )?),
+ SessionToken::EtpRt(etp_rt) => {
+ Ok(fs::write(login_file_path, format!("etp_rt:{}", etp_rt))?)
+ }
+ SessionToken::Anonymous => bail!("Anonymous login cannot be saved"),
+ }
+ } else {
+ bail!("Cannot find config path")
+ }
+ }
+}
+
+pub fn login_file_path() -> Option {
+ dirs::config_dir().map(|config_dir| config_dir.join(".crunchy-cli-core"))
+}
diff --git a/crunchy-cli-core/src/cli/mod.rs b/crunchy-cli-core/src/cli/mod.rs
new file mode 100644
index 0000000..c28f8e0
--- /dev/null
+++ b/crunchy-cli-core/src/cli/mod.rs
@@ -0,0 +1,5 @@
+pub mod archive;
+pub mod download;
+pub mod log;
+pub mod login;
+mod utils;
diff --git a/crunchy-cli-core/src/cli/utils.rs b/crunchy-cli-core/src/cli/utils.rs
new file mode 100644
index 0000000..59816d1
--- /dev/null
+++ b/crunchy-cli-core/src/cli/utils.rs
@@ -0,0 +1,178 @@
+use crate::utils::context::Context;
+use anyhow::Result;
+use crunchyroll_rs::media::{Resolution, VariantData, VariantSegment};
+use isahc::AsyncReadResponseExt;
+use log::{debug, LevelFilter};
+use std::borrow::{Borrow, BorrowMut};
+use std::collections::BTreeMap;
+use std::io;
+use std::io::Write;
+use std::sync::{mpsc, Arc, Mutex};
+use std::time::Duration;
+use tokio::task::JoinSet;
+
+pub fn find_resolution(
+ mut streaming_data: Vec,
+ resolution: &Resolution,
+) -> Option {
+ streaming_data.sort_by(|a, b| a.resolution.width.cmp(&b.resolution.width).reverse());
+ match resolution.height {
+ u64::MAX => Some(streaming_data.into_iter().next().unwrap()),
+ u64::MIN => Some(streaming_data.into_iter().last().unwrap()),
+ _ => streaming_data
+ .into_iter()
+ .find(|v| resolution.height == u64::MAX || v.resolution.height == resolution.height),
+ }
+}
+
+pub async fn download_segments(
+ ctx: &Context,
+ writer: &mut impl Write,
+ message: Option,
+ segments: Vec,
+) -> Result<()> {
+ let total_segments = segments.len();
+
+ let client = Arc::new(ctx.client.clone());
+ let count = Arc::new(Mutex::new(0));
+ let amount = Arc::new(Mutex::new(0));
+
+ // only print progress when log level is info
+ let output_handler = if log::max_level() == LevelFilter::Info {
+ let output_count = count.clone();
+ let output_amount = amount.clone();
+ Some(tokio::spawn(async move {
+ let sleep_time_ms = 100;
+ let iter_per_sec = 1000f64 / sleep_time_ms as f64;
+
+ let mut bytes_start = 0f64;
+ let mut speed = 0f64;
+ let mut percentage = 0f64;
+
+ while *output_count.lock().unwrap() < total_segments || percentage < 100f64 {
+ let tmp_amount = *output_amount.lock().unwrap() as f64;
+
+ let tmp_speed = (tmp_amount - bytes_start) / 1024f64 / 1024f64;
+ if *output_count.lock().unwrap() < 3 {
+ speed = tmp_speed;
+ } else {
+ let (old_speed_ratio, new_speed_ratio) = if iter_per_sec <= 1f64 {
+ (0f64, 1f64)
+ } else {
+ (1f64 - (1f64 / iter_per_sec), (1f64 / iter_per_sec))
+ };
+
+ // calculate the average download speed "smoother"
+ speed = (speed * old_speed_ratio) + (tmp_speed * new_speed_ratio);
+ }
+
+ percentage =
+ (*output_count.lock().unwrap() as f64 / total_segments as f64) * 100f64;
+
+ let size = terminal_size::terminal_size()
+ .unwrap_or((terminal_size::Width(60), terminal_size::Height(0)))
+ .0
+ .0 as usize;
+
+ let progress_available = size
+ - if let Some(msg) = &message {
+ 35 + msg.len()
+ } else {
+ 33
+ };
+ let progress_done_count =
+ (progress_available as f64 * (percentage / 100f64)).ceil() as usize;
+ let progress_to_do_count = progress_available - progress_done_count;
+
+ let _ = write!(
+ io::stdout(),
+ "\r:: {}{:>5.1} MiB {:>5.2} MiB/s [{}{}] {:>3}%",
+ message.clone().map_or("".to_string(), |msg| msg + " "),
+ tmp_amount / 1024f64 / 1024f64,
+ speed * iter_per_sec,
+ "#".repeat(progress_done_count),
+ "-".repeat(progress_to_do_count),
+ percentage as usize
+ );
+
+ bytes_start = tmp_amount;
+
+ tokio::time::sleep(Duration::from_millis(sleep_time_ms)).await;
+ }
+ println!()
+ }))
+ } else {
+ None
+ };
+
+ let cpus = num_cpus::get();
+ let mut segs: Vec> = Vec::with_capacity(cpus);
+ for _ in 0..cpus {
+ segs.push(vec![])
+ }
+ for (i, segment) in segments.into_iter().enumerate() {
+ segs[i - ((i / cpus) * cpus)].push(segment);
+ }
+
+ let (sender, receiver) = mpsc::channel();
+
+ let mut join_set: JoinSet> = JoinSet::new();
+ for num in 0..cpus {
+ let thread_client = client.clone();
+ let thread_sender = sender.clone();
+ let thread_segments = segs.remove(0);
+ let thread_amount = amount.clone();
+ let thread_count = count.clone();
+ join_set.spawn(async move {
+ for (i, segment) in thread_segments.into_iter().enumerate() {
+ let mut response = thread_client.get_async(&segment.url).await?;
+ let mut buf = response.bytes().await?.to_vec();
+
+ *thread_amount.lock().unwrap() += buf.len();
+
+ buf = VariantSegment::decrypt(buf.borrow_mut(), segment.key)?.to_vec();
+ debug!(
+ "Downloaded and decrypted segment {} ({})",
+ num + (i * cpus),
+ segment.url
+ );
+ thread_sender.send((num + (i * cpus), buf))?;
+
+ *thread_count.lock().unwrap() += 1;
+ }
+
+ Ok(())
+ });
+ }
+
+ let mut data_pos = 0usize;
+ let mut buf: BTreeMap> = BTreeMap::new();
+ loop {
+ // is always `Some` because `sender` does not get dropped when all threads are finished
+ let data = receiver.recv().unwrap();
+
+ if data_pos == data.0 {
+ writer.write_all(data.1.borrow())?;
+ data_pos += 1;
+ } else {
+ buf.insert(data.0, data.1);
+ }
+ while let Some(b) = buf.remove(&data_pos) {
+ writer.write_all(b.borrow())?;
+ data_pos += 1;
+ }
+
+ if *count.lock().unwrap() >= total_segments {
+ break;
+ }
+ }
+
+ while let Some(joined) = join_set.join_next().await {
+ joined??
+ }
+ if let Some(handler) = output_handler {
+ handler.await?
+ }
+
+ Ok(())
+}
diff --git a/crunchy-cli-core/src/lib.rs b/crunchy-cli-core/src/lib.rs
new file mode 100644
index 0000000..faef927
--- /dev/null
+++ b/crunchy-cli-core/src/lib.rs
@@ -0,0 +1,196 @@
+use crate::cli::log::CliLogger;
+use crate::utils::context::Context;
+use crate::utils::locale::system_locale;
+use crate::utils::log::progress;
+use anyhow::bail;
+use anyhow::Result;
+use clap::{Parser, Subcommand};
+use crunchyroll_rs::{Crunchyroll, Locale};
+use log::{debug, error, info, LevelFilter};
+use std::{env, fs};
+
+mod cli;
+mod utils;
+
+pub use cli::{archive::Archive, download::Download, login::Login};
+
+#[async_trait::async_trait(?Send)]
+trait Execute {
+ async fn execute(self, ctx: Context) -> Result<()>;
+}
+
+#[derive(Debug, Parser)]
+#[clap(author, version, about)]
+#[clap(name = "crunchy-cli")]
+pub struct Cli {
+ #[clap(flatten)]
+ verbosity: Option,
+
+ #[arg(help = "Overwrite the language in which results are returned. Default is your system language")]
+ #[arg(long)]
+ lang: Option,
+
+ #[clap(flatten)]
+ login_method: LoginMethod,
+
+ #[clap(subcommand)]
+ command: Command,
+}
+
+#[derive(Debug, Subcommand)]
+enum Command {
+ Archive(Archive),
+ Download(Download),
+ Login(Login),
+}
+
+#[derive(Debug, Parser)]
+struct Verbosity {
+ #[arg(help = "Verbose output")]
+ #[arg(short)]
+ v: bool,
+
+ #[arg(help = "Quiet output. Does not print anything unless it's a error")]
+ #[arg(long_help = "Quiet output. Does not print anything unless it's a error. Can be helpful if you pipe the output to stdout")]
+ #[arg(short)]
+ q: bool,
+}
+
+#[derive(Debug, Parser)]
+struct LoginMethod {
+ #[arg(help = "Login with credentials (username or email and password)")]
+ #[arg(long_help = "Login with credentials (username or email and password). Must be provided as user:password")]
+ #[arg(long)]
+ credentials: Option,
+ #[arg(help = "Login with the etp-rt cookie")]
+ #[arg(long_help = "Login with the etp-rt cookie. This can be obtained when you login on crunchyroll.com and extract it from there")]
+ #[arg(long)]
+ etp_rt: Option,
+}
+
+pub async fn cli_entrypoint() {
+ let cli: Cli = Cli::parse();
+
+ if let Some(verbosity) = &cli.verbosity {
+ if verbosity.v && verbosity.q {
+ eprintln!("Output cannot be verbose ('-v') and quiet ('-q') at the same time");
+ std::process::exit(1)
+ } else if verbosity.v {
+ CliLogger::init(LevelFilter::Debug).unwrap()
+ } else if verbosity.q {
+ CliLogger::init(LevelFilter::Error).unwrap()
+ }
+ } else {
+ CliLogger::init(LevelFilter::Info).unwrap()
+ }
+
+ debug!("cli input: {:?}", cli);
+
+ let ctx = match create_ctx(&cli).await {
+ Ok(ctx) => ctx,
+ Err(e) => {
+ error!("{}", e);
+ std::process::exit(1)
+ }
+ };
+ debug!("Created context");
+
+ ctrlc::set_handler(move || {
+ debug!("Ctrl-c detected");
+ if let Ok(dir) = fs::read_dir(&env::temp_dir()) {
+ for file in dir.flatten() {
+ if file
+ .path()
+ .file_name()
+ .unwrap_or_default()
+ .to_str()
+ .unwrap_or_default()
+ .starts_with(".crunchy-cli_")
+ {
+ let result = fs::remove_file(file.path());
+ debug!(
+ "Ctrl-c removed temporary file {} {}",
+ file.path().to_string_lossy(),
+ if result.is_ok() {
+ "successfully"
+ } else {
+ "not successfully"
+ }
+ )
+ }
+ }
+ }
+ std::process::exit(1)
+ })
+ .unwrap();
+ debug!("Created ctrl-c handler");
+
+ let result = match cli.command {
+ Command::Archive(archive) => archive.execute(ctx).await,
+ Command::Download(download) => download.execute(ctx).await,
+ Command::Login(login) => {
+ if login.remove {
+ Ok(())
+ } else {
+ login.execute(ctx).await
+ }
+ }
+ };
+ if let Err(err) = result {
+ error!("{}", err);
+ std::process::exit(1)
+ }
+}
+
+async fn create_ctx(cli: &Cli) -> Result {
+ let crunchy = crunchyroll_session(cli).await?;
+ // TODO: Use crunchy.client() when it's possible
+ // currently crunchy.client() has a cloudflare bypass built-in to access crunchyroll. the servers
+ // where crunchy stores their videos can't handle this bypass and simply refuses to connect
+ let client = isahc::HttpClient::new().unwrap();
+
+ Ok(Context { crunchy, client })
+}
+
+async fn crunchyroll_session(cli: &Cli) -> Result {
+ let mut builder = Crunchyroll::builder();
+ builder.locale(cli.lang.clone().unwrap_or_else(system_locale));
+
+ let _progress_handler = progress!("Logging in");
+ if cli.login_method.credentials.is_none() && cli.login_method.etp_rt.is_none() {
+ if let Some(login_file_path) = cli::login::login_file_path() {
+ if login_file_path.exists() {
+ let session = fs::read_to_string(login_file_path)?;
+ if let Some((token_type, token)) = session.split_once(':') {
+ match token_type {
+ "refresh_token" => {
+ return Ok(builder.login_with_refresh_token(token).await?)
+ }
+ "etp_rt" => return Ok(builder.login_with_etp_rt(token).await?),
+ _ => (),
+ }
+ }
+ bail!("Could not read stored session ('{}')", session)
+ }
+ }
+ bail!("Please use a login method ('--credentials' or '--etp_rt')")
+ } else if cli.login_method.credentials.is_some() && cli.login_method.etp_rt.is_some() {
+ bail!("Please use only one login method ('--credentials' or '--etp_rt')")
+ }
+
+ let crunchy = if let Some(credentials) = &cli.login_method.credentials {
+ if let Some((user, password)) = credentials.split_once(':') {
+ builder.login_with_credentials(user, password).await?
+ } else {
+ bail!("Invalid credentials format. Please provide your credentials as user:password")
+ }
+ } else if let Some(etp_rt) = &cli.login_method.etp_rt {
+ builder.login_with_etp_rt(etp_rt).await?
+ } else {
+ bail!("should never happen")
+ };
+
+ info!("Logged in");
+
+ Ok(crunchy)
+}
diff --git a/crunchy-cli-core/src/utils/clap.rs b/crunchy-cli-core/src/utils/clap.rs
new file mode 100644
index 0000000..4e27a5e
--- /dev/null
+++ b/crunchy-cli-core/src/utils/clap.rs
@@ -0,0 +1,6 @@
+use crate::utils::parse::parse_resolution;
+use crunchyroll_rs::media::Resolution;
+
+pub fn clap_parse_resolution(s: &str) -> Result {
+ parse_resolution(s.to_string()).map_err(|e| e.to_string())
+}
diff --git a/crunchy-cli-core/src/utils/context.rs b/crunchy-cli-core/src/utils/context.rs
new file mode 100644
index 0000000..6a2fd21
--- /dev/null
+++ b/crunchy-cli-core/src/utils/context.rs
@@ -0,0 +1,6 @@
+use crunchyroll_rs::Crunchyroll;
+
+pub struct Context {
+ pub crunchy: Crunchyroll,
+ pub client: isahc::HttpClient,
+}
diff --git a/crunchy-cli-core/src/utils/format.rs b/crunchy-cli-core/src/utils/format.rs
new file mode 100644
index 0000000..3dd7f74
--- /dev/null
+++ b/crunchy-cli-core/src/utils/format.rs
@@ -0,0 +1,77 @@
+use crunchyroll_rs::media::VariantData;
+use crunchyroll_rs::{Episode, Locale, Media, Movie};
+use std::time::Duration;
+
+#[derive(Clone)]
+pub struct Format {
+ pub id: String,
+ pub title: String,
+ pub description: String,
+ pub number: u32,
+ pub audio: Locale,
+
+ pub duration: Duration,
+ pub stream: VariantData,
+
+ pub series_id: String,
+ pub series_name: String,
+
+ pub season_id: String,
+ pub season_title: String,
+ pub season_number: u32,
+}
+
+impl Format {
+ pub fn new_from_episode(episode: Media, stream: VariantData) -> Self {
+ Self {
+ id: episode.id,
+ title: episode.title,
+ description: episode.description,
+ number: episode.metadata.episode_number,
+ audio: episode.metadata.audio_locale,
+
+ duration: episode.metadata.duration.to_std().unwrap(),
+ stream,
+
+ series_id: episode.metadata.series_id,
+ series_name: episode.metadata.series_title,
+
+ season_id: episode.metadata.season_id,
+ season_title: episode.metadata.season_title,
+ season_number: episode.metadata.season_number,
+ }
+ }
+
+ pub fn new_from_movie(movie: Media, stream: VariantData) -> Self {
+ Self {
+ id: movie.id,
+ title: movie.title,
+ description: movie.description,
+ number: 1,
+ audio: Locale::ja_JP,
+
+ duration: movie.metadata.duration.to_std().unwrap(),
+ stream,
+
+ series_id: movie.metadata.movie_listing_id.clone(),
+ series_name: movie.metadata.movie_listing_title.clone(),
+
+ season_id: movie.metadata.movie_listing_id,
+ season_title: movie.metadata.movie_listing_title,
+ season_number: 1,
+ }
+ }
+}
+
+pub fn format_string(s: String, format: &Format) -> String {
+ s.replace("{title}", &format.title)
+ .replace("{series_name}", &format.series_name)
+ .replace("{season_name}", &format.season_title)
+ .replace("{audio}", &format.audio.to_string())
+ .replace("{resolution}", &format.stream.resolution.to_string())
+ .replace("{season_number}", &format.season_number.to_string())
+ .replace("{episode_number}", &format.number.to_string())
+ .replace("{series_id}", &format.series_id)
+ .replace("{season_id}", &format.season_id)
+ .replace("{episode_id}", &format.id)
+}
diff --git a/crunchy-cli-core/src/utils/locale.rs b/crunchy-cli-core/src/utils/locale.rs
new file mode 100644
index 0000000..300204d
--- /dev/null
+++ b/crunchy-cli-core/src/utils/locale.rs
@@ -0,0 +1,15 @@
+use crunchyroll_rs::Locale;
+
+/// Return the locale of the system.
+pub fn system_locale() -> Locale {
+ if let Some(system_locale) = sys_locale::get_locale() {
+ let locale = Locale::from(system_locale);
+ if let Locale::Custom(_) = locale {
+ Locale::en_US
+ } else {
+ locale
+ }
+ } else {
+ Locale::en_US
+ }
+}
diff --git a/crunchy-cli-core/src/utils/log.rs b/crunchy-cli-core/src/utils/log.rs
new file mode 100644
index 0000000..b9fa939
--- /dev/null
+++ b/crunchy-cli-core/src/utils/log.rs
@@ -0,0 +1,19 @@
+use log::info;
+
+pub struct ProgressHandler;
+
+impl Drop for ProgressHandler {
+ fn drop(&mut self) {
+ info!(target: "progress_end", "")
+ }
+}
+
+macro_rules! progress {
+ ($($arg:tt)+) => {
+ {
+ log::info!(target: "progress", $($arg)+);
+ $crate::utils::log::ProgressHandler{}
+ }
+ }
+}
+pub(crate) use progress;
diff --git a/crunchy-cli-core/src/utils/mod.rs b/crunchy-cli-core/src/utils/mod.rs
new file mode 100644
index 0000000..5f7a5d2
--- /dev/null
+++ b/crunchy-cli-core/src/utils/mod.rs
@@ -0,0 +1,8 @@
+pub mod clap;
+pub mod context;
+pub mod format;
+pub mod locale;
+pub mod log;
+pub mod os;
+pub mod parse;
+pub mod sort;
diff --git a/crunchy-cli-core/src/utils/os.rs b/crunchy-cli-core/src/utils/os.rs
new file mode 100644
index 0000000..317381d
--- /dev/null
+++ b/crunchy-cli-core/src/utils/os.rs
@@ -0,0 +1,52 @@
+use log::debug;
+use std::io::ErrorKind;
+use std::path::PathBuf;
+use std::process::Command;
+use std::{env, io};
+use tempfile::{Builder, NamedTempFile};
+
+pub fn has_ffmpeg() -> bool {
+ if let Err(e) = Command::new("ffmpeg").spawn() {
+ if ErrorKind::NotFound != e.kind() {
+ debug!(
+ "unknown error occurred while checking if ffmpeg exists: {}",
+ e.kind()
+ )
+ }
+ false
+ } else {
+ true
+ }
+}
+
+/// Any tempfiles should be created with this function. The prefix and directory of every file
+/// created with this method stays the same which is helpful to query all existing tempfiles and
+/// e.g. remove them in a case of ctrl-c. Having one function also good to prevent mistakes like
+/// setting the wrong prefix if done manually.
+pub fn tempfile>(suffix: S) -> io::Result {
+ let tempfile = Builder::default()
+ .prefix(".crunchy-cli_")
+ .suffix(suffix.as_ref())
+ .tempfile_in(&env::temp_dir())?;
+ debug!(
+ "Created temporary file: {}",
+ tempfile.path().to_string_lossy()
+ );
+ Ok(tempfile)
+}
+
+/// Check if the given path exists and rename it until the new (renamed) file does not exist.
+pub fn free_file(mut path: PathBuf) -> (PathBuf, bool) {
+ let mut i = 0;
+ while path.exists() {
+ i += 1;
+
+ let ext = path.extension().unwrap().to_str().unwrap();
+ let mut filename = path.file_name().unwrap().to_str().unwrap();
+
+ filename = &filename[0..filename.len() - ext.len() - 1];
+
+ path.set_file_name(format!("{} ({}).{}", filename, i, ext))
+ }
+ (path, i != 0)
+}
diff --git a/crunchy-cli-core/src/utils/parse.rs b/crunchy-cli-core/src/utils/parse.rs
new file mode 100644
index 0000000..fbfea35
--- /dev/null
+++ b/crunchy-cli-core/src/utils/parse.rs
@@ -0,0 +1,170 @@
+use anyhow::{anyhow, bail, Result};
+use crunchyroll_rs::media::Resolution;
+use crunchyroll_rs::{Crunchyroll, MediaCollection, UrlType};
+use log::debug;
+use regex::Regex;
+
+/// Define a filter, based on season and episode number to filter episodes / movies.
+/// If a struct instance equals the [`Default::default()`] it's considered that no filter is applied.
+/// If `from_*` is [`None`] they're set to [`u32::MIN`].
+/// If `to_*` is [`None`] they're set to [`u32::MAX`].
+#[derive(Debug)]
+pub struct InnerUrlFilter {
+ from_episode: Option,
+ to_episode: Option,
+ from_season: Option,
+ to_season: Option,
+}
+
+#[derive(Debug, Default)]
+pub struct UrlFilter {
+ inner: Vec,
+}
+
+impl UrlFilter {
+ pub fn is_season_valid(&self, season: u32) -> bool {
+ self.inner.iter().any(|f| {
+ let from_season = f.from_season.unwrap_or(u32::MIN);
+ let to_season = f.to_season.unwrap_or(u32::MAX);
+
+ season >= from_season && season <= to_season
+ })
+ }
+
+ pub fn is_episode_valid(&self, episode: u32, season: u32) -> bool {
+ self.inner.iter().any(|f| {
+ let from_episode = f.from_episode.unwrap_or(u32::MIN);
+ let to_episode = f.to_episode.unwrap_or(u32::MAX);
+ let from_season = f.from_season.unwrap_or(u32::MIN);
+ let to_season = f.to_season.unwrap_or(u32::MAX);
+
+ episode >= from_episode
+ && episode <= to_episode
+ && season >= from_season
+ && season <= to_season
+ })
+ }
+}
+
+/// Parse a url and return all [`crunchyroll_rs::Media`] &
+/// [`crunchyroll_rs::Media`] which could be related to it.
+///
+/// The `with_filter` arguments says if filtering should be enabled for the url. Filtering is a
+/// specific pattern at the end of the url which declares which parts of the url content should be
+/// returned / filtered (out). _This only works if the url points to a series_.
+///
+/// Examples how filtering works:
+/// - `...[E5]` - Download the fifth episode.
+/// - `...[S1]` - Download the full first season.
+/// - `...[-S2]` - Download all seasons up to and including season 2.
+/// - `...[S3E4-]` - Download all episodes from and including season 3, episode 4.
+/// - `...[S1E4-S3]` - Download all episodes from and including season 1, episode 4, until andincluding season 3.
+/// - `...[S3,S5]` - Download episode 3 and 5.
+/// - `...[S1-S3,S4E2-S4E6]` - Download season 1 to 3 and episode 2 to episode 6 of season 4.
+
+/// In practice, it would look like this: `https://beta.crunchyroll.com/series/12345678/example[S1E5-S3E2]`.
+pub async fn parse_url(
+ crunchy: &Crunchyroll,
+ mut url: String,
+ with_filter: bool,
+) -> Result<(MediaCollection, UrlFilter)> {
+ let url_filter = if with_filter {
+ debug!("Url may contain filters");
+
+ let open_index = url.rfind('[').unwrap_or(0);
+ let close_index = url.rfind(']').unwrap_or(0);
+
+ let filter = if open_index < close_index {
+ let filter = url.as_str()[open_index + 1..close_index].to_string();
+ url = url.as_str()[0..open_index].to_string();
+ filter
+ } else {
+ "".to_string()
+ };
+
+ let filter_regex = Regex::new(r"((S(?P\d+))?(E(?P\d+))?)(((?P-)((S(?P\d+))?(E(?P\d+))?))?)(,|$)").unwrap();
+
+ let mut filters = vec![];
+
+ for capture in filter_regex.captures_iter(&filter) {
+ let dash = capture.name("dash").is_some();
+ let from_episode = capture
+ .name("from_episode")
+ .map_or(anyhow::Ok(None), |fe| Ok(Some(fe.as_str().parse()?)))?;
+ let to_episode = capture
+ .name("to_episode")
+ .map_or(anyhow::Ok(if dash { None } else { from_episode }), |te| {
+ Ok(Some(te.as_str().parse()?))
+ })?;
+ let from_season = capture
+ .name("from_season")
+ .map_or(anyhow::Ok(None), |fs| Ok(Some(fs.as_str().parse()?)))?;
+ let to_season = capture
+ .name("to_season")
+ .map_or(anyhow::Ok(if dash { None } else { from_season }), |ts| {
+ Ok(Some(ts.as_str().parse()?))
+ })?;
+
+ filters.push(InnerUrlFilter {
+ from_episode,
+ to_episode,
+ from_season,
+ to_season,
+ })
+ }
+
+ let url_filter = UrlFilter { inner: filters };
+
+ debug!("Url filter: {:?}", url_filter);
+
+ url_filter
+ } else {
+ UrlFilter::default()
+ };
+
+ let parsed_url = crunchyroll_rs::parse_url(url).map_or(Err(anyhow!("Invalid url")), Ok)?;
+ debug!("Url type: {:?}", parsed_url);
+ let media_collection = match parsed_url {
+ UrlType::Series(id) | UrlType::MovieListing(id) | UrlType::EpisodeOrMovie(id) => {
+ crunchy.media_collection_from_id(id).await?
+ }
+ };
+
+ Ok((media_collection, url_filter))
+}
+
+/// Parse a resolution given as a [`String`] to a [`crunchyroll_rs::media::Resolution`].
+pub fn parse_resolution(mut resolution: String) -> Result {
+ resolution = resolution.to_lowercase();
+
+ if resolution == "best" {
+ Ok(Resolution {
+ width: u64::MAX,
+ height: u64::MAX,
+ })
+ } else if resolution == "worst" {
+ Ok(Resolution {
+ width: u64::MIN,
+ height: u64::MIN,
+ })
+ } else if resolution.ends_with('p') {
+ let without_p = resolution.as_str()[0..resolution.len() - 2]
+ .parse()
+ .map_err(|_| anyhow!("Could not parse resolution"))?;
+ Ok(Resolution {
+ width: without_p * 16 / 9,
+ height: without_p,
+ })
+ } else if let Some((w, h)) = resolution.split_once('x') {
+ Ok(Resolution {
+ width: w
+ .parse()
+ .map_err(|_| anyhow!("Could not parse resolution"))?,
+ height: h
+ .parse()
+ .map_err(|_| anyhow!("Could not parse resolution"))?,
+ })
+ } else {
+ bail!("Could not parse resolution")
+ }
+}
diff --git a/crunchy-cli-core/src/utils/sort.rs b/crunchy-cli-core/src/utils/sort.rs
new file mode 100644
index 0000000..089fe18
--- /dev/null
+++ b/crunchy-cli-core/src/utils/sort.rs
@@ -0,0 +1,47 @@
+use crate::utils::format::Format;
+use crunchyroll_rs::{Media, Season};
+use std::collections::BTreeMap;
+
+/// Sort seasons after their season number. Crunchyroll may have multiple seasons for one season
+/// number. They generally store different language in individual seasons with the same season number.
+/// E.g. series X has one official season but crunchy has translations for it in 3 different languages
+/// so there exist 3 different "seasons" on Crunchyroll which are actual the same season but with
+/// different audio.
+pub fn sort_seasons_after_number(seasons: Vec>) -> Vec>> {
+ let mut as_map = BTreeMap::new();
+
+ for season in seasons {
+ as_map
+ .entry(season.metadata.season_number)
+ .or_insert_with(Vec::new);
+ as_map
+ .get_mut(&season.metadata.season_number)
+ .unwrap()
+ .push(season)
+ }
+
+ as_map.into_values().collect()
+}
+
+/// Sort formats after their seasons and episodes (inside it) ascending. Make sure to have only
+/// episodes from one series and in one language as argument since the function does not handle those
+/// differences which could then lead to a semi messed up result.
+pub fn sort_formats_after_seasons(formats: Vec) -> Vec> {
+ let mut as_map = BTreeMap::new();
+
+ for format in formats {
+ as_map.entry(format.season_number).or_insert_with(Vec::new);
+ as_map.get_mut(&format.season_number).unwrap().push(format);
+ }
+
+ let mut sorted = as_map
+ .into_iter()
+ .map(|(_, mut values)| {
+ values.sort_by(|a, b| a.number.cmp(&b.number));
+ values
+ })
+ .collect::>>();
+ sorted.sort_by(|a, b| a[0].series_id.cmp(&b[0].series_id));
+
+ sorted
+}
diff --git a/crunchy-cli.1 b/crunchy-cli.1
deleted file mode 100644
index 91ba30c..0000000
--- a/crunchy-cli.1
+++ /dev/null
@@ -1,219 +0,0 @@
-.TH crunchy-cli 1 "27 June 2022" "crunchy-cli" "Crunchyroll Cli Client"
-
-.SH NAME
-crunchy-cli - A cli for downloading videos and entire series from crunchyroll.
-
-.SH SYNOPSIS
-crunchy-cli [\fB-h\fR] [\fB-p\fR \fIPROXY\fR] [\fB-q\fR] [\fB-v\fR]
-.br
-crunchy-cli help
-.br
-crunchy-cli login [\fB--persistent\fR] [\fB--session-id\fR \fISESSION_ID\fR] [\fIusername\fR, \fIpassword\fR]
-.br
-crunchy-cli download [\fB-a\fR \fIAUDIO\fR] [\fB-s\fR \fISUBTITLE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
-.br
-crunchy-cli archive [\fB-l\fR \fILANGUAGE\fR] [\fB-d\fR \fIDIRECTORY\fR] [\fB-o\fR \fIOUTPUT\fR] [\fB-m\fR \fIMERGE BEHAVIOR\fR] [\fB-c\fR \fICOMPRESS\fR] [\fB-r\fR \fIRESOLUTION\fR] [\fB-g\fR \fIGOROUTINES\fR] \fIURLs...\fR
-.br
-crunchy-cli update [\fB-i\fR \fIINSTALL\fR]
-
-.SH DESCRIPTION
-.TP
-With \fBcrunchy-cli\fR you can easily download video and series from crunchyroll.
-.TP
-
-Note that you need an \fBcrunchyroll premium\fR account in order to use this tool!
-
-.SH GENERAL OPTIONS
-.TP
-This options can be passed to every action.
-.TP
-
-\fB-h, --help\fR
-Shows help.
-.TP
-
-\fB-p, --proxy PROXY\fR
-Sets a proxy through which all traffic will be routed.
-.TP
-
-\fB-q, --quiet\fR
-Disables all output.
-.TP
-
-\fB-v, --verbose\fR
-Shows verbose output.
-.TP
-
-\fB--version\fR
-Shows the current cli version.
-
-.SH LOGIN COMMAND
-This command logs in to crunchyroll and stores the session id or credentials on the drive. This needs to be done before calling other commands since they need a valid login to operate.
-.TP
-
-\fB--persistent\fR
-Stores the given credentials permanent on the drive. The *nix path for it is $HOME/.config/crunchy.
-.br
-NOTE: The credentials are stored in plain text and if you not use \fB--session-id\fR your credentials are used (if you not use the \fB--persistent\fR flag only a session id gets stored regardless if you login with username/password or a session id).
-.TP
-
-\fB--session-id SESSION_ID\fR
-Login via a session id (which can be extracted from a crunchyroll browser cookie) instead of using username and password.
-
-.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.
-.TP
-
-\fB-a, --audio AUDIO\fR
-Forces to download videos with the given audio locale. If no video with this audio locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
-.TP
-
-\fB-s, --subtitle SUBTITLE\fR
-Forces to download the videos with subtitles in the given locale / language. If no video with this subtitle locale is available, nothing will be downloaded. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
-.TP
-
-\fB-d, --directory DIRECTORY\fR
-The directory to download all files to.
-.TP
-
-\fB-o, --output OUTPUT\fR
-Name of the output file. Formatting is also supported, so if the name contains one or more of the following things, they will get replaced.
- {title} » Title of the video.
- {series_name} » Name of the series.
- {season_name} » Name of the season.
- {season_number} » Number of the season.
- {episode_number} » Number of the episode.
- {resolution} » Resolution of the video.
- {fps} » Frame Rate of the video.
- {audio} » Audio locale of the video.
- {subtitle} » Subtitle locale of the video.
-.TP
-
-\fB-r, resolution RESOLUTION\fR
-The video resolution. Can either be specified via the pixels (e.g. 1920x1080), the abbreviation for pixels (e.g. 1080p) or "common-use" words (e.g. best).
- Available pixels: 1920x1080, 1280x720, 640x480, 480x360, 426x240.
- Available abbreviations: 1080p, 720p, 480p, 360p, 240p.
- Available common-use words: best (best available resolution), worst (worst available resolution).
-.TP
-
-\fB-g, --goroutines GOROUTINES\fR
-Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
-
-.SH ARCHIVE COMMAND
-This command behaves like \fBdownload\fR besides the fact that it requires \fIffmpeg\fR and stores the output only to .mkv files.
-.TP
-
-\fB-l, --language LANGUAGE\fR
-Audio locales which should be downloaded. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
-.TP
-
-\fB-s, --sublang LANGUAGE\fR
-Subtitle languages to use, by default all are included. Can be used multiple times. Available locales are: ja-JP, en-US, es-419, es-LA, es-ES, fr-FR, pt-PT, pt-BR, it-IT, de-DE, ru-RU, ar-SA, ar-ME.
-.TP
-
-\fB-d, --directory DIRECTORY\fR
-The directory to download all files to.
-.TP
-
-\fB-o, --output OUTPUT\fR
-Name of the output file. Formatting is also supported, so if the name contains one or more of the following things, they will get replaced.
- {title} » Title of the video.
- {series_name} » Name of the series.
- {season_name} » Name of the season.
- {season_number} » Number of the season.
- {episode_number} » Number of the episode.
- {resolution} » Resolution of the video.
- {fps} » Frame Rate of the video.
- {audio} » Audio locale of the video.
- {subtitle} » Subtitle locale of the video.
-.TP
-
-\fB-m, --merge MERGE BEHAVIOR\fR
-Sets the behavior of the stream merging. Valid behaviors are 'auto', 'audio', 'video'. \fB--audio\fR stores one video and only the audio of all other languages, \fBvideo\fR stores all videos of the given languages and their audio, \fBauto\fR (which is the default) only behaves like video if the length of two videos are different (and only for the two videos), else like audio.
-.TP
-
-\fB-c, --compress COMPRESS\fR
-If is set, all output will be compresses into an archive (every url generates a new one). 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.
-Just like \fB--output\fR the name can be formatted. But the only option available here is \fI{series_name}\fR.
-.TP
-
-\fB-r, resolution RESOLUTION\fR
-The video resolution. Can either be specified via the pixels (e.g. 1920x1080), the abbreviation for pixels (e.g. 1080p) or "common-use" words (e.g. best).
- Available pixels: 1920x1080, 1280x720, 640x480, 480x360, 426x240.
- Available abbreviations: 1080p, 720p, 480p, 360p, 240p.
- Available common-use words: best (best available resolution), worst (worst available resolution).
-.TP
-
-\fB-g, --goroutines GOROUTINES\fR
-Sets the number of parallel downloads for the segments the final video is made of. Default is the number of cores the computer has.
-
-.SH UPDATE COMMAND
-Checks if a newer version is available.
-.TP
-
-\fB-i, --install INSTALL\fR
-If given, the command tries to update the executable with the newer version (if a newer is available).
-
-.SH URL OPTIONS
-If you want to download only specific episode of a series, you could either pass every single episode url to the downloader (which is fine for 1 - 3 episodes) or use filtering.
-It works pretty simple, just put a specific pattern surrounded by square brackets at the end of the url from the anime you want to download. A season and / or episode as well as a range from where to where episodes should be downloaded can be specified.
-Use the list below to get a better overview what is possible
- ...[E5] - Download the fifth episode.
- ...[S1] - Download the full first season.
- ...[-S2] - Download all seasons up to and including season 2.
- ...[S3E4-] - Download all episodes from and including season 3, episode 4.
- ...[S1E4-S3] - Download all episodes from and including season 1, episode 4, until and including season 3.
-
-In practise, it would look like this: \fIhttps://beta.crunchyroll.com/series/12345678/example[S1E5-S3E2]\fR.
-
-The \fBS\fR, followed by the number indicates the season number, \fBE\fR, followed by the number indicates an episode number. It doesn't matter if \fBS\fR, \fBE\fR or both are missing. Theoretically \fB[-]\fR is a valid pattern too. Note that \fBS\fR must always stay before \fBE\fR when used.
-
-.SH EXAMPLES
-Login via crunchyroll account email and password.
-.br
-$ crunchy-cli login user@example.com 12345678
-
-Download a episode normally. Your system locale will be used for the video's audio.
-.br
-$ crunchy-cli download https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-
-Download a episode with 720p and name it to 'darling.mp4'. Note that you need \fBffmpeg\fR to save files which do not have '.ts' as file extension.
-.br
-$ crunchy-cli download -o "darling.mp4" -r 720p https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-
-Download a episode with japanese audio and american subtitles.
-.br
-$ crunchy-cli download -a ja-JP -s en-US https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E3-E5]
-
-Stores the episode in a .mkv file.
-.br
-$ crunchy-cli archive https://beta.crunchyroll.com/watch/GRDKJZ81Y/alone-and-lonesome
-
-Downloads the first two episode of Darling in the FranXX and stores it compressed in a file.
-.br
-$ crunchy-cli archive -c "ditf.tar.gz" https://beta.crunchyroll.com/series/GY8VEQ95Y/darling-in-the-franxx[E1-E2]
-
-.SH BUGS
-If you notice any bug or want an enhancement, feel free to create a new issue or pull request in the GitHub repository.
-
-.SH AUTHOR
-Crunchy Labs Maintainers
-.br
-Source: https://github.com/crunchy-labs/crunchy-cli
-
-.SH COPYRIGHT
-Copyright (C) 2022 Crunchy Labs Maintainers
-
-This program is free software: you can redistribute it and/or
-modify it under the terms of the GNU General Public License
-as published by the Free Software Foundation, either version 3
-of the License, or (at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
diff --git a/go.mod b/go.mod
deleted file mode 100644
index 162e7a1..0000000
--- a/go.mod
+++ /dev/null
@@ -1,14 +0,0 @@
-module github.com/crunchy-labs/crunchy-cli
-
-go 1.19
-
-require (
- github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2
- github.com/grafov/m3u8 v0.11.1
- github.com/spf13/cobra v1.5.0
-)
-
-require (
- github.com/inconshreveable/mousetrap v1.0.0 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
-)
diff --git a/go.sum b/go.sum
deleted file mode 100644
index 51fe7a6..0000000
--- a/go.sum
+++ /dev/null
@@ -1,14 +0,0 @@
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2 h1:PG5++Gje126/xRtzZwCowoFU1Dl3qKzFjd3lWhVXoso=
-github.com/crunchy-labs/crunchyroll-go/v3 v3.0.2/go.mod h1:SjTQD3IX7Z+MLsMSd2fP5ttsJ4KtpXY6r08bHLwrOLM=
-github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA=
-github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
-github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
-github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
diff --git a/main.go b/main.go
deleted file mode 100644
index 72ec83b..0000000
--- a/main.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package main
-
-import (
- "github.com/crunchy-labs/crunchy-cli/cli"
-)
-
-func main() {
- cli.Execute()
-}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..5e1d695
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,4 @@
+#[tokio::main]
+async fn main() {
+ crunchy_cli_core::cli_entrypoint().await
+}
diff --git a/utils/extract.go b/utils/extract.go
deleted file mode 100644
index fa2d650..0000000
--- a/utils/extract.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package utils
-
-import (
- "fmt"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- "github.com/crunchy-labs/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]
- }
-
- 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
- }
- }
-
- var final [][]*crunchyroll.Episode
- if len(locales) > 0 {
- final = make([][]*crunchyroll.Episode, len(locales))
- 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]...)
- }
- } else {
- final = [][]*crunchyroll.Episode{episodes}
- }
-
- return final, nil
-}
diff --git a/utils/file.go b/utils/file.go
deleted file mode 100644
index 183b06f..0000000
--- a/utils/file.go
+++ /dev/null
@@ -1,49 +0,0 @@
-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
-}
diff --git a/utils/format.go b/utils/format.go
deleted file mode 100644
index 6d40142..0000000
--- a/utils/format.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package utils
-
-import (
- "fmt"
- "github.com/crunchy-labs/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
-}
diff --git a/utils/http.go b/utils/http.go
deleted file mode 100644
index f572bd9..0000000
--- a/utils/http.go
+++ /dev/null
@@ -1,51 +0,0 @@
-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
- }
-}
diff --git a/utils/locale.go b/utils/locale.go
deleted file mode 100644
index 9769940..0000000
--- a/utils/locale.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package utils
-
-import (
- "fmt"
- "github.com/crunchy-labs/crunchyroll-go/v3"
- "github.com/crunchy-labs/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
-}
diff --git a/utils/logger.go b/utils/logger.go
deleted file mode 100644
index 27ea344..0000000
--- a/utils/logger.go
+++ /dev/null
@@ -1,12 +0,0 @@
-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)
-}
diff --git a/utils/save.go b/utils/save.go
deleted file mode 100644
index 7660974..0000000
--- a/utils/save.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package utils
-
-import (
- "crypto/aes"
- "crypto/cipher"
- "crypto/rand"
- "crypto/sha256"
- "fmt"
- "github.com/crunchy-labs/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.RefreshToken), 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.RefreshToken), 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.LoginWithRefreshToken(string(body), SystemLocale(true), Client)
- if err != nil {
- Log.Debug("Failed to login with temp refresh token: %v", err)
- } else {
- Log.Debug("Logged in with refresh token %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.LoginWithRefreshToken(split[0], SystemLocale(true), Client); err != nil {
- return nil, err
- }
- Log.Debug("Logged in with refresh token %s. BLANK THIS LINE OUT IF YOU'RE ASKED TO POST THE DEBUG OUTPUT SOMEWHERE", split[0])
- }
-
- // the refresh token 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.RefreshToken), 0600); err != nil {
- return nil, err
- }
- }
-
- return
-}
diff --git a/utils/std.go b/utils/std.go
deleted file mode 100644
index 75b35f1..0000000
--- a/utils/std.go
+++ /dev/null
@@ -1,10 +0,0 @@
-package utils
-
-func ElementInSlice[T comparable](check T, toCheck []T) bool {
- for _, item := range toCheck {
- if check == item {
- return true
- }
- }
- return false
-}
diff --git a/utils/system.go b/utils/system.go
deleted file mode 100644
index ac97706..0000000
--- a/utils/system.go
+++ /dev/null
@@ -1,7 +0,0 @@
-package utils
-
-import "os/exec"
-
-func HasFFmpeg() bool {
- return exec.Command("ffmpeg", "-h").Run() == nil
-}
diff --git a/utils/vars.go b/utils/vars.go
deleted file mode 100644
index d2893a5..0000000
--- a/utils/vars.go
+++ /dev/null
@@ -1,14 +0,0 @@
-package utils
-
-import (
- "github.com/crunchy-labs/crunchyroll-go/v3"
- "net/http"
-)
-
-var Version = "development"
-
-var (
- Crunchy *crunchyroll.Crunchyroll
- Client *http.Client
- Log Logger
-)
From 502cb399238f848a2e82c310fc889c10a5df4223 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Thu, 24 Nov 2022 15:54:17 +0100
Subject: [PATCH 298/732] Fix download binary description
---
README.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2229739..db06b99 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,9 @@ A [Rust](https://www.rust-lang.org/) written cli client for [Crunchyroll](https:
### 📥 Download the latest binaries
-Checkout the [releases](https://github.com/crunchy-labs/crunchy-cli/releases) tab and get the binary from the newest release.
+~~Checkout the [releases](https://github.com/crunchy-labs/crunchy-cli/releases) tab and get the binary from the newest release.~~
+
+Currently, no pre-built binary of the rewrite / this branch is available.
### 🛠 Build it yourself
From 4fd98723ea4e344f2d904054b2594bda3b9f54fb Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sat, 26 Nov 2022 22:37:58 +0100
Subject: [PATCH 299/732] Change archive flag name from audio to locale
---
crunchy-cli-core/src/cli/archive.rs | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/crunchy-cli-core/src/cli/archive.rs b/crunchy-cli-core/src/cli/archive.rs
index 21ae13b..754d6cb 100644
--- a/crunchy-cli-core/src/cli/archive.rs
+++ b/crunchy-cli-core/src/cli/archive.rs
@@ -44,7 +44,7 @@ pub struct Archive {
#[arg(long_help = format!("Audio languages. Can be used multiple times. \
Available languages are:\n{}", Locale::all().into_iter().map(|l| format!("{:<6} → {}", l.to_string(), l.to_human_readable())).collect::>().join("\n ")))]
#[arg(short, long, default_values_t = vec![crate::utils::locale::system_locale(), Locale::ja_JP])]
- audio: Vec,
+ locale: Vec,
#[arg(help = format!("Subtitle languages. Can be used multiple times. \
Available languages are: {}", Locale::all().into_iter().map(|l| l.to_string()).collect::>().join(", ")))]
#[arg(long_help = format!("Subtitle languages. Can be used multiple times. \
@@ -292,7 +292,7 @@ async fn formats_from_series(
// get all locales which are specified but not present in the current iterated season and
// print an error saying this
let not_present_audio = archive
- .audio
+ .locale
.clone()
.into_iter()
.filter(|l| !season.iter().any(|s| &s.metadata.audio_locale == l))
@@ -309,7 +309,7 @@ async fn formats_from_series(
// remove all seasons with the wrong audio for the current iterated season number
seasons.retain(|s| {
s.metadata.season_number != season.first().unwrap().metadata.season_number
- || archive.audio.contains(&s.metadata.audio_locale)
+ || archive.locale.contains(&s.metadata.audio_locale)
})
}
@@ -318,7 +318,7 @@ async fn formats_from_series(
BTreeMap::new();
for season in series.seasons().await? {
if !url_filter.is_season_valid(season.metadata.season_number)
- || !archive.audio.contains(&season.metadata.audio_locale)
+ || !archive.locale.contains(&season.metadata.audio_locale)
{
continue;
}
From a7c2bbe807129e8844cd30817a2770f0879085c8 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 15:18:55 +0100
Subject: [PATCH 300/732] Add ci workflow
---
.github/workflows/ci.yml | 133 +++++++++++++++++++++++++++++++++++++++
1 file changed, 133 insertions(+)
create mode 100644 .github/workflows/ci.yml
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f9b30a5
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,133 @@
+name: ci
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-latest
+ toolchain: x86_64-unknown-linux-musl
+ - os: windows-latest
+ toolchain: x86_64-pc-windows-msvc
+ - os: macos-latest
+ toolchain: x86_64-apple-darwin
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Cargo cache # https://github.com/actions/cache/blob/main/examples.md#rust---cargo
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ target: ${{ matrix.toolchain }}
+ default: true
+
+ - name: Test
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+ args: --all-features
+
+ build:
+ if: github.ref == 'refs/heads/master'
+ needs:
+ - test
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-latest
+ toolchain: x86_64-unknown-linux-musl
+ ext:
+ output: crunchy_linux
+ - os: windows-latest
+ toolchain: x86_64-pc-windows-msvc
+ ext: .exe
+ output: crunchy_windows.exe
+ - os: macos-latest
+ toolchain: x86_64-apple-darwin
+ ext:
+ output: crunchy_darwin
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+
+ - name: Cargo cache # https://github.com/actions/cache/blob/main/examples.md#rust---cargo
+ uses: actions/cache@v3
+ with:
+ path: |
+ ~/.cargo/bin/
+ ~/.cargo/registry/index/
+ ~/.cargo/registry/cache/
+ ~/.cargo/git/db/
+ target/
+ key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Install toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ target: ${{ matrix.toolchain }}
+ default: true
+
+ - name: Build
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+ args: --release --all-features
+
+ - name: Bundle manpages
+ uses: thedoctor0/zip-release@0.6
+ with:
+ type: zip
+ filename: manpages.zip
+ path: ./target/release/manpages
+
+ - name: Bundle completions
+ uses: thedoctor0/zip-release@0.6
+ with:
+ type: zip
+ filename: completions.zip
+ path: ./target/release/completions
+
+ - name: Upload binary artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name: ${{ matrix.output }}
+ path: ./target/release/crunchy-cli${{ matrix.ext }}
+ if-no-files-found: error
+
+ - name: Upload manpages artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name:
+ path: ./manpages.zip
+ if-no-files-found: error
+
+ - name: Upload completions artifact
+ uses: actions/upload-artifact@v3
+ with:
+ name:
+ path: ./completions.zip
+ if-no-files-found: error
From 3f4ce3a0a92e4fa517b7dc03d66b014b5f7673dc Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 27 Nov 2022 17:11:26 +0100
Subject: [PATCH 301/732] Fix ci zip action version
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f9b30a5..50a41e7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -98,14 +98,14 @@ jobs:
args: --release --all-features
- name: Bundle manpages
- uses: thedoctor0/zip-release@0.6
+ uses: thedoctor0/zip-release@0:6
with:
type: zip
filename: manpages.zip
path: ./target/release/manpages
- name: Bundle completions
- uses: thedoctor0/zip-release@0.6
+ uses: thedoctor0/zip-release@0:6
with:
type: zip
filename: completions.zip
From a7605884414afacf120a5ffdf94bf6d53de1a78a Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 27 Nov 2022 17:11:32 +0100
Subject: [PATCH 302/732] Add ci badge
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index db06b99..0dcbc63 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,9 @@ A [Rust](https://www.rust-lang.org/) written cli client for [Crunchyroll](https:
+
+
+
From 8d8333e41413546d6cf7ca66f2afa215d7c382b2 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 27 Nov 2022 17:16:12 +0100
Subject: [PATCH 303/732] Fix ci zip workflow (again)
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 50a41e7..c27195a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -98,14 +98,14 @@ jobs:
args: --release --all-features
- name: Bundle manpages
- uses: thedoctor0/zip-release@0:6
+ uses: TheDoctor0/zip-release@0.6.2
with:
type: zip
filename: manpages.zip
path: ./target/release/manpages
- name: Bundle completions
- uses: thedoctor0/zip-release@0:6
+ uses: TheDoctor0/zip-release@0.6.2
with:
type: zip
filename: completions.zip
From 45c315e9bb1008e54f1e6a2bb03e42d5d8c4f134 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Sun, 27 Nov 2022 17:27:12 +0100
Subject: [PATCH 304/732] Fix ci upload artifact action
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c27195a..01ad86f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -121,13 +121,13 @@ jobs:
- name: Upload manpages artifact
uses: actions/upload-artifact@v3
with:
- name:
+ name: manpages.zip
path: ./manpages.zip
if-no-files-found: error
- name: Upload completions artifact
uses: actions/upload-artifact@v3
with:
- name:
+ name: completions.zip
path: ./completions.zip
if-no-files-found: error
From b118b74b994608a73a9495a8d8d2fcafb78db364 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 19:57:37 +0100
Subject: [PATCH 305/732] Update README
---
README.md | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 0dcbc63..f62c988 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,8 @@ A [Rust](https://www.rust-lang.org/) written cli client for [Crunchyroll](https:
-
-
+
+
@@ -44,9 +44,7 @@ A [Rust](https://www.rust-lang.org/) written cli client for [Crunchyroll](https:
### 📥 Download the latest binaries
-~~Checkout the [releases](https://github.com/crunchy-labs/crunchy-cli/releases) tab and get the binary from the newest release.~~
-
-Currently, no pre-built binary of the rewrite / this branch is available.
+Checkout the [releases](https://github.com/crunchy-labs/crunchy-cli/releases) tab and get the binary from the newest (pre-)release.
### 🛠 Build it yourself
From f3f41aa0a2c94b595eabfae119feed9cafa64045 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 19:58:14 +0100
Subject: [PATCH 306/732] Add crunchy-cli-core/Cargo.lock
---
crunchy-cli-core/Cargo.lock | 1608 +++++++++++++++++++++++++++++++++++
1 file changed, 1608 insertions(+)
create mode 100644 crunchy-cli-core/Cargo.lock
diff --git a/crunchy-cli-core/Cargo.lock b/crunchy-cli-core/Cargo.lock
new file mode 100644
index 0000000..61ea0ff
--- /dev/null
+++ b/crunchy-cli-core/Cargo.lock
@@ -0,0 +1,1608 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aes"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241"
+dependencies = [
+ "cfg-if",
+ "cipher",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+
+[[package]]
+name = "async-channel"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28"
+dependencies = [
+ "concurrent-queue",
+ "event-listener",
+ "futures-core",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-padding"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+
+[[package]]
+name = "bytes"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
+
+[[package]]
+name = "cache-padded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
+
+[[package]]
+name = "castaway"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6"
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-integer",
+ "num-traits",
+ "serde",
+ "time",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clap"
+version = "4.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "clap_lex",
+ "once_cell",
+ "strsim",
+ "termcolor",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8"
+dependencies = [
+ "os_str_bytes",
+]
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "1.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c"
+dependencies = [
+ "cache-padded",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crunchy-cli-core"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "chrono",
+ "clap",
+ "crunchyroll-rs",
+ "ctrlc",
+ "dirs",
+ "isahc",
+ "log",
+ "num_cpus",
+ "regex",
+ "signal-hook",
+ "sys-locale",
+ "tempfile",
+ "terminal_size",
+ "tokio",
+]
+
+[[package]]
+name = "crunchyroll-rs"
+version = "0.1.0"
+source = "git+https://github.com/crunchy-labs/crunchyroll-rs#7aedfc6c9a91a42ef46639ba9e99adba63cd0dda"
+dependencies = [
+ "aes",
+ "cbc",
+ "chrono",
+ "crunchyroll-rs-internal",
+ "http",
+ "isahc",
+ "m3u8-rs",
+ "regex",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smart-default",
+ "tokio",
+]
+
+[[package]]
+name = "crunchyroll-rs-internal"
+version = "0.1.0"
+source = "git+https://github.com/crunchy-labs/crunchyroll-rs#7aedfc6c9a91a42ef46639ba9e99adba63cd0dda"
+dependencies = [
+ "darling",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "ctrlc"
+version = "3.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173"
+dependencies = [
+ "nix",
+ "winapi",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.59+curl-7.86.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407"
+dependencies = [
+ "cc",
+ "libc",
+ "libnghttp2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "winapi",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "darling"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0dd3cd20dc6b5a876612a6e5accfe7f3dd883db6d07acfbf14c128f61550dfa"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a784d2ccaf7c98501746bf0be29b2022ba41fd62a2e622af997a03e9f972859f"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7618812407e9402654622dd402b0a89dff9ba93badd6540781526117b92aab7e"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "fastrand"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+
+[[package]]
+name = "futures-io"
+version = "0.3.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
+
+[[package]]
+name = "futures-lite"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+
+[[package]]
+name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "http"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "httpdate"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074"
+
+[[package]]
+name = "isahc"
+version = "1.7.0"
+source = "git+https://github.com/sagebind/isahc?rev=34f158ef#34f158ef9f87b2387bed2c81936916a29c1eaad1"
+dependencies = [
+ "async-channel",
+ "castaway",
+ "crossbeam-utils",
+ "curl",
+ "curl-sys",
+ "encoding_rs",
+ "event-listener",
+ "futures-lite",
+ "http",
+ "httpdate",
+ "log",
+ "mime",
+ "once_cell",
+ "polling",
+ "slab",
+ "sluice",
+ "tracing",
+ "tracing-futures",
+ "url",
+ "waker-fn",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.137"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
+
+[[package]]
+name = "libnghttp2-sys"
+version = "0.1.7+1.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "m3u8-rs"
+version = "5.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15d091887fd4a920417805283b7a838d0dcda68e8d632cd305a4439ee776d1ce"
+dependencies = [
+ "chrono",
+ "nom",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "mime"
+version = "0.3.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nix"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
+dependencies = [
+ "autocfg",
+ "bitflags",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+dependencies = [
+ "autocfg",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-src"
+version = "111.24.0+1.1.1s"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07d5c8cb6e57b3a3612064d7b18b117912b4ce70955c2504d4b741c9e244b132"
+dependencies = [
+ "autocfg",
+ "cc",
+ "libc",
+ "openssl-src",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "os_str_bytes"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
+
+[[package]]
+name = "parking"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72"
+
+[[package]]
+name = "percent-encoding"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+
+[[package]]
+name = "pin-project"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "polling"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "libc",
+ "log",
+ "wepoll-ffi",
+ "winapi",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rustix"
+version = "0.35.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9"
+dependencies = [
+ "bitflags",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "schannel"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+dependencies = [
+ "lazy_static",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+
+[[package]]
+name = "serde"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "signal-hook"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
+dependencies = [
+ "libc",
+ "signal-hook-registry",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "sluice"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5"
+dependencies = [
+ "async-channel",
+ "futures-core",
+ "futures-io",
+]
+
+[[package]]
+name = "smart-default"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sys-locale"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee"
+dependencies = [
+ "js-sys",
+ "libc",
+ "wasm-bindgen",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "libc",
+ "redox_syscall",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40ca90c434fd12083d1a6bdcbe9f92a14f96c8a1ba600ba451734ac334521f7a"
+dependencies = [
+ "rustix",
+ "windows-sys 0.42.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
+dependencies = [
+ "libc",
+ "wasi 0.10.0+wasi-snapshot-preview1",
+ "winapi",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+
+[[package]]
+name = "tokio"
+version = "1.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
+dependencies = [
+ "autocfg",
+ "num_cpus",
+ "pin-project-lite",
+ "tokio-macros",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "tracing-futures"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2"
+dependencies = [
+ "pin-project",
+ "tracing",
+]
+
+[[package]]
+name = "typenum"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "url"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "waker-fn"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
+
+[[package]]
+name = "wasi"
+version = "0.10.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wepoll-ffi"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.0",
+ "windows_i686_gnu 0.42.0",
+ "windows_i686_msvc 0.42.0",
+ "windows_x86_64_gnu 0.42.0",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.0",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
From 9bfe6b0e54f996116e786cda2b59bdeba9204461 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 20:02:37 +0100
Subject: [PATCH 307/732] Update authors
---
Cargo.toml | 3 ++-
crunchy-cli-core/Cargo.toml | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/Cargo.toml b/Cargo.toml
index c99a2a9..b9adc07 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
[package]
name = "crunchy-cli"
-version = "0.1.0"
+authors = ["Crunchy Labs Maintainers"]
+version = "3.0.0"
edition = "2021"
[features]
diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml
index 893b278..e6bba13 100644
--- a/crunchy-cli-core/Cargo.toml
+++ b/crunchy-cli-core/Cargo.toml
@@ -1,5 +1,6 @@
[package]
name = "crunchy-cli-core"
+authors = ["Crunchy Labs Maintainers"]
version = "0.1.0"
edition = "2021"
From 7fe587a89168222ef1a6be7a29049fddcf5bbcb9 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 20:22:59 +0100
Subject: [PATCH 308/732] Update version
---
Cargo.lock | 4 ++--
Cargo.toml | 2 +-
crunchy-cli-core/Cargo.lock | 2 +-
crunchy-cli-core/Cargo.toml | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 05b2d65..4c3ba10 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -263,7 +263,7 @@ dependencies = [
[[package]]
name = "crunchy-cli"
-version = "0.1.0"
+version = "3.0.0-beta.1"
dependencies = [
"chrono",
"clap",
@@ -275,7 +275,7 @@ dependencies = [
[[package]]
name = "crunchy-cli-core"
-version = "0.1.0"
+version = "3.0.0-beta.1"
dependencies = [
"anyhow",
"async-trait",
diff --git a/Cargo.toml b/Cargo.toml
index b9adc07..4cdf8dc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "crunchy-cli"
authors = ["Crunchy Labs Maintainers"]
-version = "3.0.0"
+version = "3.0.0-beta.1"
edition = "2021"
[features]
diff --git a/crunchy-cli-core/Cargo.lock b/crunchy-cli-core/Cargo.lock
index 61ea0ff..1502091 100644
--- a/crunchy-cli-core/Cargo.lock
+++ b/crunchy-cli-core/Cargo.lock
@@ -244,7 +244,7 @@ dependencies = [
[[package]]
name = "crunchy-cli-core"
-version = "0.1.0"
+version = "3.0.0-beta.1"
dependencies = [
"anyhow",
"async-trait",
diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml
index e6bba13..e0e83be 100644
--- a/crunchy-cli-core/Cargo.toml
+++ b/crunchy-cli-core/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "crunchy-cli-core"
authors = ["Crunchy Labs Maintainers"]
-version = "0.1.0"
+version = "3.0.0-beta.1"
edition = "2021"
[features]
From 1487ba222eeb10adb99f3c7536a18eaca1cb031b Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 20:24:10 +0100
Subject: [PATCH 309/732] Add short commit sha to ci build artifacts
---
.github/workflows/ci.yml | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 01ad86f..cdc3136 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,16 +58,16 @@ jobs:
include:
- os: ubuntu-latest
toolchain: x86_64-unknown-linux-musl
+ platform: linux
ext:
- output: crunchy_linux
- os: windows-latest
toolchain: x86_64-pc-windows-msvc
+ platform: windows
ext: .exe
- output: crunchy_windows.exe
- os: macos-latest
toolchain: x86_64-apple-darwin
+ platform: darwin
ext:
- output: crunchy_darwin
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -98,36 +98,41 @@ jobs:
args: --release --all-features
- name: Bundle manpages
- uses: TheDoctor0/zip-release@0.6.2
+ uses: thedoctor0/zip-release@0.6
with:
type: zip
filename: manpages.zip
path: ./target/release/manpages
- name: Bundle completions
- uses: TheDoctor0/zip-release@0.6.2
+ uses: thedoctor0/zip-release@0.6
with:
type: zip
filename: completions.zip
path: ./target/release/completions
+ - name: Get short commit SHA
+ id: short_commit_sha
+ shell: bash
+ run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+
- name: Upload binary artifact
uses: actions/upload-artifact@v3
with:
- name: ${{ matrix.output }}
+ name: crunchy-${{ short_commit_sha.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
path: ./target/release/crunchy-cli${{ matrix.ext }}
if-no-files-found: error
- name: Upload manpages artifact
uses: actions/upload-artifact@v3
with:
- name: manpages.zip
+ name: manpages-${{ short_commit_sha.sha_short }}.zip
path: ./manpages.zip
if-no-files-found: error
- name: Upload completions artifact
uses: actions/upload-artifact@v3
with:
- name: completions.zip
+ name: completions-${{ short_commit_sha.sha_short }}.zip
path: ./completions.zip
if-no-files-found: error
From 81931829b0859590512019a3147de447cc065001 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 20:43:13 +0100
Subject: [PATCH 310/732] Fix invalid ci file syntax
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cdc3136..dcf1c56 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -119,20 +119,20 @@ jobs:
- name: Upload binary artifact
uses: actions/upload-artifact@v3
with:
- name: crunchy-${{ short_commit_sha.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
+ name: crunchy-${{ steps.short_commit_sha.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
path: ./target/release/crunchy-cli${{ matrix.ext }}
if-no-files-found: error
- name: Upload manpages artifact
uses: actions/upload-artifact@v3
with:
- name: manpages-${{ short_commit_sha.sha_short }}.zip
+ name: manpages-${{ steps.short_commit_sha.sha_short }}.zip
path: ./manpages.zip
if-no-files-found: error
- name: Upload completions artifact
uses: actions/upload-artifact@v3
with:
- name: completions-${{ short_commit_sha.sha_short }}.zip
+ name: completions-${{ steps.short_commit_sha.sha_short }}.zip
path: ./completions.zip
if-no-files-found: error
From c99eedd7a7c0b29d22131813c2c08d085daa8907 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 22:14:32 +0100
Subject: [PATCH 311/732] Re-fix ci zip workflow
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dcf1c56..4e67182 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -98,14 +98,14 @@ jobs:
args: --release --all-features
- name: Bundle manpages
- uses: thedoctor0/zip-release@0.6
+ uses: thedoctor0/zip-release@0.6.2
with:
type: zip
filename: manpages.zip
path: ./target/release/manpages
- name: Bundle completions
- uses: thedoctor0/zip-release@0.6
+ uses: thedoctor0/zip-release@0.6.2
with:
type: zip
filename: completions.zip
From 24fbedc7d7a52bf96dec9f40b2313345a53b908b Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Sun, 27 Nov 2022 22:42:15 +0100
Subject: [PATCH 312/732] Fix ci short commit sha
---
.github/workflows/ci.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4e67182..9c6dba9 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -119,20 +119,20 @@ jobs:
- name: Upload binary artifact
uses: actions/upload-artifact@v3
with:
- name: crunchy-${{ steps.short_commit_sha.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
+ name: crunchy-${{ steps.short_commit_sha.outputs.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
path: ./target/release/crunchy-cli${{ matrix.ext }}
if-no-files-found: error
- name: Upload manpages artifact
uses: actions/upload-artifact@v3
with:
- name: manpages-${{ steps.short_commit_sha.sha_short }}.zip
+ name: manpages-${{ steps.short_commit_sha.outputs.sha_short }}.zip
path: ./manpages.zip
if-no-files-found: error
- name: Upload completions artifact
uses: actions/upload-artifact@v3
with:
- name: completions-${{ steps.short_commit_sha.sha_short }}.zip
+ name: completions-${{ steps.short_commit_sha.outputs.sha_short }}.zip
path: ./completions.zip
if-no-files-found: error
From 59b5e3d239c2ad0596f67a21bea248cb90aa2f01 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Nov 2022 10:11:20 +0100
Subject: [PATCH 313/732] Update version
---
Cargo.lock | 4 ++--
Cargo.toml | 2 +-
crunchy-cli-core/Cargo.lock | 2 +-
crunchy-cli-core/Cargo.toml | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 4c3ba10..26ffd7f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -263,7 +263,7 @@ dependencies = [
[[package]]
name = "crunchy-cli"
-version = "3.0.0-beta.1"
+version = "3.0.0-dev.1"
dependencies = [
"chrono",
"clap",
@@ -275,7 +275,7 @@ dependencies = [
[[package]]
name = "crunchy-cli-core"
-version = "3.0.0-beta.1"
+version = "3.0.0-dev.1"
dependencies = [
"anyhow",
"async-trait",
diff --git a/Cargo.toml b/Cargo.toml
index 4cdf8dc..7614202 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "crunchy-cli"
authors = ["Crunchy Labs Maintainers"]
-version = "3.0.0-beta.1"
+version = "3.0.0-dev.1"
edition = "2021"
[features]
diff --git a/crunchy-cli-core/Cargo.lock b/crunchy-cli-core/Cargo.lock
index 1502091..61bc6f6 100644
--- a/crunchy-cli-core/Cargo.lock
+++ b/crunchy-cli-core/Cargo.lock
@@ -244,7 +244,7 @@ dependencies = [
[[package]]
name = "crunchy-cli-core"
-version = "3.0.0-beta.1"
+version = "3.0.0-dev.1"
dependencies = [
"anyhow",
"async-trait",
diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml
index e0e83be..067679b 100644
--- a/crunchy-cli-core/Cargo.toml
+++ b/crunchy-cli-core/Cargo.toml
@@ -1,7 +1,7 @@
[package]
name = "crunchy-cli-core"
authors = ["Crunchy Labs Maintainers"]
-version = "3.0.0-beta.1"
+version = "3.0.0-dev.1"
edition = "2021"
[features]
From b1182d4f7b23adc8042998980afa53b4c5c3c5f4 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Nov 2022 11:54:04 +0100
Subject: [PATCH 314/732] Add (short) commit hash and build time to version
hash
---
build.rs | 7 -------
crunchy-cli-core/Cargo.toml | 5 ++++-
crunchy-cli-core/build.rs | 33 +++++++++++++++++++++++++++++++++
crunchy-cli-core/src/lib.rs | 30 +++++++++++++++++++++++++-----
4 files changed, 62 insertions(+), 13 deletions(-)
create mode 100644 crunchy-cli-core/build.rs
diff --git a/build.rs b/build.rs
index cc6a0cc..1e4d71f 100644
--- a/build.rs
+++ b/build.rs
@@ -2,14 +2,7 @@ use clap::{Command, CommandFactory};
use clap_complete::shells;
use std::path::{Path, PathBuf};
-// this build file generates completions for various shells as well as manual pages
-
fn main() -> std::io::Result<()> {
- // do not generate anything when building non release
- if cfg!(debug_assertions) {
- return Ok(());
- }
-
// note that we're using an anti-pattern here / violate the rust conventions. build script are
// not supposed to write outside of 'OUT_DIR'. to have the generated files in the build "root"
// (the same directory where the output binary lives) is much simpler than in 'OUT_DIR' since
diff --git a/crunchy-cli-core/Cargo.toml b/crunchy-cli-core/Cargo.toml
index 067679b..00635b0 100644
--- a/crunchy-cli-core/Cargo.toml
+++ b/crunchy-cli-core/Cargo.toml
@@ -14,7 +14,7 @@ static-ssl = ["crunchyroll-rs/static-ssl"]
[dependencies]
anyhow = "1.0"
async-trait = "0.1"
-clap = { version = "4.0", features = ["derive"] }
+clap = { version = "4.0", features = ["derive", "string"] }
chrono = "0.4"
crunchyroll-rs = { git = "https://github.com/crunchy-labs/crunchyroll-rs", default-features = false, features = ["stream", "parse"] }
ctrlc = "3.2"
@@ -28,3 +28,6 @@ tempfile = "3.3"
terminal_size = "0.2"
tokio = { version = "1.21", features = ["macros", "rt-multi-thread", "time"] }
sys-locale = "0.2"
+
+[build-dependencies]
+chrono = "0.4"
diff --git a/crunchy-cli-core/build.rs b/crunchy-cli-core/build.rs
new file mode 100644
index 0000000..f7d5974
--- /dev/null
+++ b/crunchy-cli-core/build.rs
@@ -0,0 +1,33 @@
+fn main() -> std::io::Result<()> {
+ println!(
+ "cargo:rustc-env=GIT_HASH={}",
+ get_short_commit_hash()?.unwrap_or_default()
+ );
+ println!(
+ "cargo:rustc-env=BUILD_DATE={}",
+ chrono::Utc::now().format("%F")
+ );
+
+ Ok(())
+}
+
+fn get_short_commit_hash() -> std::io::Result> {
+ let git = std::process::Command::new("git")
+ .arg("rev-parse")
+ .arg("--short")
+ .arg("HEAD")
+ .output();
+
+ match git {
+ Ok(cmd) => Ok(Some(
+ String::from_utf8_lossy(cmd.stdout.as_slice()).to_string(),
+ )),
+ Err(e) => {
+ if e.kind() != std::io::ErrorKind::NotFound {
+ Err(e)
+ } else {
+ Ok(None)
+ }
+ }
+ }
+}
diff --git a/crunchy-cli-core/src/lib.rs b/crunchy-cli-core/src/lib.rs
index faef927..4571337 100644
--- a/crunchy-cli-core/src/lib.rs
+++ b/crunchy-cli-core/src/lib.rs
@@ -20,13 +20,15 @@ trait Execute {
}
#[derive(Debug, Parser)]
-#[clap(author, version, about)]
+#[clap(author, version = version(), about)]
#[clap(name = "crunchy-cli")]
pub struct Cli {
#[clap(flatten)]
verbosity: Option,
- #[arg(help = "Overwrite the language in which results are returned. Default is your system language")]
+ #[arg(
+ help = "Overwrite the language in which results are returned. Default is your system language"
+ )]
#[arg(long)]
lang: Option,
@@ -37,6 +39,18 @@ pub struct Cli {
command: Command,
}
+fn version() -> String {
+ let package_version = env!("CARGO_PKG_VERSION");
+ let git_commit_hash = env!("GIT_HASH");
+ let build_date = env!("BUILD_DATE");
+
+ if git_commit_hash.is_empty() {
+ format!("{}", package_version)
+ } else {
+ format!("{} ({} {})", package_version, git_commit_hash, build_date)
+ }
+}
+
#[derive(Debug, Subcommand)]
enum Command {
Archive(Archive),
@@ -51,7 +65,9 @@ struct Verbosity {
v: bool,
#[arg(help = "Quiet output. Does not print anything unless it's a error")]
- #[arg(long_help = "Quiet output. Does not print anything unless it's a error. Can be helpful if you pipe the output to stdout")]
+ #[arg(
+ long_help = "Quiet output. Does not print anything unless it's a error. Can be helpful if you pipe the output to stdout"
+ )]
#[arg(short)]
q: bool,
}
@@ -59,11 +75,15 @@ struct Verbosity {
#[derive(Debug, Parser)]
struct LoginMethod {
#[arg(help = "Login with credentials (username or email and password)")]
- #[arg(long_help = "Login with credentials (username or email and password). Must be provided as user:password")]
+ #[arg(
+ long_help = "Login with credentials (username or email and password). Must be provided as user:password"
+ )]
#[arg(long)]
credentials: Option,
#[arg(help = "Login with the etp-rt cookie")]
- #[arg(long_help = "Login with the etp-rt cookie. This can be obtained when you login on crunchyroll.com and extract it from there")]
+ #[arg(
+ long_help = "Login with the etp-rt cookie. This can be obtained when you login on crunchyroll.com and extract it from there"
+ )]
#[arg(long)]
etp_rt: Option,
}
From 2d89a712039f314faf4a28774b3b9824278c5f04 Mon Sep 17 00:00:00 2001
From: bytedream
Date: Mon, 28 Nov 2022 12:18:44 +0100
Subject: [PATCH 315/732] Remove commit sha in filename
---
.github/workflows/ci.yml | 29 +++++------------------------
1 file changed, 5 insertions(+), 24 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9c6dba9..f5d01b6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -97,42 +97,23 @@ jobs:
command: build
args: --release --all-features
- - name: Bundle manpages
- uses: thedoctor0/zip-release@0.6.2
- with:
- type: zip
- filename: manpages.zip
- path: ./target/release/manpages
-
- - name: Bundle completions
- uses: thedoctor0/zip-release@0.6.2
- with:
- type: zip
- filename: completions.zip
- path: ./target/release/completions
-
- - name: Get short commit SHA
- id: short_commit_sha
- shell: bash
- run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-
- name: Upload binary artifact
uses: actions/upload-artifact@v3
with:
- name: crunchy-${{ steps.short_commit_sha.outputs.sha_short }}_${{ matrix.platform }}${{ matrix.ext }}
+ name: crunchy-cli_${{ matrix.platform }}
path: ./target/release/crunchy-cli${{ matrix.ext }}
if-no-files-found: error
- name: Upload manpages artifact
uses: actions/upload-artifact@v3
with:
- name: manpages-${{ steps.short_commit_sha.outputs.sha_short }}.zip
- path: ./manpages.zip
+ name: manpages
+ path: ./target/release/manpages
if-no-files-found: error
- name: Upload completions artifact
uses: actions/upload-artifact@v3
with:
- name: completions-${{ steps.short_commit_sha.outputs.sha_short }}.zip
- path: ./completions.zip
+ name: completions
+ path: ./target/release/completions
if-no-files-found: error
From 99002e606f11aca2f07d079ee60af555c4fc99f4 Mon Sep 17 00:00:00 2001
From: ByteDream
Date: Mon, 28 Nov 2022 22:14:57 +0100
Subject: [PATCH 316/732] Change windows ci toolchain to gnu
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f5d01b6..9aba2cd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
- os: ubuntu-latest
toolchain: x86_64-unknown-linux-musl
- os: windows-latest
- toolchain: x86_64-pc-windows-msvc
+ toolchain: x86_64-pc-windows-gnu
- os: macos-latest
toolchain: x86_64-apple-darwin
steps:
@@ -61,7 +61,7 @@ jobs:
platform: linux
ext:
- os: windows-latest
- toolchain: x86_64-pc-windows-msvc
+ toolchain: x86_64-pc-windows-gnu
platform: windows
ext: .exe
- os: macos-latest
From 4095b80477f30788aa262a50655e29265adba9d0 Mon Sep 17 00:00:00 2001
From: ByteDream