Skip to content

Conversation

@hbmartin
Copy link
Owner

@hbmartin hbmartin commented Aug 5, 2025

PR Type

Enhancement


Description

  • Add starred and deleted episode HTML pages

  • Implement episode age limiting with environment variable

  • Refactor HTML generation with shared template function

  • Add navigation between different episode views


Diagram Walkthrough

flowchart LR
  CLI["CLI html command"] --> GEN["HTML generators"]
  GEN --> PLAYED["Recently Played"]
  GEN --> STARRED["Starred Episodes"]
  GEN --> DELETED["Deleted Episodes"]
  DB["Database queries"] --> GEN
  ENV["OVERCAST_LIMIT_DAYS"] --> FILTER["Episode filtering"]
  FILTER --> DB
Loading

File Walkthrough

Relevant files
Enhancement
cli.py
Expand HTML command for multiple page types                           

overcast_to_sqlite/cli.py

  • Import new HTML generators for starred and deleted pages
  • Update html command to generate three HTML files instead of one
  • Add cleanup call after episode saving
  • Update command description and output messages
+26/-9   
datastore.py
Add episode filtering and new query methods                           

overcast_to_sqlite/datastore.py

  • Add episode age filtering based on OVERCAST_LIMIT_DAYS environment
    variable
  • Implement cleanup_old_episodes() method for database maintenance
  • Add get_starred_episodes() and get_deleted_episodes() query methods
  • Refactor URL cleaning and query building into reusable helper methods
  • Fix SQL GROUP BY clause in get_feeds_to_extend()
+166/-22
page.py
Refactor HTML generation with shared template                       

overcast_to_sqlite/html/page.py

  • Extract shared HTML generation logic into _generate_html_episodes()
    helper
  • Add generate_html_starred() and generate_html_deleted() functions
  • Make date field configurable for different episode types
  • Improve starred icon handling logic
+43/-10 
index.html
Add navigation between episode pages                                         

overcast_to_sqlite/html/index.html

  • Add navigation bar with links to all three episode pages
  • Style navigation with centered layout and borders
+5/-0     
Configuration changes
settings.local.json
Add Claude AI configuration                                                           

.claude/settings.local.json

  • Add Claude AI configuration file with bash permissions
+8/-0     
Documentation
CLAUDE.md
Add Python coding standards documentation                               

CLAUDE.md

  • Add Python coding standards and best practices documentation
  • Specify type hints, f-strings, pathlib usage requirements
+12/-0   
Dependencies
pyproject.toml
Update development dependencies                                                   

pyproject.toml

  • Update development dependency versions for mypy, ruff, and
    types-python-dateutil
+3/-3     


Important

Add functionality to generate HTML pages for starred and deleted episodes, with supporting database queries and functions.

  • Behavior:
    • html() in cli.py now generates HTML for starred and deleted episodes using generate_html_starred() and generate_html_deleted().
    • New functions get_starred_episodes() and get_deleted_episodes() in datastore.py to retrieve starred and deleted episodes.
    • HTML generation for episodes refactored into _generate_html_episodes() in page.py.
  • Database:
    • Added get_starred_episodes() and get_deleted_episodes() in datastore.py to query starred and deleted episodes.
    • _clean_enclosure_urls_simple() added to datastore.py for cleaning URLs without removing duplicates.
  • Misc:
    • Updated uv.lock to reflect new package versions and sources.

This description was created by Ellipsis for 31b6db3. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Added navigation bar to HTML pages for easier access to Recently Played, Starred, and Deleted episodes.
    • Separate HTML pages are now generated for Recently Played, Starred, and Deleted episodes.
    • Introduced configurable episode filtering and cleanup based on environment variables.
  • Improvements

    • HTML episode list generation is now more modular and consistent across different episode types.
    • Enhanced URL normalization and improved episode grouping in listings.
  • Bug Fixes

    • Fixed a SQL grouping issue affecting feed extension logic.
  • Chores

    • Updated development dependencies.
    • Generalized HTML file ignore pattern.
    • Added configuration and documentation files.

  1. Environment variable filtering in datastore.py:192-208: Modified save_feed_and_episodes() to filter out episodes where userUpdatedDate < (now -
  OVERCAST_LIMIT_DAYS) during insertion.
  2. Cleanup method in datastore.py:461-478: Added cleanup_old_episodes() method that deletes old episodes from the database, but only if there are more
  than 100 episode rows.
  3. CLI integration in cli.py:119: Added call to db.cleanup_old_episodes() at the end of the save command.

  How it works:
  - Set OVERCAST_LIMIT_DAYS=30 (or any number) to only keep episodes from the last 30 days
  - During save, episodes older than the limit won't be inserted
  - After save completes, if there are >100 episodes total, any remaining old episodes are deleted
  - If no environment variable is set, the behavior remains unchanged
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Aug 5, 2025

Reviewer's Guide

This PR introduces an environment-variable–driven episode retention mechanism and cleanup routine, refactors Datastore by extracting URL normalization and query-building helpers, adds dedicated starred and deleted episode retrieval methods, and generalizes HTML page generation and the CLI to emit separate played, starred, and deleted pages with navigation links.

Sequence diagram for CLI HTML command and page generation

sequenceDiagram
    actor User
    participant CLI
    participant Datastore
    participant HTMLPageGenerator
    User->>CLI: Run html command
    CLI->>HTMLPageGenerator: generate_html_played(db_path, played_path)
    HTMLPageGenerator->>Datastore: get_recently_played()
    CLI->>HTMLPageGenerator: generate_html_starred(db_path, starred_path)
    HTMLPageGenerator->>Datastore: get_starred_episodes()
    CLI->>HTMLPageGenerator: generate_html_deleted(db_path, deleted_path)
    HTMLPageGenerator->>Datastore: get_deleted_episodes()
    CLI->>User: Print HTML file locations
Loading

Class diagram for updated Datastore methods

classDiagram
    class Datastore {
        +save_feed_and_episodes(feed: dict, episodes: list[dict])
        +get_recently_played() list[dict[str, str]]
        +get_starred_episodes() list[dict[str, str]]
        +get_deleted_episodes() list[dict[str, str]]
        +cleanup_old_episodes() None
        -_clean_enclosure_urls() None
        -_clean_enclosure_urls_simple() None
        -_get_base_fields() list[str]
        -_build_episode_query(fields: list[str], where_clause: str, order_by: str) str
        -_process_query_results(results: list[tuple], fields: list[str]) list[dict[str, str]]
    }
    Datastore <|-- HTMLPageGenerator
    class HTMLPageGenerator {
        +_generate_html_episodes(episodes: list[dict[str, str]], title: str, html_output_path: Path, date_field: str = "userUpdatedDate") None
        +generate_html_played(db_path: str, html_output_path: Path) None
        +generate_html_starred(db_path: str, html_output_path: Path) None
        +generate_html_deleted(db_path: str, html_output_path: Path) None
    }
Loading

File-Level Changes

Change Details Files
Implement episode retention and cleanup based on OVERCAST_LIMIT_DAYS
  • Define _DEFAULT_EPISODE_LIMIT constant
  • Filter episodes in save_feed_and_episodes using OVERCAST_LIMIT_DAYS
  • Add cleanup_old_episodes method with env-var logic and delete query
  • Invoke cleanup_old_episodes in the CLI save command
overcast_to_sqlite/datastore.py
overcast_to_sqlite/cli.py
Refactor Datastore to extract query-building and URL cleaning helpers
  • Add _clean_enclosure_urls and _clean_enclosure_urls_simple methods
  • Extract _get_base_fields, _build_episode_query, and _process_query_results
  • Replace inline SQL and cleanup logic in get_recently_played
overcast_to_sqlite/datastore.py
Add support for starred and deleted episodes
  • Implement get_starred_episodes and get_deleted_episodes using the new helpers
  • Use simple URL cleaning in starred/deleted paths
  • Reuse the generic query builder and result processor
overcast_to_sqlite/datastore.py
Generalize HTML generation and update CLI html command
  • Extract _generate_html_episodes to handle any episode list
  • Rewrite generate_html_played to call the generic function
  • Add generate_html_starred and generate_html_deleted
  • Inject navigation bar into index.html and emit three HTML files in CLI
overcast_to_sqlite/html/page.py
overcast_to_sqlite/html/index.html
overcast_to_sqlite/cli.py
Bump dev dependency versions
  • Update mypy, ruff, and types-python-dateutil versions in dev dependencies
pyproject.toml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 5, 2025

Walkthrough

The changes introduce support for generating three separate HTML pages for recently played, starred, and deleted podcast episodes, with corresponding navigation links and refactored HTML generation logic. The datastore logic is modularized and enhanced to support episode filtering and cleanup based on environment variables. Additional configuration, coding guidelines, and development dependency updates are included.

Changes

Cohort / File(s) Change Summary
HTML Generation Refactor & Multi-Page Support
overcast_to_sqlite/cli.py, overcast_to_sqlite/html/page.py
Refactored HTML generation logic to use a generic helper. Added support and CLI commands for generating separate HTML files for played, starred, and deleted episodes.
Datastore Modularization & Cleanup
overcast_to_sqlite/datastore.py
Modularized episode retrieval logic, added environment-variable-driven episode filtering and cleanup, fixed SQL grouping, and improved URL normalization. Introduced new methods for starred and deleted episodes.
HTML Navigation Bar
overcast_to_sqlite/html/index.html
Added a navigation bar linking to played, starred, and deleted episode HTML pages.
Configuration & Coding Guidelines
.claude/settings.local.json, CLAUDE.md
Added local permissions configuration and a markdown file with recommended Python coding practices.
Development Dependency Updates
pyproject.toml
Upgraded versions of mypy, ruff, and types-python-dateutil in development dependencies.
Gitignore Update
.gitignore
Changed ignore pattern to exclude all root-level .html files instead of a single specific file.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI
    participant Datastore
    participant HTMLGen

    User->>CLI: run 'html' command
    CLI->>Datastore: get_recently_played()
    Datastore-->>CLI: recently played episodes
    CLI->>HTMLGen: generate_html_played()
    HTMLGen-->>CLI: overcast-played.html

    CLI->>Datastore: get_starred_episodes()
    Datastore-->>CLI: starred episodes
    CLI->>HTMLGen: generate_html_starred()
    HTMLGen-->>CLI: overcast-starred.html

    CLI->>Datastore: get_deleted_episodes()
    Datastore-->>CLI: deleted episodes
    CLI->>HTMLGen: generate_html_deleted()
    HTMLGen-->>CLI: overcast-deleted.html

    CLI->>User: Print file URLs for all three HTML files
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Poem

In the warren where code bunnies dwell,
We tidied our episodes, sorted them well—
Played, starred, and deleted, each has a page,
With a navbar to guide you, the latest stage!
Our datastore’s sharper, our HTML neat,
Three cheers for the changes—this update’s a treat!
🐇✨

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch deleted-starred-pages

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Aug 5, 2025

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Logic Error

The environment variable check uses incorrect walrus operator syntax that will always evaluate to None, causing the episode filtering to never work properly.

    if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
        limit_days = int(env_limit)
except ValueError:
    pass
Missing Parameter

The generate_html_deleted function passes an undefined parameter show_starred_icon=False to _generate_html_episodes which doesn't accept this parameter.

_generate_html_episodes(
    episodes,
    "Deleted Episodes",
    html_output_path,
    show_starred_icon=False,
)
Duplicate Logic

The environment variable parsing and date filtering logic is duplicated between save_feed_and_episodes and cleanup_old_episodes methods, creating maintenance overhead and potential inconsistencies.

limit_days = None
try:
    if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
        limit_days = int(env_limit)
except ValueError:
    pass

if not limit_days:
    return

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Changes

Hello @hbmartin, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly expands the overcast-to-sqlite tool's capabilities by introducing new HTML reports for starred and deleted podcast episodes, alongside enhanced data management features for episode retention and cleanup. The changes also include substantial refactoring to improve the codebase's structure and maintainability, as well as updates to development environment configurations.

