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",
|
"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
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 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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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
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 },
|
{ 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" },
|
||||||
|
|
Loading…
Reference in a new issue