commit 7b04d8bc16b2cc02f131cc870ab27ca349e002e9 Author: 范胜发 Date: Tue Dec 13 14:54:11 2022 +0800 feat: 初版 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed8ebf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..48ffc97 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/accessToken b/accessToken new file mode 100644 index 0000000..cde986a --- /dev/null +++ b/accessToken @@ -0,0 +1 @@ +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..ab4e2e8 --- /dev/null +++ b/app.py @@ -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) diff --git a/bgmTVApi.py b/bgmTVApi.py new file mode 100644 index 0000000..90093dc --- /dev/null +++ b/bgmTVApi.py @@ -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 diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..f9042e6 --- /dev/null +++ b/config.yaml @@ -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 diff --git a/generateMailContent.py b/generateMailContent.py new file mode 100644 index 0000000..446d55a --- /dev/null +++ b/generateMailContent.py @@ -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)) diff --git a/loadYaml.py b/loadYaml.py new file mode 100644 index 0000000..6718a86 --- /dev/null +++ b/loadYaml.py @@ -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 \ No newline at end of file diff --git a/mail.py b/mail.py new file mode 100644 index 0000000..d94eefe --- /dev/null +++ b/mail.py @@ -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('邮件发送成功') \ No newline at end of file diff --git a/template/email1.html b/template/email1.html new file mode 100644 index 0000000..cb7a6f8 --- /dev/null +++ b/template/email1.html @@ -0,0 +1,40 @@ + + + + + + + + {{title}} + + + +

{{name}}

+ + + + + + + +
+
+ +

{{desc}}

+
+
+
+

{{summary}}

+
+
+
+ + + +
+ + + \ No newline at end of file diff --git a/template/email2.html b/template/email2.html new file mode 100644 index 0000000..726c5c9 --- /dev/null +++ b/template/email2.html @@ -0,0 +1,19 @@ + + + + + + + + {{title}} + + + +

{{name}}

+
+

下载路径:{{path}}

+

请求错误

+

{{errorMsgs}}

+ + + \ No newline at end of file diff --git a/template/email3.html b/template/email3.html new file mode 100644 index 0000000..1776978 --- /dev/null +++ b/template/email3.html @@ -0,0 +1,17 @@ + + + + + + + + {{title}} + + + +

{{name}}

+
+

下载路径:{{path}}

+ + + \ No newline at end of file