feat: add historical backfill with --init CLI and episode numbering

Adds a --init mode that seeds the database with past shows from a given
anchor episode/date forward, batch-fetching likes from SoundCloud and
partitioning them into weekly buckets. Episode numbers are tracked in
the shows table and auto-incremented by the poller for new shows.

Includes full API documentation (docs/api.md) and updated README.

Made-with: Cursor
This commit is contained in:
cottongin
2026-03-12 02:09:15 -04:00
parent c88826ac4d
commit cb3ae403cf
14 changed files with 922 additions and 21 deletions

View File

@@ -93,6 +93,89 @@ async def test_poll_once_removes_unliked_tracks():
assert call_args[0][1] == [1]
@pytest.mark.asyncio
async def test_poll_once_auto_assigns_episode_number():
mock_sc = AsyncMock()
mock_sc.resolve_user.return_value = 206979918
mock_sc.fetch_likes.return_value = [
_make_track(1, "2026-03-14T01:00:00+00:00"),
]
mock_db = MagicMock()
mock_show = MagicMock()
mock_show.id = 5
mock_show.episode_number = None
mock_db.get_or_create_show.return_value = mock_show
mock_db.get_latest_episode_number.return_value = 530
poller = Poller(
db=mock_db,
soundcloud=mock_sc,
soundcloud_user="nicktherat",
show_day=2,
show_hour=22,
poll_interval=3600,
)
await poller.poll_once()
mock_db.update_show_episode_number.assert_called_once_with(5, 531)
@pytest.mark.asyncio
async def test_poll_once_skips_numbering_when_no_history():
mock_sc = AsyncMock()
mock_sc.resolve_user.return_value = 206979918
mock_sc.fetch_likes.return_value = []
mock_db = MagicMock()
mock_show = MagicMock()
mock_show.id = 1
mock_show.episode_number = None
mock_db.get_or_create_show.return_value = mock_show
mock_db.get_latest_episode_number.return_value = None
poller = Poller(
db=mock_db,
soundcloud=mock_sc,
soundcloud_user="nicktherat",
show_day=2,
show_hour=22,
poll_interval=3600,
)
await poller.poll_once()
mock_db.update_show_episode_number.assert_not_called()
@pytest.mark.asyncio
async def test_poll_once_skips_numbering_when_already_assigned():
mock_sc = AsyncMock()
mock_sc.resolve_user.return_value = 206979918
mock_sc.fetch_likes.return_value = []
mock_db = MagicMock()
mock_show = MagicMock()
mock_show.id = 1
mock_show.episode_number = 530
mock_db.get_or_create_show.return_value = mock_show
poller = Poller(
db=mock_db,
soundcloud=mock_sc,
soundcloud_user="nicktherat",
show_day=2,
show_hour=22,
poll_interval=3600,
)
await poller.poll_once()
mock_db.get_latest_episode_number.assert_not_called()
mock_db.update_show_episode_number.assert_not_called()
@pytest.mark.asyncio
async def test_poll_once_full_refresh():
mock_sc = AsyncMock()