From bbb1e56e97ee5e899fc6deae497f06b3f13595d3 Mon Sep 17 00:00:00 2001 From: Ben Sima Date: Thu, 13 Nov 2025 14:53:55 -0500 Subject: Extract format_duration utility to UI module Moved format_duration function from Web.py to UI.py for better code organization. This is a UI utility function used for displaying episode durations, so it belongs in the shared UI module rather than the web-specific module. Amp-Thread-ID: https://ampcode.com/threads/T-8edacbeb-b343-49ca-b524-1c999272acb6 Co-authored-by: Amp --- .tasks/tasks.jsonl | 6 ++--- Biz/PodcastItLater/UI.py | 36 +++++++++++++++++++++++++ Biz/PodcastItLater/Web.py | 68 +++++++++++------------------------------------ 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl index 50a23f4..f2e2f4a 100644 --- a/.tasks/tasks.jsonl +++ b/.tasks/tasks.jsonl @@ -49,9 +49,9 @@ {"taskCreatedAt":"2025-11-13T19:38:08.01779309Z","taskDependencies":[],"taskId":"t-1f9RIzd","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Account Management Page","taskType":"Epic","taskUpdatedAt":"2025-11-13T19:38:08.01779309Z"} {"taskCreatedAt":"2025-11-13T19:38:08.176692694Z","taskDependencies":[],"taskId":"t-1f9SnU7","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Queue Status Improvements","taskType":"Epic","taskUpdatedAt":"2025-11-13T19:38:08.176692694Z"} {"taskCreatedAt":"2025-11-13T19:38:08.37344762Z","taskDependencies":[],"taskId":"t-1f9Td4U","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"Navbar Styling Cleanup","taskType":"Epic","taskUpdatedAt":"2025-11-13T19:38:08.37344762Z"} -{"taskCreatedAt":"2025-11-13T19:38:32.95559213Z","taskDependencies":[],"taskId":"t-1fbym1M","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"InProgress","taskTitle":"Remove BLE001 noqa for bare Exception catches - use specific exceptions","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:42:23.431401671Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.139120541Z","taskDependencies":[],"taskId":"t-1fbz7LV","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Open","taskTitle":"Fix PLR0913 violations - refactor functions with too many parameters","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:33.139120541Z"} -{"taskCreatedAt":"2025-11-13T19:38:33.309222802Z","taskDependencies":[],"taskId":"t-1fbzQ1v","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Open","taskTitle":"Extract format_duration utility to shared UI or Core module (used only in Web.py)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:33.309222802Z"} +{"taskCreatedAt":"2025-11-13T19:38:32.95559213Z","taskDependencies":[],"taskId":"t-1fbym1M","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Done","taskTitle":"Remove BLE001 noqa for bare Exception catches - use specific exceptions","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:43:29.049855419Z"} +{"taskCreatedAt":"2025-11-13T19:38:33.139120541Z","taskDependencies":[],"taskId":"t-1fbz7LV","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Done","taskTitle":"Fix PLR0913 violations - refactor functions with too many parameters","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:44:09.820023426Z"} +{"taskCreatedAt":"2025-11-13T19:38:33.309222802Z","taskDependencies":[],"taskId":"t-1fbzQ1v","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"InProgress","taskTitle":"Extract format_duration utility to shared UI or Core module (used only in Web.py)","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:44:13.950088651Z"} {"taskCreatedAt":"2025-11-13T19:38:33.491331064Z","taskDependencies":[],"taskId":"t-1fbABoD","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Open","taskTitle":"Extract extract_og_metadata and send_magic_link to Core module for reusability","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:33.491331064Z"} {"taskCreatedAt":"2025-11-13T19:38:33.674140035Z","taskDependencies":[],"taskId":"t-1fbBmXa","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Open","taskTitle":"Review and fix type: ignore comments - improve type safety","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:33.674140035Z"} {"taskCreatedAt":"2025-11-13T19:38:33.85804778Z","taskDependencies":[],"taskId":"t-1fbC8Nq","taskNamespace":null,"taskParent":"t-1f9QP23","taskStatus":"Open","taskTitle":"Remove PLR2004 magic number - use constant for month check","taskType":"WorkTask","taskUpdatedAt":"2025-11-13T19:38:33.85804778Z"} diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py index 940abd5..99ac29b 100644 --- a/Biz/PodcastItLater/UI.py +++ b/Biz/PodcastItLater/UI.py @@ -9,6 +9,42 @@ Common UI components and utilities shared across web pages. import ludic.html as html +def format_duration(seconds: int | None) -> str: + """Format duration from seconds to human-readable format. + + Examples: + 300 -> "5m" + 3840 -> "1h 4m" + 11520 -> "3h 12m" + """ + if seconds is None or seconds <= 0: + return "Unknown" + + # Constants for time conversion + seconds_per_minute = 60 + minutes_per_hour = 60 + seconds_per_hour = 3600 + + # Round up to nearest minute + minutes = (seconds + seconds_per_minute - 1) // seconds_per_minute + + # Show as minutes only if under 60 minutes (exclusive) + # 3599 seconds rounds up to 60 minutes, which we keep as "60m" + if minutes <= minutes_per_hour: + # If exactly 3600 seconds (already 60 full minutes without rounding) + if seconds >= seconds_per_hour: + return "1h" + return f"{minutes}m" + + hours = minutes // minutes_per_hour + remaining_minutes = minutes % minutes_per_hour + + if remaining_minutes == 0: + return f"{hours}h" + + return f"{hours}h {remaining_minutes}m" + + def create_bootstrap_styles() -> html.style: """Load Bootstrap CSS and icons.""" return html.style( diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 3524be8..3a76e93 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -81,42 +81,6 @@ RSS_CONFIG = { } -def format_duration(seconds: int | None) -> str: - """Format duration from seconds to human-readable format. - - Examples: - 300 -> "5m" - 3840 -> "1h 4m" - 11520 -> "3h 12m" - """ - if seconds is None or seconds <= 0: - return "Unknown" - - # Constants for time conversion - seconds_per_minute = 60 - minutes_per_hour = 60 - seconds_per_hour = 3600 - - # Round up to nearest minute - minutes = (seconds + seconds_per_minute - 1) // seconds_per_minute - - # Show as minutes only if under 60 minutes (exclusive) - # 3599 seconds rounds up to 60 minutes, which we keep as "60m" - if minutes <= minutes_per_hour: - # If exactly 3600 seconds (already 60 full minutes without rounding) - if seconds >= seconds_per_hour: - return "1h" - return f"{minutes}m" - - hours = minutes // minutes_per_hour - remaining_minutes = minutes % minutes_per_hour - - if remaining_minutes == 0: - return f"{hours}h" - - return f"{hours}h {remaining_minutes}m" - - def extract_og_metadata(url: str) -> tuple[str | None, str | None]: """Extract Open Graph title and author from URL. @@ -531,7 +495,7 @@ class EpisodeList(Component[AnyChildren, EpisodeListAttrs]): episode_items = [] for episode in episodes: - duration_str = format_duration(episode.get("duration")) + duration_str = UI.format_duration(episode.get("duration")) episode_items.append( html.div( html.div( @@ -1506,30 +1470,30 @@ class TestDurationFormatting(Test.TestCase): def test_format_duration_minutes_only(self) -> None: """Test formatting durations less than an hour.""" - self.assertEqual(format_duration(60), "1m") - self.assertEqual(format_duration(240), "4m") - self.assertEqual(format_duration(300), "5m") - self.assertEqual(format_duration(3599), "60m") + self.assertEqual(UI.format_duration(60), "1m") + self.assertEqual(UI.format_duration(240), "4m") + self.assertEqual(UI.format_duration(300), "5m") + self.assertEqual(UI.format_duration(3599), "60m") def test_format_duration_hours_and_minutes(self) -> None: """Test formatting durations with hours and minutes.""" - self.assertEqual(format_duration(3600), "1h") - self.assertEqual(format_duration(3840), "1h 4m") - self.assertEqual(format_duration(11520), "3h 12m") - self.assertEqual(format_duration(7320), "2h 2m") + self.assertEqual(UI.format_duration(3600), "1h") + self.assertEqual(UI.format_duration(3840), "1h 4m") + self.assertEqual(UI.format_duration(11520), "3h 12m") + self.assertEqual(UI.format_duration(7320), "2h 2m") def test_format_duration_round_up(self) -> None: """Test that seconds are rounded up to nearest minute.""" - self.assertEqual(format_duration(61), "2m") - self.assertEqual(format_duration(119), "2m") - self.assertEqual(format_duration(121), "3m") - self.assertEqual(format_duration(3601), "1h 1m") + self.assertEqual(UI.format_duration(61), "2m") + self.assertEqual(UI.format_duration(119), "2m") + self.assertEqual(UI.format_duration(121), "3m") + self.assertEqual(UI.format_duration(3601), "1h 1m") def test_format_duration_edge_cases(self) -> None: """Test edge cases for duration formatting.""" - self.assertEqual(format_duration(None), "Unknown") - self.assertEqual(format_duration(0), "Unknown") - self.assertEqual(format_duration(-100), "Unknown") + self.assertEqual(UI.format_duration(None), "Unknown") + self.assertEqual(UI.format_duration(0), "Unknown") + self.assertEqual(UI.format_duration(-100), "Unknown") class TestAuthentication(BaseWebTest): -- cgit v1.2.3