From a251b4027d931d7687b4714353fcc58aee72f82b Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Tue, 15 Jun 2021 21:14:21 +1200 Subject: [PATCH 01/10] Issue 144: Option to copy instead of move file --- mnamer/setting_store.py | 12 +++++++++++- mnamer/target.py | 15 ++++++++++++--- mnamer/types.py | 6 ++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index 24311d64..3a3204f8 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_mode: Optional[RelocateType] = dataclasses.field( + default=RelocateType.DEFAULT.value, + metadata=SettingSpec( + dest="relocation_mode", + choices=[ix.name for ix in RelocateType], + flags=["--link"], + group=SettingType.PARAMETER, + help=f"--link={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link instead of move files", + )(), + ) # directive attributes ----------------------------------------------------- diff --git a/mnamer/target.py b/mnamer/target.py index ad25b543..330fe3e6 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -1,5 +1,5 @@ from datetime import date -from os import path +from os import path,symlink,link from pathlib import Path, PurePath from shutil import move from typing import Dict, List, Optional, Set, Union @@ -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,12 @@ class Target: _parsed_metadata: Metadata source: PurePath + _relocation_mode = { + RelocateType.DEFAULT.value: move, + RelocateType.HARDLINK.value: link, + RelocateType.SYMBOLICLINK.value: symlink, + } + def __init__(self, file_path: Path, settings: SettingStore = None): self._settings = settings or SettingStore() self._has_moved: False @@ -244,6 +250,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_mode[ + self._settings.relocation_mode + ] + 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..8e43ccde 100644 --- a/mnamer/types.py +++ b/mnamer/types.py @@ -25,6 +25,12 @@ class ProviderType(Enum): OMDB = "omdb" +class RelocateType(Enum): + DEFAULT = "copy" + HARDLINK = "hard" + SYMBOLICLINK = "sym" + + class SettingType(Enum): DIRECTIVE = "directive" PARAMETER = "parameter" From 0568fc75b14df3b631953a387ee9a4cacabc7ca6 Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Tue, 15 Jun 2021 21:38:56 +1200 Subject: [PATCH 02/10] Updated to accept string+RelocationType instance --- mnamer/setting_store.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index 3a3204f8..6568b14a 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -218,11 +218,11 @@ class SettingStore: help="--episode-format: set episode renaming format specification", )(), ) - relocation_mode: Optional[RelocateType] = dataclasses.field( + relocation_mode: Optional[Union[RelocateType,str]] = dataclasses.field( default=RelocateType.DEFAULT.value, metadata=SettingSpec( dest="relocation_mode", - choices=[ix.name for ix in RelocateType], + choices=[ix.value for ix in RelocateType], flags=["--link"], group=SettingType.PARAMETER, help=f"--link={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link instead of move files", From 971989f93f6742118a7dd1c423f95995d0a36c1d Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:08:05 +1200 Subject: [PATCH 03/10] Enum value name to describe move operation, add copy operation as option --- mnamer/setting_store.py | 4 ++-- mnamer/target.py | 5 +++-- mnamer/types.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index 6568b14a..94209f12 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -218,14 +218,14 @@ class SettingStore: help="--episode-format: set episode renaming format specification", )(), ) - relocation_mode: Optional[Union[RelocateType,str]] = dataclasses.field( + relocation_mode: Optional[Union[RelocateType, str]] = dataclasses.field( default=RelocateType.DEFAULT.value, metadata=SettingSpec( dest="relocation_mode", choices=[ix.value for ix in RelocateType], flags=["--link"], group=SettingType.PARAMETER, - help=f"--link={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link instead of move files", + help=f"--link={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link or copy instead of move files", )(), ) diff --git a/mnamer/target.py b/mnamer/target.py index 330fe3e6..14f83c4e 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -1,7 +1,7 @@ from datetime import date -from os import path,symlink,link +from os import path, symlink, link from pathlib import Path, PurePath -from shutil import move +from shutil import move, copy2 from typing import Dict, List, Optional, Set, Union from guessit import guessit @@ -41,6 +41,7 @@ class Target: RelocateType.DEFAULT.value: move, RelocateType.HARDLINK.value: link, RelocateType.SYMBOLICLINK.value: symlink, + RelocateType.COPY.value: copy2, } def __init__(self, file_path: Path, settings: SettingStore = None): diff --git a/mnamer/types.py b/mnamer/types.py index 8e43ccde..09323c71 100644 --- a/mnamer/types.py +++ b/mnamer/types.py @@ -26,9 +26,10 @@ class ProviderType(Enum): class RelocateType(Enum): - DEFAULT = "copy" + DEFAULT = "move" HARDLINK = "hard" SYMBOLICLINK = "sym" + COPY = "copy" class SettingType(Enum): From d602c97716cd80dd87b9ecfc986a29a08c22550e Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:18:17 +1200 Subject: [PATCH 04/10] Add copy with metadata op. Rename option --- mnamer/setting_store.py | 4 ++-- mnamer/target.py | 5 +++-- mnamer/types.py | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index 94209f12..bf664b5e 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -223,9 +223,9 @@ class SettingStore: metadata=SettingSpec( dest="relocation_mode", choices=[ix.value for ix in RelocateType], - flags=["--link"], + flags=["--relocate-operation"], group=SettingType.PARAMETER, - help=f"--link={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link or copy instead of move files", + help=f"--relocate-operation={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link or copy instead of move files", )(), ) diff --git a/mnamer/target.py b/mnamer/target.py index 14f83c4e..cba2113b 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -1,7 +1,7 @@ from datetime import date from os import path, symlink, link from pathlib import Path, PurePath -from shutil import move, copy2 +from shutil import move, copy, copy2 from typing import Dict, List, Optional, Set, Union from guessit import guessit @@ -41,7 +41,8 @@ class Target: RelocateType.DEFAULT.value: move, RelocateType.HARDLINK.value: link, RelocateType.SYMBOLICLINK.value: symlink, - RelocateType.COPY.value: copy2, + RelocateType.COPY2.value: copy2, + RelocateType.COPY.value: copy, } def __init__(self, file_path: Path, settings: SettingStore = None): diff --git a/mnamer/types.py b/mnamer/types.py index 09323c71..abe77a98 100644 --- a/mnamer/types.py +++ b/mnamer/types.py @@ -30,6 +30,7 @@ class RelocateType(Enum): HARDLINK = "hard" SYMBOLICLINK = "sym" COPY = "copy" + COPY2 = "copy-with-metadata" class SettingType(Enum): From 41da18b4159cd87399fd4090ee15e127f1e80daf Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:27:13 +1200 Subject: [PATCH 05/10] Make user help message succinct/sensible --- mnamer/setting_store.py | 2 +- mnamer/types.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index bf664b5e..d4858fee 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -225,7 +225,7 @@ class SettingStore: choices=[ix.value for ix in RelocateType], flags=["--relocate-operation"], group=SettingType.PARAMETER, - help=f"--relocate-operation={'|'.join([ix.value for ix in RelocateType])}: when given, {'|'.join([ix.value for ix in RelocateType])} link or copy instead of move files", + help=f"--relocate-operation={'|'.join([ix.value for ix in RelocateType])}: when given, link, copy or move files. Default move.", )(), ) diff --git a/mnamer/types.py b/mnamer/types.py index abe77a98..b044493d 100644 --- a/mnamer/types.py +++ b/mnamer/types.py @@ -27,8 +27,8 @@ class ProviderType(Enum): class RelocateType(Enum): DEFAULT = "move" - HARDLINK = "hard" - SYMBOLICLINK = "sym" + HARDLINK = "hardlink" + SYMBOLICLINK = "symlink" COPY = "copy" COPY2 = "copy-with-metadata" From 5132412152ea51f5062b7d150f84671d9e5a3a62 Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:39:18 +1200 Subject: [PATCH 06/10] Better variable names --- mnamer/setting_store.py | 8 ++++---- mnamer/target.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mnamer/setting_store.py b/mnamer/setting_store.py index d4858fee..aca97bb1 100644 --- a/mnamer/setting_store.py +++ b/mnamer/setting_store.py @@ -218,14 +218,14 @@ class SettingStore: help="--episode-format: set episode renaming format specification", )(), ) - relocation_mode: Optional[Union[RelocateType, str]] = dataclasses.field( + relocation_strategy: Optional[Union[RelocateType, str]] = dataclasses.field( default=RelocateType.DEFAULT.value, metadata=SettingSpec( - dest="relocation_mode", + dest="relocation_strategy", choices=[ix.value for ix in RelocateType], - flags=["--relocate-operation"], + flags=["--relocation-operation"], group=SettingType.PARAMETER, - help=f"--relocate-operation={'|'.join([ix.value for ix in RelocateType])}: when given, link, copy or move files. Default move.", + help=f"--relocation-operation={'|'.join([ix.value for ix in RelocateType])}: when given, link, copy or move files. Default move.", )(), ) diff --git a/mnamer/target.py b/mnamer/target.py index cba2113b..5416eecb 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -37,7 +37,7 @@ class Target: _parsed_metadata: Metadata source: PurePath - _relocation_mode = { + _relocation_strategy = { RelocateType.DEFAULT.value: move, RelocateType.HARDLINK.value: link, RelocateType.SYMBOLICLINK.value: symlink, @@ -252,8 +252,8 @@ def relocate(self) -> None: destination_path = Path(self.destination).resolve() destination_path.parent.mkdir(parents=True, exist_ok=True) try: - relocate_function = self._relocation_mode[ - self._settings.relocation_mode + relocate_function = self._relocation_strategy[ + self._settings.relocation_strategy ] relocate_function(self.source, destination_path) except OSError: # pragma: no cover From 2d0e40f29d827c638a6239bb300584ff7f407f73 Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:52:37 +1200 Subject: [PATCH 07/10] Tests for relocation flags. Change test suite name to relocation from move --- .../{test_moving.py => test_relocation.py} | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) rename tests/e2e/{test_moving.py => test_relocation.py} (82%) diff --git a/tests/e2e/test_moving.py b/tests/e2e/test_relocation.py similarity index 82% rename from tests/e2e/test_moving.py rename to tests/e2e/test_relocation.py index 51c3658f..080ddc48 100644 --- a/tests/e2e/test_moving.py +++ b/tests/e2e/test_relocation.py @@ -1,12 +1,59 @@ 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=hardlink"), 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=symlink"), 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=symlink"), 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.usefixtures("setup_test_dir") def test_complex_metadata(e2e_run, setup_test_files): setup_test_files( From ccaf5214d21e78fae4e16f5333888154e9e0adea Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 16:56:38 +1200 Subject: [PATCH 08/10] Correct arguments to match test name --- tests/e2e/test_relocation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/test_relocation.py b/tests/e2e/test_relocation.py index 080ddc48..ea887259 100644 --- a/tests/e2e/test_relocation.py +++ b/tests/e2e/test_relocation.py @@ -26,21 +26,21 @@ def test_relocation_operation_copy(e2e_run, 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=hardlink"), setup_test_files + 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=symlink"), setup_test_files + 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=symlink"), setup_test_files + partial(e2e_run, "--relocation-operation=move"), setup_test_files ) From 0e288e39371b2466cd2f1cbf4912d4af2d009486 Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 17:03:08 +1200 Subject: [PATCH 09/10] Issue #136 --- mnamer/providers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(), From a28f18eb8a3ebc16a29df7785c56ca57dac5a20d Mon Sep 17 00:00:00 2001 From: Ben Tomlin Date: Wed, 16 Jun 2021 18:33:12 +1200 Subject: [PATCH 10/10] Sanitize paths embedded in --episode-format. Issues #127 #100 #139 #110 --- mnamer/target.py | 6 ++++++ tests/e2e/test_relocation.py | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/mnamer/target.py b/mnamer/target.py index 5416eecb..2bd47eb2 100644 --- a/mnamer/target.py +++ b/mnamer/target.py @@ -111,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) diff --git a/tests/e2e/test_relocation.py b/tests/e2e/test_relocation.py index ea887259..bccdb407 100644 --- a/tests/e2e/test_relocation.py +++ b/tests/e2e/test_relocation.py @@ -54,6 +54,26 @@ def _run_relocation_operation(e2e_run, setup_test_files): 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(