Highlights

  • Enhanced HTML Reporting: The tool now generates dedicated HTML pages for 'Starred Episodes' and 'Deleted Episodes', in addition to the existing 'Recently Played' page. A new navigation bar has been added to index.html to easily switch between these views.
  • Episode Data Management: A new feature has been introduced to limit the retention of episodes in the database. Users can now set an OVERCAST_LIMIT_DAYS environment variable, and the system will automatically clean up episodes older than this limit during data synchronization, provided the total episode count exceeds a default threshold (100 episodes).
  • Code Refactoring and Reusability: Significant refactoring has been applied to the Datastore and HTML generation logic. Common patterns for database queries and HTML rendering have been extracted into new private helper methods, improving code reusability, readability, and maintainability.
  • Development Environment Setup: New files have been added to support development, including a .claude/settings.local.json for AI assistant permissions and a CLAUDE.md document outlining Python coding best practices and style guidelines for contributors.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@qodo-merge-pro
Copy link

qodo-merge-pro bot commented Aug 5, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix walrus operator assignment

The walrus operator assignment is incorrect. The condition should check if
env_limit is not None, but currently it assigns the result of the comparison to
env_limit. This will always be False since os.getenv() returns a string or None,
never None itself.

overcast_to_sqlite/datastore.py [202-203]

-if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
+if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
     limit_days = int(env_limit)
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies a bug in the walrus operator usage which would cause limit_days to be 1 instead of the value from the environment variable.

High
Remove invalid function parameter

The show_starred_icon=False parameter is passed to _generate_html_episodes(),
but this function doesn't accept this parameter. This will cause a TypeError
when the function is called.

overcast_to_sqlite/html/page.py [103-111]

 def generate_html_deleted(db_path: str, html_output_path: Path) -> None:
     db = Datastore(db_path)
     episodes = db.get_deleted_episodes()
     _generate_html_episodes(
         episodes,
         "Deleted Episodes",
         html_output_path,
-        show_starred_icon=False,
     )
  • Apply / Chat
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly points out that the call to _generate_html_episodes includes a non-existent parameter show_starred_icon, which would cause a TypeError.

High
  • More

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @hbmartin - I've reviewed your changes - here's some feedback:

Blocking issues:

  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)
  • Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option. (link)

General comments:

  • In save_feed_and_episodes (and cleanup_old_episodes), if env_limit := os.getenv(...) is not None assigns a boolean instead of the actual environment string; assign raw = os.getenv(...) and then check raw is not None before converting it to int.
  • generate_html_deleted calls _generate_html_episodes with a show_starred_icon argument, but the helper signature doesn’t accept that parameter; update the signature or remove the extra parameter.
  • There’s duplicated enclosure URL cleaning logic between _clean_enclosure_urls and _clean_enclosure_urls_simple; consider merging them into one method with a dedupe flag to reduce code repetition.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In save_feed_and_episodes (and cleanup_old_episodes), `if env_limit := os.getenv(...) is not None` assigns a boolean instead of the actual environment string; assign `raw = os.getenv(...)` and then check `raw is not None` before converting it to int.
- generate_html_deleted calls `_generate_html_episodes` with a `show_starred_icon` argument, but the helper signature doesn’t accept that parameter; update the signature or remove the extra parameter.
- There’s duplicated enclosure URL cleaning logic between `_clean_enclosure_urls` and `_clean_enclosure_urls_simple`; consider merging them into one method with a dedupe flag to reduce code repetition.

## Individual Comments

### Comment 1
<location> `overcast_to_sqlite/datastore.py:201` </location>
<code_context>
     ) -> None:
         """Upsert feed and episodes into database."""
+        limit_days = None
+        try:
+            if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
+                limit_days = int(env_limit)
+        except ValueError:
+            pass
+
</code_context>

<issue_to_address>
The logic for reading OVERCAST_LIMIT_DAYS from the environment is incorrect.

The current line assigns a boolean to env_limit instead of the environment variable's value. Assign the result of os.getenv to env_limit first, then check if it is not None before converting to int.
</issue_to_address>

### Comment 2
<location> `overcast_to_sqlite/datastore.py:207` </location>
<code_context>
+        except ValueError:
+            pass
+
+        if limit_days:
+            cutoff_date = datetime.datetime.now(tz=datetime.UTC) - datetime.timedelta(
+                days=int(limit_days),
</code_context>

<issue_to_address>
The check 'if limit_days:' will skip filtering if limit_days is 0.

If the environment variable is 0, no filtering occurs. Use 'if limit_days is not None:' to handle zero correctly.
</issue_to_address>

### Comment 3
<location> `overcast_to_sqlite/html/page.py:61` </location>
<code_context>
+    for ep in episodes:
</code_context>

<issue_to_address>
The starred icon logic does not handle show_starred_icon parameter.

Please ensure show_starred_icon is checked before displaying the icon, so the parameter is used as intended.
</issue_to_address>

### Comment 4
<location> `CLAUDE.md:4` </location>
<code_context>
+## Python Practices
+- Always use or add type hints
+- Prefer @dataclasses where applicable
+- Always use f-string over string formatting or concatentation (except in logging strings)
+- Use async generators and comprehensions when they might provide benefits
+- Use underscores in large numeric literals
</code_context>

<issue_to_address>
Typo: 'concatentation' should be 'concatenation'.

Please correct the spelling to 'concatenation'.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
- Always use f-string over string formatting or concatentation (except in logging strings)
=======
- Always use f-string over string formatting or concatenation (except in logging strings)
>>>>>>> REPLACE

</suggested_fix>

## Security Issues

### Issue 1
<location> `overcast_to_sqlite/datastore.py:578` </location>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

### Issue 2
<location> `overcast_to_sqlite/datastore.py:589` </location>

<issue_to_address>
**security (python.sqlalchemy.security.sqlalchemy-execute-raw-query):** Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +201 to +204
try:
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)
except ValueError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The logic for reading OVERCAST_LIMIT_DAYS from the environment is incorrect.

The current line assigns a boolean to env_limit instead of the environment variable's value. Assign the result of os.getenv to env_limit first, then check if it is not None before converting to int.

except ValueError:
pass

if limit_days:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): The check 'if limit_days:' will skip filtering if limit_days is 0.

If the environment variable is 0, no filtering occurs. Use 'if limit_days is not None:' to handle zero correctly.

