feat: 初版

This commit is contained in:
范胜发
2022-12-13 14:54:11 +08:00
commit 7b04d8bc16
12 changed files with 371 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__

38
README.md Normal file
View 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
View File

@ -0,0 +1 @@
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

106
app.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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>