diff --git a/app/bot/handlers/wordpress_handler.py b/app/bot/handlers/wordpress_handler.py index af00edf..292b0ec 100644 --- a/app/bot/handlers/wordpress_handler.py +++ b/app/bot/handlers/wordpress_handler.py @@ -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, ) diff --git a/app/bot/locales/en.ftl b/app/bot/locales/en.ftl index 884355d..c0f6cd5 100644 --- a/app/bot/locales/en.ftl +++ b/app/bot/locales/en.ftl @@ -25,6 +25,7 @@ bot_commands = [["Turn off the bot", "shutdown_bot"], ["Send log files", "send_l ask_template = { .main = |
Number: 600
+ Recording Date: 26.07.2026
Title: Episode title
Comment: Episode description
Tags: Tag1, Tag2, Tag3
@@ -35,6 +36,7 @@ ask_template = {
02:17:25 - Patrons and aftershow announcement
.aftershow = |
Number: 600
+ Recording Date: 26.07.2026
Title: Aftershow. Episode title
Comment: Episode description
}
diff --git a/app/bot/locales/ru.ftl b/app/bot/locales/ru.ftl
index 39615ad..6be2e56 100644
--- a/app/bot/locales/ru.ftl
+++ b/app/bot/locales/ru.ftl
@@ -25,6 +25,7 @@ bot_commands = [["Выключить бота", "shutdown_bot"], ["Присла
ask_template = {
.main = |
Number: 600
+ Recording Date: 26.07.2026
Title: Название эпизода
Comment: Описание эпизода
Tags: Окно, жесть, спина
@@ -35,6 +36,7 @@ ask_template = {
02:17:25 - Озвучили наших патронов и анонсировали послешоу
.aftershow = |
Number: 600
+ Recording Date: 26.07.2026
Title: Послешоу. Название эпизода
Comment: Описание эпизода
}
diff --git a/app/bot/utils/validators.py b/app/bot/utils/validators.py
index 966f0a3..bc64069 100644
--- a/app/bot/utils/validators.py
+++ b/app/bot/utils/validators.py
@@ -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:
@@ -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"(?:
-
+ "content": f"""{name.replace(podcastID + ". ", "")}
Описание:
-{summary}
-
+{summary}
Таймлайн:
{chapters}
-
-Всё это вы услышите в {podcastID}-м эпизоде подкаста «Разговорный жанр».
+Всё это вы услышите в {podcastID}-м эпизоде подкаста «Разговорный жанр».
[podlove-template template="subscriptions"]
Дата записи: {timeStr}""",
"post_name": f"Разговорный жанр — {podcastID}",
@@ -392,9 +403,6 @@ def upload_post(self, info: dict) -> bool:
"newcategory": "Название новой рубрики",
"newcategory_parent": "-1",
"trackback_url": "",
- "metakeyselect": "big_post",
- "metakeyinput": "",
- "metavalue": "1",
"comment_status": "open",
"ping_status": "open",
"post_author_override": "361",
diff --git a/app/shared/kafka/models/wordpress_event.py b/app/shared/kafka/models/wordpress_event.py
index 55de0c5..496996b 100644
--- a/app/shared/kafka/models/wordpress_event.py
+++ b/app/shared/kafka/models/wordpress_event.py
@@ -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'ов"
diff --git a/app/shared/kafka/schemas/wordpress_event.avsc b/app/shared/kafka/schemas/wordpress_event.avsc
index 768f2be..387e579 100644
--- a/app/shared/kafka/schemas/wordpress_event.avsc
+++ b/app/shared/kafka/schemas/wordpress_event.avsc
@@ -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'ов"}
]
}
diff --git a/tests/unit/publishers/wordpress/test_wordpress_client.py b/tests/unit/publishers/wordpress/test_wordpress_client.py
index d400e83..574aacd 100644
--- a/tests/unit/publishers/wordpress/test_wordpress_client.py
+++ b/tests/unit/publishers/wordpress/test_wordpress_client.py
@@ -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 = имя эпизода, без и 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 — никаких в теле
+ assert "" 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"No form here"
get_calls = {"n": 0}
diff --git a/tests/unit/utils/validators/test_validate_template.py b/tests/unit/utils/validators/test_validate_template.py
index 59d450c..3b785d9 100644
--- a/tests/unit/utils/validators/test_validate_template.py
+++ b/tests/unit/utils/validators/test_validate_template.py
@@ -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
@@ -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",
[