Merge pull request 'Reproductor fresón y ajustes' (#4) from frontend into develop

Reviewed-on: #4
This commit is contained in:
Mauricio Baeza 2022-12-09 22:19:02 -06:00
commit 04f0fd88eb
14 changed files with 285 additions and 40 deletions

View File

@ -74,6 +74,12 @@ class Person(models.Model):
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
@ -83,10 +89,24 @@ class MovieQuerySet(models.QuerySet):
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:
@ -100,6 +120,11 @@ class MovieQuerySet(models.QuerySet):
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:
@ -110,6 +135,11 @@ class MovieQuerySet(models.QuerySet):
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")),
@ -119,11 +149,20 @@ class MovieQuerySet(models.QuerySet):
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"])
@ -133,12 +172,25 @@ class MovieQuerySet(models.QuerySet):
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()
@ -146,25 +198,52 @@ class MovieQuerySet(models.QuerySet):
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(
@ -182,12 +261,54 @@ class MovieQuerySet(models.QuerySet):
except:
return None
def get_movie(self, id):
movie = Movie.objects.select_related().get(pk=id).__dict__
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()

File diff suppressed because one or more lines are too long

View File

@ -3,6 +3,7 @@
:root {
--color-primary: #375a7f;
--color-background: #343c3d;
--plyr-color-main: var(--color-primary) !important;
}
#nav {
@ -48,39 +49,39 @@
@media screen and (min-width: 300px) {
.cartels {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (min-width: 450px) {
.cartels {
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (min-width: 600px) {
.cartels {
grid-template-columns: repeat(4, 1fr);
grid-template-columns: repeat(4, 1fr);
}
}
@media screen and (min-width: 750px) {
.cartels {
grid-template-columns: repeat(5, 1fr);
grid-template-columns: repeat(5, 1fr);
}
}
@media screen and (min-width: 900px) {
.cartels {
grid-template-columns: repeat(6, 1fr);
grid-template-columns: repeat(6, 1fr);
}
}
@media screen and (min-width: 1920px) {
.cartels {
width: auto;
margin: 0 !important;
grid-template-columns: repeat(6, 1fr);
width: auto;
margin: 0 !important;
grid-template-columns: repeat(6, 1fr);
}
}
@ -144,6 +145,7 @@
.stats span {
padding-right: 2rem;
white-space: nowrap;
}
.stats i {
@ -178,6 +180,14 @@
width: 8rem;
}
.extra-info {
position: absolute;
bottom: 0;
width: 100%;
background-color: var(--color-primary);
overflow: hidden;
}
.about figure {
width: 50%;
height: auto;
@ -186,6 +196,12 @@
margin: auto;
}
@media screen and (max-width: 1024px) {
table {
margin: auto;
}
}
/* Despliegue de la ficha; Cfr:
* https://stackoverflow.com/questions/42177216/manipulate-css-class-without-javascript
* https://stackoverflow.com/questions/55858255/custom-checkbox-with-css-before-not-working-in-firefox-edge
@ -214,6 +230,34 @@
height: calc(100% - 3em);
}
/* Reproductor */
button.plyr__control--overlaid {
opacity: 1;
}
section#notice {
position: absolute;
width: 100%;
z-index: 3;
}
section#notice div {
padding: 1rem;
}
section#notice p {
font-weight: bold;
color: white;
line-height: 1.5;
}
section#notice a {
display: block;
width: fit-content;
margin: auto;
}
/* ICONS; cfr: https://css.gg */
.gg-time {

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,49 @@
// Deshabilita scroll con la tecla de espacio
// Esto es para que en 'player' quede habilitado para reproducción
window.addEventListener('keydown', (e) => {
if (e.keyCode === 32 && e.target === document.body) {
e.preventDefault();
}
});
// Añade un aviso
function add_notice () {
const divs = document.querySelectorAll('.plyr');
// Ejecuta de nuevo hasta que esté implementado el reproductor
if (divs.length == 0) {
setTimeout(add_notice, 500);
} else {
let hero_sec = document.createElement('section'),
hero_div = document.createElement('div'),
hero_tit = document.createElement('p'),
hero_btn = document.createElement('a');
hero_sec.id = 'notice';
hero_sec.classList.add('hero', 'is-small', 'is-warning', 'has-text-centered');
hero_div.classList.add('hero-body');
hero_btn.classList.add('button', 'is-small', 'is-success');
hero_btn.addEventListener('click', remove_notice);  
hero_tit.innerText = 'Mauflix funciona con pocos recursos. Para un uso más eficiente y ecológico te recomendamos descargar la película.';
hero_btn.innerText = '¡Entendido!';
hero_tit.appendChild(hero_btn);
hero_div.appendChild(hero_tit);
hero_sec.appendChild(hero_div);
divs[0].appendChild(hero_sec);
}
}
// Elimina aviso
function remove_notice () {
let notice = document.querySelector('#notice');
console.log('notice', notice)
if (notice !== null) {
notice.parentNode.removeChild(notice);
}
}
// Implementa el reproductor
const player = new Plyr('#player', {
keyboard: { focused: true, global: true },
controls: ['play-large', 'play', 'progress', 'current-time', 'mute',
'volume', 'pip', 'airplay', 'download', 'fullscreen'],
listeners: { seek: add_notice() },
});

File diff suppressed because one or more lines are too long

View File

@ -11,8 +11,8 @@ def home(request):
return render(request, "home.html", context)
def search(request):
context = {}
def search(request, prefix, query):
context = {"movies": Movie.objects.get_movies(prefix, query)}
return render(request, "search.html", context)
@ -27,10 +27,5 @@ def bugs(request):
def movie(request, id):
context = {"movie": Movie.objects.get_movie(id)}
context = {"movie": Movie.objects.get_movie_by_id(id)}
return render(request, "movie.html", context)
def movies(request, key):
context = {"sections": {"Llave": "Valor"}}
return render(request, "movies.html", context)

View File

@ -17,10 +17,11 @@ v1_api.register(ResourceMovies())
urlpatterns = [
path("", views.home, name="home"),
path("search/", views.search, name="search"),
path("search/<str:prefix>:<str:query>", views.search, name="search"),
path("search/<str:query>", views.search, {'prefix': ''}, name="search"),
path("search/", views.search, {'prefix': '', 'query': ''}, name="search"),
path("about/", views.about, name="about"),
path("bugs/", views.bugs, name="bugs"),
path("movies/<str:key>", views.movies, name="movies"),
path("movie/<int:id>", views.movie, name="movie"),
path("ultimas/rss/", LatestMoviesFeed()),
path("admin/", admin.site.urls),

View File

@ -5,7 +5,7 @@
{% include 'section.html' with section=section content=content %}
{% else %}
{% for gender, val in content.items %}
{% include 'section.html' with section=gender content=val %}
{% include 'section.html' with gender=True section=gender content=val %}
{% endfor %}
{% endif %}
{% endfor %}

View File

@ -19,12 +19,34 @@
<table class="table infobox">
<tbody>
<tr><td>Título original</td><td>{{ movie.original_name }}<td></tr>
<tr><td>Año</td><td>{{ movie.year }}<td></tr>
<tr><td>País</td><td>{{ movie.countries }}<td></tr>
<tr><td>Año</td><td>
<a href="{% url 'search' 'y' movie.year %}">{{ movie.year }}</a>
<td></tr>
<tr><td>País</td><td>
{% for country in movie.countries %}
<a href="{% url 'search' 'p' country %}">
{{ country }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
<td></tr>
<tr><td>Duración</td><td>{{ movie.duration }} min<td></tr>
<tr><td>Dirección</td><td>{{ movie.directors }}<td></tr>
<tr><td>Reparto</td><td>{{ movie.actors }}<td></tr>
<tr><td>Género</td><td>{{ movie.genders }}<td></tr>
<tr><td>Dirección</td><td>
{% for director in movie.directors %}
<a href="{% url 'search' 'd' director %}">
{{ director }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
<td></tr>
<tr><td>Reparto</td><td>
{% for actor in movie.actors %}
<a href="{% url 'search' 'a' actor %}">
{{ actor }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
<td></tr>
<tr><td>Género</td><td>
{% for gender in movie.genders %}
<a href="{% url 'search' 'g' gender %}">
{{ gender }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
<td></tr>
</tbody>
</table>
</div>
@ -32,12 +54,21 @@
{% if movie.wiki.summary %}
<h2 class="subtitle">Sinopsis de <a href="{{ movie.wiki.url }}" target="_blank">Wikipedia</a></h2>
{{ movie.wiki.summary | safe }}
{% else %}
<h2 class="subtitle">Sinopsis de Wikipedia</h2>
<p>No se encontró artículo en Wikipedia, haz <a href="https://es.wikipedia.org/wiki/{{ movie.name }}">clic aquí</a> para escribirlo o solicitarlo.</p>
{% endif %}
</div>
{% if user.is_superuser %}
<video controls>
<video id="player" playsinline controls data-poster="{{ movie.cartel }}">
<source src="{{ movie.file_name }}" type="video/mp4">
</video>
{% load static %}
<!-- CSS y JS necesario paara el reproductor -->
<!-- Cfr. https://github.com/sampotts/plyr -->
<link rel="stylesheet" href="{% static 'css/plyr.css' %}">
<script type="text/javascript" src="{% static 'js/plyr.js' %}"></script>
<script type="text/javascript" src="{% static 'js/player.js' %}"></script>
{% endif %}
{% endif %}
</div>

View File

@ -1,10 +1,11 @@
<div class="hero-foot">
<nav class="tabs is-boxed is-fullwidth">
<div class="container">
<div class="container {% if request.get_full_path == "/" %}extra-info{% endif %}">
<ul>
<li><a href="{{ movie.file_name }}" target="_blank" download>Descargar</a></li>
{% if request.get_full_path == "/" %}
<li><a href="/movie/{{ movie.id }}">Detalles</a></li>
<li><a href="/movie/{{ movie.id }}">Ver</a></li>
{% else %}
<li><a href="{{ movie.file_name }}" target="_blank" download>Descargar</a></li>
{% endif %}
</ul>
</div>

View File

@ -1,6 +0,0 @@
{% extends "base.html" %}
{% block content %}
{% for section, content in sections.items %}
{% include 'section.html' with section=section content=content %}
{% endfor %}
{% endblock %}

View File

@ -2,6 +2,6 @@
{% block content %}
<b>TODO: Búsqueda</b>
<b>TODO: Búsqueda de {{ movies }}</b>
{% endblock %}

View File

@ -1,7 +1,14 @@
<section class="hero">
<div class="hero-body hero-cartels">
{% if request.get_full_path == "/" %}
<p class="title"><a href="movies/{{ section }}">{{ section }}<span class="arrows"/></a></p>
<p class="title">
{% if gender %}
<a href="{% url 'search' 'g' section %}">
{% else %}
<a href="{% url 'search' 's' section %}">
{% endif %}
{{ section }}<span class="arrows"/></a>
</p>
{% else %}
<p class="title">{{ section }}</p>
{% endif %}