feat: 初版
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
### 说明
|
||||
|
||||
这个东西是在自建番剧影音服务器中由于 qBittorrent 的下载完成通知的模板过于简陋以及 JellyFin 的新增内容通知与下载完成的时机有一定偏差(模板有时候也不好看)而自学了两天 python 做出来用于给自己一个比较美观的邮件通知,并且可以搭配一些使用邮件触发的应用使用。
|
||||
|
||||
### 使用场景
|
||||
|
||||
这个脚本使用的 bgm.tv 的 api 用于获取番剧与剧集信息,如果想使用其他站点的 api,请自行修改。
|
||||
|
||||
### 使用方式
|
||||
|
||||
1. 将文件夹直接放到 qBittorrent 可以访问到的路径中
|
||||
2. 修改 config.yaml 文件中的配置
|
||||
3. 在 https://next.bgm.tv/demo/access-token 生成一个 Access Token, 并写入到 accessToken 文件中
|
||||
4. (可选)修改邮件模板和主题以及其他个性配置
|
||||
5. 确保 qBittorrent 的环境中有 python3 和 requests 包
|
||||
6. 在 qBittorrent 的下载完成时执行的输入框中填 `python3 <这里填路径>/bangumi-mail-notification/app.py "%N" "%D"`
|
||||
7. 测试
|
||||
|
||||
### config.yaml 配置介绍
|
||||
|
||||
```yaml
|
||||
email:
|
||||
from: ['邮件来源显示名称', 'example@163.com'] # 发送方
|
||||
user: example@example.com # 发送邮件的账户 同发送方
|
||||
passwd: xxxxxxxxxxxxxx # 发送邮件的密码(非邮件账户密码)
|
||||
smtp: smtp.163.com # smtp 邮件服务器
|
||||
port: 465 # 端口 ssl 为 True 时填 465
|
||||
ssl: True # 是否使用 ssl 加密
|
||||
# 配置收件人,同发送方
|
||||
to: [['收件人1名称', example1@163.com'], ['收件人2名称', example2@163.com']]
|
||||
|
||||
# 需要进行剧集信息搜索的分类名称(qBittorrent的分类)
|
||||
bangumiCategory: Bangumi
|
||||
|
||||
# 媒体库的地址与图标url
|
||||
mediaUrl: https://media.example.com
|
||||
mediaIcon: https://picbed.example.com/media.png
|
||||
```
|
1
accessToken
Normal file
1
accessToken
Normal file
@ -0,0 +1 @@
|
||||
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
106
app.py
Normal file
106
app.py
Normal file
@ -0,0 +1,106 @@
|
||||
|
||||
import sys
|
||||
import re
|
||||
from urllib.parse import quote
|
||||
import bgmTVApi
|
||||
import mail
|
||||
from generateMailContent import generate
|
||||
from loadYaml import loadYaml
|
||||
|
||||
# 获取 qBittorrent 传递的参数
|
||||
name = sys.argv[1]
|
||||
path = sys.argv[2]
|
||||
|
||||
# 根据路径参数获取分类与番剧名称
|
||||
category = path.split('/')[2]
|
||||
subject_name = quote(path.split('/')[3], 'utf-8')
|
||||
|
||||
# 番剧信息
|
||||
subjectInfo = {}
|
||||
isGetSubjectInfo = False
|
||||
|
||||
# 剧集信息
|
||||
episodeInfo = {}
|
||||
isGetEpisodeInfo = False
|
||||
|
||||
# 错误信息 用于发生请求或者其他错误时,可以通过邮件告知到管理员
|
||||
errorMsgs = []
|
||||
|
||||
# 读取配置文件
|
||||
config = loadYaml('config.yaml')
|
||||
|
||||
mediaUrl = config.get('mediaUrl')
|
||||
mediaIcon = config.get('mediaIcon')
|
||||
|
||||
# 是否是番剧分类,用于判断是否能够在bgmtv中获取信息
|
||||
isBangumi = category == config.get('bangumiCategory')
|
||||
|
||||
# 从标题获取集号的规则
|
||||
rules = [
|
||||
r"(.*) - (\d{1,4}|\d{1,4}\.\d{1,2})(?:v\d{1,2})?(?: )?(?:END)?(.*)",
|
||||
r"(.*)[\[ E](\d{1,3}|\d{1,3}\.\d{1,2})(?:v\d{1,2})?(?: )?(?:END)?[\] ](.*)",
|
||||
r"(.*)\[第(\d*\.*\d*)话(?:END)?\](.*)",
|
||||
r"(.*)\[第(\d*\.*\d*)話(?:END)?\](.*)",
|
||||
r"(.*)第(\d*\.*\d*)话(?:END)?(.*)",
|
||||
r"(.*)第(\d*\.*\d*)話(?:END)?(.*)",
|
||||
]
|
||||
|
||||
if isBangumi:
|
||||
# 获取集号
|
||||
episodeNo = 1
|
||||
for rule in rules:
|
||||
match_obj = re.match(rule, name, re.I)
|
||||
if match_obj is not None:
|
||||
episodeNo = match_obj.group(2)
|
||||
|
||||
# 获取番剧信息
|
||||
subjectRes = bgmTVApi.fetchSubjectInfo(subject_name)
|
||||
subjectInfo = subjectRes['data']
|
||||
isGetSubjectInfo = subjectRes['status']
|
||||
if not isGetSubjectInfo:
|
||||
errorMsgs.append(subjectRes['message'])
|
||||
|
||||
# 获取剧集信息
|
||||
episodeRes = bgmTVApi.fetchEpisodeInfo(subjectInfo.get('id'), episodeNo)
|
||||
episodeInfo = episodeRes['data']
|
||||
isGetEpisodeInfo = episodeRes['status']
|
||||
if not isGetEpisodeInfo:
|
||||
errorMsgs.append(episodeRes['message'])
|
||||
|
||||
# 确认邮件标题
|
||||
mailSubject = ''
|
||||
if isGetSubjectInfo:
|
||||
mailSubject = subjectInfo.get('name_cn') + ' 更新啦!'
|
||||
else:
|
||||
mailSubject = name + ' 下载完成'
|
||||
|
||||
# 生成邮件内容
|
||||
mailContent = ''
|
||||
if isGetSubjectInfo & isGetEpisodeInfo:
|
||||
data = {
|
||||
'title': mailSubject,
|
||||
'name': episodeInfo.get('name_cn'),
|
||||
'image': subjectInfo.get('images').get('large'),
|
||||
'summary': subjectInfo.get('summary'),
|
||||
'desc': episodeInfo.get('desc'),
|
||||
'mediaUrl': mediaUrl,
|
||||
'mediaIcon': mediaIcon
|
||||
}
|
||||
mailContent = generate('./template/email1.html', data)
|
||||
elif len(errorMsgs) > 0:
|
||||
data = {
|
||||
'title': mailSubject,
|
||||
'name': name,
|
||||
'path': path,
|
||||
'errorMsgs': errorMsgs
|
||||
}
|
||||
mailContent = generate('./template/email2.html', data)
|
||||
else:
|
||||
data = {
|
||||
'title': mailSubject,
|
||||
'name': name,
|
||||
'path': path
|
||||
}
|
||||
mailContent = generate('./template/email3.html', data)
|
||||
|
||||
mail.send(mailSubject, mailContent, isBangumi)
|
61
bgmTVApi.py
Normal file
61
bgmTVApi.py
Normal file
@ -0,0 +1,61 @@
|
||||
import requests
|
||||
from requests import exceptions
|
||||
|
||||
# 消除ssl告警
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
tokenFile = open('accessToken', encoding = "utf-8")
|
||||
token = tokenFile.read()
|
||||
tokenFile.close()
|
||||
|
||||
headers = { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json', 'User-Agent': 'Hiiragi/bangumi-mail-notification' }
|
||||
|
||||
def fetchSubjectInfo(subject_name):
|
||||
result = {
|
||||
'status': False,
|
||||
'data': None,
|
||||
'message': ''
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url=f'https://api.bgm.tv/search/subject/{subject_name}?type=2&responseGroup=medium', verify=False, headers=headers)
|
||||
except exceptions.Timeout as e:
|
||||
result['message'] = '番剧信息请求超时:' + str(e.message)
|
||||
except exceptions.HTTPError as e:
|
||||
result['message'] = '番剧信息http请求错误:' + str(e.message)
|
||||
else:
|
||||
if response.status_code == 200:
|
||||
resJson = response.json()
|
||||
if resJson.get('list') is not None:
|
||||
result['data'] = resJson.get('list')[0]
|
||||
result['status'] = True
|
||||
else:
|
||||
result['message'] = '番剧信息请求错误:' + str(response.status_code) + ',' + str(response.reason)
|
||||
|
||||
return result
|
||||
|
||||
def fetchEpisodeInfo(subject_id, episodeNo):
|
||||
result = {
|
||||
'status': False,
|
||||
'data': None,
|
||||
'message': ''
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url=f'https://api.bgm.tv/v0/episodes?subject_id={subject_id}', verify=False, headers=headers)
|
||||
except exceptions.Timeout as e:
|
||||
result['message'] = '剧集信息请求超时:' + str(e.message)
|
||||
except exceptions.HTTPError as e:
|
||||
result['message'] = '剧集信息http请求错误:' + str(e.message)
|
||||
else:
|
||||
if response.status_code == 200:
|
||||
resJson = response.json()
|
||||
if resJson.get('data') is not None:
|
||||
for d in resJson.get('data'):
|
||||
if d.get('ep') == int(episodeNo):
|
||||
result['data'] = d
|
||||
result['status'] = True
|
||||
else:
|
||||
result['message'] = '剧集信息请求错误:' + str(response.status_code) + ',' + str(response.reason)
|
||||
|
||||
return result
|
16
config.yaml
Normal file
16
config.yaml
Normal file
@ -0,0 +1,16 @@
|
||||
email:
|
||||
from: ['邮件来源显示名称', 'example@163.com'] # 发送方
|
||||
user: example@example.com # 发送邮件的账户 同发送方
|
||||
passwd: xxxxxxxxxxxxxx # 发送邮件的密码(非邮件账户密码)
|
||||
smtp: smtp.163.com # smtp 邮件服务器
|
||||
port: 465 # 端口 ssl 为 True 时填 465
|
||||
ssl: True # 是否使用 ssl 加密
|
||||
# 配置收件人,同发送方
|
||||
to: [['收件人1名称', example1@163.com'], ['收件人2名称', example2@163.com']]
|
||||
|
||||
# 需要进行剧集信息搜索的分类名称(qBittorrent的分类)
|
||||
bangumiCategory: Bangumi
|
||||
|
||||
# 媒体库的地址与图标url
|
||||
mediaUrl: https://media.example.com
|
||||
mediaIcon: https://picbed.example.com/media.png
|
17
generateMailContent.py
Normal file
17
generateMailContent.py
Normal file
@ -0,0 +1,17 @@
|
||||
import re
|
||||
from functools import partial
|
||||
|
||||
def getTemplate(filePath):
|
||||
file = open(filePath, encoding='utf-8')
|
||||
fileData = file.read()
|
||||
file.close()
|
||||
return fileData
|
||||
|
||||
def replaceStr(data, m):
|
||||
print(m.group(0))
|
||||
param = m.group(0)[2:-2]
|
||||
return '{}'.format(data.get(param))
|
||||
|
||||
def generate(templatePath, data):
|
||||
dataPat = re.compile(r'\{\{(\w*)\}\}')
|
||||
return dataPat.sub(partial(replaceStr, data), getTemplate(templatePath))
|
9
loadYaml.py
Normal file
9
loadYaml.py
Normal file
@ -0,0 +1,9 @@
|
||||
import yaml
|
||||
|
||||
def loadYaml(path):
|
||||
file = open(path, encoding='utf-8')
|
||||
configData = file.read()
|
||||
file.close()
|
||||
|
||||
config = yaml.safe_load(configData)
|
||||
return config
|
46
mail.py
Normal file
46
mail.py
Normal file
@ -0,0 +1,46 @@
|
||||
from loadYaml import loadYaml
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
|
||||
config = loadYaml('config.yaml')
|
||||
emailConfig = config.get('email')
|
||||
|
||||
sender = emailConfig.get('from')
|
||||
user = emailConfig.get('user')
|
||||
passwd = emailConfig.get('passwd')
|
||||
receivers = emailConfig.get('to')
|
||||
|
||||
receiversEmailAddress = []
|
||||
receiversAddr = []
|
||||
for recipient in receivers:
|
||||
receiversEmailAddress.append(recipient[1])
|
||||
receiversAddr.append(formataddr(recipient))
|
||||
|
||||
def send(subject, content, sendAll):
|
||||
message = MIMEMultipart('related')
|
||||
message['From'] = formataddr(sender)
|
||||
if sendAll:
|
||||
message['To'] = ", ".join(receiversAddr)
|
||||
else:
|
||||
message['To'] = receiversAddr[0]
|
||||
|
||||
|
||||
message['Subject'] = subject
|
||||
|
||||
msgAlternative = MIMEMultipart('alternative')
|
||||
message.attach(msgAlternative)
|
||||
|
||||
msgAlternative.attach(MIMEText(content, 'html', 'utf-8'))
|
||||
|
||||
server = smtplib.SMTP_SSL('smtp.163.com', 465)
|
||||
server.login(user, passwd)
|
||||
|
||||
if sendAll:
|
||||
server.sendmail(user, receiversEmailAddress, message.as_string())
|
||||
else:
|
||||
server.sendmail(user, [receiversEmailAddress[0]], message.as_string())
|
||||
|
||||
server.quit()
|
||||
print('邮件发送成功')
|
40
template/email1.html
Normal file
40
template/email1.html
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>{{name}}</h3>
|
||||
<table width="500px">
|
||||
<tr>
|
||||
<td>
|
||||
<div>
|
||||
<img src="{{image}}" width="200px" alt="" style="float: left; margin-right: 10px;">
|
||||
<p style="text-indent: 2em;">{{desc}}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<br />
|
||||
<p style="text-indent: 2em;">{{summary}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<div style="text-align: center;">
|
||||
<a href="{{mediaUrl}}" style="display:
|
||||
inline-block; padding: 5px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 5px;">
|
||||
<img src="{{mediaIcon}}" height="50px" />
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
19
template/email2.html
Normal file
19
template/email2.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>{{name}}</h3>
|
||||
<br>
|
||||
<p>下载路径:{{path}}</p>
|
||||
<h2>请求错误</h2>
|
||||
<p>{{errorMsgs}}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
17
template/email3.html
Normal file
17
template/email3.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{title}}</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h3>{{name}}</h3>
|
||||
<br>
|
||||
<p>下载路径:{{path}}</p>
|
||||
</body>
|
||||
|
||||
</html>
|
Reference in New Issue
Block a user