Support JNC Nina via dynamic domain, add optional Suffix for Series (used for language separation)

This commit is contained in:
Neshura 2025-02-19 23:20:24 +01:00
parent d828532239
commit 741eed00ac
Signed by: Neshura
GPG key ID: 4E2D47B1374C297D
2 changed files with 64 additions and 46 deletions

View file

@ -15,11 +15,6 @@ import (
"time" "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 type ApiFormat int
const ( const (
@ -56,6 +51,8 @@ type Api struct {
_format ApiFormat _format ApiFormat
_library Library _library Library
_series map[string]SerieAugmented _series map[string]SerieAugmented
_apiUrl string
_feedUrl string
} }
type Auth struct { type Auth struct {
@ -202,7 +199,8 @@ type Pagination struct {
LastPage bool `json:"lastPage"` LastPage bool `json:"lastPage"`
} }
func NewJNC() Api { func NewJNC(domain string) Api {
baseUrl := fmt.Sprintf("https://%s/", domain)
jncApi := Api{ jncApi := Api{
_auth: Auth{ _auth: Auth{
_username: "", _username: "",
@ -211,6 +209,8 @@ func NewJNC() Api {
}, },
_library: Library{}, _library: Library{},
_series: map[string]SerieAugmented{}, _series: map[string]SerieAugmented{},
_apiUrl: baseUrl + "app/v2/",
_feedUrl: baseUrl + "feed/",
} }
return jncApi return jncApi
} }
@ -240,7 +240,7 @@ func (jncApi *Api) Login() error {
return err 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 { if err != nil {
return err 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. // Logout invalidates the auth token and resets the jnc instance to a blank slate. No information remains after calling.
func (jncApi *Api) Logout() error { func (jncApi *Api) Logout() error {
fmt.Println("Logging out...") 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 { if err != nil {
panic(err) 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 // FetchLibrary retrieves the list of Book's in the logged-in User's J-Novel Club library
func (jncApi *Api) FetchLibrary() error { func (jncApi *Api) FetchLibrary() error {
fmt.Println("Fetching library contents...") 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 { if err != nil {
return err 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)) 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] 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 { if err != nil {
return err 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 // FetchVolumeInfo retrieves additional Volume Info that was not returned when retrieving the entire Library
func (jncApi *Api) FetchVolumeInfo(volume Volume) (Volume, error) { func (jncApi *Api) FetchVolumeInfo(volume Volume) (Volume, error) {
fmt.Println("Fetching Volume details...") 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 { if err != nil {
return volume, err return volume, err
} }

88
main.go
View file

@ -6,6 +6,7 @@ import (
"bytes" "bytes"
"container/list" "container/list"
"encoding/xml" "encoding/xml"
"errors"
"fmt" "fmt"
"github.com/beevik/etree" "github.com/beevik/etree"
"io" "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() { func main() {
interactive := false interactive := false
if slices.Contains(os.Args, "-I") { if slices.Contains(os.Args, "-I") {
@ -77,19 +86,27 @@ func main() {
panic("automatic mode is not implemented yet") panic("automatic mode is not implemented yet")
} }
var downloadDir string domain, err := GetArg("-D")
if slices.Contains(os.Args, "-d") { if err != nil {
idx := slices.Index(os.Args, "-d") panic(err)
downloadDir = os.Args[idx+1] }
if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 {
downloadDir = downloadDir + "/" serLan, err := GetArg("-L")
} if serLan != "" {
} else { serLan = " " + serLan
panic("working directory not specified") }
downloadDir, err := GetArg("-d")
if err != nil {
panic(err)
}
if strings.LastIndex(downloadDir, "/") != len(downloadDir)-1 {
downloadDir = downloadDir + "/"
} }
// initialize the J-Novel Club Instance // initialize the J-Novel Club Instance
jnovel := jnc.NewJNC() jnovel := jnc.NewJNC(domain)
var username string var username string
if slices.Contains(os.Args, "-u") { if slices.Contains(os.Args, "-u") {
@ -109,7 +126,7 @@ func main() {
} }
jnovel.SetPassword(password) jnovel.SetPassword(password)
err := jnovel.Login() err = jnovel.Login()
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -153,12 +170,12 @@ func main() {
serie := seriesList[s] serie := seriesList[s]
if mode == "3" || mode == "4" { if mode == "3" || mode == "4" {
if serie.Info.Type == "MANGA" { 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 mode == "3" || mode == "5" {
if serie.Info.Type == "NOVEL" { 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] serie := seriesList[seriesNumber-1]
if mode == "2" { if mode == "2" {
HandleSeries(jnovel, serie, downloadDir, false) HandleSeries(jnovel, serie, downloadDir, false, serLan)
} else { } else {
ClearScreen() ClearScreen()
fmt.Println("\n###[Volume Selection]###") fmt.Println("\n###[Volume Selection]###")
@ -206,44 +223,44 @@ func main() {
} }
volume := serie.Volumes[volumeNumber-1] 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 { for v := range serie.Volumes {
volume := serie.Volumes[v] volume := serie.Volumes[v]
if updatedOnly { if updatedOnly {
if len(volume.Downloads) != 0 && volume.UpdateAvailable() { if len(volume.Downloads) != 0 && volume.UpdateAvailable() {
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir) downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix)
HandleVolume(jnovel, serie, volume, downloadDir) HandleVolume(jnovel, serie, volume, downloadDir, titleSuffix)
} }
} else { } else {
downloadDir = PrepareSerieDirectory(serie, volume, downloadDir) downloadDir = PrepareSerieDirectory(serie, volume, downloadDir, titleSuffix)
HandleVolume(jnovel, serie, volume, downloadDir) 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, "/") splits := strings.Split(downloadDir, "/")
// last element of this split is always empty due to the trailing slash // 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" { if serie.Info.Title == "Ascendance of a Bookworm" {
partSplits := strings.Split(volume.Info.ShortTitle, " ") partSplits := strings.Split(volume.Info.ShortTitle, " ")
part := " " + partSplits[0] + " " + partSplits[1] part := " " + partSplits[0] + " " + partSplits[1]
if strings.Split(splits[len(splits)-2], " ")[0] == "Ascendance" { 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 { } else {
splits[len(splits)-1] = serie.Info.Title + part splits[len(splits)-1] = serie.Info.Title + part + titleSuffix
splits = append(splits, "") splits = append(splits, "")
} }
downloadDir = strings.Join(splits, "/") downloadDir = strings.Join(splits, "/")
} else { } else {
downloadDir += serie.Info.Title + "/" downloadDir += serie.Info.Title + titleSuffix + "/"
} }
_, err := os.Stat(downloadDir) _, err := os.Stat(downloadDir)
@ -258,7 +275,7 @@ func PrepareSerieDirectory(serie jnc.SerieAugmented, volume jnc.VolumeAugmented,
return downloadDir 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 var downloadLink string
if len(volume.Downloads) == 0 { if len(volume.Downloads) == 0 {
fmt.Printf("Volume %s currently has no downloads available. Skipping \n", volume.Info.Title) 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 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{ FormatNavigationFile := map[string]string{
"manga": "item/nav.ncx", "manga": "item/nav.ncx",
"novel": "OEBPS/toc.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") fmt.Println("No chapters found, chapter name likely not supported")
} }
basePath := downloadDirectory + volume.Info.Title + "/" basePath := downloadDirectory + volume.Info.Title + titleSuffix + "/"
PrepareVolumeDirectory(basePath) PrepareVolumeDirectory(basePath)
volume.Info, err = jnovel.FetchVolumeInfo(volume.Info) volume.Info, err = jnovel.FetchVolumeInfo(volume.Info)
if err != nil { 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 { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
@ -468,10 +485,10 @@ func DownloadAndProcessEpub(jnovel jnc.Api, serie jnc.SerieAugmented, volume jnc
var number string var number string
if serie.Info.Title == "Ascendance of a Bookworm" { if serie.Info.Title == "Ascendance of a Bookworm" {
splits := strings.Split(volume.Info.ShortTitle, " ") 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] number = splits[3]
} else { } else {
title = serie.Info.Title title = serie.Info.Title + titleSuffix
number = strconv.Itoa(volume.Info.Number) 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{ comicInfo := ComicInfo{
XMLName: "ComicInfo", XMLName: "ComicInfo",
XMLNS: "http://www.w3.org/2001/XMLSchema-instance", XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
@ -554,7 +571,7 @@ func GenerateChapterMetadata(volume jnc.VolumeAugmented, serie jnc.SerieAugmente
sInfo := serie.Info sInfo := serie.Info
comicInfo.Series = sInfo.Title comicInfo.Series = sInfo.Title
comicInfo.Title = vInfo.Title comicInfo.Title = vInfo.Title + titleSuffix
comicInfo.Number = chapterNumber comicInfo.Number = chapterNumber
comicInfo.Volume = vInfo.Number comicInfo.Volume = vInfo.Number
@ -796,8 +813,9 @@ func GetLastValidChapterNumber(currentChapter *list.Element) Chapter {
if chapterData.numberMain != -1 && chapterData.numberSub == 0 { if chapterData.numberMain != -1 && chapterData.numberSub == 0 {
return chapterData return chapterData
} }
} else {
break
} }
break
} }
return currentChapter.Value.(Chapter) return currentChapter.Value.(Chapter)
} }