API reference¶
Everything below is importable from the top-level aniliberty package unless noted.
Client¶
aniliberty.AniLiberty ¶
AniLiberty(
*,
base_url: str = DEFAULT_BASE_URL,
timeout: float = DEFAULT_TIMEOUT,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
headers: Mapping[str, str] | None = None,
http_client: AsyncClient | None = None,
token: str | None = None,
cache_dir: str | PathLike[str] | None = None,
cache_ttl: float | None = None,
)
Async client for the AniLibria API.
Groups the API into resource namespaces that follow the tags of its OpenAPI spec
(client.app, ...). Use it as an async context manager so the connection pool is
closed when you're done, or call aclose() yourself.
Attributes:
| Name | Type | Description |
|---|---|---|
http |
The low-level client every resource uses. |
|
app |
Service endpoints — API status and quick release search. |
|
releases |
Anime releases — latest, random, recommended, by ID/alias, members. |
|
episodes |
Episodes of releases, with HLS links. |
|
catalog |
The release catalog; |
|
schedule |
The weekly airing schedule. |
|
genres |
Genres, and the releases of each. |
|
franchises |
Franchises — groups of related releases. |
|
torrents |
Torrents, |
|
teams |
AniLibria's own teams, roles and members. |
|
media |
Front-page promo banners and AniLibria's YouTube videos. |
|
auth |
Logging in and out — by password, one-time code or social network. |
|
me |
The logged-in user's own account — profile, favorites, collections, watch progress and history (needs a session). |
Example
import asyncio
from aniliberty import AniLiberty
async def main() -> None:
async with AniLiberty() as client:
status = await client.app.status()
print(status.is_alive)
asyncio.run(main())
Initialize the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
API base URL including the version prefix. Defaults to the main server,
|
DEFAULT_BASE_URL
|
timeout
|
float
|
Per-request timeout, in seconds. Ignored when |
DEFAULT_TIMEOUT
|
max_concurrency
|
int
|
Maximum number of requests in flight at once, no matter how many
coroutines share this client. Requests over the limit wait for a free slot.
Consecutive requests are also spaced at least 0.1 s apart, and 429/5xx/network
failures are retried with backoff — see |
DEFAULT_MAX_CONCURRENCY
|
headers
|
Mapping[str, str] | None
|
Extra headers sent with every request, merged over the SDK's defaults
( |
None
|
http_client
|
AsyncClient | None
|
A ready |
None
|
token
|
str | None
|
Session token of an earlier login ( |
None
|
cache_dir
|
str | PathLike[str] | None
|
Directory to cache raw API responses in, e.g. |
None
|
cache_ttl
|
float | None
|
Seconds after which a cached response is stale and fetched again,
e.g. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Source code in src/aniliberty/client.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
token
property
writable
¶
token: str | None
The session token sent with every request, or None when not logged in.
Set by client.auth.login() (and the other login methods), cleared by
client.auth.logout(). Save it to reuse the session later as
AniLiberty(token=...); assign None to stop sending it without ending the
session. It is a secret — don't log it.
aclose
async
¶
aclose() -> None
Close the connection pool. Safe to call more than once.
Source code in src/aniliberty/client.py
171 172 173 | |
Resources¶
The namespaces of an AniLiberty instance: client.app, client.releases,
client.releases.ratings, client.episodes, client.catalog (with client.catalog.references), client.schedule, client.genres, client.franchises, client.torrents, client.teams, client.media, client.auth (with client.auth.otp, client.auth.social, client.auth.password), client.me (with client.me.favorites, client.me.collections and their references, client.me.views). Not meant to be created directly — import from aniliberty.resources only
for type annotations.
aniliberty.resources.AppResource ¶
AppResource(http: HttpClient)
Bases: BaseResource
Service endpoints of the API, available as client.app.
Source code in src/aniliberty/resources/_base.py
28 29 | |
status
async
¶
status() -> ApiStatus
Get the status of the API.
A cheap request that is handy as a health check. The response also lists the hosts
the API is available at, which can be used as base_url mirrors.
Returns:
| Type | Description |
|---|---|
ApiStatus
|
The API status. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
async with AniLiberty() as client:
status = await client.app.status()
print(status.is_alive, status.available_api_endpoints)
Source code in src/aniliberty/resources/app.py
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
search_releases
async
¶
search_releases(
query: str, *, refresh: bool = False
) -> list[Release]
Quick search of releases by name — the search box of the site.
Matches Russian and English names and aliases, fuzzily: "frieren",
"Фрирен" and "sousou-no-frieren" find the same release. There is no paging
and no limit: the API returns a short list (at most 20 releases in every query
checked), in an order it chooses. For filtered, paginated search use the catalog
instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
query
|
str
|
Search text. |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Release]
|
Matching releases, without the per-endpoint extras ( |
list[Release]
|
an empty list when nothing matches. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for release in await client.app.search_releases("frieren"):
print(release.id, release.name.main)
Source code in src/aniliberty/resources/app.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | |
aniliberty.resources.ReleasesResource ¶
ReleasesResource(http: HttpClient)
Bases: BaseResource
Anime releases, available as client.releases.
For filtering and sorting the whole catalog, see client.catalog; this namespace
covers single releases and the ready-made selections.
limit on every method here must be between 1 and 50 — the API answers anything else
with a 422, raised as ValidationError (see docs/api-notes.md).
Attributes:
| Name | Type | Description |
|---|---|---|
ratings |
The logged-in user's own scores of releases (needs a session). |
Source code in src/aniliberty/resources/releases.py
40 41 42 | |
latest
async
¶
latest(
*, limit: int | None = None, refresh: bool = False
) -> builtins.list[Release]
Get the latest releases — the ones that got new episodes most recently.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many releases to return, 1 to 50. Omitted, the API picks the number. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Release]
|
The releases, with |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for release in await client.releases.latest(limit=5):
print(release.fresh_at, release.name.main)
Source code in src/aniliberty/resources/releases.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | |
random
async
¶
random(
*, limit: int | None = None
) -> builtins.list[Release]
Get random releases.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many releases to return, 1 to 50. |
None
|
Returns:
| Type | Description |
|---|---|
list[Release]
|
The releases, with |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
[release] = await client.releases.random(limit=1)
print(release.name.main, release.year)
Source code in src/aniliberty/resources/releases.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
recommended
async
¶
recommended(
*,
release: int | Release | None = None,
limit: int | None = None,
) -> builtins.list[Release]
Get recommended releases, optionally ones similar to a given release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release | None
|
Release ID, or a |
None
|
limit
|
int | None
|
How many releases to return, 1 to 50. |
None
|
Returns:
| Type | Description |
|---|---|
list[Release]
|
The releases, with |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
similar = await client.releases.recommended(release=release, limit=5)
Source code in src/aniliberty/resources/releases.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | |
list
async
¶
list(
*,
ids: Sequence[int] | None = None,
aliases: Sequence[str] | None = None,
page: int | None = None,
limit: int | None = None,
refresh: bool = False,
) -> Page[Release]
Get several releases at once by their IDs and/or aliases.
Releases given both by ID and by alias are returned once. The order of the result
doesn't follow the order of ids/aliases. To fetch all pages, use
iter_list().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids
|
Sequence[int] | None
|
Release IDs. |
None
|
aliases
|
Sequence[str] | None
|
Release aliases. |
None
|
page
|
int | None
|
Page number, starting at 1. |
None
|
limit
|
int | None
|
Page size, 1 to 50. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Page[Release]
|
One page of releases, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
Neither |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.releases.list(ids=[10172, 10297], aliases=["sousou-no-frieren"])
for release in page.items:
print(release.id, release.name.main)
Source code in src/aniliberty/resources/releases.py
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
iter_list ¶
iter_list(
*,
ids: Sequence[int] | None = None,
aliases: Sequence[str] | None = None,
limit: int | None = None,
refresh: bool = False,
) -> AsyncIterator[Release]
Iterate over all releases matching ids/aliases, fetching page by page.
Same as calling list() with page=1, 2, ... until the last page; the next page
is requested only once the current one has been consumed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ids
|
Sequence[int] | None
|
Release IDs. |
None
|
aliases
|
Sequence[str] | None
|
Release aliases. |
None
|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Release]
|
An async iterator over the releases. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Neither |
Example
async for release in client.releases.iter_list(ids=favorite_ids, limit=50):
print(release.name.main)
Source code in src/aniliberty/resources/releases.py
185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | |
get
async
¶
get(
release: int | str | Release, *, refresh: bool = False
) -> Release
Get one release by its ID or alias.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | str | Release
|
Release ID ( |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Release
|
The release, with |
Release
|
|
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No release with this ID or alias. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
release = await client.releases.get("tensei-shitara-ken-deshita-2nd-season")
print(release.name.main, release.episodes_total)
Source code in src/aniliberty/resources/releases.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | |
members
async
¶
members(
release: int | str | Release, *, refresh: bool = False
) -> builtins.list[ReleaseMember]
Get the people who worked on a release (voice actors, translators, ...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | str | Release
|
Release ID, alias, or a |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ReleaseMember]
|
The release's members. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No release with this ID or alias. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for member in await client.releases.members(10297):
print(member.nickname, member.role.description if member.role else "")
Source code in src/aniliberty/resources/releases.py
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
timecodes
async
¶
timecodes(
release: int | str | Release,
) -> builtins.list[EpisodeView]
Get the logged-in user's watch records of a release's episodes. Needs a session.
Records appear about a minute after client.me.views.save_timecodes().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | str | Release
|
Release ID, alias, or a |
required |
Returns:
| Type | Description |
|---|---|
list[EpisodeView]
|
One record per episode the user has started; empty if none. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No release with this ID or alias. |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
watched = {v.release_episode_id for v in await client.releases.timecodes(9839)
if v.is_watched}
Source code in src/aniliberty/resources/releases.py
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
aniliberty.resources.RatingsResource ¶
RatingsResource(http: HttpClient)
Bases: BaseResource
The logged-in user's own scores of releases, available as client.releases.ratings.
Needs a session (client.auth.login() or AniLiberty(token=...)); without a valid
one every method raises AuthRequiredError. Releases are identified by numeric ID only:
the API answers an alias with a 404, so a str is rejected.
The release's overall Release.rating is computed by the API on its own schedule: it
didn't change within several minutes of a score being set (see docs/api-notes.md).
Source code in src/aniliberty/resources/_base.py
28 29 | |
get
async
¶
get(release: int | Release) -> UserRating
Get the user's score for a release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
Returns:
| Type | Description |
|---|---|
UserRating
|
The score; |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
AuthRequiredError
|
No valid session. |
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
mine = await client.releases.ratings.get(9839)
print(mine.score) # e.g. 8, or None
Source code in src/aniliberty/resources/ratings.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
set
async
¶
set(release: int | Release, *, score: int) -> UserRating
Rate a release, or change the user's score for it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
score
|
int
|
Whole number from 1 to 10. |
required |
Returns:
| Type | Description |
|---|---|
UserRating
|
The saved score. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
|
AuthRequiredError
|
No valid session. |
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.releases.ratings.set(release, score=9)
Source code in src/aniliberty/resources/ratings.py
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
remove
async
¶
remove(release: int | Release) -> UserRating
Withdraw the user's score for a release. A no-op if there is none.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
Returns:
| Type | Description |
|---|---|
UserRating
|
The rating afterwards, with |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
AuthRequiredError
|
No valid session. |
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.releases.ratings.remove(9839)
Source code in src/aniliberty/resources/ratings.py
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
aniliberty.resources.EpisodesResource ¶
EpisodesResource(http: HttpClient)
Bases: BaseResource
Episodes of releases, available as client.episodes.
Episode IDs come from Release.episodes (client.releases.get()/list()) or
Release.latest_episode (client.releases.latest()).
Source code in src/aniliberty/resources/_base.py
28 29 | |
get
async
¶
get(episode: str | Episode) -> Episode
Get one episode, with its HLS links and its release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
episode
|
str | Episode
|
Episode ID, a UUID string such as
|
required |
Returns:
| Type | Description |
|---|---|
Episode
|
The episode, with |
Episode
|
all of its episodes. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No episode with this ID (also for IDs that aren't UUIDs). |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
release = await client.releases.get("tensei-shitara-ken-deshita-2nd-season")
episode = await client.episodes.get(release.episodes[0].id)
print(episode.ordinal, episode.hls_1080 or episode.hls_720)
Source code in src/aniliberty/resources/episodes.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | |
timecode
async
¶
timecode(episode: str | Episode) -> EpisodeView
Get the logged-in user's watch record of an episode. Needs a session.
Records appear about a minute after client.me.views.save_timecodes(); for the
positions saved just now use client.me.views.timecodes().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
episode
|
str | Episode
|
Episode ID (a UUID string), or an |
required |
Returns:
| Type | Description |
|---|---|
EpisodeView
|
Where the user stopped in the episode and whether they finished it. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
The user has no record for this episode (yet), or there is no such episode. |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
view = await client.episodes.timecode(episode.id)
print(view.time, view.is_watched)
Source code in src/aniliberty/resources/episodes.py
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
aniliberty.resources.CatalogResource ¶
CatalogResource(http: HttpClient)
Bases: BaseResource
The release catalog with filters and sorting, available as client.catalog.
How the filters combine (checked against the live API, see docs/api-notes.md):
genres: a release must have all of the given genres (AND);types,seasons,age_ratings,publish_statuses,production_statuses: any of the given values (OR);- different filters: all of them must match (AND);
year_from/year_to: inclusive on both ends;search: the same matching asclient.app.search_releases(), and likewise capped at about 20 matches in total, whatever the other filters and the page size.
Without sorting, releases come in "FRESH_AT_DESC" order (recently updated first).
Attributes:
| Name | Type | Description |
|---|---|---|
references |
Values the catalog filters accept. |
Source code in src/aniliberty/resources/catalog.py
293 294 295 | |
releases
async
¶
releases(
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
seasons: Sequence[Season | ValueDescription[str]]
| None = None,
year_from: int | None = None,
year_to: int | None = None,
search: str | None = None,
sorting: CatalogSorting | SortingOption | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
publish_statuses: Sequence[
PublishStatus | ValueDescription[str]
]
| None = None,
production_statuses: Sequence[
ProductionStatus | ValueDescription[str]
]
| None = None,
page: int | None = None,
limit: int | None = None,
refresh: bool = False,
) -> Page[Release]
Get one page of catalog releases matching the filters.
Every filter is optional; without any, this is the whole catalog. Values can be
passed as plain strings/IDs or as the items client.catalog.references returns.
To go through all pages, use iter_releases().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genres
|
Sequence[int | Genre] | None
|
Genre IDs (or |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
Release types, e.g. |
None
|
seasons
|
Sequence[Season | ValueDescription[str]] | None
|
Seasons, e.g. |
None
|
year_from
|
int | None
|
Earliest release year, inclusive. |
None
|
year_to
|
int | None
|
Latest release year, inclusive. |
None
|
search
|
str | None
|
Text to search in names (capped at about 20 matches by the API). |
None
|
sorting
|
CatalogSorting | SortingOption | None
|
Sort order, e.g. |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
Age ratings, e.g. |
None
|
publish_statuses
|
Sequence[PublishStatus | ValueDescription[str]] | None
|
|
None
|
production_statuses
|
Sequence[ProductionStatus | ValueDescription[str]] | None
|
|
None
|
page
|
int | None
|
Page number, starting at 1. A page past the last one is empty. |
None
|
limit
|
int | None
|
Page size, 1 to 50. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Page[Release]
|
One page of releases, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
A filter value isn't one the API knows, or |
TypeError
|
A list filter got a single string ( |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.catalog.releases(
genres=[14], types=["TV"], year_from=2020, sorting="RATING_DESC", limit=25
)
print(f"{page.total} releases, page 1 of {page.total_pages}")
Source code in src/aniliberty/resources/catalog.py
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 | |
iter_releases ¶
iter_releases(
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
seasons: Sequence[Season | ValueDescription[str]]
| None = None,
year_from: int | None = None,
year_to: int | None = None,
search: str | None = None,
sorting: CatalogSorting | SortingOption | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
publish_statuses: Sequence[
PublishStatus | ValueDescription[str]
]
| None = None,
production_statuses: Sequence[
ProductionStatus | ValueDescription[str]
]
| None = None,
limit: int | None = None,
refresh: bool = False,
) -> AsyncIterator[Release]
Iterate over all catalog releases matching the filters, fetching page by page.
Takes the same filters as releases(). The next page is requested only once the
current one has been consumed, so breaking out of the loop early stops the requests.
The whole catalog is a couple of thousand releases — iterate all of it only when you
really need everything.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genres
|
Sequence[int | Genre] | None
|
See |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
See |
None
|
seasons
|
Sequence[Season | ValueDescription[str]] | None
|
See |
None
|
year_from
|
int | None
|
See |
None
|
year_to
|
int | None
|
See |
None
|
search
|
str | None
|
See |
None
|
sorting
|
CatalogSorting | SortingOption | None
|
See |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
See |
None
|
publish_statuses
|
Sequence[PublishStatus | ValueDescription[str]] | None
|
See |
None
|
production_statuses
|
Sequence[ProductionStatus | ValueDescription[str]] | None
|
See |
None
|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Release]
|
An async iterator over the releases. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Invalid filter values — raised right away, not when iteration starts. |
TypeError
|
A list filter got a single string or a value of the wrong type. |
Example
async for release in client.catalog.iter_releases(types=["MOVIE"], limit=50):
print(release.year, release.name.main)
Source code in src/aniliberty/resources/catalog.py
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 | |
aniliberty.resources.CatalogReferencesResource ¶
CatalogReferencesResource(http: HttpClient)
Bases: BaseResource
Values the catalog filters accept, available as client.catalog.references.
Each method returns the list the site shows in its catalog filters. Use them instead of
hardcoding values: the value (or id for genres, the number for years) of an item is
exactly what the corresponding catalog filter takes.
The lists are small and change rarely — fetching them once per program run is enough.
Example
genres = await client.catalog.references.genres()
action = next(g for g in genres if g.name == "Экшен")
print(action.id)
Source code in src/aniliberty/resources/_base.py
28 29 | |
genres
async
¶
genres(*, refresh: bool = False) -> list[Genre]
Get the genres the catalog can be filtered by.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Genre]
|
Genres with |
list[Genre]
|
|
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
genres = await client.catalog.references.genres()
action = next(genre for genre in genres if genre.name == "Экшен")
page = await client.catalog.releases(genres=[action])
Source code in src/aniliberty/resources/catalog.py
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
types
async
¶
types(
*, refresh: bool = False
) -> list[ValueDescription[str]]
Get the release types, e.g. value="TV", description="ТВ".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The release types. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
types = await client.catalog.references.types()
print([t.value for t in types]) # ['TV', 'ONA', 'WEB', ...]
Source code in src/aniliberty/resources/catalog.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | |
seasons
async
¶
seasons(
*, refresh: bool = False
) -> list[ValueDescription[str]]
Get the seasons, e.g. value="winter", description="Зима".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The seasons. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for season in await client.catalog.references.seasons():
print(season.value, season.description)
Source code in src/aniliberty/resources/catalog.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | |
years
async
¶
years(*, refresh: bool = False) -> list[int]
Get the years that have releases in the catalog, oldest first.
Only years with at least one release are listed, so the range can have gaps.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[int]
|
The years. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response wasn't a list of integers. |
Example
years = await client.catalog.references.years()
print(years[0], years[-1]) # the oldest and the newest year
Source code in src/aniliberty/resources/catalog.py
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
age_ratings
async
¶
age_ratings(*, refresh: bool = False) -> list[AgeRating]
Get the age ratings, e.g. value="R16_PLUS", label="16+".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[AgeRating]
|
The age ratings ( |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for rating in await client.catalog.references.age_ratings():
print(rating.value, rating.label)
Source code in src/aniliberty/resources/catalog.py
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
publish_statuses
async
¶
publish_statuses(
*, refresh: bool = False
) -> list[ValueDescription[str]]
Get the airing statuses, e.g. value="IS_ONGOING", description="Онгоинг".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The publish statuses. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
statuses = await client.catalog.references.publish_statuses()
ongoing = next(s for s in statuses if s.value == "IS_ONGOING")
page = await client.catalog.releases(publish_statuses=[ongoing])
Source code in src/aniliberty/resources/catalog.py
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | |
production_statuses
async
¶
production_statuses(
*, refresh: bool = False
) -> list[ValueDescription[str]]
Get the dubbing statuses, e.g. value="IS_IN_PRODUCTION".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The production statuses. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for status in await client.catalog.references.production_statuses():
print(status.value, status.description)
Source code in src/aniliberty/resources/catalog.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | |
sorting
async
¶
sorting(*, refresh: bool = False) -> list[SortingOption]
Get the ways the catalog can be sorted, e.g. value="FRESH_AT_DESC".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[SortingOption]
|
The sorting options. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for option in await client.catalog.references.sorting():
print(option.value, option.label)
Source code in src/aniliberty/resources/catalog.py
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |
aniliberty.resources.ScheduleResource ¶
ScheduleResource(http: HttpClient)
Bases: BaseResource
The weekly airing schedule of ongoing releases, available as client.schedule.
Source code in src/aniliberty/resources/_base.py
28 29 | |
now
async
¶
now(*, refresh: bool = False) -> ScheduleNow
Get the releases scheduled for yesterday, today and tomorrow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
ScheduleNow
|
The schedule for the three days. Each item says whether this week's episode is |
ScheduleNow
|
already out ( |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
schedule = await client.schedule.now()
for item in schedule.today:
episode = item.published_release_episode
status = f"episode {episode.ordinal} is out" if episode else "not out yet"
print(item.release.name.main, "—", status)
Source code in src/aniliberty/resources/schedule.py
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
week
async
¶
week(*, refresh: bool = False) -> list[ScheduledRelease]
Get the schedule for the whole current week.
The list is not ordered by weekday; group or sort it by
item.release.publish_day.value (1 = Monday ... 7 = Sunday) as needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[ScheduledRelease]
|
All releases in this week's schedule. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
by_day: dict[int, list[ScheduledRelease]] = {}
for item in await client.schedule.week():
day = item.release.publish_day.value if item.release.publish_day else 0
by_day.setdefault(day, []).append(item)
Source code in src/aniliberty/resources/schedule.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | |
aniliberty.resources.GenresResource ¶
GenresResource(http: HttpClient)
Bases: BaseResource
Anime genres with their images and release counts, available as client.genres.
To filter the catalog by several genres at once, use client.catalog.releases(genres=...);
releases() here lists the releases of one genre.
Source code in src/aniliberty/resources/_base.py
28 29 | |
list
async
¶
list(*, refresh: bool = False) -> builtins.list[Genre]
Get all genres.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Genre]
|
Every genre, with |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for genre in await client.genres.list():
print(genre.name, genre.total_releases)
Source code in src/aniliberty/resources/genres.py
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
get
async
¶
get(genre: int | Genre, *, refresh: bool = False) -> Genre
Get one genre.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genre
|
int | Genre
|
Genre ID, or a |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Genre
|
The genre, with |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No genre with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
genre = await client.genres.get(14)
print(genre.name, genre.total_releases)
Source code in src/aniliberty/resources/genres.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | |
random
async
¶
random(*, limit: int | None = None) -> builtins.list[Genre]
Get random genres.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many genres to return. A value outside 1..(number of genres) isn't an error: the API returns all genres instead. |
None
|
Returns:
| Type | Description |
|---|---|
list[Genre]
|
The genres, in random order. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for genre in await client.genres.random(limit=3):
print(genre.name)
Source code in src/aniliberty/resources/genres.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
releases
async
¶
releases(
genre: int | Genre,
*,
page: int | None = None,
limit: int | None = None,
refresh: bool = False,
) -> Page[Release]
Get one page of the releases of a genre.
The order is the API's own (neither by update date nor by year). For sorting and for
combining genres with other filters, use client.catalog.releases(genres=[...]).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genre
|
int | Genre
|
Genre ID or |
required |
page
|
int | None
|
Page number, starting at 1. The API treats 0 and negative numbers as 1. |
None
|
limit
|
int | None
|
Page size, 1 to 30 here (not 50 as in the catalog). Omitted, the API uses 15. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Page[Release]
|
One page of releases, with |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No genre with this ID. |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.genres.releases(14, limit=30)
print(f"{page.total} releases in this genre")
Source code in src/aniliberty/resources/genres.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
iter_releases ¶
iter_releases(
genre: int | Genre,
*,
limit: int | None = None,
refresh: bool = False,
) -> AsyncIterator[Release]
Iterate over all releases of a genre, fetching page by page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genre
|
int | Genre
|
Genre ID or |
required |
limit
|
int | None
|
Page size, 1 to 30 — affects how many requests are made, not the result. |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Release]
|
An async iterator over the releases. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
Example
async for release in client.genres.iter_releases(14, limit=30):
print(release.name.main)
Source code in src/aniliberty/resources/genres.py
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | |
aniliberty.resources.FranchisesResource ¶
FranchisesResource(http: HttpClient)
Bases: BaseResource
Franchises — groups of related releases, available as client.franchises.
Source code in src/aniliberty/resources/_base.py
28 29 | |
list
async
¶
list(*, refresh: bool = False) -> builtins.list[Franchise]
Get all franchises (a few hundred, in one response).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Franchise]
|
Every franchise, without |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
franchises = await client.franchises.list()
biggest = max(franchises, key=lambda f: f.total_releases or 0)
Source code in src/aniliberty/resources/franchises.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
get
async
¶
get(
franchise: str | Franchise, *, refresh: bool = False
) -> Franchise
Get one franchise with its releases.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
franchise
|
str | Franchise
|
Franchise ID (a UUID string), or a |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
Franchise
|
The franchise, with |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No franchise with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
franchise = (await client.franchises.list())[0]
full = await client.franchises.get(franchise)
print(full.name, [fr.release.name.main for fr in full.franchise_releases])
Source code in src/aniliberty/resources/franchises.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | |
random
async
¶
random(
*, limit: int | None = None
) -> builtins.list[Franchise]
Get random franchises.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many franchises to return. Omitted, 0, or more than there are, the API returns all franchises (a few hundred) — pass a limit. |
None
|
Returns:
| Type | Description |
|---|---|
list[Franchise]
|
The franchises, without |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for franchise in await client.franchises.random(limit=3):
print(franchise.name, franchise.first_year, franchise.last_year)
Source code in src/aniliberty/resources/franchises.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | |
for_release
async
¶
for_release(
release: int | Release, *, refresh: bool = False
) -> builtins.list[Franchise]
Get the franchises a release belongs to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Franchise]
|
The franchises (usually one), with |
list[Franchise]
|
release isn't part of any franchise. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
release = await client.releases.get("sousou-no-frieren")
for franchise in await client.franchises.for_release(release):
print(franchise.name, [r.release.name.main for r in franchise.franchise_releases])
Source code in src/aniliberty/resources/franchises.py
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | |
aniliberty.resources.TorrentsResource ¶
TorrentsResource(http: HttpClient)
Bases: BaseResource
Torrents of releases, their .torrent files and RSS feeds, as client.torrents.
A torrent is identified by its numeric id or its info hash; every method that takes
a torrent also accepts a Torrent object.
Source code in src/aniliberty/resources/_base.py
28 29 | |
list
async
¶
list(
*, page: int | None = None, limit: int | None = None
) -> Page[Torrent]
Get one page of all torrents.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
int | None
|
Page number, starting at 1. |
None
|
limit
|
int | None
|
Page size, 1 to 50. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Torrent]
|
One page of torrents, each with its |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.torrents.list(limit=10)
for torrent in page.items:
print(torrent.label, torrent.size)
Source code in src/aniliberty/resources/torrents.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
iter_list ¶
iter_list(
*, limit: int | None = None
) -> AsyncIterator[Torrent]
Iterate over all torrents (a few thousand), fetching page by page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Torrent]
|
An async iterator over the torrents. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error, while iterating. |
TransportError
|
A request failed without a response, while iterating. |
Example
async for torrent in client.torrents.iter_list(limit=50):
print(torrent.hash, torrent.seeders)
Source code in src/aniliberty/resources/torrents.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
get
async
¶
get(torrent: int | str | Torrent) -> Torrent
Get one torrent by its ID or info hash.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
torrent
|
int | str | Torrent
|
Torrent ID, info hash (any letter case), or a |
required |
Returns:
| Type | Description |
|---|---|
Torrent
|
The torrent, with |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No torrent with this ID or hash. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
torrent = await client.torrents.get("17ceda4b3c2b8823b7f1139dca0b49aea5fff933")
print(torrent.label, torrent.release.name.main if torrent.release else None)
Source code in src/aniliberty/resources/torrents.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
for_release
async
¶
for_release(
release: int | Release,
) -> builtins.list[Torrent]
Get the torrents of a release.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
Returns:
| Type | Description |
|---|---|
list[Torrent]
|
The release's torrents; an empty list if it has none. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
release = await client.releases.get("sousou-no-frieren")
for torrent in await client.torrents.for_release(release):
print(torrent.label, torrent.size, torrent.seeders)
Source code in src/aniliberty/resources/torrents.py
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
file
async
¶
file(torrent: int | str | Torrent) -> TorrentFile
Download a .torrent file into memory.
The files checked were 22 to 388 KB. To save one to disk without holding it in
memory, use download().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
torrent
|
int | str | Torrent
|
Torrent ID, info hash, or a |
required |
Returns:
| Type | Description |
|---|---|
TorrentFile
|
The file's bytes and the file name the API suggests. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No torrent with this ID or hash. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
torrent_file = await client.torrents.file(torrent)
Path(torrent_file.filename or "release.torrent").write_bytes(torrent_file.content)
Source code in src/aniliberty/resources/torrents.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
download
async
¶
download(
torrent: int | str | Torrent,
*,
path: str | PathLike[str],
) -> Path
Download a .torrent file to disk, streaming it in chunks.
The file is written to a temporary <name>.part next to the target and renamed
only once complete, so a failed download never leaves a truncated .torrent
behind. An existing file at the target path is overwritten.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
torrent
|
int | str | Torrent
|
Torrent ID, info hash, or a |
required |
path
|
str | PathLike[str]
|
Where to save it: a file path, or an existing directory — then the file is
named as the API suggests (falling back to |
required |
Returns:
| Type | Description |
|---|---|
Path
|
The path of the saved file. |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No torrent with this ID or hash. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
OSError
|
The file couldn't be written. |
Example
saved = await client.torrents.download(torrent, path="downloads/")
print(saved) # downloads/One Piece - AniLiberty.TOP [WEBRip 1080p][AVC][1-24].torrent
Source code in src/aniliberty/resources/torrents.py
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
rss
async
¶
rss(*, limit: int | None = None) -> str
Get the RSS feed of the latest torrents, as XML text.
The feed is meant for torrent clients and feed readers; each <item> has the
torrent's hash (<guid>), <torrentId>, <releaseId> and an <enclosure>
linking to the .torrent file. For the same data as models, use list().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many torrents to include. Omitted, the API includes 10. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The RSS 2.0 document. |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
Example
xml = await client.torrents.rss(limit=10)
print(xml[:60]) # <?xml version="1.0" encoding="UTF-8"?>...
Source code in src/aniliberty/resources/torrents.py
214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | |
rss_for_release
async
¶
rss_for_release(release: int | Release) -> str
Get the RSS feed of a release's torrents, as XML text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
release
|
int | Release
|
Release ID, or a |
required |
Returns:
| Type | Description |
|---|---|
str
|
The RSS 2.0 document. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
NotFoundError
|
No release with this ID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
xml = await client.torrents.rss_for_release(release)
Source code in src/aniliberty/resources/torrents.py
242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
aniliberty.resources.TeamsResource ¶
TeamsResource(http: HttpClient)
Bases: BaseResource
AniLibria's own teams, roles and members, available as client.teams.
Each endpoint returns its whole list in one response (a few dozen teams and roles, a couple of hundred members); there is no pagination or filtering, so filter in Python.
Source code in src/aniliberty/resources/_base.py
28 29 | |
list
async
¶
list(*, refresh: bool = False) -> builtins.list[Team]
Get all teams.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Team]
|
Every team, ordered by |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for team in await client.teams.list():
print(team.title, team.description)
Source code in src/aniliberty/resources/teams.py
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | |
roles
async
¶
roles(*, refresh: bool = False) -> builtins.list[TeamRole]
Get all roles team members can have.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[TeamRole]
|
Every role, ordered by |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for role in await client.teams.roles():
print(role.title, role.color)
Source code in src/aniliberty/resources/teams.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
users
async
¶
users(
*, refresh: bool = False
) -> builtins.list[TeamMember]
Get all team members, each with their team and roles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[TeamMember]
|
Every member. Not sorted by team: the API interleaves teams, so group by |
list[TeamMember]
|
|
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
members = await client.teams.users()
actors = [m.nickname for m in members if any(r.title == "Актёр" for r in m.roles)]
Source code in src/aniliberty/resources/teams.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
aniliberty.resources.MediaResource ¶
MediaResource(http: HttpClient)
Bases: BaseResource
Front-page promo banners and AniLibria's YouTube videos, available as client.media.
The ads endpoints that live under /media as well (/media/vasts,
/media/manifest.xml, spec tag Реклама.Vasts) are deliberately not covered.
Source code in src/aniliberty/resources/_base.py
28 29 | |
promotions
async
¶
promotions() -> list[Promotion]
Get the promo banners shown on the site's front page.
Returns:
| Type | Description |
|---|---|
list[Promotion]
|
Every current promotion (a handful), in random order — a new order on each |
list[Promotion]
|
call. Paid ads are included; filter them out with |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
promos = [p for p in await client.media.promotions() if not p.is_ad]
Source code in src/aniliberty/resources/media.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
videos
async
¶
videos(
*, limit: int | None = None, refresh: bool = False
) -> list[Video]
Get the latest videos from AniLibria's YouTube playlists, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
How many videos to return, 1 to 50. Omitted, the API returns 10.
0 or a negative number returns all videos (over a thousand, more than
1 MB of JSON); more than 50 is a |
None
|
refresh
|
bool
|
Fetch from the API even if the response is cached (see
|
False
|
Returns:
| Type | Description |
|---|---|
list[Video]
|
The videos, ordered by |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for video in await client.media.videos(limit=5):
print(video.created_at.date() if video.created_at else "?", video.title)
Source code in src/aniliberty/resources/media.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
aniliberty.resources.AuthResource ¶
AuthResource(http: HttpClient)
Bases: _SessionResource
Logging in and out, available as client.auth.
A successful login stores the session token on the client (client.token), so
account methods work right after it. To reuse a session across runs, save
client.token and pass it back as AniLiberty(token=...) — logging in on every run
piles up sessions on the account.
Attributes:
| Name | Type | Description |
|---|---|---|
otp |
Logging a device in with a one-time code. |
|
social |
Logging in with a social network. |
|
password |
Recovering a forgotten password. |
Source code in src/aniliberty/resources/auth.py
51 52 53 54 55 | |
login
async
¶
login(*, login: str, password: str) -> AuthToken
Log in with a login (or e-mail) and password, and use the session from now on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
login
|
str
|
The account's login or e-mail; the login is case-insensitive. |
required |
password
|
str
|
The account's password. |
required |
Returns:
| Type | Description |
|---|---|
AuthToken
|
The new session's token, also stored as |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
Wrong login or password (HTTP 401); the API's reason is in
|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. Not retried: login is a
|
ResponseParseError
|
The response didn't match |
Example
async with AniLiberty() as client:
await client.auth.login(login="me", password=os.environ["ANILIBERTY_PASSWORD"])
print(client.token is not None) # True
Source code in src/aniliberty/resources/auth.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | |
logout
async
¶
logout() -> None
End the client's session and stop sending its token.
Only this session is ended; other sessions of the account (other devices, other
logins) stay valid. client.token is None afterwards, also when the API
rejects the token as already invalid.
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
The client has no token, or the API no longer accepts it (logged out or expired) — the session is over either way (HTTP 403). |
APIError
|
The API answered with another error; the token is kept. |
TransportError
|
The request failed without a response; the token is kept. |
Example
await client.auth.logout()
print(client.token) # None
Source code in src/aniliberty/resources/auth.py
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
aniliberty.resources.OtpResource ¶
OtpResource(http: HttpClient)
Bases: _SessionResource
Logging a device in with a one-time code, available as client.auth.otp.
Meant for devices where typing a password is awkward (a TV app):
- The device calls
issue(device_id=...)and shows the code. - The user enters the code on a device where they are logged in, which calls
accept(code). - The device calls
login(code=..., device_id=...)and gets its own session.
A code lives 5 minutes and logs in once.
Source code in src/aniliberty/resources/_base.py
28 29 | |
issue
async
¶
issue(*, device_id: str) -> IssuedOtp
Issue a one-time code for a device. No session needed.
Asking again for the same device_id while the code is alive returns the same code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device_id
|
str
|
Any string identifying the device, e.g. a UUID it keeps; the same
value must be passed to |
required |
Returns:
| Type | Description |
|---|---|
IssuedOtp
|
The code and the seconds it has left. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
issued = await client.auth.otp.issue(device_id=str(uuid.uuid4()))
print(issued.otp.code, issued.remaining_time)
Source code in src/aniliberty/resources/auth.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | |
accept
async
¶
accept(code: str | int) -> None
Approve a one-time code with the client's session, letting its device log in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
str | int
|
The code the other device shows. Pass it as the string |
required |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
The client has no valid session. |
NotFoundError
|
No live code like that. |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
await client.auth.otp.accept("058701")
Source code in src/aniliberty/resources/auth.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
login
async
¶
login(*, code: str | int, device_id: str) -> AuthToken
Log the device in with an accepted code, and use the session from now on.
Sent without the client's current token, if it has one: the API refuses this request from a logged-in caller (403). The new token replaces the old one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
code
|
str | int
|
The code from |
required |
device_id
|
str
|
The same |
required |
Returns:
| Type | Description |
|---|---|
AuthToken
|
The new session's token, also stored as |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
No live code for this device — wrong code or |
ServerError
|
The code hasn't been accepted yet — the API answers 500 to that. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.auth.otp.login(code=issued.otp.code, device_id=device_id)
Source code in src/aniliberty/resources/auth.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | |
aniliberty.resources.SocialAuthResource ¶
SocialAuthResource(http: HttpClient)
Bases: _SessionResource
Logging in with a social network, available as client.auth.social.
login(provider)returns an OAuthurland astatekey.- The user opens
urlin a browser and logs in with the social network. authenticate(state)exchanges the key for a session.
Source code in src/aniliberty/resources/_base.py
28 29 | |
login
async
¶
login(provider: SocialProvider) -> SocialLogin
Start logging in with a social network.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
provider
|
SocialProvider
|
|
required |
Returns:
| Type | Description |
|---|---|
SocialLogin
|
The OAuth page to send the user to, and the key to finish with. |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
APIError
|
The API answered with an error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
start = await client.auth.social.login("discord")
print("Open in a browser:", start.url)
Source code in src/aniliberty/resources/auth.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 | |
authenticate
async
¶
authenticate(state: str) -> AuthToken
Finish a social login, and use the session from now on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
str
|
|
required |
Returns:
| Type | Description |
|---|---|
AuthToken
|
The new session's token, also stored as |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
Unknown |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.auth.social.authenticate(start.state)
Source code in src/aniliberty/resources/auth.py
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
aniliberty.resources.PasswordResource ¶
PasswordResource(http: HttpClient)
Bases: BaseResource
Recovering a forgotten password, available as client.auth.password.
Source code in src/aniliberty/resources/_base.py
28 29 | |
forget
async
¶
forget(*, email: str) -> None
Ask the API to e-mail a password reset link.
The answer is the same whether or not an account has this e-mail, so it can't be used to check for one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
email
|
str
|
The account's e-mail. |
required |
Raises:
| Type | Description |
|---|---|
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
await client.auth.password.forget(email="me@example.com")
Source code in src/aniliberty/resources/auth.py
290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 | |
reset
async
¶
reset(*, token: str, password: str) -> None
Set a new password with the token from the reset e-mail.
Doesn't log in: call client.auth.login() with the new password afterwards.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The token from the link in the e-mail sent by |
required |
password
|
str
|
The new password (sent as both |
required |
Raises:
| Type | Description |
|---|---|
NotFoundError
|
Unknown or used |
ValidationError
|
The password doesn't meet the API's rules, or a field is empty. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
await client.auth.password.reset(token=token_from_email, password=new_password)
Source code in src/aniliberty/resources/auth.py
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 | |
aniliberty.resources.MeResource ¶
MeResource(http: HttpClient)
Bases: BaseResource
The logged-in user's own account, available as client.me.
Every method needs a session: log in with client.auth.login() or pass
AniLiberty(token=...). Without a valid one the API answers 403, raised as
AuthRequiredError.
Attributes:
| Name | Type | Description |
|---|---|---|
favorites |
The user's favorite releases. |
|
collections |
The user's collections (planned, watched, watching, ...). |
|
views |
The user's watch progress (timecodes) and history. |
Source code in src/aniliberty/resources/me.py
28 29 30 31 32 | |
profile
async
¶
profile() -> UserProfile
Get the logged-in user's profile.
Returns:
| Type | Description |
|---|---|
UserProfile
|
The profile, with the account's e-mail and tracker passkey. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No session token, or the API doesn't accept it (logged out or expired). |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
async with AniLiberty(token=saved_token) as client:
me = await client.me.profile()
print(me.nickname, me.created_at)
Source code in src/aniliberty/resources/me.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
aniliberty.resources.FavoritesResource ¶
FavoritesResource(http: HttpClient)
Bases: BaseResource
The logged-in user's favorite releases, available as client.me.favorites.
Needs a session (client.auth.login() or AniLiberty(token=...)); without a valid
one every method raises AuthRequiredError.
The API checks little here (see docs/api-notes.md): add() accepts IDs of releases
that don't exist — they end up in ids(), though never in releases() — and adding a
release twice or removing one that isn't there is silently a no-op.
Attributes:
| Name | Type | Description |
|---|---|---|
references |
Values the favorites filters accept. |
Source code in src/aniliberty/resources/favorites.py
87 88 89 | |
ids
async
¶
ids() -> list[int]
Get the IDs of all releases in favorites, in the order they were added.
Cheaper than releases() for checking whether a release is a favorite.
Returns:
| Type | Description |
|---|---|
list[int]
|
Release IDs. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response wasn't a list of integers. |
Example
if release.id in await client.me.favorites.ids():
print("already a favorite")
Source code in src/aniliberty/resources/favorites.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
add
async
¶
add(*releases: int | Release) -> list[int]
Add releases to favorites.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*releases
|
int | Release
|
Release IDs or |
()
|
Returns:
| Type | Description |
|---|---|
list[int]
|
IDs of all releases in favorites afterwards, like |
Raises:
| Type | Description |
|---|---|
ValueError
|
No releases given. |
TypeError
|
A value is neither an |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. Not retried: it's a
|
ResponseParseError
|
The response wasn't a list of integers. |
Example
await client.me.favorites.add(9839, release)
Source code in src/aniliberty/resources/favorites.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
remove
async
¶
remove(*releases: int | Release) -> list[int]
Remove releases from favorites.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*releases
|
int | Release
|
Release IDs or |
()
|
Returns:
| Type | Description |
|---|---|
list[int]
|
IDs of the releases left in favorites, like |
Raises:
| Type | Description |
|---|---|
ValueError
|
No releases given. |
TypeError
|
A value is neither an |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response wasn't a list of integers. |
Example
left = await client.me.favorites.remove(9839)
Source code in src/aniliberty/resources/favorites.py
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
releases
async
¶
releases(
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
years: Sequence[int] | None = None,
search: str | None = None,
sorting: FavoritesSorting | SortingOption | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
page: int | None = None,
limit: int | None = None,
) -> Page[Release]
Get one page of favorite releases matching the filters.
The filters combine like the catalog's: a release must have all of genres
and any of the types, years and age_ratings; different filters must
all match. To go through all pages, use iter_releases().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genres
|
Sequence[int | Genre] | None
|
Genre IDs (or |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
Release types, e.g. |
None
|
years
|
Sequence[int] | None
|
Release years; any of them. A range works too: |
None
|
search
|
str | None
|
Text to search in names. |
None
|
sorting
|
FavoritesSorting | SortingOption | None
|
Sort order. Default: |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
Age ratings, e.g. |
None
|
page
|
int | None
|
Page number, starting at 1. A page past the last one is empty. |
None
|
limit
|
int | None
|
Page size, 1 to 50. Default: 25. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Release]
|
One page of releases, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
A filter value isn't one the API knows (it would silently return nothing or ignore the filter instead). |
TypeError
|
A list filter got a single value ( |
AuthRequiredError
|
No valid session. |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.me.favorites.releases(types=["TV"], sorting="YEAR_DESC")
for release in page.items:
print(release.year, release.name.main)
Source code in src/aniliberty/resources/favorites.py
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | |
iter_releases ¶
iter_releases(
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
years: Sequence[int] | None = None,
search: str | None = None,
sorting: FavoritesSorting | SortingOption | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
limit: int | None = None,
) -> AsyncIterator[Release]
Iterate over all favorite releases matching the filters, fetching page by page.
Takes the same filters as releases(); the next page is requested only once the
current one has been consumed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
genres
|
Sequence[int | Genre] | None
|
See |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
See |
None
|
years
|
Sequence[int] | None
|
See |
None
|
search
|
str | None
|
See |
None
|
sorting
|
FavoritesSorting | SortingOption | None
|
See |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
See |
None
|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Release]
|
An async iterator over the releases. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Invalid filter values — raised right away, not when iteration starts. |
TypeError
|
A list filter got a single value or a value of the wrong type. |
Example
async for release in client.me.favorites.iter_releases(limit=50):
print(release.name.main)
Source code in src/aniliberty/resources/favorites.py
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
aniliberty.resources.FavoritesReferencesResource ¶
FavoritesReferencesResource(http: HttpClient)
Bases: _OwnListReferencesResource
Values the favorites filters accept, available as client.me.favorites.references.
Unlike client.catalog.references, the lists only contain what occurs in the user's
own favorites: the genres, types, years and age ratings of the releases added there. With
empty favorites they are empty — except sorting(), which is always the full list.
Source code in src/aniliberty/resources/_base.py
28 29 | |
genres
async
¶
genres() -> list[Genre]
Get the genres of the releases in the list.
Returns:
| Type | Description |
|---|---|
list[Genre]
|
Genres with |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
# The same for client.me.collections.references.
genres = await client.me.favorites.references.genres()
print([genre.name for genre in genres])
Source code in src/aniliberty/resources/_lists.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
types
async
¶
types() -> list[ValueDescription[str]]
Get the release types in the list, e.g. value="TV", description="ТВ".
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The release types. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
types = await client.me.favorites.references.types()
page = await client.me.favorites.releases(types=types)
Source code in src/aniliberty/resources/_lists.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
years
async
¶
years() -> list[int]
Get the release years in the list, newest first.
Returns:
| Type | Description |
|---|---|
list[int]
|
The years. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response wasn't a list of integers. |
Example
years = await client.me.favorites.references.years()
page = await client.me.favorites.releases(years=years[:1]) # the newest year
Source code in src/aniliberty/resources/_lists.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
age_ratings
async
¶
age_ratings() -> list[AgeRating]
Get the age ratings in the list, e.g. value="R16_PLUS", label="16+".
Returns:
| Type | Description |
|---|---|
list[AgeRating]
|
The age ratings ( |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for rating in await client.me.collections.references.age_ratings():
print(rating.value, rating.label)
Source code in src/aniliberty/resources/_lists.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
sorting
async
¶
sorting() -> list[SortingOption]
Get the ways favorites can be sorted, e.g. value="CREATED_AT_DESC".
Returns:
| Type | Description |
|---|---|
list[SortingOption]
|
All sorting options, whatever is in favorites. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for option in await client.me.favorites.references.sorting():
print(option.value, option.label)
Source code in src/aniliberty/resources/favorites.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | |
aniliberty.resources.CollectionsResource ¶
CollectionsResource(http: HttpClient)
Bases: BaseResource
The user's collections, available as client.me.collections.
There are five collections (CollectionType): "PLANNED", "WATCHED",
"WATCHING", "POSTPONED" and "ABANDONED". A release is in at most one of them:
adding it to another collection moves it there. Collections are separate from favorites.
Needs a session (client.auth.login() or AniLiberty(token=...)); without a valid
one every method raises AuthRequiredError. As with favorites, the API accepts IDs of
releases that don't exist, and removing a release that isn't in any collection is a
no-op.
The API also applies collection changes in the background (see docs/api-notes.md):
a release removed right after being added can be back about a minute later, and removing
a release can take minutes to stick. To be sure a change held, check ids() again a
minute later.
Attributes:
| Name | Type | Description |
|---|---|---|
references |
Values the collections filters accept. |
Source code in src/aniliberty/resources/collections.py
73 74 75 | |
ids
async
¶
ids() -> list[CollectionItem]
Get every release in the collections, with the collection it's in.
Returns:
| Type | Description |
|---|---|
list[CollectionItem]
|
One item per release, in the order the releases were added. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
where = {item.release_id: item.collection for item in await client.me.collections.ids()}
print(where.get(9839)) # e.g. "WATCHED", or None
Source code in src/aniliberty/resources/collections.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
add
async
¶
add(
*releases: int | Release, to: CollectionType
) -> list[CollectionItem]
Put releases into a collection, moving them out of the one they were in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*releases
|
int | Release
|
Release IDs or |
()
|
to
|
CollectionType
|
The collection, e.g. |
required |
Returns:
| Type | Description |
|---|---|
list[CollectionItem]
|
Every release in the collections afterwards, like |
Raises:
| Type | Description |
|---|---|
ValueError
|
No releases given, or |
TypeError
|
A release is neither an |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.me.collections.add(9839, release, to="WATCHING")
await client.me.collections.add(9839, to="WATCHED") # moved
Source code in src/aniliberty/resources/collections.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | |
remove
async
¶
remove(*releases: int | Release) -> list[CollectionItem]
Take releases out of whatever collection they are in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*releases
|
int | Release
|
Release IDs or |
()
|
Returns:
| Type | Description |
|---|---|
list[CollectionItem]
|
The releases left in the collections, like |
Raises:
| Type | Description |
|---|---|
ValueError
|
No releases given. |
TypeError
|
A release is neither an |
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
await client.me.collections.remove(9839)
Source code in src/aniliberty/resources/collections.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
releases
async
¶
releases(
collection: CollectionType,
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
years: Sequence[int] | None = None,
search: str | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
page: int | None = None,
limit: int | None = None,
) -> Page[Release]
Get one page of the releases in a collection, matching the filters.
Releases come most recently added first; the API offers no other order. The filters
combine like the favorites' ones: all of genres, any of the types, years
and age_ratings. To go through all pages, use iter_releases().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
CollectionType
|
The collection, e.g. |
required |
genres
|
Sequence[int | Genre] | None
|
Genre IDs (or |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
Release types, e.g. |
None
|
years
|
Sequence[int] | None
|
Release years; any of them. A range works too: |
None
|
search
|
str | None
|
Text to search in names. |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
Age ratings, e.g. |
None
|
page
|
int | None
|
Page number, starting at 1. A page past the last one is empty. |
None
|
limit
|
int | None
|
Page size, 1 to 50. Default: 25. |
None
|
Returns:
| Type | Description |
|---|---|
Page[Release]
|
One page of releases, with |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
TypeError
|
A list filter got a single value or a value of the wrong type. |
AuthRequiredError
|
No valid session. |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.me.collections.releases("WATCHING", types=["TV"])
for release in page.items:
print(release.name.main)
Source code in src/aniliberty/resources/collections.py
155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | |
iter_releases ¶
iter_releases(
collection: CollectionType,
*,
genres: Sequence[int | Genre] | None = None,
types: Sequence[ReleaseType | ValueDescription[str]]
| None = None,
years: Sequence[int] | None = None,
search: str | None = None,
age_ratings: Sequence[
AgeRatingValue | ValueDescription[str]
]
| None = None,
limit: int | None = None,
) -> AsyncIterator[Release]
Iterate over all releases in a collection matching the filters, page by page.
Takes the same arguments as releases(); the next page is requested only once the
current one has been consumed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
collection
|
CollectionType
|
See |
required |
genres
|
Sequence[int | Genre] | None
|
See |
None
|
types
|
Sequence[ReleaseType | ValueDescription[str]] | None
|
See |
None
|
years
|
Sequence[int] | None
|
See |
None
|
search
|
str | None
|
See |
None
|
age_ratings
|
Sequence[AgeRatingValue | ValueDescription[str]] | None
|
See |
None
|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[Release]
|
An async iterator over the releases. |
Raises:
| Type | Description |
|---|---|
ValueError
|
Invalid |
TypeError
|
A list filter got a single value or a value of the wrong type. |
Example
async for release in client.me.collections.iter_releases("WATCHED"):
print(release.name.main)
Source code in src/aniliberty/resources/collections.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 | |
aniliberty.resources.CollectionsReferencesResource ¶
CollectionsReferencesResource(http: HttpClient)
Bases: _OwnListReferencesResource
Values the collections filters accept, available as client.me.collections.references.
The lists cover all the user's collections together — the API has no per-collection
variant (a type_of_collection parameter is ignored). Collections can't be sorted, so
unlike favorites there is no sorting().
Source code in src/aniliberty/resources/_base.py
28 29 | |
genres
async
¶
genres() -> list[Genre]
Get the genres of the releases in the list.
Returns:
| Type | Description |
|---|---|
list[Genre]
|
Genres with |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
# The same for client.me.collections.references.
genres = await client.me.favorites.references.genres()
print([genre.name for genre in genres])
Source code in src/aniliberty/resources/_lists.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
types
async
¶
types() -> list[ValueDescription[str]]
Get the release types in the list, e.g. value="TV", description="ТВ".
Returns:
| Type | Description |
|---|---|
list[ValueDescription[str]]
|
The release types. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
types = await client.me.favorites.references.types()
page = await client.me.favorites.releases(types=types)
Source code in src/aniliberty/resources/_lists.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | |
years
async
¶
years() -> list[int]
Get the release years in the list, newest first.
Returns:
| Type | Description |
|---|---|
list[int]
|
The years. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response wasn't a list of integers. |
Example
years = await client.me.favorites.references.years()
page = await client.me.favorites.releases(years=years[:1]) # the newest year
Source code in src/aniliberty/resources/_lists.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
age_ratings
async
¶
age_ratings() -> list[AgeRating]
Get the age ratings in the list, e.g. value="R16_PLUS", label="16+".
Returns:
| Type | Description |
|---|---|
list[AgeRating]
|
The age ratings ( |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for rating in await client.me.collections.references.age_ratings():
print(rating.value, rating.label)
Source code in src/aniliberty/resources/_lists.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
aniliberty.resources.ViewsResource ¶
ViewsResource(http: HttpClient)
Bases: BaseResource
The user's watch progress and history, available as client.me.views.
Needs a session (client.auth.login() or AniLiberty(token=...)); without a valid
one every method raises AuthRequiredError.
The API stores timecodes in two steps (see docs/api-notes.md):
save_timecodes()shows up intimecodes()at once, but inhistory(),client.episodes.timecode()andclient.releases.timecodes()only about a minute later.remove_timecodes()likewise takes about a minute to reach the history, andtimecodes()can keep listing the deleted entries after that; deleting them once more after the minute clears them.
Example
await client.me.views.save_timecodes(
Timecode(release_episode_id=episode.id, time=743.5, is_watched=False)
)
progress = {t.release_episode_id: t for t in await client.me.views.timecodes()}
Source code in src/aniliberty/resources/_base.py
28 29 | |
timecodes
async
¶
timecodes(
*, since: datetime | None = None
) -> list[Timecode]
Get the user's timecodes — the position in every episode they started.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
since
|
datetime | None
|
Only timecodes saved at or after this moment. Must be timezone-aware
(e.g. |
None
|
Returns:
| Type | Description |
|---|---|
list[Timecode]
|
The timecodes, in no documented order (updating one doesn't move it). |
Raises:
| Type | Description |
|---|---|
ValueError
|
|
AuthRequiredError
|
No valid session. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
for timecode in await client.me.views.timecodes():
print(timecode.release_episode_id, timecode.time, timecode.is_watched)
Source code in src/aniliberty/resources/views.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
save_timecodes
async
¶
save_timecodes(*timecodes: Timecode) -> None
Save the position in one or more episodes, replacing what was saved for them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*timecodes
|
Timecode
|
One |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
No timecodes given. |
TypeError
|
A value isn't a |
AuthRequiredError
|
No valid session. |
ValidationError
|
An episode ID isn't a UUID. An ID of an episode that doesn't exist is accepted, and dropped later by the API. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
await client.me.views.save_timecodes(
Timecode(release_episode_id=episode.id, time=episode.duration, is_watched=True)
)
Source code in src/aniliberty/resources/views.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
remove_timecodes
async
¶
remove_timecodes(
*episodes: str | Episode | Timecode,
) -> None
Delete the saved position in one or more episodes (and their history entries).
Episodes without a timecode are skipped. See the class docs: the deletion takes about
a minute, and timecodes() may need a second call after that.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*episodes
|
str | Episode | Timecode
|
Episode IDs, or |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
No episodes given. |
TypeError
|
A value isn't an episode ID, |
AuthRequiredError
|
No valid session. |
ValidationError
|
An episode ID isn't a UUID. |
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
Example
await client.me.views.remove_timecodes(*await client.me.views.timecodes())
Source code in src/aniliberty/resources/views.py
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
history
async
¶
history(
*, page: int | None = None, limit: int | None = None
) -> Page[HistoryEntry]
Get one page of the watch history — one entry per episode, most recent first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
page
|
int | None
|
Page number, starting at 1. A page past the last one is empty. |
None
|
limit
|
int | None
|
Page size, 1 to 50. Default: 25. |
None
|
Returns:
| Type | Description |
|---|---|
Page[HistoryEntry]
|
One page of entries, each with its episode and the episode's release. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session. |
ValidationError
|
|
APIError
|
The API answered with another error. |
TransportError
|
The request failed without a response. |
ResponseParseError
|
The response didn't match |
Example
page = await client.me.views.history(limit=10)
for entry in page.items:
episode = entry.release_episode
print(episode.release.name.main, episode.ordinal, entry.time)
Source code in src/aniliberty/resources/views.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | |
iter_history ¶
iter_history(
*, limit: int | None = None
) -> AsyncIterator[HistoryEntry]
Iterate over the whole watch history, fetching page by page.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limit
|
int | None
|
Page size, 1 to 50 — affects how many requests are made, not the result. |
None
|
Returns:
| Type | Description |
|---|---|
AsyncIterator[HistoryEntry]
|
An async iterator over the entries, most recent first. |
Raises:
| Type | Description |
|---|---|
AuthRequiredError
|
No valid session — raised once iteration starts. |
ValidationError
|
|
APIError
|
The API answered with another error, while iterating. |
TransportError
|
A request failed without a response, while iterating. |
Example
async for entry in client.me.views.iter_history(limit=50):
print(entry.updated_at, entry.release_episode_id)
Source code in src/aniliberty/resources/views.py
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
Pagination¶
aniliberty.Page ¶
Bases: AniLibertyModel, Generic[T]
One page of results from a paginated endpoint.
Paginated endpoints answer with {"data": [...], "meta": {"pagination": {...}}} (see
docs/api-notes.md); Page flattens that into the fields below. Pages are numbered
from 1. To walk through all pages, use the matching iter_*() method instead of
calling the paginated one in a loop.
Attributes:
| Name | Type | Description |
|---|---|---|
items |
list[T]
|
Items on this page. |
page |
int
|
Number of this page, starting at 1. |
limit |
int
|
Page size that was requested ( |
total |
int
|
Number of items on all pages. |
total_pages |
int
|
Number of pages. |
Example
page = await client.catalog.releases(page=1, limit=25)
print(f"page {page.page} of {page.total_pages}, {page.total} releases in total")
for release in page.items:
print(release.name.main)
Models¶
Releases¶
aniliberty.Release ¶
Bases: AniLibertyModel
An anime release.
Spec schema: models.anime.releases.v1.release plus the extra fields each endpoint adds.
List endpoints fill only part of the extras: genres comes with every endpoint except
Episode.release, background_covers with random() and get(),
members/sponsors with get() and list(), episodes with get(),
list() and Episode.release, latest_episode with latest() only. An extra
the endpoint doesn't send is an empty list or None.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Release ID. |
alias |
str
|
URL slug, e.g. |
name |
ReleaseName
|
Release names. |
type |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Release type; |
year |
int | None
|
Release year. |
season |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Release season; |
poster |
Image | None
|
Release poster. |
description |
str | None
|
Synopsis, in Russian. |
notification |
str | None
|
Important notice shown on the release page, if any. |
age_rating |
Annotated[AgeRating | None, EMPTY_VALUE_AS_NONE]
|
Age rating. |
publish_day |
Annotated[ValueDescription[int] | None, EMPTY_VALUE_AS_NONE]
|
Weekday new episodes come out on; |
is_ongoing |
bool
|
Whether the anime is still airing in its home country. |
is_in_production |
bool
|
Whether AniLibria is still dubbing it. |
is_blocked_by_geo |
bool
|
Whether it's blocked in the requester's region. |
is_blocked_by_copyrights |
bool
|
Whether it's blocked by the copyright holder. |
episodes_total |
int | None
|
Number of episodes, if known. |
average_duration_of_episode |
int | None
|
Average episode length, in minutes, if known. |
external_player |
str | None
|
Link to an external player, if any. |
fresh_at |
datetime | None
|
When the release was last bumped (e.g. by a new episode). |
created_at |
datetime | None
|
When the release was added. |
updated_at |
datetime | None
|
When the release was last edited. |
added_in_users_favorites |
int
|
How many users added it to favorites. |
added_in_planned_collection |
int
|
How many users put it in their "planned" collection. |
added_in_watched_collection |
int
|
How many users put it in "watched". |
added_in_watching_collection |
int
|
How many users put it in "watching". |
added_in_postponed_collection |
int
|
How many users put it in "postponed". |
added_in_abandoned_collection |
int
|
How many users put it in "abandoned". |
shikimori |
ExternalRating | None
|
Entry on Shikimori. |
mal |
ExternalRating | None
|
Entry on MyAnimeList. |
rating |
ReleaseRating | None
|
Score from AniLibria users. |
genres |
list[Genre]
|
Genres of the release. |
background_covers |
list[ImageSet]
|
Wide cover images for a page background. |
members |
list[ReleaseMember]
|
People who worked on the release. |
sponsors |
list[Sponsor]
|
Sponsors shown on the release page. |
episodes |
list[Episode]
|
All episodes of the release, in order. |
latest_episode |
Episode | None
|
The newest episode — only filled in by |
aniliberty.ReleaseName ¶
Bases: AniLibertyModel
Names of a release.
Attributes:
| Name | Type | Description |
|---|---|---|
main |
str
|
The main (Russian) title. |
english |
str | None
|
The English or romanized title. |
alternative |
str | None
|
Other titles, comma-separated, if any. |
aniliberty.AgeRating ¶
Bases: ValueDescription[str]
Age rating of a release, e.g. value="R16_PLUS", label="16+".
value is one of R0_PLUS, R6_PLUS, R12_PLUS, R16_PLUS, R18_PLUS
(spec enum enums.anime.releases.release.ageRating).
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str | None
|
Short label, e.g. |
is_adult |
bool | None
|
Whether the release is for adults only. |
aniliberty.ReleaseRating ¶
Bases: AniLibertyModel
Score given to a release by AniLibria users.
Attributes:
| Name | Type | Description |
|---|---|---|
average |
float | None
|
Average score from 1 to 10, |
votes |
int
|
Number of people who rated it. |
distribution |
dict[int, int]
|
Number of votes per score, for every score from 1 to 10. |
aniliberty.UserRating ¶
Bases: AniLibertyModel
The logged-in user's own score for a release, from client.releases.ratings.
Spec schema: models.anime.releases.v1.release.rating.own.
Attributes:
| Name | Type | Description |
|---|---|---|
release_id |
int
|
Release ID. |
score |
int | None
|
The user's score, 1 to 10; |
aniliberty.ExternalRating ¶
Bases: AniLibertyModel
A release's entry and score on an external site (Shikimori or MyAnimeList).
All fields are None when the release isn't linked to that site.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int | None
|
ID of the anime on the external site. |
url |
str | None
|
Link to the anime's page there. |
rating |
float | None
|
Average score there, from 1 to 10. |
votes |
int | None
|
Number of people who rated it there. |
aniliberty.ReleaseMember ¶
Bases: AniLibertyModel
A person who worked on a release (voice actor, translator, timer, ...).
Spec schema: models.anime.releases.v1.release.member.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Member entry ID (a UUID string). |
nickname |
str
|
The member's nickname. |
role |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
What they did. |
user |
LinkedAccount | None
|
Linked site account, if any (usually |
aniliberty.Sponsor ¶
Bases: AniLibertyModel
A sponsor shown on a release page.
Spec schema: models.anime.sponsors.v1.sponsor.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Sponsor ID. |
title |
str | None
|
Sponsor name. |
description |
str | None
|
Sponsor description. |
url |
str | None
|
Sponsor link. |
url_title |
str | None
|
Text of the sponsor link. |
aniliberty.Genre ¶
Bases: AniLibertyModel
An anime genre.
Spec schema: models.anime.genres.v1.genre.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Genre ID — the value to pass to catalog genre filters. |
name |
str
|
Genre name, in Russian (e.g. |
image |
Image | None
|
Genre cover image. |
total_releases |
int | None
|
Number of releases with this genre. |
Episodes¶
aniliberty.Episode ¶
Bases: AniLibertyModel
An episode of a release, with links to its HLS streams.
Spec schema: models.anime.releases.v1.release.episode. HLS links are absolute URLs of
.m3u8 playlists on the video CDN, returned exactly as the API sends them — their query
string (country, ads flags, ...) is part of the link. Downloading the video itself is out
of scope for the SDK: hand the playlist to a player or to ffmpeg.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Episode ID (a UUID string) — pass it to |
release_id |
int
|
ID of the release the episode belongs to. |
ordinal |
int | float
|
Episode number. Usually an |
sort_order |
int
|
Position of the episode in the release's list. |
name |
str | None
|
Episode title, in Russian, if it has one (most don't). |
name_english |
str | None
|
Episode title in English, if any. |
duration |
int
|
Length, in seconds. |
opening |
Annotated[SkipRange | None, BeforeValidator(_empty_skip_as_none)]
|
Where the opening is, |
ending |
Annotated[SkipRange | None, BeforeValidator(_empty_skip_as_none)]
|
Where the ending is, |
preview |
Image | None
|
Still image from the episode. Often present with all paths |
hls_480 |
str | None
|
HLS playlist, 480p. |
hls_720 |
str | None
|
HLS playlist, 720p. |
hls_1080 |
str | None
|
HLS playlist, 1080p — missing on some episodes. |
rutube_id |
str | None
|
ID of the episode on Rutube, if it's published there. |
youtube_id |
str | None
|
ID of the episode on YouTube, if it's published there. |
updated_at |
datetime | None
|
When the episode was last updated. |
release |
Release | None
|
The release with all its episodes — only filled in by
|
Example
episode = await client.episodes.get(release.episodes[0].id)
playlist = episode.hls_1080 or episode.hls_720 or episode.hls_480
aniliberty.SkipRange ¶
Bases: AniLibertyModel
Where an opening or ending is, so a player can offer to skip it.
The spec describes the numbers as "seconds from the start or the end of the episode";
in every live sample checked they were seconds from the start (an ending of a
1502-second episode at 1326–1502), see docs/api-notes.md.
Attributes:
| Name | Type | Description |
|---|---|---|
start |
int | None
|
Where the segment starts, in seconds. |
stop |
int | None
|
Where the segment ends, in seconds. |
Franchises¶
aniliberty.Franchise ¶
Bases: AniLibertyModel
A franchise: a group of related releases (seasons, movies, spin-offs).
Spec schema: models.anime.franchises.v1.franchise. The totals are precomputed by the
API and can be missing or stale — two franchises with total_releases=None still had
two releases each — so count franchise_releases when the exact number matters.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Franchise ID (a UUID string). |
name |
str
|
Name in Russian. |
name_english |
str | None
|
Name in English or romanized. |
image |
Image | None
|
Franchise cover image (its paths can be |
rating |
float | None
|
Franchise rating — |
first_year |
int | None
|
Year of the first release. |
last_year |
int | None
|
Year of the latest release. |
total_releases |
int | None
|
Number of releases. |
total_episodes |
int | None
|
Number of episodes across all releases. |
total_duration |
str | None
|
Total watch time as human-readable Russian text, e.g. |
total_duration_in_seconds |
int | None
|
Total watch time, in seconds. |
franchise_releases |
list[FranchiseRelease]
|
The releases, ordered by |
Example
franchise = await client.franchises.get(franchise_id)
for item in franchise.franchise_releases:
print(item.sort_order, item.release.year, item.release.name.main)
aniliberty.FranchiseRelease ¶
Bases: AniLibertyModel
One release of a franchise, with its place in the franchise.
Spec schema: models.anime.franchises.v1.franchise.release.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
ID of this release-to-franchise link (a UUID string). |
franchise_id |
str
|
ID of the franchise. |
release_id |
int
|
ID of the release. |
sort_order |
int
|
Position of the release in the franchise, starting at 1 — in every franchise checked, the chronological order. |
release |
Release
|
The release itself (without |
Torrents¶
aniliberty.Torrent ¶
Bases: AniLibertyModel
A torrent of a release.
Spec schema: models.anime.torrents.v1.torrent.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Torrent ID. |
hash |
str
|
Info hash (40 hex characters); |
label |
str
|
Full title, e.g. |
filename |
str
|
Name of the |
description |
str
|
Which episodes it contains, e.g. |
magnet |
str
|
Magnet link. |
size |
int
|
Total size of the files the torrent downloads, in bytes (not the size of the
|
type |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Source, e.g. |
quality |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Resolution, e.g. |
codec |
Annotated[TorrentCodec | None, EMPTY_VALUE_AS_NONE]
|
Video codec. |
color |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Color depth, |
bitrate |
int | None
|
Bitrate in kbit/s — usually |
is_hardsub |
bool
|
Whether the subtitles are burned into the video. |
seeders |
int
|
Number of seeders. |
leechers |
int
|
Number of leechers. |
completed_times |
int
|
How many times it was downloaded completely. |
sort_order |
int
|
Position among the release's torrents. |
created_at |
datetime | None
|
When the torrent was added. |
updated_at |
datetime | None
|
When the torrent was last updated. |
release |
Release | None
|
The release (without |
torrent_members |
list[TorrentMember]
|
People credited on the torrent; often empty. |
aniliberty.TorrentCodec ¶
Bases: ValueDescription[str]
Video codec of a torrent, e.g. value="x265/HEVC", label="HEVC".
value is one of AV1, x264/AVC, x265/HEVC, x265hq/HEVC-HQ (spec enum
enums.anime.torrents.torrent.codec).
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str | None
|
Short label the site shows, e.g. |
label_color |
str | None
|
Color of that label as |
label_is_visible |
bool | None
|
Whether the site shows the label. |
aniliberty.TorrentMember ¶
Bases: AniLibertyModel
A person credited on a torrent (e.g. who encoded the HEVC version).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Member entry ID (a UUID string). |
nickname |
str
|
The member's nickname. |
role |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
What they did; |
external_url |
str | None
|
The member's own link (e.g. a Telegram channel), if any. |
user |
LinkedAccount | None
|
Linked site account, if any. |
aniliberty.TorrentFile ¶
Bases: AniLibertyModel
A downloaded .torrent file, from client.torrents.file().
Attributes:
| Name | Type | Description |
|---|---|---|
filename |
str | None
|
The file name the API suggests (from |
content |
bytes
|
The file's bytes. |
Example
torrent_file = await client.torrents.file(torrent)
Path(torrent_file.filename or "release.torrent").write_bytes(torrent_file.content)
Teams¶
aniliberty.Team ¶
Bases: AniLibertyModel
An AniLibria team — one of the "leagues" the project's members are grouped into.
Spec schema: models.teams.v1.team.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Team ID (a UUID string). |
title |
str
|
Team name in Russian, e.g. |
description |
str | None
|
Short description in Russian, e.g. |
sort_order |
int
|
Position of the team in the list, from 0 (project leadership) up. |
aniliberty.TeamRole ¶
Bases: AniLibertyModel
A role a team member can have: voice actor, translator, timer, ...
Spec schema: models.teams.v1.team.role.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Role ID (a UUID string). |
title |
str
|
Role name in Russian, e.g. |
color |
str | None
|
The role's display color as a CSS hex string, e.g. |
sort_order |
int
|
Position of the role in the list. |
aniliberty.TeamMember ¶
Bases: AniLibertyModel
A member of an AniLibria team, with the team and their roles.
Spec schema: items of responses.api.v1.teams.users (models.teams.v1.team.user
with team, user and roles added).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Member ID (a UUID string). |
nickname |
str
|
The member's nickname in the team — the same name that appears in a
release's |
is_intern |
bool
|
Whether the member is an intern. |
is_vacation |
bool
|
Whether the member is on vacation. |
sort_order |
int
|
Position of the member within their team, from 1. |
team |
Team
|
The team the member belongs to. |
roles |
list[TeamRole]
|
The member's roles (at least one for every member at the time of writing). |
user |
LinkedAccount | None
|
The linked site account — |
Example
for member in await client.teams.users():
print(member.nickname, member.team.title, [role.title for role in member.roles])
Media¶
aniliberty.Promotion ¶
Bases: AniLibertyModel
A promo banner from the site's front page: a featured release, a news post or an ad.
Spec schema: models.media.promotions.v1.promotion. What a promotion links to
depends on its kind: a featured release has release and no url; a news post or
an ad has url (and usually url_label) and no release.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Promotion ID (a UUID string). |
title |
str | None
|
Banner title, e.g. the release name — can be |
description |
str | None
|
Banner text — can be |
image |
Image | None
|
Banner image. |
url |
str | None
|
External link the banner points to (absolute), or |
url_label |
str | None
|
Text for the link button, e.g. |
release |
Release | None
|
The featured release (with |
is_ad |
bool
|
Whether this is a paid ad rather than the site's own promo. |
has_overlay |
bool
|
Whether the site darkens the image under the text. |
ad_erid |
str | None
|
Ad registry token (ERID) of an ad — |
ad_origin |
str | None
|
Advertiser of an ad — |
Example
for promo in await client.media.promotions():
if promo.release:
print("Featured:", promo.release.name.main)
aniliberty.Video ¶
Bases: AniLibertyModel
A video from one of AniLibria's YouTube playlists: trailers, reviews, announcements.
Spec schema: items of responses.v1.media.videos (models.media.videos.v1.video.content
with origin added).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Video ID on the site. |
title |
str
|
Video title. |
url |
str
|
Link to watch the video ( |
video_id |
str
|
The video's YouTube ID. |
image |
Image | None
|
Preview image (its paths can be |
views |
int | None
|
View count, as last synced from YouTube — can be |
comments |
int | None
|
Comment count, as last synced from YouTube — can be |
is_announce |
bool
|
Whether the video is a season announcement. |
origin |
VideoOrigin | None
|
The playlist the video comes from. |
created_at |
datetime | None
|
When the video was published. |
updated_at |
datetime | None
|
When the site last synced the video's data. |
aniliberty.VideoOrigin ¶
Bases: AniLibertyModel
Where a video comes from: a YouTube playlist the site tracks.
Spec schema: models.media.videos.v1.video.origin.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
Origin ID (a UUID string). |
title |
str | None
|
Playlist or channel name, e.g. |
description |
str | None
|
Playlist description in Russian. |
url |
str | None
|
Playlist URL. |
type |
Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE]
|
Source type. |
is_announce |
bool
|
Whether the playlist holds season announcements. |
Schedule¶
aniliberty.ScheduledRelease ¶
Bases: AniLibertyModel
A release in the airing schedule, with where it stands this week.
Spec schema: models.anime.schedule.v1.releaseInSchedule. The weekday a release comes
out on is release.publish_day (value 1 = Monday ... 7 = Sunday).
Attributes:
| Name | Type | Description |
|---|---|---|
release |
Release
|
The release, with |
published_release_episode |
Episode | None
|
The episode that came out during the current week
(Monday to Sunday), or |
next_release_episode_number |
int | None
|
Number of the next episode to come out, |
full_season_is_released |
bool
|
Whether all episodes of the season are already out. |
Example
for item in await client.schedule.week():
day = item.release.publish_day.description if item.release.publish_day else "?"
print(day, item.release.name.main, item.next_release_episode_number)
aniliberty.ScheduleNow ¶
Bases: AniLibertyModel
Releases scheduled for yesterday, today and tomorrow.
Spec schema: responses.v1.anime.schedule.now. Each list holds the releases whose
publish_day is that day of the week.
Attributes:
| Name | Type | Description |
|---|---|---|
yesterday |
list[ScheduledRelease]
|
Releases that came out (or were due) yesterday. |
today |
list[ScheduledRelease]
|
Releases due today. |
tomorrow |
list[ScheduledRelease]
|
Releases due tomorrow. |
Catalog¶
aniliberty.SortingOption ¶
Bases: ValueDescription[str]
A way to sort a list, from client.catalog.references.sorting() or
client.me.favorites.references.sorting().
value is what the matching sorting filter takes, e.g. "FRESH_AT_DESC".
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str | None
|
Short name, in Russian, e.g. |
Catalog models (spec tags Аниме.Каталог and Аниме.Каталог.Справочники).
ReleaseType
module-attribute
¶
ReleaseType = Literal[
"TV",
"ONA",
"WEB",
"OVA",
"OAD",
"MOVIE",
"DORAMA",
"SPECIAL",
]
Release type. Spec: enums.anime.releases.release.type.
Season
module-attribute
¶
Season = Literal['winter', 'spring', 'summer', 'autumn']
Release season. Spec: enums.anime.releases.release.season.
AgeRatingValue
module-attribute
¶
AgeRatingValue = Literal[
"R0_PLUS", "R6_PLUS", "R12_PLUS", "R16_PLUS", "R18_PLUS"
]
Age rating. Spec: enums.anime.releases.release.ageRating.
PublishStatus
module-attribute
¶
PublishStatus = Literal['IS_ONGOING', 'IS_NOT_ONGOING']
Whether the anime is still airing. Spec: enums.anime.catalog.filter.publishStatus.
ProductionStatus
module-attribute
¶
ProductionStatus = Literal[
"IS_IN_PRODUCTION", "IS_NOT_IN_PRODUCTION"
]
Whether AniLibria is still dubbing it. Spec: enums.anime.catalog.filter.productionStatus.
CatalogSorting
module-attribute
¶
CatalogSorting = Literal[
"FRESH_AT_DESC",
"FRESH_AT_ASC",
"RATING_DESC",
"RATING_ASC",
"YEAR_DESC",
"YEAR_ASC",
]
Catalog sort order. Spec: enums.anime.catalog.filter.sorting.
Common¶
aniliberty.ValueDescription ¶
Bases: AniLibertyModel, Generic[T]
A machine value with its human-readable (Russian) description.
The API returns many enumerations this way — a release's type, season, publish day, a
torrent's quality, ... — e.g. {"value": "TV", "description": "ТВ"} or
{"value": 5, "description": "Пятница"} (publish_day uses weekday numbers). Fields
with extra keys next to these two (age_rating's label/is_adult, a torrent
codec's label_*) use subclasses.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
T
|
The machine value — use it for comparisons and catalog filters. |
description |
str | None
|
The value's description in Russian, for display. |
Example
if release.type and release.type.value == "MOVIE":
print(release.type.description) # "Фильм"
aniliberty.Image ¶
Bases: ImageSet
An image in its original format plus optimized (WebP) variants.
Spec schema: commons.v1.models.components.image.withOptimized — used for release
posters, episode previews, genre and franchise images, user avatars.
Attributes:
| Name | Type | Description |
|---|---|---|
optimized |
ImageSet | None
|
The same image in WebP, usually smaller. |
Example
poster = release.poster
print(poster.preview_url) # https://aniliberty.top/storage/....jpg
print(poster.optimized.preview_url) # https://aniliberty.top/storage/....webp
aniliberty.ImageSet ¶
Bases: AniLibertyModel
Links to one image in several sizes.
The API sends relative paths such as /storage/releases/posters/1/abc.jpg; they
resolve against the site's origin, not the /api/v1 base URL (see
docs/api-notes.md). The raw paths are kept in src/preview/thumbnail, and
the *_url properties return absolute URLs on the host the client talked to (or on
https://aniliberty.top for a model built by hand).
Any of the paths can be None — e.g. a user without an avatar gets an image object
whose paths are all None.
Attributes:
| Name | Type | Description |
|---|---|---|
src |
str | None
|
Original image path. Only posters and episode previews have it. |
preview |
str | None
|
Path of the medium-size image. |
thumbnail |
str | None
|
Path of the small image. |
aniliberty.LinkedAccount ¶
Bases: AniLibertyModel
The site account linked to a person in the credits or in a team.
The user of ReleaseMember, TorrentMember and TeamMember — set when the
person has an account on the site, None otherwise. Spec schemas:
models.teams.v1.team.user.account and the user of release/torrent members.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
User ID on the site. |
nickname |
str | None
|
The account's nickname. Only team members' accounts have it (it can differ
from the team nickname); for release and torrent members it's |
avatar |
Image | None
|
The account's avatar (its paths can be |
Building blocks shared by many models: value + description pairs and images.
EMPTY_VALUE_AS_NONE
module-attribute
¶
EMPTY_VALUE_AS_NONE = BeforeValidator(_empty_value_as_none)
Turns {"value": null, ...} into None for an optional ValueDescription field.
The API sometimes sends a value + description object with both keys null — seen on
season of releases in the schedule — instead of omitting it. Annotate such fields as::
season: Annotated[ValueDescription[str] | None, EMPTY_VALUE_AS_NONE] = None
Service¶
aniliberty.ApiStatus ¶
Bases: AniLibertyModel
Status of the API, returned by client.app.status().
Spec schema: responses.v1.app.status.
Attributes:
| Name | Type | Description |
|---|---|---|
is_alive |
bool
|
Whether the API reports itself as working. |
available_api_endpoints |
list[str]
|
Hosts the API can be reached at, e.g.
|
request |
RequestInfo | None
|
What the API knows about the caller, if it sent that. |
aniliberty.RequestInfo ¶
Bases: AniLibertyModel
What the API knows about the client that sent the request (geolocated by IP).
Spec schema: responses.v1.app.status → request. All fields are optional: the spec
marks none of them required, and geolocation can fail.
Attributes:
| Name | Type | Description |
|---|---|---|
ip |
str | None
|
The client's IP address as seen by the API. |
country |
str | None
|
Country name, in English (e.g. |
iso_code |
str | None
|
ISO 3166-1 alpha-2 country code (e.g. |
timezone |
str | None
|
IANA time zone name (e.g. |
Authentication¶
aniliberty.AuthToken ¶
Bases: AniLibertyModel
A session token, returned by the login methods of client.auth.
Spec schemas: responses.v1.accounts.users.auth.login,
responses.api.v1.accounts.otp.login,
responses.v1.accounts.users.auth.social.authenticate.
The token is an opaque string (32 characters at the time of writing) with no expiry
inside; the API has no refresh endpoint. Every login creates a new, independent session,
and client.auth.logout() ends only the client's own.
Attributes:
| Name | Type | Description |
|---|---|---|
token |
str
|
The session token. Keep it secret — it gives full access to the account. |
aniliberty.IssuedOtp ¶
Bases: AniLibertyModel
A one-time code issued by client.auth.otp.issue().
Spec schema: responses.api.v1.accounts.otp.get.
Attributes:
| Name | Type | Description |
|---|---|---|
otp |
Otp
|
The code. |
remaining_time |
float
|
Seconds until the code expires (fractional; about 300 for a new code). |
aniliberty.Otp ¶
Bases: AniLibertyModel
A one-time code for logging in a device without typing the password.
Spec schema: models.accounts.otp.v1.otp.
Attributes:
| Name | Type | Description |
|---|---|---|
code |
str
|
The code to show to the user, as a string — it can start with |
device_id |
str
|
The device the code was issued for. |
user_id |
int | None
|
The account that accepted the code; |
expired_at |
datetime
|
When the code expires. |
aniliberty.SocialLogin ¶
Bases: AniLibertyModel
Where to send the user to log in with a social network.
Spec schema: responses.v1.accounts.users.auth.social.login.
Attributes:
| Name | Type | Description |
|---|---|---|
url |
str
|
The OAuth page of the social network, to open in a browser. |
state |
str
|
Key to pass to |
Authentication models (spec tags Аккаунты.Пользователи.Авторизация.* and
Аккаунты.ОдноразовыеПароли).
SocialProvider
module-attribute
¶
SocialProvider = Literal[
"vk", "google", "patreon", "discord"
]
Social networks that can be used to log in.
Account¶
aniliberty.UserProfile ¶
Bases: AniLibertyModel
The logged-in user's own profile, returned by client.me.profile().
Spec schema: models.users.v1.user. email and torrents.passkey are only sent
for the user's own profile.
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
User ID. |
login |
str
|
Login, used to log in (together with |
email |
str | None
|
E-mail of the account. |
nickname |
str | None
|
Display name; defaults to the login for a new account. |
avatar |
Image | None
|
Avatar; all paths are |
torrents |
TorrentStats | None
|
Tracker passkey and traffic. |
is_banned |
bool | None
|
Whether the user is banned on the site. |
created_at |
datetime | None
|
When the account was created. |
is_with_ads |
bool | None
|
Whether ads are shown to the user — in the spec, but not sent by the API at the time of writing (see the three flags below). |
is_with_video_ads |
bool | None
|
Whether video ads are shown (not in the spec). |
is_with_content_ads |
bool | None
|
Whether ads are shown among the site's content (not in the spec). |
is_with_video_ads_always |
bool | None
|
An ad flag the API doesn't document (not in the spec); it
matches the |
aniliberty.TorrentStats ¶
Bases: AniLibertyModel
The account's torrent tracker data.
Spec schema: models.users.v1.user → torrents.
Attributes:
| Name | Type | Description |
|---|---|---|
passkey |
str | None
|
The account's tracker passkey — the |
uploaded |
int | None
|
Total uploaded, in bytes; |
downloaded |
int | None
|
Total downloaded, in bytes; |
aniliberty.CollectionItem ¶
Bases: AniLibertyModel
A release in one of the user's collections, from client.me.collections.ids().
Spec schema: responses.api.v1.accounts.users.me.collections.ids.item — the API sends
a [release_id, collection] pair, e.g. [9839, "WATCHED"]; it's validated into this
model (a dict with the field names works too).
Attributes:
| Name | Type | Description |
|---|---|---|
release_id |
int
|
Release ID. |
collection |
CollectionType
|
The collection the release is in. A release is in one collection at most. |
User account models (spec tags Аккаунты.Пользователи.Мое.*).
FavoritesSorting
module-attribute
¶
FavoritesSorting = Literal[
"CREATED_AT_DESC",
"CREATED_AT_ASC",
"FRESH_AT_DESC",
"FRESH_AT_ASC",
"RATING_DESC",
"RATING_ASC",
"YEAR_DESC",
"YEAR_ASC",
]
Sort order of favorites; CREATED_AT_* is when the release was added to favorites.
CollectionType
module-attribute
¶
CollectionType = Literal[
"PLANNED",
"WATCHED",
"WATCHING",
"POSTPONED",
"ABANDONED",
]
A user's collection: planned, watched, watching, postponed or abandoned.
Watch progress¶
aniliberty.Timecode ¶
Bases: AniLibertyModel
Where the user stopped in an episode, and whether they finished it.
What client.me.views.save_timecodes() takes and client.me.views.timecodes()
returns. Spec schema: responses.v1.accounts.users.me.views.timecodes.item — the API
sends an [episode_id, time, is_watched] triple, which is validated into this model (a
dict with the field names works too).
Attributes:
| Name | Type | Description |
|---|---|---|
release_episode_id |
str
|
Episode ID (a UUID string), as in |
time |
float
|
Position in the episode, in seconds from its start (fractions allowed, not negative). |
is_watched |
bool
|
Whether the episode counts as watched. |
Example
Timecode(release_episode_id=episode.id, time=743.5, is_watched=False)
aniliberty.EpisodeView ¶
Bases: AniLibertyModel
The stored watch record of one episode.
Returned by client.episodes.timecode() and client.releases.timecodes(). Spec
schema: models.accounts.users.v1.user.view. Records are written by the API about a
minute after client.me.views.save_timecodes() (see docs/api-notes.md).
Attributes:
| Name | Type | Description |
|---|---|---|
id |
int
|
Record ID. |
release_episode_id |
str
|
Episode ID (a UUID string). |
time |
float
|
Position in the episode, in seconds from its start. |
is_watched |
bool
|
Whether the episode counts as watched. |
updated_at |
datetime | None
|
When the record was last saved. |
user_id |
int | None
|
The user the record belongs to. |
aniliberty.HistoryEntry ¶
Bases: EpisodeView
An entry of the watch history, from client.me.views.history().
Spec schema: responses.v1.accounts.users.me.views.history → data[].
Attributes:
| Name | Type | Description |
|---|---|---|
release_episode |
Episode | None
|
The episode, with its release in |
Exceptions¶
aniliberty.AniLibertyError ¶
Bases: Exception
Base class for all errors raised by the SDK.
aniliberty.APIError ¶
APIError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
)
Bases: AniLibertyError
The API responded with an error status.
Raised as is for statuses without a dedicated subclass (e.g. 400, 405, 409); otherwise one
of the subclasses below is raised, so except APIError catches all of them.
Attributes:
| Name | Type | Description |
|---|---|---|
status_code |
HTTP status code of the response. |
|
message |
Error message — the |
|
method |
HTTP method of the failed request. |
|
url |
Full URL of the failed request. |
|
payload |
The decoded JSON body of the response, or its text if it wasn't JSON. |
Source code in src/aniliberty/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
aniliberty.NotFoundError ¶
NotFoundError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
)
Bases: APIError
The requested resource doesn't exist (404).
The API's own message for this status is an internal Laravel lookup string such as
"No query results for model [...]" — it's kept in payload, while message is a
plain "Not found".
Source code in src/aniliberty/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
aniliberty.ValidationError ¶
ValidationError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
errors: dict[str, list[str]] | None = None,
)
Bases: APIError
The API rejected the request parameters (422).
Not to be confused with pydantic.ValidationError: this one means the request was
invalid, e.g. limit above the endpoint's maximum. A response that doesn't fit the SDK's
models raises ResponseParseError instead.
Attributes:
| Name | Type | Description |
|---|---|---|
errors |
dict[str, list[str]]
|
Messages per parameter, as the API returns them (in Russian), e.g.
|
Source code in src/aniliberty/exceptions.py
81 82 83 84 85 86 87 88 89 90 91 92 | |
aniliberty.RateLimitError ¶
RateLimitError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
retry_after: float | None = None,
)
Bases: APIError
The API answered 429 Too Many Requests.
Attributes:
| Name | Type | Description |
|---|---|---|
retry_after |
The response's |
Source code in src/aniliberty/exceptions.py
102 103 104 105 106 107 108 109 110 111 112 113 | |
aniliberty.AuthRequiredError ¶
AuthRequiredError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
)
Bases: APIError
The request needs a valid session, or the credentials were wrong (401/403 JSON).
The API answers 403 "This action is unauthorized." to an account endpoint called
without a session token, with a malformed one, or with one that was logged out or has
expired — it doesn't say which. Log in again (client.auth.login()) or pass a fresh
token to AniLiberty. Wrong credentials at client.auth.login() are a 401; the
API's reason is in message.
Source code in src/aniliberty/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
aniliberty.ServerError ¶
ServerError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
)
Bases: APIError
The API (or the CDN in front of it) failed with a 5xx status.
Source code in src/aniliberty/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
aniliberty.AccessBlockedError ¶
AccessBlockedError(
message: str,
*,
status_code: int,
method: str,
url: str,
payload: Any = None,
)
Bases: APIError
Something in front of the API answered with an HTML page instead of JSON.
The SDK always sends Accept: application/json, and with that header the API itself
answers with JSON even for errors (see docs/api-notes.md). An HTML page therefore
comes from the Cloudflare edge in front of the API — a bot challenge or a block of the
network/IP. Resending the same request gets the same answer, so this is not retried.
HTML on a 5xx status is not this error but ServerError: Cloudflare renders its own
origin errors (502, 520, ...) as HTML pages too, and those are worth retrying.
Source code in src/aniliberty/exceptions.py
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
aniliberty.TransportError ¶
TransportError(message: str, *, method: str, url: str)
Bases: AniLibertyError
The request failed before any response arrived (connection error, timeout, ...).
The underlying httpx exception is available as __cause__.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
HTTP method of the failed request. |
|
url |
Full URL of the failed request. |
Source code in src/aniliberty/exceptions.py
154 155 156 157 | |
aniliberty.ResponseParseError ¶
ResponseParseError(
message: str, *, url: str, payload: Any = None
)
Bases: AniLibertyError
A successful response couldn't be decoded or didn't match the SDK's model.
Usually means the API changed its response format. The raw data is kept so it isn't
lost: payload holds the decoded JSON (or the body text if it wasn't JSON), and the
original json/pydantic exception is available as __cause__.
Attributes:
| Name | Type | Description |
|---|---|---|
url |
Full URL of the request. |
|
payload |
Decoded JSON body, or the body text if it wasn't valid JSON. |
Source code in src/aniliberty/exceptions.py
172 173 174 175 | |
Low-level HTTP client¶
Available as client.http; client.http.request(...) is the escape hatch for endpoints the
SDK doesn't cover yet.
aniliberty.HttpClient ¶
HttpClient(
*,
base_url: str = DEFAULT_BASE_URL,
timeout: float = DEFAULT_TIMEOUT,
headers: Mapping[str, str] | None = None,
http_client: AsyncClient | None = None,
token: str | None = None,
max_concurrency: int = DEFAULT_MAX_CONCURRENCY,
request_delay: float = DEFAULT_REQUEST_DELAY,
max_retries: int = DEFAULT_MAX_RETRIES,
retry_base_delay: float = DEFAULT_RETRY_BASE_DELAY,
max_retry_after: float = DEFAULT_MAX_RETRY_AFTER,
cache_dir: str | PathLike[str] | None = None,
cache_ttl: float | None = None,
sleep: Callable[
[float], Awaitable[None]
] = asyncio.sleep,
clock: Callable[[], float] = time.monotonic,
cache_clock: Callable[[], float] = time.time,
)
Sends requests to the API and turns responses into JSON or SDK exceptions.
Every resource of AniLiberty goes through this class; it is also the escape hatch for
endpoints the SDK doesn't cover yet: await client.http.request("GET", "/teams/roles")
returns the decoded JSON as is, with the same headers, limits, retries and error handling
as the rest of the SDK.
Request paths are relative to base_url and joined as strings, so
"/app/status" with the default base URL becomes
https://aniliberty.top/api/v1/app/status. Absolute URLs are not accepted.
Being polite to the API is built in:
- Concurrency — at most
max_concurrencyrequests are in flight at once, however many coroutines call the client (e.g. viaasyncio.gather). - Pacing — consecutive requests start at least
request_delayseconds apart. - Retries — 429 is retried for any method, 5xx and network errors/timeouts only for
idempotent methods (
IDEMPOTENT_METHODS). The delay is the response'sRetry-Afterwhen it has one, otherwiseretry_base_delaydoubled per attempt (1 s, 2 s, 4 s by default). Other 4xx and HTML block pages are never retried.
With cache_dir set, request(..., cache=True) keeps the decoded JSON of successful
GET responses on disk and answers repeated identical requests from there until
cache_ttl passes — see request() for what is and isn't cached.
Initialize the client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
base_url
|
str
|
API base URL including the version prefix, e.g.
|
DEFAULT_BASE_URL
|
timeout
|
float
|
Per-request timeout, in seconds. Ignored when |
DEFAULT_TIMEOUT
|
headers
|
Mapping[str, str] | None
|
Extra headers merged over |
None
|
http_client
|
AsyncClient | None
|
A ready |
None
|
token
|
str | None
|
Session token sent as |
None
|
max_concurrency
|
int
|
Maximum number of requests in flight at once. |
DEFAULT_MAX_CONCURRENCY
|
request_delay
|
float
|
Minimum time, in seconds, between the starts of two consecutive
requests. |
DEFAULT_REQUEST_DELAY
|
max_retries
|
int
|
How many times to retry a retryable failure before raising. |
DEFAULT_MAX_RETRIES
|
retry_base_delay
|
float
|
Delay before the first retry, in seconds, when the response
has no |
DEFAULT_RETRY_BASE_DELAY
|
max_retry_after
|
float
|
Longest |
DEFAULT_MAX_RETRY_AFTER
|
cache_dir
|
str | PathLike[str] | None
|
Directory for the response cache, created on the first write.
|
None
|
cache_ttl
|
float | None
|
Seconds after which a cached response is stale and fetched again.
|
None
|
sleep
|
Callable[[float], Awaitable[None]]
|
Async sleep function used for pacing and backoff. Replaceable in tests. |
sleep
|
clock
|
Callable[[], float]
|
Monotonic time source used for pacing. Replaceable in tests. |
monotonic
|
cache_clock
|
Callable[[], float]
|
Wall-clock time source used to stamp and expire cache entries. Replaceable in tests. |
time
|
Raises:
| Type | Description |
|---|---|
ValueError
|
A limit is out of range ( |
Source code in src/aniliberty/_http.py
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
site_url
property
¶
site_url: str
Origin of base_url (scheme and host), e.g. https://aniliberty.top.
Relative paths in responses, such as image paths, resolve against it.
cache_dir
property
¶
cache_dir: Path | None
The response cache directory, or None when caching is off.
token
property
writable
¶
token: str | None
Session token sent as Authorization: Bearer <token>, or None for none.
Set it to start or stop sending the token; it replaces an Authorization header
given in headers. Public endpoints work with or without it (even with an expired
one); account endpoints answer 403 without a valid one.
aclose
async
¶
aclose() -> None
Close the underlying httpx.AsyncClient, unless it was passed in by the caller.
Safe to call more than once.
Source code in src/aniliberty/_http.py
247 248 249 250 251 252 253 254 255 256 | |
url ¶
url(path: str) -> str
Return the full URL for an API path such as "/app/status".
Source code in src/aniliberty/_http.py
258 259 260 | |
request
async
¶
request(
method: str,
path: str,
*,
params: Mapping[str, Any] | None = None,
json: Any = None,
with_token: bool = True,
cache: bool = False,
refresh: bool = False,
) -> Any
Send a request and return the decoded JSON body.
Retries and limits described on the class apply.
A response is read from and saved to the cache only when all of these hold: the
client has a cache_dir, cache=True, the method is GET, and no session
token goes with the request — so nothing that depends on the logged-in user (or
carries their torrent passkey) ever lands on disk. The cache key is the method, the
full URL and the query parameters in sorted order. Only successful responses with a
body are cached; errors are raised as usual and never stored.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
HTTP method, e.g. |
required |
path
|
str
|
Path relative to |
required |
params
|
Mapping[str, Any] | None
|
Query parameters, passed to |
None
|
json
|
Any
|
JSON request body. |
None
|
with_token
|
bool
|
Whether to send |
True
|
cache
|
bool
|
Whether the response may be read from and saved to the cache (see above). The SDK's own methods pass it for endpoints whose data doesn't change from call to call. |
False
|
refresh
|
bool
|
Skip reading the cache and fetch from the API; the fresh response still replaces the cached one. |
False
|
Returns:
| Type | Description |
|---|---|
Any
|
The decoded JSON body ( |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error status — one of its subclasses for 404, 422, 429, 401/403, 5xx and HTML pages. 429 and 5xx are raised only once retries are exhausted. |
TransportError
|
The request failed without a response (network error, timeout), after retries for idempotent methods. |
ResponseParseError
|
A successful response wasn't valid JSON. |
RuntimeError
|
The client has been closed. |
Example
roles = await client.http.request("GET", "/teams/roles", cache=True)
Source code in src/aniliberty/_http.py
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 | |
request_raw
async
¶
request_raw(
method: str,
path: str,
*,
params: Mapping[str, Any] | None = None,
) -> httpx.Response
Send a request for a non-JSON resource (a file, an XML feed) and return the response.
Same limits, retries and error handling as request(); the body is fully read, so
use stream() instead for large downloads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
HTTP method, e.g. |
required |
path
|
str
|
Path relative to |
required |
params
|
Mapping[str, Any] | None
|
Query parameters, passed to |
None
|
Returns:
| Type | Description |
|---|---|
Response
|
The successful |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error status — see |
TransportError
|
The request failed without a response. |
RuntimeError
|
The client has been closed. |
Example
response = await client.http.request_raw("GET", "/anime/torrents/rss")
print(response.headers["Content-Type"], len(response.text))
Source code in src/aniliberty/_http.py
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
stream
async
¶
stream(
method: str,
path: str,
*,
params: Mapping[str, Any] | None = None,
) -> AsyncIterator[httpx.Response]
Send a request and stream the response body, for downloads.
Limits and retries apply until the response headers arrive: a 429/5xx is retried
before anything is yielded, and an error status raises like request(). The body is
then read by the caller (response.aiter_bytes()); the response is closed when the
async with block exits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
HTTP method, e.g. |
required |
path
|
str
|
Path relative to |
required |
params
|
Mapping[str, Any] | None
|
Query parameters, passed to |
None
|
Yields:
| Type | Description |
|---|---|
AsyncIterator[Response]
|
The successful |
Raises:
| Type | Description |
|---|---|
APIError
|
The API answered with an error status — see |
TransportError
|
The request failed without a response. |
RuntimeError
|
The client has been closed. |
Example
async with client.http.stream("GET", "/anime/torrents/40407/file") as response:
async for chunk in response.aiter_bytes():
...
Source code in src/aniliberty/_http.py
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 | |