Skip to content

Commit 6881162

Browse files
committed
initialized
0 parents  commit 6881162

11 files changed

+371
-0
lines changed

.gitignore

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# IntelliJ project files
2+
.idea
3+
*.iml
4+
out
5+
gen
6+
7+
# Byte-compiled / optimized / DLL files
8+
__pycache__/
9+
*.py[cod]
10+
*$py.class
11+
12+
# C extensions
13+
*.so
14+
15+
# Distribution / packaging
16+
.Python
17+
build/
18+
develop-eggs/
19+
dist/
20+
downloads/
21+
eggs/
22+
.eggs/
23+
lib/
24+
lib64/
25+
parts/
26+
sdist/
27+
var/
28+
wheels/
29+
share/python-wheels/
30+
*.egg-info/
31+
.installed.cfg
32+
*.egg
33+
MANIFEST
34+
35+
# PyInstaller
36+
# Usually these files are written by a python script from a template
37+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
38+
*.manifest
39+
*.spec
40+
41+
# Installer logs
42+
pip-log.txt
43+
pip-delete-this-directory.txt
44+
45+
# Unit test / coverage reports
46+
htmlcov/
47+
.tox/
48+
.nox/
49+
.coverage
50+
.coverage.*
51+
.cache
52+
nosetests.xml
53+
coverage.xml
54+
*.cover
55+
*.py,cover
56+
.hypothesis/
57+
.pytest_cache/
58+
cover/
59+
60+
# Translations
61+
*.mo
62+
*.pot
63+
64+
# Django stuff:
65+
*.log
66+
local_settings.py
67+
db.sqlite3
68+
db.sqlite3-journal
69+
70+
# Flask stuff:
71+
instance/
72+
.webassets-cache
73+
74+
# Scrapy stuff:
75+
.scrapy
76+
77+
# Sphinx documentation
78+
docs/_build/
79+
80+
# PyBuilder
81+
.pybuilder/
82+
target/
83+
84+
# Jupyter Notebook
85+
.ipynb_checkpoints
86+
87+
# IPython
88+
profile_default/
89+
ipython_config.py
90+
91+
# pyenv
92+
# For a library or package, you might want to ignore these files since the code is
93+
# intended to run in multiple environments; otherwise, check them in:
94+
# .python-version
95+
96+
# pipenv
97+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
98+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
99+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
100+
# install all needed dependencies.
101+
#Pipfile.lock
102+
103+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
104+
__pypackages__/
105+
106+
# Celery stuff
107+
celerybeat-schedule
108+
celerybeat.pid
109+
110+
# SageMath parsed files
111+
*.sage.py
112+
113+
# Environments
114+
.env
115+
.venv
116+
env/
117+
venv/
118+
ENV/
119+
env.bak/
120+
venv.bak/
121+
122+
# Spyder project settings
123+
.spyderproject
124+
.spyproject
125+
126+
# Rope project settings
127+
.ropeproject
128+
129+
# mkdocs documentation
130+
/site
131+
132+
# mypy
133+
.mypy_cache/
134+
.dmypy.json
135+
dmypy.json
136+
137+
# Pyre type checker
138+
.pyre/
139+
140+
# pytype static type analyzer
141+
.pytype/
142+
143+
# Cython debug symbols
144+
cython_debug/
145+

config.ini

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[sonarr]
2+
baseUrl: http://192.168.0.1:8080
3+
apiKey: xxx
4+
5+
[peashooter]
6+
baseUrl: http://192.168.0.1:8081
7+
apiKey: xxx
8+
9+
[basic]
10+
seriesMatchRatio: 90

config.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import configparser
2+
3+
4+
class Config(object):
5+
def __init__(self):
6+
config = configparser.ConfigParser()
7+
config.read('config.ini')
8+
9+
self.sonarr = {
10+
'base_url': config['sonarr']['baseUrl'],
11+
'api_key': config['sonarr']['apiKey']
12+
}
13+
self.peashooter = {
14+
'base_url': config['peashooter']['baseUrl'],
15+
'api_key': config['peashooter']['apiKey']
16+
}
17+
self.basic = {
18+
'series_match_ratio': config['basic']['seriesMatchRatio']
19+
}

environment.yaml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: sonarr-subtitle-renamer
2+
channels:
3+
- defaults
4+
dependencies:
5+
- python=3.10
6+
- pip
7+
- requests
8+
- pip:
9+
- thefuzz
10+
- PySide6
11+
- levenshtein

