jutsu-api 2.5
Simple and flexible API for jut.su
Requires Python: >=3.10

# Jutsu API

Simple and flexible API for [jut.su](https://jut.su/).

![sketch1673141061953](https://user-images.githubusercontent.com/65429873/211176796-f799629b-1b00-43e2-94da-6e43a6e63151.png)

As a weeb, I have my own favourite site for watching anime.
Now that I am also a super h4ck3r, I created an API
for accessing this site and it turned out pretty well.

# Installation

Install the module from PyPi:
```
pip install jutsu-api
```
Then import it just like that:
```py
from jutsu_api import API
```

# Documentation

## API

```py
API(verbosity:int = 0)
```

Class `API` is a singleton and can be initialized with
one parameter `verbosity`, which is responsible on how
much feedback the API sends. The feedback is being printed to stderr,
the default verbosity level is `0`.

### API.verbosity()

```py
.verbosity(v:int)
```

Set vebosity level explicitly.

### API.search()

```py
.search(keyword:str = "", filter:Filter = Filter(), maxpage:int = -1) -> list[Anime]
```

Searches the site for anime with given keyword and filter.
The [`Filter`](#filter) object is explained later.
`maxpage` parameter is used to limit the output by stopping
parsing after an amount of pages. `-1` is the default value,
and means that there should be no limitation. A list of [`Anime`](#anime)
objects is returned. If there are no results, the list will be empty.

### API.anime()

```py
.anime(id:str) -> Anime
```

Returns [`Anime`](#anime) object generated by parsing the `id` of that anime.
Anime's id is used and can be found in the link to the anime.

Example `id`: `stein-gate`

### API.episode()

```py
.episode(id:str) -> Episode
```

Returns a single [`Episode`](#episode) object generated by parsing the `id` of that episode.
Episode's id is the path link to the episode.

Example `id`: `stein-gate/season-1/episode-1.html`

## Anime

```py
Anime(
  name:Name|None = None,
  thumbnail:str|None = None,
  info:Filter|None = None,
  years:list[int]|None = None,
  age:int|None = None,
  description:str|None = None,
  content:Content|None = None,
  ongoing:bool|None = None,
  id:str|None = None
)
```

Class `Anime` provides a full information about the selected anime.
Parsing anime from [`API.search()`](#apisearch) does not give full information,
therefore some parameters have a getter, and if the corresponding parameter is `None`,
additional information is being fetched from anime's main page.
All parameters are optional, but at least one of `name:Name, id:str` must be specified.

### Anime.download()

```py
.download(quality:int|None = None, path:str = "", threads:int = 1)
```

Downloads the whole anime, walking over seasons and episodes
iteratively and downloading each. `quality` parameter specifies
the quality of the videos to be downloaded.
If `quality` is `None`, highest quality will be used.
`threads` parameter with default value `1`, if it is specified to be not equal `1`,
will create a ThreadPool and download simultaneously.
Downloading whole anime will create a folder with it's name and subfolders for seasons.
Parameter `path` will add a path in front of the output file and save the file
where specified.

Example `quality`: `720`

### Anime.selector

```py
.selector:Selector
```

Property of an `Anime` object, helps with selecting specific episodes for download.
See more in [`Selector`](#selector)

## Content

```py
Content(seasons:list[Season], films:Season|None = None)
```

`Content` object provides an accessor for the [`Season`](#season)s and films of an anime.
Some animes have no films, therefore sometimes the parameter `films` is `None`

### Content.count

```py
.count:int
```

The count property gives the total count of episodes in seasons.

## Season

```py
Season(title:str|None, episodes:list[Episode], name:Name|None = None)
```

A `Season` may be a normal season or the container for anime's films. 
Seasons' `title` is the general name of the season, which is moslty `Season <number>`
or `Full-length Films` for films container.
State of `name` can be different in following situations:

* Anime has only one season: name is `None`
* Anime has multiple unnamed seasons: name's `id` is present
* Anime has mutliple named seasons: name is fully present

More about [`Name`](#name) object is written further down.

### Season.download()

```py
.download(quality:int|None = None, path:str = "", threads:int = 1)
```

Pretty much the same as [`Anime.download()`](#animedownload).
`path` is added in front of all locations, so that
the actual downloading happens at that location.
Creates folders if the anime has multiple seasons,
downloads in the final folder directly, if the season is only one.

## Episode

```py
Episode(
  title:str|None = None,
  name:Name|None = None,
  duration:int|None = None,
  opening:Opening|None = None,
  ending:Ending|None = None,
  players:list[Player]|None = None,
  thumbnail:str|None = None,
  preview:str|None = None,
  id:str|None = None
)
```

Pretty much the same as [`Anime`](#anime) class.
`title` is the general name of the episode.
Fetching episode from [`Anime`](#anime) and getting it with
an id from [`API.anime()`](#apianime) will produce different titles.
`duration` is the length of the episode in seconds.
`preview` and `thumbnail` are links to pictures of
the preview and thumbnail respectively.
[`Opening`](#opening) and [`Ending`](#ending) objects will be explained later.
`players` property is a list of [`Player`](#player) objects,
which also will be explained later.

### Episode.download()

```py
.download(quality:int|None = None, path:str = "")
```

Downloads an episode with given `quality`.
If `quality` is `None`, highest quality will be used.
`path` will be added to the title and the episode
will be downloaded into the resulting file.

### Episode.player()

```py
.player(quality:int|None = None) -> Player|None
```

Gets a player with the given `quality` or
returns `None` if the player is not found.
If `quality` is `None`, returns highest quality player.

## Opening

```py
Opening(begin:int, end:int, link:str)
```

`begin` and `end` are timestamps of the opening.
`link` is the link to the music provided by the site.

## Ending

```py
Ending(begin:int, link:str)
```

Pretty much the same as the [`Opening`](#opening) class.

## Player

```py
Player(quality:int|None = None, link:str)
```

Internal object to manage the video of an episode.

### Player.download()

```py
.download(local:str|None = None)
```

Downloads the video to a local file with the name
of parameter `local`. If it is `None` then the
file name used on the server will be chosen.

## Filter

```py
Filter(
  genres:list[Name] = [],
  types:list[Name] = [],
  years:list[Name] = [],
  sorting:list[Name] = [],
  link:str|None = None
)
```

The `Filter` object is used to filter out
results in searching the API, or to describe
an already found anime with it's genres, types,
publishing years etc. In addition to those
lists, a link can be given, which describes the filter.

Example `link`: `adventure-comedy-game/2000-2007-and-2008-2014/`

### Filter.available

```py
Filter.available:Filter
```

Static property of the object `Filter`, gives
back another `Filter` object, which has all
the genres, types etc. available on the site.

## Name

```py
Name(name:str|None, id:str, orig:str|None = None)
```

The `Name` object is used in various places to name things.
Since the jut.su site is in russian language, the `name`
is the visible name, `orig` is the english or japanese name
and `id` is the id used on the site for navigation, fetching etc.

## Selector

```py
Selector(parent:Anime)
```

A helper object for specific selections of episodes for download.

### Selector.select_episodes()

```py
.select_episodes(quality:int|None = None, items:Iterable[int]|None = None) -> Downloader
```

Selects episodes with specific indexes. This ignores seasons and
counts each next episode as `index + 1`. Be careful: Index of episodes starts with a `0`.
If `items` is `None`, everything will be selected.
The selection quality is the parameter `quality`. If it is `None`, highest
quality will be selected.

### Selector.select_seasons()

```py
.select_seasons(quality:int|None = None, items:Iterable[int]|None = None) -> Downloader
```

Selects seasons with specific indexes. Those selected seasons are selected
completely including all their episodes. Indexing begins with a `0`.
If `items` is `None`, everything will be selected.
The selection quality is the parameter `quality`. If it is `None`, highest
quality will be selected.


### Selector.select_in_seasons()

```py
.select_in_seasons(quality:int|None = None, items:dict[int, Iterable[int]|None]) -> Downloader
```

Selects specific episodes in specific seasons. Seasons index goes into
the key of dictionary, the value is an iterator of indexes inside this episode.
The selection quality is the parameter `quality`. If it is `None`, highest
quality will be selected.

## Downloader

```py
Downloader(items:list[list[Player, str]])
```

The object being returned from [`Selector`](#selector).

### Downloader.add()

```py
.add(downloader:Downloader)
```

Adds items of another downloader to itself. Created for merging queues.

### Downloader.download()

```py
.download(path:str = "", threads:int = 1)
```

Downloads the queue of the object using threads and a `path`, which is being added
in front of the location.

# Example

The following example will download the anime
"One Punch Man" into weeb's secret anime folder
in full quality and will use 3 threads
to download simultaneously.

```py
from jutsu_api import API

api = API(verbosity = 1)

search = api.search(keyword = "punch")
onepunch = search[0]

onepunch.download(
  path = "/home/weeb/homework/",
  threads = 3
)
```

This example uses [`Selector`](#selector) to download everything after episode 5:

```py
from jutsu_api import API

api = API(verbosity = 1)

search = api.search(keyword = "punch")
onepunch = search[0]

episodes = onepunch.selector.select_episodes(
  range(5, onepunch.content.count)
)

episodes.download(
  path = "/home/weeb/homework/",
  threads = 3
)
```