anilist.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. import asyncio
  2. import datetime
  3. import time
  4. from enum import Enum
  5. from typing import Dict, List
  6. from discord import activity
  7. import requests
  8. import myanimebot.globals as globals
  9. import myanimebot.utils as utils
  10. import myanimebot.database as database
  11. from myanimebot.discord import send_embed_wrapper, build_embed
  12. ANILIST_GRAPHQL_URL = 'https://graphql.anilist.co'
  13. def get_media_name(activity):
  14. ''' Returns the media name in english if possible '''
  15. english_name = activity["media"]["title"]["english"]
  16. if english_name is not None:
  17. return english_name
  18. romaji_name = activity["media"]["title"]["romaji"]
  19. if romaji_name is not None:
  20. return romaji_name
  21. native_name = activity["media"]["title"]["native"]
  22. if native_name is not None:
  23. return native_name
  24. return ''
  25. def get_progress(feed : utils.Feed, activity : dict):
  26. ''' Tries to get progress from activity '''
  27. progress = activity["progress"]
  28. if progress is None:
  29. if feed.status == utils.MediaStatus.COMPLETED:
  30. return feed.media.episodes
  31. elif feed.status == utils.MediaStatus.PLANNING:
  32. return '0'
  33. else:
  34. return '?'
  35. return progress
  36. def get_number_episodes(activity, media_type : utils.MediaType):
  37. episodes = '?'
  38. if media_type == utils.MediaType.ANIME:
  39. episodes = activity["media"]["episodes"]
  40. elif media_type == utils.MediaType.MANGA:
  41. episodes = activity["media"]["chapters"]
  42. else:
  43. raise NotImplementedError('Error: Unknown media type "{}"'.format(media_type))
  44. if episodes is None:
  45. episodes = '?'
  46. return episodes
  47. def build_feed_from_activity(activity, user : utils.User) -> utils.Feed:
  48. if activity is None: return None
  49. media_type = utils.MediaType.from_str(activity["type"])
  50. media = utils.Media(name=get_media_name(activity),
  51. url=activity["media"]["siteUrl"],
  52. episodes=get_number_episodes(activity, media_type),
  53. image=activity["media"]["coverImage"]["large"],
  54. type=media_type)
  55. feed = utils.Feed(service=utils.Service.ANILIST,
  56. date_publication=datetime.datetime.fromtimestamp(activity["createdAt"], globals.timezone),
  57. user=user,
  58. status=utils.MediaStatus.from_str(activity["status"]),
  59. description=None,
  60. media=media,
  61. progress=None)
  62. feed.progress = get_progress(feed, activity)
  63. return feed
  64. def get_anilist_userId_from_name(user_name : str) -> int:
  65. """ Searches an AniList user by its name and returns its ID """
  66. query = '''query($userName: String){
  67. User(name: $userName) {
  68. id
  69. }
  70. }'''
  71. variables = {
  72. 'userName': user_name
  73. }
  74. try:
  75. response = requests.post(ANILIST_GRAPHQL_URL, json={'query': query, 'variables': variables})
  76. response.raise_for_status()
  77. return response.json()["data"]["User"]["id"]
  78. except requests.HTTPError as e:
  79. globals.logging.error('HTPP Error while getting the AniList user ID for "{}". Error: {}'.format(user_name, e))
  80. except Exception as e:
  81. globals.logging.error('Unknown error while getting the AniList user ID for "{}". Error: {}'.format(user_name, e))
  82. return None
  83. def get_latest_users_activities(users : List[utils.User], page: int, perPage = 5) -> List[utils.Feed]:
  84. """ Get latest users' activities """
  85. query = '''query ($userIds: [Int], $page: Int, $perPage: Int) {
  86. Page (page: $page, perPage: $perPage) {
  87. activities (userId_in: $userIds, sort: ID_DESC) {
  88. __typename
  89. ... on ListActivity {
  90. id
  91. type
  92. status
  93. progress
  94. isLocked
  95. createdAt
  96. user {
  97. id
  98. name
  99. }
  100. media {
  101. id
  102. siteUrl
  103. episodes
  104. chapters
  105. type
  106. title {
  107. romaji
  108. english
  109. native
  110. }
  111. coverImage {
  112. large
  113. }
  114. }
  115. }
  116. }
  117. }
  118. }'''
  119. variables = {
  120. "userIds": [user.service_id for user in users],
  121. "perPage": perPage,
  122. "page": page
  123. }
  124. try:
  125. # Execute GraphQL query
  126. response = requests.post(ANILIST_GRAPHQL_URL, json={'query': query, 'variables': variables})
  127. response.raise_for_status()
  128. data = response.json()["data"]["Page"]["activities"]
  129. # Create feeds from data
  130. feeds = []
  131. for activity in data:
  132. # Check if activity is a ListActivity
  133. if activity["__typename"] != 'ListActivity':
  134. continue
  135. # Find corresponding user for this ListActivity
  136. user = next((user for user in users if user.name == activity["user"]["name"]), None)
  137. if user is None:
  138. raise RuntimeError('Cannot find {} in our registered users'.format(activity["user"]["name"]))
  139. # Add new builded feed
  140. feeds.append(build_feed_from_activity(activity, user))
  141. return feeds
  142. except requests.HTTPError as e:
  143. globals.logging.error('HTPP Error while getting the latest users\' AniList activities for {} on page {} with {} items per page. Error: {}'.format(users, page, perPage, e))
  144. except Exception as e:
  145. globals.logging.error('Unknown Error while getting the latest users\' AniList activities for {} on page {} with {} items per page. Error: {}'.format(users, page, perPage, e))
  146. return None
  147. def check_username_validity(username) -> bool:
  148. """ Check if the AniList username exists """
  149. query = '''query($name: String) {
  150. User(name: $name) {
  151. name
  152. }
  153. }'''
  154. variables = {
  155. 'name': username
  156. }
  157. try:
  158. response = requests.post(ANILIST_GRAPHQL_URL, json={'query': query, 'variables': variables})
  159. response.raise_for_status()
  160. return response.json()["data"]["User"]["name"] == username
  161. except requests.HTTPError as e:
  162. status_code = e.response.status_code
  163. if status_code != 404:
  164. globals.logging.error('HTTP Error while trying to check this username validity: "{}". Error: {}'.format(username, e))
  165. except Exception as e:
  166. globals.logging.error('Unknown error while trying to check this username validity: "{}". Error: {}'.format(username, e))
  167. return False
  168. def get_latest_activity(users : List[utils.User]):
  169. """ Get the latest users' activity """
  170. # TODO Will fail if last activity is not a ListActivity
  171. query = '''query ($userIds: [Int]) {
  172. Activity(userId_in: $userIds, sort: ID_DESC) {
  173. __typename
  174. ... on ListActivity {
  175. id
  176. userId
  177. createdAt
  178. }
  179. }
  180. }'''
  181. variables = {
  182. "userIds": [user.service_id for user in users]
  183. }
  184. try:
  185. response = requests.post(ANILIST_GRAPHQL_URL, json={'query': query, 'variables': variables})
  186. response.raise_for_status()
  187. return response.json()["data"]["Activity"]
  188. except requests.HTTPError as e:
  189. globals.logging.error('HTPP Error while getting the latest AniList activity : {}'.format(e))
  190. except Exception as e:
  191. globals.logging.error('Unknown error while getting the latest AniList activity : {}'.format(e))
  192. return None
  193. def get_users_db():
  194. ''' Returns the registered users using AniList '''
  195. # TODO Make generic execute
  196. cursor = database.create_cursor()
  197. cursor.execute("SELECT id, {}, servers FROM t_users WHERE service = %s".format(globals.DB_USER_NAME), [globals.SERVICE_ANILIST])
  198. users_data = cursor.fetchall()
  199. cursor.close()
  200. return users_data
  201. def get_users() -> List[utils.User]:
  202. users = []
  203. users_data = get_users_db()
  204. if users_data is not None:
  205. for user_data in users_data:
  206. users.append(utils.User(id=user_data["id"],
  207. service_id=get_anilist_userId_from_name(user_data[globals.DB_USER_NAME]),
  208. name=user_data[globals.DB_USER_NAME],
  209. servers=user_data["servers"].split(',')))
  210. return users
  211. def get_users_id(users_data) -> List[int]:
  212. ''' Returns the id of the registered users using AniList '''
  213. users_ids = []
  214. # Get users using AniList
  215. if users_data is not None:
  216. for user_data in users_data:
  217. users_ids.append(get_anilist_userId_from_name(user_data[globals.DB_USER_NAME]))
  218. # TODO Normalement pas besoin de recuperer les ids vu que je peux faire la recherche avec les noms
  219. return users_ids
  220. async def send_embed_to_channels(activity : utils.Feed):
  221. ''' Send an embed message describing the activity to user's channel '''
  222. for server in activity.user.servers:
  223. data_channels = utils.get_channels(server)
  224. if data_channels is not None:
  225. for channel in data_channels:
  226. await send_embed_wrapper(None,
  227. channel["channel"],
  228. globals.client,
  229. build_embed(activity))
  230. def insert_feed_db(feed: utils.Feed):
  231. ''' Insert an AniList feed into database '''
  232. database.insert_feed_db(feed, globals.SERVICE_ANILIST)
  233. async def process_new_activities(last_activity_date, users : List[utils.User]):
  234. """ Fetch and process all newest activities """
  235. continue_fetching = True
  236. page_number = 1
  237. while continue_fetching:
  238. # Get activities
  239. activities = get_latest_users_activities(users, page_number)
  240. if activities == None: # An error occured, break the loop
  241. return
  242. # Processing them
  243. for activity in activities:
  244. # Get time difference between now and activity creation date
  245. diffTime = datetime.datetime.now(globals.timezone) - activity.date_publication
  246. # If the activity is older than the last_activity_date, we processed all the newest activities
  247. # Also, if the time difference is bigger than the config's "secondMax", we can stop processing them
  248. if activity.date_publication.timestamp() <= last_activity_date \
  249. or diffTime.total_seconds() > globals.secondMax:
  250. # FIXME If two or more feeds are published at the same time, this would skip them
  251. continue_fetching = False
  252. break
  253. # Process activity
  254. globals.logger.info('Adding new feed for "{}({})" about "{}"'.format(activity.user.name, activity.service.name, activity.media.name))
  255. insert_feed_db(activity)
  256. await send_embed_to_channels(activity)
  257. # Load next activities page
  258. # TODO How can I avoid duplicate if insertion in between? With storing ids?
  259. if continue_fetching:
  260. page_number += 1
  261. time.sleep(1)
  262. def get_last_activity_date_db() -> float:
  263. # Refresh database
  264. globals.conn.commit()
  265. # Get last activity date
  266. cursor = database.create_cursor()
  267. cursor.execute("SELECT published FROM t_feeds WHERE service=%s ORDER BY published DESC LIMIT 1", [globals.SERVICE_ANILIST])
  268. data = cursor.fetchone()
  269. if data is None or len(data) == 0:
  270. return 0.0
  271. else:
  272. return data["published"].timestamp()
  273. async def check_new_activities():
  274. """ Check if there is new activities and process them """
  275. last_activity_date = get_last_activity_date_db()
  276. # Get latest activity on AniList
  277. users = get_users()
  278. latest_activity = get_latest_activity(users)
  279. if latest_activity is not None:
  280. # If the latest activity is more recent than the last we stored
  281. globals.logger.debug('Comparing last registered feed ({}) with latest found feed ({})'.format(last_activity_date, latest_activity["createdAt"]))
  282. if last_activity_date < latest_activity["createdAt"]:
  283. globals.logger.debug("Found a more recent AniList feed")
  284. await process_new_activities(last_activity_date, users)
  285. async def background_check_feed(asyncioloop):
  286. ''' Main function that check the AniList feeds '''
  287. globals.logger.info("Starting up Anilist.background_check_feed")
  288. await globals.client.wait_until_ready()
  289. globals.logger.debug("Discord client connected, unlocking Anilist.background_check_feed...")
  290. while not globals.client.is_closed():
  291. globals.logger.debug('Fetching Anilist feeds')
  292. try:
  293. await check_new_activities()
  294. except Exception as e:
  295. globals.logger.exception('Error while fetching Anilist feeds : ({})'.format(e))
  296. await asyncio.sleep(globals.ANILIST_SECONDS_BETWEEN_FETCHES)
  297. # TODO Bien renvoyer vers AniList (Liens/Liste/Anime)
  298. # TODO Comment eviter doublons MAL/AniList -> Ne pas faire je pense
  299. # TODO Insert anime into DB
  300. # TODO Uniformiser labels status feed entre MAL et ANILIST