Skip to content

Instantly share code, notes, and snippets.

@nfitzen
Last active November 1, 2021 04:01
Show Gist options
  • Save nfitzen/a48d94db28a1dbb97356c6cb0912842d to your computer and use it in GitHub Desktop.
Save nfitzen/a48d94db28a1dbb97356c6cb0912842d to your computer and use it in GitHub Desktop.
Gets anime metadata from AniList for basic offline viewing.
#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
# MIT License
#
# Copyright (C) 2021 nfitzen <https://github.com/nfitzen>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice (including the
# next paragraph) shall be included in all copies or substantial portions
# of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# This is highly buggy code (as is everything I put out).
# This code is mostly for personal use.
# It generates a "metadata.json" within a specific folder.
# That folder contains notes on anime, mostly for AniList activities.
import httpx
from pathlib import Path
from typing import Optional, Union, overload
import json
from enum import Enum, auto
from sys import argv
QUERY_GET_METADATA = """
query getMetadata($id: Int) {
Media(id: $id) {
title {
romaji
}
type
format
id
idMal
siteUrl
}
}
"""
QUERY_SEARCH_METADATA = """
query searchMetadata($search: String) {
Media(search: $search) {
title {
romaji
}
type
format
id
idMal
siteUrl
}
}
"""
API_URL = "https://graphql.anilist.co/"
HEADERS = {
"Content-Type": "application/json",
"Accept": "application/json",
}
def get_metadata(id: int) -> dict:
variables = {"id": id}
request = httpx.post(
API_URL,
headers=HEADERS,
json={"query": QUERY_GET_METADATA, "variables": variables},
)
return request.json()
def search_metadata(search: str) -> dict:
variables = {"search": search}
request = httpx.post(
API_URL,
headers=HEADERS,
json={"query": QUERY_SEARCH_METADATA, "variables": variables},
)
return request.json()
def process_metadata(metadata: dict, offset: int = 0, temporary: bool = False) -> dict:
data: Optional[dict] = metadata["data"]["Media"]
if data is None:
raise ValueError("Anime data must exist.")
titleData: Union[dict, str] = data["title"]
if isinstance(titleData, dict) and len(titleData) == 1:
data["title"] = list(titleData.values())[0]
extra = {"version": 2, "offset": offset, "temporary": temporary}
extra.update({"data": data})
return extra
class QueryType(Enum):
GET = auto()
SEARCH = auto()
# This function's a mess because it's overloaded.
def process_input(
identifier: Union[int, str],
pathstr: str,
query_type: QueryType = QueryType.GET,
offset: int = 0,
temporary: Union[bool, str] = False,
):
if isinstance(temporary, str) and temporary.lower() in ("y", "true"):
temporary = True
else:
temporary = False
path = Path(pathstr)
if not path.is_file():
path = path / "metadata.json"
if query_type is QueryType.SEARCH and isinstance(identifier, str):
metadata = search_metadata(identifier)
elif query_type is QueryType.GET and isinstance(identifier, int):
metadata = get_metadata(identifier)
else:
raise ValueError("Identifier must match query type.")
print("Got the following metadata:")
print(metadata)
print("--------------------")
metadata = process_metadata(metadata, offset)
with open(path, "w") as f:
json.dump(metadata, f, indent=4)
print(f"Wrote the following to '{path}':")
print(metadata)
# This function is a total mess. I don't know how to handle user input.
# Maybe I should use argparse.
def main():
if len(argv) <= 1 or argv[1].lower() not in ('get', 'search', 'interactive'):
description = """get_metadata.py (get|search|interactive) identifier pathstr [offset] [temporary]
Fetches anime metadata from AniList and stores it in JSON.
Intended for a certain file/directory structure, which I'm too lazy
to document.
"pathstr" represents a file or folder path.
If it's a folder, it stores the metadata in metadata.json.
If a file, it stores it in that file.
If "offset" is negative, it's the offset from the "canonical" episode #.
If it's positive, it's the offset from the AniList count.
The "temporary" field just means to keep it until you finish the notes.
Subcommands:
get - gets metadata from an AniList ID.
"identifier" is the AniList ID.
search - gets metadata from the top search query on AniList.
"identifier" is the anime name.
interactive - interactive mode. Asks for all of these parameters.
"""
print(description)
quit()
if len(argv) > 1 and argv[1].lower() in ('get', 'search'):
identifier, pathstr = argv[2:4]
kwargs = {}
if argv[1].lower() == "get":
identifier = int(identifier)
query_type = QueryType.GET
else:
identifier = str(identifier)
query_type = QueryType.SEARCH
try:
offset = int(argv[4])
kwargs["offset"] = offset
except IndexError:
pass
try:
temporary = argv[5]
kwargs["temporary"] = temporary
except IndexError:
pass
process_input(identifier, pathstr, query_type=query_type, **kwargs)
return
elif argv[1].lower() == 'interactive':
while True:
try:
query_type: Union[str, QueryType] = input("Query type (get/search) [search]: ")
if query_type == "get":
query_type = QueryType.GET
identifier = int(input("AniList ID: "))
else:
query_type = QueryType.SEARCH
identifier = input("Anime/manga title: ")
pathstr = input("Metadata folder/file: ")
offset = input("Specify the offset [0]: ")
try:
offset = int(offset)
except ValueError:
offset = 0
temporary = input("Is this intended to be temporary? (Y/N) [N] ")
except KeyboardInterrupt:
print("\n\nQuitting...")
quit()
process_input(
identifier,
pathstr,
query_type=query_type,
offset=offset,
temporary=temporary,
)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment