Skip to content

Commit 86a8569

Browse files
jet-logicjet-logic
authored andcommitted
v0.0.5
1 parent 2fb44c6 commit 86a8569

File tree

4 files changed

+148
-51
lines changed

4 files changed

+148
-51
lines changed

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ If you find this project helpful, consider supporting me:
1414
## Features ✨
1515

1616
- **Pattern-based renaming** 🧩 - Use regex substitutions to transform filenames
17-
- **Smart case conversion** 🔠 - Convert to lowercase (`--lower`) or uppercase (`--upper`)
17+
- **Case conversion**: lower, upper, title, swapcase, capitalize
1818
- **URL-safe names** 🌐 - Clean filenames for web use (`--urlsafe`)
1919
- **Precise file selection** 🎯:
2020
- Include/exclude files with `--includes`/`--excludes`
@@ -65,7 +65,7 @@ python -m renx [OPTIONS] [PATHS...]
6565
The substitution pattern uses this format:
6666

6767
```
68-
REGEX❗REPLACEMENT❗FLAGS
68+
search❗replace❗[flags]❗[flags]❗[flags]
6969
```
7070

7171
Where:
@@ -125,6 +125,21 @@ For example, with `-s '/foo/bar/i'`:
125125
3. Replacement = `bar`
126126
4. Flags = `i` (case-insensitive)
127127

128+
Special flags:
129+
130+
- `upper`, `lower`, `title`, `swapcase`, `capitalize` - Case transformations
131+
- `ext` - Apply to extension only
132+
- `stem` - Apply to filename stem only
133+
134+
Examples:
135+
136+
- `-s '/foo/bar/'` - Replace 'foo' with 'bar'
137+
- `-s '/\.jpg$/.png/'` - Change .jpg extensions to .png
138+
- `-s '/^/prefix_/'` - Add prefix to all names
139+
- `-s '/_/-/g'` - Replace all underscores with hyphens
140+
- `-s '/.*//upper/'` - Convert entire name to uppercase
141+
- `-s '/\..*$//lower/ext'` - Convert extension to lowercase
142+
128143
## Important Notes
129144

