import requests import shutil import random import time import wikipediaapi from bs4 import BeautifulSoup from django.conf import settings from django.db import models from pathlib import Path class Gender(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=250) class Meta: unique_together = ["name"] ordering = ["name"] verbose_name = "Género" verbose_name_plural = "Generos" def __str__(self): return self.name class Country(models.Model): id = models.AutoField(primary_key=True) name = models.CharField(max_length=250, verbose_name="País") class Meta: unique_together = ["name"] ordering = ["name"] verbose_name = "País" verbose_name_plural = "Paises" def __str__(self): return self.name class PersonQuerySet(models.QuerySet): def directors(self): rows = self.filter(is_director=True).values_list("name", flat=True) return rows def actors(self): rows = self.filter(is_actor=True).values_list("name", flat=True) return rows class Person(models.Model): id = models.AutoField(primary_key=True) name = models.CharField("Nombre", max_length=500) country = models.ForeignKey( Country, related_name="country", on_delete=models.PROTECT ) date_born = models.DateField("Fecha de nacimiento", null=True, blank=True) is_actor = models.BooleanField("Es Actor", default=False) is_director = models.BooleanField("Es Director", default=False) is_woman = models.BooleanField("Es mujer", default=False) photo = models.ImageField( "Fotografía", upload_to="%Y/%m/%d/", null=True, blank=True ) objects = PersonQuerySet.as_manager() class Meta: unique_together = ["name"] ordering = ["name"] verbose_name = "Persona" verbose_name_plural = "Personas" def __str__(self): return self.name class MovieQuerySet(models.QuerySet): def random_pick(self, random_max=6, min_items=20, **kwargs): """ Regresa películas de manera aleatoria. Por defecto tiene que haber al menos 20 películas en la consulta. Por defecto regresa un máximo de 6 películas. """ all = list(Movie.objects.filter(**kwargs).values()) if len(all) < min_items: return None elif len(all) < random_max: return all else: return random.sample(all, random_max) def top_pick(self, key, top_max=6): """ Regresa el top de películas. El top corresponde al criterio de búsqueda en 'key'. Por defecto regresa las primeras 6 películas como máximo. """ all = list(Movie.objects.order_by(f"-{key}").values()) return all[:top_max] def top_random_pick(self, key, top_max=6): """ Regresa el top de películas de manera aleatorias. El top corresponde al criterio de búsqueda en 'key'. Por defecto regresa un máximo de 6 películas. A diferencia de 'random_pick', de todo el top selecciona de manera aleatoria según el 'top_max'. """ all = list(Movie.objects.order_by(f"-{key}").values()) top = [] for movie in all: if movie[key] == all[0][key]: top.append(movie) else: break if len(top) > top_max: return random.sample(top, top_max) else: return top def home_pick(self): """ Regresa la selección de películas para el 'home'. El 'home' es la página principal. """ genders = list(Gender.objects.values_list("id", flat=True)) sections = self.home_sections() for gender in genders: key = Gender.objects.get(pk=gender).name picked = self.random_pick(genders=gender) if picked: sections["genders"][key] = self.fix_all_data(picked) return sections def home_sections(self): """ Regresa la selección de películas en secciones para el 'home'. Las secciones son novedades, mejor valorados y más descargados. """ sections = { "Novedades": self.fix_all_data(self.top_pick("id")), "Mejor valorados": self.fix_all_data(self.top_random_pick("stars")), "Más descargados": self.fix_all_data(self.top_pick("count")), } sections["genders"] = {} return sections def fix_all_data(self, movies): """ Enmienda los datos de las películas. """ for movie in movies: self.fix_data(movie, wikipedia=False) return movies def fix_data(self, movie, wikipedia=True): """ Enmienda los datos de una película. Los enmiendos son formateos de unos valores (que se guardan en nuevas llaves) y de rutas a medios. """ movie["duration_formatted"] = self.format_duration(movie["duration"]) movie["count_formatted"] = self.format_count(movie["count"]) movie["stars_icons"] = self.format_stars(movie["stars"]) movie["file_name"] = self.fix_path(movie["file_name"]) movie["cartel"] = self.fix_path(movie["cartel"]) if wikipedia: movie["wiki"] = self.get_wiki(movie) def fix_path(self, el): """ Enmienda ruta a medio. La ruta es distinta según se esté en producción o en desarrollo ('DEBUG'). """ if settings.DEBUG: return settings.URL_DEBUG.format(el) else: return settings.MEDIA_ROOT / el def fix_summ(self, raw): """ Enmienda sinopsis de Wikipedia. Los sumarios de artículos a la Wikipedia vienen con notas al pie que no son necesarias. Para la sinopsis estas notas son eliminadas y se regresa el código HTML como una cadena de caracteres. """ html = BeautifulSoup(raw, "lxml") for ref in html.find_all("sup", "reference"): ref.decompose() clean = list(map(lambda x: str(x), html.body.children)) return " ".join(clean) def format_stars(self, num): """ Da formato a la cantidad de estrellas. Regresa la cantidad de estrellas en lugar del número de estas. """ stars = "★" * num while len(stars) < 5: stars += "☆" return stars def format_count(self, num): """ Da formato a la cantidad de descargas. Regresa la cantidad en una cifra separada por comas. """ return "{:,}".format(num) def format_duration(self, num): """ Da formato a duración. Regresa la duración en horas:minutos. """ secs = num * 60 return time.strftime("%H:%M", time.gmtime(secs)) def get_wiki(self, movie, again=True): """ Obtiene artículo de Wikipedia. Primero intenta obtener el artículo por el nombre original. Si no tiene éxito, intenta obtenerlo por el nombre en español. Regresa None si no tuvo ningún éxito. """ wiki = self.get_wiki_page(movie["original_name"]) if not wiki: wiki = self.get_wiki_page(movie["name"]) return wiki def get_wiki_page(self, title): """ Intenta obtener artículo de Wikipedia. Si no tiene éxito, regresa 'None'. """ try: lang = settings.LANGUAGE_CODE.split("-")[0] wiki = wikipediaapi.Wikipedia( lang, extract_format=wikipediaapi.ExtractFormat.HTML ) page = wiki.page(title) if page.exists(): return { "title": page.title, "url": page.fullurl, "summary": self.fix_summ(page.summary), } else: return None except: return None def get_movie_by_id(self, id): """ Obtiene película por id. Esta obtención también añade objetos relacionados. """ related = ["countries", "genders", "directors", "actors"] movie = Movie.objects.prefetch_related(*related).get(pk=id).__dict__ for key, val in movie['_prefetched_objects_cache'].items(): movie[key] = list(map(lambda x: x['name'], val.values())) movie.pop("_state") movie.pop("_prefetched_objects_cache") self.fix_data(movie) return movie def get_movies(self, prefix, query): """ Obtiene películas buscadas. El 'prefix' permite filtrar la consulta por el campo seleccionado. """ selector = self.get_movie_selector(prefix) return f"selector: {selector}; query: {query}" def get_movie_selector(self, prefix): """ Obtiene el selector para filtrar la consulta de una búsqueda. Esto permite restricciones en la búsqueda con esta sintaxis: * t:Un Título => Buscará las películas que tengan 'Un T…' en sus 'name' * d:Nombre => Buscará las películas que tengan 'N…' en sus 'directors' """ prefixes = { 't': 'name', 'o': 'original_name', 'y': 'year', 'c': 'countries', 'p': 'countries', 'd': 'directors', 'a': 'actors', 'g': 'genders', 's': 'section', } if prefix in prefixes: return prefixes[prefix] else: return '' def upload_cartel(instance, filename): first = filename[0].upper() if first.isdigit(): first = "0" return f"{first}/{filename}" class Movie(models.Model): id = models.AutoField(primary_key=True) name = models.CharField("Nombre", max_length=1000) original_name = models.CharField( "Nombre original", max_length=1000, default="", blank=True ) file_name = models.CharField( "Nombre archivo", max_length=1000, default="", blank=True ) year = models.PositiveSmallIntegerField("Año", default=1900) duration = models.PositiveSmallIntegerField("Duración", default=0) directors = models.ManyToManyField( Person, verbose_name="Director", related_name="directors" ) actors = models.ManyToManyField(Person, related_name="actors", blank=True) countries = models.ManyToManyField( Country, related_name="countries", verbose_name="País", blank=True ) genders = models.ManyToManyField( Gender, related_name="genders", verbose_name="Género", blank=True ) cartel = models.ImageField( "Cartel", upload_to=upload_cartel, null=True, blank=True ) count = models.PositiveIntegerField("Descargas", default=0) stars = models.PositiveSmallIntegerField("Estrellas", default=0) published = models.BooleanField("¿Publicar?", default=True) is_digital = models.BooleanField("Es digital", default=False) objects = MovieQuerySet.as_manager() class Meta: unique_together = ["name", "original_name"] ordering = ["name"] verbose_name = "Película" verbose_name_plural = "Películas" def __str__(self): return self.name