Compare commits
2 commits
4e89158d96
...
2d424d0be6
Author | SHA1 | Date | |
---|---|---|---|
2d424d0be6 | |||
dc88dc49e8 |
7 changed files with 163 additions and 5 deletions
|
@ -9,6 +9,7 @@ dependencies = [
|
|||
"fastapi[standard]>=0.115.6",
|
||||
"ffmpeg-normalize>=1.31.0",
|
||||
"ffmpeg-python>=0.2.0",
|
||||
"markdown>=3.7",
|
||||
"nanoid>=2.0.0",
|
||||
"pillow>=11.1.0",
|
||||
"podgen>=1.1.0",
|
||||
|
|
90
scripts/import-episode.py
Normal file
90
scripts/import-episode.py
Normal 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()
|
50
scripts/update-pub-date.py
Normal file
50
scripts/update-pub-date.py
Normal 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()
|
|
@ -1,10 +1,11 @@
|
|||
import urllib.parse
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Generator, Optional
|
||||
|
||||
import markdown
|
||||
import podgen
|
||||
import structlog
|
||||
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(
|
||||
id=episode.id,
|
||||
title=episode.name,
|
||||
publication_date=episode.publish_date,
|
||||
publication_date=episode.publish_date.astimezone(tz=timezone.utc),
|
||||
media=podgen.Media(
|
||||
urllib.parse.urljoin(
|
||||
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
|
||||
else None,
|
||||
),
|
||||
long_summary=episode.description,
|
||||
long_summary=markdown.markdown(episode.description)
|
||||
if episode.description is not None
|
||||
else None,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ class PodcastEpisode(SQLModel, table=True):
|
|||
id: str = Field(primary_key=True, default_factory=lambda: nanoid.generate())
|
||||
name: str
|
||||
duration: Optional[float] = Field(default=None)
|
||||
description: Optional[float] = Field(default=None)
|
||||
description: Optional[str] = Field(default=None)
|
||||
file_hash: str
|
||||
file_size: int
|
||||
publish_date: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
</label>
|
||||
<label>
|
||||
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>
|
||||
</fieldset>
|
||||
|
||||
|
|
11
uv.lock
11
uv.lock
|
@ -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 },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "markdown-it-py"
|
||||
version = "3.0.0"
|
||||
|
@ -435,6 +444,7 @@ dependencies = [
|
|||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "ffmpeg-normalize" },
|
||||
{ name = "ffmpeg-python" },
|
||||
{ name = "markdown" },
|
||||
{ name = "nanoid" },
|
||||
{ name = "pillow" },
|
||||
{ name = "podgen" },
|
||||
|
@ -451,6 +461,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 = "markdown", specifier = ">=3.7" },
|
||||
{ name = "nanoid", specifier = ">=2.0.0" },
|
||||
{ name = "pillow", specifier = ">=11.1.0" },
|
||||
{ name = "podgen", specifier = ">=1.1.0" },
|
||||
|
|
Loading…
Reference in a new issue