diff --git a/app.py b/app.py index 5faedc3..5760375 100644 --- a/app.py +++ b/app.py @@ -23,7 +23,7 @@ else: CACHE_TYPE = "NullCache" # Create a Flask WSGI app and configure it using values from the module. -app = Flask(__name__) +app = Flask(__name__,static_folder='web/dist/assets/',static_url_path='/assets/') app.config.from_object(__name__) cache = Cache(app) flask_db = FlaskDB(app) diff --git a/data.py b/data.py index 21c3e0c..bb96eea 100644 --- a/data.py +++ b/data.py @@ -310,5 +310,9 @@ series_data = [ slug="ClubOfMisfits", videos=["PRmVQKOy9Bo"] ) - ] + +series_data_by_slug = {} + +for series in series_data: + series_data_by_slug[series.slug] = series diff --git a/poetry.lock b/poetry.lock index f0122a6..5a01e0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -119,6 +119,7 @@ spacy = ">=3.2.0,<3.3.0" [package.source] type = "url" url = "https://github.com/explosion/spacy-models/releases/download/en_core_web_md-3.2.0/en_core_web_md-3.2.0.tar.gz" + [[package]] name = "flask" version = "2.0.2" @@ -275,6 +276,14 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pillow" +version = "8.4.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "preshed" version = "3.0.6" @@ -653,7 +662,7 @@ python-versions = "*" [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "e78a950da9152281b77bedf02d5880ca8585cee5c4cff9795be982b9118dd68e" +content-hash = "03b797097ee3ce958b736a02b1c868e627fad600b5f669eeaf287d4aa318419b" [metadata.files] about-time = [ @@ -853,6 +862,49 @@ pathy = [ peewee = [ {file = "peewee-3.14.8.tar.gz", hash = "sha256:01bd7f734defb08d7a3346a0c0ca7011bc8d0d685934ec0e001b3371d522ec53"}, ] +pillow = [ + {file = "Pillow-8.4.0-cp310-cp310-macosx_10_10_universal2.whl", hash = "sha256:81f8d5c81e483a9442d72d182e1fb6dcb9723f289a57e8030811bac9ea3fef8d"}, + {file = "Pillow-8.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3f97cfb1e5a392d75dd8b9fd274d205404729923840ca94ca45a0af57e13dbe6"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb9fc393f3c61f9054e1ed26e6fe912c7321af2f41ff49d3f83d05bacf22cc78"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d82cdb63100ef5eedb8391732375e6d05993b765f72cb34311fab92103314649"}, + {file = "Pillow-8.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62cc1afda735a8d109007164714e73771b499768b9bb5afcbbee9d0ff374b43f"}, + {file = "Pillow-8.4.0-cp310-cp310-win32.whl", hash = "sha256:e3dacecfbeec9a33e932f00c6cd7996e62f53ad46fbe677577394aaa90ee419a"}, + {file = "Pillow-8.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:620582db2a85b2df5f8a82ddeb52116560d7e5e6b055095f04ad828d1b0baa39"}, + {file = "Pillow-8.4.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:1bc723b434fbc4ab50bb68e11e93ce5fb69866ad621e3c2c9bdb0cd70e345f55"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72cbcfd54df6caf85cc35264c77ede902452d6df41166010262374155947460c"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70ad9e5c6cb9b8487280a02c0ad8a51581dcbbe8484ce058477692a27c151c0a"}, + {file = "Pillow-8.4.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25a49dc2e2f74e65efaa32b153527fc5ac98508d502fa46e74fa4fd678ed6645"}, + {file = "Pillow-8.4.0-cp36-cp36m-win32.whl", hash = "sha256:93ce9e955cc95959df98505e4608ad98281fff037350d8c2671c9aa86bcf10a9"}, + {file = "Pillow-8.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2e4440b8f00f504ee4b53fe30f4e381aae30b0568193be305256b1462216feff"}, + {file = "Pillow-8.4.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:8c803ac3c28bbc53763e6825746f05cc407b20e4a69d0122e526a582e3b5e153"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a17b5d948f4ceeceb66384727dde11b240736fddeda54ca740b9b8b1556b29"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1394a6ad5abc838c5cd8a92c5a07535648cdf6d09e8e2d6df916dfa9ea86ead8"}, + {file = "Pillow-8.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:792e5c12376594bfcb986ebf3855aa4b7c225754e9a9521298e460e92fb4a488"}, + {file = "Pillow-8.4.0-cp37-cp37m-win32.whl", hash = "sha256:d99ec152570e4196772e7a8e4ba5320d2d27bf22fdf11743dd882936ed64305b"}, + {file = "Pillow-8.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7b7017b61bbcdd7f6363aeceb881e23c46583739cb69a3ab39cb384f6ec82e5b"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:d89363f02658e253dbd171f7c3716a5d340a24ee82d38aab9183f7fdf0cdca49"}, + {file = "Pillow-8.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a0956fdc5defc34462bb1c765ee88d933239f9a94bc37d132004775241a7585"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b7bb9de00197fb4261825c15551adf7605cf14a80badf1761d61e59da347779"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72b9e656e340447f827885b8d7a15fc8c4e68d410dc2297ef6787eec0f0ea409"}, + {file = "Pillow-8.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5a4532a12314149d8b4e4ad8ff09dde7427731fcfa5917ff16d0291f13609df"}, + {file = "Pillow-8.4.0-cp38-cp38-win32.whl", hash = "sha256:82aafa8d5eb68c8463b6e9baeb4f19043bb31fefc03eb7b216b51e6a9981ae09"}, + {file = "Pillow-8.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:066f3999cb3b070a95c3652712cffa1a748cd02d60ad7b4e485c3748a04d9d76"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:5503c86916d27c2e101b7f71c2ae2cddba01a2cf55b8395b0255fd33fa4d1f1a"}, + {file = "Pillow-8.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4acc0985ddf39d1bc969a9220b51d94ed51695d455c228d8ac29fcdb25810e6e"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b052a619a8bfcf26bd8b3f48f45283f9e977890263e4571f2393ed8898d331b"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:493cb4e415f44cd601fcec11c99836f707bb714ab03f5ed46ac25713baf0ff20"}, + {file = "Pillow-8.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8831cb7332eda5dc89b21a7bce7ef6ad305548820595033a4b03cf3091235ed"}, + {file = "Pillow-8.4.0-cp39-cp39-win32.whl", hash = "sha256:5e9ac5f66616b87d4da618a20ab0a38324dbe88d8a39b55be8964eb520021e02"}, + {file = "Pillow-8.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:3eb1ce5f65908556c2d8685a8f0a6e989d887ec4057326f6c22b24e8a172c66b"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ddc4d832a0f0b4c52fff973a0d44b6c99839a9d016fe4e6a1cb8f3eea96479c2"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a3e5ddc44c14042f0844b8cf7d2cd455f6cc80fd7f5eefbe657292cf601d9ad"}, + {file = "Pillow-8.4.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c70e94281588ef053ae8998039610dbd71bc509e4acbc77ab59d7d2937b10698"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:3862b7256046fcd950618ed22d1d60b842e3a40a48236a5498746f21189afbbc"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4901622493f88b1a29bd30ec1a2f683782e57c3c16a2dbc7f2595ba01f639df"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84c471a734240653a0ec91dec0996696eea227eafe72a33bd06c92697728046b"}, + {file = "Pillow-8.4.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:244cf3b97802c34c41905d22810846802a3329ddcb93ccc432870243211c79fc"}, + {file = "Pillow-8.4.0.tar.gz", hash = "sha256:b8e2f83c56e141920c39464b852de3719dfbfb6e3c99a2d8da0edf4fb33176ed"}, +] preshed = [ {file = "preshed-3.0.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9683730127658b531120b4ed5cff1f2a567318ab75e9ab0f22cc84ae1486c23"}, {file = "preshed-3.0.6-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c98f725d8478f3ade4ab1ea00f50a92d2d9406d37276bc46fd8bab1d47452c4"}, diff --git a/pyproject.toml b/pyproject.toml index 3d43686..4186457 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ blinker = "^1.4" # tmp workaround to make flask-sentry work Flask-Caching = "^1.10.1" redis = "^4.0.2" prettytable = "^2.2.1" +Pillow = "^8.4.0" [tool.poetry.dev-dependencies] diff --git a/server.py b/server.py index 8944831..08f4573 100644 --- a/server.py +++ b/server.py @@ -12,9 +12,11 @@ from models import * # logger = logging.getLogger('peewee') # logger.addHandler(logging.StreamHandler()) # logger.setLevel(logging.DEBUG) +from ssr import ssr_routes from stats import TotalWords, MostCommonNounChunks, LongestNounChunks, LinesPerPerson, aggregate_stats from suggestions import suggestions +app.register_blueprint(ssr_routes) def add_cors(response: Response) -> Response: header = response.headers diff --git a/ssr.py b/ssr.py new file mode 100644 index 0000000..e8e132e --- /dev/null +++ b/ssr.py @@ -0,0 +1,143 @@ +""" +do some kind of pseudo SSR, modifying the index.html to contain metadata +""" +import textwrap +from io import BytesIO +from textwrap import shorten +from urllib.parse import unquote + +from PIL import Image, ImageFont, ImageDraw +from flask import Blueprint, redirect, render_template, abort, request, send_file +from playhouse.flask_utils import get_object_or_404 + +from app import cache +from data import series_data_by_slug +from models import Episode, Series + +ssr_routes = Blueprint("ssr_routes", __name__, template_folder="templates") + +with open("./web/dist/index.html") as f: + index_html = f.read() + +placeholder_token = 'CR Search' + + +def draw_image(text: str, description: str, subtitle=None) -> BytesIO: + text = text.replace("| CR Search", "").strip() + text = shorten(text, 25, placeholder=" ...") + if subtitle: + text += "\n" + subtitle + width, height = (1200, 600) + img = Image.open("./web/src/assets/background_small.png") + + mr_eves = ImageFont.truetype("web/fonts/Mr Eaves/Mr Eaves Small Caps.otf", 50) + title_font = ImageFont.truetype("web/fonts/Nodesto Caps Condensed/Nodesto Caps Condensed.otf", 120) + small_font = ImageFont.truetype("web/fonts/Scaly Sans/Scaly Sans.otf", 30) + draw = ImageDraw.Draw(img) + w, h = draw.multiline_textsize(text, title_font) + + draw.multiline_text(((width - w) / 2, (height - h) / 4), text, + font=title_font, align="center", + fill="#58180d") + description = textwrap.fill(description, 50) + w, h = draw.multiline_textsize(description, mr_eves) + draw.multiline_text(((width - w) / 2, (height - h) / 3 * 2), description, + font=mr_eves, align="center", + fill="black") + footer_text = "Critical Role Search" + w, h = draw.multiline_textsize(footer_text, small_font) + draw.multiline_text(((width - w - 10), (height - h - 10)), footer_text, + font=small_font, align="center", + fill="black") + url = request.url.split("?")[0] + url = unquote(url) + if len(url) > 50: + url = "/".join(url.split("/")[:-1]) + w, h = draw.multiline_textsize(url, small_font) + draw.multiline_text((10, (height - h - 10)), url, + font=small_font, align="center", + fill="black") + + byte_io = BytesIO() + img.save(byte_io, "PNG", optimize=True) + byte_io.seek(0) + return byte_io + + +@ssr_routes.route("/", defaults={'something': None}) +@ssr_routes.route("//") +def home_redirect(something): + return redirect("/campaign3/10/") + + +@ssr_routes.route("/episodes") +@cache.cached(timeout=60 * 60 * 24 * 30, query_string=True) +def episodes(): + num = Episode.select().count() + description = f"Overview over {num} imported episodes of Critical Role" + title = "Episode Overview | CR Search" + if request.args.get("image", None) == "true": + return send_file(draw_image(title, description), mimetype="image/png") + + additional_html = render_template( + "header.html", + description=description, + title=title, + url=request.url + ) + return index_html.replace(placeholder_token, additional_html) + + +@ssr_routes.route("/transcript///") +@cache.cached(timeout=60 * 60 * 24 * 30, query_string=True) +def transcript(series, episode_number): + episode = get_object_or_404(Episode.select(Episode, Series).where( + (Episode.episode_number == episode_number) + & + (Episode.series.slug == series) + ).join(Series)) + description = f"Browse through the transcript of episode {episode_number} of {episode.series.title} (“{episode.pretty_title}”)" + title = f"{episode.pretty_title} | Transcript | CR Search" + if request.args.get("image", None) == "true": + return send_file(draw_image(episode.pretty_title, description, subtitle="Transcript"), mimetype="image/png") + + additional_html = render_template( + "header.html", + description=description, + title=title, + url=request.url + ) + return index_html.replace(placeholder_token, additional_html) + + +@ssr_routes.route("///") +@ssr_routes.route("//", defaults={'keyword': None}) +@ssr_routes.route("///", defaults={'keyword': None}) +@cache.cached(timeout=60 * 60 * 24 * 30, query_string=True) +def search(series_slug, episodes, keyword): + one_shot = Episode.select(Episode, Series).where( + Episode.series.slug == series_slug + ).join(Series).count() == 1 + try: + series = series_data_by_slug[series_slug] + except KeyError: + return abort(404) + if keyword: + if one_shot: + description = f"Search for “{keyword}” in {series.name}" + else: + description = f"Search for “{keyword}” up to episode {episodes} of {series.name}" + title = f"{keyword} | CR Search" + else: + description = f"Search through {series.name} of Critical Role" + title = f"{series.name} | CR Search" + if request.args.get("image", None) == "true": + return send_file(draw_image(title, description), mimetype="image/png") + additional_html = render_template( + "header.html", + description=description, + title=title, + url=request.url + ) + print(additional_html) + return index_html.replace(placeholder_token, additional_html) diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..669c501 --- /dev/null +++ b/templates/header.html @@ -0,0 +1,12 @@ +{{ title|default("CR Search") }} + + + + + + + + + + + diff --git a/web/src/assets/background_small.png b/web/src/assets/background_small.png new file mode 100644 index 0000000..17eb2f8 Binary files /dev/null and b/web/src/assets/background_small.png differ diff --git a/web/src/router.ts b/web/src/router.ts index 7570c84..ef833cc 100644 --- a/web/src/router.ts +++ b/web/src/router.ts @@ -11,7 +11,7 @@ export default new Router({ routes: [ { path: "/", - redirect: "/campaign2/10/", + redirect: "/campaign3/10/", name: "home" }, { diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index 65404fb..51b2901 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -42,7 +42,8 @@ - in + in + of diff --git a/web/vue.config.js b/web/vue.config.js new file mode 100644 index 0000000..0c1b6c9 --- /dev/null +++ b/web/vue.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('@vue/cli-service').ProjectOptions} + */ +module.exports = { + assetsDir: "assets/" +}