Comment on lines 61 to +70
for ep in episodes:
ep["episode_title"] = html.escape(ep["episode_title"])
ep[DESCRIPTION] = _fix_unclosed_html_tags(
_convert_urls_to_links(ep[DESCRIPTION]),
)
user_date = ep["userUpdatedDate"].split("T")[0]
if last_user_updated_date != user_date:
user_date = ep[date_field].split("T")[0] if ep.get(date_field) else ""
if last_user_updated_date != user_date and user_date:
page_vars["episodes"] += (
"<h1><script>document.write("
f'new Date("{ep["userUpdatedDate"]}").toLocaleDateString()'
f'new Date("{ep[date_field]}").toLocaleDateString()'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): The starred icon logic does not handle show_starred_icon parameter.

Please ensure show_starred_icon is checked before displaying the icon, so the parameter is used as intended.

## Python Practices
- Always use or add type hints
- Prefer @dataclasses where applicable
- Always use f-string over string formatting or concatentation (except in logging strings)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (typo): Typo: 'concatentation' should be 'concatenation'.

Please correct the spelling to 'concatenation'.

Suggested change
- Always use f-string over string formatting or concatentation (except in logging strings)
- Always use f-string over string formatting or concatenation (except in logging strings)

if not limit_days:
return

episode_count = self.db.execute(f"SELECT COUNT(*) FROM {EPISODES}").fetchone()[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

Comment on lines +589 to +592
self.db.execute(
f"DELETE FROM {EPISODES} WHERE {USER_UPDATED_DATE} < ?",
[cutoff_iso],
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.sqlalchemy.security.sqlalchemy-execute-raw-query): Avoiding SQL string concatenation: untrusted input concatenated with raw SQL query can result in SQL Injection. In order to execute raw query safely, prepared statement should be used. SQLAlchemy provides TextualSQL to easily used prepared statement with named parameters. For complex SQL composition, use SQL Expression Language or Schema Definition Language. In most cases, SQLAlchemy ORM will be a better option.

Source: opengrep

Copy link

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Changes requested ❌

Reviewed everything up to 31b6db3 in 2 minutes and 52 seconds. Click for details.
  • Reviewed 1364 lines of code in 8 files
  • Skipped 1 files when reviewing.
  • Skipped posting 3 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. .claude/settings.local.json:8
  • Draft comment:
    Consider adding a newline at the end of the file for POSIX compliance.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% While POSIX compliance for newlines is a real thing, this seems like a minor stylistic issue that would typically be handled by editor settings or linting rules. The file is valid JSON either way. The comment doesn't indicate any actual problems this would cause. The lack of a trailing newline could potentially cause issues with some Unix tools or git diffs. Some development environments enforce this as a standard. While true, this is a minor stylistic issue that would be better handled by automated tooling or editor settings rather than manual code review comments. It doesn't affect functionality. Delete this comment as it's too minor of an issue to warrant a code review comment. This kind of formatting should be handled by automated tools.
2. overcast_to_sqlite/datastore.py:570
  • Draft comment:
    Repeat of the walrus operator issue in cleanup_old_episodes. Wrap the assignment in parentheses to correctly capture the environment variable.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 0% vs. threshold = 50% The current code if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None: is actually correct Python syntax. The walrus operator has lower precedence than the is operator, so this correctly assigns the value and then checks if it's None. Adding parentheses would change the meaning - it would first check if the getenv result is None, then assign that boolean to env_limit. Could I be wrong about operator precedence in Python? Should I double-check the Python documentation about walrus operator precedence? I am confident about this. The Python documentation and PEP 572 explicitly state that the walrus operator has lower precedence than comparison operators like is. The current code is correct and adding parentheses would change its meaning in an undesired way. The comment should be deleted because it suggests an incorrect change. The current code's operator precedence is correct and adding parentheses would actually break the intended functionality.
3. overcast_to_sqlite/datastore.py:497
  • Draft comment:
    Consider using explicit column aliases instead of parsing field names via split() in _process_query_results for more robust result mapping.
  • Reason this comment was not posted:
    Confidence changes required: 50% <= threshold 50% None

Workflow ID: wflow_TmzSwGihKkUqB5Ti

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

episodes,
"Deleted Episodes",
html_output_path,
show_starred_icon=False,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function call to _generate_html_episodes passes an unsupported keyword argument 'show_starred_icon'. Remove or update this parameter.

Suggested change
show_starred_icon=False,

"""Upsert feed and episodes into database."""
limit_days = None
try:
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The walrus operator usage is incorrect. Use parentheses: if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:

Suggested change
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:

## Python Practices
- Always use or add type hints
- Prefer @dataclasses where applicable
- Always use f-string over string formatting or concatentation (except in logging strings)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo found: In "Always use f-string over string formatting or concatentation (except in logging strings)", "concatentation" should be spelled "concatenation".

Suggested change
- Always use f-string over string formatting or concatentation (except in logging strings)
- Always use f-string over string formatting or concatenation (except in logging strings)

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several valuable enhancements, including new HTML pages for starred and deleted episodes, episode age limiting, and significant refactoring of the HTML generation and database query logic.

The refactoring improves the code's structure and maintainability. However, I've identified a few critical issues that need to be addressed:

  • A recurring bug in datastore.py with the use of the walrus operator will prevent the new episode age limiting feature from working as intended.
  • The refactored _generate_html_episodes function in page.py has several issues that will lead to runtime errors (TypeError and KeyError) and incorrect behavior.

I've also included a couple of medium-severity suggestions to align the code with the project's Python style guide regarding the use of named arguments. Once these critical issues are resolved, the PR will be in great shape.

Comment on lines +202 to +203
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There's a bug in how the environment variable is being read. The expression os.getenv(...) is not None evaluates to a boolean (True or False), which is then assigned to env_limit. int(env_limit) will then result in 1 or 0, not the actual day limit from the environment variable.

To fix this, the walrus assignment needs to be enclosed in parentheses.

Note that this same logic is duplicated in cleanup_old_episodes (lines 570-571) and should also be fixed there. To improve maintainability, consider extracting this logic into a private helper method.

if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
    limit_days = int(env_limit)

Comment on lines +570 to +571
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This is the same bug as in save_feed_and_episodes. The walrus operator assignment is incorrect, causing limit_days to be 1 instead of the value from the environment variable.

Please apply the same fix here by wrapping the assignment in parentheses. As mentioned in the other comment, extracting this duplicated logic into a shared private method would be a good refactoring to avoid this kind of issue.

if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
    limit_days = int(env_limit)

Comment on lines +43 to 83
def _generate_html_episodes(
episodes: list[dict[str, str]],
title: str,
html_output_path: Path,
date_field: str = "userUpdatedDate",
) -> None:
"""Generate HTML for any list of episodes."""
this_dir = Path(__file__).parent
page_vars = {
"title": "Recently Played",
"title": title,
"style": Path(this_dir / "mvp.css").read_text(),
"script": Path(this_dir / "search.js").read_text(),
"episodes": "",
}
page_template = (this_dir / "index.html").read_text()
episode_template = (this_dir / "episode.html").read_text()
last_user_updated_date = None

for ep in episodes:
ep["episode_title"] = html.escape(ep["episode_title"])
ep[DESCRIPTION] = _fix_unclosed_html_tags(
_convert_urls_to_links(ep[DESCRIPTION]),
)
user_date = ep["userUpdatedDate"].split("T")[0]
if last_user_updated_date != user_date:
user_date = ep[date_field].split("T")[0] if ep.get(date_field) else ""
if last_user_updated_date != user_date and user_date:
page_vars["episodes"] += (
"<h1><script>document.write("
f'new Date("{ep["userUpdatedDate"]}").toLocaleDateString()'
f'new Date("{ep[date_field]}").toLocaleDateString()'
")</script></h1><hr />"
)
last_user_updated_date = user_date
if ep["starred"] == "1":

if ep.get("starred") == "1":
ep["starred"] = "⭐&nbsp;&nbsp;"
else:
ep["starred"] = ""

try:
page_vars["episodes"] += episode_template.format_map(ep)
except KeyError as e:
print(f"Error formatting episode: KeyError {e}")
print(ep)
html_output_path.write_text(page_template.format_map(page_vars))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This refactored helper function has a few issues that will cause it to fail at runtime:

  1. TypeError: The function is called from generate_html_deleted with a show_starred_icon argument, but the function signature doesn't accept it.
  2. KeyError: For any non-starred episode, ep["starred"] is never initialized. This will cause a KeyError when the episode.html template is formatted, as it expects a {starred} key. An else block is needed to set ep["starred"] = "" for these cases.
  3. Incorrect comparison: The check ep.get("starred") == "1" is likely incorrect. The value from the database for a boolean/integer column is typically an integer (0 or 1), not a string. The comparison should be against an integer.

Here is a single suggestion that fixes all three issues by updating the function signature and logic.

def _generate_html_episodes(
    episodes: list[dict[str, str]],
    title: str,
    html_output_path: Path,
    date_field: str = "userUpdatedDate",
    show_starred_icon: bool = True,
) -> None:
    """Generate HTML for any list of episodes."""
    this_dir = Path(__file__).parent
    page_vars = {
        "title": title,
        "style": Path(this_dir / "mvp.css").read_text(),
        "script": Path(this_dir / "search.js").read_text(),
        "episodes": "",
    }
    page_template = (this_dir / "index.html").read_text()
    episode_template = (this_dir / "episode.html").read_text()
    last_user_updated_date = None

    for ep in episodes:
        ep["episode_title"] = html.escape(ep["episode_title"])
        ep[DESCRIPTION] = _fix_unclosed_html_tags(
            _convert_urls_to_links(ep[DESCRIPTION]),
        )
        user_date = ep[date_field].split("T")[0] if ep.get(date_field) else ""
        if last_user_updated_date != user_date and user_date:
            page_vars["episodes"] += (
                "<h1><script>document.write("
                f'new Date("{ep[date_field]}").toLocaleDateString()'
                ")</script></h1><hr />"
            )
            last_user_updated_date = user_date

        if show_starred_icon and ep.get("starred") == 1:
            ep["starred"] = "⭐&nbsp;&nbsp;"
        else:
            ep["starred"] = ""

        try:
            page_vars["episodes"] += episode_template.format_map(ep)
        except KeyError as e:
            print(f"Error formatting episode: KeyError {e}")
            print(ep)
    html_output_path.write_text(page_template.format_map(page_vars))

)

results = self.db.execute(query).fetchall()
return self._process_query_results(results, fields)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the project's Python style guide, you should prefer to use named arguments when calling a method with more than one argument.1 This call uses positional arguments.

This also applies to the other calls to _process_query_results in this file (lines 542 and 561).

return self._process_query_results(results=results, fields=fields)

Style Guide References

Footnotes

  1. CLAUDE.md, line 8

def generate_html_played(db_path: str, html_output_path: Path) -> None:
db = Datastore(db_path)
episodes = db.get_recently_played()
_generate_html_episodes(episodes, "Recently Played", html_output_path)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

According to the project's Python style guide, you should prefer to use named arguments when calling a method with more than one argument.1 This call uses positional arguments.

This also applies to the other calls to _generate_html_episodes in this file.

Style Guide References

Suggested change
_generate_html_episodes(episodes, "Recently Played", html_output_path)
_generate_html_episodes(episodes=episodes, title="Recently Played", html_output_path=html_output_path)

Footnotes

  1. CLAUDE.md, line 8

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (5)
overcast_to_sqlite/datastore.py (4)

467-467: Fix line length violation.

Line exceeds the 88 character limit.

Apply this diff to fix:

-f"coalesce({EPISODES_EXTENDED}.description, 'No description') as description",
+f"coalesce({EPISODES_EXTENDED}.description, "
+f"'No description') as description",

511-514: Use iterable unpacking instead of concatenation.

Replace list concatenation with unpacking for better performance and readability.

Apply this diff:

-fields = base_fields + [
-    f"{USER_UPDATED_DATE}",
-    f"CASE WHEN {USER_REC_DATE} IS NOT NULL THEN 1 ELSE 0 END AS starred",
-]
+fields = [
+    *base_fields,
+    f"{USER_UPDATED_DATE}",
+    f"CASE WHEN {USER_REC_DATE} IS NOT NULL THEN 1 ELSE 0 END AS starred",
+]

530-533: Use iterable unpacking instead of concatenation.

Apply this diff:

-fields = base_fields + [
-    f"{USER_REC_DATE} as userRecDate",
-    "1 as starred",
-]
+fields = [
+    *base_fields,
+    f"{USER_REC_DATE} as userRecDate",
+    "1 as starred",
+]

