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.
Files changed (81) hide show
  1. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/publish.yml +3 -0
  2. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/pull_request.yml +3 -0
  3. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/push.yml +14 -0
  4. {mnamer-2.7.2.dev7/mnamer.egg-info → mnamer-2.7.3.dev2}/PKG-INFO +1 -1
  5. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__version__.py +1 -1
  6. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/providers.py +9 -6
  7. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/setting_store.py +2 -6
  8. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/target.py +51 -11
  9. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2/mnamer.egg-info}/PKG-INFO +1 -1
  10. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_moving.py +34 -0
  11. mnamer-2.7.3.dev2/tests/local/test_target.py +241 -0
  12. mnamer-2.7.2.dev7/tests/local/test_target.py +0 -123
  13. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.dockerignore +0 -0
  14. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/init/action.yml +0 -0
  15. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/lint/action.yml +0 -0
  16. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/actions/test/action.yml +0 -0
  17. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/dependabot.yml +0 -0
  18. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.github/workflows/publish-docker.yml +0 -0
  19. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.gitignore +0 -0
  20. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.python-version +0 -0
  21. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/.vscode/settings.json +0 -0
  22. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/AGENTS.md +0 -0
  23. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/CLAUDE.md +0 -0
  24. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/Dockerfile +0 -0
  25. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/LICENSE.txt +0 -0
  26. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/MANIFEST.in +0 -0
  27. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/README.md +0 -0
  28. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/design.eps +0 -0
  29. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo-2.png +0 -0
  30. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo-3.png +0 -0
  31. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/logo.png +0 -0
  32. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/recording.mov +0 -0
  33. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/screenshot.eps +0 -0
  34. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/assets/screenshot.png +0 -0
  35. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/makefile +0 -0
  36. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__init__.py +0 -0
  37. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/__main__.py +0 -0
  38. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/argument.py +0 -0
  39. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/const.py +0 -0
  40. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/endpoints.py +0 -0
  41. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/exceptions.py +0 -0
  42. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/frontends.py +0 -0
  43. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/language.py +0 -0
  44. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/metadata.py +0 -0
  45. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/py.typed +0 -0
  46. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/setting_spec.py +0 -0
  47. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/tty.py +0 -0
  48. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/types.py +0 -0
  49. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer/utils.py +0 -0
  50. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/SOURCES.txt +0 -0
  51. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/dependency_links.txt +0 -0
  52. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/entry_points.txt +0 -0
  53. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/requires.txt +0 -0
  54. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/mnamer.egg-info/top_level.txt +0 -0
  55. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/pyproject.toml +0 -0
  56. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/pytest.ini +0 -0
  57. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/setup.cfg +0 -0
  58. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/__init__.py +0 -0
  59. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/conftest.py +0 -0
  60. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/__init__.py +0 -0
  61. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/conftest.py +0 -0
  62. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_directives.py +0 -0
  63. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/e2e/test_errors.py +0 -0
  64. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/__init__.py +0 -0
  65. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_argument.py +0 -0
  66. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_language.py +0 -0
  67. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_metadata.py +0 -0
  68. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_setting_spec.py +0 -0
  69. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_setting_store.py +0 -0
  70. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_tty.py +0 -0
  71. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/local/test_utils.py +0 -0
  72. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/__init__.py +0 -0
  73. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__omdb.py +0 -0
  74. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tmdb.py +0 -0
  75. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tvdb.py +0 -0
  76. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_endpoints__tvmaze.py +0 -0
  77. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__omdb.py +0 -0
  78. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tmdb.py +0 -0
  79. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tvdb.py +0 -0
  80. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/tests/network/test_providers__tvmaze.py +0 -0
  81. {mnamer-2.7.2.dev7 → mnamer-2.7.3.dev2}/uv.lock +0 -0
@@ -4,6 +4,9 @@ on:
4
4
  workflow_call:
5
5
  workflow_dispatch:
6
6
 
7
+ permissions:
8
+ contents: read
9
+
7
10
  jobs:
8
11
  publish-pypi:
9
12
  runs-on: ubuntu-latest
@@ -2,6 +2,9 @@ name: pr
2
2
 
3
3
  on: pull_request
4
4
 
5
+ permissions:
6
+ contents: read
7
+
5
8
  jobs:
6
9
  lint:
7
10
  runs-on: ubuntu-latest
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnamer
3
- Version: 2.7.2.dev7
3
+ Version: 2.7.3.dev2
4
4
  Summary: A command-line utility for organizing media files.
5
5
  Author-email: Jessy Williams <jessy@jessywilliams.com>
6
6
  Maintainer-email: Jessy Williams <jessy@jessywilliams.com>
@@ -1,4 +1,4 @@
1
1
  # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
3
 
4
- __version__ = "2.7.2.dev7"
4
+ __version__ = "2.7.3.dev2"
@@ -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
- meta = MetadataMovie(
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": self._resolve_path,
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": self._resolve_path,
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
- dir_head_ = format(self.metadata, str(self.directory))
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 = path.split(Path(file_path))
104
- filename = filename_replace(filename, self._settings.replace_after)
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
- filename = str_scenify(filename)
148
+ value = str_scenify(value)
107
149
  if self._settings.lower:
108
- filename = filename.lower()
109
- filename = str_sanitize(filename)
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}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mnamer
3
- Version: 2.7.2.dev7
3
+ Version: 2.7.3.dev2
4
4
  Summary: A command-line utility for organizing media files.
5
5
  Author-email: Jessy Williams <jessy@jessywilliams.com>
6
6
  Maintainer-email: Jessy Williams <jessy@jessywilliams.com>
@@ -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