add podcast images
All checks were successful
ci/woodpecker/push/build Pipeline was successful

This commit is contained in:
Jake Walker 2025-01-13 13:45:52 +00:00
parent 66939bc9a3
commit bbfbeddc55
6 changed files with 117 additions and 18 deletions

View file

@ -23,6 +23,7 @@ class Podcast(BaseModel):
name: str name: str
description: str description: str
explicit: bool explicit: bool
image_filename: Optional[str] = Field(default=None)
episodes: List[Episode] = list() episodes: List[Episode] = list()

68
main.py
View file

@ -1,4 +1,5 @@
import urllib.parse import urllib.parse
import uuid
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from typing import Annotated, Optional 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.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from PIL import Image
import data import data
from process import AudioProcessor from process import AudioProcessor
@ -231,6 +233,7 @@ def admin_edit_feed_post(
feed_id: str, feed_id: str,
name: Annotated[str, Form()], name: Annotated[str, Form()],
description: Annotated[str, Form()], description: Annotated[str, Form()],
image: Optional[UploadFile] = None,
): ):
repo = data.load_repository() repo = data.load_repository()
@ -244,6 +247,34 @@ def admin_edit_feed_post(
new_feed.description = description 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 repo.podcasts[feed_id] = new_feed
data.save_repository(repo) data.save_repository(repo)
@ -264,6 +295,12 @@ def get_feed(request: Request, feed_id: str):
podcast = podgen.Podcast( podcast = podgen.Podcast(
name=feed.name, name=feed.name,
description=feed.description, 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), website=urllib.parse.urljoin(str(request.base_url), feed_id),
explicit=feed.explicit, explicit=feed.explicit,
feed_url=str(request.url), 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") return Response(content=podcast.rss_str(), media_type="application/xml")
@app.get("/{feed_id}/{episode_id}") @app.get("/{feed_id}/{filename}")
def get_episode(feed_id: str, episode_id: str): def get_episode_or_cover(feed_id: str, filename: str):
episode_id = episode_id.removesuffix(".m4a")
repo = data.load_repository() repo = data.load_repository()
feed = next( feed = next(
(podcast for id, podcast in repo.podcasts.items() if id == feed_id), None (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: if feed is None:
raise HTTPException(status_code=404, detail="Podcast not found") raise HTTPException(status_code=404, detail="Podcast not found")
episode = next( if filename.endswith(".m4a"):
(episode for episode in feed.episodes if episode.id == episode_id), None episode = next(
) (
episode
for episode in feed.episodes
if episode.id == filename.removesuffix(".m4a")
),
None,
)
if episode is None: if episode is None:
raise HTTPException(status_code=404, detail="Episode not found") 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")

View file

@ -9,6 +9,7 @@ dependencies = [
"fastapi[standard]>=0.115.6", "fastapi[standard]>=0.115.6",
"ffmpeg-normalize>=1.31.0", "ffmpeg-normalize>=1.31.0",
"ffmpeg-python>=0.2.0", "ffmpeg-python>=0.2.0",
"pillow>=11.1.0",
"podgen>=1.1.0", "podgen>=1.1.0",
"pydantic>=2.10.5", "pydantic>=2.10.5",
"pydantic-settings>=2.7.1", "pydantic-settings>=2.7.1",

View file

@ -1,14 +1,28 @@
{% extends 'layout.html.j2' %} {% extends 'layout.html.j2' %}
{% block content %} {% block content %}
{% if feed.image_filename %}
<img src="/{{ id }}/{{ feed.image_filename }}" width="256px" />
<br><br>
{% endif %}
<h1>{{ feed.name }}</h1> <h1>{{ feed.name }}</h1>
<a href="/admin/{{ id }}/edit">Edit</a>
<h2>Info</h2>
<p><b>Description:</b> {{ feed.description }}</p>
<p> <p>
Subscribe at: <b>Actions:</b>
<a href="/admin/{{ id }}/edit">Edit</a>
</p> </p>
<p>
<b>Description:</b>
{{ feed.description }}
</p>
<p>
<b>Subscribe:</b>
<pre><code>{{ feed_uri }}</code></pre> <pre><code>{{ feed_uri }}</code></pre>
<h2>Upload</h2> <small><i>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.</i></small>
</p>
<h2>Upload Episode</h2>
<div> <div>
<label for="fileInput">Choose file to upload</label> <label for="fileInput">Choose file to upload</label>
<input type="file" id="fileInput" name="fileInput" onchange="reset()"> <input type="file" id="fileInput" name="fileInput" onchange="reset()">

View file

@ -1,15 +1,21 @@
{% extends 'layout.html.j2' %} {% extends 'layout.html.j2' %}
{% block content %} {% block content %}
<h1>{{ feed.name }}</h1> <h1>{{ feed.name }}</h1>
<form method="post"> <form method="post" enctype="multipart/form-data">
<fieldset> <fieldset>
<label> <label>
Name Name
<input name="name" value="{{ feed.name }}" /> <input name="name" value="{{ feed.name }}" required />
</label> </label>
<label> <label>
Description Description
<textarea name="description">{{ feed.description }}</textarea> <textarea name="description" required>{{ feed.description }}</textarea>
</label>
<label>
Image
<input name="image" type="file" aria-describedby="image-help" />
<small id="image-help">This must be a square JPG or PNG in RGB format, and at least 1400x1400px in size and
3000x3000px at most.</small>
</label> </label>
</fieldset> </fieldset>

29
uv.lock
View file

@ -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 }, { 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]] [[package]]
name = "podcast-generator" name = "podcast-generator"
version = "0.1.0" version = "0.1.0"
@ -375,6 +402,7 @@ dependencies = [
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "ffmpeg-normalize" }, { name = "ffmpeg-normalize" },
{ name = "ffmpeg-python" }, { name = "ffmpeg-python" },
{ name = "pillow" },
{ name = "podgen" }, { name = "podgen" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
@ -388,6 +416,7 @@ requires-dist = [
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
{ name = "ffmpeg-normalize", specifier = ">=1.31.0" }, { name = "ffmpeg-normalize", specifier = ">=1.31.0" },
{ name = "ffmpeg-python", specifier = ">=0.2.0" }, { name = "ffmpeg-python", specifier = ">=0.2.0" },
{ name = "pillow", specifier = ">=11.1.0" },
{ name = "podgen", specifier = ">=1.1.0" }, { name = "podgen", specifier = ">=1.1.0" },
{ name = "pydantic", specifier = ">=2.10.5" }, { name = "pydantic", specifier = ">=2.10.5" },
{ name = "pydantic-settings", specifier = ">=2.7.1" }, { name = "pydantic-settings", specifier = ">=2.7.1" },