Added function to get the airing schedule of a batch of animes and cache

This commit is contained in:
Uthman Fatih 2026-10-08 23:20:08 +01:00
parent 0dbd109d8c
commit 5140730189
3 changed files with 194 additions and 6 deletions

3
.gitignore vendored
View File

@ -220,5 +220,6 @@ __marimo__/
.streamlit/secrets.toml
# Anical
airingdata.json
listdata.json
config.toml
data.json

View File

@ -2,5 +2,8 @@
userId = 8283179
mediaType = "ANIME"
[anical]
listNames = ["Completed", "Planning", "Watching", "Rewatching", "Paused"]
[ical]
# reserved for future use

190
main.py
View File

@ -16,7 +16,7 @@ def getMediaListCollection(
mediaType=config["anilist"]["mediaType"],
userId=config["anilist"]["userId"],
*,
cachePath="data.json"
cachePath="listdata.json"
):
"""
Returns a tuple of (entries, cached)
@ -62,7 +62,7 @@ def getMediaListCollection(
if not cacheExpired and not parametersChanged:
print("Cache is fresh, using cached data...")
return (entries["response"], True)
return (entries, True)
if cacheExpired:
print("Cache has gone stale, refreshing...")
@ -147,9 +147,193 @@ def getMediaListCollection(
return (form, False)
def getAiringScheduleBatch(
animeIds,
*,
ttl=TTL,
cachePath="airingdata.json",
maxRetries=5
):
"""
Returns a tuple of (entries, cached)
Entries is a dictionary of the values to be written to the cache file, such as:
- timestamp
- ttl
- params supplied
- response
response is a dictionary of mediaId (as a string) -> {"title": {"romaji", "english"}, "schedule": [upcoming airing schedule items]}
cached is a boolean indicating whether the cache file was used or not
Takes the following arguments:
- animeIds: list of AniList media ids to fetch airing schedules for
- ttl: seconds the cache stays fresh for. Only applied when the cache is refreshed;
an existing cache keeps the ttl it was written with (defaults to value from config.toml)
- cachePath: the path to the cache file (if not supplied, defaults to "airingdata.json")
- maxRetries: how many times in a row to retry after a 429 before giving up (defaults to 5)
"""
if not animeIds:
raise RuntimeError("animeIds must not be empty")
animeIds = sorted(set(int(i) for i in animeIds))
if path.exists(cachePath):
with open(cachePath, "r") as f:
entries = json.load(f)
if (
"timestamp" in entries
and "ttl" in entries
and "response" in entries
and "params" in entries
):
timestamp = float(entries["timestamp"])
cacheTtl = float(entries["ttl"])
cacheIds = entries["params"].get("animeIds")
cacheExpired = time.time() - timestamp > cacheTtl
parametersChanged = cacheIds != animeIds
if not cacheExpired and not parametersChanged:
print("Airing cache is fresh, using cached data...")
return (entries, True)
if cacheExpired:
print("Airing cache has gone stale, refreshing...")
elif parametersChanged:
print("Airing cache parameters changed, refreshing...")
else:
print("Airing cache is invalid, refreshing...")
query = """
query ($ids: [Int], $page: Int) {
Page(page: $page, perPage: 50) {
pageInfo {
hasNextPage
currentPage
}
airingSchedules(mediaId_in: $ids, notYetAired: true, sort: TIME) {
id
airingAt
timeUntilAiring
episode
mediaId
media {
title {
romaji
english
}
}
}
}
}
"""
schedules = {}
page = 1
retries = 0
while True:
variables = {
"ids": animeIds,
"page": page,
}
resp = requests.post(
"https://graphql.anilist.co",
json={
"query": query,
"variables": variables,
},
timeout=30,
)
# retry if rate limited
if resp.status_code == 429:
retries += 1
if retries > maxRetries:
raise RuntimeError("Rate limited too many times, giving up")
wait = int(resp.headers.get("Retry-After", 60))
print(f"Rate limited, waiting {wait}s...")
time.sleep(wait)
continue
retries = 0
resp.raise_for_status()
data = resp.json()
if "errors" in data:
raise RuntimeError(data["errors"])
pageData = data["data"]["Page"]
for item in pageData["airingSchedules"]:
# media can be null, fall back to empty titles
media = item.pop("media", None) or {}
title = media.get("title") or {}
entry = schedules.setdefault(
str(item["mediaId"]),
{
"title": {
"romaji": title.get("romaji"),
"english": title.get("english"),
},
"schedule": [],
},
)
entry["schedule"].append(item)
if not pageData["pageInfo"]["hasNextPage"]:
break
page += 1
form = {
"timestamp": str(time.time()),
"ttl": str(ttl),
"params": {
"animeIds": animeIds,
},
"response": schedules
}
return (form, False)
if __name__ == "__main__":
entries, cached = getMediaListCollection()
if not cached:
with open("data.json", "w") as f:
with open("listdata.json", "w") as f:
f.write(f"{json.dumps(entries, indent=2)}")
lists = entries["response"]
print("\nList Names updating for:")
for li in config["anical"]["listNames"]:
print(li)
# get a list of all the animes ids in these lists:
animeIds = []
for li in config["anical"]["listNames"]:
if li not in lists:
print(f"Warning: list '{li}' not found, skipping")
continue
for entry in lists[li]:
animeIds.append(entry["media"]["id"])
print(f"\nFound {len(animeIds)} anime ids")
airing, airingCached = getAiringScheduleBatch(animeIds)
if not airingCached:
with open("airingdata.json", "w") as f:
f.write(f"{json.dumps(airing, indent=2)}")
schedules = airing["response"]
print(f"\nGot upcoming episodes for {len(schedules)} anime")