585 lines
22 KiB
Python
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() |