Skip to content

Commit

Permalink
Merge pull request #102 from rfsbraz/feat/101-clarification-question-…
Browse files Browse the repository at this point in the history
…removal-from-radarr-and-prevent-movie-from-being-added-to-radarr-by-lists

Radarr list exclusion support
  • Loading branch information
rfsbraz authored Apr 17, 2024
2 parents f7acea6 + 3673c3a commit f749c5c
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 7 deletions.
29 changes: 28 additions & 1 deletion app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
import yaml

from app import logger
from app.constants import VALID_ACTION_MODES, VALID_SORT_FIELDS, VALID_SORT_ORDERS
from app.constants import (
SETTINGS_PER_ACTION,
SETTINGS_PER_INSTANCE,
VALID_ACTION_MODES,
VALID_SORT_FIELDS,
VALID_SORT_ORDERS,
)
from app.modules.tautulli import Tautulli
from app.modules.trakt import Trakt
from app.utils import validate_units
Expand Down Expand Up @@ -72,6 +78,17 @@ def validate_trakt(self):
logger.debug(f"Error: {err}")
return False

def validate_settings_for_instance(self, library):
instance_type = "radarr" if "radarr" in library else "sonarr"
for setting in library:
if (
setting in SETTINGS_PER_INSTANCE
and instance_type not in SETTINGS_PER_INSTANCE[setting]
):
self.log_and_exit(
f"'{setting}' can only be set for instances of type: {SETTINGS_PER_INSTANCE[setting]}"
)

def validate_sonarr_and_radarr(self):
sonarr_settings = self.settings.get("sonarr", [])
radarr_settings = self.settings.get("radarr", [])
Expand Down Expand Up @@ -140,6 +157,7 @@ def validate_libraries(self):
self.validate_action_mode(library)
self.validate_watch_status(library)
self.validate_sort_configuration(library)
self.validate_settings_for_instance(library)

return True

Expand Down Expand Up @@ -183,6 +201,15 @@ def validate_action_mode(self, library):
f"Invalid action_mode '{library['action_mode']}' in library '{library['name']}', it should be either 'delete'."
)

# Validate settings per action
for setting in library:
if setting in SETTINGS_PER_ACTION and library[
"action_mode"
] not in SETTINGS_PER_ACTION.get(setting, []):
self.log_and_exit(
f"'{setting}' can only be set when action_mode is '{library['action_mode']}' for library '{library['name']}'."
)

def validate_watch_status(self, library):
if "watch_status" in library and library["watch_status"] not in [
"watched",
Expand Down
10 changes: 10 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@
"episodes",
]

VALID_INSTANCE_TYPES = ["radarr", "sonarr"]

# Valid sort orders
VALID_SORT_ORDERS = ["asc", "desc"]

# Valid action modes
VALID_ACTION_MODES = ["delete"]

SETTINGS_PER_ACTION = {
"add_list_exclusion_on_delete": ["delete"],
}

SETTINGS_PER_INSTANCE = {
"add_list_exclusion_on_delete": ["radarr"],
}
12 changes: 10 additions & 2 deletions app/media_cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,17 @@ def delete_movie_if_allowed(
library.get("name"),
)
if input().lower() == "y":
radarr_instance.del_movie(radarr_movie["id"], delete_files=True)
radarr_instance.del_movie(
radarr_movie["id"],
delete_files=True,
add_exclusion=library.get("add_list_exclusion_on_delete", False),
)
else:
radarr_instance.del_movie(radarr_movie["id"], delete_files=True)
radarr_instance.del_movie(
radarr_movie["id"],
delete_files=True,
add_exclusion=library.get("add_list_exclusion_on_delete", False),
)

