This commit is contained in:
2026-03-29 20:52:57 -05:00
parent a97c3a5b57
commit cf155183f2
102 changed files with 55674 additions and 0 deletions

579
animex/get.py Normal file
View File

@@ -0,0 +1,579 @@
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 = "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_anime_data(anime_obj):
"""Formats Jikan anime data into the structure needed for content.json."""
return {
"id": anime_obj['mal_id'],
"name": anime_obj.get('title_english') or anime_obj.get('title'),
"image": anime_obj['images']['jpg']['large_image_url']
}
def search_and_select_anime():
"""Prompts user to search for an anime, 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("/anime", 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, anime in enumerate(results['data'], 1):
print(f"[{i}] {anime.get('title_english') or anime.get('title')} ({anime.get('type', 'N/A')}, {anime.get('year', 'N/A')})")
print("\n[b] Back to previous menu")
print("\nSelect an anime 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 an anime to Spotlight")
print("[2] Remove an anime from Spotlight")
print("[b] Back to Main Menu")
choice = input("> ").lower()
if choice == '1':
anime_obj = search_and_select_anime()
if anime_obj:
if any(item['id'] == anime_obj['mal_id'] for item in data['spotlight']):
print(f"'{anime_obj['title']}' is already in the spotlight.")
else:
formatted = format_anime_data(anime_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 anime 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 an anime to this section (Manual Search)")
print("[2] Remove an anime 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':
anime_obj = search_and_select_anime()
if anime_obj:
if any(item['id'] == anime_obj['mal_id'] for item in section['items']):
print(f"'{anime_obj['title']}' is already in this section.")
else:
formatted = format_anime_data(anime_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 anime 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 Anime by Popularity")
print("[2] Upcoming Season")
print("[3] Top Airing Anime")
print("[b] Cancel")
choice = get_choice(3)
if choice == 'b':
return
endpoint_map = {
1: ("/top/anime", {"filter": "bypopularity", "limit": 15}),
2: ("/seasons/upcoming", {"limit": 15}),
3: ("/top/anime", {"filter": "airing", "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 anime_obj in results['data']:
if not any(item['id'] == anime_obj['mal_id'] for item in section['items']):
section['items'].append(format_anime_data(anime_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 anime you want to find.")
print("Examples: 'top 10 classic sci-fi anime', 'underrated shows with great world-building', 'anime for beginners'")
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 anime that fit this description: '{user_prompt}'.
IMPORTANT: Follow this exact format for your response:
- Return ONLY the anime 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:
Attack on Titan
Death Note
Spirited Away
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)} anime 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_anime = []
for i, suggestion in enumerate(ai_suggestions, 1):
print(f"\n[{i}/{len(ai_suggestions)}] Searching for: '{suggestion}'...")
results = jikan_api_request("/anime", 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')
print(f"--> Best match: '{title}' ({match.get('type', 'N/A')}, {match.get('year', 'N/A')})")
# 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')
print(f" [{j}] {alt_title} ({alt.get('type', 'N/A')}, {alt.get('year', 'N/A')})")
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 anime, [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_anime_data(selected_match)
# Avoid adding duplicates
if any(a['id'] == formatted['id'] for a in confirmed_anime):
print(f"--> Already added '{formatted['name']}'. Skipping.")
else:
confirmed_anime.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_anime:
print("\nNo new anime were confirmed. Returning to menu.")
input("Press Enter...")
return
clear_screen()
print("--- Final Review ---")
print("The following new anime will be added to the section:")
for item in confirmed_anime:
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 anime in confirmed_anime:
if not any(item['id'] == anime['id'] for item in section['items']):
section['items'].append(anime)
added_count += 1
else:
skipped_count += 1
print(f"\nSuccessfully added {added_count} new anime.")
if skipped_count > 0:
print(f"Skipped {skipped_count} anime 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("--- Anime 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()