Files
deploy-test/animex/manga.py
2026-03-29 20:52:57 -05:00

585 lines
22 KiB
Python

import requests
import json
import time
import os
import collections
import getpass
import re
# --- Dependency Check and Setup ---
try:
import google.generativeai as genai
except ImportError:
print("Error: The 'google-generativeai' library is not installed.")
print("Please install it by running: pip install google-generativeai")
exit()
# --- Configuration ---
JIKAN_API_BASE_URL = "https://api.jikan.moe/v4"
CONTENT_FILE = "manga_content.json"
CONFIG_FILE = "config.ini"
# Jikan rate limiting
REQUEST_TIMESTAMPS = collections.deque()
REQUEST_LIMIT = 3 # 3 requests per second
TIME_WINDOW = 1 # 1 second
# --- API Key and Configuration Management ---
def get_api_key():
"""Gets the Google AI API key, prompting the user if not found."""
# Check environment variable first
api_key = os.getenv("GOOGLE_API_KEY")
if api_key:
print("Loaded Google API Key from environment variable.")
input("Press Enter to continue...")
return api_key
# Check config file next
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
config = json.load(f)
api_key = config.get("GOOGLE_API_KEY")
if api_key:
print("Loaded Google API Key from config.ini.")
input("Press Enter to continue...")
return api_key
# If not found, prompt the user
print("\n--- Google AI API Key Required ---")
print("To use the AI generation feature, you need a Google AI API Key.")
print("You can get a free key from Google AI Studio.")
print("The key will be stored locally in 'config.ini' so you don't have to enter it again.")
api_key = getpass.getpass("Please enter your Google AI API Key: ")
# Save the key to config.ini for future use
with open(CONFIG_FILE, 'w') as f:
json.dump({"GOOGLE_API_KEY": api_key}, f)
print("API Key saved to config.ini.")
return api_key
# --- Jikan API Interaction with Rate Limiting ---
def jikan_api_request(endpoint, params=None):
"""
Makes a rate-limited request to the Jikan API.
Waits if the request limit has been reached in the last second.
"""
global REQUEST_TIMESTAMPS
while True:
now = time.time()
# Remove timestamps older than the time window
while REQUEST_TIMESTAMPS and REQUEST_TIMESTAMPS[0] < now - TIME_WINDOW:
REQUEST_TIMESTAMPS.popleft()
if len(REQUEST_TIMESTAMPS) < REQUEST_LIMIT:
break
# Calculate sleep time to respect the rate limit
sleep_time = (REQUEST_TIMESTAMPS[0] + TIME_WINDOW) - now + 0.05 # small buffer
print(f"Jikan rate limit reached. Waiting for {sleep_time:.2f} seconds...")
time.sleep(sleep_time)
try:
REQUEST_TIMESTAMPS.append(time.time())
print(f"Making Jikan request to: {JIKAN_API_BASE_URL}{endpoint}")
response = requests.get(f"{JIKAN_API_BASE_URL}{endpoint}", params=params)
response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
return response.json()
except requests.exceptions.RequestException as e:
print(f"\n--- Jikan API Error --- \n{e}\n------------------")
return None
# --- Helper Functions ---
def clear_screen():
"""Clears the console screen."""
os.system('cls' if os.name == 'nt' else 'clear')
def get_choice(max_choice, allow_back=True):
"""Gets and validates a user's integer choice."""
while True:
try:
prompt = "> "
choice = input(prompt)
if allow_back and choice.lower() == 'b':
return 'b'
choice = int(choice)
if 1 <= choice <= max_choice:
return choice
else:
print(f"Invalid choice. Please enter a number between 1 and {max_choice}.")
except ValueError:
print("Invalid input. Please enter a number.")
def format_manga_data(manga_obj):
"""Formats Jikan manga data into the structure needed for manga_content.json."""
return {
"id": manga_obj['mal_id'],
"name": manga_obj.get('title_english') or manga_obj.get('title'),
"image": manga_obj['images']['jpg']['large_image_url'],
# Optional: Add description for the hero section if needed
"description": manga_obj.get('synopsis', ''),
"year": manga_obj.get('published', {}).get('prop', {}).get('from', {}).get('year')
}
def search_and_select_manga():
"""Prompts user to search for a manga, displays results, and returns the selected one."""
query = input("Enter search term (or 'b' to go back): ")
if query.lower() == 'b':
return None
results = jikan_api_request("/manga", params={"q": query, "limit": 10})
if not results or not results.get('data'):
print("No results found.")
input("Press Enter to continue...")
return None
clear_screen()
print(f"--- Search Results for '{query}' ---")
for i, manga in enumerate(results['data'], 1):
year = manga.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
print(f"[{i}] {manga.get('title_english') or manga.get('title')} ({manga.get('type', 'N/A')}, {year})")
print("\n[b] Back to previous menu")
print("\nSelect a manga to add:")
choice = get_choice(len(results['data']))
if choice == 'b':
return None
return results['data'][choice - 1]
# --- Management Logic ---
def manage_spotlight(data):
"""Handles logic for managing the spotlight section."""
while True:
clear_screen()
print("--- Manage Spotlight Section ---")
if not data['spotlight']:
print("Spotlight is currently empty.")
else:
for i, item in enumerate(data['spotlight'], 1):
print(f"[{i}] {item['name']} (ID: {item['id']})")
print("\nOptions:")
print("[1] Add a manga to Spotlight")
print("[2] Remove a manga from Spotlight")
print("[b] Back to Main Menu")
choice = input("> ").lower()
if choice == '1':
manga_obj = search_and_select_manga()
if manga_obj:
if any(item['id'] == manga_obj['mal_id'] for item in data['spotlight']):
print(f"'{manga_obj['title']}' is already in the spotlight.")
else:
formatted = format_manga_data(manga_obj)
data['spotlight'].append(formatted)
print(f"Added '{formatted['name']}' to spotlight.")
input("Press Enter to continue...")
elif choice == '2':
if not data['spotlight']:
print("Nothing to remove.")
input("Press Enter to continue...")
continue
print("Enter the number of the manga to remove (or 'b' to cancel):")
remove_choice = get_choice(len(data['spotlight']))
if remove_choice != 'b':
removed = data['spotlight'].pop(remove_choice - 1)
print(f"Removed '{removed['name']}' from spotlight.")
input("Press Enter to continue...")
elif choice == 'b':
return
def manage_sections(data):
"""Handles logic for managing horizontal sections."""
while True:
clear_screen()
print("--- Manage Horizontal Sections ---")
if not data['sections']:
print("No sections created yet.")
else:
for i, section in enumerate(data['sections'], 1):
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
print("\nOptions:")
print("[1] Create a new section")
print("[2] Edit an existing section")
print("[3] Delete a section")
print("[b] Back to Main Menu")
choice = input("> ").lower()
if choice == '1':
title = input("Enter title for new section: ")
data['sections'].append({"title": title, "items": []})
print(f"Section '{title}' created.")
input("Press Enter...")
elif choice == '2':
if not data['sections']:
print("No sections to edit.")
input("Press Enter...")
continue
for i, section in enumerate(data['sections'], 1):
print(f"[{i}] {section['title']} ({len(section['items'])} items)")
print("Select a section to edit:")
edit_choice = get_choice(len(data['sections']))
if edit_choice != 'b':
edit_section_menu(data['sections'][edit_choice - 1])
elif choice == '3':
if not data['sections']:
print("No sections to delete.")
input("Press Enter...")
continue
print("Select a section to delete:")
delete_choice = get_choice(len(data['sections']))
if delete_choice != 'b':
removed = data['sections'].pop(delete_choice - 1)
print(f"Deleted section '{removed['title']}'.")
input("Press Enter...")
elif choice == 'b':
return
def edit_section_menu(section):
"""Menu for editing a specific section."""
while True:
clear_screen()
print(f"--- Editing Section: {section['title']} ---")
if not section['items']:
print("This section is empty.")
else:
for i, item in enumerate(section['items'], 1):
print(f" [{i}] {item['name']} (ID: {item['id']})")
print("\nOptions:")
print("[1] Add a manga to this section (Manual Search)")
print("[2] Remove a manga from this section")
print("[3] Auto-populate this section (from Jikan)")
print("[4] Generate content with AI")
print("[5] Rename this section")
print("[b] Back to Sections Menu")
choice = input("> ").lower()
if choice == '1':
manga_obj = search_and_select_manga()
if manga_obj:
if any(item['id'] == manga_obj['mal_id'] for item in section['items']):
print(f"'{manga_obj['title']}' is already in this section.")
else:
formatted = format_manga_data(manga_obj)
section['items'].append(formatted)
print(f"Added '{formatted['name']}' to '{section['title']}'.")
input("Press Enter...")
elif choice == '2':
if not section['items']:
print("Nothing to remove.")
input("Press Enter...")
continue
print("Enter the number of the manga to remove:")
remove_choice = get_choice(len(section['items']))
if remove_choice != 'b':
removed = section['items'].pop(remove_choice - 1)
print(f"Removed '{removed['name']}' from '{section['title']}'.")
input("Press Enter...")
elif choice == '3':
auto_populate_section(section)
elif choice == '4':
generate_with_ai(section)
elif choice == '5':
new_title = input(f"Enter new title for '{section['title']}': ")
section['title'] = new_title
print("Section renamed.")
input("Press Enter...")
elif choice == 'b':
return
def auto_populate_section(section):
"""Automatically populates a section from a Jikan endpoint."""
clear_screen()
print(f"--- Auto-Populate Section: {section['title']} ---")
print("Select a category to populate from:")
print("[1] Top Manga by Popularity")
print("[2] Top Manhwa")
print("[3] Top Publishing Manga")
print("[b] Cancel")
choice = get_choice(3)
if choice == 'b':
return
endpoint_map = {
1: ("/top/manga", {"filter": "bypopularity", "limit": 15}),
2: ("/manga", {"type": "manhwa", "order_by": "popularity", "limit": 15}),
3: ("/top/manga", {"filter": "publishing", "limit": 15})
}
endpoint, params = endpoint_map[choice]
results = jikan_api_request(endpoint, params=params)
if not results or not results.get('data'):
print("Could not fetch data for this category.")
input("Press Enter...")
return
added_count = 0
skipped_count = 0
for manga_obj in results['data']:
if not any(item['id'] == manga_obj['mal_id'] for item in section['items']):
section['items'].append(format_manga_data(manga_obj))
added_count += 1
else:
skipped_count += 1
print(f"Added {added_count} new items and skipped {skipped_count} duplicates in '{section['title']}'.")
input("Press Enter...")
def generate_with_ai(section):
"""Generates content for a section using Google's Generative AI."""
clear_screen()
print(f"--- AI Content Generation for: {section['title']} ---")
# 1. Get and configure API Key
try:
api_key = get_api_key()
genai.configure(api_key=api_key)
model = genai.GenerativeModel('gemini-1.5-flash')
except Exception as e:
print(f"An error occurred while configuring the AI model: {e}")
input("Press Enter to return.")
return
# 2. Get user prompt
print("Describe the kind of manga/manhwa you want to find.")
print("Examples: 'top 10 isekai manga', 'psychological thrillers similar to Monster', 'wholesome slice of life'")
user_prompt = input("\nEnter your prompt: ")
if not user_prompt:
return
# 3. Query the AI model with improved prompt
print("\nAsking the AI for suggestions... this may take a moment.")
full_prompt = f"""List exactly 10 manga that fit this description: '{user_prompt}'.
IMPORTANT: Follow this exact format for your response:
- Return ONLY the manga titles
- One title per line
- No numbers, bullets, dashes, or prefixes
- No descriptions or explanations
- No extra text before or after the list
- Use the most commonly known English or romanized title
Example format:
Berserk
Vagabond
One Piece
Your response for '{user_prompt}':"""
try:
response = model.generate_content(full_prompt)
if not response.text:
print("The AI returned an empty response. Please try again.")
input("Press Enter to return.")
return
# Clean and parse the response more robustly
ai_suggestions = []
lines = response.text.strip().split('\n')
for line in lines:
# Clean each line of common prefixes and formatting
cleaned = line.strip()
# Remove common prefixes like "1.", "- ", "• ", etc.
cleaned = re.sub(r'^[\d\.\-\\*\s]+', '', cleaned)
cleaned = cleaned.strip()
if cleaned and len(cleaned) > 1: # Ensure it's not just whitespace or single character
ai_suggestions.append(cleaned)
# Limit to 10 suggestions max
ai_suggestions = ai_suggestions[:10]
except Exception as e:
print(f"An error occurred while communicating with the AI: {e}")
input("Press Enter to return.")
return
if not ai_suggestions:
print("The AI didn't return any valid suggestions. Try a different prompt.")
input("Press Enter to return.")
return
print(f"\nAI suggested {len(ai_suggestions)} manga titles.")
# 4. Process suggestions: Search Jikan and get user confirmation
print("\n--- Confirm AI Suggestions ---")
print("For each suggestion, I will find the closest match on MyAnimeList.")
print("Please confirm if the match is correct.")
confirmed_manga = []
for i, suggestion in enumerate(ai_suggestions, 1):
print(f"\n[{i}/{len(ai_suggestions)}] Searching for: '{suggestion}'...")
results = jikan_api_request("/manga", params={"q": suggestion, "limit": 3})
if not results or not results.get('data'):
print(f"--> Could not find any match for '{suggestion}'.")
continue
# Show top match but also alternatives if the first doesn't seem right
match = results['data'][0]
title = match.get('title_english') or match.get('title')
year = match.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
print(f"--> Best match: '{title}' ({match.get('type', 'N/A')}, {year})")
# Show alternatives if available
if len(results['data']) > 1:
print(" Alternatives:")
for j, alt in enumerate(results['data'][1:3], 2):
alt_title = alt.get('title_english') or alt.get('title')
alt_year = alt.get('published', {}).get('prop', {}).get('from', {}).get('year', 'N/A')
print(f" [{j}] {alt_title} ({alt.get('type', 'N/A')}, {alt_year})")
while True:
if len(results['data']) > 1:
choice = input(" Choose: [1] Use best match, [2-3] Use alternative, [s] Skip, [Enter] Use best match: ").strip().lower()
else:
choice = input(" [Enter] Add this manga, [s] Skip: ").strip().lower()
if choice == '' or choice == '1':
selected_match = results['data'][0]
break
elif choice == 's':
selected_match = None
break
elif choice in ['2', '3'] and len(results['data']) > int(choice) - 1:
selected_match = results['data'][int(choice) - 1]
break
else:
print(" Invalid choice. Please try again.")
if selected_match:
formatted = format_manga_data(selected_match)
# Avoid adding duplicates
if any(a['id'] == formatted['id'] for a in confirmed_manga):
print(f"--> Already added '{formatted['name']}'. Skipping.")
else:
confirmed_manga.append(formatted)
print(f"--> Added '{formatted['name']}' to the list.")
else:
print(f"--> Skipped '{suggestion}'.")
# 5. Final review and add to section
if not confirmed_manga:
print("\nNo new manga were confirmed. Returning to menu.")
input("Press Enter...")
return
clear_screen()
print("--- Final Review ---")
print("The following new manga will be added to the section:")
for item in confirmed_manga:
print(f"- {item['name']}")
final_confirm = input("\nAdd these items to the section? [Y/n]: ").lower()
if final_confirm == '' or final_confirm == 'y':
added_count = 0
skipped_count = 0
for manga in confirmed_manga:
if not any(item['id'] == manga['id'] for item in section['items']):
section['items'].append(manga)
added_count += 1
else:
skipped_count += 1
print(f"\nSuccessfully added {added_count} new manga.")
if skipped_count > 0:
print(f"Skipped {skipped_count} manga that were already in the section.")
else:
print("Operation cancelled. No changes were made.")
input("Press Enter to continue...")
# --- Main Application ---
def main():
"""Main function to run the content manager."""
data = None
# Check if a content file exists and prompt the user.
if os.path.exists(CONTENT_FILE):
clear_screen()
print("--- Welcome Back ---")
print(f"Found existing content file: '{CONTENT_FILE}'")
print("\nWhat would you like to do?")
print("[1] Load the existing content")
print("[2] Start from scratch (Warning: saving will overwrite the old file)")
while data is None:
choice = input("> ")
if choice == '1':
try:
with open(CONTENT_FILE, 'r') as f:
data = json.load(f)
# Ensure the basic structure exists, in case the file is malformed
if 'spotlight' not in data: data['spotlight'] = []
if 'sections' not in data: data['sections'] = []
print("Content loaded successfully.")
except (json.JSONDecodeError, FileNotFoundError):
print(f"Error: Could not read or parse '{CONTENT_FILE}'. Starting from scratch.")
data = {"spotlight": [], "sections": []}
elif choice == '2':
print("Starting with a blank slate.")
data = {"spotlight": [], "sections": []}
else:
print("Invalid choice. Please enter 1 or 2.")
input("Press Enter to continue...")
else:
# If no content file exists, start from scratch automatically.
print(f"No '{CONTENT_FILE}' found. Starting with a blank slate.")
data = {"spotlight": [], "sections": []}
input("Press Enter to continue...")
while True:
clear_screen()
print("--- Manga Content Manager ---")
print(" (with AI-Powered Suggestions)")
print("\nSelect an option:")
print("[1] Manage Spotlight Section")
print("[2] Manage Horizontal Sections")
print("[3] Save and Exit")
print("[4] Exit Without Saving")
choice = input("> ")
if choice == '1':
manage_spotlight(data)
elif choice == '2':
manage_sections(data)
elif choice == '3':
with open(CONTENT_FILE, 'w') as f:
json.dump(data, f, indent=4)
print(f"Content saved to {CONTENT_FILE}.")
break
elif choice == '4':
print("Exiting without saving changes.")
break
else:
print("Invalid option. Please try again.")
input("Press Enter to continue...")
if __name__ == "__main__":
main()