init
This commit is contained in:
579
animex/get.py
Normal file
579
animex/get.py
Normal 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()
|
||||
Reference in New Issue
Block a user