shownamer 1.0.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.
- shownamer/__init__.py +0 -0
- shownamer/cli.py +151 -0
- shownamer-1.0.0.dist-info/METADATA +112 -0
- shownamer-1.0.0.dist-info/RECORD +8 -0
- shownamer-1.0.0.dist-info/WHEEL +5 -0
- shownamer-1.0.0.dist-info/entry_points.txt +2 -0
- shownamer-1.0.0.dist-info/licenses/LICENSE +21 -0
- shownamer-1.0.0.dist-info/top_level.txt +1 -0
shownamer/__init__.py
ADDED
|
File without changes
|
shownamer/cli.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import requests
|
|
4
|
+
import argparse
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
# Default supported video extensions
|
|
8
|
+
DEFAULT_EXTENSIONS = [".mkv", ".mp4", ".avi", ".mov", ".flv"]
|
|
9
|
+
TOOL_VERSION = "1.0.0"
|
|
10
|
+
|
|
11
|
+
# Regex to parse filename
|
|
12
|
+
FILENAME_PATTERN = re.compile(
|
|
13
|
+
r"^(?P<show>.+?)\s+[Ss](?P<season>\d{2})[Ee](?P<episode>\d{2})"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Characters not allowed in filenames
|
|
17
|
+
INVALID_CHARS = r'\/:*?"<>|'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def sanitize_filename(name):
|
|
21
|
+
"""Remove or replace characters that are not allowed in filenames."""
|
|
22
|
+
return "".join(c if c not in INVALID_CHARS else "_" for c in name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_episode_title(show_name, season, episode):
|
|
26
|
+
"""Query TVmaze API for the episode title."""
|
|
27
|
+
response = requests.get(
|
|
28
|
+
"https://api.tvmaze.com/singlesearch/shows", params={"q": show_name}
|
|
29
|
+
)
|
|
30
|
+
if response.status_code != 200:
|
|
31
|
+
return None
|
|
32
|
+
show_data = response.json()
|
|
33
|
+
show_id = show_data["id"]
|
|
34
|
+
|
|
35
|
+
episode_response = requests.get(
|
|
36
|
+
f"https://api.tvmaze.com/shows/{show_id}/episodebynumber",
|
|
37
|
+
params={"season": season, "number": episode},
|
|
38
|
+
)
|
|
39
|
+
if episode_response.status_code != 200:
|
|
40
|
+
return None
|
|
41
|
+
return episode_response.json()["name"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def list_series_summary(directory, extensions):
|
|
45
|
+
"""List all detected show names with season/episode counts."""
|
|
46
|
+
stats = defaultdict(lambda: defaultdict(int))
|
|
47
|
+
for filename in os.listdir(directory):
|
|
48
|
+
name, ext = os.path.splitext(filename)
|
|
49
|
+
if ext.lower() not in extensions:
|
|
50
|
+
continue
|
|
51
|
+
match = FILENAME_PATTERN.match(name)
|
|
52
|
+
if not match:
|
|
53
|
+
continue
|
|
54
|
+
show = match.group("show").strip()
|
|
55
|
+
season = int(match.group("season"))
|
|
56
|
+
stats[show][season] += 1
|
|
57
|
+
|
|
58
|
+
if not stats:
|
|
59
|
+
print("No recognizable TV show files found.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
print("Detected Series Summary:")
|
|
63
|
+
for show, seasons in stats.items():
|
|
64
|
+
print(f" {show}:")
|
|
65
|
+
for season, count in sorted(seasons.items()):
|
|
66
|
+
print(f" Season {season:02}: {count} episode(s)")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def rename_files(directory, extensions, dry_run=False, verbose=False):
|
|
70
|
+
for filename in os.listdir(directory):
|
|
71
|
+
name, ext = os.path.splitext(filename)
|
|
72
|
+
if ext.lower() not in extensions:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
match = FILENAME_PATTERN.match(name)
|
|
76
|
+
if not match:
|
|
77
|
+
if verbose:
|
|
78
|
+
print(f"[skip] {filename}: doesn't match expected pattern")
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
show = match.group("show").strip()
|
|
82
|
+
season = int(match.group("season"))
|
|
83
|
+
episode = int(match.group("episode"))
|
|
84
|
+
|
|
85
|
+
episode_title = get_episode_title(show, season, episode)
|
|
86
|
+
if not episode_title:
|
|
87
|
+
print(f"[fail] {filename}: could not fetch episode title")
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
safe_title = sanitize_filename(episode_title)
|
|
91
|
+
new_filename = f"{show} S{season:02}E{episode:02} - {safe_title}{ext}"
|
|
92
|
+
src = os.path.join(directory, filename)
|
|
93
|
+
dst = os.path.join(directory, new_filename)
|
|
94
|
+
|
|
95
|
+
if dry_run:
|
|
96
|
+
print(f"[dry-run] {filename} → {new_filename}")
|
|
97
|
+
else:
|
|
98
|
+
os.rename(src, dst)
|
|
99
|
+
print(f"[renamed] {filename} → {new_filename}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
parser = argparse.ArgumentParser(
|
|
104
|
+
description="Rename TV episode files by fetching episode titles from TVmaze."
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--dir",
|
|
108
|
+
default=os.getcwd(),
|
|
109
|
+
help="Directory containing video files (default: script's directory)",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"--ext",
|
|
113
|
+
nargs="*",
|
|
114
|
+
default=DEFAULT_EXTENSIONS,
|
|
115
|
+
help="List of allowed video file extensions (default: common types)",
|
|
116
|
+
)
|
|
117
|
+
parser.add_argument(
|
|
118
|
+
"--dry-run", action="store_true", help="Preview renaming without making changes"
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--verbose", action="store_true", help="Show skipped files and debug info"
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--version", action="store_true", help="Show tool version and exit"
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"--name", action="store_true", help="List show names and episode counts"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
args = parser.parse_args()
|
|
131
|
+
|
|
132
|
+
extensions = [e if e.startswith(".") else "." + e for e in args.ext]
|
|
133
|
+
|
|
134
|
+
if args.version:
|
|
135
|
+
print(f"shownamer version {TOOL_VERSION}")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
if args.name:
|
|
139
|
+
list_series_summary(args.dir, extensions)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
rename_files(
|
|
143
|
+
directory=args.dir,
|
|
144
|
+
extensions=extensions,
|
|
145
|
+
dry_run=args.dry_run,
|
|
146
|
+
verbose=args.verbose,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shownamer
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Rename TV show Media files with Respective episode Titles/Names.
|
|
5
|
+
Home-page: https://github.com/theamallalgi/shownamer
|
|
6
|
+
Author: Amal Lalgi
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Requires-Python: >=3.6
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests
|
|
14
|
+
Dynamic: author
|
|
15
|
+
Dynamic: classifier
|
|
16
|
+
Dynamic: description
|
|
17
|
+
Dynamic: description-content-type
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-dist
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
Dynamic: summary
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
# Shownamer - Media Renamer
|
|
26
|
+
A lightweight Python CLI tool that renames TV show episode video files by fetching their actual episode titles. Movies are not supported, All TV Shows and Sitcoms are supported.
|
|
27
|
+
|
|
28
|
+
>[!NOTE]
|
|
29
|
+
> Fetches details and Names from [TVmaze](https://www.tvmaze.com/).
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- Automatically detects and renames files like:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
Before: Malcolm in the Middle S01E10.mkv
|
|
37
|
+
After: Malcolm in the Middle S01E10 - Stock Car Races.mkv
|
|
38
|
+
```
|
|
39
|
+
- Fetches episode titles using the free TVmaze API.
|
|
40
|
+
- Cleans illegal filename characters automatically.
|
|
41
|
+
- CLI flags for:
|
|
42
|
+
- Custom directory
|
|
43
|
+
- File extension filter
|
|
44
|
+
- Dry-run mode
|
|
45
|
+
- Verbose logging
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
### Requirements
|
|
50
|
+
|
|
51
|
+
- Python 3.6+
|
|
52
|
+
- `requests` module
|
|
53
|
+
|
|
54
|
+
Install requirements:
|
|
55
|
+
```bash
|
|
56
|
+
pip install requests
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Run the script
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python shownamer.py
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Example Usage
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Rename all valid video files in the current directory
|
|
69
|
+
python shownamer.py
|
|
70
|
+
|
|
71
|
+
# Specify a custom directory
|
|
72
|
+
python shownamer.py --dir "/path/to/your/episodes"
|
|
73
|
+
|
|
74
|
+
# Preview changes without renaming files
|
|
75
|
+
python shownamer.py --dry-run
|
|
76
|
+
|
|
77
|
+
# Limit to specific extensions
|
|
78
|
+
python shownamer.py --ext mkv mp4 avi
|
|
79
|
+
|
|
80
|
+
# Verbose mode (shows skipped files, debug info)
|
|
81
|
+
python shownamer.py --verbose
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Supported Formats
|
|
85
|
+
|
|
86
|
+
The script supports the following file name pattern:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Show Name S01E01.ext
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It handles these video formats by default:
|
|
93
|
+
|
|
94
|
+
- `.mkv`
|
|
95
|
+
- `.mp4`
|
|
96
|
+
- `.avi`
|
|
97
|
+
- `.mov`
|
|
98
|
+
- `.flv`
|
|
99
|
+
|
|
100
|
+
You can customize this with the `--ext` flag.
|
|
101
|
+
|
|
102
|
+
## FAQ
|
|
103
|
+
|
|
104
|
+
### Will this overwrite existing files?
|
|
105
|
+
No. The script does not overwrite files. It renames only when the target filename does not exist. You can use `--dry-run` to preview the result first.
|
|
106
|
+
|
|
107
|
+
### Does it fetch subtitles or cover images?
|
|
108
|
+
No. This tool only renames the video files with accurate episode titles.
|
|
109
|
+
|
|
110
|
+
## Contributions
|
|
111
|
+
|
|
112
|
+
Pull requests, suggestions, and issues are welcome! Let's make it smarter and broader (e.g., subtitle renaming, fuzzy matching, show aliases, etc.).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
shownamer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
shownamer/cli.py,sha256=EDhQZK1ipasyj_wE9cAT2obhoh01t9b9JXAWQOTEJPc,4739
|
|
3
|
+
shownamer-1.0.0.dist-info/licenses/LICENSE,sha256=PqJKXIwhmeOuMo-i9B6aXn9GgjJ9TcMkIglF_uhesYg,1088
|
|
4
|
+
shownamer-1.0.0.dist-info/METADATA,sha256=qblwCA8L5VnbVNccPfIiBJolpzWqZQ634edbosiRuq4,2839
|
|
5
|
+
shownamer-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
shownamer-1.0.0.dist-info/entry_points.txt,sha256=Cgm3ZYbYuwA9y2ygRxk618jsXc3qeqEQ4EKEwQzIADI,49
|
|
7
|
+
shownamer-1.0.0.dist-info/top_level.txt,sha256=gFru4EHM37wcJ4uZ8dSEYz1Frj2URzqt--WDVarUHYg,10
|
|
8
|
+
shownamer-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Amal lalgi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
shownamer
|