add more filtering options

This commit is contained in:
ducklet 2021-06-21 23:48:36 +02:00
parent 7dd10f8bc3
commit d09880438d
5 changed files with 113 additions and 41 deletions

View file

@ -4,4 +4,4 @@ cd "$RUN_DIR"
[ -z "${DEBUG:-}" ] || set -x
exec uvicorn unwind:web_app --reload
exec uvicorn unwind:create_app --factory --reload

View file

@ -4,4 +4,4 @@ cd "$RUN_DIR"
[ -z "${DEBUG:-}" ] || set -x
exec uvicorn --host 0.0.0.0 unwind:web_app
exec uvicorn --host 0.0.0.0 --factory unwind:create_app

View file

@ -1 +1 @@
from .web import app as web_app
from .web import create_app

View file

@ -1,8 +1,10 @@
import logging
import re
from dataclasses import fields
from pathlib import Path
from typing import Optional, Type, TypeVar
import sqlalchemy
from databases import Database
from . import config
@ -152,6 +154,8 @@ async def find_ratings(
title: str = None,
media_type: str = None,
ignore_tv_episodes: bool = False,
include_unrated: bool = False,
year: int = None,
limit_rows=10,
):
values = {
@ -163,17 +167,19 @@ async def find_ratings(
values["escape"] = "#"
escaped_title = sql_escape(title, char=values["escape"])
values["pattern"] = "%" + "%".join(escaped_title.split()) + "%"
values["opattern"] = values["pattern"]
values["oescape"] = values["escape"]
conditions.append(
f"""
(
{Movie._table}.title LIKE :pattern ESCAPE :escape
OR {Movie._table}.original_title LIKE :opattern ESCAPE :oescape
OR {Movie._table}.original_title LIKE :pattern ESCAPE :escape
)
"""
)
if year:
values["year"] = year
conditions.append(f"{Movie._table}.release_year=:year")
if media_type:
values["media_type"] = media_type
conditions.append(f"{Movie._table}.media_type=:media_type")
@ -181,19 +187,44 @@ async def find_ratings(
if ignore_tv_episodes:
conditions.append(f"{Movie._table}.media_type!='TV Episode'")
query = f"""
WITH newest_movies
AS (
source_table = "newest_movies"
ctes = [
f"""{source_table} AS (
SELECT DISTINCT {Rating._table}.movie_id
FROM {Rating._table}
LEFT JOIN {Movie._table} ON {Movie._table}.id={Rating._table}.movie_id
{('WHERE ' + ' AND '.join(conditions)) if conditions else ''}
ORDER BY length({Movie._table}.title) ASC, {Rating._table}.rating_date DESC
LIMIT :limit_rows
)"""
]
if include_unrated:
source_table = "target_movies"
ctes.extend(
[
f"""unrated_movies AS (
SELECT DISTINCT id AS movie_id
FROM {Movie._table}
WHERE id NOT IN newest_movies
{('AND ' + ' AND '.join(conditions)) if conditions else ''}
ORDER BY length(title) ASC, release_year DESC
LIMIT :limit_rows
)""",
f"""{source_table} AS (
SELECT * FROM newest_movies
UNION ALL -- using ALL here avoids the reordering of IDs
SELECT * FROM unrated_movies
)""",
]
)
query = f"""
WITH
{','.join(ctes)}
SELECT
{User._table}.name AS user_name,
-- {User._table}.name AS user_name,
{Rating._table}.score AS user_score,
{Movie._table}.score AS imdb_score,
{Movie._table}.imdb_id AS movie_imdb_id,
@ -201,11 +232,33 @@ async def find_ratings(
{Movie._table}.title AS canonical_title,
{Movie._table}.original_title AS original_title,
{Movie._table}.release_year AS release_year
FROM newest_movies
LEFT JOIN {Rating._table} ON {Rating._table}.movie_id=newest_movies.movie_id
LEFT JOIN {User._table} ON {User._table}.id={Rating._table}.user_id
LEFT JOIN {Movie._table} ON {Movie._table}.id={Rating._table}.movie_id
FROM {source_table}
LEFT JOIN {Rating._table} ON {Rating._table}.movie_id={source_table}.movie_id
-- LEFT JOIN {User._table} ON {User._table}.id={Rating._table}.user_id
LEFT JOIN {Movie._table} ON {Movie._table}.id={source_table}.movie_id
LIMIT :limit_rows
"""
rows = await shared_connection().fetch_all(query=query, values=values)
rows = await shared_connection().fetch_all(bindparams(query, values))
return tuple(dict(r) for r in rows)
def bindparams(query: str, values: dict):
"""Bind values to a query.
This is similar to what SQLAlchemy and Databases do, but it allows to
easily use the same placeholder in multiple places.
"""
pump_vals = {}
pump_keys = {}
def pump(match):
key = match[1]
val = values[key]
pump_keys[key] = 1 + pump_keys.setdefault(key, 0)
pump_key = f"{key}_{pump_keys[key]}"
pump_vals[pump_key] = val
return f":{pump_key}"
pump_query = re.sub(r":(\w+)\b", pump, query)
return sqlalchemy.text(pump_query).bindparams(**pump_vals)

View file

@ -1,5 +1,6 @@
import base64
import binascii
import logging
from starlette.applications import Starlette
from starlette.authentication import (
@ -19,6 +20,8 @@ from . import config, db
from .db import close_connection_pool, find_ratings, open_connection_pool
from .models import Movie, asplain
log = logging.getLogger(__name__)
class BasicAuthBackend(AuthenticationBackend):
async def authenticate(self, request):
@ -48,11 +51,13 @@ def truthy(s: str):
async def ratings(request):
title = request.query_params.get("title")
media_type = request.query_params.get("media_type")
ignore_tv_episodes = truthy(request.query_params.get("ignore_tv_episodes"))
params = request.query_params
rows = await find_ratings(
title=title, media_type=media_type, ignore_tv_episodes=ignore_tv_episodes
title=params.get("title"),
media_type=params.get("media_type"),
ignore_tv_episodes=truthy(params.get("ignore_tv_episodes")),
include_unrated=truthy(params.get("include_unrated")),
year=int(params["year"]) if "year" in params else None,
)
aggr = {}
@ -69,6 +74,7 @@ async def ratings(request):
"media_type": r["media_type"],
},
)
if r["user_score"] is not None:
mov["user_scores"].append(r["user_score"])
resp = tuple(aggr.values())
@ -121,7 +127,16 @@ async def get_ratings_for_group(request):
request.path_params["group_id"]
app = Starlette(
def create_app():
if config.loglevel == "DEBUG":
logging.basicConfig(
format="%(asctime)s.%(msecs)03d [%(name)s:%(process)d] %(levelname)s: %(message)s",
datefmt="%H:%M:%S",
level=config.loglevel,
)
log.debug(f"Log level: {config.loglevel}")
return Starlette(
on_startup=[open_connection_pool],
on_shutdown=[close_connection_pool],
routes=[
@ -133,9 +148,13 @@ app = Starlette(
Route("/movies", add_movie, methods=["POST"]),
Route("/users", add_user, methods=["POST"]),
Route("/users/{user_id}/ratings", ratings_for_user),
Route("/users/{user_id}/ratings", set_rating_for_user, methods=["PUT"]),
Route(
"/users/{user_id}/ratings", set_rating_for_user, methods=["PUT"]
),
Route("/groups", add_group, methods=["POST"]),
Route("/groups/{group_id}/users", add_user_to_group, methods=["POST"]),
Route(
"/groups/{group_id}/users", add_user_to_group, methods=["POST"]
),
Route("/groups/{group_id}/ratings", get_ratings_for_group),
],
),