Skip to content

Commit

Permalink
fixes #26 - add analyze_files option (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
hollanbm authored May 22, 2024
1 parent 92eabcd commit 60e1ebf
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 23 deletions.
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,27 @@ This job uses the [Sonarr API](https://sonarr.tv/docs/api/) to do the following
* Checks if any episodes need to be [renamed](https://sonarr.tv/docs/api/#/RenameEpisode/get_api_v3_rename)
* Triggers a rename on any episodes that need be renamed (per series)

#### Analyze Files
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

### Usage

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

### Configuration

| Name | Type | Required | Default Value | Description |
| ------------------------------------------ | ------- | -------- | ------------- | -------------------------------------------------------------------------------------- |
| `sonarr` | Array | Yes | [] | One or more sonarr instances |
| `sonarr[].name` | string | Yes | N/A | user friendly instance name, used in log messages |
| `sonarr[].url` | string | Yes | N/A | url for sonarr instance |
| `sonarr[].api_key` | string | Yes | N/A | api_key for sonarr instance |
| `sonarr[].series_scanner.enabled` | boolean | Yes | N/A | enables/disables series_scanner functionality |
| `sonarr[].series_scanner.hourly_job` | boolean | Yes | N/A | disables hourly job. App will exit after first execution |
| `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 |
| `sonarr[].existing_renamer.enabled` | boolean | Yes | N/A | enables/disables existing_renamer functionality |
| `sonarr[].existing_renamer.hourly_job` | boolean | Yes | N/A | disables hourly job. App will exit after first execution |
| Name | Type | Required | Default Value | Description |
| ------------------------------------------ | ------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sonarr` | Array | Yes | [] | One or more sonarr instances |
| `sonarr[].name` | string | Yes | N/A | user friendly instance name, used in log messages |
| `sonarr[].url` | string | Yes | N/A | url for sonarr instance |
| `sonarr[].api_key` | string | Yes | N/A | api_key for sonarr instance |
| `sonarr[].series_scanner.enabled` | boolean | No | False | enables/disables series_scanner functionality |
| `sonarr[].series_scanner.hourly_job` | boolean | No | False | disables hourly job. App will exit after first execution |
| `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 |
| `sonarr[].existing_renamer.enabled` | boolean | No | False | enables/disables existing_renamer functionality |
| `sonarr[].existing_renamer.hourly_job` | boolean | No | False | disables hourly job. App will exit after first execution |
| `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. |
### Local Setup

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

#### Unit Tests
```shell
$ pytest --cov=src --cov-report=html tests --cov-branch
$ pytest
```
2 changes: 1 addition & 1 deletion docker/config.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ sonarr:
existing_renamer:
enabled: False
hourly_job: False
analyze_files: False
- name: anime
url: https://sonarr-anime.tld:8989
api_key: not-a-real-api-key
Expand All @@ -18,4 +19,3 @@ sonarr:
existing_renamer:
enabled: True
hourly_job: True

6 changes: 3 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ testpaths = [
"./tests/models"
]
addopts = [
"--cov=src",
"tests",
"--cov-branch",
"--capture=sys",
"--cov-report=xml",
"--cov-report=html",
"--import-mode=importlib",
]
mock_use_standalone_module = "True"
3 changes: 2 additions & 1 deletion src/config_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@
},
Optional(
"existing_renamer",
default=dict(enabled=False, hourly_job=False),
default=dict(enabled=False, hourly_job=False, analyze_files=False),
ignore_extra_keys=True,
): {
Optional("enabled", default=False): bool,
Optional("hourly_job", default=False): bool,
Optional("analyze_files", default=False): bool,
},
}
],
Expand Down
49 changes: 48 additions & 1 deletion src/existing_renamer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from time import sleep
from typing import List

from loguru import logger
Expand All @@ -7,14 +8,27 @@


class ExistingRenamer:
def __init__(self, name, url, api_key):
def __init__(self, name: str, url: str, api_key: str, analyze_files: bool = False):
self.name = name
self.sonarr_cli = SonarrCli(url, api_key)
self.analyze_files = analyze_files

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

if self.analyze_files:
if not self.__analyze_files_enabled():
logger.warning(
"Analyse video files is not enabled, please enable setting, in order to use the reanalyze_files feature"
)
else:
logger.info("Initiated disk scan of library")
if self.__analyze_files():
logger.info("disk scan finished successfully")
else:
logger.info("disk scan failed")

series = self.sonarr_cli.get_serie()

if len(series) == 0:
Expand Down Expand Up @@ -50,3 +64,36 @@ def scan(self):
)

logger.info("Finished Existing Renamer")

def __analyze_files(self) -> bool:
"""_summary_
Returns:
bool: if disk scan succeeded
"""
rescan_command = self.sonarr_cli._sendCommand(
{
"name": "RescanSeries",
"priority": "high",
}
)
resp: json_data = {}

# sonarr commands have to be polled for completion status
while resp.get("status") != "completed":
sleep(10)
resp = self.sonarr_cli.get_command(cid=rescan_command["id"])

return resp["result"] == "successful"

