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
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