Commit a87618c6 by PLN (Algolia)

chore: Remove Paradiso

parent 84be633f
import React, { useState } from 'react';
import Image from 'next/image';
import styles from '@/styles/paradiso.module.css';
const MovieCard = ({ movie, onVote }) => {
const [isHovered, setIsHovered] = useState(false);
const [isVoting, setIsVoting] = useState(false);
const handleVote = async (e) => {
e.preventDefault();
e.stopPropagation();
if (isVoting) return;
setIsVoting(true);
await onVote(movie.objectID);
setIsVoting(false);
};
return (
<div
className={styles.movieCard}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div className={styles.posterContainer}>
{movie.poster ? (
<Image
src={movie.poster}
alt={movie.title}
width={200}
height={300}
className={styles.poster}
unoptimized
/>
) : (
<div className={styles.noPoster}>
<span>{movie.title}</span>
</div>
)}
{isHovered && (
<div className={styles.movieOverlay}>
<h3 className={styles.movieTitle}>{movie.title}</h3>
<div className={styles.movieYear}>{movie.year}</div>
<div className={styles.movieRating}>
{movie.imdbRating || 'N/A'}
</div>
<div className={styles.movieGenres}>
{movie.genre && movie.genre.slice(0, 3).join(' • ')}
</div>
<p className={styles.moviePlot}>
{movie.plot && movie.plot.length > 150
? `${movie.plot.substring(0, 150)}...`
: movie.plot}
</p>
<div className={styles.movieActions}>
<button
className={styles.voteButton}
onClick={handleVote}
disabled={isVoting}
>
{isVoting ? 'Voting...' : `Vote (${movie.votes || 0})`}
</button>
</div>
</div>
)}
</div>
<div className={styles.movieInfo}>
<h4>{movie.title}</h4>
<div className={styles.movieMeta}>
<span>{movie.year}</span>
<span className={styles.votes}>{movie.votes || 0} votes</span>
</div>
</div>
</div>
);
};
export default MovieCard;
\ No newline at end of file
import React, { useState } from 'react';
import styles from '../styles/paradiso.module.css';
const MovieSearch = ({ onAddMovie }) => {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [searching, setSearching] = useState(false);
const [error, setError] = useState(null);
// Function to search for movies via TMDB
const searchMovies = async (e) => {
e.preventDefault();
if (!searchQuery.trim()) return;
setSearching(true);
setError(null);
try {
const response = await fetch(
`https://api.themoviedb.org/3/search/movie?api_key=3e1dd2bcd5e1265d986c9a1501d6f8c0&query=${encodeURIComponent(searchQuery)}&language=en-US&page=1&include_adult=false`
);
if (!response.ok) {
throw new Error('Failed to search for movies');
}
const data = await response.json();
setSearchResults(data.results || []);
} catch (error) {
console.error('Error searching for movies:', error);
setError('Failed to search for movies. Please try again later.');
setSearchResults([]);
} finally {
setSearching(false);
}
};
// Function to add a movie from search results
const handleAddMovie = (movie) => {
const newMovieObj = {
id: movie.id.toString(),
title: movie.title,
votes: 0,
addedDate: new Date().toISOString(),
poster: movie.poster_path ? `https://image.tmdb.org/t/p/w500${movie.poster_path}` : null,
description: movie.overview || null,
year: movie.release_date ? movie.release_date.substring(0, 4) : null
};
onAddMovie(newMovieObj);
setSearchResults([]);
setSearchQuery('');
};
return (
<div className={styles.searchContainer}>
<h3>Find a Movie</h3>
{/* Search Form */}
<form onSubmit={searchMovies} className={styles.searchBar}>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search for a movie..."
/>
<button
type="submit"
disabled={searching || !searchQuery.trim()}
>
{searching ? 'Searching...' : 'Search'}
</button>
</form>
{/* Error Message */}
{error && (
<div className={styles.errorMessage}>
{error}
</div>
)}
{/* Search Results */}
{searchResults.length > 0 && (
<div className={styles.searchResults}>
<h4>Search Results</h4>
<div className={styles.movieGrid}>
{searchResults.slice(0, 5).map(movie => (
<div key={movie.id} className={styles.searchResultItem}>
{movie.poster_path ? (
<img
src={`https://image.tmdb.org/t/p/w200${movie.poster_path}`}
alt={movie.title}
className={styles.searchResultPoster}
/>
) : (
<div className={styles.searchResultPoster} style={{
backgroundColor: '#eee',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#999'
}}>
No Image
</div>
)}
<div className={styles.searchResultInfo}>
<h5>{movie.title} {movie.release_date && `(${movie.release_date.substring(0, 4)})`}</h5>
{movie.overview && (
<p>{movie.overview.length > 100 ? `${movie.overview.substring(0, 100)}...` : movie.overview}</p>
)}
<button
onClick={() => handleAddMovie(movie)}
className={styles.addButton}
>
Add to List
</button>
</div>
</div>
))}
</div>
</div>
)}
{/* No Results Message */}
{searchResults.length === 0 && searchQuery && !searching && !error && (
<div className={styles.noResults}>
No results found for "{searchQuery}". Try a different search term.
</div>
)}
</div>
);
};
export default MovieSearch;
\ No newline at end of file
# Paradiso Bot Deployment Guide
This guide will help you deploy the Paradiso movie voting bot on your Debian server or using Replit, as you prefer.
## Environment Variables
The bot requires the following environment variables:
- `DISCORD_TOKEN`: Your Discord bot token
- `ALGOLIA_APP_ID`: Your Algolia application ID
- `ALGOLIA_API_KEY`: Your Algolia API key
- `ALGOLIA_MOVIES_INDEX`: The Algolia index for movies (e.g., `paradiso_movies`)
- `ALGOLIA_VOTES_INDEX`: The Algolia index for votes (e.g., `paradiso_votes`)
- `MOVIE_DATA_SOURCE`: Preferred movie data source (`tmdb`, `omdb`, or `fallback`, defaults to `tmdb`)
At least one of the following API keys is required:
- `TMDB_API_KEY`: Your TMDB API key
- `OMDB_API_KEY`: Your OMDB API key
## Option 1: Deploy on Your Debian Server
### Prerequisites
- Debian 9+ (tested on Debian 9.13 Stretch)
- Python 3.7+ (Python 3.5+ should work, but 3.7+ is recommended)
- pip (Python package manager)
- systemd for service management
### Installation Steps
1. Log in to your server and create a directory for the bot:
```bash
mkdir -p /opt/paradiso-bot
cd /opt/paradiso-bot
```
2. Download the bot files or clone the repository:
```bash
# If you have git installed
git clone https://your-repo-url.git .
# Or manually download and upload the files
```
3. Set up a Python virtual environment (recommended):
```bash
# Install venv if not already installed
apt-get update
apt-get install -y python3-venv
# Create and activate virtual environment
python3 -m venv venv
source venv/bin/activate
```
4. Install the required dependencies:
```bash
pip install discord.py python-dotenv algoliasearch requests
```
5. Create a `.env` file:
```bash
nano .env
```
6. Add your environment variables to the `.env` file:
```
DISCORD_TOKEN=your_discord_token
ALGOLIA_APP_ID=your_algolia_app_id
ALGOLIA_API_KEY=your_algolia_api_key
ALGOLIA_MOVIES_INDEX=paradiso_movies
ALGOLIA_VOTES_INDEX=paradiso_votes
MOVIE_DATA_SOURCE=tmdb
TMDB_API_KEY=your_tmdb_api_key
OMDB_API_KEY=your_omdb_api_key
```
7. Create a systemd service file:
```bash
sudo nano /etc/systemd/system/paradiso-bot.service
```
8. Add the following content to the service file:
```
[Unit]
Description=Paradiso Discord Bot
After=network.target
[Service]
User=your_username
WorkingDirectory=/opt/paradiso-bot
ExecStart=/opt/paradiso-bot/venv/bin/python bot.py
Restart=on-failure
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
WantedBy=multi-user.target
```
9. Enable and start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl enable paradiso-bot.service
sudo systemctl start paradiso-bot.service
```
10. Check the status of the service:
```bash
sudo systemctl status paradiso-bot.service
```
### Changing Your Server Hostname (from erable.plnech.fr to nech.pl)
To change your server hostname on Debian:
1. Edit the hostname file:
```bash
sudo nano /etc/hostname
```
2. Replace the current hostname with the new one:
```
nech.pl
```
3. Edit the hosts file:
```bash
sudo nano /etc/hosts
```
4. Update the relevant line:
```
127.0.1.1 nech.pl
```
5. Apply the changes:
```bash
sudo hostname nech.pl
```
6. Restart networking and related services:
```bash
sudo systemctl restart networking
```
7. If you have configured DNS records, update them by:
- Logging into your domain registrar or DNS provider
- Updating the A/AAAA record for `nech.pl` to point to your server's IP
- If using SSL certificates, you may need to renew them for the new domain
8. Reboot your server to ensure all services are using the new hostname:
```bash
sudo reboot
```
## Option 2: Deploy on Replit
### Prerequisites
- A Replit account
- A UptimeRobot account (to keep the bot awake)
### Installation Steps
1. Go to [Replit](https://replit.com) and sign up or log in
2. Click the "+ Create" button
3. Select "Python" as the template
4. Name your repl "ParadisoBot" or similar
5. Click "Create Repl"
6. Upload the `bot.py` file to your Repl
7. Create a `keep_alive.py` file with the following content:
```python
from flask import Flask
from threading import Thread
app = Flask('')
@app.route('/')
def home():
return "Paradiso Bot is alive!"
def run():
app.run(host='0.0.0.0', port=8080)
def keep_alive():
t = Thread(target=run)
t.start()
```
8. Modify the end of your `bot.py` file to use the keep_alive function:
```python
# At the top of the file, add:
from keep_alive import keep_alive
# At the bottom of your file, replace:
if __name__ == "__main__":
client.run(DISCORD_TOKEN)
# With:
if __name__ == "__main__":
keep_alive() # Keep the bot alive
client.run(DISCORD_TOKEN)
```
9. Add the environment variables in Replit:
- Click on the 🔒 icon in the sidebar (or find "Secrets" in the "Tools" menu)
- Add each of the environment variables listed above
10. Create a `pyproject.toml` file for dependencies:
```toml
[tool.poetry]
name = "paradiso-bot"
version = "0.1.0"
description = "Discord bot for Paradiso movie night voting"
authors = ["Your Name <your.email@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
discord = "^2.0.0"
python-dotenv = "^0.21.0"
algoliasearch = "^2.6.2"
requests = "^2.28.1"
Flask = "^2.2.2"
wikipedia = "^1.4.0" # For fallback movie data
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```
11. Click the "Run" button to start the bot
12. To keep the bot running 24/7, set up UptimeRobot:
- Go to [UptimeRobot](https://uptimerobot.com/)
- Create a free account
- Click "Add New Monitor"
- Select "HTTP(s)" as the monitor type
- Enter a friendly name like "Paradiso Bot"
- Enter the URL of your Repl webview (e.g., `https://paradisobot.yourusername.repl.co`)
- Set the monitoring interval to 5 minutes
- Click "Create Monitor"
## Movie Data Source Configuration
The bot supports multiple movie data sources in a fallback cascade:
1. Configure which API to use as primary with the `MOVIE_DATA_SOURCE` environment variable:
- `tmdb`: Use The Movie Database API (recommended)
- `omdb`: Use Open Movie Database API
- `fallback`: Attempt to use all available sources in priority order
2. The system will work with any of these configurations:
- TMDB API only: Set `TMDB_API_KEY` and `MOVIE_DATA_SOURCE=tmdb`
- OMDB API only: Set `OMDB_API_KEY` and `MOVIE_DATA_SOURCE=omdb`
- Both APIs: Set both keys and choose your preferred order with `MOVIE_DATA_SOURCE`
3. If both APIs fail, the bot will make a best effort to find information using public sources.
## Verifying the Bot is Working
1. Invite the bot to your Discord server using the OAuth2 URL from the Discord Developer Portal
2. Try using the `/help` command in your server to see all available commands
3. Test adding a movie with `/add [movie title]`
## Monitoring and Troubleshooting
### Checking Logs
On your Debian server, you can check the bot logs using:
```bash
sudo journalctl -u paradiso-bot.service
```
For recent logs only:
```bash
sudo journalctl -u paradiso-bot.service -n 50 --no-pager
```
To follow logs in real-time:
```bash
sudo journalctl -u paradiso-bot.service -f
```
### Common Issues
- **Bot not responding to commands**:
- Check if the bot is running: `systemctl status paradiso-bot`
- Verify Discord token is correct
- Ensure the bot has the proper permissions in Discord
- **Movie search not working**:
- Check if TMDB/OMDB API keys are correct
- Look for API rate limiting errors in logs
- Try switching between data sources
- **Algolia operations failing**:
- Verify Algolia credentials and index names
- Check if indices have been properly created
- Ensure you have the right permissions set for your API key
## Updating the Bot
When you want to update the bot:
1. Stop the current running instance:
```bash
sudo systemctl stop paradiso-bot.service
```
2. Update the code files:
```bash
cd /opt/paradiso-bot
# Pull updates from git or upload new files
```
3. Restart the service:
```bash
sudo systemctl start paradiso-bot.service
```
## Additional Resources
- [Discord.py Documentation](https://discordpy.readthedocs.io/)
- [Algolia Documentation](https://www.algolia.com/doc/)
- [TMDB API Documentation](https://developers.themoviedb.org/3)
- [OMDB API Documentation](http://www.omdbapi.com/)
- [Debian Service Management](https://wiki.debian.org/systemd)
- [Replit Documentation](https://docs.replit.com/)
- [UptimeRobot Documentation](https://uptimerobot.com/help/)
\ No newline at end of file
# Paradiso - Movie Night Voting System
Paradiso is a complete movie night voting system consisting of:
1. A Next.js web interface integrated with your personal site
2. A Discord bot for interaction through Discord
3. Algolia as a database and search engine
4. OMDb API for movie metadata
This system allows you and your friends to vote on movies for your next movie night, ensuring a fair and fun selection process.
## System Architecture
- **Data Storage**: [Algolia](https://www.algolia.com/) (Search & Database)
- **Movie Data**: [OMDb API](https://www.omdbapi.com/) (Open Movie Database)
- **Frontend**: Next.js component on your personal site
- **Bot**: Python Discord bot using discord.py
## Setup Instructions
### Step 1: Set Up Algolia
1. Create a free [Algolia](https://www.algolia.com/) account
2. Create a new application (or use an existing one)
3. Get your Application ID and Admin API Key from the dashboard
4. Run the setup script to configure indices and generate API keys:
```bash
# Install the required package
pip install algoliasearch
# Run the setup script
python setup.py --admin-key YOUR_ADMIN_API_KEY --app-id YOUR_APP_ID
```
The setup script creates:
- A `paradiso_movies` index for storing movie data
- A `paradiso_votes` index for tracking votes
- Necessary API keys with appropriate permissions
- Configuration files for the web app and Discord bot
### Step 2: Get an OMDb API Key
1. Go to [OMDb API](https://www.omdbapi.com/apikey.aspx)
2. Sign up for a free API key (1,000 daily requests)
3. Check your email and activate your key
4. Add your key to the `.env.frontend` and `.env.bot` files generated by the setup script
### Step 3: Set Up the Next.js Frontend
1. Create the API endpoints in your Next.js project:
- `pages/api/paradiso/vote.js` - For voting on movies
- `pages/api/paradiso/search-movie.js` - For searching movies via OMDb API
- `pages/api/paradiso/add-movie.js` - For adding movies to Algolia
2. Create the Paradiso page:
- `pages/paradiso/index.js` - The main movie voting interface
3. Install the required packages:
```bash
npm install algoliasearch react-instantsearch-dom
```
4. Add the environment variables from `.env.frontend` to your Next.js project:
```bash
# .env.local in your Next.js project
NEXT_PUBLIC_ALGOLIA_APP_ID=YOUR_APP_ID
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=YOUR_SEARCH_KEY
NEXT_PUBLIC_ALGOLIA_INDEX=paradiso_movies
ALGOLIA_ADMIN_API_KEY=YOUR_ADMIN_API_KEY
OMDB_API_KEY=YOUR_OMDB_API_KEY
```
### Step 4: Set Up the Discord Bot
1. Create a new Discord application and bot at the [Discord Developer Portal](https://discord.com/developers/applications)
2. Get your bot token
3. Add the bot to your Discord server with appropriate permissions:
- Read Messages/View Channels
- Send Messages
- Use Slash Commands
- Embed Links
4. Install the required Python packages:
```bash
pip install discord.py python-dotenv algoliasearch requests
```
5. Create a `.env` file for the bot with the environment variables from `.env.bot`:
```bash
# .env for the Discord bot
DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN
ALGOLIA_APP_ID=YOUR_APP_ID
ALGOLIA_API_KEY=YOUR_SECURED_API_KEY
ALGOLIA_MOVIES_INDEX=paradiso_movies
ALGOLIA_VOTES_INDEX=paradiso_votes
OMDB_API_KEY=YOUR_OMDB_API_KEY
```
6. Run the bot:
```bash
python bot.py
```
## Usage
### Web Interface
The web interface will be available at `https://your-site.com/paradiso`. Here, users can:
- Search for movies
- Add new movies
- Vote for movies
- See the top voted movies
### Discord Bot
The Discord bot provides the following slash commands:
- `/movies` - List all movies in the voting queue
- `/add [title]` - Add a movie to the voting queue
- `/vote [title]` - Vote for a movie in the queue
- `/remove [title]` - Remove a movie from the voting queue (admin only)
- `/top [count]` - Show the top voted movies (default: top 5)
- `/random` - Suggest a random movie from the list
- `/help` - Show help for all commands
## Security Considerations
The system is secured in the following ways:
1. **Frontend**: Uses a secured API key with restricted permissions
2. **Bot**: Uses a different secured API key with its own permissions
3. **Rate Limiting**: API calls are rate-limited to prevent abuse
4. **User Tokens**: Uses tokens to identify users and prevent duplicate votes
## Limitations and Notes
- The free tier of Algolia provides 10,000 records and 10,000 operations per month, more than enough for a personal movie voting system
- The free tier of OMDb API allows 1,000 requests per day
- The secured API keys generated by the setup script are valid for 1 year, after which you'll need to generate new ones
## Customization
You can customize various aspects of the system:
- Change the index prefix in the setup script
- Modify the web interface styling to match your site
- Adjust the Discord bot's embed colors and messages
- Add additional commands to the bot
## Troubleshooting
- If votes aren't being recorded, check your Algolia API keys and permissions
- If movie searches fail, verify your OMDb API key is active
- If the Discord bot isn't responding, ensure it has the correct permissions in your server
## Future Improvements
- Add user authentication for the web interface
- Implement more advanced voting mechanics (e.g., ranked voting)
- Add movie night scheduling features
- Create a shared watchlist for watched movies
## Credits
- Movie data provided by [OMDb API](https://www.omdbapi.com/)
- Search and database powered by [Algolia](https://www.algolia.com/)
- Discord integration using [discord.py](https://discordpy.readthedocs.io/)
\ No newline at end of file
# Hosting the Paradiso Discord Bot for Free
This guide explains how to host your Paradiso Discord bot for free using Replit, ensuring it runs 24/7 without any costs.
## What is Replit?
[Replit](https://replit.com) is a browser-based IDE that allows you to write, run, and host code in the cloud. It's perfect for hosting Discord bots because:
1. It offers a free tier with no credit card required
2. It can keep your bot running 24/7 (with some setup)
3. It's easy to use and doesn't require server management
## Step 1: Create a Replit Account
1. Go to [Replit](https://replit.com)
2. Sign up for a free account
3. Verify your email address
## Step 2: Create a New Repl
1. Click the "+ Create" button
2. Select "Python" as the template
3. Name your repl "ParadisoBot" or something similar
4. Click "Create Repl"
## Step 3: Set Up Your Bot Files
1. Upload the `paradiso_bot.py` file to your Repl
2. Create a new file called `keep_alive.py` with the following code:
```python
from flask import Flask
from threading import Thread
app = Flask('')
@app.route('/')
def home():
return "Paradiso Bot is alive!"
def run():
app.run(host='0.0.0.0', port=8080)
def keep_alive():
t = Thread(target=run)
t.start()
```
3. Modify the end of your `paradiso_bot.py` file to use the keep_alive function:
```python
# At the top of the file, add:
from keep_alive import keep_alive
# At the bottom of your file, replace:
if __name__ == "__main__":
client.run(DISCORD_TOKEN)
# With:
if __name__ == "__main__":
keep_alive() # Keep the bot alive
client.run(DISCORD_TOKEN)
```
## Step 4: Set Up Environment Variables
Replit provides a secure way to store sensitive information like API keys.
1. In your Repl, click on the 🔒 icon in the sidebar (or find "Secrets" in the "Tools" menu)
2. Add the following secrets:
- Key: `DISCORD_TOKEN`, Value: `your-discord-bot-token`
- Key: `ALGOLIA_APP_ID`, Value: `your-algolia-app-id`
- Key: `ALGOLIA_API_KEY`, Value: `your-algolia-api-key`
- Key: `ALGOLIA_MOVIES_INDEX`, Value: `paradiso_movies`
- Key: `ALGOLIA_VOTES_INDEX`, Value: `paradiso_votes`
- Key: `OMDB_API_KEY`, Value: `your-omdb-api-key`
## Step 5: Install Dependencies
1. Create a new file called `pyproject.toml` with the following content:
```toml
[tool.poetry]
name = "paradiso-bot"
version = "0.1.0"
description = "Discord bot for Paradiso movie night voting"
authors = ["Your Name <your.email@example.com>"]
[tool.poetry.dependencies]
python = "^3.8"
discord = "^2.0.0"
python-dotenv = "^0.21.0"
algoliasearch = "^2.6.2"
requests = "^2.28.1"
Flask = "^2.2.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
```
2. Replit will automatically install the dependencies when you run the bot
## Step 6: Run Your Bot
1. Click the "Run" button at the top of the page
2. Your bot should start up and connect to Discord
3. You'll see a webview showing "Paradiso Bot is alive!"
## Step 7: Keep Your Bot Running 24/7
By default, Replit will stop your bot after some time of inactivity. To keep it running:
1. Go to [UptimeRobot](https://uptimerobot.com/)
2. Create a free account
3. Click "Add New Monitor"
4. Select "HTTP(s)" as the monitor type
5. Enter a friendly name like "Paradiso Bot"
6. Enter the URL of your Repl webview (e.g., `https://paradisobot.yourusername.repl.co`)
7. Set the monitoring interval to 5 minutes
8. Click "Create Monitor"
UptimeRobot will now ping your bot every 5 minutes, keeping it alive 24/7.
## Step 8: Update Your Bot
To update your bot when you make changes:
1. Make changes to the files in your Repl
2. Click the "Stop" button if your bot is running
3. Click the "Run" button to restart your bot with the changes
## Alternative Free Hosting Options
If you prefer not to use Replit, here are some alternatives:
### Railway
[Railway](https://railway.app/) offers a generous free tier:
- 5 projects
- 500 hours of runtime per month
- 1GB memory per container
### Render
[Render](https://render.com/) offers a free tier for web services:
- Free for web services (sleeps after 15 minutes of inactivity)
- Wakes up when receiving a request
### Oracle Cloud Free Tier
[Oracle Cloud](https://www.oracle.com/cloud/free/) offers always-free services:
- 2 AMD-based Compute VMs
- 4 ARM-based Ampere A1 cores and 24 GB memory
- 200 GB of storage
## Troubleshooting
- **Bot crashes or doesn't respond**: Check the console output in Replit for error messages
- **UptimeRobot says the site is down**: Make sure your Flask app is running on port 8080
- **Bot doesn't respond to commands**: Ensure your bot has the correct permissions in your Discord server
- **Algolia operations fail**: Check that your API keys and indices are correctly configured
## Notes
- Replit's free tier may occasionally experience slowdowns during high-traffic periods
- The bot might briefly go offline when Replit performs maintenance updates
- For a more robust solution, consider upgrading to Replit's paid plan or hosting on a VPS
## Additional Resources
- [Replit Documentation](https://docs.replit.com/)
- [Discord.py Documentation](https://discordpy.readthedocs.io/)
- [UptimeRobot Documentation](https://uptimerobot.com/help/)
\ No newline at end of file
#!/usr/bin/env python
"""
Paradiso Discord Bot
A Discord bot for the Paradiso movie voting system, using Algolia for data storage.
Requirements:
- Python 3.7+
- discord.py
- python-dotenv
- algoliasearch
- requests
Usage:
1. Install dependencies: pip install discord.py python-dotenv algoliasearch requests
2. Set up a Discord bot in the Discord Developer Portal
3. Create a .env file with your Discord bot token and Algolia credentials
4. Run the bot: python paradiso_bot.py
"""
import os
import json
import random
import logging
import time
import datetime
import abc
from typing import List, Dict, Any, Optional, Union
import discord
from discord import app_commands
from dotenv import load_dotenv
import requests
from algoliasearch.search_client import SearchClient
import re
import urllib.parse
try:
import wikipedia
WIKIPEDIA_AVAILABLE = True
except ImportError:
WIKIPEDIA_AVAILABLE = False
print("Wikipedia module not installed. Wikipedia fallback won't be available.")
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler("paradiso_bot.log"),
logging.StreamHandler()
]
)
logger = logging.getLogger("paradiso_bot")
# Load environment variables
load_dotenv()
DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
ALGOLIA_APP_ID = os.getenv('ALGOLIA_APP_ID')
ALGOLIA_API_KEY = os.getenv('ALGOLIA_API_KEY')
ALGOLIA_MOVIES_INDEX = os.getenv('ALGOLIA_MOVIES_INDEX')
ALGOLIA_VOTES_INDEX = os.getenv('ALGOLIA_VOTES_INDEX')
OMDB_API_KEY = os.getenv('OMDB_API_KEY')
TMDB_API_KEY = os.getenv('TMDB_API_KEY')
MOVIE_DATA_SOURCE = os.getenv('MOVIE_DATA_SOURCE', 'tmdb')
# Check if all environment variables are set
if not all([DISCORD_TOKEN, ALGOLIA_APP_ID, ALGOLIA_API_KEY,
ALGOLIA_MOVIES_INDEX, ALGOLIA_VOTES_INDEX]):
logger.error("Missing required environment variables. Please check your .env file.")
exit(1)
# Movie Data Source Abstraction
class MovieDataSource(abc.ABC):
"""Base movie data source interface."""
@abc.abstractmethod
async def search_by_title(self, title: str) -> Optional[Dict[str, Any]]:
"""Search for a movie by title."""
pass
@abc.abstractmethod
async def search_by_id(self, movie_id: str, id_type: str = 'imdb') -> Optional[Dict[str, Any]]:
"""Search for a movie by ID."""
pass
@abc.abstractmethod
def normalize_movie(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize movie data to a consistent format."""
pass
class OMDBDataSource(MovieDataSource):
"""OMDB API implementation."""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "http://www.omdbapi.com/"
async def search_by_title(self, title: str) -> Optional[Dict[str, Any]]:
"""Search for a movie using the OMDb API."""
try:
url = f"{self.base_url}?apikey={self.api_key}&t={title}&plot=full"
response = requests.get(url)
response.raise_for_status()
data = response.json()
if data.get('Response') == 'False':
return None
return self.normalize_movie(data)
except Exception as e:
logger.error(f"Error searching movie on OMDb: {e}")
return None
async def search_by_id(self, movie_id: str, id_type: str = 'imdb') -> Optional[Dict[str, Any]]:
"""Search for a movie by ID using the OMDb API."""
if id_type != 'imdb':
raise ValueError("OMDB only supports IMDB IDs")
try:
url = f"{self.base_url}?apikey={self.api_key}&i={movie_id}&plot=full"
response = requests.get(url)
response.raise_for_status()
data = response.json()
if data.get('Response') == 'False':
return None
return self.normalize_movie(data)
except Exception as e:
logger.error(f"Error searching movie by ID on OMDb: {e}")
return None
def normalize_movie(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize OMDB movie data."""
return {
"id": raw_data["imdbID"],
"title": raw_data["Title"],
"original_title": raw_data["Title"],
"year": int(raw_data["Year"]) if raw_data.get("Year", "N/A").isdigit() else None,
"director": raw_data.get("Director", "Unknown"),
"actors": raw_data.get("Actors", "").split(", ") if raw_data.get("Actors") else [],
"genre": raw_data.get("Genre", "").split(", ") if raw_data.get("Genre") else [],
"plot": raw_data.get("Plot", ""),
"poster": raw_data.get("Poster") if raw_data.get("Poster") != "N/A" else None,
"imdb_rating": float(raw_data["imdbRating"]) if raw_data.get("imdbRating", "N/A") != "N/A" else None,
"imdb_id": raw_data["imdbID"],
"tmdb_id": None,
"source": "omdb",
"raw_data": raw_data
}
class TMDBDataSource(MovieDataSource):
"""TMDB API implementation."""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.themoviedb.org/3"
self.image_base_url = "https://image.tmdb.org/t/p/w500"
async def search_by_title(self, title: str) -> Optional[Dict[str, Any]]:
"""Search for a movie using the TMDB API."""
try:
# First search for the movie
url = f"{self.base_url}/search/movie?api_key={self.api_key}&query={title}&include_adult=false"
response = requests.get(url)
response.raise_for_status()
data = response.json()
if not data.get('results') or len(data['results']) == 0:
return None
# Get the most relevant result
movie_id = data['results'][0]['id']
# Get detailed info about the movie
return await self.search_by_id(str(movie_id), 'tmdb')
except Exception as e:
logger.error(f"Error searching movie on TMDB: {e}")
return None
async def search_by_id(self, movie_id: str, id_type: str = 'tmdb') -> Optional[Dict[str, Any]]:
"""Search for a movie by ID using the TMDB API."""
try:
tmdb_id = movie_id
# If ID is IMDb ID, first search for TMDB ID
if id_type == 'imdb':
find_url = f"{self.base_url}/find/{movie_id}?api_key={self.api_key}&external_source=imdb_id"
find_response = requests.get(find_url)
find_response.raise_for_status()
find_data = find_response.json()
if not find_data.get('movie_results') or len(find_data['movie_results']) == 0:
return None
tmdb_id = str(find_data['movie_results'][0]['id'])
# Get detailed movie info
details_url = f"{self.base_url}/movie/{tmdb_id}?api_key={self.api_key}&append_to_response=credits"
details_response = requests.get(details_url)
details_response.raise_for_status()
movie_data = details_response.json()
return self.normalize_movie(movie_data)
except Exception as e:
logger.error(f"Error searching movie by ID on TMDB: {e}")
return None
def normalize_movie(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""Normalize TMDB movie data."""
# Extract director from credits
director = "Unknown"
if raw_data.get('credits') and raw_data['credits'].get('crew'):
directors = [member['name'] for member in raw_data['credits']['crew']
if member.get('job') == 'Director']
director = ", ".join(directors) if directors else "Unknown"
# Extract actors from credits
actors = []
if raw_data.get('credits') and raw_data['credits'].get('cast'):
actors = [actor['name'] for actor in raw_data['credits']['cast'][:5]]
return {
"id": str(raw_data["id"]),
"title": raw_data["title"],
"original_title": raw_data.get("original_title", raw_data["title"]),
"year": int(raw_data["release_date"][:4]) if raw_data.get("release_date") else None,
"director": director,
"actors": actors,
"genre": [genre['name'] for genre in raw_data.get("genres", [])],
"plot": raw_data.get("overview", ""),
"poster": f"{self.image_base_url}{raw_data['poster_path']}" if raw_data.get("poster_path") else None,
"imdb_rating": float(raw_data["vote_average"]) if raw_data.get("vote_average") else None,
"imdb_id": raw_data.get("imdb_id"),
"tmdb_id": str(raw_data["id"]),
"source": "tmdb",
"raw_data": raw_data
}
class WikipediaDataSource(MovieDataSource):
"""Wikipedia fallback implementation."""
def __init__(self):
self.image_pattern = re.compile(r'https?://.*?\.(?:jpg|jpeg|png|gif)')
async def search_by_title(self, title: str) -> Optional[Dict[str, Any]]:
"""Search for a movie using Wikipedia."""
if not WIKIPEDIA_AVAILABLE:
return None
try:
# Search for the movie with "film" appended to improve results accuracy
search_query = f"{title} film"
search_results = wikipedia.search(search_query, results=5)
if not search_results:
return None
# Try to find the most relevant page
page_title = None
for result in search_results:
if "film" in result.lower() or "movie" in result.lower():
page_title = result
break
# If no film-specific result was found, use the first result
if not page_title and search_results:
page_title = search_results[0]
if not page_title:
return None
# Get the page content
page = wikipedia.page(page_title, auto_suggest=False)
# Try to extract basic information
return self.normalize_movie({
"title": page.title,
"content": page.content,
"summary": page.summary,
"url": page.url,
"images": page.images
})
except Exception as e:
logger.error(f"Error searching Wikipedia: {e}")
return None
async def search_by_id(self, movie_id: str, id_type: str = 'wiki') -> Optional[Dict[str, Any]]:
"""Not directly supported for Wikipedia."""
return None
def normalize_movie(self, raw_data: Dict[str, Any]) -> Dict[str, Any]:
"""Extract structured movie data from Wikipedia content."""
# Get a unique ID based on the URL
url_parts = urllib.parse.urlparse(raw_data["url"])
path_parts = url_parts.path.split('/')
wiki_id = path_parts[-1] if path_parts else "unknown"
# Try to extract the year from the title (often in parentheses)
year_match = re.search(r'\((\d{4})(?: film)?\)', raw_data["title"])
year = int(year_match.group(1)) if year_match else None
# Clean up the title by removing year and "film" markers
title = re.sub(r'\s*\(\d{4}(?: film)?\)', '', raw_data["title"])
# Find a suitable image (movie poster if possible)
poster = None
for img_url in raw_data.get("images", []):
if self.image_pattern.search(img_url):
if 'poster' in img_url.lower():
poster = img_url
break
# If no poster-specific image was found, use the first image
if not poster and raw_data.get("images"):
for img_url in raw_data.get("images", []):
if self.image_pattern.search(img_url):
poster = img_url
break
# Try to extract director from content
director = "Unknown"
director_match = re.search(r'(?:Directed|Director)[^\n.]*?by\s+([^.,\n]+)', raw_data["content"])
if director_match:
director = director_match.group(1).strip()
return {
"id": f"wiki_{wiki_id}",
"title": title,
"original_title": title,
"year": year,
"director": director,
"actors": [], # Would need more complex parsing
"genre": [], # Would need more complex parsing
"plot": raw_data["summary"][:500] if raw_data.get("summary") else "",
"poster": poster,
"imdb_rating": None,
"imdb_id": None,
"tmdb_id": None,
"source": "wikipedia",
"raw_data": {
"title": raw_data["title"],
"url": raw_data["url"]
}
}
def create_movie_data_source(source_type: str, api_key: str = None) -> MovieDataSource:
"""Create a movie data source instance."""
if source_type.lower() == 'omdb':
if not api_key:
logger.warning("OMDB API key not provided, using fallback source")
return WikipediaDataSource() if WIKIPEDIA_AVAILABLE else None
return OMDBDataSource(api_key)
elif source_type.lower() == 'tmdb':
if not api_key:
logger.warning("TMDB API key not provided, using fallback source")
return WikipediaDataSource() if WIKIPEDIA_AVAILABLE else None
return TMDBDataSource(api_key)
elif source_type.lower() == 'wikipedia':
return WikipediaDataSource()
else:
raise ValueError(f"Unsupported movie data source: {source_type}")
# Initialize movie data sources with improved fallback logic
primary_data_source = None
fallback_data_sources = []
# Set up primary data source
if MOVIE_DATA_SOURCE == 'tmdb' and TMDB_API_KEY:
primary_data_source = create_movie_data_source('tmdb', TMDB_API_KEY)
elif MOVIE_DATA_SOURCE == 'omdb' and OMDB_API_KEY:
primary_data_source = create_movie_data_source('omdb', OMDB_API_KEY)
elif MOVIE_DATA_SOURCE == 'fallback':
# In fallback mode, try to use sources in priority order
if TMDB_API_KEY:
primary_data_source = create_movie_data_source('tmdb', TMDB_API_KEY)
elif OMDB_API_KEY:
primary_data_source = create_movie_data_source('omdb', OMDB_API_KEY)
# Set up fallback sources
if MOVIE_DATA_SOURCE != 'omdb' and OMDB_API_KEY:
fallback_data_sources.append(create_movie_data_source('omdb', OMDB_API_KEY))
if MOVIE_DATA_SOURCE != 'tmdb' and TMDB_API_KEY:
fallback_data_sources.append(create_movie_data_source('tmdb', TMDB_API_KEY))
# Add Wikipedia as last resort fallback
if WIKIPEDIA_AVAILABLE:
fallback_data_sources.append(create_movie_data_source('wikipedia'))
# Use Wikipedia directly if no API keys are available
if not primary_data_source and WIKIPEDIA_AVAILABLE:
primary_data_source = create_movie_data_source('wikipedia')
elif not primary_data_source:
logger.error("No movie data sources available. Bot will not be able to search for movies.")
primary_data_source = None
# Initialize Algolia client
algolia_client = SearchClient.create(ALGOLIA_APP_ID, ALGOLIA_API_KEY)
movies_index = algolia_client.init_index(ALGOLIA_MOVIES_INDEX)
votes_index = algolia_client.init_index(ALGOLIA_VOTES_INDEX)
# Set up Discord client
intents = discord.Intents.default()
intents.message_content = True
intents.members = True
client = discord.Client(intents=intents)
tree = app_commands.CommandTree(client)
# Helper Functions
def generate_user_token(user_id: str) -> str:
"""Generate a user token for Algolia based on Discord user ID."""
return f"discord_{user_id}"
async def search_movie(title: str) -> Optional[Dict[str, Any]]:
"""Search for a movie using configured data sources with full fallback cascade."""
if not primary_data_source:
logger.error("No movie data sources available")
return None
# Try primary data source first
movie_data = await primary_data_source.search_by_title(title)
# Try each fallback source in order until we find a result
if not movie_data:
for source in fallback_data_sources:
movie_data = await source.search_by_title(title)
if movie_data:
logger.info(f"Found movie using fallback source: {source.__class__.__name__}")
break
return movie_data
async def add_movie_to_algolia(movie_data: Dict[str, Any], user_id: str) -> Dict[str, Any]:
"""Add a movie to Algolia index."""
try:
# Format the movie data for Algolia
movie_obj = {
"objectID": movie_data["id"],
"title": movie_data["title"],
"originalTitle": movie_data["original_title"],
"year": movie_data["year"],
"director": movie_data["director"],
"actors": movie_data["actors"],
"genre": movie_data["genre"],
"plot": movie_data["plot"],
"poster": movie_data["poster"],
"imdbRating": movie_data["imdb_rating"],
"imdbID": movie_data["imdb_id"],
"tmdbID": movie_data["tmdb_id"],
"votes": 0,
"addedDate": int(time.time()),
"addedBy": generate_user_token(user_id),
"source": movie_data["source"]
}
# Save to Algolia
movies_index.save_object(movie_obj)
return movie_obj
except Exception as e:
logger.error(f"Error adding movie to Algolia: {e}")
raise
async def vote_for_movie(movie_id: str, user_id: str) -> bool:
"""Vote for a movie in Algolia."""
try:
user_token = generate_user_token(user_id)
# Check if user already voted for this movie
search_result = votes_index.search("", {
"filters": f"userToken:{user_token} AND movieId:{movie_id}"
})
if search_result["nbHits"] > 0:
return False # User already voted
# Record the vote
votes_index.save_object({
"objectID": f"{user_token}_{movie_id}",
"userToken": user_token,
"movieId": movie_id,
"timestamp": int(time.time())
})
# Increment the movie's vote count
movies_index.partial_update_object({
"objectID": movie_id,
"votes": {
"_operation": "Increment",
"value": 1
}
})
return True
except Exception as e:
logger.error(f"Error voting for movie: {e}")
return False
async def get_top_movies(count: int = 5) -> List[Dict[str, Any]]:
"""Get the top voted movies from Algolia."""
try:
search_result = movies_index.search("", {
"filters": "votes > 0",
"hitsPerPage": count,
"sortCriteria": ["votes:desc"]
})
return search_result["hits"]
except Exception as e:
logger.error(f"Error getting top movies: {e}")
return []
async def get_all_movies() -> List[Dict[str, Any]]:
"""Get all movies from Algolia."""
try:
search_result = movies_index.search("", {
"hitsPerPage": 100
})
return search_result["hits"]
except Exception as e:
logger.error(f"Error getting all movies: {e}")
return []
async def find_movie_by_title(title: str) -> Optional[Dict[str, Any]]:
"""Find a movie by title in Algolia."""
try:
search_result = movies_index.search(title, {
"hitsPerPage": 5
})
if search_result["nbHits"] == 0:
return None
# Try to find an exact match
for hit in search_result["hits"]:
if hit["title"].lower() == title.lower():
return hit
# Return the first result if no exact match
return search_result["hits"][0]
except Exception as e:
logger.error(f"Error finding movie by title: {e}")
return None
async def remove_movie(movie_id: str) -> bool:
"""Remove a movie from Algolia."""
try:
# Delete the movie
movies_index.delete_object(movie_id)
# Delete all votes for this movie
search_result = votes_index.search("", {
"filters": f"movieId:{movie_id}",
"hitsPerPage": 100
})
if search_result["nbHits"] > 0:
object_ids = [hit["objectID"] for hit in search_result["hits"]]
votes_index.delete_objects(object_ids)
return True
except Exception as e:
logger.error(f"Error removing movie: {e}")
return False
# Bot event handlers
@client.event
async def on_ready():
"""Handle bot ready event."""
logger.info(f'{client.user} has connected to Discord!')
# Sync commands
await tree.sync()
logger.info("Commands synced")
# Bot commands
@tree.command(name="movies", description="List all movies in the voting queue")
async def cmd_movies(interaction: discord.Interaction):
"""List all movies in the voting queue."""
await interaction.response.defer()
try:
movies = await get_all_movies()
if not movies:
await interaction.followup.send("No movies have been added yet! Use `/add` to add one.")
return
# Sort movies by vote count
movies.sort(key=lambda m: m.get("votes", 0), reverse=True)
# Create an embed
embed = discord.Embed(
title="🎬 Paradiso Movie Night Voting",
description=f"Here are the movies currently in the queue ({len(movies)} total):",
color=0x03a9f4,
timestamp=datetime.datetime.now()
)
# Add each movie to the embed
for i, movie in enumerate(movies[:10]): # Limit to top 10
title = movie.get("title", "Unknown")
year = f" ({movie.get('year')})" if movie.get("year") else ""
votes = movie.get("votes", 0)
medal = "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else f"{i+1}."
embed.add_field(
name=f"{medal} {title}{year} - {votes} votes",
value=movie.get("plot", "No description available.")[:100] + "..."
if movie.get("plot") and len(movie.get("plot")) > 100
else movie.get("plot", "No description available."),
inline=False
)
if len(movies) > 10:
embed.set_footer(text=f"Showing top 10 out of {len(movies)} movies. Use /movies_page to see more.")
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Error in /movies command: {e}")
await interaction.followup.send("An error occurred while getting the movies. Please try again.")
@tree.command(name="add", description="Add a movie to the voting queue")
@app_commands.describe(title="Title of the movie to add")
async def cmd_add(interaction: discord.Interaction, title: str):
"""Add a movie to the voting queue."""
await interaction.response.defer(thinking=True)
try:
# Search for the movie
movie_data = await search_movie(title)
if not movie_data:
await interaction.followup.send(f"❌ Could not find movie: '{title}'. Please check the title and try again.")
return
# Check if movie already exists in Algolia
search_result = movies_index.search("", {
"filters": f"objectID:{movie_data['id']}"
})
if search_result["nbHits"] > 0:
await interaction.followup.send(f"❌ '{movie_data['title']}' is already in the voting queue!")
return
# Add the movie to Algolia
movie_obj = await add_movie_to_algolia(movie_data, str(interaction.user.id))
# Create embed for movie
embed = discord.Embed(
title=f"🎬 Added: {movie_obj['title']} ({movie_obj['year'] if movie_obj['year'] else 'N/A'})",
description=movie_obj["plot"] if len(movie_obj["plot"]) < 300 else movie_obj["plot"][:297] + "...",
color=0x00ff00
)
if movie_obj["director"]:
embed.add_field(name="Director", value=movie_obj["director"], inline=True)
if movie_obj["actors"]:
embed.add_field(name="Starring", value=", ".join(movie_obj["actors"][:3]), inline=True)
if movie_obj["imdbRating"]:
embed.add_field(name="Rating", value=f"⭐ {movie_obj['imdbRating']}/10", inline=True)
if movie_obj["poster"]:
embed.set_thumbnail(url=movie_obj["poster"])
embed.set_footer(text=f"Added by {interaction.user.display_name} | Source: {movie_obj['source'].upper()}")
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Error in add command: {e}")
await interaction.followup.send(f"❌ An error occurred: {str(e)}")
@tree.command(name="vote", description="Vote for a movie in the queue")
@app_commands.describe(title="Title of the movie to vote for")
async def cmd_vote(interaction: discord.Interaction, title: str):
"""Vote for a movie in the queue."""
await interaction.response.defer(thinking=True)
try:
# Find the movie in Algolia
movie = await find_movie_by_title(title)
if not movie:
await interaction.followup.send(f"❌ Could not find '{title}' in the voting queue. Use /movies to see available movies.")
return
# Record the vote
user_token = generate_user_token(str(interaction.user.id))
# Check if user already voted for this movie
search_result = votes_index.search("", {
"filters": f"userToken:{user_token} AND movieId:{movie['objectID']}"
})
if search_result["nbHits"] > 0:
await interaction.followup.send(f"❌ You have already voted for '{movie['title']}'!")
return
# Record the vote
success = await vote_for_movie(movie["objectID"], str(interaction.user.id))
if not success:
await interaction.followup.send("❌ Failed to record vote. Please try again.")
return
# Update movie information
updated_movie = await movies_index.get_object(movie["objectID"])
# Create embed for vote confirmation
embed = discord.Embed(
title=f"✅ Vote recorded for: {updated_movie['title']}",
description=f"This movie now has {updated_movie['votes']} vote(s)!",
color=0x00ff00
)
if updated_movie.get("poster"):
embed.set_thumbnail(url=updated_movie["poster"])
embed.set_footer(text=f"Voted by {interaction.user.display_name}")
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Error in vote command: {e}")
await interaction.followup.send(f"❌ An error occurred: {str(e)}")
@tree.command(name="remove", description="Remove a movie from the voting queue")
@app_commands.describe(title="Title of the movie to remove")
async def cmd_remove(interaction: discord.Interaction, title: str):
"""Remove a movie from the voting queue."""
await interaction.response.defer()
try:
# Check if user has admin privileges
if not interaction.user.guild_permissions.administrator:
await interaction.followup.send("You need administrator privileges to remove movies.")
return
# Find the movie
movie = await find_movie_by_title(title)
if not movie:
await interaction.followup.send(f"Movie '{title}' not found in the voting queue.")
return
# Remove the movie
success = await remove_movie(movie["objectID"])
if success:
await interaction.followup.send(f"Removed '{movie['title']}' from the voting queue.")
else:
await interaction.followup.send(f"Failed to remove '{movie['title']}'. Please try again.")
except Exception as e:
logger.error(f"Error in /remove command: {e}")
await interaction.followup.send("An error occurred while removing the movie. Please try again.")
@tree.command(name="top", description="Show the top voted movies")
@app_commands.describe(count="Number of top movies to show (default: 5)")
async def cmd_top(interaction: discord.Interaction, count: int = 5):
"""Show the top voted movies."""
await interaction.response.defer(thinking=True)
try:
# Limit count to reasonable values
count = max(1, min(10, count))
# Get top voted movies
top_movies = await get_top_movies(count)
if not top_movies:
await interaction.followup.send("❌ No movies have been voted for yet!")
return
# Create embed for top movies
embed = discord.Embed(
title=f"🏆 Top {len(top_movies)} Voted Movies",
description="Here are the most popular movies for our next movie night!",
color=0x00ff00
)
for i, movie in enumerate(top_movies):
# Get medal emoji for top 3
medal = "🥇" if i == 0 else "🥈" if i == 1 else "🥉" if i == 2 else f"{i+1}."
# Create field for each movie
movie_details = [
f"**Votes**: {movie['votes']}",
f"**Year**: {movie['year'] if movie.get('year') else 'N/A'}",
f"**Rating**: ⭐ {movie.get('imdbRating', 'N/A')}/10"
]
embed.add_field(
name=f"{medal} {movie['title']}",
value="\n".join(movie_details),
inline=False
)
# Add instructions on how to vote
embed.set_footer(text="Use /vote to vote for a movie!")
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Error in top command: {e}")
await interaction.followup.send(f"❌ An error occurred: {str(e)}")
@tree.command(name="random", description="Suggest a random movie from the list")
async def cmd_random(interaction: discord.Interaction):
"""Suggest a random movie from the list."""
await interaction.response.defer(thinking=True)
try:
# Get all movies
movies = await get_all_movies()
if not movies:
await interaction.followup.send("❌ No movies in the database yet! Add some with /add.")
return
# Choose a random movie
random_movie = random.choice(movies)
# Create embed for random movie
embed = discord.Embed(
title=f"🎲 Random Movie: {random_movie['title']} ({random_movie['year'] if random_movie.get('year') else 'N/A'})",
description=random_movie.get("plot", "No plot available.") if len(random_movie.get("plot", "")) < 300 else random_movie.get("plot", "")[:297] + "...",
color=0x00ff00
)
if random_movie.get("director"):
embed.add_field(name="Director", value=random_movie["director"], inline=True)
if random_movie.get("actors") and len(random_movie["actors"]) > 0:
embed.add_field(name="Starring", value=", ".join(random_movie["actors"][:3]), inline=True)
if random_movie.get("imdbRating"):
embed.add_field(name="Rating", value=f"⭐ {random_movie['imdbRating']}/10", inline=True)
embed.add_field(name="Votes", value=f"👍 {random_movie.get('votes', 0)}", inline=True)
if random_movie.get("poster"):
embed.set_thumbnail(url=random_movie["poster"])
embed.set_footer(text=f"Source: {random_movie.get('source', 'unknown').upper()} | Vote with: /vote {random_movie['title']}")
await interaction.followup.send(embed=embed)
except Exception as e:
logger.error(f"Error in random command: {e}")
await interaction.followup.send(f"❌ An error occurred: {str(e)}")
@tree.command(name="help", description="Show help for Paradiso commands")
async def cmd_help(interaction: discord.Interaction):
"""Show help for Paradiso commands."""
embed = discord.Embed(
title="Paradiso Bot Help",
description="Here are the commands you can use with the Paradiso movie voting bot:",
color=0x03a9f4
)
commands = [
{
"name": "/movies",
"description": "List all movies in the voting queue"
},
{
"name": "/add [title]",
"description": "Add a movie to the voting queue"
},
{
"name": "/vote [title]",
"description": "Vote for a movie in the queue"
},
{
"name": "/remove [title]",
"description": "Remove a movie from the voting queue (admin only)"
},
{
"name": "/top [count]",
"description": "Show the top voted movies (default: top 5)"
},
{
"name": "/random",
"description": "Suggest a random movie from the list"
}
]
for cmd in commands:
embed.add_field(name=cmd["name"], value=cmd["description"], inline=False)
embed.set_footer(text="Happy voting! 🎬")
await interaction.response.send_message(embed=embed)
if __name__ == "__main__":
client.run(DISCORD_TOKEN)
\ No newline at end of file
import React, { useState, useEffect } from 'react';
import Head from 'next/head';
import { useRouter } from 'next/router';
import algoliasearch from 'algoliasearch';
import { InstantSearch, SearchBox, Hits, Configure } from 'react-instantsearch-dom';
import Image from 'next/image';
import Layout from '@/components/layout';
import utilStyles from '@/styles/utils.module.css';
import styles from '@/styles/paradiso.module.css';
// Initialize the Algolia client
// TODO CONFIRM .env IS LOADED
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY
);
// Unique user token for this session - in a real app, this would be tied to user authentication
const getUserToken = () => {
if (typeof window === 'undefined') return null;
let userToken = localStorage.getItem('paradiso_user_token');
if (!userToken) {
// Generate a random user token
userToken = 'user_' + Math.random().toString(36).substring(2, 15);
localStorage.setItem('paradiso_user_token', userToken);
}
return userToken;
};
// Movie Hit component - Displays a single movie
const MovieHit = ({ hit, onVote }) => {
return (
<div className={styles.movieCard}>
{hit.poster ? (
<div className={styles.moviePoster}>
<Image
src={hit.poster}
alt={hit.title}
width={200}
height={300}
layout="responsive"
/>
</div>
) : (
<div className={styles.noImagePlaceholder}>No Image</div>
)}
<div className={styles.movieInfo}>
<h3 className={styles.movieTitle}>
{hit.title} {hit.year && <span className={styles.movieYear}>({hit.year})</span>}
</h3>
{hit.director && (
<p className={styles.movieDirector}>Director: {hit.director}</p>
)}
{hit.actors && hit.actors.length > 0 && (
<p className={styles.movieActors}>Starring: {hit.actors.join(', ')}</p>
)}
{hit.plot && (
<p className={styles.moviePlot}>{hit.plot}</p>
)}
<div className={styles.movieMeta}>
{hit.imdbRating && (
<span className={styles.movieRating}> {hit.imdbRating}/10</span>
)}
{hit.source && (
<span className={styles.movieSource}>Source: {hit.source.toUpperCase()}</span>
)}
</div>
<div className={styles.movieActions}>
<button
onClick={() => onVote(hit.objectID)}
className={styles.voteButton}
>
👍 Vote ({hit.votes || 0})
</button>
</div>
</div>
</div>
);
};
// Empty results component
const EmptyResults = () => (
<div className={styles.emptyResults}>
<h3>No movies found</h3>
<p>Try a different search or add a new movie below</p>
</div>
);
export default function Paradiso() {
const router = useRouter();
const [userToken, setUserToken] = useState(null);
const [isSearching, setIsSearching] = useState(false);
const [newMovieTitle, setNewMovieTitle] = useState('');
const [isAdding, setIsAdding] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);
// Set user token on client-side
useEffect(() => {
setUserToken(getUserToken());
}, []);
// Function to vote for a movie
const handleVote = async (movieId) => {
if (!userToken) return;
try {
// Use Algolia's partial update to increment the votes counter
const response = await fetch('/api/paradiso/vote', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
movieId,
userToken,
}),
});
if (!response.ok) {
throw new Error('Failed to vote for movie');
}
setSuccess('Vote recorded successfully!');
setTimeout(() => setSuccess(null), 3000);
} catch (err) {
console.error('Error voting for movie:', err);
setError('Failed to vote for movie. Please try again.');
setTimeout(() => setError(null), 5000);
}
};
// Function to add a new movie
const handleAddMovie = async (e) => {
e.preventDefault();
if (!newMovieTitle.trim() || !userToken) return;
setIsAdding(true);
setError(null);
try {
// Add the movie directly by title
const addResponse = await fetch('/api/paradiso/add-movie', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: newMovieTitle,
userToken,
}),
});
if (!addResponse.ok) {
const errorData = await addResponse.json();
throw new Error(errorData.error || 'Failed to add movie');
}
setNewMovieTitle('');
setSuccess('Movie added successfully!');
setTimeout(() => setSuccess(null), 3000);
} catch (err) {
console.error('Error adding movie:', err);
setError(err.message || 'Failed to add movie. Please try again.');
setTimeout(() => setError(null), 5000);
} finally {
setIsAdding(false);
}
};
return (
<Layout>
<Head>
<title>Paradiso - Movie Night Voting</title>
<meta name="description" content="Vote for the next movie night film" />
</Head>
<div className={styles.container}>
<h1 className={styles.title}>🎬 Paradiso</h1>
<p className={styles.subtitle}>Vote pour <b>notre</b> film et écris l'avenir du <i>Cinéma Paradiso</i>.</p>
{/* Error and success messages */}
{error && <div className={styles.errorMessage}>{error}</div>}
{success && <div className={styles.successMessage}>{success}</div>}
{/* InstantSearch component */}
{userToken && (
<InstantSearch
searchClient={searchClient}
indexName={process.env.NEXT_PUBLIC_ALGOLIA_INDEX}
>
<div className={styles.searchContainer}>
<SearchBox
className={styles.searchBox}
translations={{
placeholder: 'Search for movies...',
}}
onFocus={() => setIsSearching(true)}
onBlur={() => setTimeout(() => setIsSearching(false), 200)}
/>
<Configure
hitsPerPage={12}
distinct={true}
/>
{isSearching && (
<div className={styles.searchResults}>
<Hits
hitComponent={({ hit }) => (
<MovieHit hit={hit} onVote={handleVote} />
)}
classNames={{
list: styles.hitsList,
item: styles.hitItem,
empty: styles.noResults,
}}
emptyComponent={EmptyResults}
/>
</div>
)}
</div>
{/* Add movie form */}
<div className={styles.addMovieSection}>
<h2>Can't find the movie? Add it</h2>
<form onSubmit={handleAddMovie} className={styles.addMovieForm}>
<input
type="text"
value={newMovieTitle}
onChange={(e) => setNewMovieTitle(e.target.value)}
placeholder="Enter movie title..."
disabled={isAdding}
required
/>
<button
type="submit"
disabled={isAdding || !newMovieTitle.trim()}
>
{isAdding ? 'Adding...' : 'Add Movie'}
</button>
</form>
<p className={styles.infoText}>
Movie data is fetched from TMDB, OMDB, or Wikipedia as a fallback
</p>
</div>
{/* Top voted movies */}
<div className={styles.topMoviesSection}>
<h2>Top Voted Movies</h2>
<Configure
hitsPerPage={5}
filters="votes>0"
sortCriteria={['votes:desc', 'title:asc']}
/>
<Hits
hitComponent={({ hit }) => (
<MovieHit hit={hit} onVote={handleVote} />
)}
classNames={{
list: styles.hitsList,
item: styles.hitItem,
empty: styles.noResults,
}}
/>
</div>
</InstantSearch>
)}
</div>
<style jsx>{`
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.title {
font-size: 2.5rem;
text-align: center;
margin-bottom: 1rem;
}
.subtitle {
font-size: 1.2rem;
text-align: center;
margin-bottom: 2rem;
color: #666;
}
`}</style>
</Layout>
);
}
\ No newline at end of file
#!/usr/bin/env python
"""
Paradiso Setup Script
This script sets up Algolia indices for the Paradiso movie voting system and
generates secured API keys for the frontend and Discord bot.
Usage:
python setup.py --admin-key YOUR_ADMIN_API_KEY --app-id YOUR_APP_ID
Requirements:
- Python 3.7+
- algoliasearch package (pip install algoliasearch)
"""
import argparse
import json
import time
import hashlib
import base64
import urllib.parse
import os
from datetime import datetime, timedelta
from algoliasearch.search_client import SearchClient
def parse_args():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description='Setup Algolia for Paradiso movie voting system')
parser.add_argument('--admin-key', required=True, help='Algolia Admin API Key')
parser.add_argument('--app-id', required=True, help='Algolia Application ID')
parser.add_argument('--movies-file', default='../../data/movies.json', help='Path to movies JSON file')
parser.add_argument('--actors-file', default='../../data/actors.json', help='Path to actors JSON file')
parser.add_argument('--use-sample-data', action='store_true', help='Use sample data instead of JSON files')
return parser.parse_args()
def create_indices(client, index_prefix):
"""Create and configure indices for the movie voting system."""
# Create main movies index
movies_index_name = f"{index_prefix}_movies"
movies_index = client.init_index(movies_index_name)
# Configure movies index settings
movies_settings = {
# Searchable attributes (in order of importance)
"searchableAttributes": [
"title",
"originalTitle",
"director",
"actors",
"year",
"plot"
],
# Attributes for faceting
"attributesForFaceting": [
"searchable(genre)",
"year",
"voted"
],
# Custom ranking to prioritize more voted movies
"customRanking": [
"desc(votes)",
"desc(year)"
],
# Highlighting and snippeting configuration
"highlightPreTag": "<em>",
"highlightPostTag": "</em>",
# Pagination settings
"hitsPerPage": 20,
# Enable typo tolerance
"minWordSizefor1Typo": 3,
"minWordSizefor2Typos": 6,
# Restrict search to specific query parameters
"queryType": "prefixAll",
# Advanced settings
"removeStopWords": True,
"ignorePlurals": True,
# Disable A/B testing
"enablePersonalization": False,
# Define distinct property
"distinct": True,
"attributeForDistinct": "objectID"
}
# Apply settings to the index
movies_index.set_settings(movies_settings)
print(f"✅ Created and configured {movies_index_name} index")
# Create votes index for storing user votes
votes_index_name = f"{index_prefix}_votes"
votes_index = client.init_index(votes_index_name)
# Configure votes index settings
votes_settings = {
"searchableAttributes": [
"userToken",
"movieId"
],
"attributesForFaceting": [
"userToken",
"movieId"
],
# Simple ranking based on when the vote was cast
"customRanking": [
"desc(timestamp)"
],
"hitsPerPage": 100
}
# Apply settings to the votes index
votes_index.set_settings(votes_settings)
print(f"✅ Created and configured {votes_index_name} index")
# Create actors index
actors_index_name = f"{index_prefix}_actors"
actors_index = client.init_index(actors_index_name)
# Configure actors index settings
actors_settings = {
"searchableAttributes": [
"name",
"alternative_name"
],
"attributesForFaceting": [
"rating"
],
"customRanking": [
"desc(rating)"
],
"highlightPreTag": "<em>",
"highlightPostTag": "</em>",
"hitsPerPage": 20
}
# Apply settings to the actors index
actors_index.set_settings(actors_settings)
print(f"✅ Created and configured {actors_index_name} index")
# Return the configured index names
return {
"movies": movies_index_name,
"votes": votes_index_name,
"actors": actors_index_name
}
def generate_secured_api_key(admin_key, restrictions):
"""
Generate a secured API key with the given restrictions.
This is done manually instead of using the client to ensure compatibility.
"""
# Convert the restrictions to a string
restrictions_str = json.dumps(restrictions)
# Create the message to sign
message = admin_key.encode() + restrictions_str.encode()
# Generate the signature
hash_obj = hashlib.sha256(message).digest()
# Encode the signature in base64
signature = base64.b64encode(hash_obj).decode()
# URL encode the restrictions for the key
url_encoded_restrictions = urllib.parse.quote(restrictions_str)
# Create the secured API key
secured_key = signature + url_encoded_restrictions
return secured_key
# DOCS EXAMPLE
# Create new API Key with specific restrictions
#
# Copy
# # Create a new restricted search-only API key
# params = {
# 'description': 'Restricted search-only API key for algolia.com',
# # Allow searching only in indices with names starting with `dev_*`
# 'indexes': ['dev_*'],
# # Retrieve up to 20 results per search query
# 'maxHitsPerQuery': 20,
# # Rate-limit to 100 requests per hour per IP address
# 'maxQueriesPerIPPerHour': 100,
# # Add fixed query parameters to every search request
# 'queryParameters': 'ignorePlurals=false',
# # Only allow searches from the `algolia.com` domain
# 'referers': ['algolia.com/*'],
# # This API key expires after 300 seconds (5 minutes)
# 'validity': 300,
# }
# acl = ['search']
# res = client.add_api_key(acl, params)
# print(res["key"])
# CURRENT ERROR
# Params: {'description': 'Paradiso search-only API key', 'acl': ['search'], 'indexes': ['paradiso_movies', 'paradiso_votes', 'paradiso_actors'], 'maxQueriesPerIPPerHour': 100, 'maxHitsPerQuery': 50, 'validity': 0}
# Traceback (most recent call last):
# File "/home/pln/Work/Web/www/next/pages/paradiso/setup.py", line 496, in <module>
# main()
# File "/home/pln/Work/Web/www/next/pages/paradiso/setup.py", line 461, in main
# keys = create_api_keys(client, indices)
# File "/home/pln/Work/Web/www/next/pages/paradiso/setup.py", line 182, in create_api_keys
# search_key = client.add_api_key(search_key_params)
# File "/home/pln/.virtualenvs/paradiso/lib/python3.10/site-packages/algoliasearch/search_client.py", line 238, in add_api_key
# raw_response = self._transporter.write(
# File "/home/pln/.virtualenvs/paradiso/lib/python3.10/site-packages/algoliasearch/http/transporter.py", line 35, in write
# return self.request(verb, hosts, path, data, request_options, timeout)
# File "/home/pln/.virtualenvs/paradiso/lib/python3.10/site-packages/algoliasearch/http/transporter.py", line 72, in request
# return self.retry(hosts, request, relative_url)
# File "/home/pln/.virtualenvs/paradiso/lib/python3.10/site-packages/algoliasearch/http/transporter.py", line 91, in retry
# raise RequestException(content, response.status_code)
# algoliasearch.exceptions.RequestException: Expecting an array (near 1:10)
def create_api_keys(client, indices):
"""Create and configure API keys for the movie voting system."""
# Create a search-only API key with rate limiting
search_key_params = {
"description": "Paradiso search-only API key",
"indexes": list(indices.values()),
"maxQueriesPerIPPerHour": 100,
"maxHitsPerQuery": 50,
"validity": 0 # No expiration
}
print("Params:", search_key_params)
search_acl = ["search"]
search_key = client.add_api_key(search_acl, search_key_params)
print(f"✅ Created search-only API key: {search_key['key']}")
# Create an API key for frontend with limited permissions
frontend_key_params = {
"description": "Paradiso frontend API key",
"indexes": list(indices.values()),
"maxQueriesPerIPPerHour": 100,
"maxHitsPerQuery": 50,
"validity": 0 # No expiration
}
frontend_acl = ["search", "browse", "addObject"]
frontend_key = client.add_api_key(frontend_acl, frontend_key_params)
print(f"✅ Created frontend API key: {frontend_key['key']}")
# Create an API key for Discord bot with more permissions
bot_key_params = {
"description": "Paradiso Discord bot API key",
"indexes": list(indices.values()),
"maxQueriesPerIPPerHour": 1000,
"maxHitsPerQuery": 100,
"validity": 0 # No expiration
}
bot_acl = ["search", "browse", "addObject", "deleteObject", "settings"]
bot_key = client.add_api_key(bot_acl, bot_key_params)
print(f"✅ Created Discord bot API key: {bot_key['key']}")
# Generate secured API keys with different restrictions
# For frontend: limited to movies index with increment operation for votes
frontend_restrictions = {
"restrictIndices": [indices["movies"], indices["actors"]],
# Valid for 1 year (adjust as needed)
"validUntil": int(time.time() + 365 * 24 * 60 * 60)
}
frontend_secured_key = generate_secured_api_key(
frontend_key["key"],
frontend_restrictions
)
print(f"✅ Generated secured frontend API key")
# For Discord bot: access to both indices
bot_restrictions = {
"restrictIndices": list(indices.values()),
# Valid for 1 year (adjust as needed)
"validUntil": int(time.time() + 365 * 24 * 60 * 60)
}
bot_secured_key = generate_secured_api_key(
bot_key["key"],
bot_restrictions
)
print(f"✅ Generated secured Discord bot API key")
# Return all the keys
return {
"search_key": search_key["key"],
"frontend_key": frontend_key["key"],
"frontend_secured_key": frontend_secured_key,
"bot_key": bot_key["key"],
"bot_secured_key": bot_secured_key
}
def add_sample_data(client, indices):
"""Add some sample data to the movies index."""
movies_index = client.init_index(indices["movies"])
# Sample movies data
sample_movies = [
{
"objectID": "tt0068646",
"title": "The Godfather",
"originalTitle": "The Godfather",
"year": 1972,
"director": "Francis Ford Coppola",
"actors": ["Marlon Brando", "Al Pacino", "James Caan"],
"genre": ["Crime", "Drama"],
"plot": "The aging patriarch of an organized crime dynasty transfers control of his clandestine empire to his reluctant son.",
"poster": "https://m.media-amazon.com/images/M/MV5BM2MyNjYxNmUtYTAwNi00MTYxLWJmNWYtYzZlODY3ZTk3OTFlXkEyXkFqcGdeQXVyNzkwMjQ5NzM@._V1_SX300.jpg",
"votes": 3,
"addedDate": int(time.time()),
"addedBy": "setup_script",
"imdbRating": 9.2,
"imdbID": "tt0068646",
"tmdbID": "238"
},
{
"objectID": "tt0111161",
"title": "The Shawshank Redemption",
"originalTitle": "The Shawshank Redemption",
"year": 1994,
"director": "Frank Darabont",
"actors": ["Tim Robbins", "Morgan Freeman", "Bob Gunton"],
"genre": ["Drama"],
"plot": "Two imprisoned men bond over a number of years, finding solace and eventual redemption through acts of common decency.",
"poster": "https://m.media-amazon.com/images/M/MV5BMDFkYTc0MGEtZmNhMC00ZDIzLWFmNTEtODM1ZmRlYWMwMWFmXkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_SX300.jpg",
"votes": 5,
"addedDate": int(time.time()),
"addedBy": "setup_script",
"imdbRating": 9.3,
"imdbID": "tt0111161",
"tmdbID": "278"
},
{
"objectID": "tt0468569",
"title": "The Dark Knight",
"originalTitle": "The Dark Knight",
"year": 2008,
"director": "Christopher Nolan",
"actors": ["Christian Bale", "Heath Ledger", "Aaron Eckhart"],
"genre": ["Action", "Crime", "Drama"],
"plot": "When the menace known as the Joker wreaks havoc and chaos on the people of Gotham, Batman must accept one of the greatest psychological and physical tests of his ability to fight injustice.",
"poster": "https://m.media-amazon.com/images/M/MV5BMTMxNTMwODM0NF5BMl5BanBnXkFtZTcwODAyMTk2Mw@@._V1_SX300.jpg",
"votes": 2,
"addedDate": int(time.time()),
"addedBy": "setup_script",
"imdbRating": 9.0,
"imdbID": "tt0468569",
"tmdbID": "155"
}
]
# Add sample movies to the index
movies_index.save_objects(sample_movies)
print(f"✅ Added {len(sample_movies)} sample movies to {indices['movies']} index")
def load_from_json_files(client, indices, movies_file, actors_file):
"""Load data from JSON files into Algolia indices."""
movies_index = client.init_index(indices["movies"])
actors_index = client.init_index(indices["actors"])
# Load movies data
try:
with open(movies_file, 'r', encoding='utf-8') as f:
movies_data = json.load(f)
# Transform movies data to match our schema
formatted_movies = []
for movie in movies_data:
# Skip movies without required fields
if not movie.get('title') or not movie.get('id'):
continue
# Generate a unique objectID
object_id = movie.get('imdb_id') or f"tmdb_{movie.get('id')}"
# Format the movie data
formatted_movie = {
"objectID": object_id,
"title": movie.get('title', ''),
"originalTitle": movie.get('original_title', movie.get('title', '')),
"year": movie.get('release_date', '')[:4] if movie.get('release_date') else None,
"director": movie.get('director', 'Unknown'),
"actors": movie.get('actors', []),
"genre": [genre['name'] for genre in movie.get('genres', [])] if movie.get('genres') else [],
"plot": movie.get('overview', ''),
"poster": f"https://image.tmdb.org/t/p/w500{movie.get('poster_path')}" if movie.get('poster_path') else None,
"votes": 0,
"addedDate": int(time.time()),
"addedBy": "setup_script",
"imdbRating": movie.get('vote_average', 0),
"imdbID": movie.get('imdb_id', ''),
"tmdbID": str(movie.get('id', '')),
"source": "tmdb"
}
formatted_movies.append(formatted_movie)
# Save movies in batches to avoid hitting Algolia limits
batch_size = 1000
for i in range(0, len(formatted_movies), batch_size):
batch = formatted_movies[i:i+batch_size]
movies_index.save_objects(batch)
print(f"✅ Added batch {i//batch_size + 1}/{(len(formatted_movies) + batch_size - 1)//batch_size} of movies to {indices['movies']} index")
print(f"✅ Added {len(formatted_movies)} movies from {movies_file} to {indices['movies']} index")
except Exception as e:
print(f"❌ Error loading movies from {movies_file}: {e}")
# Load actors data
try:
with open(actors_file, 'r', encoding='utf-8') as f:
actors_data = json.load(f)
# Save actors in batches to avoid hitting Algolia limits
batch_size = 1000
for i in range(0, len(actors_data), batch_size):
batch = actors_data[i:i+batch_size]
actors_index.save_objects(batch)
print(f"✅ Added batch {i//batch_size + 1}/{(len(actors_data) + batch_size - 1)//batch_size} of actors to {indices['actors']} index")
print(f"✅ Added {len(actors_data)} actors from {actors_file} to {indices['actors']} index")
except Exception as e:
print(f"❌ Error loading actors from {actors_file}: {e}")
def save_config(app_id, indices, keys):
"""Save the configuration to a local file."""
config = {
"app_id": app_id,
"indices": indices,
"keys": keys,
"created_at": datetime.now().isoformat(),
"expires_at": (datetime.now() + timedelta(days=365)).isoformat()
}
# Save to a file
with open("paradiso_config.json", "w") as f:
json.dump(config, f, indent=2)
print(f"✅ Saved configuration to paradiso_config.json")
# Also create environment files for frontend and bot
with open(".env.frontend", "w") as f:
f.write(f"NEXT_PUBLIC_ALGOLIA_APP_ID={app_id}\n")
f.write(f"NEXT_PUBLIC_ALGOLIA_SEARCH_KEY={keys['search_key']}\n")
f.write(f"NEXT_PUBLIC_ALGOLIA_INDEX={indices['movies']}\n")
f.write(f"NEXT_PUBLIC_ALGOLIA_ACTORS_INDEX={indices['actors']}\n")
f.write(f"ALGOLIA_SECURED_KEY={keys['frontend_secured_key']}\n")
f.write(f"OMDB_API_KEY=YOUR_OMDB_API_KEY\n")
f.write(f"TMDB_API_KEY=YOUR_TMDB_API_KEY\n")
f.write(f"MOVIE_DATA_SOURCE=tmdb\n")
print(f"✅ Saved frontend environment to .env.frontend")
with open(".env.bot", "w") as f:
f.write(f"ALGOLIA_APP_ID={app_id}\n")
f.write(f"ALGOLIA_API_KEY={keys['bot_secured_key']}\n")
f.write(f"ALGOLIA_MOVIES_INDEX={indices['movies']}\n")
f.write(f"ALGOLIA_VOTES_INDEX={indices['votes']}\n")
f.write(f"ALGOLIA_ACTORS_INDEX={indices['actors']}\n")
f.write(f"DISCORD_TOKEN=YOUR_DISCORD_BOT_TOKEN\n")
f.write(f"OMDB_API_KEY=YOUR_OMDB_API_KEY\n")
f.write(f"TMDB_API_KEY=YOUR_TMDB_API_KEY\n")
f.write(f"MOVIE_DATA_SOURCE=tmdb\n")
print(f"✅ Saved Discord bot environment to .env.bot")
def get_api_key_instructions():
"""Instructions for obtaining API keys."""
print("\n== API Key Instructions ==")
print("For movie data, we're using both TMDB and OMDB APIs with fallback support.")
print("\n=== TMDB API Key ===")
print("To get a free TMDB API key:")
print("1. Visit https://www.themoviedb.org/signup")
print("2. Create an account and verify your email")
print("3. Go to https://www.themoviedb.org/settings/api")
print("4. Request an API key for a developer application")
print("5. Fill out the form and submit")
print("6. Update the .env.frontend and .env.bot files with your TMDB API key")
print("\n=== OMDB API Key ===")
print("To get a free OMDB API key:")
print("1. Visit https://www.omdbapi.com/apikey.aspx")
print("2. Sign up for a FREE API key (allows up to 1,000 daily requests)")
print("3. Check your email and activate your key")
print("4. Update the .env.frontend and .env.bot files with your OMDB API key")
print("\nWhen you get your keys, replace 'YOUR_TMDB_API_KEY' and 'YOUR_OMDB_API_KEY' in the .env files with your actual keys.")
def main():
"""Main setup function."""
args = parse_args()
print("\n== Paradiso Algolia Setup ==")
print(f"Setting up Algolia for Paradiso movie voting system...")
# Initialize the Algolia client
client = SearchClient.create(args.app_id, args.admin_key)
# Create a unique prefix for the indices
index_prefix = "paradiso"
# Create and configure indices
indices = create_indices(client, index_prefix)
# Create and configure API keys
keys = create_api_keys(client, indices)
# Add data
if args.use_sample_data:
add_sample_data(client, indices)
else:
# Resolve paths relative to the script location
script_dir = os.path.dirname(os.path.abspath(__file__))
movies_path = os.path.join(script_dir, args.movies_file)
actors_path = os.path.join(script_dir, args.actors_file)
print(f"Loading data from JSON files:")
print(f"- Movies: {movies_path}")
print(f"- Actors: {actors_path}")
load_from_json_files(client, indices, movies_path, actors_path)
# Save the configuration
save_config(args.app_id, indices, keys)
# Instructions for API keys
get_api_key_instructions()
print("\n== Setup Complete ==")
print("Your Algolia-powered Paradiso movie voting system is now set up!")
print("Keep paradiso_config.json in a secure location, as it contains your API keys.")
print("Add the environment variables from .env.frontend to your Next.js project.")
print("Add the environment variables from .env.bot to your Discord bot project.")
print("\nNotes:")
print("- The secured API keys are valid for 1 year. After that, you'll need to generate new ones.")
print("- Rate limits are set to 100 queries per hour for frontend and 1000 for the bot.")
print("- TMDB API has a limit of 1,000 requests per day with the free key.")
print("- OMDB API has a limit of 1,000 requests per day with the free key.")
if __name__ == "__main__":
main()
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment