jellycoder 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,5 @@
1
+ """GPU-accelerated video reduction library."""
2
+
3
+ from .core import ReducerConfig, reduce_videos
4
+
5
+ __all__ = ["ReducerConfig", "reduce_videos"]
@@ -0,0 +1,5 @@
1
+ from .cli import main
2
+
3
+
4
+ if __name__ == "__main__":
5
+ main()
video_reducer/cli.py ADDED
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from logging.handlers import MemoryHandler
9
+
10
+ from .core import QUALITY_PRESETS, ReducerConfig, reduce_videos
11
+
12
+ BACKEND_CHOICES = ["auto", "nvenc", "x264", "qsv", "amf"]
13
+
14
+ DEFAULT_LOG_LEVEL = logging.INFO
15
+ LOG_FORMAT = "%(asctime)s [%(levelname)s] %(message)s"
16
+
17
+
18
+ def _prompt_for_directory() -> Path:
19
+ while True:
20
+ raw = input("Enter the path to the video folder: ").strip().strip('"')
21
+ if not raw:
22
+ logging.warning("Empty input; please provide a folder path.")
23
+ continue
24
+ path = Path(raw).expanduser().resolve()
25
+ if path.is_dir():
26
+ return path
27
+ logging.warning("%s is not a valid directory; try again.", path)
28
+
29
+
30
+ def _prompt_overwrite(default: bool = False) -> bool:
31
+ raw = input("Overwrite existing files? [y/N]: ").strip().lower()
32
+ if not raw:
33
+ return default
34
+ return raw in {"y", "yes"}
35
+
36
+
37
+ def _prompt_quality(default: str = "auto") -> str:
38
+ choices = "/".join(QUALITY_PRESETS)
39
+ default_normalized = default.lower() if default else "auto"
40
+ while True:
41
+ raw = input(f"Select quality ({choices}) [{default_normalized}]: ").strip().lower()
42
+ if not raw:
43
+ return default_normalized
44
+ if raw in QUALITY_PRESETS:
45
+ return raw
46
+ logging.warning("Invalid quality selection '%s'. Choose one of: %s.", raw, ", ".join(QUALITY_PRESETS))
47
+
48
+
49
+ def _prompt_backend(default: str = "auto") -> str:
50
+ choices = "/".join(BACKEND_CHOICES)
51
+ default_normalized = default.lower() if default else "auto"
52
+ while True:
53
+ raw = input(f"Select encoder backend ({choices}) [{default_normalized}]: ").strip().lower()
54
+ if not raw:
55
+ return default_normalized
56
+ if raw in BACKEND_CHOICES:
57
+ return raw
58
+ logging.warning(
59
+ "Invalid backend selection '%s'. Choose one of: %s.", raw, ", ".join(BACKEND_CHOICES)
60
+ )
61
+
62
+
63
+ def parse_args(argv: Optional[list[str]] = None) -> argparse.Namespace:
64
+ parser = argparse.ArgumentParser(
65
+ prog="video_reducer",
66
+ description="Reduce video sizes using NVIDIA NVENC while keeping Jellyfin compatibility.",
67
+ )
68
+ parser.add_argument("input", nargs="?", help="Path to the input directory containing videos.")
69
+ parser.add_argument(
70
+ "--overwrite",
71
+ action="store_true",
72
+ help="Overwrite original files instead of writing to an output folder.",
73
+ )
74
+ parser.add_argument(
75
+ "--output",
76
+ type=Path,
77
+ help="Custom output directory root when not overwriting (default: ./output/<input-name>).",
78
+ )
79
+ parser.add_argument(
80
+ "--max-workers",
81
+ type=int,
82
+ default=1,
83
+ help="Number of concurrent encodes (default: 1).",
84
+ )
85
+ parser.add_argument(
86
+ "--log-level",
87
+ default="info",
88
+ choices=["debug", "info", "warning", "error", "critical"],
89
+ help="Logging verbosity (default: info).",
90
+ )
91
+ parser.add_argument(
92
+ "--codec",
93
+ default="auto",
94
+ choices=["auto", "h264", "hevc"],
95
+ help="Preferred codec for NVENC encoding (default: auto).",
96
+ )
97
+ parser.add_argument(
98
+ "--backend",
99
+ default="auto",
100
+ choices=BACKEND_CHOICES,
101
+ help="Video encoder backend (default: auto).",
102
+ )
103
+ parser.add_argument(
104
+ "--quality",
105
+ default=None,
106
+ type=str.lower,
107
+ choices=QUALITY_PRESETS,
108
+ help="Target video quality preset (default: auto).",
109
+ )
110
+ return parser.parse_args(argv)
111
+
112
+
113
+ def main(argv: Optional[list[str]] = None) -> None:
114
+ args = parse_args(argv)
115
+
116
+ log_level = getattr(logging, args.log_level.upper(), DEFAULT_LOG_LEVEL)
117
+ formatter = logging.Formatter(LOG_FORMAT)
118
+ stream_handler = logging.StreamHandler()
119
+ stream_handler.setFormatter(formatter)
120
+ memory_handler = MemoryHandler(capacity=10000, flushLevel=logging.CRITICAL + 1)
121
+ logging.basicConfig(
122
+ level=log_level,
123
+ handlers=[stream_handler, memory_handler],
124
+ force=True,
125
+ )
126
+
127
+ if args.input:
128
+ input_path = Path(args.input).expanduser().resolve()
129
+ else:
130
+ input_path = _prompt_for_directory()
131
+
132
+ overwrite = args.overwrite if args.input else _prompt_overwrite(default=args.overwrite)
133
+ quality = args.quality or "auto"
134
+ if not args.input:
135
+ quality = _prompt_quality(default=quality)
136
+ backend = args.backend
137
+ if not args.input:
138
+ backend = _prompt_backend(default=backend)
139
+
140
+ if overwrite:
141
+ log_dir = input_path
142
+ else:
143
+ if args.output:
144
+ log_dir = Path(args.output).expanduser()
145
+ else:
146
+ log_dir = Path.cwd() / "output" / input_path.name
147
+ log_dir = log_dir.resolve(strict=False)
148
+ log_dir.mkdir(parents=True, exist_ok=True)
149
+ log_path = log_dir / "video_reducer.log"
150
+
151
+ file_handler = logging.FileHandler(log_path, mode="w", encoding="utf-8")
152
+ file_handler.setFormatter(formatter)
153
+ root_logger = logging.getLogger()
154
+ root_logger.addHandler(file_handler)
155
+ memory_handler.setTarget(file_handler)
156
+ memory_handler.flush()
157
+ root_logger.removeHandler(memory_handler)
158
+ memory_handler.close()
159
+
160
+ config = ReducerConfig(
161
+ input_path=input_path,
162
+ overwrite=overwrite,
163
+ output_root=args.output,
164
+ max_workers=args.max_workers,
165
+ preferred_codec=None if args.codec == "auto" else args.codec,
166
+ quality=quality,
167
+ encoder_backend=backend,
168
+ )
169
+
170
+ reduce_videos(config)
171
+
172
+
173
+ if __name__ == "__main__": # pragma: no cover
174
+ main()