1
0

anilist.py 14 KB

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