Commit b19ac0f0 by PLN (Algolia)

remove paradiso

parent d2693181
ALGOLIA_ADMIN_KEY=""
ALGOLIA_APP_ID=""
TMDB_API_READ_TOKEN=""
TMDB_API_KEY=""
\ No newline at end of file
import { createDefaultMovieService } from './lib/movie-service';
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { title, userToken } = req.body;
// Validate required parameters
if (!title || !userToken) {
return res.status(400).json({ error: 'Missing required parameters' });
}
// Create movie service instance
const movieService = createDefaultMovieService();
// Add the movie
const movieData = await movieService.addMovie(title, userToken);
// Return success response
return res.status(200).json({ success: true, movie: movieData });
} catch (error) {
console.error('Error adding movie:', error);
// Return appropriate error messages
if (error.message === 'Movie not found') {
return res.status(404).json({ error: 'Movie not found' });
} else if (error.message === 'Movie already exists') {
return res.status(400).json({ error: 'Movie already exists' });
}
return res.status(500).json({ error: 'Internal server error', message: error.message });
}
}
\ No newline at end of file
/**
* Movie Repository Interface Abstraction
*
* This file defines repository interfaces for storing movie data and votes
* Currently implements Algolia, but could be extended to other storage mechanisms
*/
import { algoliasearch } from 'algoliasearch';
/**
* Base MovieRepository interface
* Any movie repository should implement this interface
*/
class MovieRepository {
/**
* Add a movie to the repository
* @param {object} movieData - Normalized movie data
* @param {string} userId - ID of the user adding the movie
* @returns {Promise<object>} - The stored movie
*/
async addMovie(movieData, userId) {
throw new Error('Method not implemented');
}
/**
* Check if a movie exists in the repository
* @param {string} movieId - The movie ID
* @returns {Promise<boolean>} - True if exists, false otherwise
*/
async movieExists(movieId) {
throw new Error('Method not implemented');
}
/**
* Vote for a movie
* @param {string} movieId - The movie ID
* @param {string} userId - ID of the user voting
* @returns {Promise<boolean>} - True if vote was successful, false otherwise
*/
async voteForMovie(movieId, userId) {
throw new Error('Method not implemented');
}
/**
* Get a movie by ID
* @param {string} movieId - The movie ID
* @returns {Promise<object|null>} - The movie data or null if not found
*/
async getMovie(movieId) {
throw new Error('Method not implemented');
}
/**
* Get top voted movies
* @param {number} count - Number of movies to return
* @returns {Promise<Array<object>>} - Array of movie data
*/
async getTopMovies(count = 5) {
throw new Error('Method not implemented');
}
/**
* Search for movies
* @param {string} query - Search query
* @param {number} limit - Maximum number of results to return
* @returns {Promise<Array<object>>} - Array of movie data
*/
async searchMovies(query, limit = 10) {
throw new Error('Method not implemented');
}
/**
* Get all movies
* @param {number} limit - Maximum number of results to return
* @returns {Promise<Array<object>>} - Array of movie data
*/
async getAllMovies(limit = 100) {
throw new Error('Method not implemented');
}
/**
* Remove a movie
* @param {string} movieId - The movie ID
* @returns {Promise<boolean>} - True if removal was successful
*/
async removeMovie(movieId) {
throw new Error('Method not implemented');
}
}
/**
* Algolia implementation of MovieRepository
*/
class AlgoliaMovieRepository extends MovieRepository {
constructor(appId, apiKey, moviesIndex, votesIndex) {
super();
this.client = algoliasearch(appId, apiKey);
this.moviesIndex = this.client.initIndex(moviesIndex);
this.votesIndex = this.client.initIndex(votesIndex);
}
/**
* Generate a user token for Algolia
* @param {string} userId - User ID
* @returns {string} - User token
*/
generateUserToken(userId) {
return userId.startsWith('discord_') ? userId : `user_${userId}`;
}
async addMovie(movieData, userId) {
// Check if movie already exists
if (await this.movieExists(movieData.id)) {
throw new Error('Movie already exists');
}
// Format the movie data for Algolia
const algoliaMovie = {
objectID: movieData.id,
title: movieData.title,
originalTitle: movieData.originalTitle || movieData.title,
year: movieData.year,
director: movieData.director || 'Unknown',
actors: movieData.actors || [],
genre: movieData.genre || [],
plot: movieData.plot || '',
poster: movieData.poster,
imdbRating: movieData.imdbRating,
imdbID: movieData.imdbID,
tmdbID: movieData.tmdbID,
votes: 0,
addedDate: Date.now(),
addedBy: this.generateUserToken(userId),
source: movieData.source || 'unknown'
};
// Save to Algolia
await this.moviesIndex.saveObject(algoliaMovie);
return algoliaMovie;
}
async movieExists(movieId) {
const searchResponse = await this.moviesIndex.search('', {
filters: `objectID:${movieId}`,
});
return searchResponse.hits.length > 0;
}
async voteForMovie(movieId, userId) {
const userToken = this.generateUserToken(userId);
// Check if user already voted for this movie
const searchResult = await this.votesIndex.search('', {
filters: `userToken:${userToken} AND movieId:${movieId}`
});
if (searchResult.nbHits > 0) {
return false; // User already voted
}
// Record the vote
await this.votesIndex.saveObject({
objectID: `${userToken}_${movieId}`,
userToken: userToken,
movieId: movieId,
timestamp: Date.now()
});
// Increment the movie's vote count
await this.moviesIndex.partialUpdateObject({
objectID: movieId,
votes: {
_operation: 'Increment',
value: 1
}
});
return true;
}
async getMovie(movieId) {
try {
const movie = await this.moviesIndex.getObject(movieId);
return movie;
} catch (error) {
return null;
}
}
async getTopMovies(count = 5) {
const searchResult = await this.moviesIndex.search('', {
filters: 'votes > 0',
hitsPerPage: count,
sortCriteria: ['votes:desc']
});
return searchResult.hits;
}
async searchMovies(query, limit = 10) {
const searchResult = await this.moviesIndex.search(query, {
hitsPerPage: limit
});
return searchResult.hits;
}
async getAllMovies(limit = 100) {
const searchResult = await this.moviesIndex.search('', {
hitsPerPage: limit
});
return searchResult.hits;
}
async removeMovie(movieId) {
try {
await this.moviesIndex.deleteObject(movieId);
// Delete all votes for this movie
const votesResponse = await this.votesIndex.search('', {
filters: `movieId:${movieId}`,
hitsPerPage: 100
});
const voteIds = votesResponse.hits.map(hit => hit.objectID);
if (voteIds.length > 0) {
await this.votesIndex.deleteObjects(voteIds);
}
return true;
} catch (error) {
console.error('Error removing movie:', error);
return false;
}
}
}
/**
* Repository Factory to create the appropriate repository
*/
class MovieRepositoryFactory {
static createRepository(type, config) {
switch (type.toLowerCase()) {
case 'algolia':
return new AlgoliaMovieRepository(
config.appId,
config.apiKey,
config.moviesIndex,
config.votesIndex
);
default:
throw new Error(`Unsupported repository type: ${type}`);
}
}
}
export { MovieRepository, AlgoliaMovieRepository, MovieRepositoryFactory };
\ No newline at end of file
/**
* Movie Service Layer
*
* This service provides a unified interface for interacting with movie data sources
* and repositories, abstracting the underlying implementation details.
*/
import { MovieSourceFactory } from './movie-sources';
import { MovieRepositoryFactory } from './movie-repository';
class MovieService {
/**
* Create a new MovieService instance
* @param {Object} config - Service configuration
* @param {Object} config.dataSource - Data source configuration
* @param {string} config.dataSource.type - Data source type ('omdb', 'tmdb')
* @param {string} config.dataSource.apiKey - API key for the data source
* @param {Object} config.repository - Repository configuration
* @param {string} config.repository.type - Repository type ('algolia')
* @param {Object} config.repository.config - Repository-specific configuration
*/
constructor(config) {
// Set up data sources with fallback cascade
const dataSources = MovieSourceFactory.createDataSourceCascade(config);
this.dataSource = dataSources.primary;
this.fallbackDataSources = dataSources.fallbacks;
// Set up repository
this.repository = MovieRepositoryFactory.createRepository(
config.repository.type,
config.repository.config
);
}
/**
* Search for a movie by title
* @param {string} title - Movie title to search for
* @returns {Promise<Object|null>} - Movie data or null if not found
*/
async searchMovie(title) {
// Try primary data source first
let movieData = await this.dataSource.searchByTitle(title);
// Try each fallback source in order until we find a result
if (!movieData) {
for (const fallbackSource of this.fallbackDataSources) {
movieData = await fallbackSource.searchByTitle(title);
if (movieData) {
console.info(`Found movie using fallback source: ${fallbackSource.constructor.name}`);
break;
}
}
}
return movieData;
}
/**
* Add a movie to the repository
* @param {string} title - Movie title to search and add
* @param {string} userId - ID of the user adding the movie
* @returns {Promise<Object>} - The added movie data
*/
async addMovie(title, userId) {
// First, search for the movie in the data source
const movieData = await this.searchMovie(title);
if (!movieData) {
throw new Error('Movie not found');
}
// Check if the movie already exists in the repository
const movieExists = await this.repository.movieExists(movieData.id);
if (movieExists) {
throw new Error('Movie already exists');
}
// Add the movie to the repository
return await this.repository.addMovie(movieData, userId);
}
/**
* Vote for a movie
* @param {string} movieId - ID of the movie to vote for
* @param {string} userId - ID of the user voting
* @returns {Promise<boolean>} - True if vote was successful, false otherwise
*/
async voteForMovie(movieId, userId) {
return await this.repository.voteForMovie(movieId, userId);
}
/**
* Get top voted movies
* @param {number} count - Number of movies to return
* @returns {Promise<Array<Object>>} - Array of movie data
*/
async getTopMovies(count = 5) {
return await this.repository.getTopMovies(count);
}
/**
* Get all movies
* @param {number} limit - Maximum number of movies to return
* @returns {Promise<Array<Object>>} - Array of movie data
*/
async getAllMovies(limit = 100) {
return await this.repository.getAllMovies(limit);
}
/**
* Search for movies in the repository
* @param {string} query - Search query
* @param {number} limit - Maximum number of results
* @returns {Promise<Array<Object>>} - Array of movie data
*/
async searchMoviesInRepository(query, limit = 10) {
return await this.repository.searchMovies(query, limit);
}
/**
* Remove a movie from the repository
* @param {string} movieId - ID of the movie to remove
* @returns {Promise<boolean>} - True if removal was successful
*/
async removeMovie(movieId) {
return await this.repository.removeMovie(movieId);
}
}
/**
* Create a movie service with the default configuration from environment variables
* @returns {MovieService} - Configured movie service
*/
function createDefaultMovieService() {
// Determine preferred data source
const movieDataSource = process.env.MOVIE_DATA_SOURCE || 'tmdb';
return new MovieService({
dataSource: {
type: movieDataSource,
apiKey: movieDataSource === 'tmdb' ? process.env.TMDB_API_KEY : process.env.OMDB_API_KEY,
fallback: {
type: movieDataSource === 'tmdb' ? 'omdb' : 'tmdb',
apiKey: movieDataSource === 'tmdb' ? process.env.OMDB_API_KEY : process.env.TMDB_API_KEY
}
},
repository: {
type: 'algolia',
config: {
appId: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
apiKey: process.env.ALGOLIA_ADMIN_API_KEY,
moviesIndex: process.env.NEXT_PUBLIC_ALGOLIA_INDEX,
votesIndex: process.env.ALGOLIA_VOTES_INDEX || 'paradiso_votes'
}
}
});
}
export { MovieService, createDefaultMovieService };
\ No newline at end of file
/**
* Movie Data Source Interface Abstraction
*
* This file defines interfaces for different movie data sources (OMDB, TMDB)
* and provides concrete implementations.
*/
/**
* Base MovieDataSource interface
* Any movie data provider should implement this interface
*/
class MovieDataSource {
/**
* Search for a movie by title
* @param {string} title - The movie title to search for
* @returns {Promise<object|null>} - Movie data or null if not found
*/
async searchByTitle(title) {
throw new Error('Method not implemented');
}
/**
* Search for a movie by ID
* @param {string} id - The movie ID (could be IMDB ID, TMDB ID, etc.)
* @param {string} idType - The type of ID (imdb, tmdb, etc.)
* @returns {Promise<object|null>} - Movie data or null if not found
*/
async searchById(id, idType) {
throw new Error('Method not implemented');
}
/**
* Normalize the movie data to a consistent format
* @param {object} rawData - Raw data from the API
* @returns {object} - Normalized movie data
*/
normalizeMovie(rawData) {
throw new Error('Method not implemented');
}
}
/**
* OMDB API Implementation
*/
class OMDBDataSource extends MovieDataSource {
constructor(apiKey) {
super();
this.apiKey = apiKey;
this.baseUrl = 'http://www.omdbapi.com/';
}
async searchByTitle(title) {
try {
const response = await fetch(`${this.baseUrl}?apikey=${this.apiKey}&t=${encodeURIComponent(title)}&plot=full`);
if (!response.ok) {
throw new Error(`OMDB API returned ${response.status}`);
}
const data = await response.json();
return data.Response === 'True' ? this.normalizeMovie(data) : null;
} catch (error) {
console.error('Error searching OMDB by title:', error);
return null;
}
}
async searchById(id, idType = 'imdb') {
if (idType !== 'imdb') {
throw new Error('OMDB only supports IMDB IDs');
}
try {
const response = await fetch(`${this.baseUrl}?apikey=${this.apiKey}&i=${id}&plot=full`);
if (!response.ok) {
throw new Error(`OMDB API returned ${response.status}`);
}
const data = await response.json();
return data.Response === 'True' ? this.normalizeMovie(data) : null;
} catch (error) {
console.error('Error searching OMDB by ID:', error);
return null;
}
}
normalizeMovie(rawData) {
return {
id: rawData.imdbID,
title: rawData.Title,
originalTitle: rawData.Title,
year: parseInt(rawData.Year) || null,
director: rawData.Director,
actors: rawData.Actors ? rawData.Actors.split(', ') : [],
genre: rawData.Genre ? rawData.Genre.split(', ') : [],
plot: rawData.Plot,
poster: rawData.Poster !== 'N/A' ? rawData.Poster : null,
imdbRating: rawData.imdbRating !== 'N/A' ? parseFloat(rawData.imdbRating) : null,
imdbID: rawData.imdbID,
source: 'omdb',
rawData: rawData
};
}
}
/**
* TMDB API Implementation
*/
class TMDBDataSource extends MovieDataSource {
constructor(apiKey) {
super();
this.apiKey = apiKey;
this.baseUrl = 'https://api.themoviedb.org/3';
this.imageBaseUrl = 'https://image.tmdb.org/t/p/w500';
}
async searchByTitle(title) {
try {
// First search for the movie
const searchResponse = await fetch(
`${this.baseUrl}/search/movie?api_key=${this.apiKey}&query=${encodeURIComponent(title)}&include_adult=false`
);
if (!searchResponse.ok) {
throw new Error(`TMDB API returned ${searchResponse.status}`);
}
const searchData = await searchResponse.json();
if (!searchData.results || searchData.results.length === 0) {
return null;
}
// Get the most relevant result
const movieId = searchData.results[0].id;
// Get detailed info about the movie
return await this.searchById(movieId, 'tmdb');
} catch (error) {
console.error('Error searching TMDB by title:', error);
return null;
}
}
async searchById(id, idType = 'tmdb') {
try {
let movieId = id;
// If ID is IMDb ID, first search for TMDB ID
if (idType === 'imdb') {
const findResponse = await fetch(
`${this.baseUrl}/find/${id}?api_key=${this.apiKey}&external_source=imdb_id`
);
if (!findResponse.ok) {
throw new Error(`TMDB API returned ${findResponse.status}`);
}
const findData = await findResponse.json();
if (!findData.movie_results || findData.movie_results.length === 0) {
return null;
}
movieId = findData.movie_results[0].id;
}
// Get detailed movie info
const detailsResponse = await fetch(
`${this.baseUrl}/movie/${movieId}?api_key=${this.apiKey}&append_to_response=credits`
);
if (!detailsResponse.ok) {
throw new Error(`TMDB API returned ${detailsResponse.status}`);
}
const movieData = await detailsResponse.json();
return this.normalizeMovie(movieData);
} catch (error) {
console.error('Error searching TMDB by ID:', error);
return null;
}
}
normalizeMovie(rawData) {
// Extract director from credits
let director = 'Unknown';
if (rawData.credits && rawData.credits.crew) {
const directors = rawData.credits.crew
.filter(member => member.job === 'Director')
.map(director => director.name);
director = directors.length > 0 ? directors.join(', ') : 'Unknown';
}
// Extract actors from credits
let actors = [];
if (rawData.credits && rawData.credits.cast) {
actors = rawData.credits.cast
.slice(0, 5) // Get top 5 actors
.map(actor => actor.name);
}
return {
id: rawData.id.toString(),
title: rawData.title,
originalTitle: rawData.original_title,
year: rawData.release_date ? parseInt(rawData.release_date.substring(0, 4)) : null,
director,
actors,
genre: rawData.genres ? rawData.genres.map(genre => genre.name) : [],
plot: rawData.overview,
poster: rawData.poster_path ? `${this.imageBaseUrl}${rawData.poster_path}` : null,
imdbRating: rawData.vote_average ? parseFloat(rawData.vote_average) : null,
imdbID: rawData.imdb_id || null,
tmdbID: rawData.id.toString(),
source: 'tmdb',
rawData: rawData
};
}
}
/**
* Wikipedia Fallback Implementation
* Uses public web APIs to get basic movie information when other sources fail
*/
class WikipediaDataSource extends MovieDataSource {
constructor() {
super();
this.baseUrl = 'https://en.wikipedia.org/api/rest_v1';
}
async searchByTitle(title) {
try {
// Search Wikipedia for the movie
const searchUrl = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(title)}%20film&format=json&origin=*`;
const searchResponse = await fetch(searchUrl);
if (!searchResponse.ok) {
throw new Error(`Wikipedia API search returned ${searchResponse.status}`);
}
const searchData = await searchResponse.json();
if (!searchData.query || !searchData.query.search || searchData.query.search.length === 0) {
return null;
}
// Get the first result that has "film" in the title or snippet
let pageTitle = null;
for (const result of searchData.query.search) {
if (result.title.toLowerCase().includes('film') ||
result.snippet.toLowerCase().includes('film')) {
pageTitle = result.title;
break;
}
}
// If no film-specific result was found, use the first result
if (!pageTitle && searchData.query.search.length > 0) {
pageTitle = searchData.query.search[0].title;
}
if (!pageTitle) {
return null;
}
// Get the page content
const contentUrl = `${this.baseUrl}/page/summary/${encodeURIComponent(pageTitle)}`;
const contentResponse = await fetch(contentUrl);
if (!contentResponse.ok) {
throw new Error(`Wikipedia API content returned ${contentResponse.status}`);
}
const pageData = await contentResponse.json();
return this.normalizeMovie(pageData);
} catch (error) {
console.error('Error searching Wikipedia by title:', error);
return null;
}
}
async searchById(id, idType = 'wiki') {
// Not directly supported for Wikipedia
return null;
}
normalizeMovie(rawData) {
// Generate an ID from the title
const titleSlug = rawData.title.toLowerCase().replace(/[^a-z0-9]+/g, '_');
const id = `wiki_${titleSlug}`;
// Try to extract the year from the title (often in parentheses)
const yearMatch = rawData.title.match(/\((\d{4})(?: film)?\)/);
const year = yearMatch ? parseInt(yearMatch[1]) : null;
// Clean up the title by removing year and "film" markers
const title = rawData.title.replace(/\s*\(\d{4}(?: film)?\)/, '');
// Extract director using a simple regex if possible
let director = 'Unknown';
if (rawData.extract) {
const directorMatch = rawData.extract.match(/(?:directed|director)[^\n.]*?by\s+([^.,\n]+)/i);
if (directorMatch) {
director = directorMatch[1].trim();
}
}
return {
id,
title,
originalTitle: title,
year,
director,
actors: [], // Would need more complex parsing
genre: [], // Would need more complex parsing
plot: rawData.extract ? rawData.extract.substring(0, 500) : '',
poster: rawData.thumbnail ? rawData.thumbnail.source : null,
imdbRating: null,
imdbID: null,
tmdbID: null,
source: 'wikipedia',
rawData: {
title: rawData.title,
url: rawData.content_urls ? rawData.content_urls.desktop.page : null
}
};
}
}
/**
* Movie Source Factory to create the appropriate data source
*/
class MovieSourceFactory {
static createDataSource(source, apiKey) {
switch (source.toLowerCase()) {
case 'omdb':
return apiKey ? new OMDBDataSource(apiKey) : new WikipediaDataSource();
case 'tmdb':
return apiKey ? new TMDBDataSource(apiKey) : new WikipediaDataSource();
case 'wikipedia':
return new WikipediaDataSource();
default:
throw new Error(`Unsupported movie data source: ${source}`);
}
}
/**
* Creates a cascade of data sources with fallback options
* @param {Object} config - Configuration for data sources
* @returns {Object} An object with primary and fallback data sources
*/
static createDataSourceCascade(config) {
const sources = {
primary: null,
fallbacks: []
};
// Set up primary data source
if (config.dataSource.type === 'tmdb' && config.dataSource.apiKey) {
sources.primary = this.createDataSource('tmdb', config.dataSource.apiKey);
} else if (config.dataSource.type === 'omdb' && config.dataSource.apiKey) {
sources.primary = this.createDataSource('omdb', config.dataSource.apiKey);
} else if (config.dataSource.type === 'fallback') {
// In fallback mode, try to use sources in priority order
if (config.dataSource.apiKey) {
sources.primary = this.createDataSource(config.dataSource.type, config.dataSource.apiKey);
} else if (config.dataSource.fallback && config.dataSource.fallback.apiKey) {
sources.primary = this.createDataSource(
config.dataSource.fallback.type,
config.dataSource.fallback.apiKey
);
}
}
// Set up fallback sources
if (config.dataSource.fallback) {
if (config.dataSource.type !== config.dataSource.fallback.type) {
sources.fallbacks.push(
this.createDataSource(config.dataSource.fallback.type, config.dataSource.fallback.apiKey)
);
}
}
// Add Wikipedia as last resort fallback
sources.fallbacks.push(this.createDataSource('wikipedia'));
// If no primary source, use Wikipedia
if (!sources.primary) {
sources.primary = this.createDataSource('wikipedia');
console.warn('No movie API keys available. Using Wikipedia as primary source.');
}
return sources;
}
}
export { MovieDataSource, OMDBDataSource, TMDBDataSource, WikipediaDataSource, MovieSourceFactory };
\ No newline at end of file
import { createDefaultMovieService } from './lib/movie-service';
export default async function handler(req, res) {
// Only allow GET requests
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { title } = req.query;
// Validate required parameters
if (!title) {
return res.status(400).json({ error: 'Missing title parameter' });
}
// Create movie service instance
const movieService = createDefaultMovieService();
// Search for the movie
const movieData = await movieService.searchMovie(title);
if (!movieData) {
return res.status(404).json({ error: 'Movie not found' });
}
// Return the movie data
return res.status(200).json(movieData);
} catch (error) {
console.error('Error searching for movie:', error);
return res.status(500).json({ error: 'Internal server error', message: error.message });
}
}
\ No newline at end of file
import { createDefaultMovieService } from './lib/movie-service';
export default async function handler(req, res) {
// Only allow POST requests
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const { movieId, userToken } = req.body;
// Validate required parameters
if (!movieId || !userToken) {
return res.status(400).json({ error: 'Missing required parameters' });
}
// Create movie service instance
const movieService = createDefaultMovieService();
// Vote for the movie
const success = await movieService.voteForMovie(movieId, userToken);
if (!success) {
return res.status(400).json({ error: 'User already voted for this movie' });
}
// Return success response
return res.status(200).json({ success: true });
} catch (error) {
console.error('Error voting for movie:', error);
return res.status(500).json({ error: 'Internal server error', message: error.message });
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
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