Skip to content

Commit

Permalink
Mangal 4
Browse files Browse the repository at this point in the history
  • Loading branch information
metafates authored Oct 31, 2022
2 parents d5558f1 + 8c70499 commit 36aab8d
Show file tree
Hide file tree
Showing 156 changed files with 9,437 additions and 1,645 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
changelog-temp.md
mangal

######
# Go #
Expand Down
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,66 @@ 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).

## 4.0.0

I've been actively working on this update lately, and I'm finally happy to share the 4th version of Mangal! 🐳

The most important feature this major version brings is significantly improved caching mechanism
which makes Mangal extremely fast and responsive.

Now, mangal makes almost no requests to the servers.
This includes Anilist, Scrapers, Update checker and so on!

<details>
<summary><strong>⚠️ BREAKING!!! ⚠️ </strong> Please, read!</summary>

1. `mangal sources` will no longer list available sources, use `mangal sources list` instead.
2. `mangal gen` and `mangal install` were removed. Use `mangal sources gen` and `mangal sources install` instead.
3. `mangal sources remove` command improved and accepts flags instead of args.

Inline JSON output is different now.

- JSON fields now follow the [camelCase](https://en.wikipedia.org/wiki/Camel_case) style instead of `PascalCase`
(actually, using PascalCase was never a goal, I just forgot to properly configure it).
But since it's a major release I can finally fix this.
- Structure was changed
- Additional fields were added

See [Inline mode wiki](https://github.com/metafates/mangal/wiki/Inline-mode) for new output schemas.

Please, consider these changes when migrating your applications that use mangal from 3rd version to 4th.
</details>

- Improved TUI experience
- Search completions in TUI. `mangal config info -k search.show_query_suggestions`
- Anilist caching significantly improved. Now, it will cache all search results (for 2 days)
- Update metadata of already downloaded manga (ComicInfo.xml, series.json, cover image) after changing Anilist bind. #124
See `mangal inline anilist update` for more info
- New command to generate json schema of inline output. See `mangal help inline schema`
- **Breaking** `downloader.default_source` was changed to `downloader.default_sources` and accepts array of strings.
See `mangal config info -k downloader.default_sources` for more info
- New `config reset` command
- Add caching for custom (lua) sources
- Include different cover sizes and color for json output #116
- Add option to omit dates for ComicInfo.xml #117
- By default, when reading a chapter, mangal will look for its downloaded copy, instead of downloading it again.
See `mangal config info -k downloader.read_downloaded`
- Overwrite old `series.json` file each time a chapter is downloaded
- Detect sources that use headless chrome and show that in the item description when selecting sources
- Option to use alternative ComicInfo.xml date.
See `mangal config info -k metadata.comic_info_xml_alternative_date` for more info
- Notify about new version in `help` command
- Include staff in ComicInfo.xml #119
- Add `--set-only` and `--unset-only` flags for `env` command. Old `--filter` flag was removed
- `version` command now has `--short` to just print the version without extra information
- **Breaking!** Your old reading history (via `mangal --continue`) will be reset
- Improved `clear` command
- Option to set threshold for tag relevance to be included in ComicInfo.xml #121
- Improved inline command json output, fixes
- Internal improvements

Enjoy!

## 3.14.2

- Do not put an invalid value for dates #114
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ help:
@echo " install Install the mangal binary"
@echo " uninstall Uninstall the mangal binary"
@echo " test Run the tests"
@echo " gif Generate usage gifs"
@echo " help Show this help message"
@echo ""

Expand All @@ -33,3 +34,7 @@ test:

uninstall:
@rm -f $(shell which mangal)

gif:
@vhs assets/tui.tape
@vhs assets/inline.tape
58 changes: 34 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<h1 align="center">Mangal 3 🪐</h1>
<h1 align="center">
<strong>Mangal 4 ☄️</strong>
</h1>

<p align="center">
<img alt="Linux" src="https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black">
Expand All @@ -11,7 +13,10 @@
The most advanced CLI manga downloader in the entire universe!
</h3>

https://user-images.githubusercontent.com/62389790/191430795-cb9859cc-5252-4155-b34b-ecf727003407.mp4
<p align="center">
<img alt="Mangal 4 TUI" src="assets/tui.gif">
</p>


## Try it!

Expand All @@ -36,15 +41,17 @@ curl -sSL mangal.metafates.one/run | sh

- __Lua Scrapers!!!__ You can add any source you want by creating your own _(or using someone's else)_ scraper with
__Lua 5.1__. See [mangal-scrapers repository](https://github.com/metafates/mangal-scrapers)
- __4 Built-in sources__ - [Mangadex](https://mangadex.org), [Manganelo](https://m.manganelo.com/wwww), [Mangakakalot](https://mangakakalot.com) & [Mangapill](https://mangapill.com)
- __4 Built-in sources__ - [Mangadex](https://mangadex.org), [Manganelo](https://m.manganelo.com/wwww), [Manganato](https://manganato.com) & [Mangapill](https://mangapill.com)
- __Download & Read Manga__ - I mean, it would be strange if you couldn't, right?
- __Caching__ - Mangal will cache as much data as possible, so you don't have to wait for it to download the same data over and over again.
- __4 Different export formats__ - PDF, CBZ, ZIP and plain images
- __3 Different modes__ - TUI, Mini and Inline
- __TUI ✨__ - You already know how to use it! (ノ>ω<)ノ :。・::・゚’★,。・:・゚’☆
- __Scriptable__ - You can use Mangal in your scripts, it's just a CLI app after all. [Examples](https://github.com/metafates/mangal/wiki/Inline-mode)
- __History__ - Resume your reading from where you left off!
- __Fast?__ - YES.
- __Monolith__ - ZERO runtime dependencies. Even Lua is built in.
- __Fancy__ - (ノ>ω<)ノ :。・::・゚’★,。・:・゚’☆
- __Cross-Platform__ - Linux, macOS, Windows, Termux
- __Anilist integration__ - Track your manga progress on Anilist when reading with Mangal.
- __Monolith__ - ZERO runtime dependencies. Even Lua is built in. Easy to install and use.
- __Cross-Platform__ - Linux, macOS, Windows, Termux, even your toaster. (¬‿¬ )
- __Anilist integration__ - Mangal will collect additional data from Anilist and use it to improve your reading experience. It can also sync your progress!

## Installation

Expand Down Expand Up @@ -129,7 +136,7 @@ make build # if you want to just build the binary

<details>
<summary>If you don't have GNU Make use this</summary>
</details>


```shell
# To build
Expand All @@ -139,6 +146,8 @@ go build -ldflags "-X 'github.com/metafates/mangal/constant.BuiltAt=$(date -u)'
go install -ldflags "-X 'github.com/metafates/mangal/constant.BuiltAt=$(date -u)' -X 'github.com/metafates/mangal/constant.BuiltBy=$(whoami)' -X 'github.com/metafates/mangal/constant.Revision=$(git rev-parse --short HEAD)' -s -w"
```

</details>

If you want to build mangal for other architecture, say ARM, you'll have to set env variables `GOOS` and `GOARCH`

```shell
Expand Down Expand Up @@ -183,29 +192,31 @@ Just run `mangal` and you're ready to go.

</details>

<img width="1280" alt="TUI" src="https://user-images.githubusercontent.com/62389790/191431456-462ef83d-52be-4fbe-8176-f5e5ecf5954e.png">
![TUI](https://user-images.githubusercontent.com/62389790/198830334-fd85c74f-cf3b-4e56-9262-5d62f7f829f4.png)

> If you wonder what those icons mean - `D` stands for "downloaded", `*` shows that chapter is marked to be downloaded.
> You can choose different icons, e.g. nerd font ones - just run mangal with `--icons nerd`.
> Available options are `nerd`, `emoji`, `kaomoji` and `squares`
### Mini

Mini mode tries to mimic [ani-cli](https://github.com/pystardust/ani-cli)

To run: `mangal mini`

<img width="613" alt="MINI" src="https://user-images.githubusercontent.com/62389790/191431713-a753743c-a4b2-4787-a054-4da322d70304.png">
![mini](https://user-images.githubusercontent.com/62389790/198830544-f2005ec4-c206-4fe0-bd08-862ffd08320e.png)

### Inline

Inline mode is intended for use with other scripts.

Example of usage:

mangal inline --source Manganelo --query "death note" --manga first --chapters all -d

> This will download all chapters of the "Death Note" from Manganelo.
Type `mangal help inline` for more information.

See [Wiki](https://github.com/metafates/mangal/wiki/Inline-mode) for more information
See [Wiki](https://github.com/metafates/mangal/wiki/Inline-mode) for more examples.

<img width="1249" alt="INLINE" src="https://user-images.githubusercontent.com/62389790/191431913-863fc67e-b30f-4656-b9b3-e2645915f86c.png">
<p align="center">
<img alt="Mangal 4 Inline" src="assets/inline.gif">
</p>

### Other

Expand Down Expand Up @@ -261,12 +272,11 @@ It should automatically appear in the list of available scrapers.

Mangal also supports integration with anilist.

It will mark chapters as read on Anilsit when you read them inside mangal.
Besides fetching metadata for each manga when downloading,
mangal can also mark chapters as read on your Anilsit profile when you read them inside mangal.

For more information see [wiki](https://github.com/metafates/mangal/wiki/Anilist-Integration)

> Maybe I'll add more sites in the future, like [myanimelist](https://myanimelist.net/). Open for suggestions!
## Honorable mentions

### Projects using mangal
Expand Down Expand Up @@ -294,11 +304,11 @@ For more information see [wiki](https://github.com/metafates/mangal/wiki/Anilist

### Contributors

And of course, thanks to all the contributors! You are awesome!
And of course, thanks to all contributors! You are awesome!

<p align="center">
<a href="https://github.com/metafates/mangal/graphs/contributors">
<img src="https://contrib.rocks/image?repo=metafates/mangal" />
<img alt="Contributors" src="https://contrib.rocks/image?repo=metafates/mangal" />
</a>
</p>

Expand All @@ -311,6 +321,6 @@ please consider starring it, that would mean a lot to me ⭐

<p align="center">
<a href="https://star-history.com/#metafates/mangal&Date">
<img src="https://api.star-history.com/svg?repos=metafates/mangal&type=Date"/>
<img alt="Star History" src="https://api.star-history.com/svg?repos=metafates/mangal&type=Date"/>
</a>
</p>
140 changes: 72 additions & 68 deletions anilist/cache.go
Original file line number Diff line number Diff line change
@@ -1,94 +1,98 @@
package anilist

import (
"encoding/json"
"github.com/metafates/mangal/filesystem"
"github.com/metafates/mangal/log"
"github.com/metafates/mangal/util"
"github.com/metafates/mangal/cache"
"github.com/metafates/mangal/where"
"io"
"os"
"github.com/samber/mo"
"path/filepath"
"time"
)

var cache = anilistCache{
data: &anilistCacheData{Mangas: make(map[string]*Manga)},
type cacheData[K comparable, T any] struct {
Mangas map[K]T `json:"mangas"`
}

type anilistCacheData struct {
Mangas map[string]*Manga `json:"mangas"`
type cacher[K comparable, T any] struct {
internal *cache.Cache[*cacheData[K, T]]
keyWrapper func(K) K
}

type anilistCache struct {
data *anilistCacheData
path string
initialized bool
}

func (a *anilistCache) Init() error {
if a.initialized {
return nil
func (c *cacher[K, T]) Get(key K) mo.Option[T] {
data := c.internal.Get()
if data.IsPresent() {
mangas, ok := data.MustGet().Mangas[c.keyWrapper(key)]
if ok {
return mo.Some(mangas)
}
}

log.Debug("Initializing anilist cacher")

path := filepath.Join(where.Cache(), "anilist_cache.json")
a.path = path
log.Debugf("Opening anilist cache file at %s", path)
file, err := filesystem.Api().OpenFile(path, os.O_RDONLY|os.O_CREATE, os.ModePerm)

if err != nil {
log.Warn(err)
return err
}

defer util.Ignore(file.Close)

contents, err := io.ReadAll(file)
if err != nil {
log.Warn(err)
return err
}
return mo.None[T]()
}

if len(contents) == 0 {
log.Debug("Anilist cache file is empty, skipping unmarshal")
return nil
func (c *cacher[K, T]) Set(key K, t T) error {
data := c.internal.Get()
if data.IsPresent() {
internal := data.MustGet()
internal.Mangas[c.keyWrapper(key)] = t
return c.internal.Set(internal)
} else {
internal := &cacheData[K, T]{Mangas: make(map[K]T)}
internal.Mangas[c.keyWrapper(key)] = t
return c.internal.Set(internal)
}
}

err = json.Unmarshal(contents, a.data)
if err != nil {
log.Warn(err)
return err
func (c *cacher[K, T]) Delete(key K) error {
data := c.internal.Get()
if data.IsPresent() {
internal := data.MustGet()
delete(internal.Mangas, c.keyWrapper(key))
return c.internal.Set(internal)
}

log.Debugf("Anilist cache file unmarshalled successfully, len is %d", len(a.data.Mangas))
return nil
}

func (a *anilistCache) Get(name string) (*Manga, bool) {
_ = a.Init()

mangas, ok := a.data.Mangas[normalizeName(name)]
return mangas, ok
var relationCacher = &cacher[string, int]{
internal: cache.New[*cacheData[string, int]](
where.AnilistBinds(),
&cache.Options{
// never expire
ExpireEvery: mo.None[time.Duration](),
},
),
keyWrapper: normalizedName,
}

func (a *anilistCache) Set(name string, manga *Manga) error {
_ = a.Init()

log.Debug("Setting anilist cacher entry")
a.data.Mangas[normalizeName(name)] = manga
marshalled, err := json.Marshal(a.data)
if err != nil {
log.Warn(err)
return err
}

file, err := filesystem.Api().OpenFile(a.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.ModePerm)
var searchCacher = &cacher[string, []int]{
internal: cache.New[*cacheData[string, []int]](
filepath.Join(where.Cache(), "anilist_search_cache.json"),
&cache.Options{
// update ids every 10 days, since new manga are not added that often
ExpireEvery: mo.Some(time.Hour * 24 * 10),
},
),
keyWrapper: normalizedName,
}

_, err = file.Write(marshalled)
if err != nil {
log.Warn(err)
}
var idCacher = &cacher[int, *Manga]{
internal: cache.New[*cacheData[int, *Manga]](
filepath.Join(where.Cache(), "anilist_id_cache.json"),
&cache.Options{
// update manga data every 2 days since it can change often
ExpireEvery: mo.Some(time.Hour * 24 * 2),
},
),
keyWrapper: func(id int) int { return id },
}

return err
var failCacher = &cacher[string, bool]{
internal: cache.New[*cacheData[string, bool]](
filepath.Join(where.Cache(), "anilist_fail_cache.json"),
&cache.Options{
// expire every minute
ExpireEvery: mo.Some(time.Minute),
},
),
keyWrapper: normalizedName,
}
Loading

0 comments on commit 36aab8d

Please sign in to comment.