From bbfbeddc551c7e43b7732cb62cb3e006f699b3a9 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Mon, 13 Jan 2025 13:45:52 +0000 Subject: [PATCH] add podcast images --- data.py | 1 + main.py | 68 ++++++++++++++++++++++++++----- pyproject.toml | 1 + templates/admin_feed.html.j2 | 24 ++++++++--- templates/admin_feed_edit.html.j2 | 12 ++++-- uv.lock | 29 +++++++++++++ 6 files changed, 117 insertions(+), 18 deletions(-) diff --git a/data.py b/data.py index 012deff..80d04da 100644 --- a/data.py +++ b/data.py @@ -23,6 +23,7 @@ class Podcast(BaseModel): name: str description: str explicit: bool + image_filename: Optional[str] = Field(default=None) episodes: List[Episode] = list() diff --git a/main.py b/main.py index c5641a1..32aca1c 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,5 @@ import urllib.parse +import uuid from datetime import timedelta from pathlib import Path from typing import Annotated, Optional @@ -9,6 +10,7 @@ from fastapi import FastAPI, Form, HTTPException, Request, Response, UploadFile from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.templating import Jinja2Templates +from PIL import Image import data from process import AudioProcessor @@ -231,6 +233,7 @@ def admin_edit_feed_post( feed_id: str, name: Annotated[str, Form()], description: Annotated[str, Form()], + image: Optional[UploadFile] = None, ): repo = data.load_repository() @@ -244,6 +247,34 @@ def admin_edit_feed_post( new_feed.description = description + if image is not None and image.size > 0: + if not (image.filename.endswith(".jpg") or image.filename.endswith(".png")): + raise HTTPException( + status_code=400, + detail="The uploaded podcast image must be a jpg or png", + ) + + im = Image.open(image.file) + + if im.size[0] != im.size[1] or im.size[0] < 1400 or im.size[0] > 3000: + raise HTTPException( + status_code=400, + detail="The uploaded podcast image must be square and between 1400x1400px and 3000x3000px in size", + ) + + if im.mode != "RGB": + raise HTTPException( + status_code=400, + detail="The uploaded podcast image must be in RGB format", + ) + + if new_feed.image_filename is not None: + (settings.directory / new_feed.image_filename).unlink(missing_ok=True) + + filename = f"img_{uuid.uuid4()}" + Path(image.filename).suffix + im.save(settings.directory / filename) + new_feed.image_filename = filename + repo.podcasts[feed_id] = new_feed data.save_repository(repo) @@ -264,6 +295,12 @@ def get_feed(request: Request, feed_id: str): podcast = podgen.Podcast( name=feed.name, description=feed.description, + image=urllib.parse.urljoin( + str(request.base_url), + f"/{feed_id}/{feed.image_filename}", + ) + if feed.image_filename is not None + else None, website=urllib.parse.urljoin(str(request.base_url), feed_id), explicit=feed.explicit, feed_url=str(request.url), @@ -291,10 +328,8 @@ def get_feed(request: Request, feed_id: str): return Response(content=podcast.rss_str(), media_type="application/xml") -@app.get("/{feed_id}/{episode_id}") -def get_episode(feed_id: str, episode_id: str): - episode_id = episode_id.removesuffix(".m4a") - +@app.get("/{feed_id}/{filename}") +def get_episode_or_cover(feed_id: str, filename: str): repo = data.load_repository() feed = next( (podcast for id, podcast in repo.podcasts.items() if id == feed_id), None @@ -303,11 +338,24 @@ def get_episode(feed_id: str, episode_id: str): if feed is None: raise HTTPException(status_code=404, detail="Podcast not found") - episode = next( - (episode for episode in feed.episodes if episode.id == episode_id), None - ) + if filename.endswith(".m4a"): + episode = next( + ( + episode + for episode in feed.episodes + if episode.id == filename.removesuffix(".m4a") + ), + None, + ) - if episode is None: - raise HTTPException(status_code=404, detail="Episode not found") + if episode is None: + raise HTTPException(status_code=404, detail="Episode not found") - return FileResponse(settings.directory / f"{episode_id}.m4a") + return FileResponse(settings.directory / f"{episode.id}.m4a") + + elif ( + filename.endswith(".jpg") or filename.endswith(".png") + ) and filename == feed.image_filename: + return FileResponse(settings.directory / feed.image_filename) + + return HTTPException(status_code=404, detail="File not found") diff --git a/pyproject.toml b/pyproject.toml index 51741cd..9f2e451 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ dependencies = [ "fastapi[standard]>=0.115.6", "ffmpeg-normalize>=1.31.0", "ffmpeg-python>=0.2.0", + "pillow>=11.1.0", "podgen>=1.1.0", "pydantic>=2.10.5", "pydantic-settings>=2.7.1", diff --git a/templates/admin_feed.html.j2 b/templates/admin_feed.html.j2 index 88f0c3f..14e8528 100644 --- a/templates/admin_feed.html.j2 +++ b/templates/admin_feed.html.j2 @@ -1,14 +1,28 @@ {% extends 'layout.html.j2' %} {% block content %} +{% if feed.image_filename %} + +

+{% endif %}

{{ feed.name }}

-Edit -

Info

-

Description: {{ feed.description }}

- Subscribe at: + Actions: + Edit

+

+ Description: + {{ feed.description }} +

+

+ Subscribe:

{{ feed_uri }}
-

Upload

+For Apple Podcasts, open the app and click on the Library tab along the bottom. Select the ellipsis in the top + right and hit "Follow a Show by URL...". For other apps, look for an option allowing you to add a show by + URL. +

+ + +

Upload Episode

diff --git a/templates/admin_feed_edit.html.j2 b/templates/admin_feed_edit.html.j2 index a156721..45355b8 100644 --- a/templates/admin_feed_edit.html.j2 +++ b/templates/admin_feed_edit.html.j2 @@ -1,15 +1,21 @@ {% extends 'layout.html.j2' %} {% block content %}

{{ feed.name }}

-
+
+
diff --git a/uv.lock b/uv.lock index 9bffe09..f436d4a 100644 --- a/uv.lock +++ b/uv.lock @@ -366,6 +366,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] +[[package]] +name = "pillow" +version = "11.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, + { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, + { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, + { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, + { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, + { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, + { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, + { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, + { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, + { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, + { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, + { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, + { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, + { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, + { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, + { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, + { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, + { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, +] + [[package]] name = "podcast-generator" version = "0.1.0" @@ -375,6 +402,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "ffmpeg-normalize" }, { name = "ffmpeg-python" }, + { name = "pillow" }, { name = "podgen" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -388,6 +416,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, { name = "ffmpeg-normalize", specifier = ">=1.31.0" }, { name = "ffmpeg-python", specifier = ">=0.2.0" }, + { name = "pillow", specifier = ">=11.1.0" }, { name = "podgen", specifier = ">=1.1.0" }, { name = "pydantic", specifier = ">=2.10.5" }, { name = "pydantic-settings", specifier = ">=2.7.1" },