def __analyze_files_enabled(self) -> bool:
"""_summary_
Returns:
bool: if analyze_files is enabled
"""
mediamanagement: json_data = self.sonarr_cli.request_get(
path="/api/v3/config/mediamanagement"
)

return mediamanagement["enableMediaInfo"]
5 changes: 3 additions & 2 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __existing_renamer_job(self, sonarr_config):
name=sonarr_config.name,
url=sonarr_config.url,
api_key=sonarr_config.api_key,
analyze_files=sonarr_config.existing_renamer.analyze_files,
).scan()
except CliArrError as exc:
logger.error(exc)
Expand Down Expand Up @@ -99,5 +100,5 @@ def start(self) -> None:
sleep(1)


if __name__ == "__main__":
Main().start()
if __name__ == "__main__": # pragma nocover
Main().start() # pragma: no cover
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ def get_serie(mocker) -> None:
mocker.patch.object(SonarrCli, "get_serie").return_value = series


@pytest.fixture
def get_serie_empty(mocker) -> None:
mocker.patch.object(SonarrCli, "get_serie").return_value = []


@pytest.fixture
def mock_loguru_error(mocker) -> None:
return mocker.patch.object(logger, "error")
Expand All @@ -30,6 +35,11 @@ def mock_loguru_debug(mocker) -> None:
return mocker.patch.object(logger, "debug")


@pytest.fixture
def mock_loguru_warning(mocker) -> None:
return mocker.patch.object(logger, "warning")


def episode_data(
id: int,
title: str,
Expand Down
52 changes: 50 additions & 2 deletions tests/test_existing_renamer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import logging
from unittest.mock import call

from existing_renamer import ExistingRenamer
from pycliarr.api import SonarrCli


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

with caplog.at_level(logging.DEBUG):
Expand Down Expand Up @@ -56,3 +56,51 @@ def test_when_multiple_episodes_need_renamed(
assert "Found episodes to be renamed" in caplog.text
assert "Renaming S01E01, S01E02" in caplog.text
rename_files.assert_called_once_with([1, 2], 1)

def test_when_disk_scan_enabled_and_analyze_files_is_not(
self, get_serie_empty, mock_loguru_warning, mocker
) -> None:
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
enableMediaInfo=False
)

ExistingRenamer("test", "test.tld", "test-api-key", True).scan()

mock_loguru_warning.assert_called_once_with(
"Analyse video files is not enabled, please enable setting, in order to use the reanalyze_files feature"
)

def test_when_disk_scan_enabled(
self, get_serie_empty, mock_loguru_info, mocker
) -> None:
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
enableMediaInfo=True
)
mocker.patch.object(SonarrCli, "_sendCommand").return_value = dict(id=1)
mocker.patch.object(SonarrCli, "get_command").return_value = dict(
status="completed", result="successful"
)
mocker.patch("existing_renamer.sleep").return_value = None

ExistingRenamer("test", "test.tld", "test-api-key", True).scan()

assert call("Initiated disk scan of library") in mock_loguru_info.call_args_list
assert (
call("disk scan finished successfully") in mock_loguru_info.call_args_list
)

def test_when_disk_scan_enabled_and_fails(
self, get_serie_empty, mock_loguru_info, mocker
) -> None:
mocker.patch.object(SonarrCli, "request_get").return_value = dict(
enableMediaInfo=True
)
mocker.patch.object(SonarrCli, "_sendCommand").return_value = dict(id=1)
mocker.patch.object(SonarrCli, "get_command").return_value = dict(
status="completed", result="failed"
)
mocker.patch("existing_renamer.sleep").return_value = None

ExistingRenamer("test", "test.tld", "test-api-key", True).scan()

assert call("disk scan failed") in mock_loguru_info.call_args_list
24 changes: 23 additions & 1 deletion tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from existing_renamer import ExistingRenamer
from main import Main
from pycliarr.api import CliArrError
from pyconfigparser import Config, configparser
from pyconfigparser import Config, ConfigError, ConfigFileNotFoundError, configparser
from schedule import Job
from series_scanner import SeriesScanner

Expand Down Expand Up @@ -106,3 +106,25 @@ def test_existing_renamer_pycliarr_exception(
Main().start()

mock_loguru_error.assert_called_once_with(exception)

def test_config_parser_error(self, mock_loguru_error, capsys, mocker) -> None:
exception = ConfigError("BOOM!")
mocker.patch("pyconfigparser.configparser.get_config").side_effect = exception

with pytest.raises(SystemExit) as excinfo:
Main().start()

mock_loguru_error.assert_called_once_with(exception)
assert excinfo.value.code == 1

def test_config_file_not_found_error(
self, mock_loguru_error, capsys, mocker
) -> None:
exception = ConfigFileNotFoundError("BOOM!")
mocker.patch("pyconfigparser.configparser.get_config").side_effect = exception

with pytest.raises(SystemExit) as excinfo:
Main().start()

mock_loguru_error.assert_called_once_with(exception)
assert excinfo.value.code == 1

0 comments on commit 60e1ebf

Please sign in to comment.