summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.tasks/tasks.jsonl6
-rw-r--r--Biz/PodcastItLater/UI.py36
-rw-r--r--Biz/PodcastItLater/Web.py68
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):