Skip to content

Commit 60e1ebf

Browse files
authored
fixes #26 - add analyze_files option (#27)
1 parent 92eabcd commit 60e1ebf

10 files changed

Lines changed: 162 additions & 23 deletions

File tree

README.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,23 +53,27 @@ This job uses the [Sonarr API](https://sonarr.tv/docs/api/) to do the following
5353
* Checks if any episodes need to be [renamed](https://sonarr.tv/docs/api/#/RenameEpisode/get_api_v3_rename)
5454
* Triggers a rename on any episodes that need be renamed (per series)
5555

56+
#### Analyze Files
57+
This config option is useful if you have audio/video codec information as part of your mediaformat, and you are transcoding files after import to sonarr. This will initiate a rescan of the files in your library, so that the mediainfo will be udpated. Then the renamer will come through and detect changes, and rename the files
58+
5659
### Usage
5760

5861
The application run immediately on startup, and then continue to schedule jobs every hour (+- 5 minutes) after the first execution.
5962

6063
### Configuration
6164

62-
| Name | Type | Required | Default Value | Description |
63-
| ------------------------------------------ | ------- | -------- | ------------- | -------------------------------------------------------------------------------------- |
64-
| `sonarr` | Array | Yes | [] | One or more sonarr instances |
65-
| `sonarr[].name` | string | Yes | N/A | user friendly instance name, used in log messages |
66-
| `sonarr[].url` | string | Yes | N/A | url for sonarr instance |
67-
| `sonarr[].api_key` | string | Yes | N/A | api_key for sonarr instance |
68-
| `sonarr[].series_scanner.enabled` | boolean | Yes | N/A | enables/disables series_scanner functionality |
69-
| `sonarr[].series_scanner.hourly_job` | boolean | Yes | N/A | disables hourly job. App will exit after first execution |
70-
| `sonarr[].series_scanner.hours_before_air` | integer | No | 4 | The number of hours before an episode has aired, to trigger a rescan when title is TBA |
71-
| `sonarr[].existing_renamer.enabled` | boolean | Yes | N/A | enables/disables existing_renamer functionality |
72-
| `sonarr[].existing_renamer.hourly_job` | boolean | Yes | N/A | disables hourly job. App will exit after first execution |
65+
| Name | Type | Required | Default Value | Description |
66+
| ------------------------------------------ | ------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
67+
| `sonarr` | Array | Yes | [] | One or more sonarr instances |
68+
| `sonarr[].name` | string | Yes | N/A | user friendly instance name, used in log messages |
69+
| `sonarr[].url` | string | Yes | N/A | url for sonarr instance |
70+
| `sonarr[].api_key` | string | Yes | N/A | api_key for sonarr instance |
71+
| `sonarr[].series_scanner.enabled` | boolean | No | False | enables/disables series_scanner functionality |
72+
| `sonarr[].series_scanner.hourly_job` | boolean | No | False | disables hourly job. App will exit after first execution |
73+
| `sonarr[].series_scanner.hours_before_air` | integer | No | 4 | The number of hours before an episode has aired, to trigger a rescan when title is TBA |
74+
| `sonarr[].existing_renamer.enabled` | boolean | No | False | enables/disables existing_renamer functionality |
75+
| `sonarr[].existing_renamer.hourly_job` | boolean | No | False | disables hourly job. App will exit after first execution |
76+
| `sonarr[].existing_renamer.analyze_files` | boolean | No | False | This will initiate a rescan of the files in your library. This is helpful if you are transcoding files, and the audio/video codecs have changed. |
7377
### Local Setup
7478

7579
#### devcontainer
@@ -89,5 +93,5 @@ $ poetry run python src/main.py
8993

9094
#### Unit Tests
9195
```shell
92-
$ pytest --cov=src --cov-report=html tests --cov-branch
96+
$ pytest
9397
```

docker/config.yml.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ sonarr:
88
existing_renamer:
99
enabled: False
1010
hourly_job: False
11+
analyze_files: False
1112
- name: anime
1213
url: https://sonarr-anime.tld:8989
1314
api_key: not-a-real-api-key
@@ -18,4 +19,3 @@ sonarr:
1819
existing_renamer:
1920
enabled: True
2021
hourly_job: True
21-

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ testpaths = [
4242
"./tests/models"
4343
]
4444
addopts = [
45+
"--cov=src",
46+
"tests",
47+
"--cov-branch",
48+
"--capture=sys",
49+
"--cov-report=xml",
50+
"--cov-report=html",
4551
"--import-mode=importlib",
4652
]
4753
mock_use_standalone_module = "True"

src/config_schema.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@
3838
},
3939
Optional(
4040
"existing_renamer",
41-
default=dict(enabled=False, hourly_job=False),
41+
default=dict(enabled=False, hourly_job=False, analyze_files=False),
4242
ignore_extra_keys=True,
4343
): {
4444
Optional("enabled", default=False): bool,
4545
Optional("hourly_job", default=False): bool,
46+
Optional("analyze_files", default=False): bool,
4647
},
4748
}
4849
],