130145
- The delimiter can be any character (but must not appear unescaped in the pattern)
@@ -173,3 +188,21 @@ For example, with `-s '/foo/bar/i'`:
173188
```bash
174189
python -m renx --max-depth 2 /path/to/files
175190
```
191+
192+
## Multiple substitution
193+
194+
When your downloaded files look like they were named by a cat walking on a keyboard 😉:
195+
196+
```bash
197+
python -m renx \
198+
-s '#(?:(YTS(?:.?\w+)|YIFY|GloDLS|RARBG|ExTrEmE))##ix' \
199+
-s '!(1080p|720p|HDRip|x264|x265|BRRip|WEB-DL|BDRip|AAC|DTS)!!i' \
200+
-s '!\[(|\w+)\]!\1!' \
201+
-s '/[\._-]+/./' \
202+
-s '/\.+/ /stem' \
203+
-s /.+//ext/lower \
204+
-s '/.+//stem/title' \
205+
.
206+
# Before: "the.matrix.[1999].1080p.[YTS.AM].BRRip.x264-[GloDLS].ExTrEmE.mKV"
207+
# After: "The Matrix 1999.mkv" 🎬✨
208+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "renx"
7-
version = "0.0.4"
7+
version = "0.0.5"
88
description = "Advanced file renaming tool with regex and case transformation support"
99
readme = "README.md"
1010
authors = [

renx/__main__.py

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,75 @@
1-
from os.path import dirname, join
1+
import unicodedata, re
22
from os import rename
3+
from os.path import dirname, join
4+
from os.path import splitext
35
from .scantree import ScanTree
4-
import unicodedata
56

67

7-
def text_to_ascii(text: str):
8+
def asciify(text: str):
89
"""
910
Converts a Unicode string to its closest ASCII equivalent by removing
1011
accent marks and other non-ASCII characters.
1112
"""
1213
return "".join(c for c in unicodedata.normalize("NFD", text) if unicodedata.category(c) != "Mn")
1314

1415

16+
def slugify(value):
17+
value = str(value)
18+
value = asciify(value)
19+
value = re.sub(r"[^a-zA-Z0-9_.+-]+", "_", value)
20+
return value
21+
22+
23+
def clean(value):
24+
value = str(value)
25+
value = re.sub(r"\-+", "-", value).strip("-")
26+
value = re.sub(r"_+", "_", value).strip("_")
27+
return value
28+
29+
30+
def urlsafe(name, parent=None):
31+
s = slugify(name)
32+
if s != name or re.search(r"[_-]\.", s) or re.search(r"[_-]+", s):
33+
assert slugify(s) == s
34+
stem, ext = splitext(s)
35+
return clean(stem) + ext
36+
return name
37+
38+
1539
def split_subs(s: str):
1640
a = s[1:].split(s[0], 3)
1741
if len(a) > 1:
1842
search = a[0]
1943
replace = a[1]
44+
extra = {}
2045
if not search:
2146
raise RuntimeError(f"Empty search pattern {s!r}")
2247
if len(a) > 2:
23-
flags = a[2]
48+
flags = None
49+
for x in a[2:]:
50+
if x in [
51+
"upper",
52+
"lower",
53+
"title",
54+
"swapcase",
55+
"expandtabs",
56+
"casefold",
57+
"capitalize",
58+
"asciify",
59+
"slugify",
60+
"urlsafe",
61+
"ext",
62+
"stem",
63+
]:
64+
if x not in ["ext", "stem"]:
65+
assert not replace
66+
pass
67+
extra[x] = True
68+
else:
69+
flags = x
2470
if flags:
2571
search = f"(?{flags}){search}"
26-
return search, replace, {}
72+
return search, replace, extra
2773
raise RuntimeError(f"Invalid pattern {s!r}")
2874

2975

@@ -61,37 +107,65 @@ def start(self):
61107
_subs.append((lambda name, parent: name.upper()))
62108

63109
if self.urlsafe:
64-
from os.path import splitext
65-
66-
def slugify(value):
67-
value = str(value)
68-
value = text_to_ascii(value)
69-
value = re.sub(r"[^a-zA-Z0-9_.+-]+", "_", value)
70-
return value
71-
72-
def clean(value):
73-
value = str(value)
74-
value = re.sub(r"\-+", "-", value).strip("-")
75-
value = re.sub(r"_+", "_", value).strip("_")
76-
return value
77-
78-
def urlsafe(name, parent):
79-
s = slugify(name)
80-
if s != name or re.search(r"[_-]\.", s) or re.search(r"[_-]+", s):
81-
assert slugify(s) == s
82-
stem, ext = splitext(s)
83-
return clean(stem) + ext
84-
return name
85-
86110
_subs.append(urlsafe)
87111

88-
def _append(rex, rep, extra):
112+
def _append(rex, rep: str, extra):
113+
if extra:
114+
115+
def fn(name: str, parent):
116+
if extra.get("stem"):
117+
S, x = splitext(name)
118+
fin = lambda r: r + x
119+
elif extra.get("ext"):
120+
x, S = splitext(name)
121+
fin = lambda r: x + r
122+
else:
123+
S = name
124+
fin = lambda r: r
125+
126+
# def fr():
127+
# return rex.sub(rep, S)
128+
129+
if extra.get("lower"):
130+
R = lambda m: m.group(0).lower()
131+
elif extra.get("upper"):
132+
R = lambda m: m.group(0).upper()
133+
elif extra.get("title"):
134+
R = lambda m: m.group(0).title()
135+
elif extra.get("swapcase"):
136+
R = lambda m: m.group(0).swapcase()
137+
elif extra.get("casefold"):
138+
R = lambda m: m.group(0).casefold()
139+
elif extra.get("capitalize"):
140+
R = lambda m: m.group(0).capitalize()
141+
elif extra.get("asciify"):
142+
R = lambda m: asciify(m.group(0))
143+
elif extra.get("urlsafe"):
144+
R = lambda m: urlsafe(m.group(0))
145+
elif extra.get("slugify"):
146+
R = lambda m: urlsafe(m.group(0))
147+
else:
148+
R = rep
149+
# return fin(fx(fr()))
150+
151+
return fin(rex.sub(R, S))
152+
153+
else:
154+
155+
def fn(name, parent):
156+
return rex.sub(rep, name)
157+
158+
fn.regx = rex
159+
89160
# print("REX", rex, rep)
90-
_subs.append((lambda name, parent: rex.sub(rep, name)))
161+
_subs.append(fn)
91162

92163
for s in self.subs:
93164
search, replace, extra = split_subs(s)
94-
rex = regex(search)
165+
try:
166+
rex = regex(search)
167+
except Exception as e:
168+
raise RuntimeError(f"Bad regexp {search!r}: {e}")
95169
_append(rex, replace, extra)
96170

97171
self._subs = _subs
@@ -107,6 +181,7 @@ def process_entry(self, de):
107181
for fn in self._subs:
108182
v = fn(name2, parent)
109183
# print("PE_subs", de.path, name2, v)
184+
# print("fn", getattr(fn, "regx", "?"))
110185
if v:
111186
name2 = v
112187
# print("PE", de.path, [name1, name2])

renx/main.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@ def __init__(self, *args: str, **kwargs):
1515
self.args = args
1616
self.kwargs = kwargs
1717

18-
def _add(
19-
self, name: str, type_: type, argp: "argparse.ArgumentParser", that: "Any"
20-
) -> None:
18+
def _add(self, name: str, type_: type, argp: "argparse.ArgumentParser", that: "Any") -> None:
2119
"""Add argument to parser."""
2220
args = []
2321
kwargs = {**self.kwargs}
@@ -26,15 +24,12 @@ def _add(
2624
action = kwargs.get("action")
2725
const = kwargs.get("const")
2826
default = kwargs.get("default", INVALID)
27+
# kind = type(default)
2928
# print(name, type_, that, "_add", action, flag_arg)
3029

3130
if action is None:
3231
if const is not None:
33-
kwargs["action"] = (
34-
"append_const"
35-
if type_ and issubclass(type_, list)
36-
else "store_const"
37-
)
32+
kwargs["action"] = "append_const" if type_ and issubclass(type_, list) else "store_const"
3833
elif type_ is None:
3934
kwargs["action"] = "store"
4035
elif issubclass(type_, bool):
@@ -50,7 +45,7 @@ def _add(
5045
else:
5146
assert default is INVALID or default is False
5247
kwargs["action"] = "store_true"
53-
elif issubclass(type_, list):
48+
elif issubclass(type_, list) or isinstance(default, list):
5449
if "nargs" not in kwargs:
5550
kwargs["action"] = "append"
5651
if "default" not in kwargs:
@@ -76,9 +71,7 @@ def _add(
7671
else:
7772

7873
def add_args(x: str) -> None:
79-
args.append(
80-
x if x.startswith("-") else (f"--{x}" if len(x) > 1 else f"-{x}")
81-
)
74+
args.append(x if x.startswith("-") else (f"--{x}" if len(x) > 1 else f"-{x}"))
8275

8376
for x in self.args:
8477
if " " in x or "\t" in x:
@@ -128,9 +121,7 @@ def __getattr__(self, name: str) -> "Any":
128121
try:
129122
m = super().__getattr__
130123
except AttributeError:
131-
raise AttributeError(
132-
f"{self.__class__.__name__} has no attribute {name}"
133-
) from None
124+
raise AttributeError(f"{self.__class__.__name__} has no attribute {name}") from None
134125
else:
135126
return m(name)
136127

@@ -165,9 +156,7 @@ def add_arguments(self, argp: "argparse.ArgumentParser") -> None:
165156
for k, v, t in _arg_fields(self):
166157
v._add(k, t, argp, self)
167158

168-
def parse_arguments(
169-
self, argp: "argparse.ArgumentParser", args: "Sequence[str]|None"
170-
) -> None:
159+
def parse_arguments(self, argp: "argparse.ArgumentParser", args: "Sequence[str]|None") -> None:
171160
"""Parse command line arguments."""
172161
sp = None
173162
for s, k in self.sub_args():

0 commit comments

Comments
 (0)