Repository: mxpv/podsync

Issue:
Disable cleanup for all feeds?
Right now, you have to specify clean for all feeds, a global setting would be nice.

Also, does (and if so, document it) `keep_last = 0` work, or do I have to do the '99999' way?

Discussion:
@bw4518: I would like an answer to this as well. I am testing with `keep_last = 0` and it doesn't seem to be cleaning the stuff out.@mxpv: you can now specify global clean policies.
pls give it a try in our nightly builds.

Solution patch:
diff --git a/CLAUDE.md b/CLAUDE.md
index a40136b..9308e6b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -96,13 +96,22 @@ The application uses TOML configuration files. See `config.toml.example` for all
 - The project uses golangci-lint with strict formatting rules - code must pass ALL checks
 
 ### Git Workflow
-- Write short, expressive commit messages that clearly describe the change
+- Keep commit messages brief and to the point
+- Use a short, descriptive commit title (50 characters or less)
+- Include a brief commit body that summarizes changes in 1-3 sentences when needed
 - Do not include automated signatures or generation notices in commit messages or pull requests
 - Don't add "Generated with Claude Code" to commit messages or pull request descriptions
 - Don't add "Co-Authored-By: Claude noreply@anthropic.com" to commit messages or pull request descriptions
 - Keep commits focused and atomic - one logical change per commit
 - Ensure the build passes before pushing commits
 
+### Pull Request Guidelines
+- Keep PR descriptions concise and focused
+- Include the brief commit body summary plus relevant examples if applicable
+- Avoid verbose sections like "Changes Made", "Test Plan", or extensive bullet lists
+- Focus on what the change does and why, not exhaustive implementation details
+- Include code examples only when they help demonstrate usage or key functionality
+
 ## Key Conventions
 
 - Configuration validation happens at startup
diff --git a/cmd/podsync/config.go b/cmd/podsync/config.go
index 46f9d9f..b972347 100644
--- a/cmd/podsync/config.go
+++ b/cmd/podsync/config.go
@@ -35,6 +35,8 @@ type Config struct {
 	Tokens map[model.Provider]StringSlice `toml:"tokens"`
 	// Downloader (youtube-dl) configuration
 	Downloader ytdl.Config `toml:"downloader"`
+	// Global cleanup policy applied to feeds that don't specify their own cleanup policy
+	Cleanup *feed.Cleanup `toml:"cleanup"`
 }
 
 type Log struct {
@@ -179,6 +181,11 @@ func (c *Config) applyDefaults(configPath string) {
 		if _feed.PlaylistSort == "" {
 			_feed.PlaylistSort = model.SortingAsc
 		}
+
+		// Apply global cleanup policy if feed doesn't have its own
+		if _feed.Clean == nil && c.Cleanup != nil {
+			_feed.Clean = c.Cleanup
+		}
 	}
 }
 
diff --git a/config.toml.example b/config.toml.example
index 24874a1..611a8c4 100644
--- a/config.toml.example
+++ b/config.toml.example
@@ -1,5 +1,11 @@
 # This is an example of TOML configuration file for Podsync.
 
+# Global cleanup policy applied to feeds that don't specify their own cleanup policy.
+# When set, this policy is used as a fallback for all feeds.
+# Comment out or remove this section if you don't want a global cleanup policy.
+[cleanup]
+keep_last = 50  # Keep last 50 episodes globally (unless overridden per feed)
+
 # Web server related configuration.
 [server]
 # HTTP server port.
@@ -75,8 +81,9 @@ vimeo = [ # Multiple keys will be rotated.
   # If set then overwrite 'update_period'.
   cron_schedule = "@every 12h"
 
-  # Whether to cleanup old episodes.
+  # Whether to cleanup old episodes for this specific feed.
   # Keep last 10 episodes (order desc by PubDate)
+  # This overrides the global cleanup policy if one is set.
   clean = { keep_last = 10 }
 
   # Optional Golang regexp format.
diff --git a/pkg/feed/config.go b/pkg/feed/config.go
index 30c943d..379b1ea 100644
--- a/pkg/feed/config.go
+++ b/pkg/feed/config.go
@@ -33,7 +33,7 @@ type Config struct {
 	// Only download episodes that match the filters (defaults to matching anything)
 	Filters Filters `toml:"filters"`
 	// Clean is a cleanup policy to use for this feed
-	Clean Cleanup `toml:"clean"`
+	Clean *Cleanup `toml:"clean"`
 	// Custom is a list of feed customizations
 	Custom Custom `toml:"custom"`
 	// List of additional youtube-dl arguments passed at download time
diff --git a/services/update/updater.go b/services/update/updater.go
index efb89c2..fbe9432 100644
--- a/services/update/updater.go
+++ b/services/update/updater.go
@@ -327,11 +327,16 @@ func (u *Manager) cleanup(ctx context.Context, feedConfig *feed.Config) error {
 	var (
 		feedID = feedConfig.ID
 		logger = log.WithField("feed_id", feedID)
-		count  = feedConfig.Clean.KeepLast
 		list   []*model.Episode
 		result *multierror.Error
 	)
 
+	if feedConfig.Clean == nil {
+		logger.Debug("no cleanup policy configured")
+		return nil
+	}
+
+	count := feedConfig.Clean.KeepLast
 	if count < 1 {
 		logger.Info("nothing to clean")
 		return nil



Test patch:
diff --git a/cmd/podsync/config_test.go b/cmd/podsync/config_test.go
index 15230eb..aca66d7 100644
--- a/cmd/podsync/config_test.go
+++ b/cmd/podsync/config_test.go
@@ -84,6 +84,7 @@ timeout = 15
 	assert.EqualValues(t, 86400, feed.Filters.MaxDuration)
 	assert.EqualValues(t, 365, feed.Filters.MaxAge)
 	assert.EqualValues(t, 1, feed.Filters.MinAge)
+	require.NotNil(t, feed.Clean)
 	assert.EqualValues(t, 10, feed.Clean.KeepLast)
 	assert.EqualValues(t, model.SortingDesc, feed.PlaylistSort)
 
@@ -233,6 +234,114 @@ data_dir = "/data"
 	assert.True(t, config.Database.Badger.FileIO)
 }
 
+func TestGlobalCleanupPolicy(t *testing.T) {
+	t.Run("global cleanup policy applied to feeds without cleanup", func(t *testing.T) {
+		const file = `
+[cleanup]
+keep_last = 25
+
+[server]
+data_dir = "/data"
+
+[feeds]
+  [feeds.FEED1]
+  url = "https://youtube.com/channel/test1"
+  
+  [feeds.FEED2]
+  url = "https://youtube.com/channel/test2"
+  clean = { keep_last = 5 }
+`
+		path := setup(t, file)
+		defer os.Remove(path)
+
+		config, err := LoadConfig(path)
+		assert.NoError(t, err)
+		require.NotNil(t, config)
+
+		// Global cleanup policy should be set
+		require.NotNil(t, config.Cleanup)
+		assert.EqualValues(t, 25, config.Cleanup.KeepLast)
+
+		// FEED1 should inherit global cleanup policy
+		feed1, ok := config.Feeds["FEED1"]
+		assert.True(t, ok)
+		require.NotNil(t, feed1.Clean)
+		assert.EqualValues(t, 25, feed1.Clean.KeepLast)
+
+		// FEED2 should keep its own cleanup policy
+		feed2, ok := config.Feeds["FEED2"]
+		assert.True(t, ok)
+		require.NotNil(t, feed2.Clean)
+		assert.EqualValues(t, 5, feed2.Clean.KeepLast)
+	})
+
+	t.Run("no global cleanup policy", func(t *testing.T) {
+		const file = `
+[server]
+data_dir = "/data"
+
+[feeds]
+  [feeds.FEED1]
+  url = "https://youtube.com/channel/test1"
+  
+  [feeds.FEED2]
+  url = "https://youtube.com/channel/test2"
+  clean = { keep_last = 5 }
+`
+		path := setup(t, file)
+		defer os.Remove(path)
+
+		config, err := LoadConfig(path)
+		assert.NoError(t, err)
+		require.NotNil(t, config)
+
+		// Global cleanup policy should not be set
+		assert.Nil(t, config.Cleanup)
+
+		// FEED1 should have no cleanup policy
+		feed1, ok := config.Feeds["FEED1"]
+		assert.True(t, ok)
+		assert.Nil(t, feed1.Clean)
+
+		// FEED2 should keep its own cleanup policy
+		feed2, ok := config.Feeds["FEED2"]
+		assert.True(t, ok)
+		require.NotNil(t, feed2.Clean)
+		assert.EqualValues(t, 5, feed2.Clean.KeepLast)
+	})
+
+	t.Run("feed cleanup overrides global cleanup", func(t *testing.T) {
+		const file = `
+[cleanup]
+keep_last = 100
+
+[server]
+data_dir = "/data"
+
+[feeds]
+  [feeds.FEED1]
+  url = "https://youtube.com/channel/test1"
+  clean = { keep_last = 10 }
+`
+		path := setup(t, file)
+		defer os.Remove(path)
+
+		config, err := LoadConfig(path)
+		assert.NoError(t, err)
+		require.NotNil(t, config)
+
+		// Global cleanup policy should be set
+		require.NotNil(t, config.Cleanup)
+		assert.EqualValues(t, 100, config.Cleanup.KeepLast)
+
+		// FEED1 should use its own cleanup policy, not the global one
+		feed1, ok := config.Feeds["FEED1"]
+		assert.True(t, ok)
+		require.NotNil(t, feed1.Clean)
+		assert.EqualValues(t, 10, feed1.Clean.KeepLast)
+	})
+}
+
 func setup(t *testing.T, file string) string {
 	t.Helper()