main.py

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import os.path
2+
import re
3+
import sys
4+
from zipfile import ZipFile
5+
6+
import config
7+
from peashooter.client import PeashooterClient
8+
from sonarr.client import SonarrClient
9+
from thefuzz import fuzz
10+
11+
seriesMatchRatio = int(config.Config().basic['series_match_ratio'])
12+
13+
14+
def get_episode_list(series_id, season):
15+
sonarr_client = SonarrClient()
16+
file_list = sonarr_client.get_episode_file_list(series_id)
17+
file_list = list(filter(lambda item: item['seasonNumber'] == season, file_list))
18+
file_list.sort(key=lambda item: item['relativePath'])
19+
return file_list
20+
21+
22+
def find_series(name):
23+
"""
24+
模糊查询 series
25+
"""
26+
peashooter_client = PeashooterClient()
27+
series_list = peashooter_client.get_series_list()
28+
return list(filter(lambda series: fuzz.partial_ratio(series['name'], name) >= seriesMatchRatio, series_list))
29+
30+
31+
def get_output_dir(episode_list):
32+
"""
33+
获取 series 的存储路径
34+
"""
35+
output_dir = set()
36+
for episode in episode_list:
37+
# 先替换开始的 '/'
38+
split_parts = episode['path'].replace('/', '', 1).split('/')
39+
# 不包括映射目录,所以从 1 开始
40+
output_dir.add("/".join(split_parts[1:len(split_parts) - 1]))
41+
if len(output_dir) != 1:
42+
raise ValueError("输出路径有误,请检查")
43+
return output_dir.pop()
44+
45+
46+
def is_tc(filename):
47+
"""
48+
是繁体字幕
49+
"""
50+
pattern = re.compile(r".tcjp.|.tc.|-tc.", re.IGNORECASE)
51+
match = pattern.search(filename)
52+
return match is not None
53+
54+
55+
def array_fill(two_dimension_list):
56+
# 系统最大值递减来填充列表
57+
maxsize = sys.maxsize
58+
len_list = [len(item) for item in two_dimension_list]
59+
if set(len_list) != 1:
60+
maximum_len = max(*len_list)
61+
for item in two_dimension_list:
62+
for _ in range(maximum_len - len(item)):
63+
maxsize = maxsize - 1
64+
item.append(str(maxsize))
65+
return two_dimension_list
66+
67+
68+
def deduce_episode_num_solt(name_list):
69+
"""
70+
推断 episode 的位置
71+
"""
72+
pattern = re.compile(r'(\d+)')
73+
match_items = [pattern.findall(name) for name in name_list]
74+
# 填充列表,避免有空数据
75+
match_items = array_fill(match_items)
76+
for i, v in enumerate([set(item) for item in zip(*match_items)]):
77+
if len(v) == len(name_list):
78+
return i
79+
return -1
80+
81+
82+
def get_filename_episode_num_map(name_list):
83+
"""
84+
生成 "压缩文件内成员名 -> episode num" 的映射
85+
"""
86+
result = dict()
87+
solt = deduce_episode_num_solt(name_list)
88+
pattern = re.compile(r'(\d+)')
89+
for name in name_list:
90+
it = pattern.finditer(name)
91+
for i, v in enumerate(it):
92+
if i == solt:
93+
result[name] = int(v.group())
94+
return result
95+
96+
97+
def get_episode_num_filename_map(episode_list):
98+
"""
99+
生成 "episode num -> 最终字幕文件名" 的映射
100+
"""
101+
pattern = re.compile(r'S(\d+)E(\d+)')
102+
result = dict()
103+
for i, episode in enumerate(episode_list):
104+
# 最终字幕文件名称
105+
filename = re.sub("mkv|mp4", "zh.ass", episode['relativePath'].split("/")[1])
106+
match = pattern.search(filename)
107+
# 获取 episode 编号
108+
episode_num = int(match.group(2))
109+
if i + 1 != episode_num:
110+
raise ValueError('Episode 编号不匹配')
111+
result[episode_num] = filename
112+
return result
113+
114+
115+
def rename(series_id, season, zip_file_path, drive):
116+
# 从 sonarr 获取 episode 文件列表
117+
episode_file_list = get_episode_list(series_id, season)
118+
output_dir = os.path.join(drive, get_output_dir(episode_file_list))
119+
# 解压缩
120+
with ZipFile(zip_file_path) as zip_file:
121+
item_file_name_list = zip_file.namelist()
122+
item_file_name_list = list(filter(lambda item: not is_tc(item), item_file_name_list))
123+
item_file_name_list.sort()
124+
filename_episode_num_map = get_filename_episode_num_map(item_file_name_list)
125+
episode_num_filename_map = get_episode_num_filename_map(episode_file_list)
126+
127+
for item_file_name in item_file_name_list:
128+
with zip_file.open(item_file_name) as item_file:
129+
data = item_file.read()
130+
final_item_file_name = episode_num_filename_map.get(filename_episode_num_map.get(item_file_name))
131+
if final_item_file_name is not None:
132+
with open(os.path.join(output_dir, final_item_file_name), 'w+b') as copied_file:
133+
copied_file.write(data)
134+
135+
136+
if __name__ == '__main__':
137+
pass

peashooter/__init__.py

Whitespace-only changes.

peashooter/api.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import config
2+
3+
4+
class PeashooterApi(object):
5+
def __init__(self):
6+
cf = config.Config()
7+
self.api_key = cf.peashooter['api_key']
8+
self.base_url = cf.peashooter['base_url']
9+
10+
def get_series_list(self):
11+
pass

peashooter/client.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import json
2+
import requests
3+
from peashooter.api import PeashooterApi
4+
5+
6+
class PeashooterClient(PeashooterApi):
7+
8+
def get_series_list(self):
9+
get_series_list_uri = '/api/sonarr/series'
10+
params = {"apiKey": self.api_key}
11+
resp = requests.get(self.base_url + get_series_list_uri, params)
12+
return json.loads(resp.text)['data'] if resp.status_code == 200 else []

sonarr/__init__.py

Whitespace-only changes.

sonarr/api.py

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import config
2+
3+
4+
class SonarrApi(object):
5+
def __init__(self):
6+
cf = config.Config()
7+
self.base_url = cf.sonarr['base_url']
8+
self.api_key = cf.sonarr['api_key']
9+
10+
def get_episode_file_list(self, series_id):
11+
pass

sonarr/client.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import json
2+
import requests
3+
from sonarr.api import SonarrApi
4+
5+
6+
class SonarrClient(SonarrApi):
7+
8+
def get_episode_file_list(self, series_id):
9+
get_episode_file_list_uri = '/api/episodefile'
10+
params = {
11+
"apikey": self.api_key,
12+
"seriesId": series_id
13+
}
14+
resp = requests.get(self.base_url + get_episode_file_list_uri, params)
15+
return json.loads(resp.text) if resp.status_code == 200 else []

0 commit comments

Comments
 (0)