diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d9b1fb..28d07e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: # An extremely fast Python linter and formatter. - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.16 + rev: v0.15.22 hooks: - id: ruff-format types_or: [ python, pyi, jupyter, pyproject ] diff --git a/discord_rss_bot/feeds.py b/discord_rss_bot/feeds.py index 7af4f3c..df35e85 100644 --- a/discord_rss_bot/feeds.py +++ b/discord_rss_bot/feeds.py @@ -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: diff --git a/discord_rss_bot/git_backup.py b/discord_rss_bot/git_backup.py index b7a7f16..72298f0 100644 --- a/discord_rss_bot/git_backup.py +++ b/discord_rss_bot/git_backup.py @@ -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, diff --git a/discord_rss_bot/healthcheck.py b/discord_rss_bot/healthcheck.py index 7fa0231..1a2c07e 100644 --- a/discord_rss_bot/healthcheck.py +++ b/discord_rss_bot/healthcheck.py @@ -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) diff --git a/discord_rss_bot/hoyolab_api.py b/discord_rss_bot/hoyolab_api.py index c1aa2c1..954127b 100644 --- a/discord_rss_bot/hoyolab_api.py +++ b/discord_rss_bot/hoyolab_api.py @@ -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)] diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py index 44b85a0..70b8de6 100644 --- a/discord_rss_bot/main.py +++ b/discord_rss_bot/main.py @@ -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} -""" # 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="*", diff --git a/discord_rss_bot/templates/feed.html b/discord_rss_bot/templates/feed.html index e2d270a..7b20cb1 100644 --- a/discord_rss_bot/templates/feed.html +++ b/discord_rss_bot/templates/feed.html @@ -103,7 +103,7 @@ {% elif delivery_mode == "screenshot" %}
- Choose between:
- Embed: a Discord embed with title, description, and images.
+ Choose between:
+
+ Embed: a Discord embed with title, description, and images.
+
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
- Screenshot: full-page screenshot of the entry link.
+ Screenshot: full-page screenshot of the entry link.
+
{% endif %}
Text: plain message.
uv run playwright install chromium to enable it.
+ uv run playwright install chromium once.
- uv run playwright install chromium once on this machine.
- uv run playwright install chromium once on this machine.
+