diff --git a/CHANGELOG.md b/CHANGELOG.md index c492b000..5348660b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). +## 3.14.0 + +- New commands related to the anilist manga linkage. Now you can set what anilist manga should be linked with what titles by id. See `mangal inline anilist` for more information. #106 +- Increase default http timeout to 20 seconds #108 +- Fixed nil panic when trying to resume reading from history with mini mode + ## 3.13.0 - Support environment variables for `downloader.path` config field #103 diff --git a/anilist/find.go b/anilist/find.go index 0e5cd547..bea2f480 100644 --- a/anilist/find.go +++ b/anilist/find.go @@ -41,7 +41,7 @@ func FindClosest(name string) (*Manga, error) { } // search for manga on anilist - mangas, err := Search(name) + mangas, err := SearchByName(name) if err != nil { log.Error(err) return nil, err diff --git a/anilist/query.go b/anilist/query.go index cca7a54b..ccffb295 100644 --- a/anilist/query.go +++ b/anilist/query.go @@ -1,49 +1,62 @@ package anilist -var searchQuery = ` +import "fmt" + +var mangaSubquery = ` +id +idMal +title { + romaji + english + native +} +description(asHtml: false) +tags { + name +} +genres +coverImage { + extraLarge +} +characters (page: 1, perPage: 10, role: MAIN) { + nodes { + name { + full + } + } +} +startDate { + year + month + day +} +endDate { + year + month + day +} +status +synonyms +siteUrl +countryOfOrigin +externalLinks { + url +} +` + +var searchByNameQuery = fmt.Sprintf(` query ($query: String) { Page (page: 1, perPage: 30) { media (search: $query, type: MANGA) { - id - idMal - title { - romaji - english - native - } - description(asHtml: false) - tags { - name - } - genres - coverImage { - extraLarge - } - characters (page: 1, perPage: 10, role: MAIN) { - nodes { - name { - full - } - } - } - startDate { - year - month - day - } - endDate { - year - month - day - } - status - synonyms - siteUrl - countryOfOrigin - externalLinks { - url - } + %s } } } -` +`, mangaSubquery) + +var searchByIDQuery = fmt.Sprintf(` +query ($id: Int) { + Media (id: $id, type: MANGA) { + %s + } +}`, mangaSubquery) diff --git a/anilist/search.go b/anilist/search.go index 24174e0c..0c5e4a2c 100644 --- a/anilist/search.go +++ b/anilist/search.go @@ -10,7 +10,7 @@ import ( "strconv" ) -type anilistResponse struct { +type searchByNameResponse struct { Data struct { Page struct { Media []*Manga `json:"media"` @@ -18,9 +18,67 @@ type anilistResponse struct { } `json:"data"` } +type searchByIDResponse struct { + Data struct { + Media *Manga `json:"media"` + } `json:"data"` +} + var searchCache = make(map[string][]*Manga) -func Search(name string) ([]*Manga, error) { +func GetByID(id int) (*Manga, error) { + // prepare body + log.Infof("Searching anilist for manga with id: %d", id) + body := map[string]interface{}{ + "query": searchByIDQuery, + "variables": map[string]interface{}{ + "id": id, + }, + } + + // parse body to json + jsonBody, err := json.Marshal(body) + if err != nil { + log.Error(err) + return nil, err + } + + // send request + log.Info("Sending request to Anilist") + req, err := http.NewRequest(http.MethodPost, "https://graphql.anilist.co", bytes.NewBuffer(jsonBody)) + if err != nil { + log.Error(err) + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + + resp, err := network.Client.Do(req) + + if err != nil { + log.Error(err) + return nil, err + } + + if resp.StatusCode != http.StatusOK { + log.Error("Anilist returned status code " + strconv.Itoa(resp.StatusCode)) + return nil, fmt.Errorf("invalid response code %d", resp.StatusCode) + } + + // decode response + var response searchByIDResponse + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + log.Error(err) + return nil, err + } + + manga := response.Data.Media + log.Infof("Got response from Anilist, found manga with id %d", manga.ID) + return manga, nil +} + +func SearchByName(name string) ([]*Manga, error) { if mangas, ok := searchCache[name]; ok { return mangas, nil } @@ -28,7 +86,7 @@ func Search(name string) ([]*Manga, error) { // prepare body log.Info("Searching anilist for manga: " + name) body := map[string]interface{}{ - "query": searchQuery, + "query": searchByNameQuery, "variables": map[string]interface{}{ "query": name, }, @@ -64,7 +122,7 @@ func Search(name string) ([]*Manga, error) { } // decode response - var response anilistResponse + var response searchByNameResponse if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { log.Error(err) diff --git a/anilist/search_test.go b/anilist/search_test.go index f4d3c85c..e566f502 100644 --- a/anilist/search_test.go +++ b/anilist/search_test.go @@ -9,7 +9,7 @@ func TestSearch(t *testing.T) { Convey(`Given a query "Death Note"`, t, func() { query := "Death Note" Convey(`When I search for it`, func() { - results, err := Search(query) + results, err := SearchByName(query) Convey(`Then I should get a result`, func() { So(err, ShouldBeNil) So(results, ShouldNotBeEmpty) diff --git a/cmd/inline.go b/cmd/inline.go index 1a62b538..0a936a6e 100644 --- a/cmd/inline.go +++ b/cmd/inline.go @@ -1,8 +1,10 @@ package cmd import ( + "encoding/json" "errors" "fmt" + "github.com/metafates/mangal/anilist" "github.com/metafates/mangal/constant" "github.com/metafates/mangal/converter" "github.com/metafates/mangal/filesystem" @@ -122,3 +124,89 @@ When using the json flag manga selector could be omitted. That way, it will sele handleErr(inline.Run(options)) }, } + +func init() { + inlineCmd.AddCommand(inlineAnilistCmd) +} + +var inlineAnilistCmd = &cobra.Command{ + Use: "anilist", + Short: "Anilist related commands", +} + +func init() { + inlineAnilistCmd.AddCommand(inlineAnilistSearchCmd) + + inlineAnilistSearchCmd.Flags().StringP("name", "n", "", "manga name to search") + inlineAnilistSearchCmd.Flags().IntP("id", "i", 0, "anilist manga id") + + inlineAnilistSearchCmd.MarkFlagsMutuallyExclusive("name", "id") +} + +var inlineAnilistSearchCmd = &cobra.Command{ + Use: "search", + Short: "Search anilist manga by name", + Run: func(cmd *cobra.Command, args []string) { + mangaName := lo.Must(cmd.Flags().GetString("name")) + mangaId := lo.Must(cmd.Flags().GetInt("id")) + + var toEncode any + + if mangaName != "" { + mangas, err := anilist.SearchByName(mangaName) + handleErr(err) + toEncode = mangas + } else { + manga, err := anilist.GetByID(mangaId) + handleErr(err) + toEncode = manga + } + + handleErr(json.NewEncoder(os.Stdout).Encode(toEncode)) + }, +} + +func init() { + inlineAnilistCmd.AddCommand(inlineAnilistGetCmd) + + inlineAnilistGetCmd.Flags().StringP("name", "n", "", "manga name to get bind for") +} + +var inlineAnilistGetCmd = &cobra.Command{ + Use: "get", + Short: "Get anilist manga that is bind to manga name", + Run: func(cmd *cobra.Command, args []string) { + name := lo.Must(cmd.Flags().GetString("name")) + anilistManga, ok := anilist.GetRelation(name) + + if !ok { + var err error + anilistManga, err = anilist.FindClosest(name) + handleErr(err) + } + + handleErr(json.NewEncoder(os.Stdout).Encode(anilistManga)) + }, +} + +func init() { + inlineAnilistCmd.AddCommand(inlineAnilistBindCmd) + + inlineAnilistBindCmd.Flags().StringP("name", "n", "", "manga name") + inlineAnilistBindCmd.Flags().IntP("id", "i", 0, "anilist manga id") + + inlineAnilistBindCmd.MarkFlagsRequiredTogether("name", "id") +} + +var inlineAnilistBindCmd = &cobra.Command{ + Use: "set", + Short: "Bind manga name to the anilist manga by id", + Run: func(cmd *cobra.Command, args []string) { + anilistManga, err := anilist.GetByID(lo.Must(cmd.Flags().GetInt("id"))) + handleErr(err) + + mangaName := lo.Must(cmd.Flags().GetString("name")) + + handleErr(anilist.SetRelation(mangaName, anilistManga)) + }, +} diff --git a/constant/meta.go b/constant/meta.go index 8f80437a..e00d6ffc 100644 --- a/constant/meta.go +++ b/constant/meta.go @@ -2,6 +2,6 @@ package constant const ( Mangal = "mangal" - Version = "3.13.0" + Version = "3.14.0" UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" ) diff --git a/go.mod b/go.mod index de8863e0..add98be8 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/gocolly/colly/v2 v2.1.0 github.com/ivanpirog/coloredcobra v1.0.1 github.com/ka-weihe/fast-levenshtein v0.0.0-20201227151214-4c99ee36a1ba - github.com/metafates/mangal-lua-libs v0.4.1-0.20220920195433-ce228b4abd95 + github.com/metafates/mangal-lua-libs v0.4.1 github.com/muesli/reflow v0.3.0 github.com/pdfcpu/pdfcpu v0.3.13 github.com/samber/lo v1.33.0 diff --git a/go.sum b/go.sum index 77b88b08..5238f764 100644 --- a/go.sum +++ b/go.sum @@ -252,6 +252,8 @@ github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWV github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/metafates/mangal-lua-libs v0.4.1-0.20220920195433-ce228b4abd95 h1:8EbMD890jS9WWjtgSzqnURJMTvX1Eat4BKq3Ydpqz6s= github.com/metafates/mangal-lua-libs v0.4.1-0.20220920195433-ce228b4abd95/go.mod h1:/g3V2cAx3iZHdUcDt1Hr4O69xKhI+jlEiRdvwAReBiA= +github.com/metafates/mangal-lua-libs v0.4.1 h1:wqHgFbZ82gOqiPIFlWAeJGPMvZNSvA8CaQ98hVjPXOo= +github.com/metafates/mangal-lua-libs v0.4.1/go.mod h1:+0gexBk9l//rXWwjcPRmO222qnet+Akm3Sps4jNYnHk= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= diff --git a/mini/states.go b/mini/states.go index 7fddb278..74b7a788 100644 --- a/mini/states.go +++ b/mini/states.go @@ -380,7 +380,7 @@ func (m *mini) handleHistorySelectState() error { defaultProviders := provider.Builtins() customProviders := provider.Customs() - var providers = make([]*provider.Provider, len(defaultProviders)+len(customProviders)) + var providers = make([]*provider.Provider, 0) providers = append(providers, defaultProviders...) providers = append(providers, customProviders...) diff --git a/network/client.go b/network/client.go index b31cd685..5a059126 100644 --- a/network/client.go +++ b/network/client.go @@ -17,6 +17,6 @@ func init() { } var Client = &http.Client{ - Timeout: 10 * time.Second, + Timeout: 30 * time.Second, Transport: transport, } diff --git a/tui/handlers.go b/tui/handlers.go index 571c41ff..51aeab29 100644 --- a/tui/handlers.go +++ b/tui/handlers.go @@ -304,7 +304,7 @@ func (b *statefulBubble) fetchAnilist(manga *source.Manga) tea.Cmd { return func() tea.Msg { log.Info("fetching anilist for " + manga.Name) b.progressStatus = fmt.Sprintf("Fetching anilist for %s", style.Magenta(manga.Name)) - mangas, err := anilist.Search(manga.Name) + mangas, err := anilist.SearchByName(manga.Name) if err != nil { log.Error(err) b.errorChannel <- err