From 741eed00ace0e78eedafcde0143430e105bee4a2 Mon Sep 17 00:00:00 2001 From: Neshura <neshura@neshweb.net> Date: Wed, 19 Feb 2025 23:20:24 +0100 Subject: [PATCH] Support JNC Nina via dynamic domain, add optional Suffix for Series (used for language separation) --- jnc/jnc.go | 22 +++++++------- main.go | 88 ++++++++++++++++++++++++++++++++---------------------- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/jnc/jnc.go b/jnc/jnc.go index 44eb389..e89aa71 100644 --- a/jnc/jnc.go +++ b/jnc/jnc.go @@ -15,11 +15,6 @@ import ( "time" ) -const BaseUrl = "https://labs.j-novel.club/" -const ApiV1Url = BaseUrl + "app/v1/" -const ApiV2Url = BaseUrl + "app/v2/" -const FeedUrl = BaseUrl + "feed/" - type ApiFormat int const ( @@ -56,6 +51,8 @@ type Api struct { _format ApiFormat _library Library _series map[string]SerieAugmented + _apiUrl string + _feedUrl string } type Auth struct { @@ -202,7 +199,8 @@ type Pagination struct { LastPage bool `json:"lastPage"` } -func NewJNC() Api { +func NewJNC(domain string) Api { + baseUrl := fmt.Sprintf("https://%s/", domain) jncApi := Api{ _auth: Auth{ _username: "", @@ -211,6 +209,8 @@ func NewJNC() Api { }, _library: Library{}, _series: map[string]SerieAugmented{}, + _apiUrl: baseUrl + "app/v2/", + _feedUrl: baseUrl + "feed/", } return jncApi } @@ -240,7 +240,7 @@ func (jncApi *Api) Login() error { return err } - res, err := http.Post(ApiV2Url+"auth/login?"+jncApi.ReturnFormat(), "application/json", bytes.NewBuffer(jsonAuthBody)) + res, err := http.Post(jncApi._apiUrl+"auth/login?"+jncApi.ReturnFormat(), "application/json", bytes.NewBuffer(jsonAuthBody)) if err != nil { return err } @@ -275,7 +275,7 @@ func (jncApi *Api) AuthParam() string { // Logout invalidates the auth token and resets the jnc instance to a blank slate. No information remains after calling. func (jncApi *Api) Logout() error { fmt.Println("Logging out...") - res, err := http.Post(ApiV2Url+"auth/logout?"+jncApi.ReturnFormat()+"&"+jncApi.AuthParam(), "application/json", nil) + res, err := http.Post(jncApi._apiUrl+"auth/logout?"+jncApi.ReturnFormat()+"&"+jncApi.AuthParam(), "application/json", nil) if err != nil { panic(err) } @@ -305,7 +305,7 @@ func (jncApi *Api) SetUsername(username string) { // FetchLibrary retrieves the list of Book's in the logged-in User's J-Novel Club library func (jncApi *Api) FetchLibrary() error { fmt.Println("Fetching library contents...") - res, err := http.Get(ApiV2Url + "me/library?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) + res, err := http.Get(jncApi._apiUrl + "me/library?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) if err != nil { return err } @@ -364,7 +364,7 @@ func (jncApi *Api) FetchLibrarySeries() error { fmt.Printf("\rFetching Series Info: %s/%s - %s%%", strconv.Itoa(progress), strconv.Itoa(len(jncApi._series)), strconv.FormatFloat(float64(progress)/float64(len(jncApi._series))*float64(100), 'f', 2, 32)) serie := jncApi._series[i] - res, err := http.Get(ApiV2Url + "series/" + serie.Info.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) + res, err := http.Get(jncApi._apiUrl + "series/" + serie.Info.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) if err != nil { return err @@ -394,7 +394,7 @@ func (jncApi *Api) FetchLibrarySeries() error { // FetchVolumeInfo retrieves additional Volume Info that was not returned when retrieving the entire Library func (jncApi *Api) FetchVolumeInfo(volume Volume) (Volume, error) { fmt.Println("Fetching Volume details...") - res, err := http.Get(ApiV2Url + "volumes/" + volume.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) + res, err := http.Get(jncApi._apiUrl + "volumes/" + volume.Id + "?" + jncApi.ReturnFormat() + "&" + jncApi.AuthParam()) if err != nil { return volume, err } diff --git a/main.go b/main.go index 4b315f8..5fc732a 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "bytes" "container/list" "encoding/xml" + "errors" "fmt" "github.com/beevik/etree" "io" @@ -67,6 +68,14 @@ func ClearScreen() { } } +func GetArg(argKey string) (string, error) { + if slices.Contains(os.Args, argKey) { + idx := slices.Index(os.Args, argKey) + return os.Args[idx+1], nil + } + return "", errors.New("arg " + argKey + " not found") +} + func main() { interactive := false if slices.Contains(os.Args, "-I") { @@ -77,19 +86,27 @@ func main() { panic("automatic mode is not implemented yet") } - var downloadDir string - if slices.Contains(os.Args, "-d") { - idx := slices.Index(os.Args, "-d") - downloadDir = os.Args[idx+1] - if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 { - downloadDir = downloadDir + "/" - } - } else { - panic("working directory not specified") + domain, err := GetArg("-D") + if err != nil { + panic(err) + } + + serLan, err := GetArg("-L") + if serLan != "" { + serLan = " " + serLan + } + + downloadDir, err := GetArg("-d") + if err != nil { + panic(err) + } + + if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 { + downloadDir = downloadDir + "/" } // initialize the J-Novel Club Instance - jnovel := jnc.NewJNC() + jnovel := jnc.NewJNC(domain) var username string if slices.Contains(os.Args, "-u") { @@ -109,7 +126,7 @@ func main() { } jnovel.SetPassword(password) - err := jnovel.Login() + err = jnovel.Login() if err != nil { panic(err) } @@ -153,12 +170,12 @@ func main() { serie := seriesList[s] if mode == "3" || mode == "4" { if serie.Info.Type == "MANGA" { - HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y") + HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y", serLan) } } if mode == "3" || mode == "5" { if serie.Info.Type == "NOVEL" { - HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y") + HandleSeries(jnovel, serie, downloadDir, updatedOnly == "Y" || updatedOnly == "y", serLan) } } } @@ -185,7 +202,7 @@ func main() { serie := seriesList[seriesNumber-1] if mode == "2" { - HandleSeries(jnovel, serie, downloadDir, false) + HandleSeries(jnovel, serie, downloadDir, false, serLan) } else { ClearScreen() fmt.Println("\n###[Volume Selection]###") @@ -206,44 +223,44 @@ func main() { } volume := serie.Volumes[volumeNumber-1] - HandleVolume(jnovel, serie, volume, downloadDir) + HandleVolume(jnovel, serie, volume, downloadDir, serLan) } } } -func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool) { +func HandleSeries(jnovel jnc.Api, serie jnc.SerieAugmented, downloadDir string, updatedOnly bool, titleSuffix string) { for v := range serie.Volumes { volume := serie.Volumes[v] if updatedOnly { if len(volume.Downloads) != 0 && volume.UpdateAvailable() { - downloadDir = PrepareSerieDirectory(serie, volume, downloadDir) - HandleVolume(jnovel, serie, volume, downloadDir) + downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix) + HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix) } } else { - downloadDir = PrepareSerieDirectory(serie, volume, downloadDir) - HandleVolume(jnovel, serie, volume, downloadDir) + downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix) + HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix) } } } -func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) string { +func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string, titleSuffix string) string { splits := strings.Split(downloadDir, "/") // last element of this split is always empty due to the trailing slash - if splits[len(splits)-2] != serie.Info.Title { + if splits[len(splits)-2] != serie.Info.Title+titleSuffix { if serie.Info.Title == "Ascendance of a Bookworm" { partSplits := strings.Split(volume.Info.ShortTitle, " ") part := " " + partSplits[0] + " " + partSplits[1] if strings.Split(splits[len(splits)-2], " ")[0] == "Ascendance" { - splits[len(splits)-2] = serie.Info.Title + part + splits[len(splits)-2] = serie.Info.Title + part + titleSuffix } else { - splits[len(splits)-1] = serie.Info.Title + part + splits[len(splits)-1] = serie.Info.Title + part + titleSuffix splits = append(splits, "") } downloadDir = strings.Join(splits, "/") } else { - downloadDir += serie.Info.Title + "/" + downloadDir += serie.Info.Title + titleSuffix + "/" } _, err := os.Stat(downloadDir) @@ -258,7 +275,7 @@ func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented, return downloadDir } -func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string) { +func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadDir string, titleSuffix string) { var downloadLink string if len(volume.Downloads) == 0 { fmt.Printf("Volume %s currently has no downloads available. Skipping \n", volume.Info.Title) @@ -270,10 +287,10 @@ func HandleVolume(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAug downloadLink = volume.Downloads[0].Link } - DownloadAndProcessEpub(jnovel, serie, volume, downloadLink, downloadDir) + DownloadAndProcessEpub(jnovel, serie, volume, downloadLink, downloadDir, titleSuffix) } -func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadLink string, downloadDirectory string) { +func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc.VolumeAugmented, downloadLink string, downloadDirectory string, titleSuffix string) { FormatNavigationFile := map[string]string{ "manga": "item/nav.ncx", "novel": "OEBPS/toc.ncx", @@ -344,7 +361,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc fmt.Println("No chapters found, chapter name likely not supported") } - basePath := downloadDirectory + volume.Info.Title + "/" + basePath := downloadDirectory + volume.Info.Title + titleSuffix + "/" PrepareVolumeDirectory(basePath) volume.Info, err = jnovel.FetchVolumeInfo(volume.Info) if err != nil { @@ -427,7 +444,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc } } - comicInfo, err := GenerateChapterMetadata(volume, serie, len(chap.pages), language, chap.chDisplay) + comicInfo, err := GenerateChapterMetadata(volume, serie, len(chap.pages), language, chap.chDisplay, titleSuffix) if err != nil { fmt.Println(err) } @@ -468,10 +485,10 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc var number string if serie.Info.Title == "Ascendance of a Bookworm" { splits := strings.Split(volume.Info.ShortTitle, " ") - title = serie.Info.Title + " " + splits[0] + " " + splits[1] + title = serie.Info.Title + " " + splits[0] + " " + splits[1] + titleSuffix number = splits[3] } else { - title = serie.Info.Title + title = serie.Info.Title + titleSuffix number = strconv.Itoa(volume.Info.Number) } @@ -543,7 +560,7 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc } } -func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmented, pageCount int, language string, chapterNumber string) ([]byte, error) { +func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmented, pageCount int, language string, chapterNumber string, titleSuffix string) ([]byte, error) { comicInfo := ComicInfo{ XMLName: "ComicInfo", XMLNS: "http://www.w3.org/2001/XMLSchema-instance", @@ -554,7 +571,7 @@ func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmente sInfo := serie.Info comicInfo.Series = sInfo.Title - comicInfo.Title = vInfo.Title + comicInfo.Title = vInfo.Title + titleSuffix comicInfo.Number = chapterNumber comicInfo.Volume = vInfo.Number @@ -796,8 +813,9 @@ func GetLastValidChapterNumber(currentChapter *list.Element) Chapter { if chapterData.numberMain != -1 && chapterData.numberSub == 0 { return chapterData } + } else { + break } - break } return currentChapter.Value.(Chapter) }