diff --git a/mnamer/providers.py b/mnamer/providers.py index b6f4278d..d636d803 100644 --- a/mnamer/providers.py +++ b/mnamer/providers.py @@ -276,7 +276,7 @@ def _search_id( season=entry["airedSeason"], series=series_data["data"]["seriesName"], language=language, - synopsis=(entry["overview"] or None) + synopsis=(entry["overview"] or "") .replace("\r\n", "") .replace(" ", "") .strip(), diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index 24311d64..aca97bb1 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -8,7 +8,7 @@ from mnamer.exceptions import MnamerException from mnamer.language import Language from mnamer.setting_spec import SettingSpec -from mnamer.types import MediaType, ProviderType, SettingType +from mnamer.types import MediaType, ProviderType, SettingType, RelocateType from mnamer.utils import crawl_out, json_loads, normalize_containers __all__ = ["SettingStore"] @@ -218,6 +218,16 @@ class SettingStore: help="--episode-format: set episode renaming format specification", )(), ) + relocation_strategy: Optional[Union[RelocateType, str]] = dataclasses.field( + default=RelocateType.DEFAULT.value, + metadata=SettingSpec( + dest="relocation_strategy", + choices=[ix.value for ix in RelocateType], + flags=["--relocation-operation"], + group=SettingType.PARAMETER, + help=f"--relocation-operation={'|'.join([ix.value for ix in RelocateType])}: when given, link, copy or move files. Default move.", + )(), + ) # directive attributes ----------------------------------------------------- diff --git a/mnamer/target.py b/mnamer/target.py index ad25b543..2bd47eb2 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -1,7 +1,7 @@ from datetime import date -from os import path +from os import path, symlink, link from pathlib import Path, PurePath -from shutil import move +from shutil import move, copy, copy2 from typing import Dict, List, Optional, Set, Union from guessit import guessit @@ -11,7 +11,7 @@ from mnamer.metadata import Metadata, MetadataEpisode, MetadataMovie from mnamer.providers import Provider from mnamer.setting_store import SettingStore -from mnamer.types import MediaType, ProviderType +from mnamer.types import MediaType, ProviderType, RelocateType from mnamer.utils import ( crawl_in, filename_replace, @@ -37,6 +37,14 @@ class Target: _parsed_metadata: Metadata source: PurePath + _relocation_strategy = { + RelocateType.DEFAULT.value: move, + RelocateType.HARDLINK.value: link, + RelocateType.SYMBOLICLINK.value: symlink, + RelocateType.COPY2.value: copy2, + RelocateType.COPY.value: copy, + } + def __init__(self, file_path: Path, settings: SettingStore = None): self._settings = settings or SettingStore() self._has_moved: False @@ -103,6 +111,12 @@ def destination(self) -> PurePath: ) file_path = self._make_path(file_path) dir_tail, filename = path.split(file_path) + + # Required to sanitize paths that have been inserted into --episode-format + dir_tail = self._make_path( + *[str_sanitize(px) for px in self._make_path(dir_tail).parts] + ) + filename = filename_replace(filename, self._settings.replace_after) if self._settings.scene: filename = str_scenify(filename) @@ -244,6 +258,9 @@ def relocate(self) -> None: destination_path = Path(self.destination).resolve() destination_path.parent.mkdir(parents=True, exist_ok=True) try: - move(self.source, destination_path) + relocate_function = self._relocation_strategy[ + self._settings.relocation_strategy + ] + relocate_function(self.source, destination_path) except OSError: # pragma: no cover raise MnamerException diff --git a/mnamer/types.py b/mnamer/types.py index 8d2a251d..b044493d 100644 --- a/mnamer/types.py +++ b/mnamer/types.py @@ -25,6 +25,14 @@ class ProviderType(Enum): OMDB = "omdb" +class RelocateType(Enum): + DEFAULT = "move" + HARDLINK = "hardlink" + SYMBOLICLINK = "symlink" + COPY = "copy" + COPY2 = "copy-with-metadata" + + class SettingType(Enum): DIRECTIVE = "directive" PARAMETER = "parameter" diff --git a/tests/e2e/test_moving.py b/tests/e2e/test_relocation.py similarity index 76% rename from tests/e2e/test_moving.py rename to tests/e2e/test_relocation.py index 51c3658f..bccdb407 100644 --- a/tests/e2e/test_moving.py +++ b/tests/e2e/test_relocation.py @@ -1,12 +1,79 @@ from pathlib import Path import pytest +from functools import partial from mnamer.const import SUBTITLE_CONTAINERS pytestmark = pytest.mark.e2e +@pytest.mark.usefixtures("setup_test_dir") +def test_relocation_operation_copy2(e2e_run, setup_test_files): + _run_relocation_operation( + partial(e2e_run, "--relocation-operation=copy-with-metadata"), + setup_test_files, + ) + + +@pytest.mark.usefixtures("setup_test_dir") +def test_relocation_operation_copy(e2e_run, setup_test_files): + _run_relocation_operation( + partial(e2e_run, "--relocation-operation=copy"), setup_test_files + ) + + +@pytest.mark.usefixtures("setup_test_dir") +def test_relocation_operation_symlink(e2e_run, setup_test_files): + _run_relocation_operation( + partial(e2e_run, "--relocation-operation=symlink"), setup_test_files + ) + + +@pytest.mark.usefixtures("setup_test_dir") +def test_relocation_operation_hardlink(e2e_run, setup_test_files): + _run_relocation_operation( + partial(e2e_run, "--relocation-operation=hardlink"), setup_test_files + ) + + +@pytest.mark.usefixtures("setup_test_dir") +def test_relocation_operation_move(e2e_run, setup_test_files): + _run_relocation_operation( + partial(e2e_run, "--relocation-operation=move"), setup_test_files + ) + + +def _run_relocation_operation(e2e_run, setup_test_files): + setup_test_files( + "Quien a hierro mata [MicroHD 1080p][DTS 5.1-Castellano-AC3 5.1-Castellano+Subs][ES-EN].mkv" + ) + result = e2e_run("--batch", "--media=movie", ".") + assert result.code == 0 + assert "Quien a Hierro Mata (2019).mkv" in result.out + assert "1 out of 1 files processed successfully" in result.out + + +@pytest.mark.tvdb +@pytest.mark.usefixtures("setup_test_dir") +def test_sanitize_episode_format_path(e2e_run, setup_test_files): + setup_test_files( + "The.Great.Fire.In.Real.Time.S01E01.1080p.HEVC.x265-MeGusta.mkv" + ) + result = e2e_run( + "--batch", + "--episode-api=tvdb", + "--episode-format='{series}/{series}'", + ".", + ) + print(result.out) + assert result.code == 0 + assert ( + "The Great Fire in Real Time/The Great Fire in Real Time" in result.out + ) + assert "1 out of 1 files processed successfully" in result.out + + @pytest.mark.usefixtures("setup_test_dir") def test_complex_metadata(e2e_run, setup_test_files): setup_test_files(