mnamer 2.7.2.dev7__tar.gz → 2.7.3.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/publish.yml +3 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/pull_request.yml +3 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/push.yml +14 -0
- {mnamer-2.7.2.dev7/mnamer.egg-info → mnamer-2.7.3.dev2}/PKG-INFO +1 -1
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__version__.py +1 -1
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/providers.py +9 -6
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/setting_store.py +2 -6
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/target.py +51 -11
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2/mnamer.egg-info}/PKG-INFO +1 -1
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_moving.py +34 -0
- mnamer-2.7.3.dev2/tests/local/test_target.py +241 -0
- mnamer-2.7.2.dev7/tests/local/test_target.py +0 -123
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.dockerignore +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/init/action.yml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/lint/action.yml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/test/action.yml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/dependabot.yml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/publish-docker.yml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.gitignore +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.python-version +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.vscode/settings.json +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/AGENTS.md +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/CLAUDE.md +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/Dockerfile +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/LICENSE.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/MANIFEST.in +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/README.md +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/design.eps +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo-2.png +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo-3.png +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo.png +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/recording.mov +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/screenshot.eps +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/screenshot.png +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/makefile +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__init__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__main__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/argument.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/const.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/endpoints.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/exceptions.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/frontends.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/language.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/metadata.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/py.typed +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/setting_spec.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/tty.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/types.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/utils.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/SOURCES.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/dependency_links.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/entry_points.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/requires.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/top_level.txt +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/pyproject.toml +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/pytest.ini +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/setup.cfg +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/__init__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/conftest.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/__init__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/conftest.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_directives.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_errors.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/__init__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_argument.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_language.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_metadata.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_setting_spec.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_setting_store.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_tty.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_utils.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/__init__.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__omdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tmdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tvdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tvmaze.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__omdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tmdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tvdb.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tvmaze.py +0 -0
- {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/uv.lock +0 -0
|
@@ -11,6 +11,9 @@ on:
|
|
|
11
11
|
- published
|
|
12
12
|
- prereleased
|
|
13
13
|
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
|
|
14
17
|
jobs:
|
|
15
18
|
lint:
|
|
16
19
|
runs-on: ubuntu-latest
|
|
@@ -50,6 +53,9 @@ jobs:
|
|
|
50
53
|
secrets:
|
|
51
54
|
inherit
|
|
52
55
|
|
|
56
|
+
permissions:
|
|
57
|
+
contents: read
|
|
58
|
+
|
|
53
59
|
uses: ./.github/workflows/publish.yml
|
|
54
60
|
|
|
55
61
|
publish-docker-main:
|
|
@@ -65,6 +71,10 @@ jobs:
|
|
|
65
71
|
secrets:
|
|
66
72
|
inherit
|
|
67
73
|
|
|
74
|
+
permissions:
|
|
75
|
+
contents: read
|
|
76
|
+
packages: write
|
|
77
|
+
|
|
68
78
|
uses: ./.github/workflows/publish-docker.yml
|
|
69
79
|
with:
|
|
70
80
|
target: latest-sha
|
|
@@ -85,6 +95,10 @@ jobs:
|
|
|
85
95
|
secrets:
|
|
86
96
|
inherit
|
|
87
97
|
|
|
98
|
+
permissions:
|
|
99
|
+
contents: read
|
|
100
|
+
packages: write
|
|
101
|
+
|
|
88
102
|
uses: ./.github/workflows/publish-docker.yml
|
|
89
103
|
with:
|
|
90
104
|
target: event-release
|
|
@@ -31,7 +31,7 @@ from mnamer.language import Language
|
|
|
31
31
|
from mnamer.metadata import Metadata, MetadataEpisode, MetadataMovie
|
|
32
32
|
from mnamer.setting_store import SettingStore
|
|
33
33
|
from mnamer.types import MediaType, ProviderType
|
|
34
|
-
from mnamer.utils import parse_date, year_range_parse
|
|
34
|
+
from mnamer.utils import parse_date, year_parse, year_range_parse
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
class Provider[M: Metadata](ABC):
|
|
@@ -212,6 +212,7 @@ class Tmdb(Provider[MetadataMovie]):
|
|
|
212
212
|
self, name: str, year: str | None, language: Language | None
|
|
213
213
|
) -> Iterator[MetadataMovie]:
|
|
214
214
|
assert self.api_key
|
|
215
|
+
requested_year = year_parse(year) if year else None
|
|
215
216
|
page = 1
|
|
216
217
|
page_max = 5 # each page yields a maximum of 20 results
|
|
217
218
|
found = False
|
|
@@ -226,17 +227,19 @@ class Tmdb(Provider[MetadataMovie]):
|
|
|
226
227
|
)
|
|
227
228
|
for entry in response["results"]:
|
|
228
229
|
try:
|
|
229
|
-
|
|
230
|
+
result_year = year_parse(entry.get("release_date", ""))
|
|
231
|
+
if result_year is None:
|
|
232
|
+
continue
|
|
233
|
+
if requested_year and result_year != requested_year:
|
|
234
|
+
continue
|
|
235
|
+
found = True
|
|
236
|
+
yield MetadataMovie(
|
|
230
237
|
id_tmdb=str(entry["id"]),
|
|
231
238
|
name=entry["title"],
|
|
232
239
|
language=language,
|
|
233
240
|
synopsis=entry.get("overview"),
|
|
234
241
|
year=entry.get("release_date"),
|
|
235
242
|
)
|
|
236
|
-
if not meta.year:
|
|
237
|
-
continue
|
|
238
|
-
yield meta
|
|
239
|
-
found = True
|
|
240
243
|
except (AttributeError, KeyError, TypeError, ValueError):
|
|
241
244
|
continue
|
|
242
245
|
if page == response["total_pages"]:
|
|
@@ -366,20 +366,16 @@ class SettingStore:
|
|
|
366
366
|
if f.metadata
|
|
367
367
|
]
|
|
368
368
|
|
|
369
|
-
@staticmethod
|
|
370
|
-
def _resolve_path(path: str | Path) -> Path:
|
|
371
|
-
return Path(path).resolve()
|
|
372
|
-
|
|
373
369
|
@override
|
|
374
370
|
def __setattr__(self, key: str, value: Any):
|
|
375
371
|
converter_map: dict[str, Callable[[Any], Any]] = {
|
|
376
372
|
"episode_api": ProviderType,
|
|
377
|
-
"episode_directory":
|
|
373
|
+
"episode_directory": Path,
|
|
378
374
|
"language": Language.parse,
|
|
379
375
|
"mask": normalize_containers,
|
|
380
376
|
"media": MediaType,
|
|
381
377
|
"movie_api": ProviderType,
|
|
382
|
-
"movie_directory":
|
|
378
|
+
"movie_directory": Path,
|
|
383
379
|
"targets": lambda targets: [Path(target) for target in targets],
|
|
384
380
|
}
|
|
385
381
|
converter: Callable[[Any], Any] | None = converter_map.get(key)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
|
-
from os import path
|
|
5
4
|
from pathlib import Path
|
|
6
5
|
from shutil import move
|
|
7
6
|
from typing import Any, ClassVar, Self, override
|
|
@@ -94,21 +93,62 @@ class Target:
|
|
|
94
93
|
preferences.
|
|
95
94
|
"""
|
|
96
95
|
if self.directory:
|
|
97
|
-
|
|
98
|
-
dir_head_ = str_sanitize(dir_head_)
|
|
99
|
-
dir_head = Path(dir_head_)
|
|
96
|
+
dir_head = self._format_directory(self.directory)
|
|
100
97
|
else:
|
|
101
98
|
dir_head = self.source.parent
|
|
99
|
+
|
|
102
100
|
file_path = format(self.metadata, self._settings.formatting_for(self.metadata))
|
|
103
|
-
dir_tail, filename =
|
|
104
|
-
|
|
101
|
+
dir_tail, filename = self._split_formatted_path(file_path)
|
|
102
|
+
directory = Path(dir_head, self._process_directory(dir_tail))
|
|
103
|
+
filename = self._process_filename(filename)
|
|
104
|
+
return Path(directory, filename).resolve()
|
|
105
|
+
|
|
106
|
+
def _format_directory(self, directory: Path) -> Path:
|
|
107
|
+
"""Format and post-process a configured directory template.
|
|
108
|
+
|
|
109
|
+
Each part of the original (un-resolved) directory is formatted
|
|
110
|
+
independently so we can tell template substitutions apart from literal
|
|
111
|
+
user-typed parts. For relative paths every part is transformed; for
|
|
112
|
+
absolute paths only template parts are, keeping literal filesystem
|
|
113
|
+
prefixes like ``/Volumes/Media`` intact.
|
|
114
|
+
"""
|
|
115
|
+
is_absolute = directory.is_absolute()
|
|
116
|
+
processed_parts: list[str] = []
|
|
117
|
+
for original_part in directory.parts:
|
|
118
|
+
formatted_part = format(self.metadata, original_part)
|
|
119
|
+
if not is_absolute or "{" in original_part:
|
|
120
|
+
formatted_part = self._process_path_text(formatted_part)
|
|
121
|
+
processed_parts.append(formatted_part)
|
|
122
|
+
return Path(*processed_parts) if processed_parts else Path()
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _split_formatted_path(file_path: str) -> tuple[Path, str]:
|
|
126
|
+
"""Split a formatted file template into optional directories and filename."""
|
|
127
|
+
formatted_path = Path(file_path)
|
|
128
|
+
dir_tail = formatted_path.parent
|
|
129
|
+
if str(dir_tail) == ".":
|
|
130
|
+
dir_tail = Path()
|
|
131
|
+
return dir_tail, formatted_path.name
|
|
132
|
+
|
|
133
|
+
def _process_directory(self, directory: Path) -> Path:
|
|
134
|
+
"""Apply filename post-processing rules to each generated directory path."""
|
|
135
|
+
parts = tuple(self._process_path_text(part) for part in directory.parts)
|
|
136
|
+
return Path(*parts) if parts else Path()
|
|
137
|
+
|
|
138
|
+
def _process_filename(self, filename: str) -> str:
|
|
139
|
+
"""Apply configured post-processing rules to a generated filename."""
|
|
140
|
+
return self._process_path_text(filename)
|
|
141
|
+
|
|
142
|
+
def _process_path_text(self, value: str) -> str:
|
|
143
|
+
"""Apply replacement, scene, lower, and sanitize transforms in one place."""
|
|
144
|
+
if value in (".", ".."):
|
|
145
|
+
return value
|
|
146
|
+
value = filename_replace(value, self._settings.replace_after)
|
|
105
147
|
if self._settings.scene:
|
|
106
|
-
|
|
148
|
+
value = str_scenify(value)
|
|
107
149
|
if self._settings.lower:
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
directory = Path(dir_head, dir_tail)
|
|
111
|
-
return Path(directory, filename)
|
|
150
|
+
value = value.lower()
|
|
151
|
+
return str_sanitize(value)
|
|
112
152
|
|
|
113
153
|
def _parse(self, file_path: Path):
|
|
114
154
|
path_data: dict[str, Any] = {"language": self._settings.language}
|
|
@@ -145,6 +145,40 @@ def test_multiple_nested_directories(e2e_run, setup_test_files):
|
|
|
145
145
|
assert expected in result.out
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
@pytest.mark.usefixtures("setup_test_dir")
|
|
149
|
+
def test_lower_directory(e2e_run, setup_test_files):
|
|
150
|
+
setup_test_files("Ninja Turtles (1990).mkv")
|
|
151
|
+
result = e2e_run(
|
|
152
|
+
"--batch",
|
|
153
|
+
"--lower",
|
|
154
|
+
"--movie_directory=Movies/{name[0]}",
|
|
155
|
+
"Ninja Turtles (1990).mkv",
|
|
156
|
+
)
|
|
157
|
+
expected = str(Path("/movies/t/teenage mutant ninja turtles (1990).mkv"))
|
|
158
|
+
assert result.code == 0
|
|
159
|
+
assert expected in result.out
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.usefixtures("setup_test_dir")
|
|
163
|
+
def test_scene_directory(e2e_run, setup_test_files):
|
|
164
|
+
setup_test_files("Ninja Turtles (1990).mkv")
|
|
165
|
+
result = e2e_run(
|
|
166
|
+
"--batch",
|
|
167
|
+
"--scene",
|
|
168
|
+
"--movie_directory=Movie Library/{name}",
|
|
169
|
+
"Ninja Turtles (1990).mkv",
|
|
170
|
+
)
|
|
171
|
+
expected = str(
|
|
172
|
+
Path(
|
|
173
|
+
"/movie.library"
|
|
174
|
+
"/teenage.mutant.ninja.turtles"
|
|
175
|
+
"/teenage.mutant.ninja.turtles.1990.mkv"
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
assert result.code == 0
|
|
179
|
+
assert expected in result.out
|
|
180
|
+
|
|
181
|
+
|
|
148
182
|
@pytest.mark.omdb
|
|
149
183
|
@pytest.mark.usefixtures("setup_test_dir")
|
|
150
184
|
def test_format_id(e2e_run, setup_test_files):
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from mnamer.metadata import MetadataEpisode, MetadataMovie
|
|
7
|
+
from mnamer.setting_store import SettingStore
|
|
8
|
+
from mnamer.target import Target
|
|
9
|
+
from mnamer.types import MediaType
|
|
10
|
+
|
|
11
|
+
pytestmark = pytest.mark.local
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parse__media__movie():
|
|
15
|
+
target = Target(Path("ninja turtles (1990).mkv"), SettingStore())
|
|
16
|
+
assert target.metadata.to_media_type() is MediaType.MOVIE
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parse__media__episode():
|
|
20
|
+
target = Target(Path("ninja turtles s01e01.mkv"), SettingStore())
|
|
21
|
+
assert target.metadata.to_media_type() is MediaType.EPISODE
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_parse__quality():
|
|
25
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mkv")
|
|
26
|
+
target = Target(file_path, SettingStore())
|
|
27
|
+
assert target.metadata.quality == "1080p dolby digital"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_parse__group():
|
|
31
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mkv")
|
|
32
|
+
target = Target(file_path, SettingStore())
|
|
33
|
+
assert target.metadata.group == "RARGB"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_parse__container():
|
|
37
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
38
|
+
target = Target(file_path, SettingStore())
|
|
39
|
+
assert target.metadata.container == ".mp4"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_parse__date():
|
|
43
|
+
file_path = Path("the.colbert.show.2010.10.01.avi")
|
|
44
|
+
target = Target(file_path, SettingStore())
|
|
45
|
+
assert isinstance(target.metadata, MetadataEpisode)
|
|
46
|
+
assert target.metadata.date == dt.date(2010, 10, 1)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_parse__episode():
|
|
50
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
51
|
+
target = Target(file_path, SettingStore())
|
|
52
|
+
assert isinstance(target.metadata, MetadataEpisode)
|
|
53
|
+
assert target.metadata.episode == 4
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_parse__season():
|
|
57
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
58
|
+
target = Target(file_path, SettingStore())
|
|
59
|
+
assert isinstance(target.metadata, MetadataEpisode)
|
|
60
|
+
assert target.metadata.season == 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_parse__series():
|
|
64
|
+
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
65
|
+
target = Target(file_path, SettingStore())
|
|
66
|
+
assert isinstance(target.metadata, MetadataEpisode)
|
|
67
|
+
assert target.metadata.series == "Ninja Turtles"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_parse__year():
|
|
71
|
+
file_path = Path("the.goonies.1985")
|
|
72
|
+
target = Target(file_path, SettingStore())
|
|
73
|
+
assert isinstance(target.metadata, MetadataMovie)
|
|
74
|
+
assert target.metadata.year == 1985
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def testparse__name():
|
|
78
|
+
file_path = Path("the.goonies.1985")
|
|
79
|
+
target = Target(file_path, SettingStore())
|
|
80
|
+
assert isinstance(target.metadata, MetadataMovie)
|
|
81
|
+
assert target.metadata.name == "The Goonies"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize("media", MediaType)
|
|
85
|
+
def test_media__override(media: MediaType):
|
|
86
|
+
target = Target(Path(), SettingStore(media=media))
|
|
87
|
+
assert target.metadata.to_media_type() == media
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_directory__movie():
|
|
91
|
+
movie_path = Path("/some/movie/path").absolute()
|
|
92
|
+
target = Target(
|
|
93
|
+
Path(), SettingStore(media=MediaType.MOVIE, movie_directory=movie_path)
|
|
94
|
+
)
|
|
95
|
+
assert target.directory == movie_path
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_directory__episode():
|
|
99
|
+
episode_path = Path("/some/episode/path").absolute()
|
|
100
|
+
target = Target(
|
|
101
|
+
Path(),
|
|
102
|
+
SettingStore(media=MediaType.EPISODE, episode_directory=episode_path),
|
|
103
|
+
)
|
|
104
|
+
assert target.directory == episode_path
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_ambiguous_subtitle_language():
|
|
108
|
+
target = Target(
|
|
109
|
+
Path("Subs/Nancy.Drew.S01E01.WEBRip.x264-ION10.srt"), SettingStore()
|
|
110
|
+
)
|
|
111
|
+
assert target.metadata.language is None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_destination__simple():
|
|
115
|
+
pass # TODO
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_destination__relative_directory_lowered():
|
|
119
|
+
"""Every part of a relative configured directory receives --lower."""
|
|
120
|
+
settings = SettingStore(
|
|
121
|
+
batch=True,
|
|
122
|
+
media=MediaType.MOVIE,
|
|
123
|
+
lower=True,
|
|
124
|
+
movie_directory=Path("Movies/{name[0]}"),
|
|
125
|
+
)
|
|
126
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
127
|
+
assert target.destination == Path("movies/n/ninja turtles (1990).mkv").resolve()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_destination__absolute_directory_preserves_literal_parts():
|
|
131
|
+
"""Literal parts of an absolute configured directory survive --lower."""
|
|
132
|
+
settings = SettingStore(
|
|
133
|
+
batch=True,
|
|
134
|
+
media=MediaType.MOVIE,
|
|
135
|
+
lower=True,
|
|
136
|
+
movie_directory=Path("/Media Library/Movies"),
|
|
137
|
+
)
|
|
138
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
139
|
+
assert target.destination == Path("/Media Library/Movies/ninja turtles (1990).mkv")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_destination__absolute_directory_transforms_template_parts():
|
|
143
|
+
"""Template parts within an absolute configured directory are transformed."""
|
|
144
|
+
settings = SettingStore(
|
|
145
|
+
batch=True,
|
|
146
|
+
media=MediaType.MOVIE,
|
|
147
|
+
lower=True,
|
|
148
|
+
movie_directory=Path("/Media Library/{name[0]}"),
|
|
149
|
+
)
|
|
150
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
151
|
+
assert target.destination == Path("/Media Library/n/ninja turtles (1990).mkv")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_destination__format_template_directory_components_transformed():
|
|
155
|
+
"""Directory components emitted by the format template are post-processed."""
|
|
156
|
+
settings = SettingStore(
|
|
157
|
+
batch=True,
|
|
158
|
+
media=MediaType.MOVIE,
|
|
159
|
+
lower=True,
|
|
160
|
+
movie_format="{name}/{name} ({year}).{extension}",
|
|
161
|
+
)
|
|
162
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
163
|
+
assert (
|
|
164
|
+
target.destination == Path("ninja turtles/ninja turtles (1990).mkv").resolve()
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_destination__relative_directory_scene():
|
|
169
|
+
"""--scene applies to literal and templated parts of a relative directory."""
|
|
170
|
+
settings = SettingStore(
|
|
171
|
+
batch=True,
|
|
172
|
+
media=MediaType.MOVIE,
|
|
173
|
+
scene=True,
|
|
174
|
+
movie_directory=Path("Movie Library/{name}"),
|
|
175
|
+
)
|
|
176
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
177
|
+
assert (
|
|
178
|
+
target.destination
|
|
179
|
+
== Path("movie.library/ninja.turtles/ninja.turtles.1990.mkv").resolve()
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def test_destination__absolute_directory_scene_preserves_literal_parts():
|
|
184
|
+
"""Literal parts of an absolute configured directory survive --scene."""
|
|
185
|
+
settings = SettingStore(
|
|
186
|
+
batch=True,
|
|
187
|
+
media=MediaType.MOVIE,
|
|
188
|
+
scene=True,
|
|
189
|
+
movie_directory=Path("/Media Library/Movies"),
|
|
190
|
+
)
|
|
191
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
192
|
+
assert target.destination == Path("/Media Library/Movies/ninja.turtles.1990.mkv")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_destination__absolute_directory_scene_transforms_template_parts():
|
|
196
|
+
"""Template parts within an absolute configured directory survive --scene."""
|
|
197
|
+
settings = SettingStore(
|
|
198
|
+
batch=True,
|
|
199
|
+
media=MediaType.MOVIE,
|
|
200
|
+
scene=True,
|
|
201
|
+
movie_directory=Path("/Media Library/{name}"),
|
|
202
|
+
)
|
|
203
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
204
|
+
assert target.destination == Path(
|
|
205
|
+
"/Media Library/ninja.turtles/ninja.turtles.1990.mkv"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_destination__parent_directory_navigation_preserved():
|
|
210
|
+
"""A `..` segment in a relative directory survives sanitization."""
|
|
211
|
+
settings = SettingStore(
|
|
212
|
+
batch=True,
|
|
213
|
+
media=MediaType.MOVIE,
|
|
214
|
+
lower=True,
|
|
215
|
+
movie_directory=Path("../Movies"),
|
|
216
|
+
)
|
|
217
|
+
target = Target(Path("ninja turtles (1990).mkv"), settings)
|
|
218
|
+
assert target.destination == Path("../movies/ninja turtles (1990).mkv").resolve()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_destination__same_directory_matches_source(tmp_path, monkeypatch):
|
|
222
|
+
"""`--movie_directory=.` resolves to the source path so the no-op is skippable."""
|
|
223
|
+
tmp = tmp_path.resolve()
|
|
224
|
+
monkeypatch.chdir(tmp)
|
|
225
|
+
source = tmp / "Ninja Turtles (1990).mkv"
|
|
226
|
+
source.touch()
|
|
227
|
+
settings = SettingStore(
|
|
228
|
+
batch=True,
|
|
229
|
+
media=MediaType.MOVIE,
|
|
230
|
+
movie_directory=Path("."),
|
|
231
|
+
)
|
|
232
|
+
target = Target(source, settings)
|
|
233
|
+
assert target.destination == target.source
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_query():
|
|
237
|
+
pass # TODO
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_relocate():
|
|
241
|
+
pass # TODO
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import datetime as dt
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
|
|
6
|
-
from mnamer.metadata import MetadataEpisode, MetadataMovie
|
|
7
|
-
from mnamer.setting_store import SettingStore
|
|
8
|
-
from mnamer.target import Target
|
|
9
|
-
from mnamer.types import MediaType
|
|
10
|
-
|
|
11
|
-
pytestmark = pytest.mark.local
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_parse__media__movie():
|
|
15
|
-
target = Target(Path("ninja turtles (1990).mkv"), SettingStore())
|
|
16
|
-
assert target.metadata.to_media_type() is MediaType.MOVIE
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_parse__media__episode():
|
|
20
|
-
target = Target(Path("ninja turtles s01e01.mkv"), SettingStore())
|
|
21
|
-
assert target.metadata.to_media_type() is MediaType.EPISODE
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_parse__quality():
|
|
25
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mkv")
|
|
26
|
-
target = Target(file_path, SettingStore())
|
|
27
|
-
assert target.metadata.quality == "1080p dolby digital"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_parse__group():
|
|
31
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mkv")
|
|
32
|
-
target = Target(file_path, SettingStore())
|
|
33
|
-
assert target.metadata.group == "RARGB"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def test_parse__container():
|
|
37
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
38
|
-
target = Target(file_path, SettingStore())
|
|
39
|
-
assert target.metadata.container == ".mp4"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_parse__date():
|
|
43
|
-
file_path = Path("the.colbert.show.2010.10.01.avi")
|
|
44
|
-
target = Target(file_path, SettingStore())
|
|
45
|
-
assert isinstance(target.metadata, MetadataEpisode)
|
|
46
|
-
assert target.metadata.date == dt.date(2010, 10, 1)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_parse__episode():
|
|
50
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
51
|
-
target = Target(file_path, SettingStore())
|
|
52
|
-
assert isinstance(target.metadata, MetadataEpisode)
|
|
53
|
-
assert target.metadata.episode == 4
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def test_parse__season():
|
|
57
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
58
|
-
target = Target(file_path, SettingStore())
|
|
59
|
-
assert isinstance(target.metadata, MetadataEpisode)
|
|
60
|
-
assert target.metadata.season == 1
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_parse__series():
|
|
64
|
-
file_path = Path("ninja.turtles.s01e04.1080p.ac3.rargb.sample.mp4")
|
|
65
|
-
target = Target(file_path, SettingStore())
|
|
66
|
-
assert isinstance(target.metadata, MetadataEpisode)
|
|
67
|
-
assert target.metadata.series == "Ninja Turtles"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def test_parse__year():
|
|
71
|
-
file_path = Path("the.goonies.1985")
|
|
72
|
-
target = Target(file_path, SettingStore())
|
|
73
|
-
assert isinstance(target.metadata, MetadataMovie)
|
|
74
|
-
assert target.metadata.year == 1985
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def testparse__name():
|
|
78
|
-
file_path = Path("the.goonies.1985")
|
|
79
|
-
target = Target(file_path, SettingStore())
|
|
80
|
-
assert isinstance(target.metadata, MetadataMovie)
|
|
81
|
-
assert target.metadata.name == "The Goonies"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@pytest.mark.parametrize("media", MediaType)
|
|
85
|
-
def test_media__override(media: MediaType):
|
|
86
|
-
target = Target(Path(), SettingStore(media=media))
|
|
87
|
-
assert target.metadata.to_media_type() == media
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def test_directory__movie():
|
|
91
|
-
movie_path = Path("/some/movie/path").absolute()
|
|
92
|
-
target = Target(
|
|
93
|
-
Path(), SettingStore(media=MediaType.MOVIE, movie_directory=movie_path)
|
|
94
|
-
)
|
|
95
|
-
assert target.directory == movie_path
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def test_directory__episode():
|
|
99
|
-
episode_path = Path("/some/episode/path").absolute()
|
|
100
|
-
target = Target(
|
|
101
|
-
Path(),
|
|
102
|
-
SettingStore(media=MediaType.EPISODE, episode_directory=episode_path),
|
|
103
|
-
)
|
|
104
|
-
assert target.directory == episode_path
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def test_ambiguous_subtitle_language():
|
|
108
|
-
target = Target(
|
|
109
|
-
Path("Subs/Nancy.Drew.S01E01.WEBRip.x264-ION10.srt"), SettingStore()
|
|
110
|
-
)
|
|
111
|
-
assert target.metadata.language is None
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def test_destination__simple():
|
|
115
|
-
pass # TODO
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def test_query():
|
|
119
|
-
pass # TODO
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_relocate():
|
|
123
|
-
pass # TODO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|