|
| 1 | +import requests |
| 2 | +import tempfile |
| 3 | +import os |
| 4 | +import re |
| 5 | + |
| 6 | +def format_date(date): |
| 7 | + if not date or not date.get('year'): |
| 8 | + return "?" |
| 9 | + year = date.get('year') |
| 10 | + month = str(date.get('month', '??')).zfill(2) if date.get('month') else "??" |
| 11 | + day = str(date.get('day', '??')).zfill(2) if date.get('day') else "??" |
| 12 | + return f"{year}-{month}-{day}" |
| 13 | + |
| 14 | +def format_status(status): |
| 15 | + status_map = { |
| 16 | + 'FINISHED': 'Finished', |
| 17 | + 'RELEASING': 'Ongoing', |
| 18 | + 'NOT_YET_RELEASED': 'Not Yet Released', |
| 19 | + 'CANCELLED': 'Cancelled', |
| 20 | + 'HIATUS': 'Hiatus' |
| 21 | + } |
| 22 | + return status_map.get(status, status) |
| 23 | + |
| 24 | +def format_type(type_): |
| 25 | + type_map = { |
| 26 | + 'TV': 'TV Series', |
| 27 | + 'TV_SHORT': 'TV Short', |
| 28 | + 'MOVIE': 'Movie', |
| 29 | + 'SPECIAL': 'Special', |
| 30 | + 'OVA': 'OVA', |
| 31 | + 'ONA': 'ONA', |
| 32 | + 'MUSIC': 'Music' |
| 33 | + } |
| 34 | + return type_map.get(type_, type_) |
| 35 | + |
| 36 | +def format_source(source): |
| 37 | + source_map = { |
| 38 | + 'ORIGINAL': 'Original', |
| 39 | + 'MANGA': 'Manga', |
| 40 | + 'LIGHT_NOVEL': 'Light Novel', |
| 41 | + 'VISUAL_NOVEL': 'Visual Novel', |
| 42 | + 'VIDEO_GAME': 'Video Game', |
| 43 | + 'NOVEL': 'Novel', |
| 44 | + 'DOUJINSHI': 'Doujinshi', |
| 45 | + 'ANIME': 'Anime' |
| 46 | + } |
| 47 | + return source_map.get(source, source) |
| 48 | + |
| 49 | +def format_season(season): |
| 50 | + season_map = { |
| 51 | + 'WINTER': 'Winter', |
| 52 | + 'SPRING': 'Spring', |
| 53 | + 'SUMMER': 'Summer', |
| 54 | + 'FALL': 'Fall' |
| 55 | + } |
| 56 | + return season_map.get(season, season) |
| 57 | + |
| 58 | +def clean_description(description): |
| 59 | + if not description: |
| 60 | + return "No description available" |
| 61 | + desc = re.sub(r'<[^>]*>', '', description) |
| 62 | + return desc[:700] + ("..." if len(desc) > 700 else "") |
| 63 | + |
| 64 | +def fetch_anime_info(anime_name, send_message_func): |
| 65 | + # send_message_func(msg, attachment_path=None) |
| 66 | + query = ''' |
| 67 | + query ($search: String) { |
| 68 | + Media(search: $search, type: ANIME) { |
| 69 | + title { romaji english native } |
| 70 | + description(asHtml: false) |
| 71 | + coverImage { extraLarge large color } |
| 72 | + bannerImage |
| 73 | + episodes |
| 74 | + duration |
| 75 | + status |
| 76 | + averageScore |
| 77 | + meanScore |
| 78 | + popularity |
| 79 | + favourites |
| 80 | + genres |
| 81 | + studios(isMain: true) { nodes { name } } |
| 82 | + startDate { year month day } |
| 83 | + endDate { year month day } |
| 84 | + season |
| 85 | + seasonYear |
| 86 | + format |
| 87 | + source |
| 88 | + countryOfOrigin |
| 89 | + hashtag |
| 90 | + trailer { id site thumbnail } |
| 91 | + nextAiringEpisode { airingAt timeUntilAiring episode } |
| 92 | + relations { edges { relationType node { title { romaji english } siteUrl } } } |
| 93 | + recommendations { nodes { mediaRecommendation { title { romaji } siteUrl } } } |
| 94 | + siteUrl |
| 95 | + } |
| 96 | + } |
| 97 | + ''' |
| 98 | + variables = {"search": anime_name} |
| 99 | + send_message_func("🔍 Searching anime info...") |
| 100 | + try: |
| 101 | + response = requests.post('https://graphql.anilist.co', json={"query": query, "variables": variables}) |
| 102 | + response.raise_for_status() |
| 103 | + anime = response.json()['data']['Media'] |
| 104 | + if not anime: |
| 105 | + send_message_func(f"❌ No results found for \"{anime_name}\". Please check the name and try again.") |
| 106 | + return |
| 107 | + description = clean_description(anime.get('description')) |
| 108 | + relations = anime.get('relations', {}).get('edges', []) |
| 109 | + relations_text = "" |
| 110 | + if relations: |
| 111 | + relations_text = '\n'.join([ |
| 112 | + f"{edge.get('relationType')}: {edge.get('node', {}).get('title', {}).get('romaji')}" for edge in relations[:3] |
| 113 | + ]) |
| 114 | + recommendations = anime.get('recommendations', {}).get('nodes', []) |
| 115 | + recommendations_text = "" |
| 116 | + if recommendations: |
| 117 | + recommendations_text = '\n'.join([ |
| 118 | + f"- {node.get('mediaRecommendation', {}).get('title', {}).get('romaji')}" for node in recommendations[:3] |
| 119 | + ]) |
| 120 | + next_ep = anime.get('nextAiringEpisode') |
| 121 | + next_episode_text = "" |
| 122 | + if next_ep: |
| 123 | + time_until = next_ep.get('timeUntilAiring', 0) |
| 124 | + days = time_until // (24 * 60 * 60) |
| 125 | + hours = (time_until % (24 * 60 * 60)) // (60 * 60) |
| 126 | + next_episode_text = f"\n⏳ Next Episode: #{next_ep.get('episode')} in {days}d {hours}h" |
| 127 | + studio = anime.get('studios', {}).get('nodes', [{}])[0].get('name', 'Unknown') |
| 128 | + title = anime['title'] |
| 129 | + info_msg = f"🎌 𝗧𝗶𝘁𝗹𝗲: {title.get('romaji') or title.get('english')}\n" |
| 130 | + if title.get('english'): |
| 131 | + info_msg += f"🏴 𝗘𝗻𝗴𝗹𝗶𝘀𝗵: {title['english']}\n" |
| 132 | + if title.get('native'): |
| 133 | + info_msg += f"🗾 𝗡𝗮𝘁𝗶𝘃𝗲: {title['native']}\n\n" |
| 134 | + info_msg += f"📌 𝗦𝘁𝗮𝘁𝘂𝘀: {format_status(anime.get('status'))}\n" |
| 135 | + info_msg += f"📺 𝗘𝗽𝗶𝘀𝗼𝗱𝗲𝘀: {anime.get('episodes', 'Unknown')} ({anime.get('duration', '?')} min/ep)\n" |
| 136 | + info_msg += f"⭐ 𝗥𝗮𝘁𝗶𝗻𝗴: {anime.get('averageScore', '?')}/100 ({anime.get('meanScore', '?')} mean)\n" |
| 137 | + info_msg += f"❤️ 𝗙𝗮𝘃𝗼𝗿𝗶𝘁𝗲𝘀: {anime.get('favourites', 0):,}\n" |
| 138 | + info_msg += f"🔥 𝗣𝗼𝗽𝘂𝗹𝗮𝗿𝗶𝘁𝘆: #{anime.get('popularity', '?')}\n\n" |
| 139 | + info_msg += f"🎬 𝗙𝗼𝗿𝗺𝗮𝘁: {format_type(anime.get('format'))}\n" |
| 140 | + info_msg += f"🎥 𝗦𝗼𝘂𝗿𝗰𝗲: {format_source(anime.get('source'))}\n" |
| 141 | + info_msg += f"🏢 𝗦𝘁𝘂𝗱𝗶𝗼: {studio}\n" |
| 142 | + info_msg += f"🌐 𝗖𝗼𝘂𝗻𝘁𝗿𝘆: {anime.get('countryOfOrigin', 'Japan')}\n\n" |
| 143 | + info_msg += f"🗓️ 𝗔𝗶𝗿𝗲𝗱: {format_date(anime.get('startDate'))} to {format_date(anime.get('endDate'))}\n" |
| 144 | + if anime.get('season'): |
| 145 | + info_msg += f"🍂 𝗦𝗲𝗮𝘀𝗼𝗻: {format_season(anime['season'])} {anime.get('seasonYear', '')}\n" |
| 146 | + info_msg += next_episode_text |
| 147 | + info_msg += f"\n🏷️ 𝗚𝗲𝗻𝗿𝗲𝘀: {', '.join(anime.get('genres', []))}\n\n" |
| 148 | + # info_msg += f"📝 𝗗𝗲𝘀𝗰𝗿𝗶𝗽𝘁𝗶𝗼𝗻:\n{description}\n\n" # Description turned off as requested |
| 149 | + if relations_text: |
| 150 | + info_msg += f"🔗 𝗥𝗲𝗹𝗮𝘁𝗶𝗼𝗻𝘀:\n{relations_text}\n\n" |
| 151 | + if recommendations_text: |
| 152 | + info_msg += f"💡 𝗥𝗲𝗰𝗼𝗺𝗺𝗲𝗻𝗱𝗲𝗱:\n{recommendations_text}\n\n" |
| 153 | + info_msg += f"🔗 𝗠𝗼𝗿𝗲 𝗜𝗻𝗳𝗼: {anime.get('siteUrl')}" |
| 154 | + trailer = anime.get('trailer') |
| 155 | + if trailer and trailer.get('id'): |
| 156 | + info_msg += f"\n🎬 𝗧𝗿𝗮𝗶𝗹𝗲𝗿: https://youtube.com/watch?v={trailer['id']}" |
| 157 | + image_url = anime.get('coverImage', {}).get('extraLarge') or anime.get('coverImage', {}).get('large') |
| 158 | + if image_url: |
| 159 | + with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as img_file: |
| 160 | + img_resp = requests.get(image_url) |
| 161 | + img_file.write(img_resp.content) |
| 162 | + img_path = img_file.name |
| 163 | + # Telegram caption limit is 1024 chars |
| 164 | + if len(info_msg) > 1024: |
| 165 | + short_caption = title.get('romaji') or title.get('english') or 'Anime Info' |
| 166 | + send_message_func(short_caption, attachment_path=img_path) |
| 167 | + # Send the rest as a text message |
| 168 | + send_message_func(info_msg) |
| 169 | + else: |
| 170 | + send_message_func(info_msg, attachment_path=img_path) |
| 171 | + os.unlink(img_path) |
| 172 | + else: |
| 173 | + send_message_func(info_msg) |
| 174 | + except Exception as e: |
| 175 | + send_message_func("❌ Error fetching anime info. Please try again later.") |
| 176 | + print(f"❌ Anime Error: {str(e)}") |
| 177 | + |
| 178 | +def fetch_manga_info(manga_name, send_message_func): |
| 179 | + query = ''' |
| 180 | + query ($search: String) { |
| 181 | + Media(search: $search, type: MANGA) { |
| 182 | + title { romaji english native } |
| 183 | + description(asHtml: false) |
| 184 | + coverImage { extraLarge large color } |
| 185 | + chapters |
| 186 | + volumes |
| 187 | + status |
| 188 | + averageScore |
| 189 | + meanScore |
| 190 | + popularity |
| 191 | + favourites |
| 192 | + genres |
| 193 | + startDate { year month day } |
| 194 | + endDate { year month day } |
| 195 | + siteUrl |
| 196 | + } |
| 197 | + } |
| 198 | + ''' |
| 199 | + variables = {"search": manga_name} |
| 200 | + send_message_func("🔍 Searching manga info...") |
| 201 | + try: |
| 202 | + response = requests.post('https://graphql.anilist.co', json={"query": query, "variables": variables}) |
| 203 | + response.raise_for_status() |
| 204 | + manga = response.json()['data']['Media'] |
| 205 | + if not manga: |
| 206 | + send_message_func(f"❌ No results found for \"{manga_name}\". Please check the name and try again.") |
| 207 | + return |
| 208 | + title = manga['title'] |
| 209 | + info_msg = f"📖 𝗧𝗶𝘁𝗹𝗲: {title.get('romaji') or title.get('english')}\n" |
| 210 | + if title.get('english'): |
| 211 | + info_msg += f"🏴 𝗘𝗻𝗴𝗹𝗶𝘀𝗵: {title['english']}\n" |
| 212 | + if title.get('native'): |
| 213 | + info_msg += f"🗾 𝗡𝗮𝘁𝗶𝘃𝗲: {title['native']}\n\n" |
| 214 | + info_msg += f"📌 𝗦𝘁𝗮𝘁𝘂𝘀: {format_status(manga.get('status'))}\n" |
| 215 | + info_msg += f"📚 𝗖𝗵𝗮𝗽𝘁𝗲𝗿𝘀: {manga.get('chapters', 'Unknown')}\n" |
| 216 | + info_msg += f"📖 𝗩𝗼𝗹𝘂𝗺𝗲𝘀: {manga.get('volumes', 'Unknown')}\n" |
| 217 | + info_msg += f"⭐ 𝗥𝗮𝘁𝗶𝗻𝗴: {manga.get('averageScore', '?')}/100 ({manga.get('meanScore', '?')} mean)\n" |
| 218 | + info_msg += f"❤️ 𝗙𝗮𝘃𝗼𝗿𝗶𝘁𝗲𝘀: {manga.get('favourites', 0):,}\n" |
| 219 | + info_msg += f"🔥 𝗣𝗼𝗽𝘂𝗹𝗮𝗿𝗶𝘁𝘆: #{manga.get('popularity', '?')}\n\n" |
| 220 | + info_msg += f"🗓️ 𝗔𝗶𝗿𝗲𝗱: {format_date(manga.get('startDate'))} to {format_date(manga.get('endDate'))}\n" |
| 221 | + info_msg += f"🏷️ 𝗚𝗲𝗻𝗿𝗲𝘀: {', '.join(manga.get('genres', []))}\n\n" |
| 222 | + # info_msg += f"📝 𝗗𝗲𝘀𝗰𝗿𝗶𝗽𝘁𝗶𝗼𝗻:\n{clean_description(manga.get('description'))}\n\n" # Description turned off as requested |
| 223 | + info_msg += f"🔗 𝗠𝗼𝗿𝗲 𝗜𝗻𝗳𝗼: {manga.get('siteUrl')}" |
| 224 | + image_url = manga.get('coverImage', {}).get('extraLarge') or manga.get('coverImage', {}).get('large') |
| 225 | + if image_url: |
| 226 | + with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as img_file: |
| 227 | + img_resp = requests.get(image_url) |
| 228 | + img_file.write(img_resp.content) |
| 229 | + img_path = img_file.name |
| 230 | + # Telegram caption limit is 1024 chars |
| 231 | + if len(info_msg) > 1024: |
| 232 | + short_caption = title.get('romaji') or title.get('english') or 'Manga Info' |
| 233 | + send_message_func(short_caption, attachment_path=img_path) |
| 234 | + send_message_func(info_msg) |
| 235 | + else: |
| 236 | + send_message_func(info_msg, attachment_path=img_path) |
| 237 | + os.unlink(img_path) |
| 238 | + else: |
| 239 | + send_message_func(info_msg) |
| 240 | + except Exception as e: |
| 241 | + send_message_func("❌ Error fetching manga info. Please try again later.") |
| 242 | + print(f"❌ Manga Error: {str(e)}") |
| 243 | + |
| 244 | +# Example usage: |
| 245 | +# def send_message(msg, attachment_path=None): |
| 246 | +# print(msg) |
| 247 | +# if attachment_path: |
| 248 | +# print(f"[Image at {attachment_path}]") |
| 249 | +# fetch_anime_info("Naruto", send_message) |
0 commit comments