src/existing_renamer.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from time import sleep
12
from typing import List
23

34
from loguru import logger
@@ -7,14 +8,27 @@
78

89

910
class ExistingRenamer:
10-
def __init__(self, name, url, api_key):
11+
def __init__(self, name: str, url: str, api_key: str, analyze_files: bool = False):
1112
self.name = name
1213
self.sonarr_cli = SonarrCli(url, api_key)
14+
self.analyze_files = analyze_files
1315

1416
def scan(self):
1517
with logger.contextualize(instance=self.name):
1618
logger.info("Starting Existing Renamer")
1719

20+
if self.analyze_files:
21+
if not self.__analyze_files_enabled():
22+
logger.warning(
23+
"Analyse video files is not enabled, please enable setting, in order to use the reanalyze_files feature"
24+
)
25+
else:
26+
logger.info("Initiated disk scan of library")
27+
if self.__analyze_files():
28+
logger.info("disk scan finished successfully")
29+
else:
30+
logger.info("disk scan failed")
31+
1832
series = self.sonarr_cli.get_serie()
1933

2034
if len(series) == 0:
@@ -50,3 +64,36 @@ def scan(self):
5064
)
5165

5266
logger.info("Finished Existing Renamer")
67+
68+
def __analyze_files(self) -> bool:
69+
"""_summary_
70+
71+
Returns:
72+
bool: if disk scan succeeded
73+
"""
74+
rescan_command = self.sonarr_cli._sendCommand(
75+
{
76+
"name": "RescanSeries",
77+
"priority": "high",
78+
}
79+
)
80+
resp: json_data = {}
81+
82+
# sonarr commands have to be polled for completion status
83+
while resp.get("status") != "completed":
84+
sleep(10)
85+
resp = self.sonarr_cli.get_command(cid=rescan_command["id"])
86+
87+
return resp["result"] == "successful"
88+
89+
def __analyze_files_enabled(self) -> bool:
90+
"""_summary_
91+
92+
Returns:
93+
bool: if analyze_files is enabled
94+
"""
95+
mediamanagement: json_data = self.sonarr_cli.request_get(
96+
path="/api/v3/config/mediamanagement"
97+
)
98+
99+
return mediamanagement["enableMediaInfo"]

src/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __existing_renamer_job(self, sonarr_config):
5151
name=sonarr_config.name,
5252
url=sonarr_config.url,
5353
api_key=sonarr_config.api_key,
54+
analyze_files=sonarr_config.existing_renamer.analyze_files,
5455
).scan()
5556
except CliArrError as exc:
5657
logger.error(exc)
@@ -99,5 +100,5 @@ def start(self) -> None:
99100
sleep(1)
100101

101102

102-
if __name__ == "__main__":
103-
Main().start()
103+
if __name__ == "__main__": # pragma nocover
104+
Main().start() # pragma: no cover

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ def get_serie(mocker) -> None:
1515
mocker.patch.object(SonarrCli, "get_serie").return_value = series
1616

1717

18+
@pytest.fixture
19+
def get_serie_empty(mocker) -> None:
20+
mocker.patch.object(SonarrCli, "get_serie").return_value = []
21+
22+
1823
@pytest.fixture
1924
def mock_loguru_error(mocker) -> None:
2025
return mocker.patch.object(logger, "error")
@@ -30,6 +35,11 @@ def mock_loguru_debug(mocker) -> None:
3035
return mocker.patch.object(logger, "debug")
3136

3237