549-552: Use iterable unpacking instead of concatenation.

Apply this diff:

-fields = base_fields + [
-    f"{USER_UPDATED_DATE}",
-    "0 as starred",
-]
+fields = [
+    *base_fields,
+    f"{USER_UPDATED_DATE}",
+    "0 as starred",
+]
overcast_to_sqlite/html/index.html (1)

18-22: Consider moving inline styles to the stylesheet.

For better maintainability and consistency, consider moving the inline styles to the CSS section.

Move these styles to the <style> section:

nav {
    text-align: center;
    padding: 1rem;
    border-bottom: 1px solid var(--border);
    margin-bottom: 1rem;
}

nav a {
    margin: 0 1rem;
}

Then simplify the HTML:

-<nav style="text-align: center; padding: 1rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem;">
-    <a href="overcast-played.html" style="margin: 0 1rem;">Recently Played</a>
-    <a href="overcast-starred.html" style="margin: 0 1rem;">Starred Episodes</a>
-    <a href="overcast-deleted.html" style="margin: 0 1rem;">Deleted Episodes</a>
+<nav>
+    <a href="overcast-played.html">Recently Played</a>
+    <a href="overcast-starred.html">Starred Episodes</a>
+    <a href="overcast-deleted.html">Deleted Episodes</a>
</nav>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a47395d and 31b6db3.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • .claude/settings.local.json (1 hunks)
  • .gitignore (1 hunks)
  • CLAUDE.md (1 hunks)
  • overcast_to_sqlite/cli.py (3 hunks)
  • overcast_to_sqlite/datastore.py (9 hunks)
  • overcast_to_sqlite/html/index.html (1 hunks)
  • overcast_to_sqlite/html/page.py (1 hunks)
  • pyproject.toml (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
overcast_to_sqlite/cli.py (2)
overcast_to_sqlite/html/page.py (3)
  • generate_html_deleted (103-111)
  • generate_html_played (86-89)
  • generate_html_starred (92-100)
overcast_to_sqlite/datastore.py (1)
  • cleanup_old_episodes (563-593)
overcast_to_sqlite/html/page.py (2)
overcast_to_sqlite/cli.py (1)
  • html (303-328)
overcast_to_sqlite/datastore.py (3)
  • get_recently_played (506-523)
  • get_starred_episodes (525-542)
  • get_deleted_episodes (544-561)
🪛 LanguageTool
CLAUDE.md

[grammar] ~4-~4: Ensure spelling is correct
Context: ... use f-string over string formatting or concatentation (except in logging strings) - Use async...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 GitHub Actions: Lint
overcast_to_sqlite/datastore.py

[error] 216-216: Ruff FURB162: Unnecessary timezone replacement with zero offset. Remove .replace() call.


[error] 467-467: Ruff E501: Line too long (90 > 88).


[error] 511-514: Ruff RUF005: Consider iterable unpacking instead of concatenation. Replace with iterable unpacking.


[error] 530-533: Ruff RUF005: Consider iterable unpacking instead of concatenation. Replace with iterable unpacking.


[error] 549-552: Ruff RUF005: Consider iterable unpacking instead of concatenation. Replace with iterable unpacking.

🔇 Additional comments (9)
pyproject.toml (1)

51-53: LGTM!

The development dependency updates are appropriate and help keep the tooling current with the latest bug fixes and improvements.

.gitignore (1)

5-5: LGTM!

Good generalization of the ignore pattern to accommodate multiple generated HTML files.

overcast_to_sqlite/datastore.py (2)

267-267: Good fix for SQL ambiguity.

Making the GROUP BY clause explicit with the table name prevents potential ambiguity issues.


407-428: Verify safety of duplicate URL deduplication

Our search didn’t find any tests or additional documentation covering the deduplication step in _clean_enclosure_urls, so there’s a risk of unintended data loss if distinct episodes differ only by query parameters.

• Location: overcast_to_sqlite/datastore.py (lines 407–428)
• No tests found for duplicate‐URL handling
• No comments explaining why multiple query‐string variants might legitimately coexist

Please manually confirm that:

  • No valid episodes rely on unique query parameters for differentiation, or
  • New test cases are added to cover any edge cases in _clean_enclosure_urls.
overcast_to_sqlite/cli.py (2)

11-15: LGTM! Clean import organization.

The new HTML generation function imports are properly organized and alphabetically sorted.


123-123: LGTM! Proper integration of cleanup logic.

The cleanup call is appropriately placed after feed processing and has built-in safety checks in the datastore implementation.

overcast_to_sqlite/html/page.py (3)

43-84: LGTM! Well-structured helper function with good error handling.

The refactored helper function properly consolidates the HTML generation logic with:

  • Safe field access using .get() methods
  • Proper HTML escaping and tag fixing
  • Flexible date field handling
  • Good error handling for template formatting

86-89: LGTM! Clean refactor using the new helper function.

The function is properly simplified while maintaining the same functionality.


92-100: LGTM! Correct implementation for starred episodes.

The function properly uses "userRecDate" as the date field, which is appropriate for starred episodes.

Comment on lines +1 to +8
{
"permissions": {
"allow": [
"Bash(find:*)"
],
"deny": []
}
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Local-only config should not live in VCS

settings.local.json implies host-specific overrides. Committing it will (1) leak personal tooling preferences, (2) force-merge noisy changes from every developer, and (3) risk exposing permissive command scopes in CI.
Move this file to your local workspace and add a tracked template (e.g. settings.example.json) instead.

+# .gitignore
+/.claude/settings.local.json

🏁 Script executed:

#!/bin/bash
# Check if settings.local.json is tracked in the repo
echo "Tracked status for .claude/settings.local.json:"
git ls-files .claude/settings.local.json || echo "Not tracked"

# Check if .claude/settings.local.json is ignored in .gitignore
echo "Ignore entries for .claude/settings.local.json in .gitignore:"
grep -R "\.claude/settings.local.json" -n .gitignore || echo "No ignore entry found"

Length of output: 416


Remove local-only config from version control

.claude/settings.local.json is currently tracked and not ignored. Local overrides shouldn’t live in VCS—they can leak personal tooling prefs, generate noisy merge conflicts, and expose overly broad permissions in CI. Please:

• Untrack the file and add it to .gitignore

# .gitignore
+/.claude/settings.local.json
git rm --cached .claude/settings.local.json
git commit -m "Remove local settings from VCS and ignore file"

• Add a tracked template for defaults, e.g. .claude/settings.example.json, with placeholder permissions
• Update docs (e.g., README) to instruct devs to copy the template to settings.local.json and customize locally

🤖 Prompt for AI Agents
In .claude/settings.local.json lines 1 to 8, the local configuration file is
currently tracked in version control, which risks leaking personal settings and
causing merge conflicts. To fix this, untrack the file using 'git rm --cached
.claude/settings.local.json' and commit the change, then add the file to
.gitignore to prevent future tracking. Create a new tracked template file named
.claude/settings.example.json with placeholder permissions for defaults.
Finally, update the project documentation to instruct developers to copy the
example file to settings.local.json and customize their local settings
accordingly.

Comment on lines +3 to +5
"allow": [
"Bash(find:*)"
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

"Bash(find:*)" is overly broad—tighten the permission

Allowing every possible find invocation lets prompt users traverse the entire repo or disk and chain arbitrary flags (-exec, -delete, etc.). That’s a sizable escalation surface.

Explicitly enumerate the safe sub-commands or restrict flags, e.g.:

-      "Bash(find:*)"
+      "Bash(find . -type f -name '*.md')"

or place a complementary deny rule blocking -exec, -delete, -execdir, etc.

🤖 Prompt for AI Agents
In .claude/settings.local.json around lines 3 to 5, the permission
"Bash(find:*)" is too broad and allows unsafe usage of the find command with any
flags. To fix this, replace the wildcard with an explicit list of safe find
sub-commands or add deny rules to block dangerous flags like -exec, -delete, and
-execdir. This will restrict the allowed find invocations to a safer subset and
reduce security risks.

## Python Practices
- Always use or add type hints
- Prefer @dataclasses where applicable
- Always use f-string over string formatting or concatentation (except in logging strings)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix typo in coding guidelines.

There's a spelling error: "concatentation" should be "concatenation".

Apply this diff to fix the typo:

-- Always use f-string over string formatting or concatentation (except in logging strings)
+- Always use f-string over string formatting or concatenation (except in logging strings)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- Always use f-string over string formatting or concatentation (except in logging strings)
- Always use f-string over string formatting or concatenation (except in logging strings)
🧰 Tools
🪛 LanguageTool

[grammar] ~4-~4: Ensure spelling is correct
Context: ... use f-string over string formatting or concatentation (except in logging strings) - Use async...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🤖 Prompt for AI Agents
In CLAUDE.md at line 4, correct the spelling mistake by changing
"concatentation" to "concatenation" in the coding guidelines to fix the typo.

Comment on lines +307 to +328
"""Generate HTML pages for recently played, starred, and deleted episodes."""
if output_path:
if Path(output_path).is_dir():
html_output_path = Path(output_path) / "overcast-played.html"
else:
html_output_path = Path(output_path)
output_dir = (
Path(output_path)
if Path(output_path).is_dir()
else Path(output_path).parent
)
else:
html_output_path = Path(db_path).parent / "overcast-played.html"
generate_html_played(db_path, html_output_path)
print(f"📝Saved HTML to: file://{html_output_path.absolute()}")
output_dir = Path(db_path).parent

played_path = output_dir / "overcast-played.html"
starred_path = output_dir / "overcast-starred.html"
deleted_path = output_dir / "overcast-deleted.html"

generate_html_played(db_path, played_path)
generate_html_starred(db_path, starred_path)
generate_html_deleted(db_path, deleted_path)

print("📝Saved HTML files to:")
print(f" Recently Played: file://{played_path.absolute()}")
print(f" Starred Episodes: file://{starred_path.absolute()}")
print(f" Deleted Episodes: file://{deleted_path.absolute()}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Improve path handling logic for better robustness.

The path handling logic has potential issues:

  1. Path(output_path).is_dir() returns False if the path doesn't exist, even if it's intended to be a directory
  2. The fallback to .parent assumes non-directories are files, which may not always be correct

Consider this more robust approach:

-    if output_path:
-        output_dir = (
-            Path(output_path)
-            if Path(output_path).is_dir()
-            else Path(output_path).parent
-        )
-    else:
-        output_dir = Path(db_path).parent
+    if output_path:
+        output_path_obj = Path(output_path)
+        if output_path_obj.exists() and output_path_obj.is_dir():
+            output_dir = output_path_obj
+        elif output_path_obj.suffix:  # Has file extension, treat as file
+            output_dir = output_path_obj.parent
+        else:  # No extension, treat as directory and create if needed
+            output_dir = output_path_obj
+            output_dir.mkdir(parents=True, exist_ok=True)
+    else:
+        output_dir = Path(db_path).parent

This handles non-existent paths more reliably and creates directories as needed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""Generate HTML pages for recently played, starred, and deleted episodes."""
if output_path:
if Path(output_path).is_dir():
html_output_path = Path(output_path) / "overcast-played.html"
else:
html_output_path = Path(output_path)
output_dir = (
Path(output_path)
if Path(output_path).is_dir()
else Path(output_path).parent
)
else:
html_output_path = Path(db_path).parent / "overcast-played.html"
generate_html_played(db_path, html_output_path)
print(f"📝Saved HTML to: file://{html_output_path.absolute()}")
output_dir = Path(db_path).parent
played_path = output_dir / "overcast-played.html"
starred_path = output_dir / "overcast-starred.html"
deleted_path = output_dir / "overcast-deleted.html"
generate_html_played(db_path, played_path)
generate_html_starred(db_path, starred_path)
generate_html_deleted(db_path, deleted_path)
print("📝Saved HTML files to:")
print(f" Recently Played: file://{played_path.absolute()}")
print(f" Starred Episodes: file://{starred_path.absolute()}")
print(f" Deleted Episodes: file://{deleted_path.absolute()}")
"""Generate HTML pages for recently played, starred, and deleted episodes."""
if output_path:
output_path_obj = Path(output_path)
if output_path_obj.exists() and output_path_obj.is_dir():
output_dir = output_path_obj
elif output_path_obj.suffix: # Has file extension, treat as file
output_dir = output_path_obj.parent
else: # No extension, treat as directory and create if needed
output_dir = output_path_obj
output_dir.mkdir(parents=True, exist_ok=True)
else:
output_dir = Path(db_path).parent
played_path = output_dir / "overcast-played.html"
starred_path = output_dir / "overcast-starred.html"
deleted_path = output_dir / "overcast-deleted.html"
generate_html_played(db_path, played_path)
generate_html_starred(db_path, starred_path)
generate_html_deleted(db_path, deleted_path)
print("📝Saved HTML files to:")
print(f" Recently Played: file://{played_path.absolute()}")
print(f" Starred Episodes: file://{starred_path.absolute()}")
print(f" Deleted Episodes: file://{deleted_path.absolute()}")
🤖 Prompt for AI Agents
In overcast_to_sqlite/cli.py around lines 307 to 328, the current path handling
logic incorrectly assumes that if output_path does not exist or is not a
directory, it must be a file and uses its parent directory, which can cause
errors. To fix this, check if output_path exists and is a directory; if not,
treat output_path as a directory path and create it if necessary before
proceeding. This ensures the output directory is correctly identified or
created, improving robustness.

Comment on lines +202 to +203
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect walrus operator usage.

The current code compares the assignment result with is not None rather than the assigned value. This will always evaluate to True when os.getenv() returns any value.

Apply this diff to fix the logic:

-if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
+if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)
if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
limit_days = int(env_limit)
🤖 Prompt for AI Agents
In overcast_to_sqlite/datastore.py around lines 202 to 203, the walrus operator
is used incorrectly by combining assignment and a comparison with 'is not None',
causing the condition to always be true when a value is returned. Fix this by
first assigning the result of os.getenv("OVERCAST_LIMIT_DAYS") to a variable
using the walrus operator, then checking if that variable is not None before
converting it to int and assigning to limit_days.

Comment on lines +215 to +218
and datetime.datetime.fromisoformat(
episode[USER_UPDATED_DATE].replace("Z", "+00:00"),
)
>= cutoff_date
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify timezone handling.

The manual replacement of 'Z' with '+00:00' is unnecessary when using fromisoformat in Python 3.11+, which supports the 'Z' suffix directly.

Apply this diff to simplify:

-datetime.datetime.fromisoformat(
-    episode[USER_UPDATED_DATE].replace("Z", "+00:00"),
-)
+datetime.datetime.fromisoformat(episode[USER_UPDATED_DATE])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
and datetime.datetime.fromisoformat(
episode[USER_UPDATED_DATE].replace("Z", "+00:00"),
)
>= cutoff_date
and datetime.datetime.fromisoformat(episode[USER_UPDATED_DATE])
>= cutoff_date
🧰 Tools
🪛 GitHub Actions: Lint

[error] 216-216: Ruff FURB162: Unnecessary timezone replacement with zero offset. Remove .replace() call.

🤖 Prompt for AI Agents
In overcast_to_sqlite/datastore.py around lines 215 to 218, the code manually
replaces the 'Z' suffix with '+00:00' before calling
datetime.datetime.fromisoformat, which is unnecessary in Python 3.11 and later.
Remove the .replace("Z", "+00:00") call and pass the ISO string with 'Z'
directly to fromisoformat to simplify the timezone handling.

Comment on lines +570 to +571
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix incorrect walrus operator usage.

Same issue as earlier - the parentheses are needed around the assignment.

Apply this diff:

-if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
+if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if env_limit := os.getenv("OVERCAST_LIMIT_DAYS") is not None:
limit_days = int(env_limit)
if (env_limit := os.getenv("OVERCAST_LIMIT_DAYS")) is not None:
limit_days = int(env_limit)
🤖 Prompt for AI Agents
In overcast_to_sqlite/datastore.py around lines 570 to 571, the walrus operator
is used incorrectly without parentheses, causing the assignment to not work as
intended. Add parentheses around the assignment expression so that env_limit is
assigned the result of os.getenv("OVERCAST_LIMIT_DAYS") before checking if it is
not None. This will correctly assign and check the environment variable in one
step.

Comment on lines +103 to +111
def generate_html_deleted(db_path: str, html_output_path: Path) -> None:
db = Datastore(db_path)
episodes = db.get_deleted_episodes()
_generate_html_episodes(
episodes,
"Deleted Episodes",
html_output_path,
show_starred_icon=False,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix the unused parameter causing TypeError.

The show_starred_icon=False parameter is passed to _generate_html_episodes, but this parameter is not defined in the helper function's signature, causing a TypeError at runtime.

Either remove the unused parameter or add it to the helper function:

Option 1: Remove the parameter (simpler)

-    _generate_html_episodes(
-        episodes,
-        "Deleted Episodes",
-        html_output_path,
-        show_starred_icon=False,
-    )
+    _generate_html_episodes(
+        episodes,
+        "Deleted Episodes",
+        html_output_path,
+    )

Option 2: Add parameter support to the helper function

 def _generate_html_episodes(
     episodes: list[dict[str, str]],
     title: str,
     html_output_path: Path,
     date_field: str = "userUpdatedDate",
+    show_starred_icon: bool = True,
 ) -> None:

Then modify the starred icon logic on Line 75:

-        if ep.get("starred") == "1":
+        if show_starred_icon and ep.get("starred") == "1":
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def generate_html_deleted(db_path: str, html_output_path: Path) -> None:
db = Datastore(db_path)
episodes = db.get_deleted_episodes()
_generate_html_episodes(
episodes,
"Deleted Episodes",
html_output_path,
show_starred_icon=False,
)
def generate_html_deleted(db_path: str, html_output_path: Path) -> None:
db = Datastore(db_path)
episodes = db.get_deleted_episodes()
_generate_html_episodes(
episodes,
"Deleted Episodes",
html_output_path,
)
🤖 Prompt for AI Agents
In overcast_to_sqlite/html/page.py around lines 103 to 111, the call to
_generate_html_episodes includes the parameter show_starred_icon=False, but this
parameter is not defined in the helper function's signature, causing a
TypeError. To fix this, either remove the show_starred_icon parameter from the
call if it is not needed, or add show_starred_icon as a parameter to the
_generate_html_episodes function definition and update its logic accordingly,
especially around line 75 where starred icon handling occurs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants