Skip to content

feat: per-plugin download trigger in the sidebar UI #32

@jschloman

Description

@jschloman

Overview

Each source plugin's sidebar expander should contain a button that lets the user trigger the data collection phase without leaving the app or opening a terminal. This bridges the gap between the documented two-phase model (download → display) and the actual user experience, where right now the download step is invisible inside the UI.

Motivation

The architecture doc (#31) clearly separates collection from display, but the current sidebar only surfaces the display side (a file path selector). A first-time user who has never run a sync script has no obvious path forward from inside the app. A download button inside each plugin's expander makes the collection phase a first-class UI action while preserving the architectural boundary — the button triggers the CLI script; load() still reads only from the resulting local file.

Proposed Design

SourcePlugin ABC changes

Add two new optional methods to SourcePlugin:

def can_download(self) -> bool:
    """Return True if this plugin supports in-app triggered downloads.

    Plugins that only accept manual exports (e.g. a ZIP from a website)
    should return False. Plugins with a CLI sync script should return True.
    """
    return False

def download(self, config: dict[str, Any]) -> None:
    """Run the collection phase for this plugin.

    Called by the sidebar when the user clicks the download button.
    This is the ONLY place in the plugin that may make network calls
    or use credentials. It must write its output to the path(s) declared
    in get_config_fields() so that a subsequent load() call succeeds.

    Args:
        config: Same config dict passed to load(), giving access to the
                output path where data should be written.

    Raises:
        RuntimeError: With a user-readable message if the download fails
                      (missing credentials, network error, etc.).
    """
    raise NotImplementedError

Plugins that support automated download override both methods. Plugins backed by a manual website export (e.g. Goodreads, Netflix) leave can_download() returning False and show instructional text in place of the button.

Sidebar UI changes

Inside each plugin's st.sidebar.expander, after the file path selector(s):

  • If plugin.can_download() is True: show a "Download latest data" button. On click, call plugin.download(config) in a st.spinner, catch RuntimeError and surface it as st.error, refresh on success.
  • If plugin.can_download() is False: show a st.caption with a short instruction linking to the source's export page (sourced from a new EXPORT_INSTRUCTIONS class attribute).
┌─ 🎧 Last.fm Music History ──────────────────────────────────┐
│  Last.fm CSV file                                            │
│  [G:/My Drive/Projects/Last.fm/tracks.csv         ] [...]   │
│                                                              │
│  [↓ Download latest data]                                    │
└──────────────────────────────────────────────────────────────┘

┌─ 📺 Netflix ─────────────────────────────────────────────────┐
│  Viewing history CSV                                         │
│  [                                                ] [...]   │
│                                                              │
│  ℹ Export at netflix.com → Account → Viewing Activity        │
└──────────────────────────────────────────────────────────────┘

SourcePlugin attribute additions

EXPORT_INSTRUCTIONS: str = ""
# Human-readable sentence shown when can_download() is False.
# Example: "Export at letterboxd.com → Settings → Data Export → Download ZIP"

Acceptance Criteria

  • can_download() and download() added to SourcePlugin ABC as optional overrides (default False / NotImplementedError)
  • EXPORT_INSTRUCTIONS class attribute added with empty-string default
  • Sidebar renders a download button for plugins where can_download() is True
  • Sidebar renders st.caption(EXPORT_INSTRUCTIONS) for plugins where can_download() is False (and instructions are non-empty)
  • download() is called inside st.spinner; RuntimeError surfaces as st.error; success triggers st.rerun() so the file path selector reflects the new file
  • download() implementations write credentials from env vars (AUTOBIO_*), never from hardcoded values
  • LastFmPlugin: can_download() returns True; download() delegates to the existing autobiographer.py fetch pipeline; AUTOBIO_LASTFM_API_KEY / AUTOBIO_LASTFM_USERNAME required
  • SwarmPlugin: can_download() returns False; EXPORT_INSTRUCTIONS links to the Foursquare data export page
  • Unit tests: can_download() contract for both built-in plugins; download() called correctly when button is clicked (mock the underlying fetch); instructions rendered when can_download() is False
  • Full quality gate passes (ruff, mypy, pytest --cov ≥ 80%)
  • Architecture contract maintained: load() still contains zero network calls after this change

Out of Scope

  • Progress reporting within a long-running download (follow-up)
  • Scheduling / auto-refresh (follow-up)
  • Download for plugins not yet implemented (each plugin issue handles its own)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions