├── .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 | ![](https://user-images.githubusercontent.com/35327600/209752390-4e45180b-d8cc-4378-bd98-c489638f7cb7.png) 70 | 71 | 剧集: 72 | 73 | ![](https://user-images.githubusercontent.com/35327600/209752275-bad230b0-97a7-47e5-9a77-081afae7d6cf.png) 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 | --------------------------------------------------------------------------------