Sub-plan 5 of the audiobooks absorption. Ports the plugin's
podcastfeed.Refresher into internal/audiobooks/podcastfeed and
registers it as a 10-minute scheduled task in silo's task manager.
For each row in podcast_feeds (sub-plan 1 mig 140), the refresher:
- Fetches the RSS feed with a 30-second HTTP timeout
- Parses entries via gofeed (github.com/mmcdole/gofeed)
- UPSERTs new entries into episodes (joined to the podcast's
media_items row via podcast_feeds.media_item_id = series_id)
- Updates podcast_feeds.last_refreshed_at + last_refresh_error
Migration 144 adds podcast_guid and podcast_audio_url columns to
episodes, plus a partial unique index on (series_id, podcast_guid)
WHERE podcast_guid IS NOT NULL so the refresher can ON CONFLICT upsert
without touching non-podcast episode rows.
Enclosure file download is deferred; this stage upserts episode
metadata only. Existing content_ids are reused when a feed re-emits a
known guid so per-user progress rows stay bound to the same FK target.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
55 lines
2.0 KiB
Go
55 lines
2.0 KiB
Go
package tasks
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/Silo-Server/silo-server/internal/audiobooks/podcastfeed"
|
|
"github.com/Silo-Server/silo-server/internal/taskmanager"
|
|
)
|
|
|
|
// SyncPodcastFeedsTask refreshes RSS podcast feeds that are due for a poll.
|
|
// It wraps podcastfeed.Refresher so the task manager can invoke it on a
|
|
// schedule, report progress, and display it in the admin task panel.
|
|
type SyncPodcastFeedsTask struct {
|
|
refresher *podcastfeed.Refresher
|
|
store podcastfeed.Store
|
|
}
|
|
|
|
// NewSyncPodcastFeedsTask constructs the task. refresher and store come
|
|
// from the audiobooks subsystem wired in cmd/silo/main.go.
|
|
func NewSyncPodcastFeedsTask(refresher *podcastfeed.Refresher, store podcastfeed.Store) *SyncPodcastFeedsTask {
|
|
return &SyncPodcastFeedsTask{refresher: refresher, store: store}
|
|
}
|
|
|
|
func (t *SyncPodcastFeedsTask) Key() string { return "sync_podcast_feeds" }
|
|
func (t *SyncPodcastFeedsTask) Name() string { return "Sync Podcast Feeds" }
|
|
func (t *SyncPodcastFeedsTask) Description() string {
|
|
return "Refreshes RSS podcast feeds that are due for polling and upserts new episodes"
|
|
}
|
|
func (t *SyncPodcastFeedsTask) Category() taskmanager.TaskCategory {
|
|
return taskmanager.TaskCategoryLibrary
|
|
}
|
|
func (t *SyncPodcastFeedsTask) IsHidden() bool { return false }
|
|
|
|
func (t *SyncPodcastFeedsTask) DefaultTriggers() []taskmanager.TriggerConfig {
|
|
return []taskmanager.TriggerConfig{
|
|
{Type: taskmanager.TriggerTypeInterval, IntervalMs: 10 * 60 * 1000}, // every 10 minutes
|
|
}
|
|
}
|
|
|
|
func (t *SyncPodcastFeedsTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
|
|
progress.Report(0, "Checking for due podcast feeds")
|
|
|
|
attempted, err := t.refresher.RefreshDue(ctx, t.store)
|
|
if err != nil {
|
|
return fmt.Errorf("podcast feed refresh: %w", err)
|
|
}
|
|
|
|
result, _ := json.Marshal(map[string]int{"feeds_attempted": attempted})
|
|
progress.SetResultData(result)
|
|
progress.Report(100, fmt.Sprintf("Podcast feed sync complete (%d feeds attempted)", attempted))
|
|
return nil
|
|
}
|