anilist.py 13 KB

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