From 29f0e73c0be9e6193bf8bba7a71607f2c28527c2 Mon Sep 17 00:00:00 2001 From: Edward Tirado Jr Date: Sun, 13 Apr 2025 01:47:22 -0500 Subject: [PATCH] added support for movie search --- docker-compose.yml | 6 +- init-scripts/01-create-db.sh | 10 +++ movie_db/__init__.py | 0 movie_db/admin.py | 3 + movie_db/apps.py | 6 ++ movie_db/db_providers/omdb.py | 39 +++++++++ movie_db/migrations/__init__.py | 0 movie_db/models.py | 3 + movie_db/movie_db.py | 8 ++ movie_db/serializers.py | 22 +++++ movie_db/tests.py | 3 + movie_db/views.py | 12 +++ .../0007_alter_movie_critic_score.py | 18 ++++ movie_manager/models.py | 5 +- movie_manager/serializers.py | 9 +- movie_manager/views.py | 83 +++++++++++++------ movienight/urls.py | 2 + requirements.txt | 1 + 18 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 init-scripts/01-create-db.sh create mode 100644 movie_db/__init__.py create mode 100644 movie_db/admin.py create mode 100644 movie_db/apps.py create mode 100644 movie_db/db_providers/omdb.py create mode 100644 movie_db/migrations/__init__.py create mode 100644 movie_db/models.py create mode 100644 movie_db/movie_db.py create mode 100644 movie_db/serializers.py create mode 100644 movie_db/tests.py create mode 100644 movie_db/views.py create mode 100644 movie_manager/migrations/0007_alter_movie_critic_score.py diff --git a/docker-compose.yml b/docker-compose.yml index 34ef508..20f5b0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,15 +5,18 @@ services: environment: POSTGRES_DB: ${DATABASE_NAME} POSTGRES_USER: ${DATABASE_USERNAME} + PGUSER: ${DATABASE_USERNAME} POSTGRES_PASSWORD: ${DATABASE_PASSWORD} env_file: - .env ports: - "5432:5432" + user: postgres volumes: - movienight_data:/var/lib/postgresql/data + - ./init-scripts:/docker-entrypoint-initdb.d healthcheck: - test: ["CMD-SHELL", "pg_isready", "-d", "$DATABASE_NAME"] + test: ["CMD-SHELL", "pg_isready -U $DATABASE_USERNAME"] interval: 30s timeout: 60s retries: 5 @@ -40,6 +43,7 @@ services: DATABASE_PASSWORD: ${DATABASE_PASSWORD} DATABASE_HOST: ${DATABASE_HOST} DATABASE_PORT: ${DATABASE_PORT} + OMDB_API_KEY: ${OMDB_API_KEY} env_file: - .env volumes: diff --git a/init-scripts/01-create-db.sh b/init-scripts/01-create-db.sh new file mode 100644 index 0000000..13d3573 --- /dev/null +++ b/init-scripts/01-create-db.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e + +# Create a postgres database for the user set in the .env file. +# Everything works fine without this, but this prevents a FATAL +# error from spamming the logs +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE DATABASE "$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_USER" TO "$POSTGRES_USER"; +EOSQL \ No newline at end of file diff --git a/movie_db/__init__.py b/movie_db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_db/admin.py b/movie_db/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/movie_db/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/movie_db/apps.py b/movie_db/apps.py new file mode 100644 index 0000000..d9aa88e --- /dev/null +++ b/movie_db/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class MoviedbConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'movie_db' diff --git a/movie_db/db_providers/omdb.py b/movie_db/db_providers/omdb.py new file mode 100644 index 0000000..08e9c4f --- /dev/null +++ b/movie_db/db_providers/omdb.py @@ -0,0 +1,39 @@ +import logging +import os + +from movie_db.movie_db import MovieDB +import requests + +from movie_db.serializers import MovieSerializer, MovieResultSerializer + + +class OMDb(MovieDB): + def __init__(self): + api_key = os.getenv("OMDB_API_KEY") + logging.log(1, f"WTF: {api_key}") + self.api_key = f"{api_key}" + self.base_url = "https://www.omdbapi.com/?apikey=" + self.api_key + super().__init__() + + def search(self, query, options=None): + if options["type"] == "imdb_id": + return self.search_by_imdb_id(query) + elif options["type"] == "title": + return self.search_by_title(query) + else: + return self.search_by_term(query) + + def search_by_title(self, title): + response = requests.get(self.base_url + "&t=" + title).json() + return MovieSerializer(response).data + + def search_by_imdb_id(self, imdb_id): + response = requests.get(self.base_url + "&i=" + imdb_id).json() + return MovieSerializer(response).data + + def search_by_term(self, term): + response = requests.get(self.base_url + "&s=" + term).json() + try: + return MovieResultSerializer(response["Search"], many=True).data + except KeyError: + return {"error": response} diff --git a/movie_db/migrations/__init__.py b/movie_db/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_db/models.py b/movie_db/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/movie_db/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/movie_db/movie_db.py b/movie_db/movie_db.py new file mode 100644 index 0000000..d65a019 --- /dev/null +++ b/movie_db/movie_db.py @@ -0,0 +1,8 @@ +from abc import ABC + +class MovieDB(ABC): + def __init__(self): + pass + + def search(self, query, options=None): + pass \ No newline at end of file diff --git a/movie_db/serializers.py b/movie_db/serializers.py new file mode 100644 index 0000000..0f5eca2 --- /dev/null +++ b/movie_db/serializers.py @@ -0,0 +1,22 @@ +from rest_framework import serializers + + +class MovieSerializer(serializers.Serializer): + director = serializers.CharField(source="Director") + genre = serializers.CharField(source="Genre") + imdb_id = serializers.CharField(source="imdbID") + imdb_rating = serializers.CharField(source="imdbRating") + media_type = serializers.CharField(source="Type") + plot = serializers.CharField(source="Plot") + poster = serializers.CharField(source="Poster") + runtime = serializers.CharField(source="Runtime") + title = serializers.CharField(source="Title") + year = serializers.CharField(source="Year") + + +class MovieResultSerializer(serializers.Serializer): + title = serializers.CharField(source="Title") + year = serializers.CharField(source="Year") + imdb_id = serializers.CharField(source="imdbID") + media_type = serializers.CharField(source="Type") + poster = serializers.CharField(source="Poster") diff --git a/movie_db/tests.py b/movie_db/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/movie_db/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/movie_db/views.py b/movie_db/views.py new file mode 100644 index 0000000..602e418 --- /dev/null +++ b/movie_db/views.py @@ -0,0 +1,12 @@ +from django.http import JsonResponse +from movie_db.db_providers.omdb import OMDb + + +def omdb_search(request): + query = request.GET.get("q") + if not query: + return JsonResponse({"Error": "Missing query"}, status=400) + + search_type = request.GET.get("type") + omdb = OMDb() + return JsonResponse(omdb.search(query, {"type": search_type}), safe=False) diff --git a/movie_manager/migrations/0007_alter_movie_critic_score.py b/movie_manager/migrations/0007_alter_movie_critic_score.py new file mode 100644 index 0000000..2d27d83 --- /dev/null +++ b/movie_manager/migrations/0007_alter_movie_critic_score.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-04-12 04:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('movie_manager', '0006_remove_showing_slug_schedule_slug'), + ] + + operations = [ + migrations.AlterField( + model_name='movie', + name='critic_score', + field=models.CharField(blank=True, max_length=500, null=True), + ), + ] diff --git a/movie_manager/models.py b/movie_manager/models.py index 171e9e2..1b0c9bf 100644 --- a/movie_manager/models.py +++ b/movie_manager/models.py @@ -1,11 +1,12 @@ from django.db import models from django.contrib.auth.models import User + class Movie(models.Model): title = models.CharField(max_length=100) imdb_id = models.CharField(max_length=100) year = models.IntegerField() - critic_score = models.CharField(max_length=500) + critic_score = models.CharField(max_length=500, null=True, blank=True) genre = models.CharField(max_length=100) director = models.CharField(max_length=500) actors = models.CharField(max_length=500) @@ -39,6 +40,7 @@ class MovieList(models.Model): def __str__(self): return self.name + class Schedule(models.Model): name = models.CharField(max_length=100) owner = models.ForeignKey(User, on_delete=models.CASCADE) @@ -49,6 +51,7 @@ class Schedule(models.Model): updated_at = models.DateTimeField(auto_now=True) deleted_at = models.DateTimeField(null=True, blank=True) + class Showing(models.Model): movie = models.ForeignKey(Movie, on_delete=models.CASCADE) owner = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/movie_manager/serializers.py b/movie_manager/serializers.py index bebebb0..2118dc1 100644 --- a/movie_manager/serializers.py +++ b/movie_manager/serializers.py @@ -6,7 +6,7 @@ from movie_manager.models import Movie, MovieList, Schedule, Showing class MovieSerializer(serializers.ModelSerializer): class Meta: model = Movie - fields = '__all__' + fields = "__all__" class MovieListSerializer(serializers.ModelSerializer): @@ -15,12 +15,12 @@ class MovieListSerializer(serializers.ModelSerializer): class Meta: model = MovieList - fields = ["id","name","owner","public", "movies", "movie_count"] - + fields = ["id", "name", "owner", "public", "movies", "movie_count"] def get_movie_count(self, obj): return len(obj.movies.all()) + class UserSerializer(serializers.Serializer): class Meta: model = User @@ -41,5 +41,4 @@ class ScheduleSerializer(serializers.ModelSerializer): class Meta: model = Schedule - fields = ["name", "owner","public","slug", "showings"] - + fields = ["name", "owner", "public", "slug", "showings"] diff --git a/movie_manager/views.py b/movie_manager/views.py index e08d86e..1b98766 100644 --- a/movie_manager/views.py +++ b/movie_manager/views.py @@ -1,14 +1,27 @@ import datetime +import json from django.http import JsonResponse from django.contrib.auth.models import User from rest_framework import permissions, viewsets from knox.auth import TokenAuthentication -from rest_framework.decorators import action +from rest_framework.decorators import action, api_view from rest_framework.exceptions import NotFound +from rest_framework.permissions import AllowAny, SAFE_METHODS +from movie_db.db_providers.omdb import OMDb from movie_manager.models import Movie, MovieList, Schedule, Showing -from movie_manager.serializers import MovieListSerializer, MovieSerializer, ScheduleSerializer, ShowingSerializer +from movie_manager.serializers import ( + MovieListSerializer, + MovieSerializer, + ScheduleSerializer, + ShowingSerializer, +) + + +class ReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + return request.method in SAFE_METHODS # Create your views here. @@ -19,26 +32,25 @@ class MovieViewset(viewsets.ModelViewSet): serializer_class = MovieSerializer + class MovieListViewset(viewsets.ModelViewSet): queryset = MovieList.objects.all().order_by("name") authentication_classes = [TokenAuthentication] - permission_classes = [permissions.IsAuthenticated] + permission_classes = [permissions.IsAuthenticated | ReadOnly] serializer_class = MovieListSerializer - def retrieve(self, request, pk=None, *args, **kwargs): movie_list = MovieList.objects.get(pk=pk) return JsonResponse(MovieListSerializer(movie_list).data) - def update(self, request, pk=None, *args, **kwargs): movie_list = MovieList.objects.get(pk=pk) - movie_list.name = request.data.get('name') + movie_list.name = request.data.get("name") movie_list.owner = User.objects.get(pk=request.data.get("owner")) - if request.data.get('movies'): - movie_ids = request.data.get('movies') + if request.data.get("movies"): + movie_ids = request.data.get("movies") for movie_id in movie_ids: try: movie = Movie.objects.get(pk=movie_id) @@ -54,14 +66,33 @@ class MovieListViewset(viewsets.ModelViewSet): return JsonResponse(MovieListSerializer(movie_list).data) - @action(detail=True, methods=['put', 'delete'], url_path='movie/(?P[0-9]+)') - def add_movie(self, request, pk=None, movie_id=None, *args, **kwargs): - if request.method == 'DELETE': - return self.remove_movie(request, pk, movie_id) + @action( + detail=True, methods=["put", "delete"], url_path="movie/(?Ptt[0-9]+)" + ) + def add_movie(self, request, pk=None, imdb_id=None, *args, **kwargs): + if request.method == "DELETE": + return self.remove_movie(request, pk, imdb_id) movie_list = MovieList.objects.get(pk=pk) - movie = Movie.objects.get(pk=movie_id) - movie_list.movies.add(movie) + try: + new_movie = Movie.objects.get(imdb_id=imdb_id) + except Movie.DoesNotExist: + omdb = OMDb() + movie = omdb.search(imdb_id, {"type": "imdb_id"}) + + new_movie = Movie.objects.create( + title=movie["title"], + year=movie["year"], + imdb_id=movie["imdb_id"], + poster=movie["poster"], + plot=movie["plot"], + genre=movie["genre"], + critic_score=movie["imdb_rating"], + director=movie["director"], + added_by_id=request.user.id, + ) + + movie_list.movies.add(new_movie) return JsonResponse(MovieListSerializer(movie_list).data) @@ -73,6 +104,7 @@ class MovieListViewset(viewsets.ModelViewSet): return JsonResponse(MovieListSerializer(movie_list).data) + class ScheduleViewset(viewsets.ModelViewSet): queryset = Schedule.objects.all().order_by("name") authentication_classes = [TokenAuthentication] @@ -92,19 +124,22 @@ class ScheduleViewset(viewsets.ModelViewSet): data = serializer.data # Replace all showings with only future showings - data['showings'] = ShowingSerializer(upcoming_showings, many=True).data + data["showings"] = ShowingSerializer(upcoming_showings, many=True).data - - if request.GET.get('past_showings') == 'true': + if request.GET.get("past_showings") == "true": past_showings = instance.showings.filter(showtime__lt=today) # Add both to the response - data['past_showings'] = [ - {'id': showing.id, 'showtime': showing.showtime.isoformat(), "movie": MovieSerializer(showing.movie).data} + data["past_showings"] = [ + { + "id": showing.id, + "showtime": showing.showtime.isoformat(), + "movie": MovieSerializer(showing.movie).data, + } for showing in past_showings ] else: - data['past_showings'] = [] + data["past_showings"] = [] return JsonResponse(data) @@ -117,10 +152,10 @@ class ShowingViewset(viewsets.ModelViewSet): serializer_class = ShowingSerializer def create(self, request, *args, **kwargs): - movie_id = request.data.get('movie') + movie_id = request.data.get("movie") movie = Movie.objects.get(pk=movie_id) - schedule_id = request.data.get('schedule') + schedule_id = request.data.get("schedule") schedule = Schedule.objects.get(pk=schedule_id) showing = Showing.objects.create( @@ -128,11 +163,9 @@ class ShowingViewset(viewsets.ModelViewSet): schedule=schedule, showtime=request.data.get("showtime"), public=request.data.get("public"), - owner=User.objects.get(pk=request.data.get("owner")) + owner=User.objects.get(pk=request.data.get("owner")), ) schedule.showings.add(showing) return JsonResponse(ShowingSerializer(showing).data) - - diff --git a/movienight/urls.py b/movienight/urls.py index 6889ee7..bf84fd0 100644 --- a/movienight/urls.py +++ b/movienight/urls.py @@ -12,6 +12,7 @@ from rest_framework.routers import DefaultRouter from users import views as user_views from movie_manager import views as movie_views +from movie_db import views as movie_db_views from rest_framework.authtoken.views import obtain_auth_token router = DefaultRouter() @@ -28,5 +29,6 @@ urlpatterns = [ path(r"api/auth/token/", obtain_auth_token), path(r"api/auth/login/", user_views.LoginView.as_view(), name="knox_login"), path(r"api/auth/register/", user_views.register, name="register"), + path(r"api/movies/search", movie_db_views.omdb_search, name="omdb_search"), path(r"api/auth/", include("knox.urls")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/requirements.txt b/requirements.txt index 7c9b3b9..456b8f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ django-filter gunicorn==23.0.0 psycopg2-binary==2.9.10 django-cors-headers==4.7.0 +requests==2.32.3 \ No newline at end of file