Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/bot/handlers/wordpress_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ async def upload_WP(callback: CallbackQuery, language: str, username: str) -> No
tags=info["tags"],
slug=info["slug"],
duration=info["duration"],
recording_date=info.get("recording_date"),
type_episode=type_episode,
)

Expand Down
2 changes: 2 additions & 0 deletions app/bot/locales/en.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ bot_commands = [["Turn off the bot", "shutdown_bot"], ["Send log files", "send_l
ask_template = {
.main = |
<pre language="text">Number: 600
Recording Date: 26.07.2026
Title: Episode title
Comment: Episode description
Tags: Tag1, Tag2, Tag3
Expand All @@ -35,6 +36,7 @@ ask_template = {
02:17:25 - Patrons and aftershow announcement</pre>
.aftershow = |
<pre language="text">Number: 600
Recording Date: 26.07.2026
Title: Aftershow. Episode title
Comment: Episode description</pre>
}
2 changes: 2 additions & 0 deletions app/bot/locales/ru.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ bot_commands = [["Выключить бота", "shutdown_bot"], ["Присла
ask_template = {
.main = |
<pre language="text">Number: 600
Recording Date: 26.07.2026
Title: Название эпизода
Comment: Описание эпизода
Tags: Окно, жесть, спина
Expand All @@ -35,6 +36,7 @@ ask_template = {
02:17:25 - Озвучили наших патронов и анонсировали послешоу</pre>
.aftershow = |
<pre language="text">Number: 600
Recording Date: 26.07.2026
Title: Послешоу. Название эпизода
Comment: Описание эпизода</pre>
}
27 changes: 27 additions & 0 deletions app/bot/utils/validators.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
import re
from datetime import datetime
from pathlib import Path

from loguru import logger

# Дата записи в шаблоне: принимаем DD.MM.YYYY (также /, - как разделители),
# нормализуем в ISO YYYY-MM-DD — именно этот формат ждёт Podlove.
_RECORDING_DATE_RE = re.compile(r"^[ \t]*Recording Date:[ \t]*(.+?)[ \t]*$\n?", re.MULTILINE)


def _parse_recording_date(raw: str) -> str | None:
"""DD.MM.YYYY (./-/пробел как разделитель) -> ISO YYYY-MM-DD, иначе None."""
for fmt in ("%d.%m.%Y", "%d/%m/%Y", "%d-%m-%Y", "%Y-%m-%d"):
try:
return datetime.strptime(raw.strip(), fmt).strftime("%Y-%m-%d")
except ValueError:
continue
logger.warning(f"Unrecognized recording date {raw!r}; ignoring")
return None


@logger.catch
def validate_template(text: str) -> dict[str, str] | None:
Expand All @@ -21,6 +37,14 @@ def validate_template(text: str) -> dict[str, str] | None:
>>> validate_template(template)
{'number': '1', 'title': '1. Example of a header', 'comment': 'Example of a comment'}
"""
# Дату записи парсим отдельно и вырезаем строку до основного regex —
# так поле остаётся опциональным и не усложняет и без того капризный шаблон.
recording_date = None
date_match = _RECORDING_DATE_RE.search(text)
if date_match:
recording_date = _parse_recording_date(date_match.group(1))
text = text[: date_match.start()] + text[date_match.end() :]

headers = ["number", "title", "comment"]
if "chapters" in text.lower():
reg = r"(?:<pre.*?>)?Number: (\d+)\nTitle: (.*?)\nComment: (.*?)\nTags: (.*?)\nChapters: \|\n((?:(?!<\/pre>).)*)(?:<\/pre>)?$"
Expand All @@ -45,6 +69,9 @@ def validate_template(text: str) -> dict[str, str] | None:
if "tags" in res:
res["tags"] = list({tag.strip() for tag in re.split(r",\s*|,\s*|\s*,\s*", res["tags"])})

if recording_date:
res["recording_date"] = recording_date

return res


Expand Down
1 change: 1 addition & 0 deletions app/publishers/WordPress/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ async def publish(self, event: WordPressEvent) -> None: # type: ignore[override
"tags": event.tags,
"slug": event.slug,
"duration": event.duration,
"recording_date": event.recording_date,
}

# WordPress.upload_post() — синхронный (requests-based). Запускаем
Expand Down
28 changes: 18 additions & 10 deletions app/publishers/WordPress/wordpress.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,13 +292,19 @@ def _format_duration(seconds) -> str:

def _update_podlove_episode(self, episode_id: int, info: dict) -> None:
payload = {
"title": f"Разговорный жанр — {info['number']}",
"title": info["title"],
"summary": info["comment"],
"number": int(info["number"]) if str(info["number"]).isdigit() else info["number"],
"slug": info["slug"],
"duration": self._format_duration(info["duration"]),
"type": "full",
}
# Дата записи задаётся при оформлении (info["recording_date"], ISO
# YYYY-MM-DD). Если её нет — не шлём ключ, чтобы Podlove не затирал
# значение пустотой.
recording_date = info.get("recording_date")
if recording_date:
payload["recording_date"] = recording_date
self._rest_request("POST", f"/podlove/v2/episodes/{episode_id}", json_body=payload)
logger.debug(f"Podlove episode {episode_id} metadata updated")

Expand Down Expand Up @@ -356,7 +362,15 @@ def upload_post(self, info: dict) -> bool:
for time_str, chapterName in info["chapters"]:
chapters += f"[skipto time={time_str}]{time_str}[/skipto] — {chapterName}\n"

# Дата записи: если задана при оформлении (ISO YYYY-MM-DD) — берём её,
# иначе фолбэк на текущую дату (как было раньше).
recording_iso = info.get("recording_date")
time = datetime.now(self._timezone)
if recording_iso:
try:
time = datetime.strptime(recording_iso, "%Y-%m-%d")
except ValueError as e:
logger.warning(f"Invalid recording_date {recording_iso!r} ({e}); falling back to today")

months = (
"января",
Expand All @@ -376,25 +390,19 @@ def upload_post(self, info: dict) -> bool:

form = {
"post_title": f"Разговорный жанр — {podcastID}",
"content": f"""<span style="font-size: large;">{name.replace(podcastID + ". ", "")}</span><code>
</code>
"content": f"""<span style="font-size: large;">{name.replace(podcastID + ". ", "")}</span>
<b><i>Описание:</i></b>
<code>{summary}
</code>
{summary}
<!--more--><b><i>Таймлайн:</i></b>
{chapters}
<code>
</code>Всё это вы услышите в {podcastID}-м эпизоде подкаста «Разговорный жанр».
Всё это вы услышите в {podcastID}-м эпизоде подкаста «Разговорный жанр».
[podlove-template template="subscriptions"]
<span style="font-size: small;">Дата записи: {timeStr}</span>""",
"post_name": f"Разговорный жанр — {podcastID}",
"post_category[]": "3",
"newcategory": "Название новой рубрики",
"newcategory_parent": "-1",
"trackback_url": "",
"metakeyselect": "big_post",
"metakeyinput": "",
"metavalue": "1",
"comment_status": "open",
"ping_status": "open",
"post_author_override": "361",
Expand Down
1 change: 1 addition & 0 deletions app/shared/kafka/models/wordpress_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class WordPressEvent(BaseModel):
tags: list[str] = Field(default_factory=list)
slug: str = Field(..., description="Slug файла")
duration: int | None = Field(None, description="Длительность в секундах")
recording_date: str | None = Field(None, description="Дата записи (ISO YYYY-MM-DD)")

type_episode: str | None = Field(
None, description="main | aftershow — определяет paywall для платных publisher'ов"
Expand Down
1 change: 1 addition & 0 deletions app/shared/kafka/schemas/wordpress_event.avsc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
{"name": "tags", "type": {"type": "array", "items": "string"}},
{"name": "slug", "type": "string", "doc": "Slug файла (имя без .mp3)"},
{"name": "duration", "type": ["null", "long"], "default": null, "doc": "Длительность в секундах"},
{"name": "recording_date", "type": ["null", "string"], "default": null, "doc": "Дата записи (ISO YYYY-MM-DD)"},
{"name": "type_episode", "type": ["null", "string"], "default": null, "doc": "main | aftershow — определяет paywall для платных publisher'ов"}
]
}
39 changes: 39 additions & 0 deletions tests/unit/publishers/wordpress/test_wordpress_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,45 @@ def _request(method, url, **kwargs):
mock_session.request.assert_called()
wp._rest_session.request.assert_called()

def test_upload_post_form_and_rest_payload(self, mock_session, sample_post_info):
"""Регресс на баги черновика: Title = имя эпизода, без <code> и big_post,
recording_date уходит в Podlove и в тело поста."""
sample_post_info["recording_date"] = "2026-06-05"

get_resp = MagicMock(status_code=200, ok=True, content=_FORM_PAGE, text=_FORM_PAGE.decode())
post_resp = MagicMock(status_code=302, ok=True, text="")
captured = {}

def _request(method, url, **kwargs):
if method == "POST" and url.endswith("/wp-admin/post.php"):
captured["form"] = kwargs["data"]
return post_resp
return get_resp

mock_session.request.side_effect = _request

wp = _make_wp(mock_session)
with patch.object(wp, "_dump_cookies", return_value=True):
assert wp.upload_post(sample_post_info) is True

content = captured["form"]["content"]
# #3 — никаких <code> в теле
assert "<code>" not in content
# описание присутствует как обычный текст
assert sample_post_info["comment"] in content
# #2 — дата записи из info, а не сегодняшняя
assert "Дата записи: 5 июня 2026" in content
# #4 — big_post убран
assert "metakeyselect" not in captured["form"]
assert "metavalue" not in captured["form"]

# #1 + #2 — REST-обновление Podlove: title = имя эпизода, recording_date проброшен
episode_calls = [c for c in wp._rest_session.request.call_args_list if "/podlove/v2/episodes/" in c.args[1]]
assert episode_calls, "Podlove episode update not called"
payload = episode_calls[0].kwargs["json"]
assert payload["title"] == sample_post_info["title"]
assert payload["recording_date"] == "2026-06-05"

def test_upload_post_no_form_retries_login(self, mock_session, sample_post_info):
empty_page = b"<html><body>No form here</body></html>"
get_calls = {"n": 0}
Expand Down
50 changes: 49 additions & 1 deletion tests/unit/utils/validators/test_validate_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@
],
)
def test_valid_template(template):
expected_output = {"number": "1", "title": "1. Example header", "comment": "Example comment"}
expected_output = {
"number": "1",
"title": "1. Example header",
"comment": "Example comment",
}
assert validate_template(template) == expected_output


Expand Down Expand Up @@ -39,6 +43,50 @@ def test_template_with_chapters_and_tags(template):
assert result["chapters"] == expected_output["chapters"]


@pytest.mark.parametrize(
("template", "expected_date"),
[
# DD.MM.YYYY -> ISO
(
"Number: 5\nRecording Date: 05.06.2026\nTitle: Header\nComment: Comment",
"2026-06-05",
),
# разделитель "/" тоже принимаем
(
"Number: 5\nRecording Date: 05/06/2026\nTitle: Header\nComment: Comment",
"2026-06-05",
),
# с тегами/главами дата всё ещё вырезается корректно
(
"Number: 5\nRecording Date: 05.06.2026\nTitle: Header\nComment: Comment"
"\nTags: t1, t2\nChapters: |\n00:00:00 - Intro",
"2026-06-05",
),
],
)
def test_template_with_recording_date(template, expected_date):
result = validate_template(template)
assert result is not None
assert result["recording_date"] == expected_date
# дата вырезана — Title/Comment распарсились штатно
assert result["title"] == "5. Header"
assert result["comment"] == "Comment"


def test_template_without_recording_date_has_no_key():
result = validate_template("Number: 1\nTitle: Header\nComment: Comment")
assert result is not None
assert "recording_date" not in result


def test_template_with_unparseable_recording_date_is_ignored():
# Некорректная дата не роняет шаблон — просто отбрасывается
result = validate_template("Number: 1\nRecording Date: не дата\nTitle: Header\nComment: Comment")
assert result is not None
assert "recording_date" not in result
assert result["title"] == "1. Header"


@pytest.mark.parametrize(
"template",
[
Expand Down
Loading