Version 1.2.0

This commit is contained in:
bytedream 2021-10-14 19:57:10 +02:00
parent 10a58ae932
commit 4242a2f4cf
4 changed files with 192 additions and 38 deletions

View file

@ -1,4 +1,4 @@
VERSION=1.1.0 VERSION=1.2.0
BINARY_NAME=crunchy BINARY_NAME=crunchy
VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION) VERSION_BINARY_NAME=$(BINARY_NAME)-v$(VERSION)

View file

@ -26,6 +26,8 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
<a href="#-credits">Credits 🙏</a> <a href="#-credits">Credits 🙏</a>
<a href="#-notice">Notice 🗒️</a>
<a href="#-license">License ⚖</a> <a href="#-license">License ⚖</a>
</p> </p>
@ -36,9 +38,9 @@ A [Go](https://golang.org) library & cli for the undocumented [crunchyroll](http
#### Get the executable #### Get the executable
- 📥 Download the latest binaries [here](https://github.com/ByteDream/crunchyroll-go/releases/latest) or get it from below - 📥 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.1.0/crunchy-v1.1.0_linux) - [Linux (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.0/crunchy-v1.2.0_linux)
- [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.1.0/crunchy-v1.1.0_windows.exe) - [Windows (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.0/crunchy-v1.2.0_windows.exe)
- [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.1.0/crunchy-v1.1.0_darwin) - [MacOS (x64)](https://github.com/ByteDream/crunchyroll-go/releases/download/v1.2.0/crunchy-v1.2.0_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/) - 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 $ yay -S crunchyroll-go
@ -111,6 +113,7 @@ $ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/da
- `--audio` » forces audio of the video(s) - `--audio` » forces audio of the video(s)
- `--subtitle` » forces subtitle 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 - `--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 - `-d`, `--directory` » directory to download the video(s) to
- `-o`, `--output` » name of the output file - `-o`, `--output` » name of the output file
@ -119,6 +122,8 @@ $ crunchy download --audio ja-JP --subtitle de-DE https://www.crunchyroll.com/da
- `--alternative-progress` » shows an alternative, not so user-friendly progress instead of the progress bar - `--alternative-progress` » shows an alternative, not so user-friendly progress instead of the progress bar
- `-g`, `--goroutines` » sets how many parallel segment downloads should be used
#### Help #### Help
- General help - General help
``` ```
@ -329,6 +334,10 @@ $ go test .
- [m3u8](https://github.com/grafov/m3u8) (not the m3u8 library from above) » mpeg stream info library - [m3u8](https://github.com/grafov/m3u8) (not the m3u8 library from above) » mpeg stream info library
- [cobra](https://github.com/spf13/cobra) » cli library - [cobra](https://github.com/spf13/cobra) » cli library
# 🗒️ Notice
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 # ⚖ License
This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file This project is licensed under the GNU Lesser General Public License v3.0 (LGPL-3.0) - see the [LICENSE](LICENSE) file

View file

@ -24,6 +24,7 @@ var (
audioFlag string audioFlag string
subtitleFlag string subtitleFlag string
noHardsubFlag bool noHardsubFlag bool
onlySubFlag bool
directoryFlag string directoryFlag string
outputFlag string outputFlag string
@ -31,6 +32,8 @@ var (
resolutionFlag string resolutionFlag string
alternativeProgressFlag bool alternativeProgressFlag bool
goroutinesFlag int
) )
var getCmd = &cobra.Command{ var getCmd = &cobra.Command{
@ -67,6 +70,7 @@ func init() {
getCmd.Flags().StringVarP(&audioFlag, "audio", "a", "", "The locale of the audio. Available locales: "+strings.Join(allLocalesAsStrings(), ", ")) 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().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(&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")
cwd, _ := os.Getwd() cwd, _ := os.Getwd()
getCmd.Flags().StringVarP(&directoryFlag, "directory", "d", cwd, "The directory to download the file to") getCmd.Flags().StringVarP(&directoryFlag, "directory", "d", cwd, "The directory to download the file to")
@ -87,15 +91,19 @@ func init() {
"\tAvailable common-use words: best (best available resolution), worst (worst available resolution)\n") "\tAvailable common-use words: best (best available resolution), worst (worst available resolution)\n")
getCmd.Flags().BoolVar(&alternativeProgressFlag, "alternative-progress", false, "Shows an alternative, not so user-friendly progress instead of the progress bar") getCmd.Flags().BoolVar(&alternativeProgressFlag, "alternative-progress", false, "Shows an alternative, not so user-friendly progress instead of the progress bar")
// 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")
} }
type episodeInformation struct { type episodeInformation struct {
Format *crunchyroll.Format Format *crunchyroll.Format
Title string Title string
URL string URL string
SeriesTitle string SeriesTitle string
SeasonNum int SeasonNum int
EpisodeNum int EpisodeNum int
AllSubtitles []*crunchyroll.Subtitle
} }
type information struct { type information struct {
@ -113,9 +121,23 @@ type information struct {
} }
func download(urls []string) { func download(urls []string) {
if path.Ext(outputFlag) != ".ts" && !hasFFmpeg() { switch path.Ext(outputFlag) {
out.Fatalf("The file ending for the output file (%s) is not `.ts`. "+ case ".ts":
"Install ffmpeg (https://ffmpeg.org/download.html) use other media file endings (e.g. `.mp4`)\n", outputFlag) // 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) allEpisodes, total, successes := parseURLs(urls)
out.Infof("%d of %d episodes could be parsed\n", successes, total) out.Infof("%d of %d episodes could be parsed\n", successes, total)
@ -124,12 +146,22 @@ func download(urls []string) {
if len(allEpisodes) == 0 { if len(allEpisodes) == 0 {
out.Fatalf("Nothing to download, aborting\n") out.Fatalf("Nothing to download, aborting\n")
} }
out.Infof("Downloads:") if onlySubFlag {
out.Infof("Downloads (only subtitles):")
} else {
out.Infof("Downloads:")
}
for i, episode := range allEpisodes { for i, episode := range allEpisodes {
video := episode.Format.Video video := episode.Format.Video
out.Infof("\t%d. %s » %spx, %.2f FPS, %s audio (%s S%02dE%02d)\n", if onlySubFlag && subtitleFlag == "" {
i+1, episode.Title, video.Resolution, video.FrameRate, utils.LocaleLanguage(episode.Format.AudioLocale), episode.SeriesTitle, episode.SeasonNum, episode.EpisodeNum) 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 fileInfo, stat := os.Stat(directoryFlag); os.IsNotExist(stat) {
if err := os.MkdirAll(directoryFlag, 0777); err != nil { if err := os.MkdirAll(directoryFlag, 0777); err != nil {
@ -195,13 +227,63 @@ func download(urls []string) {
filename = strings.ReplaceAll(filename, char, "") filename = strings.ReplaceAll(filename, char, "")
} }
out.Empty() if onlySubFlag {
if downloadFormat(episode.Format, filename, info) { var found bool
success++ 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()
} }
} }
out.Infof("Downloaded %d out of %d videos\n", success, len(allEpisodes)) 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) { func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successes int) {
@ -259,7 +341,7 @@ func parseURLs(urls []string) (allEpisodes []episodeInformation, total, successe
allEpisodes = append(allEpisodes, parsed...) allEpisodes = append(allEpisodes, parsed...)
} else if _, _, ok = crunchyroll.MatchEpisode(url); ok { } else if _, _, ok = crunchyroll.MatchEpisode(url); ok {
out.Debugf("Parsed url %d as episode\n", i+1) out.Debugf("Parsed url %d as episode\n", i+1)
if episode := parseEpisodes(dupe.(*utils.EpisodeStructure), url); (episodeInformation{}) != episode { if episode := parseEpisodes(dupe.(*utils.EpisodeStructure), url); episode.Format != nil {
allEpisodes = append(allEpisodes, episode) allEpisodes = append(allEpisodes, episode)
localSuccesses++ localSuccesses++
} else { } else {
@ -325,6 +407,13 @@ func parseVideo(videoStructure utils.VideoStructure, url string) (episodeInforma
info.SeasonNum, info.EpisodeNum = 1, 1 info.SeasonNum, info.EpisodeNum = 1, 1
} }
for _, audioFormat := range formats {
if audioFormat.AudioLocale == crunchyroll.JP {
info.AllSubtitles = audioFormat.Subtitles
break
}
}
episodeInformations = append(episodeInformations, info) episodeInformations = append(episodeInformations, info)
out.Debugf("Successful parsed %s\n", title) out.Debugf("Successful parsed %s\n", title)
} }
@ -338,18 +427,27 @@ func parseEpisodes(episodeStructure *utils.EpisodeStructure, url string) episode
episode, _ := episodeStructure.GetEpisodeByURL(url) episode, _ := episodeStructure.GetEpisodeByURL(url)
ordered, _ := episodeStructure.OrderFormatsByEpisodeNumber() ordered, _ := episodeStructure.OrderFormatsByEpisodeNumber()
var subtitles []*crunchyroll.Subtitle
formats := ordered[episode.EpisodeNumber] 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)) out.Debugf("Found %d formats\n", len(formats))
if format := findFormat(formats, episode.Title); format != nil { if format := findFormat(formats, episode.Title); format != nil {
episode, _ = episodeStructure.GetEpisodeByFormat(format) episode, _ = episodeStructure.GetEpisodeByFormat(format)
out.Debugf("Found matching episode %s\n", episode.Title) out.Debugf("Found matching episode %s\n", episode.Title)
return episodeInformation{ return episodeInformation{
Format: format, Format: format,
Title: episode.Title, AllSubtitles: subtitles,
URL: url, Title: episode.Title,
SeriesTitle: episode.SeriesTitle, URL: url,
SeasonNum: episode.SeasonNumber, SeriesTitle: episode.SeriesTitle,
EpisodeNum: episode.EpisodeNumber, SeasonNum: episode.SeasonNumber,
EpisodeNum: episode.EpisodeNumber,
} }
} }
return episodeInformation{} return episodeInformation{}
@ -357,6 +455,13 @@ func parseEpisodes(episodeStructure *utils.EpisodeStructure, url string) episode
func findFormat(formats []*crunchyroll.Format, name string) (format *crunchyroll.Format) { func findFormat(formats []*crunchyroll.Format, name string) (format *crunchyroll.Format) {
formatStructure := utils.NewFormatStructure(formats) 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 var audioLocale, subtitleLocale crunchyroll.LOCALE
if audioFlag != "" { if audioFlag != "" {
@ -414,7 +519,7 @@ func findFormat(formats []*crunchyroll.Format, name string) (format *crunchyroll
return return
} }
func downloadFormat(format *crunchyroll.Format, outFile string, info information) bool { func downloadFormat(format *crunchyroll.Format, subtitles []*crunchyroll.Subtitle, outFile string, info information) bool {
oldOutFile := outFile oldOutFile := outFile
outFile, changed := freeFileName(outFile) outFile, changed := freeFileName(outFile)
ext := path.Ext(outFile) ext := path.Ext(outFile)
@ -451,27 +556,29 @@ func downloadFormat(format *crunchyroll.Format, outFile string, info information
var err error var err error
if ext == ".ts" { if ext == ".ts" {
file, err := os.Create(outFile) var file *os.File
file, err = os.Create(outFile)
defer file.Close() defer file.Close()
if err != nil { if err != nil {
out.Errf("Could not create file '%s' to download episode '%s' (%s): %s, skipping\n", outFile, info.Title, info.OriginalURL, err) out.Errf("Could not create file '%s' to download episode '%s' (%s): %s, skipping\n", outFile, info.Title, info.OriginalURL, err)
return false return false
} }
err = format.Download(file, downloadProgress) err = format.DownloadGoroutines(file, goroutinesFlag, downloadProgress)
// newline to avoid weird output // newline to avoid weird output
fmt.Println() fmt.Println()
} else { } else {
tempDir, err := os.MkdirTemp("", "crunchy_") var tempDir string
tempDir, err = os.MkdirTemp("", "crunchy_")
if err != nil { if err != nil {
out.Errln("Failed to create temp download dir. Skipping") out.Errln("Failed to create temp download dir. Skipping")
return false return false
} }
var segmentCount int var segmentCount int
err = format.DownloadSegments(tempDir, 4, func(segment *m3u8.MediaSegment, current, total int, file *os.File, err error) error { err = format.DownloadSegments(tempDir, goroutinesFlag, func(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
segmentCount++ segmentCount++
return downloadProgress(segment, current, total, file, err) return downloadProgress(segment, current, total, file)
}) })
// newline to avoid weird output // newline to avoid weird output
fmt.Println() fmt.Println()
@ -483,19 +590,49 @@ func downloadFormat(format *crunchyroll.Format, outFile string, info information
defer os.Remove(f.Name()) defer os.Remove(f.Name())
f.Close() f.Close()
cmd := exec.Command("ffmpeg", args := []string{
"-f", "concat", "-f", "concat",
"-safe", "0", "-safe", "0",
"-i", f.Name(), "-i", f.Name(),
"-c", "copy", }
outFile) 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)
if err != nil {
out.Errf("Could not create file to download %s subtitles to: %v", subtitle.Locale, err)
continue
}
if err = subtitle.Download(file); err != nil {
out.Errf("Failed to download subtitles: %s", err)
continue
}
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]))
}
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() err = cmd.Run()
} }
os.RemoveAll(cleanupPath) os.RemoveAll(cleanupPath)
cleanupPath = "" cleanupPath = ""
if err != nil { if err != nil {
out.Errln("Failed to download video, skipping") out.Errf("Failed to download video, skipping: %v", err)
} else { } else {
if info.Subtitle == "" { if info.Subtitle == "" {
out.Infof("Downloaded '%s' as '%s' with %s audio locale\n", info.Title, outFile, strings.ToLower(utils.LocaleLanguage(info.Audio))) out.Infof("Downloaded '%s' as '%s' with %s audio locale\n", info.Title, outFile, strings.ToLower(utils.LocaleLanguage(info.Audio)))
@ -525,7 +662,7 @@ func downloadFormat(format *crunchyroll.Format, outFile string, info information
return true return true
} }
func downloadProgress(segment *m3u8.MediaSegment, current, total int, file *os.File, err error) error { func downloadProgress(segment *m3u8.MediaSegment, current, total int, file *os.File) error {
if cleanupPath == "" { if cleanupPath == "" {
cleanupPath = path.Dir(file.Name()) cleanupPath = path.Dir(file.Name())
} }

View file

@ -70,6 +70,10 @@ The directory to download all files to.
Same as '-s', but the subtitles are not stored in the video itself, but in a separate file. Same as '-s', but the subtitles are not stored in the video itself, but in a separate file.
.TP .TP
\fB--only-sub\fR
Downloads only the subtitles without the corresponding video.
.TP
\fB-o, --output OUTPUT\fR \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. 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. {title} » Title of the video.
@ -91,6 +95,10 @@ The video resolution. Can either be specified via the pixels (e.g. 1920x1080), t
\fB-s, --subtitle LOCALE\fR \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. 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.
.TP
\fB-g, --goroutines GOROUTINES\fR
Sets the number of parallel downloads for the segments the final video is made of.
.SH EXAMPLES .SH EXAMPLES
Login via crunchyroll account email and password. Login via crunchyroll account email and password.