Compare commits

...

2 commits

Author SHA1 Message Date
2d424d0be6 add markdown descriptions
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2025-01-17 16:51:51 +00:00
dc88dc49e8 add scripts 2025-01-17 16:48:51 +00:00
7 changed files with 163 additions and 5 deletions

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",
"markdown>=3.7",
"nanoid>=2.0.0", "nanoid>=2.0.0",
"pillow>=11.1.0", "pillow>=11.1.0",
"podgen>=1.1.0", "podgen>=1.1.0",

90
scripts/import-episode.py Normal file
View file

@ -0,0 +1,90 @@
import argparse
import hashlib
import shutil
import sys
from pathlib import Path
import ffmpeg
import structlog
from sqlmodel import Session, select
# Add the src directory to the system path
sys.path.append(str(Path(__file__).resolve().parent.parent / "src"))
import models as models
from settings import settings
log = structlog.get_logger()
def import_episode(filename: Path, podcast_id: str, process: bool, move: bool = True):
if process:
raise NotImplementedError("Importing with processing is not implemented")
if filename.suffix != ".m4a" and not process:
log.error("Input file must be in an m4a container if not processing")
return
with Session(models.engine) as session:
podcast = session.exec(
select(models.Podcast).where(models.Podcast.id == podcast_id)
).first()
if podcast is None:
log.error("Failed importing episode, podcast does not exist.")
return
episode = models.PodcastEpisode(
name=filename.stem, file_size=0, file_hash="", podcast_id=podcast.id
)
episode_filename = settings.directory / f"{episode.id}.m4a"
if move:
log.info("Moving episode to %s...", episode_filename)
shutil.move(filename, episode_filename)
else:
log.info("Copying episode to %s...", episode_filename)
shutil.copyfile(filename, episode_filename)
probe = ffmpeg.probe(str(episode_filename))
stream = next(
(stream for stream in probe["streams"] if stream["codec_type"] == "audio"),
None,
)
file_hash = hashlib.sha256()
with open(episode_filename, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
file_hash.update(byte_block)
episode.duration = (
float(stream["duration"])
if stream is not None and "duration" in stream
else None
)
episode.file_hash = file_hash.hexdigest()
episode.file_size = episode_filename.stat().st_size
session.add(episode)
session.commit()
log.info("Imported episode as %s", episode.id)
def main():
parser = argparse.ArgumentParser(
prog="import-episode.py",
description="Import an episode",
)
parser.add_argument("filename")
parser.add_argument("podcast_id")
parser.add_argument("--process", action="store_true")
args = parser.parse_args()
import_episode(Path(args.filename), args.podcast_id, args.process)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,50 @@
import argparse
import sys
from datetime import datetime
from pathlib import Path
import structlog
from sqlmodel import Session, select
# Add the src directory to the system path
sys.path.append(str(Path(__file__).resolve().parent.parent / "src"))
import models as models
log = structlog.get_logger()
def update_pub_date(episode_id: str, new_date: str):
with Session(models.engine) as session:
episode = session.exec(
select(models.PodcastEpisode).where(models.PodcastEpisode.id == episode_id)
).first()
if episode is None:
log.error("Could not find episode")
return
episode.publish_date = datetime.fromisoformat(new_date)
assert episode.publish_date.tzinfo is not None, "timezone is required"
session.add(episode)
session.commit()
log.info("Updated episode", episode.id)
def main():
parser = argparse.ArgumentParser(
prog="update-pub-date.py",
description="Update an episode publish date",
)
parser.add_argument("episode_id")
parser.add_argument("new_date")
args = parser.parse_args()
update_pub_date(args.episode_id, args.new_date)
if __name__ == "__main__":
main()

View file

@ -1,10 +1,11 @@
import urllib.parse import urllib.parse
import uuid import uuid
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from datetime import timedelta from datetime import timedelta, timezone
from pathlib import Path from pathlib import Path
from typing import Annotated, Any, Generator, Optional from typing import Annotated, Any, Generator, Optional
import markdown
import podgen import podgen
import structlog import structlog
from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response, UploadFile from fastapi import Depends, FastAPI, Form, HTTPException, Request, Response, UploadFile
@ -532,7 +533,7 @@ def get_podcast_feed(session: SessionDep, request: Request, podcast_id: str):
podgen.Episode( podgen.Episode(
id=episode.id, id=episode.id,
title=episode.name, title=episode.name,
publication_date=episode.publish_date, publication_date=episode.publish_date.astimezone(tz=timezone.utc),
media=podgen.Media( media=podgen.Media(
urllib.parse.urljoin( urllib.parse.urljoin(
str(request.base_url), f"{podcast.id}/{episode.id}.m4a" str(request.base_url), f"{podcast.id}/{episode.id}.m4a"
@ -542,7 +543,9 @@ def get_podcast_feed(session: SessionDep, request: Request, podcast_id: str):
if episode.duration is not None if episode.duration is not None
else None, else None,
), ),
long_summary=episode.description, long_summary=markdown.markdown(episode.description)
if episode.description is not None
else None,
) )
) )

View file

@ -24,7 +24,7 @@ class PodcastEpisode(SQLModel, table=True):
id: str = Field(primary_key=True, default_factory=lambda: nanoid.generate()) id: str = Field(primary_key=True, default_factory=lambda: nanoid.generate())
name: str name: str
duration: Optional[float] = Field(default=None) duration: Optional[float] = Field(default=None)
description: Optional[float] = Field(default=None) description: Optional[str] = Field(default=None)
file_hash: str file_hash: str
file_size: int file_size: int
publish_date: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) publish_date: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

View file

@ -10,7 +10,10 @@
</label> </label>
<label> <label>
Description Description
<textarea name="description">{% if episode.description %}{{ episode.description }}{% endif %}</textarea> <textarea name="description"
aria-describedby="description-help">{% if episode.description %}{{ episode.description }}{% endif %}</textarea>
<small id="description-help"><a href="https://www.markdownguide.org/cheat-sheet/">Markdown</a> is supported
for any content in here.</small>
</label> </label>
</fieldset> </fieldset>

11
uv.lock
View file

@ -341,6 +341,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 }, { url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 },
] ]
[[package]]
name = "markdown"
version = "3.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/28/3af612670f82f4c056911fbbbb42760255801b3068c48de792d354ff4472/markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", size = 357086 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803", size = 106349 },
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "3.0.0" version = "3.0.0"
@ -435,6 +444,7 @@ dependencies = [
{ name = "fastapi", extra = ["standard"] }, { name = "fastapi", extra = ["standard"] },
{ name = "ffmpeg-normalize" }, { name = "ffmpeg-normalize" },
{ name = "ffmpeg-python" }, { name = "ffmpeg-python" },
{ name = "markdown" },
{ name = "nanoid" }, { name = "nanoid" },
{ name = "pillow" }, { name = "pillow" },
{ name = "podgen" }, { name = "podgen" },
@ -451,6 +461,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 = "markdown", specifier = ">=3.7" },
{ name = "nanoid", specifier = ">=2.0.0" }, { name = "nanoid", specifier = ">=2.0.0" },
{ name = "pillow", specifier = ">=11.1.0" }, { name = "pillow", specifier = ">=11.1.0" },
{ name = "podgen", specifier = ">=1.1.0" }, { name = "podgen", specifier = ">=1.1.0" },