├── AnilistPython ├── logs │ ├── __init__.py │ ├── logs.json │ ├── setup.json │ ├── __pycache__ │ │ ├── setup.cpython-37.pyc │ │ ├── __init__.cpython-37.pyc │ │ ├── log_data.cpython-37.pyc │ │ └── setup_module.cpython-37.pyc │ ├── report_logs.py │ ├── internet_latency_identifier.py │ ├── log_data.py │ └── setup_module.py ├── databases │ ├── __init__.py │ ├── anime_database.sqlite3 │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ ├── search_engine.cpython-37.pyc │ │ ├── database_searcher.cpython-37.pyc │ │ └── database_anime_retrieval.cpython-37.pyc │ ├── anime_database_files │ │ ├── anime_by_id_cache.json │ │ └── anime_by_tag_cache.json │ ├── cipher.py │ ├── search_engine.py │ └── database_anime_retrieval.py ├── support_files │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ ├── constants.cpython-37.pyc │ │ ├── exceptions.cpython-37.pyc │ │ ├── translate.cpython-37.pyc │ │ ├── translate.cpython-38.pyc │ │ ├── translate_structure.cpython-37.pyc │ │ └── deep_search_exceptions.cpython-37.pyc │ ├── test.py │ ├── deep_search_exceptions.py │ ├── translate_structure.py │ ├── constants.py │ └── translate.py ├── requirements.txt ├── anilistpython_info.py ├── user.py ├── deep_search.py ├── test_cases.py ├── README.md ├── character.py ├── retrieve_id.py ├── manga.py ├── retrieve_data.py ├── anime.py ├── __init__.py └── query_strings.py ├── requirements.txt ├── .gitignore ├── dist ├── AnilistPython-0.0.7.tar.gz ├── AnilistPython-0.0.8.tar.gz ├── AnilistPython-0.0.9.tar.gz ├── AnilistPython-0.1.0.tar.gz ├── AnilistPython-0.1.1.tar.gz ├── AnilistPython-0.1.3.tar.gz ├── AnilistPython-0.0.7-py3-none-any.whl ├── AnilistPython-0.0.8-py3-none-any.whl ├── AnilistPython-0.0.9-py3-none-any.whl ├── AnilistPython-0.1.0-py3-none-any.whl ├── AnilistPython-0.1.1-py3-none-any.whl └── AnilistPython-0.1.3-py3-none-any.whl ├── MANIFEST.in ├── .github └── workflows │ ├── github-actions-demo.yml │ └── python-publish.yml ├── LICENSE └── README.md /AnilistPython/logs/__init__.py: -------------------------------------------------------------------------------- 1 | #placeholder -------------------------------------------------------------------------------- /AnilistPython/databases/__init__.py: -------------------------------------------------------------------------------- 1 | #placeholder -------------------------------------------------------------------------------- /AnilistPython/databases/anime_database.sqlite3: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AnilistPython/support_files/__init__.py: -------------------------------------------------------------------------------- 1 | #placeholder -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pytest 3 | numpy 4 | -------------------------------------------------------------------------------- /AnilistPython/logs/logs.json: -------------------------------------------------------------------------------- 1 | { 2 | "pd": "pd" 3 | } -------------------------------------------------------------------------------- /AnilistPython/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | requests 3 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.7.tar.gz -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.8.tar.gz -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.9.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.9.tar.gz -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.0.tar.gz -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.1.tar.gz -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.3.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.3.tar.gz -------------------------------------------------------------------------------- /AnilistPython/logs/setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "setup": 0, 3 | "numpy": 0, 4 | "pytest": 0, 5 | "requests": 0 6 | } -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include AnilistPython/logs *.json 2 | recursive-include AnilistPython/databases/anime_database_files *.json -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.7-py3-none-any.whl -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.8-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.8-py3-none-any.whl -------------------------------------------------------------------------------- /dist/AnilistPython-0.0.9-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.0.9-py3-none-any.whl -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.0-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.0-py3-none-any.whl -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.1-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.1-py3-none-any.whl -------------------------------------------------------------------------------- /dist/AnilistPython-0.1.3-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/dist/AnilistPython-0.1.3-py3-none-any.whl -------------------------------------------------------------------------------- /AnilistPython/logs/__pycache__/setup.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/logs/__pycache__/setup.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/logs/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/logs/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/logs/__pycache__/log_data.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/logs/__pycache__/log_data.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/logs/__pycache__/setup_module.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/logs/__pycache__/setup_module.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/databases/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/databases/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/databases/__pycache__/search_engine.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/databases/__pycache__/search_engine.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/constants.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/constants.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/exceptions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/exceptions.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/translate.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/translate.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/translate.cpython-38.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/translate.cpython-38.pyc -------------------------------------------------------------------------------- /AnilistPython/databases/__pycache__/database_searcher.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/databases/__pycache__/database_searcher.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/databases/__pycache__/database_anime_retrieval.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/databases/__pycache__/database_anime_retrieval.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/translate_structure.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/translate_structure.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/support_files/__pycache__/deep_search_exceptions.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReZeroE/AnilistPython/HEAD/AnilistPython/support_files/__pycache__/deep_search_exceptions.cpython-37.pyc -------------------------------------------------------------------------------- /AnilistPython/anilistpython_info.py: -------------------------------------------------------------------------------- 1 | class AnilistPythonInfo: 2 | ''' 3 | Built-in help module. Currently Under Development 4 | ''' 5 | def help(self): 6 | print("\nFor the full documentation, please visit .") -------------------------------------------------------------------------------- /AnilistPython/logs/report_logs.py: -------------------------------------------------------------------------------- 1 | 2 | from datetime import datetime 3 | 4 | class ReportLog: 5 | 6 | ''' 7 | Reports log files for improvement. Currently under development. 8 | ''' 9 | 10 | def __init__(self): 11 | pass 12 | 13 | def report_log_file(self): 14 | pass 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AnilistPython/user.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from .retrieve_data import ExtractInfo 4 | 5 | class User: 6 | def __init__(self, access_info, activated=True): 7 | self.extractInfo = ExtractInfo(access_info, activated) 8 | 9 | def GetUserActivity(self, page, perpage): 10 | activity = self.extractInfo.user_activity(page, perpage) 11 | activity_data = activity["data"] 12 | if len(activity_data) > 0: 13 | return activity_data 14 | else: 15 | return None -------------------------------------------------------------------------------- /AnilistPython/support_files/test.py: -------------------------------------------------------------------------------- 1 | 2 | import time 3 | 4 | # start2 = time.time() 5 | # from deep_translator import GoogleTranslator 6 | 7 | # translator = GoogleTranslator(source='auto', target='japanese') 8 | 9 | # for i in range(500): 10 | # line = 'owari no seraph' 11 | # text = translator.translate(f'{line} anime') 12 | # print(text) 13 | # if i % 50 == 0: 14 | # print(f'Count: {i}') 15 | 16 | # end2 = time.time() 17 | 18 | 19 | start = time.time() 20 | from translate import AnilistPythonTranslate 21 | 22 | translator = AnilistPythonTranslate(source='english', target='japanese') 23 | 24 | for j in range(500): 25 | line = 'owari no seraph' 26 | text = translator.translate(f'{line} anime') 27 | text = text.replace(' ', '') 28 | print(text) -------------------------------------------------------------------------------- /AnilistPython/logs/internet_latency_identifier.py: -------------------------------------------------------------------------------- 1 | import urllib.error as ue 2 | import urllib.request as ur 3 | 4 | class InternetChecker: 5 | ''' 6 | Identifies the internet status of the user. Prompts the user to use the provided local database if their 7 | internet latency is too high. 8 | ''' 9 | def internet_latency_check(self) -> bool: 10 | ''' 11 | Attempts to access google.com with an one second timeout theshold. 12 | :rtype: bool 13 | ''' 14 | try: 15 | s = ur.urlopen("http://www.google.com", timeout=1) 16 | return True 17 | except ue.URLError as ex: 18 | return False 19 | 20 | if __name__ == "__main__": 21 | ic = InternetChecker() 22 | print(ic.internet_latency_check()) -------------------------------------------------------------------------------- /.github/workflows/github-actions-demo.yml: -------------------------------------------------------------------------------- 1 | name: Testing 2 | on: [push] 3 | jobs: 4 | Explore-GitHub-Actions: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." 8 | - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" 9 | - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." 10 | - name: Check out repository code 11 | uses: actions/checkout@v2 12 | - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." 13 | - run: echo "🖥️ The workflow is now ready to test your code on the runner." 14 | - name: List files in the repository 15 | run: | 16 | ls ${{ github.workspace }} 17 | - run: echo "🍏 This job's status is ${{ job.status }}." 18 | -------------------------------------------------------------------------------- /AnilistPython/logs/log_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import datetime 4 | 5 | 6 | class LogData: 7 | ''' 8 | Class responsible for logging the data generated from running the lib. Not in use. 9 | ''' 10 | def __init__(self): 11 | self.log_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs.json") 12 | 13 | def log_data(self, category, input, output): 14 | now = datetime.datetime.now() 15 | date_time = now.strftime("%m/%d/%Y, %H:%M:%S") 16 | 17 | log_dict = self.load_json(self.log_file_path) 18 | log_dict[date_time] = [category, input, output] 19 | 20 | with open(self.log_file_path, "w", encoding="utf-8") as f: 21 | json.dump(log_dict, f, indent=4) 22 | 23 | def load_json(self, filename): 24 | with open(filename, "r", encoding="utf-8") as f: 25 | data = json.load(f) 26 | return data 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin L. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /AnilistPython/deep_search.py: -------------------------------------------------------------------------------- 1 | import re 2 | from support_files.translate import AnilistPythonTranslate 3 | 4 | class DeepSearch(): 5 | ''' 6 | Translation wrapper module that offers more accurate search/retrieval results Currently in BETA testing phase. 7 | ''' 8 | def __init__(self): 9 | pass 10 | 11 | def deep_search_name_conversion(self, anime_name) -> str: 12 | ''' 13 | Function for converting the name of the anime into Japanese using the built-in Google translator. 14 | Note: this is an optional function due to its instability (translation failures). 15 | 16 | :param anime_name: the name of the anime to be searched 17 | :rtype: str 18 | ''' 19 | translator = AnilistPythonTranslate(source='english', target='japanese') 20 | anime_name_jp = translator.translate(f'{anime_name} anime') 21 | 22 | # deep search failed 23 | if re.search('^[a-zA-Z]*$', anime_name_jp) != None: 24 | return '-1' 25 | 26 | anime_name_final = anime_name_jp.replace(' ', '').replace('アニメ', '') 27 | 28 | # print(anime_name_final) 29 | return anime_name_final 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | deploy: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package 33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /AnilistPython/support_files/deep_search_exceptions.py: -------------------------------------------------------------------------------- 1 | class BaseError(Exception): 2 | """ 3 | base error structure class 4 | """ 5 | 6 | def __init__(self, val, message): 7 | """ 8 | @param val: actual value 9 | @param message: message shown to the user 10 | """ 11 | self.val = val 12 | self.message = message 13 | super().__init__() 14 | 15 | def __str__(self): 16 | return "Error Code {} --> {}".format(self.val, self.message) 17 | 18 | 19 | class DeepSearchError(BaseError): 20 | """ 21 | exception thrown if the deep-search has failed 22 | """ 23 | 24 | def __init__(self, val, message="There is an error in while executing deep-search. Deep-search is dependent on Google services. Please try again later."): 25 | super().__init__(val, message) 26 | 27 | 28 | class InvalidInput(BaseError): 29 | """ 30 | exception thrown if the user inputted an invalid name 31 | """ 32 | 33 | def __init__(self, 34 | val, 35 | message='Your search input is invalid. Please try a different name or refrain from using deep-search with this name.'): 36 | super().__init__(val, message) -------------------------------------------------------------------------------- /AnilistPython/support_files/translate_structure.py: -------------------------------------------------------------------------------- 1 | """parent translator class""" 2 | 3 | from .deep_search_exceptions import InvalidInput 4 | import string 5 | class BaseTranslator(): 6 | """ 7 | Abstract class that serve as a parent translator for other different translators 8 | """ 9 | def __init__(self, 10 | base_url=None, 11 | source="auto", 12 | target="ja", 13 | payload_key=None, 14 | element_tag=None, 15 | element_query=None, 16 | **url_params): 17 | """ 18 | @param source: source language to translate from 19 | @param target: target language to translate to 20 | """ 21 | if source == target: 22 | raise InvalidInput(source) 23 | 24 | self.__base_url = base_url 25 | self._source = source 26 | self._target = target 27 | self._url_params = url_params 28 | self._element_tag = element_tag 29 | self._element_query = element_query 30 | self.payload_key = payload_key 31 | self.headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) ' 32 | 'AppleWebit/535.19' 33 | '(KHTML, like Gecko) Chrome/18.0.1025.168 Safari/535.19'} 34 | super(BaseTranslator, self).__init__() 35 | 36 | @staticmethod 37 | def _validate_payload(payload, min_chars=1, max_chars=5000): 38 | """ 39 | validate the target text to translate 40 | @param payload: text to translate 41 | @return: bool 42 | """ 43 | 44 | if not payload or not isinstance(payload, str) or not payload.strip() or payload.isdigit(): 45 | raise InvalidInput(payload) 46 | 47 | # check if payload contains only symbols 48 | if all(i in string.punctuation for i in payload): 49 | raise InvalidInput(payload) 50 | 51 | if not BaseTranslator.__check_length(payload, min_chars, max_chars): 52 | raise InvalidInput(payload, min_chars, max_chars) 53 | return True 54 | 55 | @staticmethod 56 | def __check_length(payload, min_chars, max_chars): 57 | """ 58 | check length of the provided target text to translate 59 | @param payload: text to translate 60 | @param min_chars: minimum characters allowed 61 | @param max_chars: maximum characters allowed 62 | @return: bool 63 | """ 64 | return True if min_chars <= len(payload) < max_chars else False 65 | 66 | -------------------------------------------------------------------------------- /AnilistPython/support_files/constants.py: -------------------------------------------------------------------------------- 1 | 2 | BASE_URLS = { 3 | "GOOGLE_TRANSLATE": "https://translate.google.com/m" 4 | } 5 | 6 | GOOGLE_CODES_TO_LANGUAGES = { 7 | 'af': 'afrikaans', 8 | 'sq': 'albanian', 9 | 'am': 'amharic', 10 | 'ar': 'arabic', 11 | 'hy': 'armenian', 12 | 'az': 'azerbaijani', 13 | 'eu': 'basque', 14 | 'be': 'belarusian', 15 | 'bn': 'bengali', 16 | 'bs': 'bosnian', 17 | 'bg': 'bulgarian', 18 | 'ca': 'catalan', 19 | 'ceb': 'cebuano', 20 | 'ny': 'chichewa', 21 | 'zh': 'chinese', 22 | 'zh-cn': 'chinese (simplified)', 23 | 'zh-tw': 'chinese (traditional)', 24 | 'co': 'corsican', 25 | 'hr': 'croatian', 26 | 'cs': 'czech', 27 | 'da': 'danish', 28 | 'nl': 'dutch', 29 | 'en': 'english', 30 | 'eo': 'esperanto', 31 | 'et': 'estonian', 32 | 'tl': 'filipino', 33 | 'fi': 'finnish', 34 | 'fr': 'french', 35 | 'fy': 'frisian', 36 | 'gl': 'galician', 37 | 'ka': 'georgian', 38 | 'de': 'german', 39 | 'el': 'greek', 40 | 'gu': 'gujarati', 41 | 'ht': 'haitian creole', 42 | 'ha': 'hausa', 43 | 'haw': 'hawaiian', 44 | 'iw': 'hebrew', 45 | 'hi': 'hindi', 46 | 'hmn': 'hmong', 47 | 'hu': 'hungarian', 48 | 'is': 'icelandic', 49 | 'ig': 'igbo', 50 | 'id': 'indonesian', 51 | 'ga': 'irish', 52 | 'it': 'italian', 53 | 'ja': 'japanese', 54 | 'jw': 'javanese', 55 | 'kn': 'kannada', 56 | 'kk': 'kazakh', 57 | 'km': 'khmer', 58 | 'ko': 'korean', 59 | 'ku': 'kurdish (kurmanji)', 60 | 'ky': 'kyrgyz', 61 | 'lo': 'lao', 62 | 'la': 'latin', 63 | 'lv': 'latvian', 64 | 'lt': 'lithuanian', 65 | 'lb': 'luxembourgish', 66 | 'mk': 'macedonian', 67 | 'mg': 'malagasy', 68 | 'ms': 'malay', 69 | 'ml': 'malayalam', 70 | 'mt': 'maltese', 71 | 'mi': 'maori', 72 | 'mr': 'marathi', 73 | 'mn': 'mongolian', 74 | 'my': 'myanmar (burmese)', 75 | 'ne': 'nepali', 76 | 'no': 'norwegian', 77 | 'ps': 'pashto', 78 | 'fa': 'persian', 79 | 'pl': 'polish', 80 | 'pt': 'portuguese', 81 | 'pa': 'punjabi', 82 | 'ro': 'romanian', 83 | 'ru': 'russian', 84 | 'sm': 'samoan', 85 | 'gd': 'scots gaelic', 86 | 'sr': 'serbian', 87 | 'st': 'sesotho', 88 | 'sn': 'shona', 89 | 'sd': 'sindhi', 90 | 'si': 'sinhala', 91 | 'sk': 'slovak', 92 | 'sl': 'slovenian', 93 | 'so': 'somali', 94 | 'es': 'spanish', 95 | 'su': 'sundanese', 96 | 'sw': 'swahili', 97 | 'sv': 'swedish', 98 | 'tg': 'tajik', 99 | 'ta': 'tamil', 100 | 'te': 'telugu', 101 | 'th': 'thai', 102 | 'tr': 'turkish', 103 | 'uk': 'ukrainian', 104 | 'ur': 'urdu', 105 | 'uz': 'uzbek', 106 | 'vi': 'vietnamese', 107 | 'cy': 'welsh', 108 | 'xh': 'xhosa', 109 | 'yi': 'yiddish', 110 | 'yo': 'yoruba', 111 | 'zu': 'zulu', 112 | 'fil': 'Filipino', 113 | 'he': 'Hebrew' 114 | } 115 | 116 | GOOGLE_LANGUAGES_TO_CODES = {v: k for k, v in GOOGLE_CODES_TO_LANGUAGES.items()} -------------------------------------------------------------------------------- /AnilistPython/logs/setup_module.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import pkg_resources 5 | import re 6 | 7 | 8 | class Setup: 9 | def __init__(self): 10 | self.required_lib = { 11 | 'numpy': 0, 12 | 'pytest': 0, 13 | 'requests': 0 14 | } 15 | 16 | self.setup_var = self.setup_lib() 17 | 18 | 19 | def setup_lib(self): 20 | 21 | d = dict() 22 | try: 23 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'setup.json'), 'r', encoding="utf-8") as rf: 24 | d = json.load(rf) 25 | except Exception as e: 26 | pass 27 | # print('Setup file error.') 28 | 29 | 30 | if d['setup'] == 0: 31 | control = self.download_lib() 32 | else: 33 | for lib, dow in d.items(): 34 | if dow == 0: 35 | control = self.download_lib() 36 | 37 | d = dict((k, 0) for k, v in d.items()) 38 | 39 | with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'setup.json'), 'w') as wf: 40 | json.dump(d, wf, indent=4) 41 | 42 | def download_lib(self): 43 | required = {lib.lower() for lib, dow in self.required_lib.items()} 44 | installed = {pkg.key for pkg in pkg_resources.working_set} 45 | missing = required - installed 46 | 47 | if missing: 48 | import subprocess 49 | 50 | print(f"Warning: Required Libraries Missing >>> {missing}") 51 | user_input = input("Initiate automatic library installation? Proceed [y/n]") 52 | 53 | if user_input.lower() == 'n': 54 | print("Program Terminated...") 55 | sys.exit(0) 56 | elif user_input.lower() == 'y': 57 | print("Installing required libraries...") 58 | for lib in missing: 59 | subprocess.run(f"py -{self.get_python_version()} -m pip install {lib}", shell=True) 60 | print(f"Library >{lib}< has been installed. Please restart the program.") 61 | sys.exit(0) 62 | else: 63 | print("User input unrecognized. Program terminated...") 64 | sys.exit(0) 65 | 66 | return True # has missing lib (lib installed) 67 | return False # no missing lib 68 | 69 | 70 | def validate_installation(self): 71 | required = {lib.lower() for lib, dow in self.required_lib.items()} 72 | installed = {pkg.key for pkg in pkg_resources.working_set} 73 | missing = required - installed 74 | 75 | 76 | if missing: 77 | print(f"Libraries {missing} failed to be installed. Program Terminated.") 78 | sys.exit(0) 79 | else: 80 | print("Library validation complete. All required Libraries has been sucessfully installed.") 81 | 82 | def get_python_version(self): 83 | from platform import python_version 84 | ret = re.match('[0-9].[0-9]', python_version()) 85 | return ret.group(0) 86 | 87 | 88 | if __name__ == '__main__': 89 | import time 90 | 91 | start = time.time() 92 | s = Setup() 93 | s.setup_lib() 94 | print(time.time() - start) -------------------------------------------------------------------------------- /AnilistPython/support_files/translate.py: -------------------------------------------------------------------------------- 1 | from .constants import BASE_URLS, GOOGLE_LANGUAGES_TO_CODES 2 | from .deep_search_exceptions import DeepSearchError, InvalidInput 3 | from .translate_structure import BaseTranslator 4 | 5 | from time import sleep 6 | import requests 7 | import re 8 | 9 | class AnilistPythonTranslate(BaseTranslator): 10 | """ 11 | class that wraps functions, which use google translate under the hood to translate text(s) 12 | """ 13 | _languages = GOOGLE_LANGUAGES_TO_CODES 14 | supported_languages = list(_languages.keys()) 15 | 16 | def __init__(self, source="en", target="ja", proxies=None, **kwargs): 17 | """ 18 | @param source: source language to translate from 19 | @param target: target language to translate to 20 | """ 21 | self.__base_url = BASE_URLS.get("GOOGLE_TRANSLATE") 22 | self.proxies = proxies 23 | 24 | self._source, self._target = self.map_language_to_code(source.lower(), target.lower()) 25 | 26 | super(AnilistPythonTranslate, self).__init__(base_url=self.__base_url, 27 | source=self._source, 28 | target=self._target, 29 | element_tag='div', 30 | payload_key='q', # key of text in the url 31 | hl=self._target, 32 | sl=self._source, 33 | **kwargs) 34 | 35 | def map_language_to_code(self, *languages): 36 | """ 37 | map language to its corresponding code (abbreviation) if the language was passed by its full name by the user 38 | @param languages: list of languages 39 | @return: mapped value of the language or raise an exception if the language is not supported 40 | """ 41 | for language in languages: 42 | if language in self._languages.values() or language == 'auto': 43 | yield language 44 | elif language in self._languages.keys(): 45 | yield self._languages[language] 46 | else: 47 | raise InvalidInput(language) 48 | 49 | 50 | def translate(self, text, **kwargs): 51 | """ 52 | function that uses google translate to translate a text 53 | @param text: desired text to translate 54 | @return: str: translated text 55 | """ 56 | if self._validate_payload(text): 57 | text = text.strip() 58 | 59 | if self.payload_key: 60 | self._url_params[self.payload_key] = text 61 | 62 | response = requests.get(self.__base_url, 63 | params=self._url_params) 64 | 65 | if response.status_code == 429: 66 | raise InvalidInput(response.status_code) 67 | 68 | if response.status_code != 200: 69 | raise InvalidInput(response.status_code) 70 | 71 | translated_text = re.search('
.*?
', response.text).group(0) 72 | translated_text = translated_text.replace('
', '').replace('
', '') 73 | return translated_text 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /AnilistPython/databases/anime_database_files/anime_by_id_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "placeholder": "placeholder", 3 | "11757": { 4 | "name_romaji": "Sword Art Online", 5 | "name_english": "Sword Art Online", 6 | "starting_time": "7/8/2012", 7 | "ending_time": "12/23/2012", 8 | "cover_image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx11757-Q9P2zjCPICq5.jpg", 9 | "banner_image": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/11757-TlEEV9weG4Ag.jpg", 10 | "airing_format": "TV", 11 | "airing_status": "FINISHED", 12 | "airing_episodes": 25, 13 | "season": "SUMMER", 14 | "desc": "In the near future, a Virtual Reality Massive Multiplayer Online Role-Playing Game (VRMMORPG) called Sword Art Online has been released where players control their avatars with their bodies using a piece of technology called Nerve Gear. One day, players discover they cannot log out, as the game creator is holding them captive unless they reach the 100th floor of the game's tower and defeat the final boss. However, if they die in the game, they die in real life. Their struggle for survival starts now...

