This commit is contained in:
parent
66939bc9a3
commit
bbfbeddc55
6 changed files with 117 additions and 18 deletions
1
data.py
1
data.py
|
@ -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
68
main.py
|
@ -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")
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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()">
|
||||||
|
|
|
@ -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
29
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 },
|
{ 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" },
|
||||||
|
|
Loading…
Reference in a new issue