From dbf14c9239ae465c68aec79f110d6e76afac747f Mon Sep 17 00:00:00 2001 From: arimatakao <104305796+arimatakao@users.noreply.github.com> Date: Wed, 19 Jun 2024 15:12:00 +0300 Subject: [PATCH] Add "--this" flag --- README.md | 8 ++- cmd/consts.go | 2 +- cmd/download.go | 110 +++++++++++++++++++++++++++++------------ cmd/info.go | 10 ++-- cmd/root.go | 6 ++- mangadexapi/api.go | 92 +++++++++++++++++++++++++++++++--- mangadexapi/chapter.go | 19 +++++++ 7 files changed, 200 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index fcfb118..883d8ef 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ mdx is a simple CLI application for downloading manga from the [MangaDex website ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/arimatakao/mdx/total) ![GitHub Repo stars](https://img.shields.io/github/stars/arimatakao/mdx) -![demo v1.4.0](./.github/assets/demo.gif) +![demo](./.github/assets/demo.gif) @@ -58,6 +58,10 @@ mdx dl -e pdf mangadex.org/title/319df2e2-e6a6-4e3a-a31c-68539c140a84 # download a specific chapter mdx dl -c 123 mangadex.org/title/319df2e2-e6a6-4e3a-a31c-68539c140a84/slam-dunk +# or set direct link to the chapter +mdx dl --this mangadex.org/chapter/b5461c55-6bb7-4d53-9534-9caabf8c069f +# or +mdx dl mangadex.org/chapter/b5461c55-6bb7-4d53-9534-9caabf8c069f # download a range of chapters mdx dl -c 12-34 mangadex.org/title/319df2e2-e6a6-4e3a-a31c-68539c140a84 @@ -128,7 +132,7 @@ mdx ping - [ ] Add flag `random` in `info` subcommand to get information about random manga. - [ ] Add flag to `download`: - [ ] `last` - download latest chapter. - - [ ] `this` - download specific chapter using link from user. Make download chapter get the chapter link instead of the manga link. + - [X] `this` - download a specific chapter using a link provided by the user. - [ ] `volume` - download all chapters of specified volume. - [ ] `volume-range` - download all chapters of specified volume range. - [ ] `oneshot` - download all oneshots of manga (if available). diff --git a/cmd/consts.go b/cmd/consts.go index e7e9beb..5c89c84 100644 --- a/cmd/consts.go +++ b/cmd/consts.go @@ -1,7 +1,7 @@ package cmd const ( - MDX_APP_VERSION = "v1.7.0" + MDX_APP_VERSION = "v1.8.0" MANGADEX_API_VERSION = "v5.10.2" MDX_USER_AGENT = "mdx-cli " + MDX_APP_VERSION diff --git a/cmd/download.go b/cmd/download.go index 9e60722..21a919a 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -36,16 +36,18 @@ var ( func init() { rootCmd.AddCommand(downloadCmd) + downloadCmd.Flags().StringVarP(&mangaUrl, + "url", "u", "", "specify the URL for the manga") + downloadCmd.Flags().StringVarP(&mangaChapterUrl, + "this", "s", "", "specify the direct URL to a specific chapter") downloadCmd.Flags().StringVarP(&outputExt, "ext", "e", "cbz", "choose output file format: cbz pdf epub") - downloadCmd.Flags().StringVarP(&mangaurl, - "url", "u", "", "specify the URL for the manga") downloadCmd.Flags().StringVarP(&outputDir, "output", "o", ".", "specify output directory for file") downloadCmd.Flags().StringVarP(&language, "language", "l", "en", "specify language") downloadCmd.Flags().StringVarP(&translateGroup, - "translated-by", "t", "", "specify part of name translation group") + "translated-by", "t", "", "specify a part of the translation group's name") downloadCmd.Flags().StringVarP(&chaptersRange, "chapter", "c", "1", "specify chapters") downloadCmd.Flags().BoolVarP(&isJpgFileFormat, @@ -59,22 +61,43 @@ func init() { } func checkDownloadArgs(cmd *cobra.Command, args []string) { - if len(args) == 0 && mangaurl == "" { + if len(args) == 0 && mangaUrl == "" && mangaChapterUrl == "" { cmd.Help() os.Exit(0) } - if mangaurl == "" { - mangaId = mangadexapi.GetMangaIdFromArg(args) + if mangaUrl == "" { + mangaId = mangadexapi.GetMangaIdFromArgs(args) + } else { + mangaId = mangadexapi.GetMangaIdFromUrl(mangaUrl) + } + + if mangaChapterUrl == "" { + mangaChapterId = mangadexapi.GetChapterIdFromArgs(args) } else { - mangaId = mangadexapi.GetMangaIdFromUrl(mangaurl) + mangaChapterId = mangadexapi.GetChapterIdFromUrl(mangaChapterUrl) } - if mangaId == "" { + if mangaId == "" && mangaChapterId == "" { e.Println("Malformated URL") os.Exit(0) } + if isJpgFileFormat { + imgExt = "jpg" + } + + if outputExt != filekit.CBZ_EXT && + outputExt != filekit.PDF_EXT && + outputExt != filekit.EPUB_EXT { + e.Printfln("%s format of file is not supported", outputExt) + os.Exit(0) + } + + if mangaChapterId != "" { + return + } + singleChapter, err := strconv.Atoi(chaptersRange) if err == nil { if singleChapter < 0 { @@ -110,26 +133,21 @@ func checkDownloadArgs(cmd *cobra.Command, args []string) { os.Exit(0) } - if isJpgFileFormat { - imgExt = "jpg" - } - - if outputExt != filekit.CBZ_EXT && - outputExt != filekit.PDF_EXT && - outputExt != filekit.EPUB_EXT { - e.Printf("%s format of file is not supported\n", outputExt) - os.Exit(0) - } } func downloadManga(cmd *cobra.Command, args []string) { + if mangaChapterId != "" { + downloadSingleChapter() + return + } + c := mangadexapi.NewClient(MDX_USER_AGENT) spinnerMangaInfo, _ := pterm.DefaultSpinner.Start("Fetching manga info...") resp, err := c.GetMangaInfo(mangaId) if err != nil { spinnerMangaInfo.Fail("Failed to get manga info") - e.Println("While getting manga info, maybe you get malformated link") + e.Println("While getting manga info, maybe you set malformated link") os.Exit(1) } mangaInfo := resp.MangaInfo() @@ -162,6 +180,36 @@ func downloadManga(cmd *cobra.Command, args []string) { } } +func downloadSingleChapter() { + c := mangadexapi.NewClient(MDX_USER_AGENT) + spinnerChapInfo, _ := pterm.DefaultSpinner.Start("Fetching chapter info...") + resp, err := c.GetChapterInfo(mangaChapterId) + if err != nil { + spinnerChapInfo.Fail("Failed to get chapter info") + os.Exit(1) + } + chapterInfo := resp.GetChapterInfo() + chapterFullInfo, err := c.GetChapterImagesInFullInfo(chapterInfo) + if err != nil { + spinnerChapInfo.Fail("Failed to get chapter info") + os.Exit(1) + } + spinnerChapInfo.Success("Fetched chapter info") + + mangaId := chapterInfo.GetMangaId() + spinnerMangaInfo, _ := pterm.DefaultSpinner.Start("Fetching manga info...") + respManga, err := c.GetMangaInfo(mangaId) + if err != nil { + spinnerMangaInfo.Fail("Failed to get manga info") + os.Exit(1) + } + mangaInfo := respManga.MangaInfo() + spinnerMangaInfo.Success("Fetched manga info") + chapArr := []mangadexapi.ChapterFullInfo{chapterFullInfo} + printShortMangaInfo(mangaInfo) + downloadChapters(c, mangaInfo, chapArr, outputExt, MDX_USER_AGENT, isJpgFileFormat) +} + func printShortMangaInfo(i mangadexapi.MangaInfo) { optionPrint.Print("Manga title: ") dp.Println(i.Title("en")) @@ -172,6 +220,18 @@ func printShortMangaInfo(i mangadexapi.MangaInfo) { dp.Println("==============") } +func printChapterInfo(c mangadexapi.ChapterFullInfo) { + tableData := pterm.TableData{ + {optionPrint.Sprint("Chapter"), dp.Sprint(c.Number())}, + {optionPrint.Sprint("Chapter title"), dp.Sprint(c.Title())}, + {optionPrint.Sprint("Volume"), dp.Sprint(c.Volume())}, + {optionPrint.Sprint("Language"), dp.Sprint(c.Language())}, + {optionPrint.Sprint("Translated by"), dp.Sprint(c.Translator())}, + {optionPrint.Sprint("Uploaded by"), dp.Sprint(c.UploadedBy())}, + } + pterm.DefaultTable.WithData(tableData).Render() +} + func downloadMergeChapters(client mangadexapi.Clientapi, mangaInfo mangadexapi.MangaInfo, chapters []mangadexapi.ChapterFullInfo, @@ -236,18 +296,6 @@ func downloadChapters(client mangadexapi.Clientapi, } } -func printChapterInfo(c mangadexapi.ChapterFullInfo) { - tableData := pterm.TableData{ - {optionPrint.Sprint("Chapter"), dp.Sprint(c.Number())}, - {optionPrint.Sprint("Chapter title"), dp.Sprint(c.Title())}, - {optionPrint.Sprint("Volume"), dp.Sprint(c.Volume())}, - {optionPrint.Sprint("Language"), dp.Sprint(c.Language())}, - {optionPrint.Sprint("Translated by"), dp.Sprint(c.Translator())}, - {optionPrint.Sprint("Uploaded by"), dp.Sprint(c.UploadedBy())}, - } - pterm.DefaultTable.WithData(tableData).Render() -} - func downloadProcess( client mangadexapi.Clientapi, chapter mangadexapi.ChapterFullInfo, diff --git a/cmd/info.go b/cmd/info.go index 566e0b5..7e32d39 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -20,19 +20,19 @@ var ( func init() { rootCmd.AddCommand(infoCmd) - infoCmd.Flags().StringVarP(&mangaurl, "url", "u", "", "specify the URL for the manga") + infoCmd.Flags().StringVarP(&mangaUrl, "url", "u", "", "specify the URL for the manga") } func checkInfoArgs(cmd *cobra.Command, args []string) { - if len(args) == 0 && mangaurl == "" { + if len(args) == 0 && mangaUrl == "" { cmd.Help() os.Exit(0) } - if mangaurl == "" { - mangaId = mangadexapi.GetMangaIdFromArg(args) + if mangaUrl == "" { + mangaId = mangadexapi.GetMangaIdFromArgs(args) } else { - mangaId = mangadexapi.GetMangaIdFromUrl(mangaurl) + mangaId = mangadexapi.GetMangaIdFromUrl(mangaUrl) } if mangaId == "" { diff --git a/cmd/root.go b/cmd/root.go index f638766..6d1ed00 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,8 +9,10 @@ import ( var ( // general flags - mangaurl string - mangaId string + mangaUrl string + mangaId string + mangaChapterUrl string + mangaChapterId string ) var ( diff --git a/mangadexapi/api.go b/mangadexapi/api.go index da77ffe..f12c50d 100644 --- a/mangadexapi/api.go +++ b/mangadexapi/api.go @@ -19,6 +19,7 @@ const ( manga_path = "/manga" specific_manga_path = "/manga/{id}" manga_feed_path = "/manga/{id}/feed" + chapter_info_path = "/chapter/{id}" chapter_images_path = "/at-home/server/{id}" download_high_quility_path = "/data/{chapterHash}/{imageFilename}" download_low_quility_path = "/data-saver/{chapterHash}/{imageFilename}" @@ -31,29 +32,35 @@ var ( ErrUnexpectedHeader = errors.New("unexpected response header value") ) -func GetMangaIdFromUrl(link string) string { - +func getMangaDexPaths(link string) []string { if !strings.HasPrefix(link, "https://") && !strings.HasPrefix(link, "http://") { link = "https://" + link } parsedUrl, err := url.Parse(link) if err != nil { - return "" + return []string{} } if parsedUrl.Host != "mangadex.org" { - return "" + return []string{} } - paths := strings.Split(parsedUrl.Path, "/") + return strings.Split(parsedUrl.Path, "/") +} + +func GetMangaIdFromUrl(link string) string { + paths := getMangaDexPaths(link) if len(paths) < 3 { return "" } + if paths[1] != "title" { + return "" + } return paths[2] } -func GetMangaIdFromArg(args []string) string { +func GetMangaIdFromArgs(args []string) string { for _, arg := range args { if u := GetMangaIdFromUrl(arg); u != "" { return u @@ -62,6 +69,26 @@ func GetMangaIdFromArg(args []string) string { return "" } +func GetChapterIdFromUrl(link string) string { + paths := getMangaDexPaths(link) + if len(paths) < 3 { + return "" + } + if paths[1] != "chapter" { + return "" + } + return paths[2] +} + +func GetChapterIdFromArgs(args []string) string { + for _, arg := range args { + if u := GetChapterIdFromUrl(arg); u != "" { + return u + } + } + return "" +} + type Clientapi struct { c *resty.Client } @@ -159,6 +186,31 @@ func (a Clientapi) GetMangaInfo(mangaId string) (MangaInfoResponse, error) { return info, nil } +func (a Clientapi) GetChapterInfo(chapterId string) (ResponseChapter, error) { + if chapterId == "" { + return ResponseChapter{}, ErrBadInput + } + + chapterInfo := ResponseChapter{} + respErr := ErrorResponse{} + + resp, err := a.c.R(). + SetError(&respErr). + SetResult(&chapterInfo). + SetPathParam("id", chapterId). + SetQueryString("includes[]=scanlation_group&includes[]=user"). + Get(chapter_info_path) + if err != nil { + return ResponseChapter{}, ErrConnection + } + + if resp.IsError() { + return ResponseChapter{}, &respErr + } + + return chapterInfo, nil +} + func (a Clientapi) GetChaptersList(limit, offset int, mangaId, language string) (ResponseChapterList, error) { if mangaId == "" { @@ -251,6 +303,34 @@ func (a Clientapi) DownloadImage(baseUrl, chapterHash, imageFilename string, return resp.Body(), nil } +func (a Clientapi) GetChapterImagesInFullInfo(chap Chapter) (ChapterFullInfo, error) { + chapImages := ResponseChapterImages{} + respErr := ErrorResponse{} + + resp, err := a.c.R(). + SetError(&respErr). + SetResult(&chapImages). + SetPathParam("id", chap.ID). + Get(chapter_images_path) + if err != nil { + return ChapterFullInfo{}, ErrConnection + } + + if resp.IsError() { + return ChapterFullInfo{}, &respErr + } + + fullInfo := ChapterFullInfo{ + info: chap, + DownloadBaseURL: chapImages.BaseURL, + HashId: chapImages.ChapterMetaInfo.Hash, + PngFiles: chapImages.ChapterMetaInfo.Data, + JpgFiles: chapImages.ChapterMetaInfo.DataSaver, + } + + return fullInfo, nil +} + func (a Clientapi) GetFullChaptersInfo(mangaId, language, translationGroup string, lowestChapter, highestChapter int) ([]ChapterFullInfo, error) { diff --git a/mangadexapi/chapter.go b/mangadexapi/chapter.go index cbcf23e..5498ad5 100644 --- a/mangadexapi/chapter.go +++ b/mangadexapi/chapter.go @@ -69,6 +69,15 @@ func (c Chapter) getTranslator() string { return "" } +func (c Chapter) GetMangaId() string { + for _, rel := range c.Relationships { + if rel.Type == "manga" { + return rel.ID + } + } + return "" +} + type ResponseChapterList struct { Result string `json:"result"` Response string `json:"response"` @@ -78,6 +87,16 @@ type ResponseChapterList struct { Total int `json:"total"` } +type ResponseChapter struct { + Result string `json:"result"` + Response string `json:"response"` + Data Chapter `json:"data"` +} + +func (r ResponseChapter) GetChapterInfo() Chapter { + return r.Data +} + func (l ResponseChapterList) GetChapters(lowest, highest int, transgp string) ([]Chapter, int) { if len(l.Data) == 0 { return []Chapter{}, 0