Only show screenshot things if Playwright has chromium installed; use names instead of ids when ignoring Ruff errors
All checks were successful
Test and build Docker image / docker (push) Successful in 1m54s
All checks were successful
Test and build Docker image / docker (push) Successful in 1m54s
This commit is contained in:
parent
59d9062ded
commit
d61e8ccf10
15 changed files with 224 additions and 174 deletions
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
import concurrent.futures
|
||||
import datetime
|
||||
import functools
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
|
|
@ -30,6 +31,7 @@ from httpx2 import HTTPError
|
|||
from httpx2 import Response
|
||||
from markdownify import markdownify
|
||||
from playwright.sync_api import Browser
|
||||
from playwright.sync_api import Error as PlaywrightError
|
||||
from playwright.sync_api import Page
|
||||
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
|
@ -140,7 +142,7 @@ MESSAGE_PAYLOAD_KEYS: tuple[str, ...] = (
|
|||
)
|
||||
|
||||
|
||||
def extract_domain(url: str) -> str: # noqa: PLR0911
|
||||
def extract_domain(url: str) -> str: # ruff:ignore[too-many-return-statements]
|
||||
"""Extract the domain name from a URL.
|
||||
|
||||
Args:
|
||||
|
|
@ -153,7 +155,7 @@ def extract_domain(url: str) -> str: # noqa: PLR0911
|
|||
if not url:
|
||||
return "Other"
|
||||
|
||||
try: # noqa: PLW0717
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
# Special handling for YouTube feeds
|
||||
if "youtube.com/feeds/videos.xml" in url:
|
||||
return "YouTube"
|
||||
|
|
@ -258,7 +260,7 @@ def extract_steam_app_id_from_url(url: str) -> str | None:
|
|||
normalized_netloc: str = parsed_url.netloc.lower().removeprefix("www.")
|
||||
path_segments: list[str] = [segment for segment in parsed_url.path.split("/") if segment]
|
||||
return _extract_steam_app_id_from_path(normalized_netloc, path_segments) or _extract_steam_app_id_from_query(
|
||||
parsed_url
|
||||
parsed_url,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -453,7 +455,7 @@ def get_feed_webhook_text_length_limit(reader: Reader, feed: Feed | str) -> int:
|
|||
return coerce_webhook_text_length_limit(value)
|
||||
|
||||
|
||||
def coerce_media_gallery_image_limit(value: JsonValue) -> int: # noqa: PLR0911
|
||||
def coerce_media_gallery_image_limit(value: JsonValue) -> int: # ruff:ignore[too-many-return-statements]
|
||||
"""Return the supported media gallery image limit for a stored tag value."""
|
||||
if isinstance(value, bool):
|
||||
return 1
|
||||
|
|
@ -835,7 +837,7 @@ def get_webhook_query_params(
|
|||
return clean_webhook_url, params
|
||||
|
||||
|
||||
def get_webhook_files(webhook: DiscordWebhook) -> list[WebhookFile]: # noqa: C901
|
||||
def get_webhook_files(webhook: DiscordWebhook) -> list[WebhookFile]: # ruff:ignore[complex-structure]
|
||||
"""Return files attached to a webhook object in a normalized shape."""
|
||||
raw_files = getattr(webhook, "files", None)
|
||||
files: list[WebhookFile] = []
|
||||
|
|
@ -854,7 +856,7 @@ def get_webhook_files(webhook: DiscordWebhook) -> list[WebhookFile]: # noqa: C9
|
|||
files.append(file_value)
|
||||
continue
|
||||
|
||||
if not isinstance(file_value, tuple) or len(file_value) < 2: # noqa: PLR2004
|
||||
if not isinstance(file_value, tuple) or len(file_value) < 2: # ruff:ignore[magic-value-comparison]
|
||||
continue
|
||||
|
||||
first, second = file_value[0], file_value[1]
|
||||
|
|
@ -862,7 +864,7 @@ def get_webhook_files(webhook: DiscordWebhook) -> list[WebhookFile]: # noqa: C9
|
|||
files.append(WebhookFile(filename=first, content=second))
|
||||
continue
|
||||
|
||||
if isinstance(second, tuple) and len(second) >= 2: # noqa: PLR2004
|
||||
if isinstance(second, tuple) and len(second) >= 2: # ruff:ignore[magic-value-comparison]
|
||||
nested_file = cast("tuple[object, ...]", second)
|
||||
nested_filename, nested_content = nested_file[0], nested_file[1]
|
||||
if isinstance(nested_filename, str) and isinstance(nested_content, bytes):
|
||||
|
|
@ -916,7 +918,7 @@ def request_discord_webhook(
|
|||
request_kwargs["json"] = payload
|
||||
|
||||
response: Response = httpx2.request(method, url, **request_kwargs)
|
||||
if not rate_limit_retry or response.status_code != 429: # noqa: PLR2004
|
||||
if not rate_limit_retry or response.status_code != 429: # ruff:ignore[magic-value-comparison]
|
||||
return response
|
||||
|
||||
retry_after: float | None = get_retry_after_seconds(response)
|
||||
|
|
@ -1183,7 +1185,7 @@ def update_sent_webhook_record_for_entry(
|
|||
)
|
||||
|
||||
|
||||
def update_sent_webhooks_for_modified_entries( # noqa: C901
|
||||
def update_sent_webhooks_for_modified_entries( # ruff:ignore[complex-structure]
|
||||
reader: Reader,
|
||||
modified_entries: Iterable[tuple[str, str]],
|
||||
) -> int:
|
||||
|
|
@ -1253,7 +1255,7 @@ def create_text_webhook(
|
|||
"""
|
||||
webhook_message: str = ""
|
||||
|
||||
if get_custom_message(reader, entry.feed) != "": # noqa: PLC1901
|
||||
if get_custom_message(reader, entry.feed) != "": # ruff:ignore[compare-to-empty-string]
|
||||
webhook_message = replace_tags_in_text_message(entry=entry, reader=reader)
|
||||
|
||||
if not webhook_message and use_default_message_on_empty:
|
||||
|
|
@ -1370,6 +1372,40 @@ def screenshot_filename_for_entry(entry: Entry, *, extension: str = "png") -> st
|
|||
return f"{safe_name[:80]}.{safe_extension}"
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def is_chromium_installed() -> bool:
|
||||
"""Check if Playwright's Chromium browser is installed.
|
||||
|
||||
Uses a cached check so the browser is only probed once per process lifetime.
|
||||
Offloads to a thread when called from an active event loop (e.g. FastAPI).
|
||||
|
||||
Returns:
|
||||
bool: True if Chromium launched successfully, False otherwise.
|
||||
"""
|
||||
|
||||
def _check() -> bool:
|
||||
try:
|
||||
with sync_playwright() as playwright:
|
||||
browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
args=["--disable-dev-shm-usage", "--no-sandbox"],
|
||||
)
|
||||
browser.close()
|
||||
except (OSError, PlaywrightError):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return _check()
|
||||
else:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future: concurrent.futures.Future[bool] = executor.submit(_check)
|
||||
return future.result()
|
||||
|
||||
|
||||
def capture_full_page_screenshot(
|
||||
url: str,
|
||||
*,
|
||||
|
|
@ -1417,7 +1453,7 @@ def _capture_full_page_screenshot_sync(
|
|||
Returns:
|
||||
bytes | None: PNG bytes on success, otherwise None.
|
||||
"""
|
||||
try: # noqa: PLW0717
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
with sync_playwright() as playwright:
|
||||
browser: Browser = playwright.chromium.launch(
|
||||
headless=True,
|
||||
|
|
@ -1537,7 +1573,7 @@ def set_description(
|
|||
# Discord allows 2048, but we keep a small safety margin by default.
|
||||
embed_description: str = custom_embed.description
|
||||
if len(embed_description) > max_description_length:
|
||||
if max_description_length <= 3: # noqa: PLR2004
|
||||
if max_description_length <= 3: # ruff:ignore[magic-value-comparison]
|
||||
embed_description = embed_description[:max_description_length]
|
||||
else:
|
||||
embed_description = f"{embed_description[: max_description_length - 3]}..."
|
||||
|
|
@ -1635,7 +1671,7 @@ def get_ttvdrops_reward_description(drop: JsonObject, reward: JsonObject) -> str
|
|||
return reward_name
|
||||
|
||||
|
||||
def extract_ttvdrops_media_gallery_items(value: JsonValue, *, hide_paid: bool = False) -> list[JsonObject]: # noqa: C901
|
||||
def extract_ttvdrops_media_gallery_items(value: JsonValue, *, hide_paid: bool = False) -> list[JsonObject]: # ruff:ignore[complex-structure]
|
||||
"""Extract benefit/reward media gallery items from a ttvdrops API response.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1688,7 +1724,7 @@ def fetch_ttvdrops_campaign_media_items(entry: Entry) -> list[JsonObject]:
|
|||
|
||||
try:
|
||||
response: Response = httpx2.get(api_url, follow_redirects=True, timeout=10.0)
|
||||
if response.status_code != 200: # noqa: PLR2004
|
||||
if response.status_code != 200: # ruff:ignore[magic-value-comparison]
|
||||
logger.warning("Failed to fetch ttvdrops campaign data from %s: %s", api_url, response.text[:500])
|
||||
return []
|
||||
|
||||
|
|
@ -1743,7 +1779,7 @@ def truncate_component_text(
|
|||
"""
|
||||
if len(content) <= max_text_display_length:
|
||||
return content
|
||||
if max_text_display_length <= 3: # noqa: PLR2004
|
||||
if max_text_display_length <= 3: # ruff:ignore[magic-value-comparison]
|
||||
return content[:max_text_display_length]
|
||||
return f"{content[: max_text_display_length - 3]}..."
|
||||
|
||||
|
|
@ -1837,7 +1873,7 @@ def create_components_v2_webhook(
|
|||
)
|
||||
|
||||
|
||||
def create_embed_webhook( # noqa: C901, PLR0912
|
||||
def create_embed_webhook( # ruff:ignore[complex-structure, too-many-branches]
|
||||
webhook_url: str,
|
||||
entry: Entry,
|
||||
reader: Reader,
|
||||
|
|
@ -2121,7 +2157,7 @@ def truncate_webhook_message(
|
|||
"""
|
||||
if len(webhook_message) <= max_content_length:
|
||||
return webhook_message
|
||||
if max_content_length <= 3: # noqa: PLR2004
|
||||
if max_content_length <= 3: # ruff:ignore[magic-value-comparison]
|
||||
return webhook_message[:max_content_length]
|
||||
|
||||
head_length = (max_content_length - 3) // 2
|
||||
|
|
@ -2145,7 +2181,7 @@ def remove_invalid_new_feed(reader: Reader, feed_url: str) -> None:
|
|||
logger.exception("Failed to remove invalid feed after initial update: %s", feed_url)
|
||||
|
||||
|
||||
def create_feed(reader: Reader, feed_url: str, webhook_dropdown: str) -> None: # noqa: C901, PLR0912
|
||||
def create_feed(reader: Reader, feed_url: str, webhook_dropdown: str) -> None: # ruff:ignore[complex-structure, too-many-branches]
|
||||
"""Add a new feed, update it and mark every entry as read.
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess # noqa: S404
|
||||
import subprocess # ruff:ignore[suspicious-subprocess-import]
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
|
@ -102,22 +102,22 @@ def setup_backup_repo(backup_path: Path) -> bool:
|
|||
Returns:
|
||||
``True`` if the repository is ready, ``False`` on any error.
|
||||
"""
|
||||
try: # noqa: PLW0717
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
backup_path.mkdir(parents=True, exist_ok=True)
|
||||
git_dir: Path = backup_path / ".git"
|
||||
if not git_dir.exists():
|
||||
subprocess.run([GIT_EXECUTABLE, "init", str(backup_path)], check=True, capture_output=True) # noqa: S603
|
||||
subprocess.run([GIT_EXECUTABLE, "init", str(backup_path)], check=True, capture_output=True) # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
logger.info("Initialized git backup repository at %s", backup_path)
|
||||
|
||||
# Ensure a local identity exists so that `git commit` always works.
|
||||
for key, value in (("user.email", "discord-rss-bot@localhost"), ("user.name", "discord-rss-bot")):
|
||||
result: subprocess.CompletedProcess[bytes] = subprocess.run( # noqa: S603
|
||||
result: subprocess.CompletedProcess[bytes] = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "config", "--local", key],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
subprocess.run( # noqa: S603
|
||||
subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "config", "--local", key, value],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
|
@ -127,14 +127,14 @@ def setup_backup_repo(backup_path: Path) -> bool:
|
|||
remote_url: str = get_backup_remote()
|
||||
if remote_url:
|
||||
# Check if remote "origin" already exists.
|
||||
check_remote: subprocess.CompletedProcess[bytes] = subprocess.run( # noqa: S603
|
||||
check_remote: subprocess.CompletedProcess[bytes] = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "remote", "get-url", "origin"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
if check_remote.returncode != 0:
|
||||
# Remote doesn't exist, add it.
|
||||
subprocess.run( # noqa: S603
|
||||
subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "remote", "add", "origin", remote_url],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
|
@ -144,7 +144,7 @@ def setup_backup_repo(backup_path: Path) -> bool:
|
|||
# Remote exists, update it if the URL has changed.
|
||||
current_url: str = check_remote.stdout.decode().strip()
|
||||
if current_url != remote_url:
|
||||
subprocess.run( # noqa: S603
|
||||
subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "remote", "set-url", "origin", remote_url],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
|
@ -169,7 +169,7 @@ def export_state(reader: Reader, backup_path: Path) -> None:
|
|||
for tag in _FEED_TAGS:
|
||||
try:
|
||||
value: TagValue = reader.get_tag(feed, tag, None)
|
||||
if value is not None and value != "": # noqa: PLC1901
|
||||
if value is not None and value != "": # ruff:ignore[compare-to-empty-string]
|
||||
feed_data[tag] = value
|
||||
except Exception:
|
||||
logger.exception("Failed to read tag '%s' for feed '%s' during state export", tag, feed.url)
|
||||
|
|
@ -219,13 +219,13 @@ def commit_state_change(reader: Reader, message: str) -> None:
|
|||
if not setup_backup_repo(backup_path):
|
||||
return
|
||||
|
||||
try: # noqa: PLW0717
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
export_state(reader, backup_path)
|
||||
|
||||
subprocess.run([GIT_EXECUTABLE, "-C", str(backup_path), "add", "-A"], check=True, capture_output=True) # noqa: S603
|
||||
subprocess.run([GIT_EXECUTABLE, "-C", str(backup_path), "add", "-A"], check=True, capture_output=True) # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
|
||||
# Only create a commit if there are staged changes.
|
||||
diff_result: subprocess.CompletedProcess[bytes] = subprocess.run( # noqa: S603
|
||||
diff_result: subprocess.CompletedProcess[bytes] = subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "diff", "--cached", "--exit-code"],
|
||||
check=False,
|
||||
capture_output=True,
|
||||
|
|
@ -234,7 +234,7 @@ def commit_state_change(reader: Reader, message: str) -> None:
|
|||
logger.debug("No state changes to commit for: %s", message)
|
||||
return
|
||||
|
||||
subprocess.run( # noqa: S603
|
||||
subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "commit", "-m", message],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
|
@ -243,7 +243,7 @@ def commit_state_change(reader: Reader, message: str) -> None:
|
|||
|
||||
# Push to remote if configured.
|
||||
if get_backup_remote():
|
||||
subprocess.run( # noqa: S603
|
||||
subprocess.run( # ruff:ignore[subprocess-without-shell-equals-true]
|
||||
[GIT_EXECUTABLE, "-C", str(backup_path), "push", "origin", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ def healthcheck() -> None:
|
|||
sys.exit(0)
|
||||
sys.exit(1)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Healthcheck failed: {e}", file=sys.stderr) # noqa: T201
|
||||
print(f"Healthcheck failed: {e}", file=sys.stderr) # ruff:ignore[print]
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ def fetch_hoyolab_post(post_id: str) -> JsonObject | None:
|
|||
return None
|
||||
|
||||
http_ok = 200
|
||||
try: # noqa: PLW0717
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
url: str = f"https://bbs-api-os.hoyolab.com/community/post/wapi/getPostFull?post_id={post_id}"
|
||||
response: requests.Response = requests.get(url, timeout=10)
|
||||
|
||||
|
|
@ -94,7 +94,7 @@ def fetch_hoyolab_post(post_id: str) -> JsonObject | None:
|
|||
return None
|
||||
|
||||
|
||||
def create_hoyolab_webhook(webhook_url: str, entry: Entry, post_data: JsonObject) -> DiscordWebhook: # noqa: C901, PLR0912, PLR0914, PLR0915
|
||||
def create_hoyolab_webhook(webhook_url: str, entry: Entry, post_data: JsonObject) -> DiscordWebhook: # ruff:ignore[complex-structure, too-many-branches, too-many-locals, too-many-statements]
|
||||
"""Create a webhook with data from the Hoyolab API.
|
||||
|
||||
Args:
|
||||
|
|
@ -184,8 +184,8 @@ def create_hoyolab_webhook(webhook_url: str, entry: Entry, post_data: JsonObject
|
|||
|
||||
# Only show Youtube URL if available
|
||||
structured_content: str = str(post.get("structured_content", ""))
|
||||
if structured_content: # noqa: PLR1702
|
||||
try: # noqa: PLW0717
|
||||
if structured_content: # ruff:ignore[too-many-nested-blocks]
|
||||
try: # ruff:ignore[too-many-statements-in-try-clause]
|
||||
loaded_structured_content = cast("JsonValue", json.loads(structured_content))
|
||||
structured_content_data: list[JsonObject] = (
|
||||
[cast("JsonObject", item) for item in loaded_structured_content if isinstance(item, dict)]
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ from discord_rss_bot.feeds import get_feed_media_gallery_image_limit
|
|||
from discord_rss_bot.feeds import get_feed_webhook_text_length_limit
|
||||
from discord_rss_bot.feeds import get_screenshot_layout
|
||||
from discord_rss_bot.feeds import get_sent_webhook_records
|
||||
from discord_rss_bot.feeds import is_chromium_installed
|
||||
from discord_rss_bot.feeds import is_steam_feed_url
|
||||
from discord_rss_bot.feeds import send_entry_to_discord
|
||||
from discord_rss_bot.feeds import send_to_discord
|
||||
|
|
@ -138,7 +139,7 @@ LOGGING_CONFIG = {
|
|||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"standard": {
|
||||
"format": "%(asctime)s [%(processName)s: %(process)d] [%(threadName)s: %(thread)d] [%(levelname)s] %(name)s: %(message)s", # noqa: E501
|
||||
"format": "%(asctime)s [%(processName)s: %(process)d] [%(threadName)s: %(thread)d] [%(levelname)s] %(name)s: %(message)s", # ruff:ignore[line-too-long]
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
|
|
@ -1259,7 +1260,7 @@ async def get_embed_page(
|
|||
|
||||
|
||||
@app.post("/embed", response_class=HTMLResponse)
|
||||
async def post_embed( # noqa: C901
|
||||
async def post_embed( # ruff:ignore[complex-structure]
|
||||
feed_url: Annotated[str, Form()],
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
title: Annotated[str, Form()] = "",
|
||||
|
|
@ -1272,6 +1273,7 @@ async def post_embed( # noqa: C901
|
|||
author_icon_url: Annotated[str, Form()] = "",
|
||||
footer_text: Annotated[str, Form()] = "",
|
||||
footer_icon_url: Annotated[str, Form()] = "",
|
||||
*,
|
||||
show_steam_game_icon_in_thumbnail: Annotated[bool, Form()] = False,
|
||||
) -> RedirectResponse:
|
||||
"""Set the embed settings.
|
||||
|
|
@ -1778,7 +1780,7 @@ def get_add(
|
|||
|
||||
|
||||
@app.get("/feed", response_class=HTMLResponse)
|
||||
async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
||||
async def get_feed( # ruff:ignore[complex-structure, too-many-branches, too-many-locals, too-many-statements]
|
||||
feed_url: str,
|
||||
request: Request,
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
|
|
@ -1874,6 +1876,7 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|||
"max_webhook_text_length_limit": 4000,
|
||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||
"is_steam_feed": is_steam_feed_url(feed.url),
|
||||
"chromium_installed": is_chromium_installed(),
|
||||
}
|
||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||
|
||||
|
|
@ -1941,11 +1944,12 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|||
"max_webhook_text_length_limit": 4000,
|
||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||
"is_steam_feed": is_steam_feed_url(feed.url),
|
||||
"chromium_installed": is_chromium_installed(),
|
||||
}
|
||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||
|
||||
|
||||
def create_html_for_feed( # noqa: C901, PLR0914
|
||||
def create_html_for_feed( # ruff:ignore[complex-structure, too-many-locals]
|
||||
reader: Reader,
|
||||
entries: Iterable[Entry],
|
||||
current_feed_url: str = "",
|
||||
|
|
@ -2051,7 +2055,7 @@ def create_html_for_feed( # noqa: C901, PLR0914
|
|||
{video_embed_html}
|
||||
{image_html}
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
""" # ruff:ignore[line-too-long]
|
||||
return html.strip()
|
||||
|
||||
|
||||
|
|
@ -2152,7 +2156,7 @@ async def get_settings(
|
|||
global_delivery_mode = "embed"
|
||||
|
||||
global_webhook_text_length_limit: int = coerce_webhook_text_length_limit(
|
||||
reader.get_tag((), "webhook_text_length_limit", 4000)
|
||||
reader.get_tag((), "webhook_text_length_limit", 4000),
|
||||
)
|
||||
|
||||
# Get all feeds with their intervals
|
||||
|
|
@ -2181,6 +2185,7 @@ async def get_settings(
|
|||
"global_webhook_text_length_limit": global_webhook_text_length_limit,
|
||||
"max_webhook_text_length_limit": 4000,
|
||||
"feed_intervals": feed_intervals,
|
||||
"chromium_installed": is_chromium_installed(),
|
||||
}
|
||||
return templates.TemplateResponse(request=request, name="settings.html", context=context)
|
||||
|
||||
|
|
@ -2603,8 +2608,8 @@ def create_webhook_feed_url_preview(
|
|||
webhook_feeds: list[Feed],
|
||||
replace_from: str,
|
||||
replace_to: str,
|
||||
resolve_urls: bool, # noqa: FBT001
|
||||
force_update: bool = False, # noqa: FBT001, FBT002
|
||||
resolve_urls: bool, # ruff:ignore[boolean-type-hint-positional-argument]
|
||||
force_update: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
existing_feed_urls: set[str] | None = None,
|
||||
) -> list[dict[str, str | bool | None]]:
|
||||
"""Create preview rows for bulk feed URL replacement.
|
||||
|
|
@ -2670,8 +2675,8 @@ def build_webhook_mass_update_context(
|
|||
all_feeds: list[Feed],
|
||||
replace_from: str,
|
||||
replace_to: str,
|
||||
resolve_urls: bool, # noqa: FBT001
|
||||
force_update: bool = False, # noqa: FBT001, FBT002
|
||||
resolve_urls: bool, # ruff:ignore[boolean-type-hint-positional-argument]
|
||||
force_update: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
) -> dict[str, str | bool | int | list[dict[str, str | bool | None]] | dict[str, int]]:
|
||||
"""Build context data used by the webhook mass URL update preview UI.
|
||||
|
||||
|
|
@ -2732,8 +2737,8 @@ async def get_webhook_entries_mass_update_preview(
|
|||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
replace_from: str = "",
|
||||
replace_to: str = "",
|
||||
resolve_urls: bool = True, # noqa: FBT001, FBT002
|
||||
force_update: bool = False, # noqa: FBT001, FBT002
|
||||
resolve_urls: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
force_update: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
) -> HTMLResponse:
|
||||
"""Render the mass-update preview fragment for a webhook using HTMX.
|
||||
|
||||
|
|
@ -2771,15 +2776,15 @@ async def get_webhook_entries_mass_update_preview(
|
|||
|
||||
|
||||
@app.get("/webhook_entries", response_class=HTMLResponse)
|
||||
async def get_webhook_entries( # noqa: C901, PLR0914
|
||||
async def get_webhook_entries( # ruff:ignore[complex-structure, too-many-locals]
|
||||
webhook_url: str,
|
||||
request: Request,
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
starting_after: str = "",
|
||||
replace_from: str = "",
|
||||
replace_to: str = "",
|
||||
resolve_urls: bool = True, # noqa: FBT001, FBT002
|
||||
force_update: bool = False, # noqa: FBT001, FBT002
|
||||
resolve_urls: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
force_update: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
|
||||
message: str = "",
|
||||
) -> HTMLResponse:
|
||||
"""Get all latest entries from all feeds for a specific webhook.
|
||||
|
|
@ -2901,13 +2906,13 @@ async def get_webhook_entries( # noqa: C901, PLR0914
|
|||
|
||||
|
||||
@app.post("/bulk_change_feed_urls", response_class=HTMLResponse)
|
||||
async def post_bulk_change_feed_urls( # noqa: C901, PLR0914, PLR0912, PLR0915
|
||||
async def post_bulk_change_feed_urls( # ruff:ignore[complex-structure, too-many-locals, too-many-branches, too-many-statements]
|
||||
webhook_url: Annotated[str, Form()],
|
||||
replace_from: Annotated[str, Form()],
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
replace_to: Annotated[str, Form()] = "",
|
||||
resolve_urls: Annotated[bool, Form()] = True, # noqa: FBT002
|
||||
force_update: Annotated[bool, Form()] = False, # noqa: FBT002
|
||||
resolve_urls: Annotated[bool, Form()] = True, # ruff:ignore[boolean-default-value-positional-argument]
|
||||
force_update: Annotated[bool, Form()] = False, # ruff:ignore[boolean-default-value-positional-argument]
|
||||
) -> RedirectResponse:
|
||||
"""Bulk-change feed URLs attached to a webhook.
|
||||
|
||||
|
|
@ -3044,7 +3049,7 @@ if __name__ == "__main__":
|
|||
uvicorn.run(
|
||||
"main:app",
|
||||
log_level="debug",
|
||||
host="0.0.0.0", # noqa: S104
|
||||
host="0.0.0.0", # ruff:ignore[hardcoded-bind-all-interfaces]
|
||||
port=3000,
|
||||
proxy_headers=True,
|
||||
forwarded_allow_ips="*",
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
{% elif delivery_mode == "screenshot" %}
|
||||
<li>Screenshot layout: {{ screenshot_layout }}.</li>
|
||||
{% endif %}
|
||||
<li>Webhook text</li> is truncated to {{ webhook_text_length_limit }} characters.</li>
|
||||
<li>Webhook text is truncated to {{ webhook_text_length_limit }} characters.</li>
|
||||
<li>
|
||||
Filters:
|
||||
{% if has_blacklist_filters and has_whitelist_filters %}
|
||||
|
|
@ -173,10 +173,13 @@
|
|||
<section class="mb-3">
|
||||
<h4 class="h6 text-muted mb-2">Delivery Mode</h4>
|
||||
<p class="text-muted small mb-2">
|
||||
Choose between:<br>
|
||||
Embed: a Discord embed with title, description, and images.<br>
|
||||
Choose between:
|
||||
<br>
|
||||
Embed: a Discord embed with title, description, and images.
|
||||
<br>
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
Screenshot: full-page screenshot of the entry link.<br>
|
||||
Screenshot: full-page screenshot of the entry link.
|
||||
<br>
|
||||
{% endif %}
|
||||
Text: plain message.
|
||||
</p>
|
||||
|
|
@ -184,39 +187,56 @@
|
|||
{% if delivery_mode != "embed" %}
|
||||
<form action="/use_embed" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
style="border-top-right-radius: 0; border-bottom-right-radius: 0;"
|
||||
style="border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Embed</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="btn btn-primary btn-sm disabled"
|
||||
style="border-top-right-radius: 0; border-bottom-right-radius: 0;">Embed</span>
|
||||
style="border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0">Embed</span>
|
||||
{% endif %}
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
{% if delivery_mode != "screenshot" %}
|
||||
<form action="/use_screenshot" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm rounded-0"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Screenshot</button>
|
||||
</form>
|
||||
{% if chromium_installed %}
|
||||
{% if delivery_mode != "screenshot" %}
|
||||
<form action="/use_screenshot" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm rounded-0"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Screenshot</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="btn btn-primary btn-sm disabled rounded-0">Screenshot</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="btn btn-primary btn-sm disabled rounded-0">Screenshot</span>
|
||||
<span class="btn btn-outline-light btn-sm rounded-0"
|
||||
title="Playwright Chromium is not installed. Run 'uv run playwright install chromium' to enable."
|
||||
style="opacity: 0.5;
|
||||
cursor: not-allowed">Screenshot</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if delivery_mode != "text" %}
|
||||
<form action="/use_text" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
style="border-top-left-radius: 0; border-bottom-left-radius: 0;"
|
||||
style="border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Text</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="btn btn-primary btn-sm disabled"
|
||||
style="border-top-left-radius: 0; border-bottom-left-radius: 0;">Text</span>
|
||||
style="border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0">Text</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not chromium_installed and not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
<div class="mt-2 small text-muted">
|
||||
Screenshot mode requires Chromium for Playwright.
|
||||
Run <code class="text-light">uv run playwright install chromium</code> to enable it.
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% if delivery_mode == "screenshot" and not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
{% if delivery_mode == "screenshot" and chromium_installed and not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
<hr class="border-secondary" />
|
||||
<section class="mb-3">
|
||||
<h4 class="h6 text-muted mb-2">Screenshot Layout</h4>
|
||||
|
|
@ -237,10 +257,6 @@
|
|||
<span class="btn btn-primary btn-sm disabled">Desktop</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mt-2 small text-muted">
|
||||
Requires Chromium for Playwright. Run
|
||||
<code class="text-light">uv run playwright install chromium</code> once.
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
|
|
@ -283,7 +299,7 @@
|
|||
oninput="this.form.elements.image_limit_value.value = this.value" />
|
||||
<div class="d-flex justify-content-between text-muted small">
|
||||
<span>0</span>
|
||||
</span> <span>{{ max_media_gallery_items }}</span>
|
||||
<span>{{ max_media_gallery_items }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm" type="submit">Save image limit</button>
|
||||
|
|
@ -472,14 +488,10 @@
|
|||
href="/blacklist?feed_url={{ feed.url|encode_url }}">Blacklist</a>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a class="btn {{ 'btn-primary' if delivery_mode == 'text' else 'btn-outline-light' }}"
|
||||
href="/custom?feed_url={{ feed.url|encode_url }}">
|
||||
Customize message
|
||||
</a>
|
||||
href="/custom?feed_url={{ feed.url|encode_url }}">Customize message</a>
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
<a class="btn {{ 'btn-primary' if delivery_mode == 'embed' else 'btn-outline-light' }}"
|
||||
href="/embed?feed_url={{ feed.url|encode_url }}">
|
||||
Customize embed
|
||||
</a>
|
||||
href="/embed?feed_url={{ feed.url|encode_url }}">Customize embed</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@
|
|||
name="delivery_mode">
|
||||
<option value="embed"
|
||||
{% if global_delivery_mode == "embed" %}selected{% endif %}>Embed</option>
|
||||
<option value="screenshot"
|
||||
{% if global_delivery_mode == "screenshot" %}selected{% endif %}
|
||||
{% if not chromium_installed %}disabled{% endif %}>
|
||||
Screenshot{% if not chromium_installed %} (Chromium not installed){% endif %}
|
||||
</option>
|
||||
<option value="text"
|
||||
{% if global_delivery_mode == "text" %}selected{% endif %}>Text</option>
|
||||
</select>
|
||||
|
|
@ -73,9 +78,12 @@
|
|||
<div class="form-text text-muted mt-2">
|
||||
New feeds inherit this value. Existing feeds keep their current screenshot layout.
|
||||
</div>
|
||||
<div class="form-text screenshot-requirement mt-1">
|
||||
Screenshot mode requires Chromium to be installed for Playwright. Run <code>uv run playwright install chromium</code> once on this machine.
|
||||
</div>
|
||||
{% if not chromium_installed %}
|
||||
<div class="form-text text-muted mt-1">
|
||||
Screenshot mode requires Chromium to be installed for Playwright.
|
||||
Run <code>uv run playwright install chromium</code> once on this machine.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
<form action="/set_global_webhook_text_length_limit"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class WebhookFile:
|
|||
class DiscordEmbed:
|
||||
"""Small Discord embed payload builder used by the webhook sender."""
|
||||
|
||||
def __init__(self) -> None: # noqa: D107
|
||||
def __init__(self) -> None: # ruff:ignore[undocumented-public-init]
|
||||
self._payload: JsonObject = {}
|
||||
|
||||
def to_dict(self) -> JsonObject:
|
||||
|
|
@ -54,7 +54,7 @@ class DiscordEmbed:
|
|||
def set_thumbnail(self, *, url: str) -> None:
|
||||
self._payload["thumbnail"] = {"url": url}
|
||||
|
||||
def set_image(self, *, url: str, **_ignored: Any) -> None: # noqa: ANN401
|
||||
def set_image(self, *, url: str, **_ignored: Any) -> None: # ruff:ignore[any-type]
|
||||
self._payload["image"] = {"url": url}
|
||||
|
||||
def set_footer(self, *, text: str, icon_url: str | None = None) -> None:
|
||||
|
|
@ -85,7 +85,7 @@ class DiscordWebhook:
|
|||
while leaving the actual HTTP transport to `httpx2`.
|
||||
"""
|
||||
|
||||
def __init__( # noqa: D107
|
||||
def __init__( # ruff:ignore[undocumented-public-init]
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
|
|
@ -99,7 +99,7 @@ class DiscordWebhook:
|
|||
thread_id: str | None = None,
|
||||
timeout: float | None = None,
|
||||
rate_limit_retry: bool = False,
|
||||
**_ignored: Any, # noqa: ANN401
|
||||
**_ignored: Any, # ruff:ignore[any-type]
|
||||
) -> None:
|
||||
self.url: str = url
|
||||
self.thread_id: str | None = thread_id
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue