### ApplicationProperties YAML Configuration Example Source: https://context7.com/schaka/janitorr/llms.txt Example YAML configuration for ApplicationProperties, demonstrating how to set dry-run, whole-tv-show, leaving-soon duration, and exclusion tags. ```yaml # application: # dry-run: false # whole-tv-show: false # leaving-soon: 14d # exclusion-tags: # - "janitorr_keep" # - "janitorr_archive" ``` -------------------------------- ### Jellyseerr Configuration Example Source: https://context7.com/schaka/janitorr/llms.txt Example configuration for enabling and connecting to a Jellyseerr instance. Ensure the URL, API key, and `match-server` setting are correctly configured for your environment. ```yaml clients: jellyseerr: enabled: true url: "http://jellyseerr:5055" api-key: "..." match-server: false # true: only delete requests tied to THIS Sonarr/Radarr instance ``` -------------------------------- ### MediaDeletion YAML Configuration Example Source: https://context7.com/schaka/janitorr/llms.txt Example YAML for MediaDeletion, showing how to configure movie and season expiration based on free disk space percentages. ```yaml # application: # media-deletion: # enabled: true # movie-expiration: # 5: 15d # 10: 30d # 15: 60d # 20: 90d # season-expiration: # 5: 15d # 10: 20d # 15: 60d # 20: 120d ``` -------------------------------- ### Example Local Development Startup Log Source: https://github.com/schaka/janitorr/blob/main/docs/local-development.md After successful startup, the logs will display the URLs and credentials for all running services, indicating Janitorr is connected and ready for interaction. ```text Started Radarr at http://localhost:54321 => Login via: admin/admin | API-Key: ... Started Sonarr at http://localhost:54322 => Login via: admin/admin | API-Key: ... Started Jellyfin at http://localhost:54323 => Login via: admin/adminadmin | API-Key: ... Started Seerr at http://localhost:54324 => Login via: admin/adminadmin | API-Key: ... Started Janitorr-Stats at http://localhost:54325 ``` -------------------------------- ### Docker Compose Deployment Example Source: https://context7.com/schaka/janitorr/llms.txt This is a sample Docker Compose configuration for deploying Janitorr along with its dependencies like Jellyfin, Sonarr, Radarr, and Jellyseerr. It outlines a full production stack. ```yaml version: "3.8" services: janitorr: image: "ghcr.io/schaka/janitorr:jvm-stable" container_name: "janitorr" restart: unless-stopped ports: - "8080:8080" volumes: - "./config:/config" - "./logs:/logs" - "./data:/data" environment: - "SONARR_API_KEY=${SONARR_API_KEY}" - "RADARR_API_KEY=${RADARR_API_KEY}" - "BAZARR_API_KEY=${BAZARR_API_KEY}" - "JELLYFIN_API_KEY=${JELLYFIN_API_KEY}" - "JELLYFIN_PASSWORD=${JELLYFIN_PASSWORD}" - "SEERR_API_KEY=${SEERR_API_KEY}" - "JELLYSTAT_API_KEY=${JELLYSTAT_API_KEY}" - "JANITORR_STATS_API_KEY=${JANITORR_STATS_API_KEY}" jellyfin: image: "linuxserver/jellyfin" container_name: "jellyfin" restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC volumes: - "./jellyfin/config:/config" - "./jellyfin/tv:/tv" - "./jellyfin/movies:/movies" ports: - "8096:8096" sonarr: image: "ghcr.io/hotio/sonarr" container_name: "sonarr" restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC volumes: - "./sonarr/config:/config" - "./sonarr/tv:/tv" ports: - "8989:8989" radarr: image: "ghcr.io/hotio/radarr" container_name: "radarr" restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=Etc/UTC volumes: - "./radarr/config:/config" - "./radarr/movies:/movies" ports: - "7878:7878" jellyseerr: image: "ghcr.io/sct/jellyseerr" container_name: "jellyseerr" restart: unless-stopped environment: - "PUID=1000" - "PGID=1000" - "TZ=Etc/UTC" - "NODE_ENV=production" - "PORT=5055" volumes: - "./jellyseerr/config:/config" ports: - "5055:5055" jellystat: image: "ghcr.io/jellystat/jellystat" container_name: "jellystat" restart: unless-stopped environment: - "PUID=1000" - "PGID=1000" - "TZ=Etc/UTC" volumes: - "./jellystat/config:/config" ports: - "3000:3000" ``` -------------------------------- ### Janitorr Docker Compose Configuration Source: https://context7.com/schaka/janitorr/llms.txt Example docker-compose.yml file for setting up Janitorr and its companion services like Jellyfin, Radarr, Sonarr, and a PostgreSQL database for stats. ```yaml services: jellyfin: image: jellyfin/jellyfin:latest container_name: jellyfin volumes: - /appdata/jellyfin:/config - /share_media:/data - /appdata/jellyfin/cache:/cache ports: - 8096:8096 restart: unless-stopped radarr: image: lscr.io/linuxserver/radarr:latest container_name: radarr environment: - PUID=1000 - PGID=1000 volumes: - /appdata/radarr:/config - /share_media:/data ports: - 7878:7878 sonarr: image: lscr.io/linuxserver/sonarr:latest container_name: sonarr environment: - PUID=1000 - PGID=1000 volumes: - /appdata/sonarr:/config - /share_media:/data ports: - 8989:8989 jellyseerr: image: fallenbagel/jellyseerr:latest container_name: jellyseerr volumes: - /appdata/jellyseerr:/app/config ports: - 5050:5050 # janitorr-stats: recommended companion for new setups janitorr-stats-db: image: postgres:18 container_name: janitorr-stats-db user: 1000:1000 environment: POSTGRES_DB: janitorr_stats POSTGRES_USER: janitorr_stats POSTGRES_PASSWORD: secret volumes: - /appdata/janitorr-stats/postgres-data:/var/lib/postgresql/data restart: unless-stopped janitorr-stats: image: ghcr.io/schaka/janitorr-stats:stable container_name: janitorr-stats user: 1000:1000 volumes: - /appdata/janitorr-stats/application.yaml:/work/config/application.yaml - /appdata/janitorr-stats/data:/data depends_on: - janitorr-stats-db restart: unless-stopped janitorr: image: ghcr.io/schaka/janitorr:jvm-stable container_name: janitorr user: 1000:1000 mem_limit: 256M mem_swappiness: 0 volumes: - /appdata/janitorr/config/application.yml:/config/application.yml - /appdata/janitorr/logs:/logs - /share_media:/data # must match Sonarr/Radarr/Jellyfin volume mapping exactly ``` -------------------------------- ### Janitorr Configuration - application.yml Source: https://context7.com/schaka/janitorr/llms.txt This YAML file controls Janitorr's runtime behavior. Ensure it is supplied at /config/application.yml for the application to start. Key settings include logging levels, file system access, dry-run mode, and various media deletion strategies (disk-threshold, tag-based, episode-based). ```yaml # /appdata/janitorr/config/application.yml logging: level: com.github.schaka: INFO # DEBUG or TRACE for verbose output file: name: "/logs/janitorr.log" file-system: access: true validate-seeding: true # skip deletion if original torrent file still exists leaving-soon-dir: "/data/media/leaving-soon" media-server-leaving-soon-dir: "/data/media/leaving-soon" # path as Jellyfin sees it from-scratch: true # rebuild leaving-soon dir from scratch each cycle free-space-check-dir: "/" # directory used for disk-space percentage checks application: dry-run: true # SAFE DEFAULT: nothing is deleted until set to false run-once: false # terminate after one cleanup cycle (useful in cron setups) whole-tv-show: false # treat entire show as one unit instead of per-season whole-show-seeding-check: false leaving-soon: 14d # window before deletion when item enters Leaving Soon leaving-soon-threshold-offset-percent: 5 exclusion-tags: - "janitorr_keep" media-deletion: enabled: true movie-expiration: 5: 15d # delete movies older than 15 days when free disk < 5% 10: 30d 15: 60d 20: 90d season-expiration: 5: 15d 10: 20d 15: 60d 20: 120d tag-based-deletion: enabled: true minimum-free-disk-percent: 100 # trigger immediately (100 = always) schedules: - tag: "janitorr_30d" expiration: 30d - tag: "janitorr_7d" expiration: 7d episode-deletion: enabled: true clean-older-seasons: false tag: "janitorr_daily" max-episodes: 10 max-age: 30d clients: default: connect-timeout: 60s read-timeout: 60s level: NONE sonarr: enabled: true url: "http://sonarr:8989" api-key: "${SONARR_API_KEY}" delete-empty-shows: true determine-age-by: MOST_RECENT # MOST_RECENT | OLDEST; omit to auto-detect from quality profile import-exclusions: false radarr: enabled: true url: "http://radarr:7878" api-key: "${RADARR_API_KEY}" only-delete-files: false # keep Radarr entry but delete files determine-age-by: MOST_RECENT import-exclusions: false bazarr: enabled: false url: "http://bazarr:6767" api-key: "${BAZARR_API_KEY}" jellyfin: enabled: true url: "http://jellyfin:8096" api-key: "${JELLYFIN_API_KEY}" username: "Janitorr" # dedicated user account required for file deletion password: "${JELLYFIN_PASSWORD}" delete: true exclude-favorited: false leaving-soon-tv: "Shows (Leaving Soon)" leaving-soon-movies: "Movies (Leaving Soon)" leaving-soon-type: MOVIES_AND_TV # MOVIES | TV | MOVIES_AND_TV | NONE # emby: # mutually exclusive with jellyfin # enabled: false # ...same keys as jellyfin... jellyseerr: enabled: true url: "http://jellyseerr:5055" api-key: "${SEERR_API_KEY}" match-server: false jellystat: # mutually exclusive with streamystats enabled: true whole-tv-show: false url: "http://jellystat:3000" api-key: "${JELLYSTAT_API_KEY}" # streamystats: # mutually exclusive with jellystat # enabled: false # ... janitorr-stats: enabled: false url: "http://janitorr-stats:8080" ``` -------------------------------- ### Configure File Logging in Janitorr Source: https://github.com/schaka/janitorr/wiki/FAQ Use these properties to configure file logging if the default setup in the readme does not work. You can customize the log file path and rolling policy. ```yaml logging: file: path: "/logs" name: "/logs/janitorr.log" logback: rollingpolicy: max-history: 14 max-file-size: 100MB clean-history-on-start: true threshold: file: TRACE ``` -------------------------------- ### Update Jellyfin/Emby 'Leaving Soon' Collection Source: https://context7.com/schaka/janitorr/llms.txt Creates or updates a virtual 'Leaving Soon' library collection in Jellyfin/Emby, backed by filesystem symlinks. This function handles directory creation, virtual library setup, and symlink generation. It can optionally clean up the target directory before creating new links. ```kotlin override fun updateLeavingSoon( cleanupType: CleanupType, libraryType: LibraryType, items: List, onlyAddLinks: Boolean = false ) { val path = Path(fileSystemProperties.leavingSoonDir, libraryType.folderName, cleanupType.folderName) Files.createDirectories(path) var collection = listLibraries().firstOrNull { it.Name == libraryType.collectionName(mediaServerProperties) } if (collection == null) collection = createLibrary(libraryType.collectionName(mediaServerProperties), libraryType, mediaServerPath) if (!collection.Locations.contains(mediaServerPath)) addPathToLibrary(collection, mediaServerPath) if (fileSystemProperties.fromScratch && !onlyAddLinks) cleanupPath(fileSystemProperties.leavingSoonDir, libraryType, cleanupType) createLinks(items, path, libraryType) // creates symlinks from media library → leaving-soon dir } ``` -------------------------------- ### Run LocalJanitorrStarterKt from IntelliJ Source: https://github.com/schaka/janitorr/blob/main/docs/local-development.md To run the local development environment, open the LocalJanitorrStarter.kt file and execute the main function. The first run may take several minutes to complete. ```kotlin fun main() { LocalJanitorrStarter().run( LocalDevEnvironment.Builder() .withRadarr(true) .withSonarr(true) .withJellyfin(true) .withSeerr(true) .withJanitorrStats(true) .build() ) } ``` -------------------------------- ### Configure Leaving Soon Directories Source: https://github.com/schaka/janitorr/blob/main/README.md Set the `leaving-soon-dir` and `media-server-leaving-soon-dir` in your `application.yml` to ensure Janitorr and your media server (e.g., Jellyfin) correctly identify the directory for items marked as 'leaving soon'. This is crucial when volume mappings differ between Janitorr and the media server. ```yaml leaving-soon-dir: "/data/media/leaving-soon" media-server-leaving-soon-dir: "/library/leaving-soon" ``` -------------------------------- ### Handle Application Startup Failure (Missing Properties) Source: https://github.com/schaka/janitorr/wiki/Troubleshooting This error indicates that Janitorr cannot find a required property in your application configuration. Ensure your application.yml is correctly mapped into the container. ```text Failed to bind properties under 'application' to com.github.schaka.janitorr.config.ApplicationProperties: Reason: java.lang.NullPointerException: Parameter specified as non-null is null: method com.github.schaka.janitorr.config.ApplicationProperties., parameter mediaDeletion Action: Update your application's configuration ``` -------------------------------- ### Media Cleanup Schedule Logic Source: https://context7.com/schaka/janitorr/llms.txt Implements disk-threshold cleanup for media. It determines deletion durations for TV seasons and movies based on free disk space percentage and configured expiration policies. Also computes 'Leaving Soon' windows. ```kotlin // com.github.schaka.janitorr.cleanup.MediaCleanupSchedule @Service class MediaCleanupSchedule(...) : AbstractCleanupSchedule(CleanupType.MEDIA, ...), Schedule { override fun runSchedule() { val freeSpacePercentage = getFreeSpacePercentage() // reads from file-system.free-space-check-dir val seasonExpiration = determineDeletionDuration( applicationProperties.mediaDeletion.seasonExpiration, freeSpacePercentage ) val movieExpiration = determineDeletionDuration( applicationProperties.mediaDeletion.movieExpiration, freeSpacePercentage ) // Also compute "earlier" expiration for Leaving Soon population val leavingSoonOffset = freeSpacePercentage - applicationProperties.leavingSoonThresholdOffsetPercent val seasonLeavingSoon = determineDeletionDuration(applicationProperties.mediaDeletion.seasonExpiration, leavingSoonOffset) val movieLeavingSoon = determineDeletionDuration(applicationProperties.mediaDeletion.movieExpiration, leavingSoonOffset) scheduleDelete(TV_SHOWS, seasonExpiration, seasonLeavingSoon) scheduleDelete(MOVIES, movieExpiration, movieLeavingSoon) } // Selects expiration: finds lowest threshold key > freeSpacePercentage, returns its duration. // Returns Duration.FOREVER (no deletion) if no threshold is exceeded. protected fun determineDeletionDuration( deletionConditions: Map, freeSpacePercentage: Double ): Duration { if (!fileSystemProperties.access) { return deletionConditions.entries.maxByOrNull { it.value.toDays() }?.value ?: FOREVER.duration } return deletionConditions.entries .filter { freeSpacePercentage < it.key } .minByOrNull { it.key }?.value ?: FOREVER.duration } } ``` -------------------------------- ### FileSystemProperties Configuration in Kotlin Source: https://context7.com/schaka/janitorr/llms.txt Configures Janitorr's filesystem access, controlling symlink creation, seeding detection, and disk-space-based expiration. Set `mediaServerLeavingSoonDir` when Janitorr and media servers have different mount points for the leaving-soon directory. ```kotlin @ConfigurationProperties(prefix = "file-system") data class FileSystemProperties( val access: Boolean = false, val leavingSoonDir: String = "/data/media/leaving-soon", val mediaServerLeavingSoonDir: String?, // null = same as leavingSoonDir val validateSeeding: Boolean = true, val fromScratch: Boolean = true, val freeSpaceCheckDir: String = "/" ) ``` ```yaml file-system: access: true leaving-soon-dir: "/data/media/leaving-soon" # path inside Janitorr container media-server-leaving-soon-dir: "/library/leaving-soon" # path inside Jellyfin container from-scratch: true # wipe and rebuild leaving-soon dir each cycle free-space-check-dir: "/data/media" # check space on media volume specifically ``` -------------------------------- ### Docker Compose Configuration for Janitorr Source: https://github.com/schaka/janitorr/blob/main/README.md Use this docker-compose.yml to set up and run Janitorr. Ensure the application.yml is correctly mapped to the /config/application.yml path within the container. Ports do not need to be published on the host. ```yaml services: janitorr: container_name: janitorr image: ghcr.io/schaka/janitorr:jvm-stable user: 1000:1000 # Replace with your user who should own your application.yml file mem_limit: 256M # is used to dynamically calculate heap size, can go as low as 200MB, but 256 is recommended - higher for very large libraries mem_swappiness: 0 volumes: - /appdata/janitorr/config/application.yml:/config/application.yml - /appdata/janitorr/logs:/logs - /share_media:/data ``` -------------------------------- ### Tag Deletion Configuration Source: https://context7.com/schaka/janitorr/llms.txt Defines data structures for tag-based media deletion. Use `TagDeletion` to enable the feature and configure minimum free disk space, and `TagDeleteSchedule` to specify tags and their expiration durations. ```kotlin data class TagDeletion( val enabled: Boolean = true, val minimumFreeDiskPercent: Double = 20.0, // trigger only below this free-space level val schedules: List = listOf() ) data class TagDeleteSchedule( val tag: String, // Sonarr/Radarr tag label to match val expiration: Duration ) ``` ```yaml application: tag-based-deletion: enabled: true minimum-free-disk-percent: 100 schedules: - tag: "janitorr_30d" expiration: 30d - tag: "janitorr_7d" expiration: 7d ``` -------------------------------- ### Enable janitorr-stats Integration Source: https://github.com/schaka/janitorr/blob/main/README.md Configure Janitorr to use the janitorr-stats companion service for storing watch history. Ensure janitorr-stats is running and accessible via the specified URL. ```yaml clients: janitorr-stats: enabled: true url: "http://janitorr-stats:8080" ``` -------------------------------- ### Hourly Cleanup Scheduler Source: https://context7.com/schaka/janitorr/llms.txt The `CleanupRunner` service triggers all cleanup schedules hourly. It also handles cache eviction and can be configured to exit after a single run using `run-once`. ```kotlin // com.github.schaka.janitorr.cleanup.CleanupRunner @Service class CleanupRunner( val applicationProperties: ApplicationProperties, val cacheManager: CacheManager, val cleanupSchedules: List // Spring-injected: MediaCleanupSchedule, TagBasedCleanupSchedule, WeeklyEpisodeCleanupSchedule ) { @Scheduled(fixedDelay = 1000 * 60 * 60) // every 60 minutes fun runSchedules() { for (schedule in cleanupSchedules) { cleanCachesByName(SonarrRestService.CACHE_NAME, RadarrRestService.CACHE_NAME) schedule.runSchedule() } if (applicationProperties.runOnce) { exitProcess(0) } } } ``` ```yaml application: run-once: true ``` -------------------------------- ### Tag-Based Cleanup Schedule Logic Source: https://context7.com/schaka/janitorr/llms.txt Handles tag-driven deletion for Sonarr/Radarr entries. Deletion is gated by minimum free disk percentage. Items are scheduled for deletion based on per-tag expiration. ```kotlin // com.github.schaka.janitorr.cleanup.TagBasedCleanupSchedule @Service class TagBasedCleanupSchedule(...) : AbstractCleanupSchedule(CleanupType.TAG, ...), Schedule { override fun runSchedule() { if (!applicationProperties.tagBasedDeletion.enabled) return // Wipe and rebuild the tag-based leaving-soon directories from scratch if (fileSystemProperties.fromScratch) { mediaServerService.cleanupPath(fileSystemProperties.leavingSoonDir, TV_SHOWS, CleanupType.TAG) mediaServerService.cleanupPath(fileSystemProperties.leavingSoonDir, MOVIES, CleanupType.TAG) } for (tag in applicationProperties.tagBasedDeletion.schedules) { // onlyAddLinks=true: never wipe the combined leaving-soon dir mid-loop scheduleDelete(TV_SHOWS, tag.expiration, tag.expiration, entryFilter = { item -> item.tags.contains(tag.tag) }, onlyAddLinks = true) scheduleDelete(MOVIES, tag.expiration, tag.expiration, entryFilter = { item -> item.tags.contains(tag.tag) }, onlyAddLinks = true) } } // Deletion triggered only when free disk ≤ minimumFreeDiskPercent override fun needToDelete(type: LibraryType): Boolean = if (fileSystemProperties.access) getFreeSpacePercentage() <= applicationProperties.tagBasedDeletion.minimumFreeDiskPercent else true } ``` -------------------------------- ### Episode Deletion Configuration Source: https://context7.com/schaka/janitorr/llms.txt Configures rolling window deletion for daily/weekly shows. Use `EpisodeDeletion` to enable the feature, specify the Sonarr tag, maximum episodes to keep, maximum age, and whether to clean older seasons. ```kotlin data class EpisodeDeletion( val enabled: Boolean = false, val tag: String = "janitorr_daily", // Sonarr tag that opts a show into this rule val maxEpisodes: Int = 10, // keep only the N most recent episodes val maxAge: Duration = Duration.ofDays(30), val cleanOlderSeasons: Boolean = false ) ``` ```yaml application: episode-deletion: enabled: true tag: "janitorr_daily" max-episodes: 5 max-age: 14d clean-older-seasons: true ``` -------------------------------- ### Logging Configuration in YAML Source: https://context7.com/schaka/janitorr/llms.txt Configures Janitorr's logging behavior, including file logging with a rolling policy and log levels. Setting the level to `DEBUG` or `TRACE` provides detailed decision logs. ```yaml logging: level: com.github.schaka: DEBUG # INFO | DEBUG | TRACE file: path: "/logs" name: "/logs/janitorr.log" logback: rollingpolicy: max-history: 14 max-file-size: 100MB clean-history-on-start: true threshold: file: TRACE ``` ```yaml # Docker environment variable injection for secrets (Spring Boot ${} syntax): # clients: # sonarr: # api-key: "${SONARR_API_KEY}" # radarr: # api-key: "${RADARR_API_KEY}" # jellyfin: # api-key: "${JELLYFIN_API_KEY}" # password: "${JELLYFIN_PASSWORD}" ``` -------------------------------- ### ApplicationProperties Kotlin Data Class Source: https://context7.com/schaka/janitorr/llms.txt Defines core application settings including dry-run, run-once, and exclusion tags. Ensure 'application' prefix is used in YAML configuration. ```kotlin // com.github.schaka.janitorr.config.ApplicationProperties @ConfigurationProperties(prefix = "application", ignoreUnknownFields = false) data class ApplicationProperties( val mediaDeletion: MediaDeletion, val tagBasedDeletion: TagDeletion, val episodeDeletion: EpisodeDeletion, val runOnce: Boolean = false, val dryRun: Boolean = false, // No destructive actions while true val trainingRun: Boolean = false, val wholeTvShow: Boolean = false, // Treat show as one unit (not per-season) val wholeShowSeedingCheck: Boolean = false, val leavingSoon: Duration = Duration.ofDays(14), val leavingSoonThresholdOffsetPercent: Int = 0, val exclusionTags: List = listOf("janitorr_keep") ) ``` -------------------------------- ### Handle Unbindable Property Error in Application Startup Source: https://github.com/schaka/janitorr/wiki/Troubleshooting This error occurs when Janitorr encounters a property in your application.yml that it no longer recognizes, often due to property changes. Compare your configuration with application-template.yml to identify and update old properties. ```text *************************** APPLICATION FAILED TO START *************************** Description: Binding to target [Bindable@78bb3be5 type = com.github.schaka.janitorr.config.ApplicationProperties, value = 'none', annotations = array[@org.springframework.boot.context.properties.ConfigurationProperties(ignoreInvalidFields=false, ignoreUnknownFields=false, prefix="application", value="application")]] failed: Property: application.exclusion-tag Value: "janitorr_keep" Origin: URL [file:application.yml] - 27:18 Reason: The elements [application.exclusion-tag] were left unbound. Action: Update your application's configuration ``` -------------------------------- ### Core Deletion Pipeline in Kotlin Source: https://context7.com/schaka/janitorr/llms.txt Orchestrates the deletion process for TV shows and movies, integrating with Sonarr/Radarr and media servers like Jellyfin/Emby. It handles filtering, populating media server IDs, watch history, excluding favorites, and managing 'Leaving Soon' collections. ```kotlin package com.github.schaka.janitorr.cleanup import com.github.schaka.janitorr.LibraryItem import com.github.schaka.janitorr.LibraryType import com.github.schaka.janitorr.LibraryType.MOVIES import com.github.schaka.janitorr.LibraryType.TV_SHOWS import com.github.schaka.janitorr.media.mediaServerService import com.github.schaka.janitorr.radarr.RadarrService import com.github.schaka.janitorr.seerr.SeerrService import com.github.schaka.janitorr.sonarr.SonarrService import com.github.schaka.janitorr.stats.StatsService import java.time.Duration // com.github.schaka.janitorr.cleanup.AbstractCleanupSchedule // Called by each concrete cleanup schedule: protected fun scheduleDelete( libraryType: LibraryType, expiration: Duration, // deletion threshold leavingSoonExpiration: Duration, entryFilter: (LibraryItem) -> Boolean = { true }, onlyAddLinks: Boolean = false ) { val shouldDelete = needToDelete(libraryType) val deletionExpiration = if (shouldDelete) expiration else FOREVER.duration when (libraryType) { TV_SHOWS -> cleanupMediaType(libraryType, sonarrService, deletionExpiration, leavingSoonExpiration, this::deleteTvShows, entryFilter, onlyAddLinks) MOVIES -> cleanupMediaType(libraryType, radarrService, deletionExpiration, leavingSoonExpiration, this::deleteMovies, entryFilter, onlyAddLinks) } } // Full pipeline internals (simplified): // 1. servarrService.getEntries() → all library items from Sonarr/Radarr // 2. filter by entryFilter // 3. pre-filter candidates past leaving-soon window // 4. mediaServerService.populateMediaServerIds(...) → Jellyfin/Emby IDs // 5. statsService.populateWatchHistory(...) → overwrite importedDate with lastSeen // 6. mediaServerService.filterOutFavorites(...) // 7. separate into leavingSoon and toDelete lists // 8. mediaServerService.updateLeavingSoon(...) → create/update Jellyfin collection + symlinks // 9. deleteTask(toDeleteMedia) → removeEntries + cleanupMovies/TvShows + seerrService.cleanupRequests ``` -------------------------------- ### MediaDeletion Kotlin Data Class Source: https://context7.com/schaka/janitorr/llms.txt Defines disk-threshold expiration rules for media deletion. Maps free disk percentage to expiration durations for movies and seasons. ```kotlin // com.github.schaka.janitorr.config.MediaDeletion data class MediaDeletion( val enabled: Boolean = true, val movieExpiration: Map, // percent → duration val seasonExpiration: Map ) ``` -------------------------------- ### Janitorr Docker Memory Optimization Source: https://github.com/schaka/janitorr/blob/main/README.md For extremely memory-constrained environments, you can adjust JAVA_TOOL_OPTIONS to reduce Janitorr's memory footprint. Experiment with these settings cautiously, as they may impact stability. Recommended values are provided in the surrounding documentation. ```yaml environment: - JAVA_TOOL_OPTIONS=-Xms10m -Xmx30m -XX:+UseSerialGC -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders -XX:MaxDirectMemorySize=10M -XX:MaxMetaspaceSize=20M -XX:ReservedCodeCacheSize=10M -Xss150K ``` -------------------------------- ### Enable Debug Logging in Janitorr Source: https://github.com/schaka/janitorr/blob/main/README.md To enable debug logging, change the log level from INFO to DEBUG or TRACE in the application.yml file. This is useful for troubleshooting. ```yaml com.github.schaka: INFO ``` -------------------------------- ### Cleanup Jellyseerr/Overseerr Requests Source: https://context7.com/schaka/janitorr/llms.txt Removes matching requests from Jellyseerr/Overseerr when media items are deleted from Jellyfin/Emby. Matching is performed using external service IDs and metadata. Optionally, it validates that the request belongs to the configured Sonarr/Radarr server if `match-server` is true. ```kotlin override fun cleanupRequests(items: List) { val allRequests = getAllRequests() // paginated: 1000 per page for (item in items) { val requests = if (item.season != null) allRequests.filter { req -> mediaMatches(item, req) && req.seasons?.any { it.seasonNumber == item.season } == true } else allRequests.filter { req -> mediaMatches(item, req) } for (request in requests) { if (!applicationProperties.dryRun) { seerrClient.deleteRequest(request.id) log.info("Deleting request for {} | IMDB: {}", item.filePath, item.imdbId) } } } } ``` -------------------------------- ### Weekly Episode Cleanup Schedule in Kotlin Source: https://context7.com/schaka/janitorr/llms.txt Manages shows tagged for episode deletion in Sonarr. It purges episodes from the latest season based on age or count, and optionally cleans older seasons. Requires SonarrClient and ApplicationProperties. ```kotlin package com.github.schaka.janitorr.cleanup import com.github.schaka.janitorr.ApplicationProperties import com.github.schaka.janitorr.sonarr.SonarrClient import com.github.schaka.janitorr.sonarr.model.MonitoringRequest import org.springframework.stereotype.Service import java.time.LocalDate import java.time.LocalDateTime // com.github.schaka.janitorr.cleanup.WeeklyEpisodeCleanupSchedule @Service class WeeklyEpisodeCleanupSchedule( val applicationProperties: ApplicationProperties, val sonarrClient: SonarrClient, // ... other dependencies ) : Schedule { override fun runSchedule() { val today = LocalDateTime.now() // Only process series carrying the configured episode-deletion tag val series = sonarrClient.getAllSeries().filter { it.tags.contains(episodeTag.id) } for (show in series) { val latestSeason = show.seasons.maxBy { it.seasonNumber } deleteOlderSeasons(show, latestSeason) // respects clean-older-seasons flag val episodes = sonarrClient.getAllEpisodes(show.id, latestSeason.seasonNumber) .filter { it.airDate != null && LocalDate.parse(it.airDate!!) <= today.toLocalDate() } .toMutableList() val episodesHistory = sonarrClient.getHistory(show.id, latestSeason.seasonNumber) .sortedBy { parseDate(it.date) } .distinctBy { it.episodeId } // Phase 1: delete by age for (episodeHistory in episodesHistory) { val episode = episodes.first { it.id == episodeHistory.episodeId } if (parseDate(episodeHistory.date) + applicationProperties.episodeDeletion.maxAge <= today) { if (!applicationProperties.dryRun && episode.episodeFileId != null) { sonarrClient.changeMonitoringStatus(MonitoringRequest(listOf(episode.id))) sonarrClient.deleteEpisodeFile(episode.episodeFileId) episodes.remove(episode) } } } // Phase 2: delete by count — keep only the N most recent if (episodes.size > applicationProperties.episodeDeletion.maxEpisodes) { val keep = episodes.sortedByDescending { it.episodeNumber } .take(applicationProperties.episodeDeletion.maxEpisodes) episodes.removeAll(keep) for (episode in episodes) { if (!applicationProperties.dryRun && episode.episodeFileId != null) { sonarrClient.changeMonitoringStatus(MonitoringRequest(listOf(episode.id))) sonarrClient.deleteEpisodeFile(episode.episodeFileId) } } } } } } ``` -------------------------------- ### LibraryItem Data Model in Kotlin Source: https://context7.com/schaka/janitorr/llms.txt Represents a deletable media entry. The `historyAge` property calculates the effective age based on `lastSeen` or `importedDate`. Used to model movies and TV seasons. ```kotlin data class LibraryItem( val id: Int, // Sonarr/Radarr internal ID val importedDate: LocalDateTime, // grab/import date from *arr history val originalPath: String, // path at time of download (for seeding check) val libraryPath: String, val parentPath: String, // e.g. /data/media/tv/Seinfeld val rootFolderPath: String, // e.g. /data/media/tv val filePath: String, // current path of the season/movie folder val imdbId: String? = null, val tvdbId: Int? = null, val tmdbId: Int? = null, val season: Int? = null, // null = movie; non-null = TV season number var mediaServerIds: MutableList = mutableListOf(), var seeding: Boolean = false, // true = skip deletion (torrent still active) var lastSeen: LocalDateTime? = null, val tags: List = listOf(), val extraFiles: MutableList = mutableListOf() // e.g. subtitle paths from Bazarr ) { // Effective age: most recent of lastSeen and importedDate val historyAge: LocalDateTime get() = if (lastSeen?.isAfter(importedDate) == true) lastSeen!! else importedDate } ``` ```kotlin val movie = LibraryItem( id = 42, importedDate = LocalDateTime.of(2024, 1, 15, 10, 0), originalPath = "/downloads/complete/Inception.2010.mkv", libraryPath = "/data/media/movies/Inception (2010)/Inception.2010.mkv", parentPath = "/data/media/movies/Inception (2010)", rootFolderPath = "/data/media/movies", filePath = "/data/media/movies/Inception (2010)/Inception.2010.mkv", imdbId = "tt1375666", tmdbId = 27205, lastSeen = LocalDateTime.of(2024, 3, 1, 20, 0) ) ``` -------------------------------- ### Radarr Library Item Fetching and Removal Source: https://context7.com/schaka/janitorr/llms.txt Fetches movie library items from Radarr's API, filtering out protected tags. Entries are removed by deleting movies, with options for file-only deletion and import list exclusion. ```kotlin // com.github.schaka.janitorr.servarr.radarr.RadarrRestService @Service class RadarrRestService(val radarrClient: RadarrClient, ...) : ServarrService { @Cacheable("radarr-cache") override fun getEntries(): List { val allTags = radarrClient.getAllTags() return radarrClient.getAllMovies() .filter { !it.tags.any { id -> keepTags.any { kt -> kt.id == id } } } .mapNotNull { movie -> radarrClient.getHistory(movie.id) .filter { it.eventType == "downloadFolderImported" && it.data.droppedPath != null } .map { LibraryItem(movie.id, LocalDateTime.parse(it.date.dropLast(1)), it.data.droppedPath!!, it.data.importedPath!!, movie.path, movie.rootFolderPath!!, movie.movieFile!!.path, tmdbId = movie.tmdbId, imdbId = movie.imdbId, tags = allTags.filter { t -> movie.tags.contains(t.id) }.map { t -> t.label }) } .sortedWith(byDate(historySort)) .firstOrNull() } } override fun removeEntries(items: List) { for (movie in items) { // Skip seeding torrents if filesystem validation enabled if (fileSystemProperties.access && fileSystemProperties.validateSeeding && Path(movie.originalPath).exists()) { movie.seeding = true; continue } if (!applicationProperties.dryRun) { unmonitorMovie(movie.id) deleteMovie(movie) // radarrClient.deleteMovie(id, deleteFiles=true, addImportExclusion) } } } } ``` ```yaml // Radarr client config: // clients: // radarr: // enabled: true // url: "http://radarr:7878" // api-key: "..." // only-delete-files: false # true = keep Radarr entry, only delete files // import-exclusions: false # add tmdbId to Radarr import exclusions on delete ``` -------------------------------- ### Sonarr Library Item Fetching and Removal Source: https://context7.com/schaka/janitorr/llms.txt Fetches TV show library items from Sonarr's API, supporting per-season or per-show modes. Entries can be removed by deleting episode files and unmonitoring seasons, or by deleting the entire series. ```kotlin // com.github.schaka.janitorr.servarr.sonarr.SonarrRestService @Service class SonarrRestService(val sonarrClient: SonarrClient, ...) : ServarrService { @Cacheable("sonarr-cache") override fun getEntries(): List { // Returns one LibraryItem per season (or per show if whole-tv-show=true) // Each item carries: id, importedDate, imdbId, tvdbId, season, tags, filePath, rootFolderPath return if (applicationProperties.wholeTvShow) getEntriesPerShow(allTags) else getEntriesPerSeason(allTags) } override fun removeEntries(items: List) { if (applicationProperties.wholeTvShow) { // Delete entire show in Sonarr (deleteEmptyShows must be true, or files-only) items.map { it.id }.distinct() .map { sonarrClient.getSeries(it) } .forEach { show -> deleteShow(show) } } else { // Per-season: unmonitor season, delete episode files, clean up empty shows removeBySeason(items) } } } ``` ```yaml // Sonarr client Feign interface (prefix clients.sonarr): // clients: // sonarr: // enabled: true // url: "http://sonarr:8989" // api-key: "..." // delete-empty-shows: true // determine-age-by: MOST_RECENT # MOST_RECENT | OLDEST // import-exclusions: false # add tvdbId to Sonarr import exclusions on delete ``` -------------------------------- ### Resolve Radarr/Sonarr Access Failures (Redirection) Source: https://github.com/schaka/janitorr/wiki/Troubleshooting This error indicates a redirection issue when accessing Radarr or Sonarr, often due to SSL being enabled or a reverse proxy. Use HTTPS directly in your URL or disable SSL for internal network access. ```text feign.RetryableException: cannot retry due to redirection, in streaming mode executing PUT http://localhost:8989/api/v3/series/35 ```