\n(Source: Crunchyroll)", 15 | "average_score": 68, 16 | "genres": [ 17 | "Action", 18 | "Adventure", 19 | "Fantasy", 20 | "Romance" 21 | ], 22 | "next_airing_ep": null, 23 | "cache_pos": 1 24 | }, 25 | "20594": { 26 | "name_romaji": "Sword Art Online II", 27 | "name_english": "Sword Art Online II", 28 | "starting_time": "7/5/2014", 29 | "ending_time": "12/20/2014", 30 | "cover_image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/nx20594-FhRgZ1H9Istt.jpg", 31 | "banner_image": "https://s4.anilist.co/file/anilistcdn/media/anime/banner/20594-BZOLwqidcS1G.jpg", 32 | "airing_format": "TV", 33 | "airing_status": "FINISHED", 34 | "airing_episodes": 24, 35 | "season": "SUMMER", 36 | "desc": "One year after the SAO incident, Kirito is approached by Seijiro Kikuoka from Japan's Ministry of Internal Affairs and Communications Department \"VR Division\" with a rather peculiar request.\r\n\r\nThat was an investigation on the \"Death Gun\" incident that occurred in the gun and steel filled VRMMO called Gun Gale Online (GGO). \"Players who are shot by a mysterious avatar with a jet black gun lose their lives even in the real world...\" Failing to turn down Kikuoka's bizarre request, Kirito logs in to GGO even though he is not completely convinced that the virtual world could physically affect the real world.
\r\n
\r\nKirito wanders in an unfamiliar world in order to gain any clues about the \"Death Gun.\" Then, a female sniper named Sinon who owns a gigantic \"Hecate II\" rifle extends Kirito a helping hand. With Sinon's help, Kirito decides to enter the \"Bullet of Bullets,\" a large tournament to choose the most powerful gunner within the realm of GGO, in hopes to become the target of the \"Death Gun\" and make direct contact with the mysterious avatar.
\r\n
\r\n(Source: Crunchyroll)", 37 | "average_score": 65, 38 | "genres": [ 39 | "Action", 40 | "Adventure", 41 | "Fantasy", 42 | "Sci-Fi" 43 | ], 44 | "next_airing_ep": null, 45 | "cache_pos": 2 46 | }, 47 | "16099": { 48 | "name_romaji": "Sword Art Offline", 49 | "name_english": "Sword Art OFFline", 50 | "starting_time": "10/24/2012", 51 | "ending_time": "6/26/2013", 52 | "cover_image": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/medium/16099.jpg", 53 | "banner_image": null, 54 | "airing_format": "SPECIAL", 55 | "airing_status": "FINISHED", 56 | "airing_episodes": 9, 57 | "season": "FALL", 58 | "desc": "BD/DVD specials.", 59 | "average_score": 54, 60 | "genres": [ 61 | "Comedy" 62 | ], 63 | "next_airing_ep": null, 64 | "cache_pos": 3 65 | } 66 | } -------------------------------------------------------------------------------- /AnilistPython/test_cases.py: -------------------------------------------------------------------------------- 1 | from __init__ import Anilist 2 | instance = Anilist() 3 | 4 | from deep_search import DeepSearch 5 | ds = DeepSearch() 6 | 7 | class TestCase: 8 | ''' 9 | Simple AnilistPython test case module provided to the users. 10 | ''' 11 | def __init__(self): 12 | pass 13 | 14 | # BOT SUPPORT ==================================================================================== 15 | def runTests(self): 16 | ''' 17 | Test case runner. (Depending on your internet connection, runtime should be ~5 seconds) 18 | ''' 19 | self.test_getAnime() 20 | self.test_getAnimeWithID() 21 | 22 | self.test_getCharacter() 23 | self.test_getCharacterWithID() 24 | 25 | self.test_anilistAnimeInfo() 26 | self.test_deepSearch() 27 | 28 | self.test_searchAnime() 29 | 30 | def test_getAnime(self): 31 | data = instance.get_anime("Code Geass Rebellion") 32 | assert data["name_romaji"] == "Code Geass: Hangyaku no Lelouch" 33 | assert data["name_english"] == "Code Geass: Lelouch of the Rebellion" 34 | assert data["starting_time"] == "10/6/2006" 35 | assert data["ending_time"] == "7/28/2007" 36 | assert data["airing_episodes"] == 25 37 | 38 | def test_getAnimeDatabase(self): 39 | pass 40 | 41 | def test_getAnimeWithID(self): 42 | ID = 13759 #Sakurasou 43 | data = instance.get_anime_with_id(ID) 44 | assert data["name_romaji"] == "Sakurasou no Pet na Kanojo" 45 | assert data["name_english"] == "The Pet Girl of Sakurasou" 46 | assert data["starting_time"] == "10/9/2012" 47 | assert data["ending_time"] == "3/26/2013" 48 | assert data["airing_episodes"] == 24 49 | 50 | def test_getCharacter(self): 51 | data = instance.get_character("Emilia Tan") 52 | assert data["first_name"] == "Emilia" 53 | assert data["last_name"] == None 54 | assert data["native_name"] == "エミリア" 55 | assert data["image"] == "https://s4.anilist.co/file/anilistcdn/character/large/b88572-v2KimyNuU4XZ.jpg" 56 | 57 | def test_getCharacterWithID(self): 58 | ID = 42314 #Harutora from Tokyo Ravens 59 | data = instance.get_character_with_id(ID) 60 | assert data["first_name"] == "Harutora" 61 | assert data["last_name"] == 'Tsuchimikado' 62 | assert data["native_name"] == "土御門 春虎" 63 | assert data["image"] == "https://s4.anilist.co/file/anilistcdn/character/large/42314.jpg" 64 | 65 | def test_anilistAnimeInfo(self): 66 | data = instance.get_anime("Konosuba S1") 67 | assert data["name_romaji"] == "Kono Subarashii Sekai ni Shukufuku wo! 2" 68 | assert data["name_english"] == "KONOSUBA -God's blessing on this wonderful world! 2" 69 | assert data["starting_time"] == "1/12/2017" 70 | assert data["ending_time"] == "3/16/2017" 71 | assert data["airing_episodes"] == 10 72 | 73 | def test_deepSearch(self): 74 | assert ds.deep_search_name_conversion("Code Geass") == 'コードギアス' 75 | assert ds.deep_search_name_conversion("Eighty-Six") == '86' 76 | assert ds.deep_search_name_conversion("Re:Zero") == 'Re:ゼロから始める異世界生活' 77 | assert ds.deep_search_name_conversion("Tensei Slime") == '転生したらスライム' 78 | assert ds.deep_search_name_conversion("Princess Connect") == 'プリンセスコネクト' 79 | 80 | def test_searchAnime(self): 81 | data = instance.get_anime('Re:Zero kara Hajimeru Isekai Seikatsu') 82 | assert instance.search_anime(genre=['Action', 'Adventure', 'Drama', 'Fantasy', 'Psychological', 'Romance', 'Thriller'], year='2016', score=range(80, 90)) == [data] 83 | 84 | data_id = instance.get_anime_id('Re:Zero kara Hajimeru Isekai Seikatsu') 85 | assert instance.search_anime(genre=['Action', 'Adventure', 'Drama', 'Fantasy', 'Psychological', 'Romance', 'Thriller'], year='2016', score=range(80, 90), id_only=True) == [str(data_id)] 86 | 87 | 88 | 89 | testCase = TestCase() 90 | testCase.runTests() 91 | print('=====================================') 92 | print("| TEST COMPLETED! NO ERRORS FOUND! |") 93 | print('=====================================') 94 | -------------------------------------------------------------------------------- /AnilistPython/databases/cipher.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Script not in use (Ver 0.1.1) 3 | ''' 4 | 5 | import os 6 | import sys 7 | import time 8 | import random 9 | 10 | class EncryptDatabase(): 11 | def __init__(self): 12 | self.output_dir = os.path.dirname(os.path.abspath(__file__)) 13 | self.encrypted_filename = 'Anime-Storage-Encrypted.txt' 14 | self.decrypted_filename = 'Anime-Storage-Decrypted.txt' 15 | 16 | self.encryption_key = 'key.tsv' 17 | self.databse_file = 'test_dataset.json' 18 | 19 | def encrypt(self): 20 | data = [] 21 | encrypted_data = [] 22 | key = [] 23 | 24 | with open(f"{self.output_dir}/{self.databse_file}", "r", encoding="utf-8") as file_ptr: 25 | data = file_ptr.readlines() 26 | 27 | # Kevin-Encryption (not very robust but works haha) 28 | count = 0 29 | for line in data: 30 | 31 | encrypted_line = '' 32 | encryption_key = '' 33 | for char in line: 34 | if char == '\n': 35 | encrypted_line += '\n' 36 | encryption_key += f'z-z\t' 37 | break 38 | 39 | rand_subtractor = random.randint(ord(char) - 31, ord(char) - 1) 40 | min_char_val = int(32 / (ord(char) - rand_subtractor)) 41 | max_char_val = int(126 / (ord(char) - rand_subtractor)) 42 | 43 | rand_multiplier = random.randint(min_char_val + 1, max_char_val) 44 | 45 | assert ord(char) - rand_subtractor > 0 46 | assert rand_subtractor > 0 47 | 48 | char_val = (ord(char) - rand_subtractor) * rand_multiplier 49 | new_char = chr(int(char_val)) 50 | 51 | encrypted_line += new_char 52 | encryption_key += f'{rand_subtractor}-{rand_multiplier}\t' 53 | 54 | # print(f'Original Line: >{line}<, Encrypted Line: >{encrypted_line}<') 55 | encrypted_data.append(f'{encrypted_line}') 56 | key.append(encryption_key) 57 | 58 | count += 1 59 | 60 | if count % 1000 == 0: 61 | print(f'Encrypt Count: {count}') 62 | 63 | 64 | print('Logging encrypted data and encryption keys...') 65 | with open(f"{self.output_dir}/{self.encrypted_filename}", "w", encoding="utf-8") as file_ptr: 66 | for encry_line in encrypted_data: 67 | file_ptr.write(f'{encry_line}') 68 | 69 | with open(f"{self.output_dir}/{self.encryption_key}", "w", encoding="utf-8") as file_ptr: 70 | for val in key: 71 | file_ptr.write(f'{val}\n') 72 | 73 | time.sleep(5) # avoid PC lag 74 | 75 | def decrypt(self): 76 | keys = [] 77 | encrypted_data = [] 78 | encry_key_dict = {} 79 | decrypted_data = [] 80 | 81 | 82 | with open(f"{self.output_dir}/{self.encryption_key}", "r", encoding="utf-8") as file_ptr: 83 | keys = file_ptr.readlines() 84 | 85 | with open(f"{self.output_dir}/{self.encrypted_filename}", "r", encoding="utf-8") as file_ptr: 86 | encrypted_data = file_ptr.readlines() 87 | 88 | encry_key_dict = dict(zip(keys, encrypted_data)) 89 | 90 | decryption_count = 0 91 | for line_keys in encry_key_dict: 92 | key_count = 0 93 | key = line_keys.strip('\n').split('\t') 94 | decrypted_line = '' 95 | 96 | for encrypted_char in encry_key_dict[line_keys]: 97 | decryption_multipliers = key[key_count].split('-') #[rand_subtractor - rand_multiplier] 98 | key_count += 1 99 | 100 | if encrypted_char == '\n': break 101 | decrypted_char_val = int(ord(encrypted_char) / int(decryption_multipliers[1]) + int(decryption_multipliers[0])) 102 | decrypted_line += chr(decrypted_char_val) 103 | 104 | decrypted_data.append(decrypted_line) 105 | if decryption_count % 1000 == 0: print(f'Decryption Count: {decryption_count}') 106 | decryption_count += 1 107 | 108 | 109 | with open(f"{self.output_dir}/{self.decrypted_filename}", "w", encoding="utf-8") as file_ptr: 110 | for line in decrypted_data: 111 | file_ptr.write(f'{line}\n') 112 | 113 | 114 | 115 | if __name__ == '__main__': 116 | start_time = time.time() 117 | 118 | encryptor = EncryptDatabase() 119 | encryptor.encrypt() 120 | encryptor.decrypt() 121 | 122 | print(f'Total Time Spent: [{time.time() - start_time} seconds]') 123 | 124 | 125 | 126 | # if char_val < 32: 127 | # print(f'RandSub = {rand_subtractor}, min_multi = {min_char_val}, max_multi = {max_char_val}, randMulti = {rand_multiplier}, orgVal = {ord(char)}') 128 | # sys.exit(0) -------------------------------------------------------------------------------- /AnilistPython/README.md: -------------------------------------------------------------------------------- 1 | # AnilistPython 2 | 3 | ![example workflow](https://github.com/ReZeroE/AnilistPython/actions/workflows/github-actions-demo.yml/badge.svg) 4 | ![downloads](https://img.shields.io/github/workflow/status/ReZeroE/AnilistPython/GitHub%20Actions%20Demo) 5 | ![downloads](https://img.shields.io/pypi/dm/AnilistPython) 6 | ![licence](https://img.shields.io/github/license/ReZeroE/AnilistPython) 7 | ![Test](https://pepy.tech/badge/anilistpython) 8 | 9 | AniList Python library (anilist.co APIv2 wrapper) that allows you to **easily search up and retrieve anime, manga, animation studio, and character information.** This library is both beginner-friendly and offers the freedom for more experienced developers to interact with the retrieved information. Provides bot support. 10 | 11 | ![alt text](https://i.imgur.com/uGzW7vr.jpg) 12 | 13 | ## Version 0.1.3 Overview 14 | This recent update for AnilistPython has resulted in a moderate change in the library's archetecture for increased efficiency and speed. Various features have also been added to the library. Listed below are some of the main additions and alterations made to the library. 15 | 16 | **New features**: 17 | 1. Anime search by genre, year, and/or average score (finally!) 18 | 2. Offline anime retrieval support for anime - BETA 19 | 3. Manga search support 20 | 4. Auto setup feature that help new python programmers to setup required libraries automatically 21 | 22 | Optimization and updates: 23 | 1. The lib now has its own prebuild anime database! 24 | 2. Anime, manga, and character search functions have all been optimized, making searches faster! 25 | 3. Improved the deepsearch feature in `.get_anime()`. 26 | 4. Manually selecting results feature is now a parameter instead of a seperate function (see usage below). 27 | 28 | 29 | ## How to use? 30 | **Step One:** Library Installation 31 | ``` python 32 | pip install AnilistPython==0.1.3 33 | ``` 34 | **Step Two:** Instance Creation 35 | ```python 36 | from AnilistPython import Anilist 37 | anilist = Anilist() 38 | ``` 39 | **Step Three**: Usage 40 | 41 | The AnilistPython library has been split into three distinct sections. Each section possess a different set of functions used for retrieving data in that category. Please visit the full documentation for more info or skip to the General Function Overview section for usage. 42 | - **Anime** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Anime)) 43 | - **Manga** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Manga)) 44 | - **Character** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Character)) 45 | 46 | 47 | ## General Function Overview 48 | The following functions are supported by AnilistPyhon version 0.1.3. Only the default parameter has been displayed below. For more information, visit the [full documentation](https://github.com/ReZeroE/AnilistPython/wiki). 49 | ```python 50 | # ANIME 51 | anilist.get_anime("Owari no Seraph") # returns a dictionary containing info about owari no seraph 52 | anilist.get_anime_with_id(126830) # returns a dictionary with Code Geass (ID:126830) info 53 | anilist.get_anime_id("ReZero") # returns Re:Zero's ID on Anilist 54 | anilist.print_anime_info("Madoka Magica") # prints all information regarding the anime Madoka Magica 55 | 56 | # returns a list of anime with the given restrictions 57 | anilist.search_anime(genre=['Action', 'Adventure', 'Drama'], year=[2016, 2019], score=range(80, 95)) 58 | 59 | #CHARACTER 60 | anilist.get_character("Emilia") # returns a dictionary containing the info about Emilia-tan 61 | anilist.get_character_with_id(13701) # returns a dictionary with Misaka Mikoto (ID:13701) info 62 | anilist.get_character_id("Milim") # returns character Milim's ID on Anilist 63 | anilist.print_anime_info("Kirito") # prints all information regarding the character Kirito 64 | 65 | # MANGA 66 | anilist.get_manga("Seraph of the End") # returns a dictionary containing info about seraph of the end 67 | anilist.get_manga_with_id(113399) # returns a dictionary with Tearmoon (ID:113399) info 68 | anilist.get_manga_id("Tearmoon Empire") # returns Tearmoon Empire's ID on Anilist (manga) 69 | anilist.print_manga_info("Tensei Slime") # prints all information regarding the manga Tensei Slime 70 | ``` 71 | 72 | Note: The feature for manully selecting the top three search results in the terminal is now controlled by a parameter (`manual_select`) in .get functions. For more information, please visit the full documentation. A sample program that has manual select enabled would be: 73 | 74 | ```python 75 | anilist.get_anime("Owari no Seraph", manual_select=True) 76 | ``` 77 | 78 | 79 | ## Discord Bot Support 80 | AnilistPython was originially designed to support various Discord Bot features in relation to anime, but through out the course of its development, more features became available to use by the general programs other than Discord bots. With that been said, the current version of AnilistPython has further optimized its functions for bot support. From the pre-formatted JSON file upon data retrieval to offline database support (see full documentation), it is now able to be implemented in bots with ease. 81 | 82 | Upcoming AnilistPython Version 0.2.0 will provide functions to generate pre-formated Discord embeds (Anime, Manga, Character embeds) as well as other features that make AnilistPython bot implementations easy to use. 83 | 84 | Sample anime discord bot supported by AnilistPython V0.1.3: [Anime C.C. Discord Bot](https://github.com/ReZeroE/Anime-Discord-Bot) 85 | 86 | Note: Please make sure that parameter `manual_select` has not been set to True in bot implementations. (False by default) 87 | 88 | ## Credits 89 | Lead Developer: Kevin L. (ReZeroE) 90 | 91 | Special thanks to AniList's ApiV2 GraphQL Dev team for making this possible. 92 | -------------------------------------------------------------------------------- /AnilistPython/databases/search_engine.py: -------------------------------------------------------------------------------- 1 | 2 | from unittest import result 3 | import numpy as np 4 | import time 5 | import json 6 | import os 7 | 8 | 9 | class SearchEngine: 10 | def __init__(self): 11 | self.dir_path = os.path.dirname(os.path.realpath(__file__)) 12 | self.storage_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'anime_database_files') 13 | 14 | self.id_dict_dir = os.path.join(self.storage_dir, "anime_by_id.json") 15 | self.id_cache_dict_dir = os.path.join(self.storage_dir, "anime_by_id_cache.json") 16 | self.tag_dict_dir = os.path.join(self.storage_dir, "anime_by_tag.json") 17 | self.tag_cache_dict_dir = os.path.join(self.storage_dir, "anime_by_tag_cache.json") 18 | 19 | self.cache_size = 500 20 | 21 | def search_anime_database(self, anime_name, accuracy_threshold=0.7, full_record_override=False): 22 | tag_dict = self.load_json(self.tag_dict_dir) 23 | 24 | resulting_dict = dict() 25 | caching_dict = self.load_json(self.tag_cache_dict_dir) 26 | caching_queue = [] 27 | 28 | ini_len = len(resulting_dict) 29 | 30 | # searches the cached database 31 | import copy 32 | temp_caching_dict = copy.deepcopy(caching_dict) 33 | if full_record_override == False: 34 | for anime_tag, anime_id in temp_caching_dict.items(): 35 | curr_tags = ['ph'] 36 | if anime_tag.find('|=|') != -1: 37 | curr_tags = anime_tag.split('|=|') 38 | curr_tags = [tag.lower() for tag in curr_tags] 39 | else: 40 | curr_tags[0] = anime_tag.lower() 41 | 42 | for tag in curr_tags: 43 | acc = self.levenshtein_ratio(anime_name, tag, ratio_calc=True) 44 | if acc >= accuracy_threshold: 45 | resulting_dict[anime_id] = acc 46 | 47 | if len(curr_tags) == 1: 48 | caching_dict[curr_tags[0]] = anime_id 49 | else: 50 | caching_dict[f'{curr_tags[0]}|=|{curr_tags[1]}'] = anime_id 51 | 52 | 53 | # searches record from the master database if 54 | # - cache search record len < 2 55 | # (OR) 56 | # - full_record_override is on 57 | if len(resulting_dict) - ini_len < 2 or full_record_override == True: 58 | for anime_tag, anime_id in tag_dict.items(): 59 | curr_tags = ['ph'] 60 | if anime_tag.find('|=|') != -1: 61 | curr_tags = anime_tag.split('|=|') 62 | curr_tags = [tag.lower() for tag in curr_tags] 63 | else: 64 | curr_tags[0] = anime_tag.lower() 65 | 66 | for tag in curr_tags: 67 | acc = self.levenshtein_ratio(anime_name, tag, ratio_calc=True) 68 | if acc >= accuracy_threshold: 69 | resulting_dict[anime_id] = acc 70 | 71 | if len(curr_tags) == 1: 72 | caching_dict[curr_tags[0]] = anime_id 73 | else: 74 | caching_dict[f'{curr_tags[0]}|=|{curr_tags[1]}'] = anime_id 75 | 76 | resulting_dict = dict(sorted(resulting_dict.items(), key=lambda item: item[1], reverse=True)) 77 | 78 | 79 | # queue the record for removal if cache size > max allowed cache size 80 | if len(caching_queue) > self.cache_size: 81 | for k, v in caching_dict.items(): 82 | caching_queue.append({k : v}) 83 | 84 | while len(caching_queue) > self.cache_size: 85 | caching_queue.pop(0) 86 | 87 | caching_dict.clear() 88 | for d in caching_queue: 89 | caching_dict.update(d) 90 | 91 | # load data to the cache database 92 | with open(self.tag_cache_dict_dir, 'w') as wf: 93 | json.dump(caching_dict, wf, indent=4) 94 | 95 | 96 | resulting_list = [] 97 | id_dict = self.load_json(self.id_dict_dir) 98 | for anime_id, acc in resulting_dict.items(): 99 | resulting_list.append(id_dict[anime_id]) 100 | 101 | return resulting_list 102 | 103 | def levenshtein_ratio(self, s, t, ratio_calc = False): 104 | ''' 105 | levenshtein_ratio_and_distance: 106 | Calculates levenshtein distance between two strings. 107 | If ratio_calc = True, the function computes the 108 | levenshtein distance ratio of similarity between two strings 109 | For all i and j, distance[i,j] will contain the Levenshtein 110 | distance between the first i characters of s and the 111 | first j characters of t 112 | ''' 113 | 114 | 115 | if t.find(s) != -1: 116 | return 1.01 # name contains enterned characters 117 | 118 | rows = len(s)+1 119 | cols = len(t)+1 120 | distance = np.zeros((rows,cols),dtype = int) 121 | for i in range(1, rows): 122 | for k in range(1,cols): 123 | distance[i][0] = i 124 | distance[0][k] = k 125 | 126 | for col in range(1, cols): 127 | for row in range(1, rows): 128 | if s[row-1] == t[col-1]: 129 | cost = 0 130 | else: 131 | if ratio_calc == True: 132 | cost = 2 133 | else: 134 | cost = 1 135 | distance[row][col] = min(distance[row-1][col] + 1, 136 | distance[row][col-1] + 1, 137 | distance[row-1][col-1] + cost) 138 | if ratio_calc == True: 139 | Ratio = ((len(s)+len(t)) - distance[row][col]) / (len(s)+len(t)) 140 | return Ratio 141 | else: 142 | return "The strings are {} edits away".format(distance[row][col]) 143 | 144 | def load_json(self, filename): 145 | with open(filename, "r", encoding="utf-8") as f: 146 | data = json.load(f) 147 | return data 148 | 149 | def reverse_dict(self, d): 150 | return {value : key for (key, value) in d.items()} 151 | 152 | if __name__ == '__main__': 153 | import time 154 | start = time.time() 155 | se = SearchEngine() 156 | se.search_anime_database('react', accuracy_threshold=0.7) 157 | print(time.time() - start) -------------------------------------------------------------------------------- /AnilistPython/character.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from retrieve_data import ExtractInfo 4 | from retrieve_id import ExtractID 5 | 6 | class Character: 7 | def __init__(self, access_info, activated): 8 | self.extractInfo = ExtractInfo(access_info, activated) 9 | self.extractID = ExtractID(access_info, activated) 10 | 11 | 12 | def getCharacter(self, character_name, manual_select=False) -> dict: 13 | ''' 14 | Retrieve character info in the form of a json object. 15 | Retrieve json object will be reformatted in a easily accessable json obj. 16 | 17 | :param character_name: The name of the character 18 | :return: parsed dict containing the character's data 19 | :rtype: dict 20 | ''' 21 | 22 | character_id = self.getCharacterID(character_name, manual_select) 23 | if character_id == -1: 24 | return None 25 | 26 | data = self.extractInfo.character(character_id) 27 | character_lvl = data['data']['Character'] 28 | 29 | first_name = character_lvl['name']['first'] 30 | last_name = character_lvl['name']['last'] 31 | native_name = character_lvl['name']['native'] 32 | 33 | desc = character_lvl['description'] 34 | image = character_lvl['image']['large'] 35 | 36 | character_dict = {"first_name": first_name, 37 | "last_name": last_name, 38 | "native_name": native_name, 39 | "desc": desc, 40 | "image": image,} 41 | 42 | return character_dict 43 | 44 | 45 | def getCharacterWithID(self, character_id) -> dict: 46 | ''' 47 | Retrieve character info in the form of a json object. 48 | Retrieve json object will be reformatted in a easily accessable json obj. 49 | 50 | :param character_name: The name of the character 51 | :return: parsed dict containing the character's data 52 | :rtype: dict 53 | ''' 54 | data = self.extractInfo.character(character_id) 55 | character_lvl = data['data']['Character'] 56 | 57 | first_name = character_lvl['name']['first'] 58 | last_name = character_lvl['name']['last'] 59 | native_name = character_lvl['name']['native'] 60 | 61 | desc = character_lvl['description'] 62 | image = character_lvl['image']['large'] 63 | 64 | character_dict = {"first_name": first_name, 65 | "last_name": last_name, 66 | "native_name": native_name, 67 | "desc": desc, 68 | "image": image,} 69 | 70 | return character_dict 71 | 72 | 73 | def getCharacterID(self, character_name, manual_select=False): 74 | ''' 75 | Retrieves the character ID on Anilist. 76 | 77 | :param character_name: The character of the anime 78 | :return: The character's ID on Anilist. Returns -1 if an error is caught. 79 | :rtype: int 80 | ''' 81 | 82 | # if manual select is turned off ============================================================================ 83 | if manual_select == False: 84 | character_list = [] 85 | data = self.extractID.character(character_name) 86 | for i in range(len(data['data']['Page']['characters'])): 87 | first_name = data['data']['Page']['characters'][i]['name']['first'] 88 | last_name = data['data']['Page']['characters'][i]['name']['last'] 89 | character_list.append([first_name, last_name]) 90 | 91 | try: 92 | character_ID = data['data']['Page']['characters'][0]['id'] 93 | except IndexError: 94 | raise IndexError('Character Not Found') 95 | 96 | return character_ID 97 | 98 | 99 | # if manual select is turned on ============================================================================= 100 | elif manual_select == True: 101 | max_result = 0 102 | counter = 0 # number of displayed results from search 103 | data = self.extractID.character(character_name) 104 | for i in range(len(data['data']['Page']['characters'])): 105 | first_name = data['data']['Page']['characters'][i]['name']['first'] 106 | last_name = data['data']['Page']['characters'][i]['name']['last'] 107 | max_result = i + 1 108 | 109 | if last_name == None: 110 | print(f"{counter + 1}. {first_name}") 111 | elif first_name == None: 112 | print(f"{counter + 1}. {last_name}") 113 | else: 114 | print(f'{counter + 1}. {last_name}, {first_name}') 115 | counter += 1 116 | 117 | if counter > 1: # only one result found if counter == 1 118 | try: 119 | user_input = int(input("Please select the character that you are searching for in number: ")) 120 | except TypeError: 121 | print(f"Your input is incorrect! Please try again!") 122 | return -1 123 | 124 | if user_input > max_result or user_input <= 0: 125 | print("Your input does not correspound to any of the characters!") 126 | return -1 127 | elif counter == 0: 128 | print(f'No search result has been found for the character "{character_name}"!') 129 | return -1 130 | else: 131 | user_input = 1 132 | 133 | return data['data']['Page']['characters'][user_input - 1]['id'] 134 | 135 | else: 136 | # placeholder 137 | pass 138 | 139 | 140 | def displayCharacterInfo(self, character_name, manual_select=False): 141 | ''' 142 | Displays all character data. 143 | Auto formats the displayed version of the data. 144 | 145 | :param character_name: The character of the anime 146 | ''' 147 | 148 | char_dict = self.getCharacter(character_name, manual_select) 149 | if char_dict == None: 150 | print("Character Search Error") 151 | else: 152 | arr = ["First Name", "Last Name", "Japanese Name", "Description", "Image"] 153 | counter = 0 154 | 155 | print("========================================================================") 156 | print("============================ CHARACTER INFO ============================") 157 | for key, value in char_dict.items(): 158 | print(f"{arr[counter]}: {value}") 159 | counter += 1 -------------------------------------------------------------------------------- /AnilistPython/retrieve_id.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from query_strings import QSData 4 | qsObj = QSData() 5 | 6 | class ExtractID: 7 | def __init__(self, access, status): 8 | self.access = access 9 | self.status = status # Boolean value used for bots 10 | 11 | def anime(self, term, page = 1, perpage = 3): 12 | """ 13 | Search for anime by string (words) 14 | Page shows which page the result is currently on. 15 | Perpage represents the number of items retrieved. 16 | 17 | :param term str: Name of the anime 18 | :param page int: Which page for the program to start looking at. Default = 1 19 | :param perpage int: Number of retreived results from the page 20 | :return: A list of perpage num elements containing the dictionaries of each anime or None. 21 | :rtype: dict list or None 22 | """ 23 | 24 | preset = {"query": term, "page": page, "perpage": perpage} 25 | req = requests.post(self.access['apiurl'], 26 | headers=self.access['header'], 27 | json={'query': qsObj.animeIDQS, 'variables': preset}) 28 | 29 | if req.status_code != 200: 30 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 31 | 32 | try: 33 | extracted_data = json.loads(req.text) 34 | except ValueError: 35 | return None 36 | except TypeError: 37 | return None 38 | else: 39 | return extracted_data 40 | 41 | 42 | def character(self, term, page = 1, perpage = 3): 43 | """ 44 | Search for a character by string (words). 45 | Page shows which page the result is currently on. 46 | Perpage represents the number of items retrieved. 47 | 48 | :param term str: Name of the character 49 | :param page int: Which page for the program to start looking at. Default = 1 50 | :param perpage int: Number of retreived results from the page 51 | :return: A list of perpage num elements containing the dictionaries of each character or None. 52 | :rtype: dict list or None 53 | """ 54 | 55 | preset = {"query": term, "page": page, "perpage": perpage} 56 | req = requests.post(self.access['apiurl'], 57 | headers=self.access['header'], 58 | json={'query': qsObj.characterIDQS, 'variables': preset}) 59 | 60 | if req.status_code != 200: 61 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 62 | 63 | try: 64 | extracted_data = json.loads(req.text) 65 | except ValueError: 66 | return None 67 | except TypeError: 68 | return None 69 | else: 70 | return extracted_data 71 | 72 | def manga(self, term, page = 1, perpage = 3): 73 | """ 74 | Search for a Manga by string (words). 75 | Page shows which page the result is currently on. 76 | Perpage represents the number of items retrieved. 77 | 78 | :param term str: Name of the Manga 79 | :param page int: Which page for the program to start looking at. Default = 1 80 | :param perpage int: Number of retreived results from the page 81 | :return: A list of perpage num elements containing the dictionaries of each Manga or None. 82 | :rtype: dict list or None 83 | """ 84 | 85 | preset = {"query": term, "page": page, "perpage": perpage} 86 | req = requests.post(self.access['apiurl'], 87 | headers=self.access['header'], 88 | json={'query': qsObj.mangaIDQS, 'variables': preset}) 89 | 90 | if req.status_code != 200: 91 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 92 | 93 | try: 94 | extracted_data = json.loads(req.text) 95 | except ValueError: 96 | return None 97 | except TypeError: 98 | return None 99 | else: 100 | return extracted_data 101 | 102 | def staff(self, term, page = 1, perpage = 3): 103 | """ 104 | Search for a staff by string (words). 105 | Page shows which page the result is currently on. 106 | Perpage represents the number of items retrieved. 107 | 108 | :param term str: Name of the staff 109 | :param page int: Which page for the program to start looking at. Default = 1 110 | :param perpage int: Number of retreived results from the page 111 | :return: A list of perpage num elements containing the dictionaries of each staff or None. 112 | :rtype: dict list or None 113 | """ 114 | 115 | preset = {"query": term, "page": page, "perpage": perpage} 116 | req = requests.post(self.access['apiurl'], 117 | headers=self.access['header'], 118 | json={'query': qsObj.staffIDQS, 'variables': preset}) 119 | 120 | if req.status_code != 200: 121 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 122 | 123 | try: 124 | extracted_data = json.loads(req.text) 125 | except ValueError: 126 | return None 127 | except TypeError: 128 | return None 129 | else: 130 | return extracted_data 131 | 132 | def studio(self, term, page = 1, perpage = 3): 133 | """ 134 | Search for a studio by string (words). 135 | Page shows which page the result is currently on. 136 | Perpage represents the number of items retrieved. 137 | 138 | :param term str: Name of the studio 139 | :param page int: Which page for the program to start looking at. Default = 1 140 | :param perpage int: Number of retreived results from the page 141 | :return: A list of perpage num elements containing the dictionaries of each studio or None. 142 | :rtype: dict list or None 143 | """ 144 | 145 | preset = {"query": term, "page": page, "perpage": perpage} 146 | req = requests.post(self.access['apiurl'], 147 | headers=self.access['header'], 148 | json={'query': qsObj.studioIDQS, 'variables': preset}) 149 | 150 | if req.status_code != 200: 151 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 152 | 153 | try: 154 | extracted_data = json.loads(req.text) 155 | except ValueError: 156 | return None 157 | except TypeError: 158 | return None 159 | else: 160 | return extracted_data -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnilistPython 2 | 3 | ![example workflow](https://github.com/ReZeroE/AnilistPython/actions/workflows/github-actions-demo.yml/badge.svg) 4 | ![downloads](https://img.shields.io/pypi/dm/AnilistPython) 5 | ![licence](https://img.shields.io/github/license/ReZeroE/AnilistPython) 6 | ![Test](https://pepy.tech/badge/anilistpython) 7 | 8 | AniList Python library (anilist.co APIv2 wrapper) that allows you to **easily search up and retrieve anime, manga, animation studio, and character information.** This library is both beginner-friendly and offers the freedom for more experienced developers to interact with the retrieved information. Provides bot support. 9 | 10 | ![alt text](https://i.imgur.com/uGzW7vr.jpg) 11 | 12 | ## Version 0.1.3 Overview 13 | This recent update for AnilistPython has resulted in a moderate change in the library's archetecture for increased efficiency and speed. Various features have also been added to the library. Listed below are some of the main additions and alterations made to the library. 14 | 15 | **New features**: 16 | 1. Anime search by genre, year, and/or average score (finally!) 17 | 2. Offline anime retrieval support for anime - BETA 18 | 3. Manga search support 19 | 4. Package setup assitant (helps with package setup if any required package is missing) 20 | 21 | Optimization and updates: 22 | 1. The lib now has its own prebuilt anime database! 23 | 2. Anime, manga, and character search functions have all been optimized, making searches even faster! 24 | 3. Improved deepsearch feature in `.get_anime()`. 25 | 4. Result's manual-select feature is now a parameter instead of a seperate function (see usage below). 26 | 27 | 28 | ## How to use? 29 | **Step One:** Library Installation 30 | ``` python 31 | pip install AnilistPython==0.1.3 32 | ``` 33 | **Step Two:** Instance Creation 34 | ```python 35 | from AnilistPython import Anilist 36 | anilist = Anilist() 37 | ``` 38 | **Step Three**: Usage 39 | 40 | The AnilistPython library has been split into three distinct sections. Each section has a different set of functions used for retrieving data in that category. Please visit the full documentation for more info or skip to the **General Function Overview** section below for usage. 41 | - **Anime** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Anime)) 42 | - **Manga** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Manga)) 43 | - **Character** - ([Documentation](https://github.com/ReZeroE/AnilistPython/wiki/Character)) 44 | 45 | ## General Function Overview 46 | The following functions are supported by AnilistPyhon version 0.1.3. Only the default parameter has been displayed below. For more information, visit the [full documentation](https://github.com/ReZeroE/AnilistPython/wiki). 47 | ```python 48 | # ANIME 49 | anilist.get_anime("Owari no Seraph") # returns a dictionary containing info about owari no seraph 50 | anilist.get_anime_with_id(126830) # returns a dictionary with Code Geass (ID:126830) info 51 | anilist.get_anime_id("ReZero") # returns Re:Zero's ID on Anilist 52 | anilist.print_anime_info("Madoka Magica") # prints all information regarding the anime Madoka Magica 53 | 54 | # returns a list of anime with the given restrictions 55 | anilist.search_anime(genre=['Action', 'Adventure', 'Drama'], year=[2016, 2019], score=range(80, 95)) 56 | 57 | #CHARACTER 58 | anilist.get_character("Emilia") # returns a dictionary containing the info about Emilia-tan 59 | anilist.get_character_with_id(13701) # returns a dictionary with Misaka Mikoto (ID:13701) info 60 | anilist.get_character_id("Milim") # returns character Milim's ID on Anilist 61 | anilist.print_anime_info("Kirito") # prints all information regarding the character Kirito 62 | 63 | # MANGA 64 | anilist.get_manga("Seraph of the End") # returns a dictionary containing info about seraph of the end 65 | anilist.get_manga_with_id(113399) # returns a dictionary with Tearmoon (ID:113399) info 66 | anilist.get_manga_id("Tearmoon Empire") # returns Tearmoon Empire's ID on Anilist (manga) 67 | anilist.print_manga_info("Tensei Slime") # prints all information regarding the manga Tensei Slime 68 | ``` 69 | 70 | Note: The feature for manully selecting the top three search results in the terminal is now controlled by a parameter (`manual_select`) in .get functions. For more information, please visit the full documentation. A sample program that has manual select enabled would be: 71 | 72 | ```python 73 | anilist.get_anime("Owari no Seraph", manual_select=True) 74 | ``` 75 | 76 | If an error occurs while running AnilistPython, please refer the to the [Error Fixes](https://github.com/ReZeroE/AnilistPython#error-fixes) section. 77 | 78 | ## Discord Bot Support 79 | AnilistPython was originially designed to support various Discord Bot features in relation to anime, but throughout the course of its development, more features became available for a wide range of applications other than Discord bots. With that been said, the current version of AnilistPython has further optimized its functions for bot support. From the pre-formatted JSON file upon data retrieval to offline database support (see full documentation), it is now able to be implemented in bots with ease. 80 | 81 | Upcoming AnilistPython Version 0.2.0 will provide functions to generate pre-formated Discord embeds (Anime, Manga, Character embeds) as well as other features that make AnilistPython bot implementations easy to use. 82 | 83 | Sample anime discord bot supported by AnilistPython V0.1.3: [Anime C.C. Discord Bot](https://github.com/ReZeroE/Anime-Discord-Bot) 84 | 85 | Note: Please make sure that parameter `manual_select` has not been set to True in bot implementations. (False by default) 86 | 87 | ## Upcoming Version 0.1.4 (Releasing before 1/1/2023) 88 | The upcoming version 0.1.4 of the AnilistPython lib will include the following features and fixes. 89 | - SQLite3 database support 90 | - Provides functions to update the local database 91 | - Updated and optimized local DB search functions, reducing the search time by >95%. 92 | - New categories for DB search 93 | - Partial Discord bot embed support 94 | - Fixes to known errors 95 | - Missing requirements in requirements.txt 96 | - Failed auto setup module 97 | - New data fields for `.get_anime()`: 98 | - isAdult 99 | - popularity 100 | - isLicensed 101 | - countryOfOrigin 102 | - duration 103 | - updatedAt 104 | - source 105 | - siteUrl 106 | 107 | ## Error Fixes 108 | 1. `ModuleNotFoundError` for `requests` 109 | - This error has been reported and will be fixed in the upcoming version. 110 | - To fix the error, run `pip install requests` or `pip3 install requests` 111 | 112 | If you issue is not present here, feel free to [open a new issue](https://github.com/ReZeroE/AnilistPython/issues) for help! 113 | 114 | ## Credits 115 | Lead Developer: Kevin L. (ReZeroE) 116 | 117 | Special thanks to the AniList's ApiV2 GraphQL Dev team for making this possible. 118 | -------------------------------------------------------------------------------- /AnilistPython/databases/database_anime_retrieval.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | 5 | class DatabaseSearcher: 6 | def __init__(self): 7 | self.dir_path = os.path.dirname(os.path.realpath(__file__)) 8 | self.storage_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'anime_database_files') 9 | 10 | self.id_dict_dir = os.path.join(self.storage_dir, "anime_by_id.json") 11 | self.genre_dict_dir = os.path.join(self.storage_dir, "anime_by_genre.json") 12 | self.score_dict_dir = os.path.join(self.storage_dir, "anime_by_score.json") 13 | self.year_dict_dir = os.path.join(self.storage_dir, "anime_by_year.json") 14 | 15 | 16 | 17 | def anime_mix_search(self, retrieve_count=None, genre=None, year=None, score=None, id_only=False) -> list: 18 | ''' 19 | Search anime with the given retriction/parameters. 20 | 21 | :param retrieve_count: Max number of anime records to be retrieved. Retrieve all (-1) by default. 22 | :param genre: The genre of the anime in str or list of str (i.e. 'Action' or ['Action', 'Romance']) 23 | :param year: The year of the anime in str or list of str (i.e. '2012' or ['2012', '2013']) 24 | :param score: The score of the anime in str as in range (i.e. '50' or '50-60') 25 | :param id_only: Only retrieve the ID of the anime. False by default. 26 | 27 | :return: a list of dictionaries containing the anime within the given restrictions. 28 | :rtype: list 29 | ''' 30 | if genre == None and year == None and score == None: 31 | print('Warning -> You have not specified any restrictions (parameters) for this anime search.') 32 | print("Please use .getAnime(anime_name) if you wish to retrieve an anime's data without any retrictive parameters") 33 | sys.exit(1) 34 | 35 | options_list = [] 36 | 37 | # Genre Restriction 38 | genre_option_list = [] 39 | if isinstance(genre, str): 40 | genre_dict = self.load_json(self.genre_dict_dir) 41 | if genre == 'scifi': genre = 'sci-fi' 42 | try: 43 | genre_option_list = genre_dict[genre.strip().lower()] 44 | except KeyError: 45 | raise KeyError(f'Incorrect genre category -> {genre.strip().lower()}. For the full genre list, please visit .') 46 | 47 | elif isinstance(genre, list): 48 | genre_dict = self.load_json(self.genre_dict_dir) 49 | 50 | first = True 51 | for g in genre: 52 | if g == 'scifi': g = 'sci-fi' 53 | try: 54 | temp_list = genre_dict[g.strip().lower()] 55 | except KeyError: 56 | raise KeyError(f'Incorrect genre category -> {g.strip().lower()}. For the full genre list, please visit .') 57 | 58 | if first: 59 | genre_option_list = temp_list 60 | first = False 61 | else: 62 | genre_option_list = list(set(temp_list) & set(genre_option_list)) 63 | elif genre != None: 64 | print("Error -> paramter genre needs to be a str or list. (i.e. 'Action' or ['Action', 'Romance'])") 65 | sys.exit(1) 66 | 67 | 68 | # Year Retriction 69 | temp_list = [] 70 | year_option_list = [] 71 | if isinstance(year, int): 72 | year = str(year) 73 | if isinstance(year, str): 74 | year_dict = self.load_json(self.year_dict_dir) 75 | year_option_list += year_dict[year] 76 | elif isinstance(year, list): 77 | if len(year) > 0 and isinstance(year[0], int): 78 | year = [str(y) for y in year] 79 | 80 | year_dict = self.load_json(self.year_dict_dir) 81 | for y in year: 82 | 83 | try: 84 | temp_list = year_dict[y] 85 | year_option_list += year_dict[y] 86 | except KeyError: 87 | raise KeyError(f'Incorrect year value entered -> {y}') 88 | 89 | 90 | elif year != None: 91 | print("Error -> paramter year needs to be a str or list. (i.e. '2012' or ['2012', '2013'])") 92 | sys.exit(1) 93 | 94 | 95 | # Avg Score Striction 96 | score_option_list = [] 97 | if isinstance(score, int): 98 | score = str(score) 99 | if isinstance(score, str): 100 | score_dict = self.load_json(self.score_dict_dir) 101 | 102 | if score.find('-') != -1: 103 | min_score = score.split('-')[0] 104 | max_score = score.split('-')[1] 105 | 106 | try: 107 | min_score = int(min_score) 108 | max_score = int(max_score) 109 | except Exception: 110 | print(f'parameter score incorrect.') 111 | raise Exception 112 | 113 | for key, val in score_dict.items(): 114 | if int(key) > max_score: 115 | break 116 | 117 | if int(key) > min_score: 118 | score_option_list += val 119 | else: 120 | try: 121 | score_option_list += score_dict[score] 122 | except Exception: 123 | print(f'Parameter score incorrect -> only value 0-100 are accepted') 124 | raise Exception 125 | elif isinstance(score, range): 126 | 127 | min_score = score.start 128 | max_score = score.stop 129 | 130 | score_dict = self.load_json(self.score_dict_dir) 131 | for key, val in score_dict.items(): 132 | if int(key) > max_score: 133 | break 134 | 135 | if int(key) > min_score: 136 | score_option_list += val 137 | 138 | 139 | 140 | resulting_list = [] 141 | if genre != None and year != None and score != None: 142 | resulting_list = list(set(genre_option_list) & set(year_option_list) & set(score_option_list)) 143 | 144 | elif genre != None and year != None: 145 | resulting_list = list(set(genre_option_list) & set(year_option_list)) 146 | elif genre != None and score != None: 147 | resulting_list = list(set(genre_option_list) & set(score_option_list)) 148 | elif year != None and score != None: 149 | resulting_list = list(set(year_option_list) & set(score_option_list)) 150 | 151 | elif genre != None: 152 | resulting_list = genre_option_list 153 | elif year != None: 154 | resulting_list = year_option_list 155 | elif score != None: 156 | resulting_list = score_option_list 157 | 158 | 159 | return_list = [] 160 | 161 | if id_only == False: 162 | id_dict = self.load_json(self.id_dict_dir) 163 | for anime_id in resulting_list: 164 | return_list.append(id_dict[anime_id]) 165 | else: 166 | return resulting_list 167 | 168 | return return_list 169 | 170 | 171 | 172 | def load_json(self, filename): 173 | with open(filename, "r", encoding="utf-8") as f: 174 | data = json.load(f) 175 | return data 176 | 177 | -------------------------------------------------------------------------------- /AnilistPython/manga.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from retrieve_data import ExtractInfo 4 | from retrieve_id import ExtractID 5 | 6 | class Manga: 7 | def __init__(self, access_info, activated=True): 8 | self.extractInfo = ExtractInfo(access_info, activated) 9 | self.extractID = ExtractID(access_info, activated) 10 | 11 | 12 | def getManga(self, manga_name, manual_select=False): 13 | ''' 14 | Retrieve character info in the form of a json object. 15 | Retrieve json object will be reformatted in a easily accessable json obj. 16 | 17 | :param character_name: The name of the character 18 | :return: parsed dict containing the character's data 19 | :rtype: dict 20 | ''' 21 | manga_dict = {} 22 | 23 | manga_id = self.getMangaID(manga_name, manual_select) 24 | if manga_id == -1: 25 | return None 26 | 27 | data = self.extractInfo.manga(manga_id) 28 | media_lvl = data['data']['Media'] 29 | 30 | manga_dict['name_romaji'] = media_lvl['title']['romaji'] 31 | manga_dict['name_english'] = media_lvl['title']['english'] 32 | 33 | start_year = media_lvl['startDate']['year'] 34 | start_month = media_lvl['startDate']['month'] 35 | start_day = media_lvl['startDate']['day'] 36 | 37 | end_year = media_lvl['endDate']['year'] 38 | end_month = media_lvl['endDate']['month'] 39 | end_day = media_lvl['endDate']['day'] 40 | 41 | manga_dict['starting_time'] = f'{start_month}/{start_day}/{start_year}' 42 | manga_dict['ending_time'] = f'{end_month}/{end_day}/{end_year}' 43 | 44 | manga_dict['cover_image'] = media_lvl['coverImage']['large'] 45 | manga_dict['banner_image'] = media_lvl['bannerImage'] 46 | 47 | manga_dict['release_format'] = media_lvl['format'] 48 | manga_dict['release_status'] = media_lvl['status'] 49 | 50 | manga_dict['chapters'] = media_lvl['chapters'] 51 | manga_dict['volumes'] = media_lvl['volumes'] 52 | 53 | manga_dict['desc'] = media_lvl['description'] 54 | 55 | manga_dict['average_score'] = media_lvl['averageScore'] 56 | manga_dict['mean_score'] = media_lvl['meanScore'] 57 | 58 | manga_dict['genres'] = media_lvl['genres'] 59 | manga_dict['synonyms'] = media_lvl['synonyms'] 60 | 61 | return manga_dict 62 | 63 | 64 | def getMangaWithID(self, manga_id) -> dict: 65 | ''' 66 | Retrieve character info in the form of a json object. 67 | Retrieve json object will be reformatted in a easily accessable json obj. 68 | 69 | :param character_name: The name of the character 70 | :return: parsed dict containing the character's data 71 | :rtype: dict 72 | ''' 73 | manga_dict = {} 74 | 75 | data = self.extractInfo.manga(manga_id) 76 | media_lvl = data['data']['Media'] 77 | 78 | manga_dict['name_romaji'] = media_lvl['title']['romaji'] 79 | manga_dict['name_english'] = media_lvl['title']['english'] 80 | 81 | start_year = media_lvl['startDate']['year'] 82 | start_month = media_lvl['startDate']['month'] 83 | start_day = media_lvl['startDate']['day'] 84 | 85 | end_year = media_lvl['endDate']['year'] 86 | end_month = media_lvl['endDate']['month'] 87 | end_day = media_lvl['endDate']['day'] 88 | 89 | manga_dict['starting_time'] = f'{start_month}/{start_day}/{start_year}' 90 | manga_dict['ending_time'] = f'{end_month}/{end_day}/{end_year}' 91 | 92 | manga_dict['cover_image'] = media_lvl['coverImage']['large'] 93 | manga_dict['banner_image'] = media_lvl['bannerImage'] 94 | 95 | manga_dict['release_format'] = media_lvl['format'] 96 | manga_dict['release_status'] = media_lvl['status'] 97 | 98 | manga_dict['chapters'] = media_lvl['chapters'] 99 | manga_dict['volumes'] = media_lvl['volumes'] 100 | 101 | manga_dict['desc'] = media_lvl['description'] 102 | 103 | manga_dict['average_score'] = media_lvl['averageScore'] 104 | manga_dict['mean_score'] = media_lvl['meanScore'] 105 | 106 | manga_dict['genres'] = media_lvl['genres'] 107 | manga_dict['synonyms'] = media_lvl['synonyms'] 108 | 109 | return manga_dict 110 | 111 | 112 | def getMangaID(self, manga_name, manual_select=False): 113 | ''' 114 | Retrieves the character ID on Anilist. 115 | 116 | :param character_name: The character of the Manga 117 | :return: The character's ID on Anilist. Returns -1 if an error is caught. 118 | :rtype: int 119 | ''' 120 | 121 | if manual_select == False: 122 | manga_list = [] 123 | data = self.extractID.manga(manga_name) 124 | for i in range(len(data['data']['Page']['media'])): 125 | curr_manga = data['data']['Page']['media'][i]['title']['romaji'] 126 | manga_list.append(curr_manga) 127 | 128 | print(manga_list) 129 | 130 | # returns the first manga found 131 | try: 132 | manga_ID = data['data']['Page']['media'][0]['id'] 133 | except IndexError: 134 | raise IndexError('Manga Not Found') 135 | 136 | return manga_ID 137 | 138 | 139 | elif manual_select == True: 140 | max_result = 0 141 | counter = 0 # number of displayed results from search 142 | data = self.extractID.manga(manga_name) 143 | for i in range(len(data['data']['Page']['media'])): 144 | curr_manga = data['data']['Page']['media'][i]['title']['romaji'] 145 | print(f"{counter + 1}. {curr_manga}") 146 | max_result = i + 1 147 | counter += 1 148 | 149 | if counter > 1: # only one result found if counter == 1 150 | try: 151 | user_input = int(input("Please select the manga that you are searching for in number: ")) 152 | except TypeError or ValueError: 153 | print(f"Your input is incorrect! Please try again!") 154 | return -1 155 | 156 | if user_input > max_result or user_input <= 0: 157 | print("Your input does not correspound to any of the manga displayed!") 158 | return -1 159 | elif counter == 0: 160 | print(f'No search result has been found for the manga "{manga_name}"!') 161 | return -1 162 | else: 163 | user_input = 1 164 | 165 | return data['data']['Page']['media'][user_input - 1]['id'] 166 | 167 | else: 168 | # placeholder 169 | pass 170 | 171 | 172 | def displayMangaInfo(self, manga_name, manual_select=False): 173 | ''' 174 | Displays all character data. 175 | Auto formats the displayed version of the data. 176 | 177 | :param character_name: The character of the Manga 178 | ''' 179 | 180 | manga_dict = self.getManga(manga_name, manual_select) 181 | if manga_dict == None: 182 | print("Manga Search Error - Manga Not Found") 183 | else: 184 | arr = ['name_romaji', 'name_english', 'starting_time', 'ending_time', 'cover_image', 'banner_image', \ 185 | 'release_format', 'release_status', 'chapters', 'volumes', 'desc', 'average_score', 'mean_score', \ 186 | 'genres', 'synonyms'] 187 | counter = 0 188 | 189 | print('\n') 190 | print("========================================================================") 191 | print("============================== MANGA INFO ==============================") 192 | for key, value in manga_dict.items(): 193 | print(f"{arr[counter]}: {value}") 194 | counter += 1 -------------------------------------------------------------------------------- /AnilistPython/retrieve_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | import json 4 | import requests 5 | from .query_strings import QSData 6 | qsObj = QSData() 7 | 8 | class ExtractInfo: 9 | def __init__(self, access, status): 10 | self.access = access 11 | self.status = status # Boolean value used for bots 12 | 13 | self.logfile = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'error-log.txt') 14 | 15 | def anime(self, anime_id): 16 | """ 17 | Function to extract anime info provided the anime ID num. 18 | Returns None if activte status is False. 19 | 20 | :param anime_id: input anime ID number 21 | :return: dict or None 22 | :rtype: dict or NoneType 23 | """ 24 | 25 | if self.status == False: 26 | raise Exception("Current function status is False.") 27 | 28 | id_val = {"id": anime_id} 29 | req = requests.post(self.access['apiurl'], 30 | headers=self.access['header'], 31 | json={'query': qsObj.animeInfoQS, 'variables': id_val}) 32 | 33 | if req.status_code != 200: 34 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 35 | 36 | try: 37 | extracted_data = json.loads(req.text) 38 | except ValueError: 39 | return None 40 | except TypeError: 41 | return None 42 | else: 43 | return extracted_data 44 | 45 | def manga(self, manga_id): 46 | """ 47 | The function to retrieve an manga's information. 48 | Returns None if activte status is False. 49 | 50 | :param int manga_id: the manga's ID 51 | :return: dict or None 52 | :rtype: dict or NoneType 53 | """ 54 | 55 | if self.status == False: 56 | raise Exception("Current function status is False.") 57 | 58 | id_val = {"id": manga_id} 59 | req = requests.post(self.access['apiurl'], 60 | headers=self.access['header'], 61 | json={'query': qsObj.mangaInfoQS, 'variables': id_val}) 62 | 63 | if req.status_code != 200: 64 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 65 | 66 | try: 67 | extracted_data = json.loads(req.text) 68 | except ValueError: 69 | return None 70 | except TypeError: 71 | return None 72 | else: 73 | return extracted_data 74 | 75 | def staff(self, staff_id): 76 | """ 77 | The function to retrieve a staff's information. 78 | Returns None if activte status is False. 79 | 80 | :param int staff_id: the anime's ID 81 | :return: dict or None 82 | :rtype: dict or NoneType 83 | """ 84 | 85 | if self.status == False: 86 | raise Exception("Current function status is False.") 87 | 88 | id_val = {"id": staff_id} 89 | req = requests.post(self.access['apiurl'], 90 | headers=self.access['header'], 91 | json={'query': qsObj.staffInfoQS, 'variables': id_val}) 92 | 93 | if req.status_code != 200: 94 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 95 | 96 | try: 97 | extracted_data = json.loads(req.text) 98 | except ValueError: 99 | return None 100 | except TypeError: 101 | return None 102 | else: 103 | return extracted_data 104 | 105 | def studio(self, studio_id): 106 | """ 107 | The function to retrieve a studio's information. 108 | Returns None if activte status is False. 109 | 110 | :param int studio_id: the studio's ID 111 | :return: dict or None 112 | :rtype: dict or NoneType 113 | """ 114 | 115 | if self.status == False: 116 | raise Exception("Current function status is False.") 117 | 118 | id_val = {"id": studio_id} 119 | req = requests.post(self.access['apiurl'], 120 | headers=self.access['header'], 121 | json={'query': qsObj.studioInfoQS, 'variables': id_val}) 122 | 123 | if req.status_code != 200: 124 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 125 | 126 | try: 127 | extracted_data = json.loads(req.text) 128 | except ValueError: 129 | return None 130 | except TypeError: 131 | return None 132 | else: 133 | return extracted_data 134 | 135 | def character(self, character_id): 136 | """ 137 | The function to retrieve a character's information. 138 | Returns None if activte status is False. 139 | 140 | :param int character_id: the character's ID 141 | :return: dict or None 142 | :rtype: dict or NoneType 143 | """ 144 | 145 | if self.status == False: 146 | raise Exception("Current function status is False.") 147 | 148 | id_val = {"id": character_id} 149 | req = requests.post(self.access['apiurl'], 150 | headers=self.access['header'], 151 | json={'query': qsObj.characterInfoQS, 'variables': id_val}) 152 | 153 | if req.status_code != 200: 154 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 155 | 156 | try: 157 | extracted_data = json.loads(req.text) 158 | except ValueError: 159 | return None 160 | except TypeError: 161 | return None 162 | else: 163 | return extracted_data 164 | 165 | def review(self, review_id, html): 166 | """ 167 | Function that retrieve review information. HTML may be set to True of False. 168 | Returns None if activte status is False. 169 | 170 | :param review_id: the ID of the review 171 | :param html: boolean to the format of the return value 172 | :return: json obj -> review info 173 | :rtype: json obj -> review info 174 | """ 175 | 176 | id_val = {"id": review_id, "html": html} 177 | req = requests.post(self.access['apiurl'], 178 | headers=self.access['header'], 179 | json={'query': qsObj.reviewInfoQS, 'variables': id_val}) 180 | 181 | try: 182 | extracted_data = json.loads(req.text) 183 | except ValueError: 184 | return None 185 | except TypeError: 186 | return None 187 | else: 188 | return extracted_data 189 | 190 | def user_activity(self, page, perpage): 191 | """ 192 | A Function to get the activity of the currently logged in user. 193 | 194 | :param page: the page of user activity 195 | :param perpage: how many items per page 196 | """ 197 | if self.status == False: 198 | raise Exception("Current function status is False.") 199 | 200 | token = self.access['token'] 201 | if not token: 202 | return None 203 | else: 204 | headers = { 205 | "Authorization": f"Bearer {token}", 206 | "Content-Type": "application/json", 207 | "Accept": "application/json" 208 | } 209 | req = requests.post(self.access['apiurl'], headers=headers, json={'query': qsObj.user_get_idQS, 'variables': {}}) 210 | if req.status_code != 200: 211 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 212 | else: 213 | try: 214 | extracted_user = json.loads(req.text) 215 | id_val2 = {"id": extracted_user["data"]["Viewer"]["id"], "page": page, "perPage": perpage} 216 | req2 = requests.post(self.access['apiurl'], headers=headers, json={'query': qsObj.user_activyQS, 'variables': id_val2}) 217 | if req2.status_code != 200: 218 | raise Exception(f"Data post unsuccessful. ({req.status_code})") 219 | else: 220 | try: 221 | extracted_data = json.loads(req2.text) 222 | except ValueError: 223 | return None 224 | except TypeError: 225 | return None 226 | else: 227 | return extracted_data 228 | except: 229 | return None -------------------------------------------------------------------------------- /AnilistPython/anime.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from retrieve_data import ExtractInfo 4 | from retrieve_id import ExtractID 5 | 6 | class Anime: 7 | def __init__(self, access_info, activated=True): 8 | self.extractInfo = ExtractInfo(access_info, activated) 9 | self.extractID = ExtractID(access_info, activated) 10 | 11 | 12 | def getAnime(self, anime_name, manual_select=False) -> dict: 13 | ''' 14 | Retrieve anime info in the form of a json object. 15 | Retrieve json object will be reformatted in a easily accessable json obj. 16 | 17 | :param anime_name: The name of the anime 18 | :return: parsed dict containing the anime's data 19 | :rtype: dict 20 | ''' 21 | 22 | anime_id = self.getAnimeID(anime_name, manual_select) 23 | if anime_id == -1: 24 | return None 25 | 26 | data = self.extractInfo.anime(anime_id) 27 | media_lvl = data['data']['Media'] 28 | 29 | name_romaji = media_lvl['title']['romaji'] 30 | name_english = media_lvl['title']['english'] 31 | 32 | start_year = media_lvl['startDate']['year'] 33 | start_month = media_lvl['startDate']['month'] 34 | start_day = media_lvl['startDate']['day'] 35 | 36 | end_year = media_lvl['endDate']['year'] 37 | end_month = media_lvl['endDate']['month'] 38 | end_day = media_lvl['endDate']['day'] 39 | 40 | starting_time = f'{start_month}/{start_day}/{start_year}' 41 | ending_time = f'{end_month}/{end_day}/{end_year}' 42 | 43 | cover_image = media_lvl['coverImage']['large'] 44 | banner_image = media_lvl['bannerImage'] 45 | 46 | airing_format = media_lvl['format'] 47 | airing_status = media_lvl['status'] 48 | airing_episodes = media_lvl['episodes'] 49 | season = media_lvl['season'] 50 | 51 | desc = media_lvl['description'] 52 | 53 | average_score = media_lvl['averageScore'] 54 | genres = media_lvl['genres'] 55 | 56 | next_airing_ep = media_lvl['nextAiringEpisode'] 57 | 58 | anime_dict = {"name_romaji": name_romaji, 59 | "name_english": name_english, 60 | "starting_time": starting_time, 61 | "ending_time": ending_time, 62 | "cover_image": cover_image, 63 | "banner_image": banner_image, 64 | "airing_format": airing_format, 65 | "airing_status": airing_status, 66 | "airing_episodes": airing_episodes, 67 | "season": season, 68 | "desc": desc, 69 | "average_score": average_score, 70 | "genres": genres, 71 | "next_airing_ep": next_airing_ep,} 72 | 73 | return anime_dict 74 | 75 | 76 | def getAnimeWithID(self, anime_id) -> dict: 77 | ''' 78 | Retrieve anime info in the form of a json object. 79 | Retrieve json object will be reformatted in a easily accessable json obj. 80 | 81 | :param anime_name: The name of the anime 82 | :return: parsed dict containing the anime's data 83 | :rtype: dict 84 | ''' 85 | 86 | data = self.extractInfo.anime(anime_id) 87 | media_lvl = data['data']['Media'] 88 | 89 | name_romaji = media_lvl['title']['romaji'] 90 | name_english = media_lvl['title']['english'] 91 | 92 | start_year = media_lvl['startDate']['year'] 93 | start_month = media_lvl['startDate']['month'] 94 | start_day = media_lvl['startDate']['day'] 95 | 96 | end_year = media_lvl['endDate']['year'] 97 | end_month = media_lvl['endDate']['month'] 98 | end_day = media_lvl['endDate']['day'] 99 | 100 | starting_time = f'{start_month}/{start_day}/{start_year}' 101 | ending_time = f'{end_month}/{end_day}/{end_year}' 102 | 103 | cover_image = media_lvl['coverImage']['large'] 104 | banner_image = media_lvl['bannerImage'] 105 | 106 | airing_format = media_lvl['format'] 107 | airing_status = media_lvl['status'] 108 | airing_episodes = media_lvl['episodes'] 109 | season = media_lvl['season'] 110 | 111 | desc = media_lvl['description'] 112 | 113 | average_score = media_lvl['averageScore'] 114 | genres = media_lvl['genres'] 115 | 116 | next_airing_ep = media_lvl['nextAiringEpisode'] 117 | 118 | anime_dict = {"name_romaji": name_romaji, 119 | "name_english": name_english, 120 | "starting_time": starting_time, 121 | "ending_time": ending_time, 122 | "cover_image": cover_image, 123 | "banner_image": banner_image, 124 | "airing_format": airing_format, 125 | "airing_status": airing_status, 126 | "airing_episodes": airing_episodes, 127 | "season": season, 128 | "desc": desc, 129 | "average_score": average_score, 130 | "genres": genres, 131 | "next_airing_ep": next_airing_ep,} 132 | 133 | return anime_dict 134 | 135 | 136 | def getAnimeID(self, anime_name, manual_select=False): 137 | ''' 138 | Retrieves the anime ID on Anilist. 139 | 140 | :param anime_name: The name of the anime 141 | :return: The anime's ID on Anilist. Returns -1 if an error is caught. 142 | :rtype: int 143 | ''' 144 | 145 | # if manual select is turned off ============================================================================ 146 | if manual_select == False: 147 | anime_list = [] 148 | data = self.extractID.anime(anime_name) 149 | for i in range(len(data['data']['Page']['media'])): 150 | curr_anime = data['data']['Page']['media'][i]['title']['romaji'] 151 | anime_list.append(curr_anime) 152 | 153 | # returns the first anime found 154 | try: 155 | anime_ID = data['data']['Page']['media'][0]['id'] 156 | except IndexError: 157 | raise IndexError('Anime Not Found') 158 | 159 | return anime_ID 160 | 161 | # if manual select is turned on ============================================================================= 162 | elif manual_select == True: 163 | data = self.extractID.anime(anime_name) 164 | max_result = 0 165 | counter = 0 # number of displayed results from search 166 | for i in range(len(data['data']['Page']['media'])): 167 | curr_anime = data['data']['Page']['media'][i]['title']['romaji'] 168 | print(f"{counter + 1}. {curr_anime}") 169 | max_result = i + 1 170 | counter += 1 171 | 172 | if counter > 1: # only one result found if counter == 1 173 | try: 174 | user_input = int(input("Please select the anime that you are searching for in number: ")) 175 | except TypeError: 176 | print(f"Your input is incorrect! Please try again!") 177 | return -1 178 | 179 | if user_input > max_result or user_input <= 0: 180 | print("Your input does not correspound to any of the anime displayed!") 181 | return -1 182 | elif counter == 0: 183 | print(f'No search result has been found for the anime "{anime_name}"!') 184 | return -1 185 | else: 186 | user_input = 1 187 | 188 | return data['data']['Page']['media'][user_input - 1]['id'] 189 | 190 | else: 191 | # placeholder 192 | pass 193 | 194 | 195 | def displayAnimeInfo(self, anime_name, manual_select=False): 196 | ''' 197 | Displays all anime data. 198 | Auto formats the displayed version of the data. 199 | 200 | :param anime_name: The name of the anime 201 | ''' 202 | 203 | ani_dict = self.getAnime(anime_name, manual_select) 204 | if ani_dict == None: 205 | print('Name Error') 206 | else: 207 | arr = ["Name(romaji)", "Name(Eng)", "Started Airing On", "Ended On", "Cover Image", "Banner Image", 208 | "Airing Format", "Airing Status", "Total Ep Count", "Season", "Description", "Ave. Score", "Genres", 209 | "Next Ep Airing Date"] 210 | counter = 0 211 | 212 | print("====================================================================") 213 | print("============================ ANIME INFO ============================") 214 | for key, value in ani_dict.items(): 215 | print(f"{arr[counter]}: {value}") 216 | counter += 1 -------------------------------------------------------------------------------- /AnilistPython/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import sys 3 | import time 4 | import requests 5 | 6 | from .anime import Anime 7 | from .character import Character 8 | from .manga import Manga 9 | from .deep_search import DeepSearch 10 | from .user import User 11 | from .databases.database_anime_retrieval import DatabaseSearcher 12 | from .databases.search_engine import SearchEngine 13 | 14 | from .anilistpython_info import AnilistPythonInfo 15 | 16 | from .logs.log_data import LogData 17 | 18 | 19 | except ModuleNotFoundError: 20 | 21 | from .logs.setup_module import Setup 22 | setup = Setup() 23 | 24 | 25 | class Anilist: 26 | """ 27 | Initialize a new instance to the Anilist driver API. 28 | The instance is responsible for all search and retrieve operations. 29 | In calls that require a user's auth token, you will need to provide it. 30 | :ivar dict access: Access required data used through out the program 31 | :ivar ALAuth auth: Handle Authorization endpoints 32 | """ 33 | def __init__(self, cid = None, csecret = None, credentials = None, activated = True): 34 | """ 35 | :param cid: Client ID 36 | :param csecret: Client Secret 37 | :param credentials: If provided, a JWT token for auth requests 38 | :param: activated: Bot Support - ensures that the program is activated. Default = True 39 | """ 40 | self.access = {'header': {'Content-Type': 'application/json', 41 | 'User-Agent': 'AnilistPython (github.com/ReZeroE/AnilistPython)', 42 | 'Accept': 'application/json'}, 43 | 'authurl': 'https://anilist.co/api', 44 | 'apiurl': 'https://graphql.anilist.co', 45 | 'cid': cid, 46 | 'csecret': csecret, 47 | 'token': credentials} 48 | 49 | self.anime = Anime(self.access, activated) 50 | self.character = Character(self.access, activated) 51 | self.manga = Manga(self.access, activated) 52 | self.user = User(self.access, activated) 53 | self.log_data = LogData() 54 | 55 | 56 | def help(self): 57 | ''' 58 | Prints a basic explanation on the functionalities of this module. Includes links where help could be found. 59 | ''' 60 | api_help = AnilistPythonInfo() 61 | api_help.help() 62 | 63 | # ANIME ===================================================================================================================== 64 | def get_anime_id(self, anime_name, manual_select=False) -> int: 65 | ''' 66 | Retrieves the anime ID on Anilist. 67 | 68 | :param anime_name: The name of the anime 69 | :manual_select: prompts the user the top three results to select in the terminal 70 | :return: The anime's ID on Anilist. Returns -1 if an error is caught. 71 | :rtype: int 72 | ''' 73 | 74 | return self.anime.getAnimeID(anime_name, manual_select) 75 | 76 | def get_anime(self, anime_name, deepsearch=False, manual_select=False) -> dict: 77 | ''' 78 | Retrieve the anime info in the form of a dictionary. 79 | 80 | :param anime_name: the name of the anime 81 | :param deepsearch: deepsearch control value. False by default. 82 | :manual_select: prompts the user the top three results to select in the terminal 83 | :return: parsed dict containing the anime's data 84 | :rtype: dict 85 | ''' 86 | 87 | # deepsearch 88 | if deepsearch == True: 89 | ds = DeepSearch() 90 | return self.anime.getAnime(ds.deep_search_name_conversion(anime_name), manual_select) 91 | 92 | # normal search 93 | else: 94 | return self.anime.getAnime(anime_name, manual_select) 95 | 96 | def get_anime_from_database(self, anime_name) -> list: 97 | ''' 98 | Retrieve the anime info in the form of a dictionary (from the local database). 99 | 100 | :param anime_name: the name of the anime 101 | :param deepsearch: deepsearch control value. False by default. 102 | :return: a list of dictionaries containing all the search results. 103 | :rtype: list 104 | ''' 105 | se = SearchEngine() 106 | return se.search_anime_database(anime_name) 107 | 108 | def get_anime_with_id(self, anime_id) -> dict: 109 | ''' 110 | Retrieve anime info in the form of a dictionary using the anime's ID. 111 | 112 | :param anime_id: The ID of the anime on Anilist 113 | :rtype: dict 114 | ''' 115 | 116 | return self.anime.getAnimeWithID(anime_id) 117 | 118 | def print_anime_info(self, anime_name): 119 | ''' 120 | Displays all anime data. 121 | Auto formats the displayed version of the data. 122 | 123 | :param anime_name: The name of the anime 124 | ''' 125 | 126 | self.anime.displayAnimeInfo(anime_name) 127 | 128 | def search_anime(self, genre=None, year=None, score=None, id_only=False) -> list: 129 | ''' 130 | Searches anime with genre, season, and/or year. Returns a list of anime within the given restrictions. 131 | Auto formats the displayed version of the data. (Accesses local database only) 132 | 133 | :param retrieve_count: Max number of anime records to be retrieved. Retrieve all (-1) by default. 134 | :param genre: The genre of the anime in str or list of str (i.e. 'Action' or ['Action', 'Romance']) 135 | :param year: The year of the anime in str or list of str (i.e. '2012' or ['2012', '2013']) 136 | :param score: The score of the anime in str as in range (i.e. '50' or '50-60' or range(50, 60)) 137 | :param id_only: Only retrieve the ID of the anime. False by default. 138 | 139 | :return: a list of parsed dict containing the anime's data 140 | :rtype: list 141 | ''' 142 | 143 | database_searcher = DatabaseSearcher() 144 | return database_searcher.anime_mix_search(genre=genre, year=year, score=score, id_only=id_only) 145 | 146 | # CHARACTER ================================================================================================================= 147 | def get_character_id(self, character_name, manual_select=False) -> int: 148 | ''' 149 | Retrieves the character ID on Anilist. 150 | 151 | :param character_name: The character of the anime 152 | :manual_select: prompts the user the top three results to select in the terminal 153 | :return: The character's ID on Anilist. Returns -1 if an error is caught. 154 | :rtype: int 155 | ''' 156 | return self.character.getCharacterID(character_name, manual_select) 157 | 158 | def get_character(self, character_name, manual_select=False) -> dict: 159 | ''' 160 | Retrieve the character info in the form of a dictionary. 161 | 162 | :param anime_name: the name of the character 163 | :manual_select: prompts the user the top three results to select in the terminal 164 | :return: parsed dict containing the character's data 165 | :rtype: dict 166 | ''' 167 | return self.character.getCharacter(character_name, manual_select) 168 | 169 | def get_character_with_id(self, character_id) -> dict: 170 | ''' 171 | Retrieve character info in the form of a dictionary with the character's ID on Anilist. 172 | 173 | :param character_name: The name of the character 174 | :return: parsed dict containing the character's data 175 | :rtype: dict 176 | ''' 177 | return self.character.getCharacterWithID(character_id) 178 | 179 | def print_character_info(self, character_name, manual_select=False): 180 | ''' 181 | Displays all character data. 182 | Auto formats the displayed version of the data. 183 | 184 | :param character_name: The name of the character 185 | :manual_select: prompts the user the top three results to select in the terminal 186 | ''' 187 | self.character.displayCharacterInfo(character_name, manual_select) 188 | 189 | # Manga ===================================================================================================================== 190 | def get_manga_id(self, manga_name, manual_select=False) -> int: 191 | ''' 192 | Displays all character data. 193 | Auto formats the displayed version of the data. 194 | 195 | :param character_name: The manga's name 196 | :manual_select: prompts the user the top three results to select in the terminal 197 | :return: the id of the manga 198 | :rtype: int 199 | ''' 200 | return self.manga.getMangaID(manga_name, manual_select) 201 | 202 | def get_manga(self, manga_name, manual_select=False) -> dict: 203 | ''' 204 | Retrieve manga info in the form of a dictionary. 205 | 206 | :param manga_name: The name of the manga 207 | :return: parsed dict containing the manga's data 208 | :manual_select: prompts the user the top three results to select in the terminal 209 | :rtype: dict 210 | ''' 211 | 212 | return self.manga.getManga(manga_name, manual_select) 213 | 214 | def get_manga_with_id(self, manga_id) -> dict: 215 | ''' 216 | Retrieve manga info in the form of a dictionary with the manga's ID on Anilist. 217 | 218 | :param manga_id: The id of the manga 219 | :return: parsed dict containing the manga's data 220 | :rtype: dict 221 | ''' 222 | return self.manga.getMangaWithID(manga_id) 223 | 224 | def print_manga_info(self, manga_name, manual_select=False): 225 | ''' 226 | Displays all manga data. 227 | Auto formats the displayed version of the data. 228 | 229 | :param manga_name: The name of the manga 230 | :manual_select: prompts the user the top three results to select in the terminal 231 | ''' 232 | 233 | self.manga.displayMangaInfo(manga_name, manual_select) 234 | 235 | def get_user_activity(self, page:int , perpage:int) -> list: 236 | ''' 237 | Retrieve user activity info in the form of a json object. 238 | Retrieve json object will be reformatted in a easily accessable json obj. 239 | :param page: the page of user activity 240 | :param perpage: how many items per page 241 | :return: parsed list containing the user activity 242 | :rtype: list 243 | ''' 244 | return self.user.GetUserActivity(page, perpage) -------------------------------------------------------------------------------- /AnilistPython/query_strings.py: -------------------------------------------------------------------------------- 1 | class QSData: 2 | ''' 3 | Class for storing query strings. 4 | ''' 5 | def __init__(self): 6 | # ANIME ===================================================== 7 | self.animeInfoQS = """\ 8 | query ($id: Int) { 9 | Media(id: $id, type: ANIME) { 10 | title { 11 | romaji 12 | english 13 | } 14 | startDate { 15 | year 16 | month 17 | day 18 | } 19 | endDate { 20 | year 21 | month 22 | day 23 | } 24 | coverImage { 25 | large 26 | } 27 | bannerImage 28 | format 29 | status 30 | episodes 31 | season 32 | description 33 | averageScore 34 | meanScore 35 | genres 36 | synonyms 37 | nextAiringEpisode { 38 | airingAt 39 | timeUntilAiring 40 | episode 41 | } 42 | } 43 | } 44 | """ 45 | 46 | # MANGA ===================================================== 47 | self.mangaInfoQS = """\ 48 | query ($id: Int) { 49 | Media(id: $id, type: MANGA) { 50 | title { 51 | romaji 52 | english 53 | } 54 | startDate { 55 | year 56 | month 57 | day 58 | } 59 | endDate { 60 | year 61 | month 62 | day 63 | } 64 | coverImage { 65 | large 66 | } 67 | tags { 68 | name 69 | } 70 | bannerImage 71 | format 72 | chapters 73 | volumes 74 | status 75 | description 76 | averageScore 77 | meanScore 78 | genres 79 | synonyms 80 | } 81 | } 82 | """ 83 | 84 | # STAFF ===================================================== 85 | self.staffInfoQS = """\ 86 | query ($id: Int) { 87 | Staff(id: $id) { 88 | name { 89 | first 90 | last 91 | native 92 | } 93 | description 94 | language 95 | } 96 | } 97 | """ 98 | 99 | # STUDIO ===================================================== 100 | self.studioInfoQS = """\ 101 | query ($id: Int) { 102 | Studio(id: $id) { 103 | name 104 | } 105 | } 106 | """ 107 | 108 | # CHARACTER ===================================================== 109 | self.characterInfoQS = """\ 110 | query ($id: Int) { 111 | Character (id: $id) { 112 | name { 113 | first 114 | last 115 | native 116 | } 117 | description 118 | image { 119 | large 120 | } 121 | } 122 | } 123 | """ 124 | 125 | # REVIEW ===================================================== 126 | self.reviewInfoQS = """\ 127 | query ($id: Int, $html: Boolean) { 128 | Review (id: $id) { 129 | summary 130 | body(asHtml: $html) 131 | score 132 | rating 133 | ratingAmount 134 | createdAt 135 | updatedAt 136 | private 137 | media { 138 | id 139 | } 140 | user { 141 | id 142 | name 143 | avatar { 144 | large 145 | } 146 | } 147 | } 148 | } 149 | """ 150 | 151 | #============================================================================= 152 | #/////////////////////////////I Love Emilia/////////////////////////////////// 153 | #============================================================================= 154 | 155 | 156 | # ANIME ID =================================================================== 157 | self.animeIDQS = """\ 158 | query ($query: String, $page: Int, $perpage: Int) { 159 | Page (page: $page, perPage: $perpage) { 160 | pageInfo { 161 | total 162 | currentPage 163 | lastPage 164 | hasNextPage 165 | } 166 | media (search: $query, type: ANIME) { 167 | id 168 | title { 169 | romaji 170 | english 171 | } 172 | coverImage { 173 | large 174 | } 175 | averageScore 176 | popularity 177 | episodes 178 | season 179 | hashtag 180 | isAdult 181 | } 182 | } 183 | } 184 | """ 185 | 186 | # CHARACTER ID =================================================================== 187 | self.characterIDQS = """\ 188 | query ($query: String, $page: Int, $perpage: Int) { 189 | Page (page: $page, perPage: $perpage) { 190 | pageInfo { 191 | total 192 | currentPage 193 | lastPage 194 | hasNextPage 195 | } 196 | characters (search: $query) { 197 | id 198 | name { 199 | first 200 | last 201 | } 202 | image { 203 | large 204 | } 205 | } 206 | } 207 | } 208 | """ 209 | 210 | # MANGA ID =================================================================== 211 | self.mangaIDQS = """\ 212 | query ($query: String, $page: Int, $perpage: Int) { 213 | Page (page: $page, perPage: $perpage) { 214 | pageInfo { 215 | total 216 | currentPage 217 | lastPage 218 | hasNextPage 219 | } 220 | media (search: $query, type: MANGA) { 221 | id 222 | title { 223 | romaji 224 | english 225 | } 226 | coverImage { 227 | large 228 | } 229 | averageScore 230 | popularity 231 | chapters 232 | volumes 233 | season 234 | hashtag 235 | isAdult 236 | } 237 | } 238 | } 239 | """ 240 | 241 | # STAFF ID =================================================================== 242 | self.staffIDQS = """\ 243 | query ($query: String, $page: Int, $perpage: Int) { 244 | Page (page: $page, perPage: $perpage) { 245 | pageInfo { 246 | total 247 | currentPage 248 | lastPage 249 | hasNextPage 250 | } 251 | staff (search: $query) { 252 | id 253 | name { 254 | first 255 | last 256 | } 257 | image { 258 | large 259 | } 260 | } 261 | } 262 | } 263 | """ 264 | 265 | # STUDIO ID =================================================================== 266 | self.studioIDQS = """\ 267 | query ($query: String, $page: Int, $perpage: Int) { 268 | Page (page: $page, perPage: $perpage) { 269 | pageInfo { 270 | total 271 | currentPage 272 | lastPage 273 | hasNextPage 274 | } 275 | studios (search: $query) { 276 | id 277 | name 278 | } 279 | } 280 | } 281 | """ 282 | 283 | # GET AUTHENTICATED CURRENT USER ID 284 | self.user_get_idQS = """\ 285 | query{ 286 | Viewer{ 287 | id 288 | } 289 | } 290 | """ 291 | 292 | # GET AUTHENTICATED CURRENT ACTIVITY 293 | self.user_activyQS = """\ 294 | query($id:Int,$type:ActivityType,$page:Int,$perPage: Int){ 295 | Page(page:$page,perPage:$perPage){ 296 | pageInfo{ 297 | total 298 | perPage 299 | currentPage 300 | lastPage 301 | hasNextPage 302 | } 303 | activities(userId:$id,type:$type,sort:[PINNED,ID_DESC]){ 304 | ... on ListActivity{ 305 | id 306 | type 307 | replyCount 308 | status 309 | progress 310 | isLocked 311 | isSubscribed 312 | isLiked 313 | isPinned 314 | likeCount 315 | createdAt 316 | user{ 317 | id 318 | name 319 | avatar{ 320 | large 321 | } 322 | } 323 | media{ 324 | id 325 | type 326 | status(version:2) 327 | isAdult 328 | bannerImage 329 | title{ 330 | userPreferred 331 | } 332 | coverImage{ 333 | large 334 | } 335 | } 336 | } 337 | ... on TextActivity{ 338 | id 339 | type 340 | text 341 | replyCount 342 | isLocked 343 | isSubscribed 344 | isLiked 345 | isPinned 346 | likeCount 347 | createdAt 348 | user{ 349 | id 350 | name 351 | avatar{ 352 | large 353 | } 354 | } 355 | } 356 | ... on MessageActivity{ 357 | id 358 | type 359 | message 360 | replyCount 361 | isPrivate 362 | isLocked 363 | isSubscribed 364 | isLiked 365 | likeCount 366 | createdAt 367 | user:recipient{ 368 | id 369 | } 370 | messenger{ 371 | id 372 | name 373 | donatorTier 374 | donatorBadge 375 | moderatorRoles 376 | avatar{ 377 | large 378 | } 379 | } 380 | } 381 | } 382 | } 383 | } 384 | """ -------------------------------------------------------------------------------- /AnilistPython/databases/anime_database_files/anime_by_tag_cache.json: -------------------------------------------------------------------------------- 1 | { 2 | "code geass: akito the exiled - the wyvern divided|=|code geass: boukoku no akito 2 - hikisakareshi yokuryuu": "15197", 3 | "code geass: akito the exiled - the brightness falls|=|code geass: boukoku no akito 3 - kagayakumono ten yori otsu": "15199", 4 | "code geass: akito the exiled - memories of hatred|=|code geass: boukoku no akito 4 - nikushimi no kioku kara": "15201", 5 | "code geass: oz the reflection picture drama|=|code geass: soubou no oz picture drama": "17277", 6 | "code geass: hangyaku no lelouch - kiseki no birthday picture drama flash special": "18881", 7 | "code geass: akito the exiled - to beloved ones|=|code geass: boukoku no akito 5 - itoshiki mono-tachi e": "21178", 8 | "code geass: lelouch of the re;surrection|=|code geass: fukkatsu no lelouch": "97880", 9 | "code geass: lelouch of the rebellion i - initiation|=|code geass: hangyaku no lelouch i - koudou": "101811", 10 | "code geass: lelouch of the rebellion ii - transgression|=|code geass: hangyaku no lelouch ii - handou": "101812", 11 | "code geass: lelouch of the rebellion iii - glorification|=|code geass: hangyaku no lelouch iii - oudou": "101813", 12 | "code geass: boukoku no akito 2 - hikisakareshi yokuryuu picture drama": "109002", 13 | "code geass: boukoku no akito 3 - kagayakumono ten yori otsu picture drama": "109003", 14 | "code geass: boukoku no akito 4 - nikushimi no kioku kara picture drama": "109004", 15 | "code geass: boukoku no akito 5 - itoshiki mono-tachi e picture drama": "109005", 16 | "code geass: hangyaku no lelouch picture drama - hajimari no zenya": "110253", 17 | "code geass: hangyaku no lelouch picture drama - kiseki no anniversary": "112900", 18 | "code geass: fukkatsu no lelouch picture drama - shinkai no kakera": "117408", 19 | "code geass hangyaku no lelouch picture drama - kamen kokuhaku taikai": "125783", 20 | "code geass: hangyaku no lelouch dvd magazine picture drama": "126169", 21 | "code geass: hangyaku no lelouch r2 picture drama turn 12.06 - last moratorium": "132317", 22 | "code geass: lelouch of the rebellion|=|code geass: hangyaku no lelouch": "1575", 23 | "code geass: hangyaku no lelouch picture drama": "1953", 24 | "code geass: lelouch of the rebellion r2|=|code geass: hangyaku no lelouch r2": "2904", 25 | "code geass: lelouch of the rebellion special edition black rebellion|=|code geass: hangyaku no lelouch special edition black rebellion": "4596", 26 | "code geass: hangyaku no lelouch r2 picture drama": "5163", 27 | "code geass: lelouch of the rebellion r2 special edition zero requiem|=|code geass: hangyaku no lelouch r2 special edition zero requiem": "6768", 28 | "code geass: hangyaku no lelouch - kiseki no birthday picture drama": "8728", 29 | "code geass: akito the exiled - the wyvern arrives|=|code geass: boukoku no akito 1 - yokuryuu wa maiorita": "8888", 30 | "code geass: lelouch of the rebellion r2: flash specials|=|code geass: hangyaku no lelouch r2 omake flash - kaette kita baba gekijou": "9591", 31 | "code geass: nunnally in wonderland": "12685", 32 | "code geass: dakkan no z": "126830", 33 | "seraph of the end: vampire reign|=|owari no seraph": "20829", 34 | "ring no seraph": "112022", 35 | "Otona nya Koi no Shikata ga Wakaranee!": "123081", 36 | "Zhuzhu Xia: Konglong Riji": "123113", 37 | "Zhuzhu Xia: Jingsu Xiao Yingxiong": "123115", 38 | "Zhuzhu Xia: Bukeseyi de Shijie": "123116", 39 | "Hulu Xiao Jingang": "123119", 40 | "Hulu Xiongdi (Movie)": "123120", 41 | "Xin Hulu Xiongdi": "123139", 42 | "Xiongmao He Xiao Yanshu": "123144", 43 | "Galaxy Racers|=|Xingji Biaoche Wang": "123146", 44 | "Tianshu Qi Tan": "123147", 45 | "Hwasan Golae": "123148", 46 | "Mirai Harmony": "123155", 47 | "Xin Yu Gong Yu Shan": "123198", 48 | "MOVE TO THE FUTURE": "123284", 49 | "Machikado Mazoku: 2-Choume": "123330", 50 | "Z Densetsu: Owari Naki Kakumei": "123347", 51 | "Zassou-kun": "123348", 52 | "Retronix Symphony Betsu Vector Version": "123349", 53 | "One \u2192 Shota \u2190 One THE ANIMATION": "123360", 54 | "Hua Jianghu: Buliang Ren 4": "123375", 55 | "Jian Wang 3: Xia Gan Yi Dan Shen Jianxin - Chang Piao": "123379", 56 | "Re:Zero kara Hajimeru Isekai Seikatsu - Hyouketsu no Kizuna - Manner Movie": "123381", 57 | "Ninjala Episode 0: Ninja-Gum is Born|=|Ninjala Episode 0: Ninja-Gum Tanjou": "123382", 58 | "White Blue": "123383", 59 | "Zhong Jia Ji Shen: Shen Jianglin": "123394", 60 | "Suiyoubi no Yakusoku": "123405", 61 | "Yuusei": "123406", 62 | "Tenetene": "123407", 63 | "Gomi": "123408", 64 | "Xiang Shi Chuanshuo": "123417", 65 | "Kono Sekai no Tanoshimikata: Secret Story Film": "123434", 66 | "Trapped in the Past|=|Kako ni Torawarete Iru": "123436", 67 | "An Enormous Delta": "123437", 68 | "Ebe Pt01": "123438", 69 | "Xue Haizi": "123447", 70 | "Kanojo, Okarishimasu Collab Mini Animation": "123464", 71 | "Oh, Suddenly Egyptian God|=|Toutotsu ni Egypt Shin": "123474", 72 | "The Prince of Tennis II Hyotei vs. Rikkai Game of Future OVAs|=|Shin Tennis no Ouji-sama: Hyoutei vs Rikkai - Game of Future": "123476", 73 | "Mr.Fixer": "123490", 74 | "Farewell, My Dear Cramer|=|Sayonara Watashi no Cramer": "123494", 75 | "Sakura Kakumei: Hanasaku Otome-tachi": "123612", 76 | "Art of Yoh Yoshinari: Rough Sketches - Tezuka Characters|=|Yoshinari You Gashuu Rakugaki-hen - Tezuka Osamu Characters": "123638", 77 | "Farewell, My Dear Cramer: First Touch|=|Sayonara Watashi no Cramer: First Touch": "123645", 78 | "Hukakumei Zenya": "123746", 79 | "Shenmue the Animation": "123752", 80 | "Dokyuu Hentai HxEros OVA": "123769", 81 | "Ninjala 2D Cartoon Anime|=|Ninjala Cartoon Anime": "123778", 82 | "Dr. Ramune -Mysterious Disease Specialist-|=|Kai Byoui Ramune": "123779", 83 | "Scar on the Praeter|=|Praeter no Kizu": "123785", 84 | "The Saint's Magic Power is Omnipotent|=|Seijo no Maryoku wa Bannou desu": "123802", 85 | "Munou na Nana: Mini Anime": "123803", 86 | "Zhonghua Chuangshi Zhu Shen Ji: Nuwa Bu Tian Lu": "123841", 87 | "Shikoyaka naru Toki mo Hameru Toki mo": "123845", 88 | "Rebirth: Virtual to no Souguu": "123885", 89 | "Higashi Asia Bunka Toshi 2019 Toshima": "123886", 90 | "Sing a Bit of Harmony|=|Ai no Utagoe wo Kikasete": "123899", 91 | "Kanojo, Okarishimasu Petit": "123901", 92 | "Fudanshi Shoukan: Isekai de Shinjuu ni Hameraremashita Mini Anime": "123903", 93 | "Sakugasaku": "123948", 94 | "Dazzling White Town": "123952", 95 | "Go Just Go!": "123994", 96 | "B.O.Y.": "123996", 97 | "Annie": "123997", 98 | "No Guns Life Mini": "124014", 99 | "Jikai Yokoku \"No Guns Life\"": "124027", 100 | "That is the Bottleneck|=|Sore dake ga Neck": "124028", 101 | "Ginga Eiyuu Densetsu: Die Neue These - Gekitotsu 1": "124032", 102 | "Days of Wonderland|=|DAYS OF WONDERLAND": "124055", 103 | "Baraou no Souretsu": "124060", 104 | "Horimiya": "124080", 105 | "Azur Lane Anime PV": "124082", 106 | "Mulan: Hengkong Chushi": "124114", 107 | "Girls und Panzer das Finale \u2013 Part 3|=|Girls und Panzer: Saishuushou 3": "124115", 108 | "Fu Cang: Wujie Zhanzheng": "124116", 109 | "Da Huang Miyu: Linglong Shan": "124117", 110 | "Denglong Dao": "124118", 111 | "Gekidol": "124131", 112 | "Natsume Yuujinchou: Ishi Okoshi to Ayashiki Raihousha": "124132", 113 | "Armor Shop for Ladies & Gentlemen II|=|Otona no Bouguya-san II": "124136", 114 | "Maou-sama, Petit Retry!": "124138", 115 | "Sword Art Online the Movie -Progressive- Aria of a Starless Night|=|Sword Art Online: Progressive - Hoshinaki Yoru no Aria": "124140", 116 | "SK8 the Infinity|=|SK\u221e": "124153", 117 | "Fruits Basket The Final Season|=|Fruits Basket: The Final": "124194", 118 | "Baki Hanma|=|Hanma Baki": "124195", 119 | "Prayer X": "124196", 120 | "Ruguo Lishi Shi Yi Qun Miao 5": "124204", 121 | "Umamusume: Pretty Derby Season 2|=|Uma Musume: Pretty Derby Season 2": "124223", 122 | "Shaonu Qianxian: Renxing Xiaojuchang 2": "124230", 123 | "Miao Qiansui de Chengnian Li": "124243", 124 | "Mahoutsukai no Yome: Gakuin-hen": "124337", 125 | "Zhan Shuang Panini": "124338", 126 | "Fatchi Baike": "124339", 127 | "Su Shen Xiaoren": "124340", 128 | "Arad: Gyakuten no Wa": "124341", 129 | "Jingju Mao: Bawang Zhe": "124342", 130 | "Erlang Shen: Shenhai Jialong": "124343", 131 | "Xian Jian Qi Xia Zhuan: Huan Li Jing": "124344", 132 | "Hallelujah - Unmei no Sentaku": "124345", 133 | "Free": "124393", 134 | "Yatogame-chan Kansatsu Nikki 3|=|Yatogame-chan Kansatsu Nikki 3 Satsume": "124394", 135 | "Uzaki-chan wa Asobitai! 2": "124395", 136 | "Dream Hunter Rem: Rem Toujou": "124396", 137 | "Menghuan Shuyuan 4": "124397", 138 | "Kanojo, Okarishimasu 2": "124410", 139 | "HUMAN LOST (Music)": "124430", 140 | "Hello Kitty: Ringo no Mori no Mystery": "124434", 141 | "Hello Kitty: Ringo no Mori to Parallel Town": "124435", 142 | "Akarui Sekai": "124437", 143 | "Net Miracle Shopping": "124438", 144 | "Net Miracle Shopping 2nd Season": "124439", 145 | "Hong Mao Lan Tu: A Muxing": "124449", 146 | "Starlight Kiseki": "124466", 147 | "Himote House: Dai Panic! Minna de Gokiburi Taiji": "124472", 148 | "Choujikuu Seiki Orguss Memorial": "124473", 149 | "Resident Evil: Infinite Darkness|=|Biohazard: Infinite Darkness": "124494", 150 | "Idolls!": "124555", 151 | "GOTCHA!": "124561", 152 | "Taimanin Asagi: Toraware no Niku Ningyou": "124600", 153 | "Dogeza de Tanondemita: Isekai-hen": "124612", 154 | "Ibitsu": "124635", 155 | "Jashin-chan Dropkick X": "124641", 156 | "Kirameki Inokori Daisensou": "124662", 157 | "Slimetachi no Idobata Kaigi": "124668", 158 | "Osamake: Romcom Where The Childhood Friend Won't Lose|=|Osananajimi ga Zettai ni Makenai Love Come": "124675", 159 | "Ekaki Uta": "124698", 160 | "Boukyaku Battery": "124703", 161 | "Golden Kamuy 2 OVA": "124756", 162 | "Godzilla Singular Point|=|Godzilla: Singular Point": "124786", 163 | "Sore Yuke! Gakkyuu Iinchou": "124833", 164 | "WONDER EGG PRIORITY|=|Wonder Egg Priority": "124845", 165 | "Moriarty the Patriot Part 2|=|Yuukoku no Moriarty Part 2": "124858", 166 | "Nanatsu no Taizai: Tsumi no Kokuhaku Dennou Grimoire!": "124874", 167 | "Bear Bear Bear Kuma!": "124896", 168 | "Curry Meshi in Miracle": "124912", 169 | "OTMGirls no YOKIYOKI Channel": "124960", 170 | "Aggressive Girl": "124963", 171 | "Eoneu Nal Jameseo Kkaeeoboni Bagelyeoga Doeeo Isseotda": "124965", 172 | "Baymax": "125034", 173 | "Kai: Legend of the Icy Lake|=|Kai: Geoul Hosuui Jeonseol": "125035", 174 | "SHADOWS HOUSE|=|Shadows House": "125038", 175 | "Ajisai no Chiru Koro ni": "125067", 176 | "Himawari wa Yoru ni Saku": "125068", 177 | "Rikei ga Koi ni Ochita no de Shoumei shitemita. r=1-sin\u03b8 (Heart)": "125124", 178 | "Eagle Talon: Golden Spell|=|Himitsukessha Taka no Tsume: Golden Spell": "125153", 179 | "Uta no Uta": "125154", 180 | "Hige Hige Gehi Ponpon": "125155", 181 | "Funicul\u00ec funicul\u00e0: Tozan Densha": "125157", 182 | "Ano Ko no Kawari ni Suki na Dake": "125183", 183 | "Minami no Shima no Hanayome-san": "125202", 184 | "Watashi no Nyanko": "125203", 185 | "Uchuu wa Tanoshii Festival": "125204", 186 | "Daijoubu": "125205", 187 | "TSUKIMICHI -Moonlit Fantasy-|=|Tsuki ga Michibiku Isekai Douchuu": "125206", 188 | "Ton Ton Ton": "125207", 189 | "Tales of Crestoria: The Wake of Sin|=|Tales of Crestoria: Toga Waga wo Shoite Kare wa Tatsu": "125208", 190 | "Tsurune: Kazemai Koukou Kyuudou-bu Movie": "125261", 191 | "Kotobadori": "125262", 192 | "Kotobadori (Minna no Uta)": "125263", 193 | "Kaeru no Pickles: Kimochi no Iro": "125264", 194 | "Shayou": "125266", 195 | "Toaru Kagaku no Suzushina Yuriko": "125268", 196 | "Gon 2nd Season": "125271", 197 | "DiSCOVER THE FUTURE": "125274", 198 | "Telecaster B-Boy": "125293", 199 | "Pokmon: Twilight Wings - The Gathering of Stars|=|Hakumei no Tsubasa EXPANSION: Hoshi no Matsuri": "125308", 200 | "Mogura no Motoro": "125322", 201 | "Joyato|=|Jouyatou": "125330", 202 | "Sirotsumekusa": "125331", 203 | "Hetalia: World Stars|=|Hetalia World Stars": "125351", 204 | "Kanashimi no Kodomo-tachi": "125355", 205 | "Kaguya-sama: Love is War -Ultra Romantic-|=|Kaguya-sama wa Kokurasetai: Ultra Romantic": "125367", 206 | "Kaguya-sama wa Kokurasetai: Tensaitachi no Renai Zunousen OVA": "125368", 207 | "Tokyo BABYLON 2021": "125369", 208 | "Isaku: Tsumi to Batsu": "125395", 209 | "Melting Juice Lolis' Spa Services: Let us do Slurp-Slurp-Melting Care!|=|Toromitsu Musume no Hitou Service: Torottoro Churu Churu Gohoushi Sasete Kudasai": "125425", 210 | "The Way of the Househusband|=|Gokushufudou": "125426", 211 | "High-Rise Invasion|=|Tenkuu Shinpan": "125428", 212 | "Rilakkuma's Theme Park Adventure|=|Rilakkuma to Yuuenchi": "125440", 213 | "Kageki Shojo!!|=|Kageki Shoujo!!": "125446", 214 | "Thermae Romae Novae": "125447", 215 | "Gebaude=Baude": "125491", 216 | "One Arm|=|Kataude": "125513", 217 | "Honda-san x Taka no Tsume": "125514", 218 | "Haitoku no Kyoukai": "125515", 219 | "Kimi ga Suki.: THE ANIMATION": "125516", 220 | "Star Smash PV": "125517", 221 | "Minna no Umi": "125573", 222 | "Sharon no Teryouri vs Mew no Cosplay vs Nina no Kisei Jijitsu Mitsudomoe no Kessen": "125574", 223 | "Suna no Akari": "125575", 224 | "Nescaf Hong Kong TVCM": "125576", 225 | "Taisei Kensetsu CM: Myanmar": "125600", 226 | "Stratos 4.1 \"CODE: XXX DUTCH ROLL\"": "125601", 227 | "Getter Robo Arc": "125640", 228 | "Cantarella: Grace Edition": "125641", 229 | "Robot Girls Z Petit Chara Anime": "125642", 230 | "Eiyuu-ou, Bu wo Kiwameru Tame Tenseisu: Soshite, Sekai Saikyou no Minarai Kishi\u2640": "125647", 231 | "CUE!": "125682", 232 | "Attakaito": "125713", 233 | "Code Geass Hangyaku no Lelouch Picture Drama - Kamen Kokuhaku Taikai": "125783", 234 | "Evangelion x KATE CM": "125785", 235 | "San-biki no Koguma-san": "125790", 236 | "Evangelion x Attack ZERO": "125796", 237 | "Shiroi Spitz": "125818", 238 | "Ohayou no Uta": "125819", 239 | "ACUTE": "125821", 240 | "ReAct": "125822", 241 | "react": "125822", 242 | "rec": "710" 243 | } --------------------------------------------------------------------------------