jellyplex-sync 0.3.2__tar.gz → 0.3.4__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 (23) hide show
  1. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/PKG-INFO +25 -39
  2. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/README.md +24 -38
  3. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/pyproject.toml +2 -1
  4. jellyplex_sync-0.3.2/src/jellyplex_sync/cli/sync.py → jellyplex_sync-0.3.4/src/jellyplex_sync/cli/main.py +12 -40
  5. jellyplex_sync-0.3.4/src/jellyplex_sync/cli/sync.py +186 -0
  6. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/jellyfin.py +1 -1
  7. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/materializer.py +1 -1
  8. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/LICENSE +0 -0
  9. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/__init__.py +0 -0
  10. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/cli/__init__.py +0 -0
  11. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/compare.py +0 -0
  12. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/disambig.py +0 -0
  13. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/discover.py +0 -0
  14. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/json_output.py +0 -0
  15. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/library.py +0 -0
  16. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/model.py +0 -0
  17. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/plan.py +0 -0
  18. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/planner.py +0 -0
  19. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/plex.py +0 -0
  20. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/py.typed +0 -0
  21. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/realize.py +0 -0
  22. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/sync.py +1 -1
  23. {jellyplex_sync-0.3.2 → jellyplex_sync-0.3.4}/src/jellyplex_sync/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jellyplex-sync
3
- Version: 0.3.2
3
+ Version: 0.3.4
4
4
  Summary: Convert your media library between Jellyfin and Plex formats by creating a hard-linked mirror
5
5
  Author: Stefan Schönberger
6
6
  Author-email: Stefan Schönberger <stefan@sniner.dev>
@@ -44,7 +44,7 @@ The easiest way to install the CLI is via [uv](https://docs.astral.sh/uv/):
44
44
  uv tool install jellyplex-sync
45
45
  ```
46
46
 
47
- This places `jellyplex-sync` on your `PATH` in an isolated environment. `pipx install jellyplex-sync` works the same way if you prefer pipx.
47
+ This places `jellyplex` and `jellyplex-sync` on your `PATH` in an isolated environment. `pipx install jellyplex-sync` works the same way if you prefer pipx.
48
48
 
49
49
  ### Docker
50
50
 
@@ -62,17 +62,19 @@ docker build -t jellyplex-sync .
62
62
 
63
63
  ## Usage
64
64
 
65
- The CLI is split into four subcommands:
65
+ The main CLI is `jellyplex` with four subcommands:
66
66
 
67
- - **`sync`** — mirror a source library into the target layout (the historical operation; this is also the default if you omit the subcommand).
68
- - **`diff`** — read-only comparison of source and target. No filesystem changes. Useful before deleting your source after a migration.
69
- - **`plan`** — show the Plan a sync would execute (text or `--json`), without touching either side. Pre-flight check before sync, or machine-readable input for tooling.
70
- - **`import`** — **(experimental)** import video files from a staging area into a structured library. See below.
67
+ - **`jellyplex sync`** — mirror a source library into the target layout.
68
+ - **`jellyplex diff`** — read-only comparison of source and target. No filesystem changes. Useful before deleting your source after a migration.
69
+ - **`jellyplex plan`** — show the Plan a sync would execute (text or `--json`), without touching either side. Pre-flight check before sync, or machine-readable input for tooling.
70
+ - **`jellyplex import`** — **(experimental)** import video files from a staging area into a structured library. See below.
71
+
72
+ For backward compatibility, `jellyplex-sync` is also installed as a standalone command that does exactly what `jellyplex sync` does — same options, no subcommand needed.
71
73
 
72
74
  ### `sync`
73
75
 
74
76
  ```bash
75
- jellyplex-sync sync [OPTIONS] /path/to/source/library /path/to/target/library
77
+ jellyplex sync [OPTIONS] /path/to/source/library /path/to/target/library
76
78
  ```
77
79
 
78
80
  The first positional is the source library, the second is the target. By default the tool auto-detects whether the source is a Jellyfin or Plex layout and converts to the other format.
@@ -101,31 +103,31 @@ Use `--copy` when source and target are on different filesystems (NAS to local d
101
103
  Mirror a Jellyfin library into a new Plex structure:
102
104
 
103
105
  ```bash
104
- jellyplex-sync sync --create ~/Media/Jellyfin ~/Media/Plex
106
+ jellyplex sync --create ~/Media/Jellyfin ~/Media/Plex
105
107
  ```
106
108
 
107
109
  Migration with cleanup (target becomes a clean mirror):
108
110
 
109
111
  ```bash
110
- jellyplex-sync sync --delete --create ~/Media/Jellyfin ~/Media/Plex
112
+ jellyplex sync --delete --create ~/Media/Jellyfin ~/Media/Plex
111
113
  ```
112
114
 
113
115
  Cross-filesystem copy with safe re-runs:
114
116
 
115
117
  ```bash
116
- jellyplex-sync sync --copy --create /mnt/nas/Movies /mnt/local/Plex
118
+ jellyplex sync --copy --create /mnt/nas/Movies /mnt/local/Plex
117
119
  ```
118
120
 
119
121
  Dry-run a verbose, full sync with deletion:
120
122
 
121
123
  ```bash
122
- jellyplex-sync sync --dry-run --verbose --delete --create ~/Media/Jellyfin ~/Media/Plex
124
+ jellyplex sync --dry-run --verbose --delete --create ~/Media/Jellyfin ~/Media/Plex
123
125
  ```
124
126
 
125
127
  ### `diff`
126
128
 
127
129
  ```bash
128
- jellyplex-sync diff [OPTIONS] /path/to/source/library /path/to/target/library
130
+ jellyplex diff [OPTIONS] /path/to/source/library /path/to/target/library
129
131
  ```
130
132
 
131
133
  Compares the two libraries without touching anything. Reports movies that exist only on one side, file-level differences inside shared movies, translation losses (Plex labels with no Jellyfin equivalent, for example), and entries the scanner ignored.
@@ -141,13 +143,13 @@ Exit codes follow the Unix `diff` convention:
141
143
  Verify a target is a complete mirror before deleting the source:
142
144
 
143
145
  ```bash
144
- jellyplex-sync diff ~/Media/Jellyfin ~/Media/Plex
146
+ jellyplex diff ~/Media/Jellyfin ~/Media/Plex
145
147
  ```
146
148
 
147
149
  ### `plan`
148
150
 
149
151
  ```bash
150
- jellyplex-sync plan [OPTIONS] /path/to/source/library /path/to/target/library
152
+ jellyplex plan [OPTIONS] /path/to/source/library /path/to/target/library
151
153
  ```
152
154
 
153
155
  Shows the Plan a sync would execute — every movie folder, every translated video filename, every loose file, every asset subdirectory — without touching either filesystem. Useful as a pre-flight check ("will any movies clash? what gets translated?") or as a machine-readable input for tooling with `--json`.
@@ -164,13 +166,13 @@ Exit codes:
164
166
  Pre-flight a sync, see exactly which target filenames would be created:
165
167
 
166
168
  ```bash
167
- jellyplex-sync plan ~/Media/Plex ~/Media/Jellyfin
169
+ jellyplex plan ~/Media/Plex ~/Media/Jellyfin
168
170
  ```
169
171
 
170
172
  Machine-readable, with `jq` to find videos that needed disambiguation:
171
173
 
172
174
  ```bash
173
- jellyplex-sync plan --json ~/Media/Plex ~/Media/Jellyfin \
175
+ jellyplex plan --json ~/Media/Plex ~/Media/Jellyfin \
174
176
  | jq '.movies[].videos[] | select(.disambiguation) | {source, target_name, disambiguation}'
175
177
  ```
176
178
 
@@ -179,7 +181,7 @@ jellyplex-sync plan --json ~/Media/Plex ~/Media/Jellyfin \
179
181
  > ⚠️ **This subcommand is experimental.** The exact behaviour may change in future releases as real-world use cases become clearer. Always start with `--dry-run`.
180
182
 
181
183
  ```bash
182
- jellyplex-sync import [OPTIONS] /path/to/staging /path/to/target/library
184
+ jellyplex import [OPTIONS] /path/to/staging /path/to/target/library
183
185
  ```
184
186
 
185
187
  Imports video files from a staging area — a flat directory (or directory tree) where properly named video files have been dumped without the one-folder-per-movie structure that `sync` expects. Files are grouped by their parsed movie identity (title, year, provider ID) and placed into the correct movie folder in the target library.
@@ -208,13 +210,13 @@ Other flags: `--dry-run`, `--create`, `--source-format`, `--target-format`, `-v`
208
210
  Move Plex-named video files into a new Jellyfin library:
209
211
 
210
212
  ```bash
211
- jellyplex-sync import --create /mnt/staging /mnt/library/Jellyfin
213
+ jellyplex import --create /mnt/staging /mnt/library/Jellyfin
212
214
  ```
213
215
 
214
216
  Preview first, keeping the source:
215
217
 
216
218
  ```bash
217
- jellyplex-sync import --copy --dry-run /mnt/staging /mnt/library/Jellyfin
219
+ jellyplex import --copy --dry-run /mnt/staging /mnt/library/Jellyfin
218
220
  ```
219
221
 
220
222
  ### JSON output
@@ -243,40 +245,24 @@ The schema (still evolving — pin a version when consuming):
243
245
  Show every file that would be replaced (with full paths):
244
246
 
245
247
  ```bash
246
- jellyplex-sync sync --json --dry-run /src /dst \
248
+ jellyplex sync --json --dry-run /src /dst \
247
249
  | jq '.events[] | select(.action == "replace") | {source, target}'
248
250
  ```
249
251
 
250
252
  Verify nothing important gets deleted before running a migration with `--delete`:
251
253
 
252
254
  ```bash
253
- jellyplex-sync sync --json --dry-run --delete /src /dst \
255
+ jellyplex sync --json --dry-run --delete /src /dst \
254
256
  | jq '.events[] | select(.action == "remove")'
255
257
  ```
256
258
 
257
259
  Action distribution:
258
260
 
259
261
  ```bash
260
- jellyplex-sync sync --json --dry-run /src /dst \
262
+ jellyplex sync --json --dry-run /src /dst \
261
263
  | jq '[.events[].action] | group_by(.) | map({action: .[0], count: length})'
262
264
  ```
263
265
 
264
- ### Default subcommand
265
-
266
- For backward compatibility, omitting the subcommand is treated as an implicit `sync` (as long as the first argument is a positional, not a flag):
267
-
268
- ```bash
269
- jellyplex-sync ~/Media/Jellyfin ~/Media/Plex
270
- ```
271
-
272
- is equivalent to
273
-
274
- ```bash
275
- jellyplex-sync sync ~/Media/Jellyfin ~/Media/Plex
276
- ```
277
-
278
- If you start with a flag, you must spell out `sync` explicitly.
279
-
280
266
  ### Docker
281
267
 
282
268
  ```bash
@@ -28,7 +28,7 @@ The easiest way to install the CLI is via [uv](https://docs.astral.sh/uv/):
28
28
  uv tool install jellyplex-sync
29
29
  ```
30
30
 
31
- This places `jellyplex-sync` on your `PATH` in an isolated environment. `pipx install jellyplex-sync` works the same way if you prefer pipx.
31
+ This places `jellyplex` and `jellyplex-sync` on your `PATH` in an isolated environment. `pipx install jellyplex-sync` works the same way if you prefer pipx.
32
32
 
33
33
  ### Docker
34
34
 
@@ -46,17 +46,19 @@ docker build -t jellyplex-sync .
46
46
 
47
47
  ## Usage
48
48
 
49
- The CLI is split into four subcommands:
49
+ The main CLI is `jellyplex` with four subcommands:
50
50
 
51
- - **`sync`** — mirror a source library into the target layout (the historical operation; this is also the default if you omit the subcommand).
52
- - **`diff`** — read-only comparison of source and target. No filesystem changes. Useful before deleting your source after a migration.
53
- - **`plan`** — show the Plan a sync would execute (text or `--json`), without touching either side. Pre-flight check before sync, or machine-readable input for tooling.
54
- - **`import`** — **(experimental)** import video files from a staging area into a structured library. See below.
51
+ - **`jellyplex sync`** — mirror a source library into the target layout.
52
+ - **`jellyplex diff`** — read-only comparison of source and target. No filesystem changes. Useful before deleting your source after a migration.
53
+ - **`jellyplex plan`** — show the Plan a sync would execute (text or `--json`), without touching either side. Pre-flight check before sync, or machine-readable input for tooling.
54
+ - **`jellyplex import`** — **(experimental)** import video files from a staging area into a structured library. See below.
55
+
56
+ For backward compatibility, `jellyplex-sync` is also installed as a standalone command that does exactly what `jellyplex sync` does — same options, no subcommand needed.
55
57
 
56
58
  ### `sync`
57
59
 
58
60
  ```bash
59
- jellyplex-sync sync [OPTIONS] /path/to/source/library /path/to/target/library
61
+ jellyplex sync [OPTIONS] /path/to/source/library /path/to/target/library
60
62
  ```
61
63
 
62
64
  The first positional is the source library, the second is the target. By default the tool auto-detects whether the source is a Jellyfin or Plex layout and converts to the other format.
@@ -85,31 +87,31 @@ Use `--copy` when source and target are on different filesystems (NAS to local d
85
87
  Mirror a Jellyfin library into a new Plex structure:
86
88
 
87
89
  ```bash
88
- jellyplex-sync sync --create ~/Media/Jellyfin ~/Media/Plex
90
+ jellyplex sync --create ~/Media/Jellyfin ~/Media/Plex
89
91
  ```
90
92
 
91
93
  Migration with cleanup (target becomes a clean mirror):
92
94
 
93
95
  ```bash
94
- jellyplex-sync sync --delete --create ~/Media/Jellyfin ~/Media/Plex
96
+ jellyplex sync --delete --create ~/Media/Jellyfin ~/Media/Plex
95
97
  ```
96
98
 
97
99
  Cross-filesystem copy with safe re-runs:
98
100
 
99
101
  ```bash
100
- jellyplex-sync sync --copy --create /mnt/nas/Movies /mnt/local/Plex
102
+ jellyplex sync --copy --create /mnt/nas/Movies /mnt/local/Plex
101
103
  ```
102
104
 
103
105
  Dry-run a verbose, full sync with deletion:
104
106
 
105
107
  ```bash
106
- jellyplex-sync sync --dry-run --verbose --delete --create ~/Media/Jellyfin ~/Media/Plex
108
+ jellyplex sync --dry-run --verbose --delete --create ~/Media/Jellyfin ~/Media/Plex
107
109
  ```
108
110
 
109
111
  ### `diff`
110
112
 
111
113
  ```bash
112
- jellyplex-sync diff [OPTIONS] /path/to/source/library /path/to/target/library
114
+ jellyplex diff [OPTIONS] /path/to/source/library /path/to/target/library
113
115
  ```
114
116
 
115
117
  Compares the two libraries without touching anything. Reports movies that exist only on one side, file-level differences inside shared movies, translation losses (Plex labels with no Jellyfin equivalent, for example), and entries the scanner ignored.
@@ -125,13 +127,13 @@ Exit codes follow the Unix `diff` convention:
125
127
  Verify a target is a complete mirror before deleting the source:
126
128
 
127
129
  ```bash
128
- jellyplex-sync diff ~/Media/Jellyfin ~/Media/Plex
130
+ jellyplex diff ~/Media/Jellyfin ~/Media/Plex
129
131
  ```
130
132
 
131
133
  ### `plan`
132
134
 
133
135
  ```bash
134
- jellyplex-sync plan [OPTIONS] /path/to/source/library /path/to/target/library
136
+ jellyplex plan [OPTIONS] /path/to/source/library /path/to/target/library
135
137
  ```
136
138
 
137
139
  Shows the Plan a sync would execute — every movie folder, every translated video filename, every loose file, every asset subdirectory — without touching either filesystem. Useful as a pre-flight check ("will any movies clash? what gets translated?") or as a machine-readable input for tooling with `--json`.
@@ -148,13 +150,13 @@ Exit codes:
148
150
  Pre-flight a sync, see exactly which target filenames would be created:
149
151
 
150
152
  ```bash
151
- jellyplex-sync plan ~/Media/Plex ~/Media/Jellyfin
153
+ jellyplex plan ~/Media/Plex ~/Media/Jellyfin
152
154
  ```
153
155
 
154
156
  Machine-readable, with `jq` to find videos that needed disambiguation:
155
157
 
156
158
  ```bash
157
- jellyplex-sync plan --json ~/Media/Plex ~/Media/Jellyfin \
159
+ jellyplex plan --json ~/Media/Plex ~/Media/Jellyfin \
158
160
  | jq '.movies[].videos[] | select(.disambiguation) | {source, target_name, disambiguation}'
159
161
  ```
160
162
 
@@ -163,7 +165,7 @@ jellyplex-sync plan --json ~/Media/Plex ~/Media/Jellyfin \
163
165
  > ⚠️ **This subcommand is experimental.** The exact behaviour may change in future releases as real-world use cases become clearer. Always start with `--dry-run`.
164
166
 
165
167
  ```bash
166
- jellyplex-sync import [OPTIONS] /path/to/staging /path/to/target/library
168
+ jellyplex import [OPTIONS] /path/to/staging /path/to/target/library
167
169
  ```
168
170
 
169
171
  Imports video files from a staging area — a flat directory (or directory tree) where properly named video files have been dumped without the one-folder-per-movie structure that `sync` expects. Files are grouped by their parsed movie identity (title, year, provider ID) and placed into the correct movie folder in the target library.
@@ -192,13 +194,13 @@ Other flags: `--dry-run`, `--create`, `--source-format`, `--target-format`, `-v`
192
194
  Move Plex-named video files into a new Jellyfin library:
193
195
 
194
196
  ```bash
195
- jellyplex-sync import --create /mnt/staging /mnt/library/Jellyfin
197
+ jellyplex import --create /mnt/staging /mnt/library/Jellyfin
196
198
  ```
197
199
 
198
200
  Preview first, keeping the source:
199
201
 
200
202
  ```bash
201
- jellyplex-sync import --copy --dry-run /mnt/staging /mnt/library/Jellyfin
203
+ jellyplex import --copy --dry-run /mnt/staging /mnt/library/Jellyfin
202
204
  ```
203
205
 
204
206
  ### JSON output
@@ -227,40 +229,24 @@ The schema (still evolving — pin a version when consuming):
227
229
  Show every file that would be replaced (with full paths):
228
230
 
229
231
  ```bash
230
- jellyplex-sync sync --json --dry-run /src /dst \
232
+ jellyplex sync --json --dry-run /src /dst \
231
233
  | jq '.events[] | select(.action == "replace") | {source, target}'
232
234
  ```
233
235
 
234
236
  Verify nothing important gets deleted before running a migration with `--delete`:
235
237
 
236
238
  ```bash
237
- jellyplex-sync sync --json --dry-run --delete /src /dst \
239
+ jellyplex sync --json --dry-run --delete /src /dst \
238
240
  | jq '.events[] | select(.action == "remove")'
239
241
  ```
240
242
 
241
243
  Action distribution:
242
244
 
243
245
  ```bash
244
- jellyplex-sync sync --json --dry-run /src /dst \
246
+ jellyplex sync --json --dry-run /src /dst \
245
247
  | jq '[.events[].action] | group_by(.) | map({action: .[0], count: length})'
246
248
  ```
247
249
 
248
- ### Default subcommand
249
-
250
- For backward compatibility, omitting the subcommand is treated as an implicit `sync` (as long as the first argument is a positional, not a flag):
251
-
252
- ```bash
253
- jellyplex-sync ~/Media/Jellyfin ~/Media/Plex
254
- ```
255
-
256
- is equivalent to
257
-
258
- ```bash
259
- jellyplex-sync sync ~/Media/Jellyfin ~/Media/Plex
260
- ```
261
-
262
- If you start with a flag, you must spell out `sync` explicitly.
263
-
264
250
  ### Docker
265
251
 
266
252
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jellyplex-sync"
3
- version = "0.3.2"
3
+ version = "0.3.4"
4
4
  description = "Convert your media library between Jellyfin and Plex formats by creating a hard-linked mirror"
5
5
  authors = [{ name = "Stefan Schönberger", email = "stefan@sniner.dev" }]
6
6
  requires-python = ">=3.11"
@@ -17,6 +17,7 @@ classifiers = [
17
17
  dependencies = []
18
18
 
19
19
  [project.scripts]
20
+ jellyplex = "jellyplex_sync.cli.main:main"
20
21
  jellyplex-sync = "jellyplex_sync.cli.sync:main"
21
22
 
22
23
  [dependency-groups]
@@ -7,12 +7,10 @@ import sys
7
7
 
8
8
  import jellyplex_sync as jp
9
9
 
10
- _SUBCOMMANDS = {"sync", "diff", "plan", "import"}
11
-
12
10
 
13
11
  def _build_parser() -> argparse.ArgumentParser:
14
12
  parser = argparse.ArgumentParser(
15
- prog="jellyplex-sync",
13
+ prog="jellyplex",
16
14
  description="Convert a media library between Plex and Jellyfin layouts.",
17
15
  )
18
16
 
@@ -41,7 +39,7 @@ def _build_parser() -> argparse.ArgumentParser:
41
39
  sync_p = sub.add_parser(
42
40
  "sync",
43
41
  parents=[common],
44
- help="Mirror a source library into the target layout (default).",
42
+ help="Mirror a source library into the target layout.",
45
43
  )
46
44
  _add_io_args(sync_p)
47
45
  mode = sync_p.add_mutually_exclusive_group()
@@ -157,24 +155,9 @@ def _add_io_args(p: argparse.ArgumentParser) -> None:
157
155
  )
158
156
 
159
157
 
160
- def _inject_default_subcommand(argv: list[str]) -> list[str]:
161
- """If the first positional argument isn't a known subcommand, treat the
162
- call as `sync ...`. Keeps the historical invocation `jellyplex-sync
163
- <source> <target>` working alongside the new `jellyplex-sync sync ...`.
164
- """
165
- if not argv:
166
- return argv
167
- first = argv[0]
168
- if first.startswith("-"):
169
- return argv
170
- if first in _SUBCOMMANDS:
171
- return argv
172
- return ["sync", *argv]
173
-
174
-
175
158
  def main() -> None:
176
159
  parser = _build_parser()
177
- args = parser.parse_args(_inject_default_subcommand(sys.argv[1:]))
160
+ args = parser.parse_args()
178
161
 
179
162
  # Under --json, INFO chatter on stderr distracts from the JSON document
180
163
  # on stdout. Quiet stderr to WARNING by default; --verbose or --debug
@@ -239,8 +222,15 @@ def _do_sync(args: argparse.Namespace) -> int:
239
222
 
240
223
  if args.json:
241
224
  assert stats is not None and reporter is not None
242
- source_short, target_short = _resolve_formats_for_json(args)
243
225
  from jellyplex_sync.json_output import write_sync_json
226
+ from jellyplex_sync.sync import _resolve_formats
227
+
228
+ resolved = _resolve_formats(
229
+ pathlib.Path(args.source),
230
+ args.source_format,
231
+ args.target_format,
232
+ )
233
+ source_short, target_short = resolved if resolved else ("?", "?")
244
234
 
245
235
  write_sync_json(
246
236
  sys.stdout,
@@ -309,10 +299,7 @@ def _do_plan(args: argparse.Namespace) -> int:
309
299
  def _do_import(args: argparse.Namespace) -> int:
310
300
  from jellyplex_sync.sync import import_media
311
301
 
312
- if args.mode == "move":
313
- materializer = jp.MoveMaterializer()
314
- else:
315
- materializer = jp.CopyMaterializer()
302
+ materializer = jp.MoveMaterializer() if args.mode == "move" else jp.CopyMaterializer()
316
303
 
317
304
  try:
318
305
  return import_media(
@@ -334,20 +321,5 @@ def _do_import(args: argparse.Namespace) -> int:
334
321
  return 99
335
322
 
336
323
 
337
- def _resolve_formats_for_json(args: argparse.Namespace) -> tuple[str, str]:
338
- """Re-run the same format resolution `sync()` did so the JSON payload
339
- reports the formats that were actually used. Returns ('?', '?') if
340
- resolution fails — sync() will already have logged the error and the
341
- caller's exit code reflects it."""
342
- import pathlib
343
-
344
- from jellyplex_sync.sync import _resolve_formats
345
-
346
- resolved = _resolve_formats(
347
- pathlib.Path(args.source), args.source_format, args.target_format
348
- )
349
- return resolved if resolved else ("?", "?")
350
-
351
-
352
324
  if __name__ == "__main__":
353
325
  main()
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/python3
2
+ """Backward-compatible ``jellyplex-sync`` entry point.
3
+
4
+ Provides the same interface as the original single-command CLI: flat
5
+ argument list, no subcommands.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import logging
12
+ import pathlib
13
+ import sys
14
+
15
+ import jellyplex_sync as jp
16
+
17
+
18
+ def _build_parser() -> argparse.ArgumentParser:
19
+ format_choices = [
20
+ jp.JellyfinLibraryReader.shortname(),
21
+ jp.PlexLibraryReader.shortname(),
22
+ "auto",
23
+ ]
24
+
25
+ parser = argparse.ArgumentParser(
26
+ prog="jellyplex-sync",
27
+ description="Mirror a source library into the target layout.",
28
+ )
29
+ parser.add_argument(
30
+ "-v",
31
+ "--verbose",
32
+ action="store_true",
33
+ help="Show more information messages.",
34
+ )
35
+ parser.add_argument(
36
+ "--debug",
37
+ action="store_true",
38
+ help="Show debug messages.",
39
+ )
40
+ parser.add_argument(
41
+ "--json",
42
+ action="store_true",
43
+ help="Emit a machine-readable JSON document on stdout. Logs continue to go to stderr.",
44
+ )
45
+ parser.add_argument("source", help="Source media library path.")
46
+ parser.add_argument("target", help="Target media library path.")
47
+ parser.add_argument(
48
+ "--source-format",
49
+ type=str,
50
+ choices=format_choices,
51
+ default="auto",
52
+ help="Source library format ('auto' detects from the source layout).",
53
+ )
54
+ parser.add_argument(
55
+ "--target-format",
56
+ type=str,
57
+ choices=format_choices,
58
+ default="auto",
59
+ help="Target library format ('auto' picks the opposite of the source).",
60
+ )
61
+
62
+ mode = parser.add_mutually_exclusive_group()
63
+ mode.add_argument(
64
+ "--hardlink",
65
+ dest="mode",
66
+ action="store_const",
67
+ const="hardlink",
68
+ help="Use hardlinks (default; needs source and target on the same filesystem).",
69
+ )
70
+ mode.add_argument(
71
+ "--copy",
72
+ dest="mode",
73
+ action="store_const",
74
+ const="copy",
75
+ help="Copy files; on re-runs skip files whose size and mtime already match.",
76
+ )
77
+ mode.add_argument(
78
+ "--force-copy",
79
+ dest="mode",
80
+ action="store_const",
81
+ const="force-copy",
82
+ help="Always copy and overwrite, regardless of existing target state.",
83
+ )
84
+ parser.set_defaults(mode="hardlink")
85
+ parser.add_argument(
86
+ "--dry-run",
87
+ action="store_true",
88
+ help="Show what would be done, don't change anything on disk.",
89
+ )
90
+ parser.add_argument(
91
+ "--delete",
92
+ action="store_true",
93
+ help="Remove stray folders from the target library.",
94
+ )
95
+ parser.add_argument(
96
+ "--create",
97
+ action="store_true",
98
+ help="Create the target library directory if it doesn't exist.",
99
+ )
100
+
101
+ return parser
102
+
103
+
104
+ def main() -> None:
105
+ parser = _build_parser()
106
+ args = parser.parse_args()
107
+
108
+ if args.debug:
109
+ level = logging.DEBUG
110
+ elif args.json and not args.verbose:
111
+ level = logging.WARNING
112
+ else:
113
+ level = logging.INFO
114
+
115
+ logging.basicConfig(
116
+ level=level,
117
+ stream=sys.stderr,
118
+ format="%(levelname)s: %(asctime)s -- %(message)s",
119
+ )
120
+
121
+ if args.mode == "hardlink":
122
+ materializer: jp.FileMaterializer = jp.HardlinkMaterializer()
123
+ elif args.mode == "copy":
124
+ materializer = jp.CopyMaterializer()
125
+ elif args.mode == "force-copy":
126
+ materializer = jp.ForceCopyMaterializer()
127
+ else:
128
+ raise ValueError(f"unknown materialization mode: {args.mode!r}")
129
+
130
+ from jellyplex_sync.library import CollectingReporter
131
+ from jellyplex_sync.sync import LibraryStats
132
+
133
+ stats = LibraryStats() if args.json else None
134
+ reporter = CollectingReporter() if args.json else None
135
+
136
+ try:
137
+ rc = jp.sync(
138
+ args.source,
139
+ args.target,
140
+ dry_run=args.dry_run,
141
+ delete=args.delete,
142
+ create=args.create,
143
+ verbose=args.verbose,
144
+ debug=args.debug,
145
+ source_format=args.source_format,
146
+ target_format=args.target_format,
147
+ materializer=materializer,
148
+ reporter=reporter,
149
+ stats=stats,
150
+ )
151
+ except KeyboardInterrupt:
152
+ logging.info("INTERRUPTED")
153
+ sys.exit(10)
154
+ except Exception as exc:
155
+ logging.error("Exception: %s", exc)
156
+ sys.exit(99)
157
+
158
+ if args.json:
159
+ assert stats is not None and reporter is not None
160
+ from jellyplex_sync.json_output import write_sync_json
161
+ from jellyplex_sync.sync import _resolve_formats
162
+
163
+ resolved = _resolve_formats(
164
+ pathlib.Path(args.source),
165
+ args.source_format,
166
+ args.target_format,
167
+ )
168
+ source_short, target_short = resolved if resolved else ("?", "?")
169
+
170
+ write_sync_json(
171
+ sys.stdout,
172
+ source_path=pathlib.Path(args.source),
173
+ source_format=source_short,
174
+ target_path=pathlib.Path(args.target),
175
+ target_format=target_short,
176
+ dry_run=args.dry_run,
177
+ exit_code=rc,
178
+ stats=stats,
179
+ drops=reporter.drops,
180
+ )
181
+
182
+ sys.exit(rc)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -125,7 +125,7 @@ class JellyfinLibraryWriter(_JellyfinBase):
125
125
  parts.append(f"[{key}id-{value}]")
126
126
 
127
127
  # Report any other provider IDs we couldn't fit; Jellyfin tolerates
128
- # multiple bracket-IDs in the folder name, but jellyplex-sync only
128
+ # multiple bracket-IDs in the folder name, but jellyplex only
129
129
  # emits one for now.
130
130
  for key, value in movie.attributes.items():
131
131
  if primary and key == primary[0]:
@@ -2,7 +2,7 @@
2
2
 
3
3
  A `FileMaterializer` knows how to make a target path reflect a source path.
4
4
  The first (and currently only) implementation, `HardlinkMaterializer`, uses
5
- filesystem hardlinks — the behavior jellyplex-sync has always had. Copy
5
+ filesystem hardlinks — the behavior this tool has always had. Copy
6
6
  backends arrive in a later step of Paket 4.
7
7
 
8
8
  The seam exists so the CLI can swap materialization strategies (`--copy`,
File without changes
@@ -6,6 +6,7 @@ import re
6
6
  from collections.abc import Callable
7
7
  from dataclasses import dataclass, field
8
8
 
9
+ from .discover import FlatDiscoverer
9
10
  from .jellyfin import JellyfinLibraryReader, JellyfinLibraryWriter
10
11
  from .library import (
11
12
  ACCEPTED_VIDEO_SUFFIXES,
@@ -19,7 +20,6 @@ from .library import (
19
20
  Reporter,
20
21
  dedupe_drops,
21
22
  )
22
- from .discover import FlatDiscoverer
23
23
  from .materializer import FileMaterializer, HardlinkMaterializer, MoveMaterializer
24
24
  from .planner import Planner
25
25
  from .plex import PlexLibraryReader, PlexLibraryWriter