├── .gitignore
├── dockerfile_v2
└── dockerfile
├── dockerfile
└── dockerfile
├── MovieTitleTranslate.py
├── README.md
├── Emby_WithWatchdog.py
└── Emby_WithWatchdog_v2.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /.local/
--------------------------------------------------------------------------------
/dockerfile_v2/dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:alpine3.17
2 |
3 | LABEL maintainer="Xu@nCh3n"
4 |
5 | ENV TZ=Asia/Shanghai LANG=zh_CN.UTF-8
6 |
7 | RUN set -eux; \
8 | \
9 | update-ca-certificates; \
10 | \
11 | mkdir -p /usr/src/myapp/; \
12 | wget -O /usr/src/myapp/mywatchdog.py "https://raw.githubusercontent.com/Ccccx159/Emby_WithWatchdog/main/Emby_WithWatchdog_v2.py"; \
13 | python3 -m pip install --no-cache-dir watchdog requests -q;
14 |
15 | ENTRYPOINT ["python3"]
16 | CMD ["/usr/src/myapp/mywatchdog.py"]
17 |
--------------------------------------------------------------------------------
/dockerfile/dockerfile:
--------------------------------------------------------------------------------
1 | FROM ubuntu:latest
2 |
3 | LABEL maintainer=Xu@nCh3n
4 |
5 | ENV TZ Asia/Shanghai
6 | ENV LANG zh_CN.UTF-8
7 |
8 | RUN apt-get update -y \
9 | && apt-get -y install --no-install-recommends wget \
10 | && apt-get -y install --no-install-recommends python3 python3-pip python3-dev\
11 | && apt-get -y install --no-install-recommends libxml2-utils \
12 | && apt-get clean \
13 | && apt-get autoclean \
14 | && rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/*
15 |
16 | RUN cd /usr/bin \
17 | && ln -s python3 python \
18 | && python3 -m pip install --upgrade pip \
19 | && python3 -m pip install --no-cache-dir watchdog requests -q
20 |
21 | RUN wget -O /home/Emby_WithWatchdog.py "https://raw.githubusercontent.com/Ccccx159/Emby_WithWatchdog/main/Emby_WithWatchdog.py"
22 |
23 | ENTRYPOINT ["python3"]
24 | CMD ["/home/Emby_WithWatchdog.py"]
--------------------------------------------------------------------------------
/MovieTitleTranslate.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: UTF-8 -*-
3 |
4 | from unittest.result import failfast
5 | import xml.etree.ElementTree as ET
6 | import sys, os
7 |
8 |
9 | startDir = sys.argv[1]
10 | print(startDir)
11 | failedList = []
12 | movies = os.listdir(startDir)
13 | for m in movies:
14 | title = ''
15 | year = ''
16 | mPath = startDir + '/' + m
17 | print('==========================')
18 | print('begin to rename:', m)
19 | try:
20 | for home, dirs, files in os.walk(mPath):
21 | for fName in files:
22 | if fName.endswith('nfo'):
23 | fPath = mPath + '/' + fName
24 | nfoTree = ET.parse(fPath)
25 | nfoRoot = nfoTree.getroot()
26 | title = nfoRoot.find('title').text
27 | year = nfoRoot.find('year').text
28 | break
29 | break
30 | newMPath = startDir + '/' + title + '(' + year + ')'
31 | print(newMPath)
32 | os.rename(mPath, newMPath)
33 | except:
34 | print("rename <%s> failed!..." % m)
35 | print('==========================')
36 | failedList.append(mPath)
37 | continue
38 | print('rename successfully!!!!!!!!')
39 | print('==========================')
40 |
41 | if len(failedList) > 0:
42 | print('\n\nThe list of renamed failed:')
43 | for m in failedList:
44 | print(m)
45 | else:
46 | print('\n\nAll movie dir renamed Finished!!')
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Watchdog for Emby Media Server
2 |
3 |
4 | **!!! 基于 Emby Server 原生插件 webhoosk 实现的 Emby Notifier 以发布,不再依赖 Emby 刮削生成 nfo 元数据文件,不在依赖媒体库和媒体文件路径,推送消息效果和本项目一致,对挂载类的网盘媒体库更加友好,详情请参考:[Emby Notifier](https://github.com/Ccccx159/Emby_Notifier) !!!**
5 |
6 |
7 | ## 修订版本
8 |
9 | v1.x版本后续将不再更新维护,如有需要请更新使用v2.x版本!!!
10 |
11 | | 版本 | 日期 | 修订说明 |
12 | | ----- | ----- | ----- |
13 | | v2.000.4 | 2022.12.29 |
bug修复,推送消息中TMDB链接写死了类型为'movie',修复当类型为剧集时,TMDB链接指向错误的问题;删除IMDB链接尾部多余'$'符号,解决链接404的错误 |
14 | | v2.000.3 | 2022.12.28 | 新增v2.x版本dockerfile,docker image已上传至hub.docker.com |
15 | | v2.000.2 | 2022.12.27 | V2源码优化log文件路径获取方式,修改为可支持配置log文件目录;V2源码bug修复,在文件创建事件触发后,增加1s延时,修复由于文件创建后过快进入xml解析,导致解析xml失败,提示元素为0的错误 |
16 | | v2.000.1 | 2022.12.20 | 新增V2版本,去除外部xmllint依赖,由ElementTree解析nfo文件;bug修复,修复提前释放剧集存在air_date字段为None的情况下直接进行字符串的追加和替换导致的错误崩溃 |
17 | | v1.000.7 | 2022.10.31 | 新增dockerfile,基于ubuntu基础镜像构建,已上传镜像"b1gfac3c4t/overwatch"至hub.docker.com |
18 | | v1.000.6 | 2022.10.31 | 优化过滤条件,"movies"修改为"movie";"episodes"修改为"episode" |
19 | | v1.000.5 | 2022.10.28 | 优化python编码风格,修改log文件存储目录 |
20 | | v1.000.4 | 2022.08.18 | 新增MovieTitleTranslate.py小工具,用于按照刮削后,将电影目录重命名为nfo文件中的`title(year)`,统一所有电影目录的命名风格; |
21 | | v1.000.3 | 2022.06.10 | bug修复,修复新增剧集时,"season.nfo"未排除导致解析异常的问题; |
22 | | v1.000.2 | 2022.05.28 | bug修复,修复当文件名中出现单引号时,无法正确解析的问题; |
23 | | v1.000.1 | 2022.05.20 | 参数修改为从环境变量获取;原剧集信息获取逻辑修改,修复因tmdb的tv/detail更新不及时,导致新入库剧集信息不匹配的问题 |
24 |
25 | ## 简介
26 |
27 | 借助python中的看门狗模块(“watchdog”)监视emby媒体库目录,通过电报(telegram)的bot和channel,向频道订阅者推送Emby媒体库中新增影片信息,包括电影和剧集。
28 |
29 | ## 实现说明
30 |
31 | v2.x版本中,删除了原始版本中的xmllint依赖,仅通过python完成所有功能实现。**因此在dockerfile中将基础镜像由`ubuntu:latest`变更为`python:alpine3.17`,拉取后镜像体积由231MB减小至69.5MB,体积减少约70%**。
32 |
33 | **watchdog_for_Emby** 对 Emby Server 自动影片刮削生成的“xxxx.nfo”文件进行监控。影片新入库后,Emby Server 自动执行刮削生成xml格式的nfo文件,~~通过xmllint可以解析到部分该影片或者剧集的信息~~通过“ElementTree”模块解析nfo文件,获取当前影片的基本信息。而影片的封面图,和剧集的详细信息,则需要通过TMDB的api进行查询获取,通过调用"requests.get()"方法完成查询。在按照电报bot的api文档对payload数据组装后,调用"requests.post()"方法推送给bot,由bot发布至对应频道。
34 |
35 | ## 依赖项
36 |
37 | 1. python3.10及以上版本(v2.x版本中,使用了match..case..语法,仅在3.10及以上版本完成支持)
38 | 2. python Module: *watchdog*, *requests* (cmd: `pip3 install watchdog requests`),*ElementTree*
39 | 3. ~~xmllint (os: ubuntu 20.04,cmd: `sudo apt-get install libxml2-utils`)~~ v2.x版本中已去除此依赖
40 |
41 | ## 环境变量设置
42 |
43 | | 参数 | 说明 |
44 | | -- | -- |
45 | | BOT_TOKEN | 电报 bot token |
46 | | CHAT_ID | 电报频道 chat_id |
47 | | TMDB_API | TMDB api token |
48 | | MEDIA_PATH | Emby 媒体库路径 |
49 | | LOG_PATH | <可选>日志文件路径,默认为`/var/tmp/overwatch.log` |
50 |
51 | ## Docker Run
52 |
53 | ~~~shell
54 | docker run -d --name=watchdog-emby --restart=unless-stopped \
55 | -v "your media lib's host path":"media lib's container path" \
56 | -e BOT_TOKEN="your telegram bot's token" \
57 | -e CHAT_ID="your telegram channle's chat_id" \
58 | -e TMDB_API="tmdb api token" \
59 | -e MEDIA_PATH="media lib's container path" \
60 | -e LOG_PATH="log's output path" \
61 | b1gfac3c4t/overwatch
62 |
63 | ~~~
64 |
65 | ## 效果展示
66 |
67 | 电影:
68 |
69 | 
70 |
71 | 剧集:
72 |
73 | 
74 |
75 | ## 参考文档
76 |
77 | + tmdb api 文档:https://developers.themoviedb.org/3
78 | + telegram bot api 文档:https://core.telegram.org/bots/api
79 |
--------------------------------------------------------------------------------
/Emby_WithWatchdog.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: UTF-8 -*-
3 | from curses import resize_term
4 | import os
5 | import time
6 | import sys
7 | import re
8 | import json
9 | import logging
10 | from unicodedata import name
11 | from watchdog.observers import Observer
12 | from watchdog.events import FileSystemEventHandler, LoggingEventHandler
13 | import requests
14 |
15 | logging.basicConfig(
16 | level=logging.INFO,
17 | filename='/home/watchdog.log',
18 | filemode='a+',
19 | format='%(asctime)s - %(message)s',
20 | datefmt='%Y-%m-%d %H:%M:%S',
21 | )
22 |
23 | # 填充电报机器人的token
24 | TG_BOT_TOKEN = os.getenv('BOT_TOKEN')
25 | # 填充电报频道 chat_id
26 | TG_CHAT_ID = os.getenv('CHAT_ID')
27 | # 填充tmdb api token
28 | TMDB_API_TOKEN = os.getenv('TMDB_API')
29 | # 填充Emby媒体库路径
30 | EMBY_MEDIA_LIB_PATH = os.getenv('MEDIA_PATH')
31 |
32 | # 文件名中需额外转义字符
33 | ESCAPE_CHAR = [' ', '(', ')', '\'']
34 | # 排除的文件
35 | EXCLUDE_FILE = ['tvshow.nfo', 'season.nfo']
36 |
37 |
38 | def post_movieInfo(media_dir):
39 | tmp_list = list(media_dir)
40 | i = 0
41 | le = len(tmp_list)
42 | while i < le:
43 | if tmp_list[i] in ESCAPE_CHAR:
44 | tmp_list.insert(i, '\\')
45 | le += 1
46 | i += 2
47 | else:
48 | i += 1
49 | media_dir = ''.join(tmp_list)
50 | print(media_dir)
51 |
52 | # 获取电影标题
53 | cmd = (
54 | "echo \"cat //movie/title/text()\" | xmllint --shell "
55 | + media_dir
56 | + " | sed '1d;$d'"
57 | )
58 | media_title = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
59 | print(media_title)
60 | # 获取发行年份
61 | cmd = "xmllint --xpath '//movie/year/text()' " + media_dir
62 | media_year = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
63 | print(media_year)
64 | # 获取电影类型
65 | cmd = (
66 | "echo \"cat //movie/genre/text()\" | xmllint --shell "
67 | + media_dir
68 | + " | sed '1d;$d'"
69 | )
70 | media_type = os.popen(cmd).read()
71 | reg = re.compile('\n -------\n')
72 | media_type = reg.sub('|', media_type)[0 : len(reg.sub('|', media_type)) - 1]
73 | print(media_type)
74 | # 获取内容简介
75 | cmd = (
76 | "echo \"cat //movie/plot/text()\" | xmllint --nocdata --shell "
77 | + media_dir
78 | + " | sed '1d;$d'"
79 | )
80 | media_intro = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
81 | print(media_intro)
82 | # 获取上映日期
83 | cmd = "xmllint --xpath '//movie/releasedate/text()' " + media_dir
84 | media_rel = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
85 | print(media_rel)
86 | # 获取tmdb id
87 | cmd = "xmllint --xpath '//movie/tmdbid/text()' " + media_dir
88 | media_tmdbid = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
89 | print(media_tmdbid)
90 | # 获取imdb id
91 | cmd = "xmllint --xpath '//movie/imdbid/text()' " + media_dir
92 | media_imdbid = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
93 | print(media_imdbid)
94 | # 获取评分
95 | cmd = "xmllint --xpath '//movie/rating/text()' " + media_dir
96 | media_rating = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
97 | print(media_rating)
98 |
99 | # 组装tg_bot的post主体
100 | caption = (
101 | '#影视更新\n\[电影]\n片名: *'
102 | + media_title
103 | + '* ('
104 | + media_year
105 | + ')\n类型: '
106 | + media_type
107 | + '\n评分: '
108 | + media_rating
109 | + '\n\n上映日期: '
110 | + media_rel
111 | + '\n\n内容简介: '
112 | + media_intro
113 | + '\n\n相关链接: [TMDB](https://www.themoviedb.org/movie/'
114 | + media_tmdbid
115 | + '?language=zh-CN) | [IMDB](https://www.imdb.com/title/'
116 | + media_imdbid
117 | + ')\n'
118 | )
119 | print(caption)
120 |
121 | # 从tmdb获取电影封面
122 | tmdb_url = (
123 | "https://api.themoviedb.org/3/movie/"
124 | + media_tmdbid
125 | + "/images?api_key="
126 | + TMDB_API_TOKEN
127 | )
128 | res = requests.get(tmdb_url)
129 | img_num = len(res.json()['posters'])
130 | print(img_num)
131 | imgs = []
132 | for i in range(img_num):
133 | imgs.append(res.json()['posters'][i]['file_path'])
134 | print(imgs)
135 | index = 0
136 | while index < img_num:
137 | try:
138 |
139 | media_imgurl = "https://image.tmdb.org/t/p/w500" + imgs[index]
140 |
141 | post_data = {
142 | 'method': 'sendPhoto',
143 | 'chat_id': TG_CHAT_ID,
144 | 'photo': media_imgurl,
145 | 'caption': caption,
146 | 'parse_mode': 'Markdown',
147 | }
148 |
149 | # doPost
150 | tg_url = 'https://api.telegram.org/bot' + TG_BOT_TOKEN + '/'
151 | res = requests.post(tg_url, json=post_data)
152 | res.raise_for_status()
153 | break
154 | except:
155 | index += 1
156 | print("err occur! try next...")
157 | continue
158 | if index == img_num:
159 | post_data = {
160 | 'method': 'sendMessage',
161 | 'chat_id': TG_CHAT_ID,
162 | 'text': caption,
163 | 'parse_mode': 'Markdown',
164 | }
165 | try:
166 | # doPost
167 | tg_url = 'https://api.telegram.org/bot' + TG_BOT_TOKEN + '/'
168 | res = requests.post(tg_url, json=post_data)
169 | res.raise_for_status()
170 | except:
171 | print("Err!!!!!!!! plz check!!!!!!!!!!")
172 |
173 |
174 | def post_episodesInfo(media_dir):
175 | tmp_list = list(media_dir)
176 | i = 0
177 | le = len(tmp_list)
178 | while i < le:
179 | if tmp_list[i] in ESCAPE_CHAR:
180 | tmp_list.insert(i, '\\')
181 | le += 1
182 | i += 2
183 | else:
184 | i += 1
185 | media_dir = ''.join(tmp_list)
186 | print(media_dir)
187 | # 先从剧集nfo文件中提取当前的season和episode
188 | cmd = "xmllint --xpath '//episodedetails/season/text()' " + media_dir
189 | media_season = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
190 | cmd = "xmllint --xpath '//episodedetails/episode/text()' " + media_dir
191 | media_episode = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
192 | print('第' + media_season + '季|第' + media_episode + '集')
193 | # 获取剧集id
194 | media_dir = media_dir[0 : media_dir.find('Season')]
195 | # print(media_dir)
196 | media_dir += 'tvshow.nfo'
197 | # 发行年份
198 | cmd = "xmllint --xpath '//tvshow/year/text()' " + media_dir
199 | media_year = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
200 | # 剧集imdb id
201 | cmd = "xmllint --xpath '//tvshow/imdb_id/text()' " + media_dir
202 | media_imdbid = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
203 | # 剧集tmdb id
204 | cmd = "xmllint --xpath '//tvshow/tmdbid/text()' " + media_dir
205 | media_tmdbid = os.popen(cmd).read()[0 : len(os.popen(cmd).read()) - 1]
206 | print('tmdb_id: ' + media_tmdbid)
207 | # 获取剧集details
208 | tmdb_url = (
209 | 'https://api.themoviedb.org/3/tv/'
210 | + media_tmdbid
211 | + '?api_key='
212 | + TMDB_API_TOKEN
213 | + '&language=zh-CN'
214 | )
215 | res_tmdb = requests.get(tmdb_url)
216 | res_tmdb.encoding = 'utf-8'
217 | # print(res_tmdb.json())
218 | # 获取 剧集标题
219 | media_title = res_tmdb.json()['name']
220 | print(media_title)
221 | # 获取剧集类型
222 | media_genres = ''
223 | for i in range(len(res_tmdb.json()['genres'])):
224 | if 10765 == res_tmdb.json()['genres'][i]['id']:
225 | continue
226 | else:
227 | media_genres = (
228 | media_genres + res_tmdb.json()['genres'][i]['name'] + '|'
229 | )
230 | media_genres = media_genres[0 : len(media_genres) - 1]
231 | print(media_genres)
232 | # 获取评分
233 | media_rating = res_tmdb.json()['vote_average']
234 | print(media_rating)
235 |
236 | # 从tmdb获取剧集封面
237 | media_imgurl = (
238 | "https://image.tmdb.org/t/p/w500" + res_tmdb.json()['poster_path']
239 | )
240 | print(media_imgurl)
241 |
242 | # 获取当前集信息
243 | tmdb_url = (
244 | 'https://api.themoviedb.org/3/tv/'
245 | + media_tmdbid
246 | + '/season/'
247 | + media_season
248 | + '/episode/'
249 | + media_episode
250 | + '?api_key='
251 | + TMDB_API_TOKEN
252 | + '&language=zh-CN'
253 | )
254 | print(tmdb_url)
255 | res_tmdb = requests.get(tmdb_url)
256 | res_tmdb.encoding = 'utf-8'
257 | # print(res_tmdb.json())
258 |
259 | # 获取 season id + episodes id
260 | media_epinfo = (
261 | '第'
262 | + media_season
263 | + '季 | 第'
264 | + media_episode
265 | + '集 '
266 | + res_tmdb.json()['name']
267 | )
268 | print(media_epinfo)
269 | # 获取发布日期
270 | media_airDate = res_tmdb.json()['air_date']
271 | print(media_airDate)
272 | # 获取内容简介
273 | media_intro = res_tmdb.json()['overview']
274 | print(media_intro)
275 |
276 | # 组装tg_bot的post主体
277 | caption = (
278 | '#影视更新\n\[剧集]\n片名: *'
279 | + media_title
280 | + '* ('
281 | + media_year
282 | + ')\n剧集: '
283 | + media_epinfo
284 | + '\n类型: '
285 | + media_genres
286 | + '\n评分: '
287 | + str(media_rating)
288 | + '\n\n发布日期: '
289 | + str(media_airDate or 'no release date')
290 | + '\n\n内容简介: '
291 | + media_intro
292 | + '\n\n相关链接: [TMDB](https://www.themoviedb.org/tv/'
293 | + media_tmdbid
294 | + '?language=zh-CN) | [IMDB](https://www.imdb.com/title/'
295 | + media_imdbid
296 | + ')\n'
297 | )
298 | print(caption)
299 | post_data = {
300 | 'method': 'sendPhoto',
301 | 'chat_id': TG_CHAT_ID,
302 | 'photo': media_imgurl,
303 | 'caption': caption,
304 | "parse_mode": "Markdown",
305 | }
306 | # doPost
307 | tg_url = 'https://api.telegram.org/bot' + TG_BOT_TOKEN + '/'
308 | res = requests.post(tg_url, json=post_data)
309 |
310 |
311 | class LogHandler(LoggingEventHandler):
312 | def on_created(self, event):
313 | path = event.src_path
314 | if event.is_directory:
315 | pass
316 | else:
317 | logging.info(path + "文件新增")
318 |
319 | def on_modified(self, event):
320 | pass
321 |
322 |
323 | class MyHandler(FileSystemEventHandler):
324 | def on_created(self, event):
325 | path = event.src_path
326 | file_name = os.path.basename(path)
327 | if file_name.endswith("nfo") and path.find('movie') > 0:
328 | post_movieInfo(path)
329 | elif (
330 | file_name.endswith("nfo")
331 | and path.find('episode') > 0
332 | and path.find('recycle') < 0
333 | and path.find('eaDir') < 0
334 | and file_name not in EXCLUDE_FILE
335 | ):
336 | post_episodesInfo(path)
337 | else:
338 | pass
339 |
340 | print(event)
341 |
342 |
343 | if __name__ == '__main__':
344 | event_handler = MyHandler()
345 | observer = Observer()
346 | watch = observer.schedule(
347 | event_handler, path=EMBY_MEDIA_LIB_PATH, recursive=True
348 | )
349 |
350 | log_handler = LogHandler()
351 | observer.add_handler_for_watch(log_handler, watch) # 写入日志
352 | observer.start()
353 |
354 | try:
355 | while True:
356 | time.sleep(1)
357 | except KeyboardInterrupt:
358 | observer.stop()
359 | observer.join()
360 |
--------------------------------------------------------------------------------
/Emby_WithWatchdog_v2.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python3
2 | # -*- coding: UTF-8 -*-
3 | try:
4 | import xml.etree.cElementTree as ET
5 | except ImportError:
6 | import xml.etree.ElementTree as ET
7 |
8 | import string, requests, os, logging, time
9 | from xml.etree.ElementTree import Element
10 | from watchdog.observers import Observer
11 | from watchdog.events import FileSystemEventHandler, LoggingEventHandler
12 |
13 | # 填充电报机器人的token
14 | TG_BOT_URL = 'https://api.telegram.org/bot%s/' % os.getenv('BOT_TOKEN')
15 | # 填充电报频道 chat_id
16 | TG_CHAT_ID = os.getenv('CHAT_ID')
17 | # 填充tmdb api token
18 | TMDB_API_TOKEN = os.getenv('TMDB_API')
19 | # 填充Emby媒体库路径
20 | EMBY_MEDIA_LIB_PATH = os.getenv('MEDIA_PATH')
21 | # 日志路径
22 | LOG_PATH = os.getenv('LOG_PATH', "/var/tmp") + '/overwatch.log'
23 |
24 | EXCLUDE_FILE = ['tvshow.nfo', 'season.nfo']
25 |
26 | logging.basicConfig(
27 | level=logging.INFO,
28 | filename=LOG_PATH,
29 | filemode='a+',
30 | format='%(asctime)s - %(message)s',
31 | datefmt='%Y-%m-%d %H:%M:%S',
32 | )
33 |
34 |
35 | class POST_ERR(Exception):
36 | def __init__(self, value):
37 | self.value = value
38 |
39 | def __str__(self):
40 | return repr(self.value)
41 |
42 |
43 | class Media:
44 | def __init__(self, path: string, type: string) -> None:
45 | self.m_path = path
46 | self.m_type = type
47 | self.m_genre = ''
48 | self.m_tmdbid = ''
49 | self.m_imdbid = ''
50 | self.m_caption = (
51 | '#影视更新\n'
52 | + '\[{type_ch}]\n'
53 | + '片名: *{title}* ({year})\n'
54 | + '{episode}'
55 | + '类型: {genre}\n'
56 | + '评分: {rating}\n\n'
57 | + '上映日期: {rel}\n\n'
58 | + '内容简介: {intro}\n\n'
59 | + '相关链接: [TMDB](https://www.themoviedb.org/{type}/{tmdbid}?language=zh-CN) | [IMDB](https://www.imdb.com/title/{imdbid})\n'
60 | )
61 | self.m_caption = self.m_caption.replace('{type}', self.m_type)
62 |
63 | def m_PraseNfo(self) -> None:
64 | print('this is a parent prasing functiong')
65 |
66 | def m_post2Bot(self, imgUrl: list) -> None:
67 | for i, val in enumerate(imgUrl):
68 | try:
69 | media_poster_url = 'https://image.tmdb.org/t/p/w500%s' % val
70 | post_data = {
71 | 'method': 'sendPhoto',
72 | 'chat_id': TG_CHAT_ID,
73 | 'photo': media_poster_url,
74 | 'caption': self.m_caption,
75 | 'parse_mode': 'Markdown',
76 | }
77 | res = requests.post(TG_BOT_URL, json=post_data)
78 | res.raise_for_status()
79 | break
80 | except:
81 | print("err occur! try again...")
82 | continue
83 | if res.status_code != requests.codes.ok:
84 | print(
85 | 'media [%s] poster send failed...try send message...'
86 | % self.m_path
87 | )
88 | try:
89 | post_data = {
90 | 'method': 'sendMessage',
91 | 'chat_id': TG_CHAT_ID,
92 | 'text': self.m_caption,
93 | 'parse_mode': 'Markdown',
94 | }
95 | res = requests.post(TG_BOT_URL, json=post_data)
96 | res.raise_for_status()
97 | except:
98 | raise POST_ERR(
99 | 'ERR!!! Media [%s] post caption failed!!!!' % self.m_path
100 | )
101 |
102 | def m_printCaption(self) -> None:
103 | print(self.m_caption)
104 |
105 |
106 | class Movie(Media):
107 | def __init__(self, path: string, type: string) -> None:
108 | super().__init__(path, type)
109 |
110 | def m_PraseNfo(self) -> None:
111 | tree = ET.ElementTree(file=self.m_path)
112 | root = tree.getroot()
113 | for child in root:
114 | match child.tag:
115 | case 'title':
116 | self.m_caption = self.m_caption.replace(
117 | '{title}', child.text
118 | )
119 | case 'year':
120 | self.m_caption = self.m_caption.replace(
121 | '{year}', child.text
122 | )
123 | case 'genre':
124 | self.m_genre += child.text + '|'
125 | case 'rating':
126 | self.m_caption = self.m_caption.replace(
127 | '{rating}', child.text
128 | )
129 | case 'releasedate':
130 | self.m_caption = self.m_caption.replace('{rel}', child.text)
131 | case 'plot':
132 | self.m_caption = self.m_caption.replace(
133 | '{intro}', child.text
134 | )
135 | case 'tmdbid':
136 | self.m_caption = self.m_caption.replace(
137 | '{tmdbid}', child.text
138 | )
139 | self.m_tmdbid = child.text
140 | case 'imdbid':
141 | self.m_caption = self.m_caption.replace(
142 | '{imdbid}', child.text
143 | )
144 | case _:
145 | continue
146 | if len(self.m_genre) - 1 > 0:
147 | self.m_genre = self.m_genre[0 : len(self.m_genre) - 1]
148 | self.m_caption = self.m_caption.replace('{genre}', self.m_genre)
149 | self.m_caption = self.m_caption.replace('{episode}', '')
150 | self.m_caption = self.m_caption.replace('{type_ch}', '电影')
151 |
152 | def m_getPosterImgUrlList(self) -> list:
153 | tmdb_url = 'https://api.themoviedb.org/3/movie/%s/images?api_key=%s' % (
154 | self.m_tmdbid,
155 | TMDB_API_TOKEN,
156 | )
157 | res = requests.get(tmdb_url)
158 | img_num = len(res.json()['posters'])
159 | imgUrls = []
160 | for i in range(img_num):
161 | imgUrls.append(res.json()['posters'][i]['file_path'])
162 |
163 | return imgUrls
164 |
165 |
166 | class Episode(Media):
167 | def __init__(self, path: string, type: string) -> None:
168 | super().__init__(path, type)
169 |
170 | def m_PraseNfo(self) -> None:
171 | tree = ET.ElementTree(file=self.m_path)
172 | root = tree.getroot()
173 | for child in root:
174 | match child.tag:
175 | case 'season':
176 | season = child.text
177 | case 'episode':
178 | episode = child.text
179 | case _:
180 | continue
181 | #
182 | tvshow_path = self.m_path[0 : self.m_path.find('Season')] + 'tvshow.nfo'
183 | tree = ET.ElementTree(file=tvshow_path)
184 | root = tree.getroot()
185 | for child in root:
186 | match child.tag:
187 | case 'year':
188 | self.m_caption = self.m_caption.replace(
189 | '{year}', child.text
190 | )
191 | case 'tmdbid':
192 | self.m_caption = self.m_caption.replace(
193 | '{tmdbid}', child.text
194 | )
195 | self.m_tmdbid = child.text
196 | case 'imdb_id':
197 | self.m_caption = self.m_caption.replace(
198 | '{imdbid}', child.text
199 | )
200 | case _:
201 | continue
202 |
203 | # try get episode details
204 | tmdb_url = (
205 | 'https://api.themoviedb.org/3/tv/%s?api_key=%s&language=zh-CN'
206 | % (
207 | self.m_tmdbid,
208 | TMDB_API_TOKEN,
209 | )
210 | )
211 | res_tmdb = requests.get(tmdb_url)
212 | res_tmdb.encoding = 'utf-8'
213 | self.m_caption = self.m_caption.replace('{type_ch}', '剧集')
214 | self.m_caption = self.m_caption.replace(
215 | '{title}', res_tmdb.json()['name']
216 | )
217 | for i in range(len(res_tmdb.json()['genres'])):
218 | if 10765 == res_tmdb.json()['genres'][i]['id']:
219 | self.m_genre = self.m_genre + '科幻|'
220 | else:
221 | self.m_genre = (
222 | self.m_genre + res_tmdb.json()['genres'][i]['name'] + '|'
223 | )
224 | if len(self.m_genre) - 1 > 0:
225 | self.m_genre = self.m_genre[0 : len(self.m_genre) - 1]
226 | self.m_caption = self.m_caption.replace('{genre}', self.m_genre)
227 | self.m_caption = self.m_caption.replace(
228 | '{rating}', str(res_tmdb.json()['vote_average'])
229 | )
230 | # stored poster url
231 | self.m_posterUrl = [res_tmdb.json()['poster_path']]
232 |
233 | # try get current episode info
234 | tmdb_url = (
235 | 'https://api.themoviedb.org/3/tv/%s/season/%s/episode/%s?api_key=%s&language=zh-CN'
236 | % (self.m_tmdbid, season, episode, TMDB_API_TOKEN)
237 | )
238 | res_tmdb = requests.get(tmdb_url)
239 | res_tmdb.encoding = 'utf-8'
240 | self.m_caption = self.m_caption.replace(
241 | '{episode}',
242 | '剧集:第%s季|第%s集 %s\n' % (season, episode, res_tmdb.json()['name']),
243 | )
244 | self.m_caption = self.m_caption.replace(
245 | '{rel}', str(res_tmdb.json()['air_date'] or 'no release date')
246 | )
247 | self.m_caption = self.m_caption.replace(
248 | '{intro}', res_tmdb.json()['overview']
249 | )
250 |
251 | def m_getPosterImgUrlList(self) -> list:
252 | return self.m_posterUrl
253 |
254 |
255 | def MajorProcessOnCreate(path: string, type: string) -> None:
256 | if path[len(path) - 1] == '\n':
257 | path = path[0 : len(path) - 1]
258 | if 'movie' == type:
259 | mediaItem = Movie(path, type)
260 | tmp_path = path[0 : path.rfind('/')]
261 | name = tmp_path[tmp_path.rfind('/') + 1 :]
262 | elif 'tv' == type:
263 | mediaItem = Episode(path, type)
264 | tmp_path = path[0 : path.rfind('/')]
265 | tmp_path_x = tmp_path[0 : tmp_path.rfind('/')]
266 | name = tmp_path_x[tmp_path.rfind('/') + 1 :]
267 | mediaItem.m_PraseNfo()
268 | # mediaItem.m_printCaption()
269 | # mediaItem.m_post2Bot(mediaItem.m_getPosterImgUrlList())
270 | try:
271 | mediaItem.m_post2Bot(mediaItem.m_getPosterImgUrlList())
272 | except POST_ERR as e:
273 | print('[ERR] %s' % name)
274 | print('[OK] %s' % name)
275 |
276 |
277 | class LogHandler(LoggingEventHandler):
278 | def on_created(self, event):
279 | path = event.src_path
280 | if event.is_directory:
281 | pass
282 | else:
283 | logging.info(path + "文件新增")
284 |
285 | def on_modified(self, event):
286 | pass
287 |
288 |
289 | class MyHandler(FileSystemEventHandler):
290 | def on_created(self, event):
291 | # 在监测到文件创建后,添加1s延时,避免运行过快导致ET parse失败
292 | time.sleep(1)
293 | path = event.src_path
294 | file_name = os.path.basename(path)
295 | if file_name.endswith("nfo") and path.find('movie') > 0:
296 | MajorProcessOnCreate(path, 'movie')
297 | elif (
298 | file_name.endswith("nfo")
299 | and path.find('episode') > 0
300 | and path.find('recycle') < 0
301 | and path.find('eaDir') < 0
302 | and file_name not in EXCLUDE_FILE
303 | ):
304 | MajorProcessOnCreate(path, 'tv')
305 | else:
306 | pass
307 |
308 | print(event)
309 |
310 |
311 | if __name__ == '__main__':
312 | event_handler = MyHandler()
313 | observer = Observer()
314 | watch = observer.schedule(
315 | event_handler, path=EMBY_MEDIA_LIB_PATH, recursive=True
316 | )
317 |
318 | log_handler = LogHandler()
319 | observer.add_handler_for_watch(log_handler, watch) # 写入日志
320 | try:
321 | observer.start()
322 | except Exception as e:
323 | print('start err: %s' % str(e.args))
324 |
325 |
326 | try:
327 | while True:
328 | time.sleep(1)
329 | except KeyboardInterrupt:
330 | observer.stop()
331 | observer.join()
332 |
--------------------------------------------------------------------------------