def get_library_config(self, config, show):
return next(
Expand Down
1 change: 1 addition & 0 deletions config/settings.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ libraries:
- name: "Movies" # The name of your Plex library
radarr: "Radarr" # The Radarr instance to use for this library
action_mode: "delete" # Actions can be "delete"
add_list_exclusion_on_delete: True # Prevents radarr from importing the deleted movie automatically again from a list
last_watched_threshold: 30 # Time threshold in days. Media not watched in this period will be subject to actions
watch_status: watched # Watched status of the media
apply_last_watch_threshold_to_collections: true # If true, the last watched threshold will be applied to all other items in the collection
Expand Down
3 changes: 2 additions & 1 deletion docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,9 @@ For each of your Plex libraries, specify how you want Deleterr to behave. Define
| `series_type` | Only used if `sonarr` is set. It's required to filter for the show type, defaults to `standard`. | `"standard", "anime"` | `standard`, `anime`, `daily` |
| `action_mode` | The action to perform on the media items. | `delete` | `delete` |
| `last_watched_threshold` | Time threshold in days. Media watched in this period will not be actionable | `90` | - |
| `add_list_exclusion_on_delete` | Prevent Radarr/Sonarr from importing the media automatically again from a list. Currently only works with Radarr. | `true` | `true`,`false` |
| `watch_status` | Watch status. Media not in this is state will not be actionable | `-` | `watched`, `unwatched` |
| `apply_last_watch_threshold_to_collections` | If set to `true`, the last watched threshold will be applied to all other items in the same collection. | `true` | `true` / `false` |
| `apply_last_watch_threshold_to_collections` | If set to `true`, the last watched threshold will be applied to all other items in the same collection. | `true` | `true`, `false` |
| `added_at_threshold` | Media that added to Plex within this period (in days) will not be actionable | `180` | - |
| `disk_size_threshold` | Library deletion will only happen when below this threshold. It requires a `path` (that the `sonarr` or `radarr` instance can access) and a size threshold | `path: /media/local` </br> `threshold: 1TB` | Valid units: [`B`, `KB`, `MB`, `GB`, `TB`, `PB`, `EB`] |
| `max_actions_per_run` | Limit the number of actions performed per run. Defaults to `10` | `3000` | - |
Expand Down
47 changes: 46 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import pytest

from app.config import Config
from app.constants import VALID_ACTION_MODES, VALID_SORT_FIELDS, VALID_SORT_ORDERS
from app.constants import (
SETTINGS_PER_ACTION,
SETTINGS_PER_INSTANCE,
VALID_ACTION_MODES,
VALID_INSTANCE_TYPES,
VALID_SORT_FIELDS,
VALID_SORT_ORDERS,
)


# Test case for validate_libraries
Expand Down Expand Up @@ -66,6 +73,44 @@ def test_invalid_sorting_options(library_config):
validator.validate_libraries()


@pytest.mark.parametrize("action_mode", VALID_ACTION_MODES)
@pytest.mark.parametrize("setting", SETTINGS_PER_ACTION.keys())
@pytest.mark.parametrize("instance", VALID_INSTANCE_TYPES)
def test_settings_per_instance_and_action_mode(action_mode, setting, instance):
library_config = {
"name": "TV Shows",
"action_mode": action_mode,
instance: "test",
setting: True,
}

instance_config = [
{"name": "test", "url": "http://localhost:8989", "api_key": "API_KEY"}
]

validator = Config({"libraries": [library_config], instance: instance_config})

if (
# If the setting is valid for the action mode or the action mode is not specified
(setting in SETTINGS_PER_ACTION and action_mode in SETTINGS_PER_ACTION[setting])
or (setting not in SETTINGS_PER_ACTION)
) and (
(
# And if the setting is valid for the instance
setting in SETTINGS_PER_INSTANCE
and instance in SETTINGS_PER_INSTANCE[setting]
)
# Or the instance is not specified
or (setting not in SETTINGS_PER_INSTANCE)
):
# Then the validation should pass
assert validator.validate_libraries() == True
else:
# Otherwise, the validation should fail
with pytest.raises(SystemExit):
validator.validate_libraries()


# Test case for validate_libraries
@pytest.mark.parametrize("sort_field", VALID_SORT_FIELDS)
@pytest.mark.parametrize("sort_order", VALID_SORT_ORDERS)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_media_cleaner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ def test_delete_movie_if_allowed_interactive_yes(mock_input, standard_config):
# Assert
mock_input.assert_called_once()
radarr_instance.del_movie.assert_called_once_with(
radarr_movie["id"], delete_files=True
radarr_movie["id"], delete_files=True, add_exclusion=False
)


Expand Down Expand Up @@ -1120,7 +1120,7 @@ def test_delete_movie_if_allowed_not_interactive(standard_config):

# Assert
radarr_instance.del_movie.assert_called_once_with(
radarr_movie["id"], delete_files=True
radarr_movie["id"], delete_files=True, add_exclusion=False
)


Expand Down

0 comments on commit f749c5c

Please sign in to comment.