38+
@pytest.fixture
39+
def mock_loguru_warning(mocker) -> None:
40+
return mocker.patch.object(logger, "warning")
41+
42+
3343
def episode_data(
3444
id: int,
3545
title: str,

tests/test_existing_renamer.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import logging
2+
from unittest.mock import call
23

34
from existing_renamer import ExistingRenamer
45
from pycliarr.api import SonarrCli
56

67

78
class TestExistingRenamer:
8-
def test_no_series_returned(self, caplog, mocker) -> None:
9-
mocker.patch.object(SonarrCli, "get_serie").return_value = []
9+
def test_no_series_returned(self, get_serie_empty, caplog, mocker) -> None:
1010
rename_files = mocker.patch.object(SonarrCli, "rename_files")
1111

1212
with caplog.at_level(logging.DEBUG):
@@ -56,3 +56,51 @@ def test_when_multiple_episodes_need_renamed(
5656
assert "Found episodes to be renamed" in caplog.text
5757
assert "Renaming S01E01, S01E02" in caplog.text
5858
rename_files.assert_called_once_with([1, 2], 1)
59+
60+
def test_when_disk_scan_enabled_and_analyze_files_is_not(
61+
self, get_serie_empty, mock_loguru_warning, mocker
62+
) -> None:
63+
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
64+
enableMediaInfo=False
65+
)
66+
67+
ExistingRenamer("test", "test.tld", "test-api-key", True).scan()
68+
69+
mock_loguru_warning.assert_called_once_with(
70+
"Analyse video files is not enabled, please enable setting, in order to use the reanalyze_files feature"
71+
)
72+
73+
def test_when_disk_scan_enabled(
74+
self, get_serie_empty, mock_loguru_info, mocker
75+
) -> None:
76+
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
77+
enableMediaInfo=True
78+
)
79+
mocker.patch.object(SonarrCli, "_sendCommand").return_value = dict(id=1)
80+
mocker.patch.object(SonarrCli, "get_command").return_value = dict(
81+
status="completed", result="successful"
82+
)
83+
mocker.patch("existing_renamer.sleep").return_value = None
84+
85+
ExistingRenamer("test", "test.tld", "test-api-key", True).scan()
86+
87+
assert call("Initiated disk scan of library") in mock_loguru_info.call_args_list
88+
assert (
89+
call("disk scan finished successfully") in mock_loguru_info.call_args_list
90+
)
91+
92+
def test_when_disk_scan_enabled_and_fails(
93+
self, get_serie_empty, mock_loguru_info, mocker
94+
) -> None:
95+
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
96+
enableMediaInfo=True
97+
)
98+
mocker.patch.object(SonarrCli, "_sendCommand").return_value = dict(id=1)
99+
mocker.patch.object(SonarrCli, "get_command").return_value = dict(
100+
status="completed", result="failed"
101+
)
102+
mocker.patch("existing_renamer.sleep").return_value = None
103+
104+
ExistingRenamer("test", "test.tld", "test-api-key", True).scan()
105+
106+
assert call("disk scan failed") in mock_loguru_info.call_args_list

tests/test_main.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from existing_renamer import ExistingRenamer
44
from main import Main
55
from pycliarr.api import CliArrError
6-
from pyconfigparser import Config, configparser
6+
from pyconfigparser import Config, ConfigError, ConfigFileNotFoundError, configparser
77
from schedule import Job
88
from series_scanner import SeriesScanner
99

@@ -106,3 +106,25 @@ def test_existing_renamer_pycliarr_exception(
106106
Main().start()
107107

108108
mock_loguru_error.assert_called_once_with(exception)
109+
110+
def test_config_parser_error(self, mock_loguru_error, capsys, mocker) -> None:
111+
exception = ConfigError("BOOM!")
112+
mocker.patch("pyconfigparser.configparser.get_config").side_effect = exception
113+
114+
with pytest.raises(SystemExit) as excinfo:
115+
Main().start()
116+
117+
mock_loguru_error.assert_called_once_with(exception)
118+
assert excinfo.value.code == 1
119+
120+
def test_config_file_not_found_error(
121+
self, mock_loguru_error, capsys, mocker
122+
) -> None:
123+
exception = ConfigFileNotFoundError("BOOM!")
124+
mocker.patch("pyconfigparser.configparser.get_config").side_effect = exception
125+
126+
with pytest.raises(SystemExit) as excinfo:
127+
Main().start()
128+
129+
mock_loguru_error.assert_called_once_with(exception)
130+
assert excinfo.value.code == 1

0 commit comments

Comments
 (0)