diff --git a/requirements.txt b/requirements.txt index 9881b6f..49e6533 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ django-admin-list-filter-dropdown wikipedia-api bs4 lxml +unidecode diff --git a/source/main/admin.py b/source/main/admin.py index 85e25e3..e447f68 100644 --- a/source/main/admin.py +++ b/source/main/admin.py @@ -98,7 +98,9 @@ class AdminMovie(admin.ModelAdmin): return def _public_telegram(self, message, cartel): - url = f"https://api.telegram.org/bot{settings.TOKEN_TELEGRAM}/sendPhoto" + url = ( + f"https://api.telegram.org/bot{settings.TOKEN_TELEGRAM}/sendPhoto" + ) url_cartel = settings.URL_CDN.format(cartel) data = { "chat_id": settings.CHAT_ID, diff --git a/source/main/migrations/0001_initial.py b/source/main/migrations/0001_initial.py index 23749fa..9ddee64 100644 --- a/source/main/migrations/0001_initial.py +++ b/source/main/migrations/0001_initial.py @@ -79,7 +79,9 @@ class Migration(migrations.Migration): ), ( "is_actor", - models.BooleanField(default=False, verbose_name="Es Actor"), + models.BooleanField( + default=False, verbose_name="Es Actor" + ), ), ( "is_director", diff --git a/source/main/migrations/0003_movie_is_digital.py b/source/main/migrations/0003_movie_is_digital.py index 800ffdf..608ed2b 100644 --- a/source/main/migrations/0003_movie_is_digital.py +++ b/source/main/migrations/0003_movie_is_digital.py @@ -13,6 +13,8 @@ class Migration(migrations.Migration): migrations.AddField( model_name="movie", name="is_digital", - field=models.BooleanField(default=False, verbose_name="Es digital"), + field=models.BooleanField( + default=False, verbose_name="Es digital" + ), ), ] diff --git a/source/main/migrations/0005_auto_20230109_1810.py b/source/main/migrations/0005_auto_20230109_1810.py index 1d899e9..c8b8fee 100644 --- a/source/main/migrations/0005_auto_20230109_1810.py +++ b/source/main/migrations/0005_auto_20230109_1810.py @@ -6,28 +6,28 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('main', '0004_auto_20210807_2207'), + ("main", "0004_auto_20210807_2207"), ] operations = [ migrations.AlterField( - model_name='country', - name='id', + model_name="country", + name="id", field=models.AutoField(primary_key=True, serialize=False), ), migrations.AlterField( - model_name='gender', - name='id', + model_name="gender", + name="id", field=models.AutoField(primary_key=True, serialize=False), ), migrations.AlterField( - model_name='movie', - name='id', + model_name="movie", + name="id", field=models.AutoField(primary_key=True, serialize=False), ), migrations.AlterField( - model_name='person', - name='id', + model_name="person", + name="id", field=models.AutoField(primary_key=True, serialize=False), ), ] diff --git a/source/main/models.py b/source/main/models.py index 3449778..c8d3335 100644 --- a/source/main/models.py +++ b/source/main/models.py @@ -1,10 +1,12 @@ import random import time +import re import wikipediaapi from bs4 import BeautifulSoup from django.conf import settings from django.db import models from pathlib import Path +from unidecode import unidecode class Gender(models.Model): @@ -71,14 +73,15 @@ class Person(models.Model): class MovieQuerySet(models.QuerySet): - def random_pick(self, random_max=6, min_items=20, **kwargs): + def random_pick(self, random_max=6, min_items=20, all=None, **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 all is None: + all = list(Movie.objects.filter(**kwargs).values()) if len(all) < min_items: return None elif len(all) < random_max: @@ -94,7 +97,7 @@ class MovieQuerySet(models.QuerySet): Por defecto regresa las primeras 6 películas como máximo. """ all = list(Movie.objects.order_by(f"-{key}").values()) - return all[:top_max] + return self.fix_all(all[:top_max]) def top_random_pick(self, key, top_max=6): """ @@ -113,9 +116,10 @@ class MovieQuerySet(models.QuerySet): else: break if len(top) > top_max: - return random.sample(top, top_max) + movies = random.sample(top, top_max) else: - return top + movies = top + return self.fix_all(movies) def home_pick(self): """ @@ -139,9 +143,9 @@ class MovieQuerySet(models.QuerySet): Las secciones son novedades, mejor valorados y más descargados. """ sections = { - "Novedades": self.fix_all(self.top_pick("id")), - "Mejor valorados": self.fix_all(self.top_random_pick("stars")), - "Más descargados": self.fix_all(self.top_pick("count")), + "Novedades": self.top_pick("id"), + "Mejor valorados": self.top_random_pick("stars"), + "Más descargados": self.top_pick("count"), } sections["genders"] = {} return sections @@ -161,6 +165,8 @@ class MovieQuerySet(models.QuerySet): Los enmiendos son formateos de unos valores (que se guardan en nuevas llaves) y de rutas a medios. """ + if len(movie["file_name"]) == 0 or len(movie["cartel"]) == 0: + print(f"WARN: película sin ruta de video o cartel:\n{movie}") movie["duration_formatted"] = self.format_duration(movie["duration"]) movie["count_formatted"] = self.format_count(movie["count"]) movie["stars_icons"] = self.format_stars(movie["stars"]) @@ -177,7 +183,7 @@ class MovieQuerySet(models.QuerySet): ('DEBUG'). La URL_DEBUB apunta a la dirección en nebula. """ if settings.DEBUG: - if str(Path(el).parent) == '.': + if str(Path(el).parent) == "." and len(el) > 0: el = f"{el[0]}/{el}" return settings.URL_DEBUG.format(el) else: @@ -282,53 +288,146 @@ class MovieQuerySet(models.QuerySet): except Exception: return None + def get_related(self): + """ + Regresa los campos relacionados a las películas. + """ + return ["countries", "genders", "directors", "actors"] + 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"] + related = self.get_related() 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())) + 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): + def get_movies(self, 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}" + movies = Movie.objects.prefetch_related(*self.get_related()) + for q in self.get_queries(query): + selector = None + if re.match(r"^\w:", q) is not None: + selector = self.get_selector(re.sub(r"^(\w):.*", r"\1", q)) + q = re.sub(r"^\w:(.*)", r"\1", q) + movies = self.get_movies_by_query(selector, q, movies) + if len(movies) != len(Movie.objects.all()): + if hasattr(movies, "values"): + movies = self.fix_all(list(movies.values())) + movies = self.random_pick(random_max=100, min_items=0, all=movies) + return movies + else: + return [] - def get_movie_selector(self, prefix): + def get_movies_by_query(self, selector, query, movies): + """ + Obtiene película por query. + """ + if selector is None: + return self.get_movies_by_query_any(query, movies) + elif selector != "section": + return self.get_movies_by_query_selector(selector, query, movies) + else: + return self.get_movies_by_query_section(query, movies) + + def get_movies_by_query_selector(self, selector, query, movies): + """ + Obtiene película por query que tiene selector 'w:'. + """ + if selector == "year": + selector = f"{selector}__iregex" + try: + query = int(query) + except Exception: + query = 0 + else: + if selector == "name" or selector == "original_name": + selector = f"{selector}__unaccent__iregex" + else: + selector = f"{selector}__name__unaccent__iregex" + query = f"[[:<:]]{query}" + kwargs = {selector: query} + return movies.filter(**kwargs) + + def get_movies_by_query_section(self, query, movies): + """ + Obtiene película por query que tiene selector 'w:' para sección. + """ + if query == "mejor valorados": + return self.top_random_pick("stars", top_max=100) + elif query == "novedades": + return self.top_pick("id", top_max=100) + elif query == "mas descargados": + return self.top_pick("count", top_max=100) + else: + return list(movies.values()) + + def get_movies_by_query_any(self, query, movies): + """ + Obtiene película por query que no tiene ningún selector. + """ + for field in ["name"] + self.get_related(): + if field != "name": + field = f"{field}__name" + kwargs = {f"{field}__unaccent__iregex": f"[[:<:]]{query}"} + result = movies.filter(**kwargs) + if len(result) > 0: + movies = result + return movies + + def get_queries(self, query): + """ + Devuelve un conjunto de queries sanitizado; p. ej.: + de: d:Bruno--Stagñaro y:1997 pizzá, birra, faso + a: ['d:bruno-stagnaro', 'y:1997', 'pizza', 'birra', 'faso'] + """ + queries = re.sub(r"\s+", " ", unidecode(str(query)).lower()).split() + return list(map(lambda q: self.clean_query(q), queries)) + + def clean_query(self, word): + """ + Limpia query de caracteres 'W' al inicio y al final. También sustituye + '-' por un espacio. + """ + word = re.sub(r"^\W+", "", word) + word = re.sub(r"\W+$", "", word) + word = re.sub(r"-+", " ", word) + return word + + def get_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' + * 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' + El guion (-) se usa como separador de palabras, puede contener acentos + o eñe, aunque va a ser decodificado. """ prefixes = { - 't': 'name', - 'o': 'original_name', - 'y': 'year', - 'c': 'countries', - 'p': 'countries', - 'd': 'directors', - 'a': 'actors', - 'g': 'genders', - 's': 'section', + "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 '' + return None def upload_cartel(instance, filename): diff --git a/source/main/static/css/main.css b/source/main/static/css/main.css index 1533953..82c1966 100644 --- a/source/main/static/css/main.css +++ b/source/main/static/css/main.css @@ -19,6 +19,10 @@ display: flex !important; } +.navbar-item img { + width: 100%; +} + /* Cada flecha en los títulos de secciones */ .arrows { font-size: 1.5rem; @@ -291,6 +295,21 @@ section#notice a { margin: auto; } +/* SEARCH */ + +.search-div > * { + display: inline-block; +} + +.search-div input { + width: calc(100% - 4.1rem); +} + +.search-div button { + width: 3.8rem; + height: 3.8rem; +} + /* ICONS; cfr: https://css.gg */ .gg-time { @@ -396,3 +415,30 @@ section#notice a { border-bottom-right-radius: 40px; right: -6px } + +.gg-search { + box-sizing: border-box; + position: relative; + display: block; + transform: scale(var(--ggs,1)); + width: 16px; + height: 16px; + border: 2px solid; + border-radius: 100%; + margin-left: -4px; + margin-top: -4px +} + +.gg-search::after { + content: ""; + display: block; + box-sizing: border-box; + position: absolute; + border-radius: 3px; + width: 2px; + height: 8px; + background: currentColor; + transform: rotate(-45deg); + top: 10px; + left: 12px +} diff --git a/source/main/static/img/favicon.png b/source/main/static/img/favicon.png index 2ff4692..d42379d 100644 Binary files a/source/main/static/img/favicon.png and b/source/main/static/img/favicon.png differ diff --git a/source/main/static/img/favicon.svg b/source/main/static/img/favicon.svg new file mode 100644 index 0000000..86d0fdd --- /dev/null +++ b/source/main/static/img/favicon.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/source/main/static/img/logo-mauflix.png b/source/main/static/img/logo-mauflix.png index e0c704f..fd1eac6 100644 Binary files a/source/main/static/img/logo-mauflix.png and b/source/main/static/img/logo-mauflix.png differ diff --git a/source/main/static/img/logo-mauflix.svg b/source/main/static/img/logo-mauflix.svg new file mode 100644 index 0000000..02de9b1 --- /dev/null +++ b/source/main/static/img/logo-mauflix.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/source/main/views.py b/source/main/views.py index 9a5d294..6e51182 100644 --- a/source/main/views.py +++ b/source/main/views.py @@ -11,8 +11,12 @@ def home(request): return render(request, "home.html", context) -def search(request, prefix, query): - context = {"movies": Movie.objects.get_movies(prefix, query)} +def search(request): + if request.GET.get("q"): + query = request.GET["q"] + else: + query = "" + context = {"movies": Movie.objects.get_movies(query), "query": query} return render(request, "search.html", context) @@ -21,6 +25,11 @@ def about(request): return render(request, "about.html", context) +def help(request): + context = {} + return render(request, "help.html", context) + + def bugs(request): context = {} return render(request, "bugs.html", context) diff --git a/source/mauflix/settings.py b/source/mauflix/settings.py index 4156916..dea7bb1 100644 --- a/source/mauflix/settings.py +++ b/source/mauflix/settings.py @@ -48,6 +48,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "django.contrib.postgres", "main.apps.MainConfig", ] diff --git a/source/mauflix/urls.py b/source/mauflix/urls.py index df9feba..5a4e6ec 100644 --- a/source/mauflix/urls.py +++ b/source/mauflix/urls.py @@ -8,6 +8,7 @@ from django.conf.urls import include from main import views from main.feeds import LatestMoviesFeed + # ~ from main.api import ResourceMovies @@ -17,10 +18,9 @@ from main.feeds import LatestMoviesFeed urlpatterns = [ path("", views.home, name="home"), - path("search/:", views.search, name="search"), - path("search/", views.search, {'prefix': ''}, name="search"), - path("search/", views.search, {'prefix': '', 'query': ''}, name="search"), + path("search/", views.search, name="search"), path("about/", views.about, name="about"), + path("help/", views.help, name="help"), path("bugs/", views.bugs, name="bugs"), path("movie/", views.movie, name="movie"), path("ultimas/rss/", LatestMoviesFeed()), diff --git a/source/templates/base.html b/source/templates/base.html index 9e77e1d..67d53bc 100644 --- a/source/templates/base.html +++ b/source/templates/base.html @@ -14,7 +14,7 @@
-

Otras plataformas tienen todas las películas, excepto las que nos gustan :)

+

Mauflix. Otras plataformas tienen todas las películas, excepto las que nos gustan :)

{% include 'nav.html' %} diff --git a/source/templates/help.html b/source/templates/help.html new file mode 100644 index 0000000..5bc1b08 --- /dev/null +++ b/source/templates/help.html @@ -0,0 +1,103 @@ +{% extends "base.html" %} + +{% block content %} +
+
+

Ayuda

+
+

Sobre las búsquedas

+

En las búsquedas existen las siguientes consideraciones:

+
    +
  • + Las búsquedas tienen un límite de cien resultados. +
  • +
  • + Cualquier búsqueda ignora tildes, mayúsculas y eñes, + así que «Iñarritú» + es lo mismo que «inarritu». +
  • +
  • + Cuando una búsqueda encuentra más de cien películas, + los cien resultados son seleccionados y ordenados de manera aleatoria: + nunca obtendrás los mismos resultados. +
  • +
+

Refinamiento en las búsquedas

+

Para buscar por campos específicos se usan algunos de los siguientes prefijos:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
prefijo:ejemploDescripción
t:lucioBusca «lucio» en los títulos
o:monkeyBusca «monkey» en los títulos en idioma original
y:1989Busca películas publicadas en el año (year) «1989»
p:MéxicoBusca películas hechas en el país «México»
d:vardaBusca películas dirigidas por «varda»
a:umaBusca películas en donde actúe «uma»
g:dramaBusca películas del género «drama»
+

Esto añade las siguientes consideraciones:

+
    +
  • + Para dos o más palabras con prefijos se usan guiones en lugar de espacios, + como en «p:estados-unidos». +
  • +
  • + Se pueden usar varias palabras con prefijos o no para restringir la búsqueda, + como en «p:mexico d:buñuel cielo». +
  • +
+

API

+

+ Los resultados de las búsquedas pueden ser en JSON si usas la + API. +

+

Obtención de películas

+

+ La API para obtener películas usa la misma sintaxis a una búsqueda ordinaria, + solo cambia la url search por api. +

+

+ Es decir, en lugar de + search/?q=p:mexico+d:cuaron usa + api/?q=p:mexico+d:cuaron. +

+

Obtención de ficha

+

+ Para obtener una película en específico solo necesitas su ID. +

+ Por ejemplo, para obtener la ficha de Gremlins usa + api/?id=1596. +

+

Happy hacking 😎

+
+
+
+{% endblock %} diff --git a/source/templates/home.html b/source/templates/home.html index 9ed4f80..38a969e 100644 --- a/source/templates/home.html +++ b/source/templates/home.html @@ -2,7 +2,13 @@ {% block content %} {% for section, content in sections.items %} {% if section != "genders"%} - {% include 'section.html' with section=section content=content %} + {% if section == 'Más descargados' %} + {% if user.is_authenticated %} + {% include 'section.html' with section=section content=content %} + {% endif %} + {% else %} + {% include 'section.html' with section=section content=content %} + {% endif %} {% else %} {% for gender, val in content.items %} {% include 'section.html' with gender=True section=gender content=val %} diff --git a/source/templates/info-body.html b/source/templates/info-body.html index 62450f5..155647e 100644 --- a/source/templates/info-body.html +++ b/source/templates/info-body.html @@ -2,8 +2,10 @@ {% if request.get_full_path == "/" %}

{{ movie.stars_icons }}

-

{{ movie.duration_formatted }}

-

{{ movie.count_formatted }}

+

{{ movie.duration_formatted | safe }}

+ {% if user.is_authenticated %} +

{{ movie.count_formatted }}

+ {% endif %}
{% else %}
@@ -20,30 +22,30 @@ Título original{{ movie.original_name }} Año - {{ movie.year }} + {{ movie.year }} País {% for country in movie.countries %} - + {{ country }}{% if not forloop.last %},{% endif %} {% endfor %} Duración{{ movie.duration }} min Dirección {% for director in movie.directors %} - + {{ director }}{% if not forloop.last %},{% endif %} {% endfor %} Reparto {% for actor in movie.actors %} - + {{ actor }}{% if not forloop.last %},{% endif %} {% endfor %} Género {% for gender in movie.genders %} - + {{ gender }}{% if not forloop.last %},{% endif %} {% endfor %} @@ -59,7 +61,7 @@

No se encontró su artículo en Wikipedia, haz clic aquí para escribirlo o solicitarlo.

{% endif %}
- {% if user.is_superuser %} + {% if user.is_authenticated %} diff --git a/source/templates/info-head.html b/source/templates/info-head.html index 33462fd..9eb0c3a 100644 --- a/source/templates/info-head.html +++ b/source/templates/info-head.html @@ -3,8 +3,10 @@ {% if request.get_full_path != "/" %}

{{ movie.stars_icons }} - {{ movie.duration_formatted }} - {{ movie.count_formatted }} + {{ movie.duration_formatted | safe }} + {% if user.is_authenticated %} + {{ movie.count_formatted }} + {% endif %}

{% endif %} diff --git a/source/templates/nav.html b/source/templates/nav.html index 6a007b2..a1242bd 100644 --- a/source/templates/nav.html +++ b/source/templates/nav.html @@ -15,15 +15,18 @@ {% if request.get_full_path != "/" %} Inicio {% endif %} - {% if request.get_full_path != "/search/" %} + {% if request.path != "/search/" %} Buscar {% endif %} diff --git a/source/templates/search.html b/source/templates/search.html index eb1ac2b..9460127 100644 --- a/source/templates/search.html +++ b/source/templates/search.html @@ -2,6 +2,32 @@ {% block content %} -TODO: Búsqueda de {{ movies }} +
+
+
+ +
+ +
+
+
+
+ +{% if movies %} + {% include 'section.html' with section='Resultados de búsqueda:' content=movies %} +{% else %} +
+
+

+ {% if request.get_full_path == "/search/" %} + Explora el catálogo de Mauflix. + {% else %} + ¡Ups! Tu búsqueda no arrojó ningún resultado. + {% endif %} +

+

Visita la ayuda para obtener mejores resultados. +

+
+{% endif %} {% endblock %} diff --git a/source/templates/section.html b/source/templates/section.html index ff4735e..50b5451 100644 --- a/source/templates/section.html +++ b/source/templates/section.html @@ -3,9 +3,9 @@ {% if request.get_full_path == "/" %}

{% if gender %} - + {% else %} - + {% endif %} {{ section }}