Merge remote-tracking branch 'origin/main'

This commit is contained in:
jxxghp
2024-01-05 08:20:17 +08:00
7 changed files with 215 additions and 209 deletions

View File

@@ -34,7 +34,7 @@
"DoubanSync": {
"name": "豆瓣想看",
"description": "同步豆瓣想看数据,自动添加订阅。",
"version": "1.1",
"version": "1.2",
"icon": "douban.png",
"author": "jxxghp",
"level": 2
@@ -58,7 +58,7 @@
"DoubanRank": {
"name": "豆瓣榜单订阅",
"description": "监控豆瓣热门榜单,自动添加订阅。",
"version": "1.2",
"version": "1.4",
"icon": "movie.jpg",
"author": "jxxghp",
"level": 2
@@ -66,7 +66,7 @@
"LibraryScraper": {
"name": "媒体库刮削",
"description": "定时对媒体库进行刮削,补齐缺失元数据和图片。",
"version": "1.1",
"version": "1.2",
"icon": "scraper.png",
"author": "jxxghp",
"level": 1
@@ -210,7 +210,7 @@
"BrushFlow": {
"name": "站点刷流",
"description": "自动托管刷流,将会提高对应站点的访问频率。",
"version": "1.0",
"version": "1.1",
"icon": "brush.jpg",
"author": "jxxghp",
"level": 2
@@ -258,7 +258,7 @@
"CloudDiskDel": {
"name": "云盘文件删除",
"description": "媒体库删除strm文件后同步删除云盘资源。",
"version": "1.1",
"version": "1.2",
"icon": "clouddisk.png",
"author": "thsrite",
"level": 1
@@ -322,7 +322,7 @@
"FFmpegThumb": {
"name": "FFmpeg缩略图",
"description": "TheMovieDb没有背景图片时使用FFmpeg截取视频文件缩略图",
"version": "1.0",
"version": "1.2",
"icon": "ffmpeg.png",
"author": "jxxghp",
"level": 1

View File

@@ -31,7 +31,7 @@ class BrushFlow(_PluginBase):
# 插件图标
plugin_icon = "brush.jpg"
# 插件版本
plugin_version = "1.0"
plugin_version = "1.1"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -64,6 +64,7 @@ class BrushFlow(_PluginBase):
_downloader = "qbittorrent"
_disksize = 0
_freeleech = "free"
_hr = "no"
_maxupspeed = 0
_maxdlspeed = 0
_maxdlcount = 0
@@ -96,6 +97,7 @@ class BrushFlow(_PluginBase):
self._downloader = config.get("downloader")
self._disksize = config.get("disksize")
self._freeleech = config.get("freeleech")
self._hr = config.get("hr")
self._maxupspeed = config.get("maxupspeed")
self._maxdlspeed = config.get("maxdlspeed")
self._maxdlcount = config.get("maxdlcount")
@@ -411,6 +413,26 @@ class BrushFlow(_PluginBase):
}
]
},
{
'component': 'VCol',
'props': {
"cols": 12,
"md": 4
},
'content': [
{
'component': 'VSelect',
'props': {
'model': 'hr',
'label': '排除H&R',
'items': [
{'title': '', 'value': 'yes'},
{'title': '', 'value': 'no'},
]
}
}
]
},
{
'component': 'VCol',
'props': {
@@ -732,7 +754,28 @@ class BrushFlow(_PluginBase):
]
}
]
}
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '注意排除H&R并不保证能完全适配所有站点部分站点在列表页不显示H&R标志但实际上是有H&R的请注意核对使用'
}
}
]
}
]
}
]
}
], {
@@ -740,7 +783,8 @@ class BrushFlow(_PluginBase):
"notify": True,
"onlyonce": False,
"clear_task": False,
"freeleech": "free"
"freeleech": "free",
"hr": "yes",
}
def get_page(self) -> List[dict]:
@@ -1215,6 +1259,7 @@ class BrushFlow(_PluginBase):
"downloader": self._downloader,
"disksize": self._disksize,
"freeleech": self._freeleech,
"hr": self._hr,
"maxupspeed": self._maxupspeed,
"maxdlspeed": self._maxdlspeed,
"maxdlcount": self._maxdlcount,
@@ -1284,6 +1329,9 @@ class BrushFlow(_PluginBase):
continue
if self._freeleech == "2xfree" and torrent.uploadvolumefactor != 2:
continue
# H&R
if self._hr == "yes" and torrent.hit_and_run:
continue
# 包含规则
if self._include and not re.search(r"%s" % self._include, torrent.title, re.I):
continue

View File

@@ -20,7 +20,7 @@ class CloudDiskDel(_PluginBase):
# 插件图标
plugin_icon = "clouddisk.png"
# 插件版本
plugin_version = "1.1"
plugin_version = "1.2"
# 插件作者
plugin_author = "thsrite"
# 作者主页
@@ -84,9 +84,13 @@ class CloudDiskDel(_PluginBase):
season_num = event_data.get("season_num")
episode_num = event_data.get("episode_num")
# 不是网盘监控路径,直接排除
cloud_file_flag = False
# 判断删除媒体路径是否与配置的媒体库路径相符,相符则继续删除,不符则跳过
for library_path in list(self._paths.keys()):
if str(media_path).startswith(library_path):
cloud_file_flag = True
# 替换网盘路径
media_path = str(media_path).replace(library_path, self._paths.get(library_path))
logger.info(f"获取到moviepilot本地云盘挂载路径 {media_path}")
@@ -128,63 +132,64 @@ class CloudDiskDel(_PluginBase):
break
# 发送消息
image = 'https://emby.media/notificationicon.png'
media_type = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV
if self._notify:
backrop_image = self.chain.obtain_specific_image(
if cloud_file_flag:
# 发送消息
image = 'https://emby.media/notificationicon.png'
media_type = MediaType.MOVIE if media_type in ["Movie", "MOV"] else MediaType.TV
if self._notify:
backrop_image = self.chain.obtain_specific_image(
mediaid=tmdb_id,
mtype=media_type,
image_type=MediaImageType.Backdrop,
season=season_num,
episode=episode_num
) or image
# 类型
if media_type == MediaType.MOVIE:
msg = f'电影 {media_name} {tmdb_id}'
# 删除电视剧
elif media_type == MediaType.TV and not season_num and not episode_num:
msg = f'剧集 {media_name} {tmdb_id}'
# 删除季 S02
elif media_type == MediaType.TV and season_num and not episode_num:
msg = f'剧集 {media_name} S{season_num} {tmdb_id}'
# 删除剧集S02E02
elif media_type == MediaType.TV and season_num and episode_num:
msg = f'剧集 {media_name} S{season_num}E{episode_num} {tmdb_id}'
else:
msg = media_name
# 发送通知
self.post_message(
mtype=NotificationType.MediaServer,
title="云盘同步删除任务完成",
image=backrop_image,
text=f"{msg}\n"
f"时间 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}"
)
# 读取历史记录
history = self.get_data('history') or []
# 获取poster
poster_image = self.chain.obtain_specific_image(
mediaid=tmdb_id,
mtype=media_type,
image_type=MediaImageType.Backdrop,
season=season_num,
episode=episode_num
image_type=MediaImageType.Poster,
) or image
history.append({
"type": media_type.value,
"title": media_name,
"path": media_path,
"season": season_num if season_num and str(season_num).isdigit() else None,
"episode": episode_num if episode_num and str(episode_num).isdigit() else None,
"image": poster_image,
"del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
})
# 类型
if media_type == MediaType.MOVIE:
msg = f'电影 {media_name} {tmdb_id}'
# 删除电视剧
elif media_type == MediaType.TV and not season_num and not episode_num:
msg = f'剧集 {media_name} {tmdb_id}'
# 删除季 S02
elif media_type == MediaType.TV and season_num and not episode_num:
msg = f'剧集 {media_name} S{season_num} {tmdb_id}'
# 删除剧集S02E02
elif media_type == MediaType.TV and season_num and episode_num:
msg = f'剧集 {media_name} S{season_num}E{episode_num} {tmdb_id}'
else:
msg = media_name
# 发送通知
self.post_message(
mtype=NotificationType.MediaServer,
title="云盘同步删除任务完成",
image=backrop_image,
text=f"{msg}\n"
f"时间 {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time()))}"
)
# 读取历史记录
history = self.get_data('history') or []
# 获取poster
poster_image = self.chain.obtain_specific_image(
mediaid=tmdb_id,
mtype=media_type,
image_type=MediaImageType.Poster,
) or image
history.append({
"type": media_type.value,
"title": media_name,
"path": media_path,
"season": season_num if season_num and str(season_num).isdigit() else None,
"episode": episode_num if episode_num and str(episode_num).isdigit() else None,
"image": poster_image,
"del_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.time()))
})
# 保存历史
self.save_data("history", history)
# 保存历史
self.save_data("history", history)
def get_state(self) -> bool:
return self._enabled

View File

@@ -27,7 +27,7 @@ class DoubanRank(_PluginBase):
# 插件图标
plugin_icon = "movie.jpg"
# 插件版本
plugin_version = "1.2"
plugin_version = "1.4"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -48,7 +48,7 @@ class DoubanRank(_PluginBase):
_douban_address = {
'movie-ustop': 'https://rsshub.app/douban/movie/ustop',
'movie-weekly': 'https://rsshub.app/douban/movie/weekly',
'movie-real-time': 'https://rsshub.app/douban/movie/weekly/subject_real_time_hotest',
'movie-real-time': 'https://rsshub.app/douban/movie/weekly/movie_real_time_hotest',
'show-domestic': 'https://rsshub.app/douban/movie/weekly/show_domestic',
'movie-hot-gaia': 'https://rsshub.app/douban/movie/weekly/movie_hot_gaia',
'tv-hot': 'https://rsshub.app/douban/movie/weekly/tv_hot',
@@ -101,10 +101,8 @@ class DoubanRank(_PluginBase):
logger.error(f"豆瓣榜单订阅服务启动失败,错误信息:{str(e)}")
self.systemmessage.put(f"豆瓣榜单订阅服务启动失败,错误信息:{str(e)}")
else:
self._scheduler.add_job(func=self.__refresh_rss, trigger='date',
run_date=datetime.datetime.now(
tz=pytz.timezone(settings.TZ)) + datetime.timedelta(seconds=3)
)
self._scheduler.add_job(func=self.__refresh_rss, trigger=CronTrigger.from_crontab("0 8 * * *"),
name="豆瓣榜单订阅")
logger.info("豆瓣榜单订阅服务启动,周期:每天 08:00")
if self._onlyonce:
@@ -484,12 +482,14 @@ class DoubanRank(_PluginBase):
title = rss_info.get('title')
douban_id = rss_info.get('doubanid')
year = rss_info.get('year')
unique_flag = f"doubanrank: {title} (DB:{douban_id})"
# 检查是否已处理过
if unique_flag in [h.get("unique") for h in history]:
continue
# 元数据
meta = MetaInfo(title)
meta.year = year
# 识别媒体信息
if douban_id:
# 识别豆瓣信息
@@ -565,25 +565,36 @@ class DoubanRank(_PluginBase):
items = rootNode.getElementsByTagName("item")
for item in items:
try:
rss_info = {}
# 标题
title = DomUtils.tag_value(item, "title", default="")
# 链接
link = DomUtils.tag_value(item, "link", default="")
# 年份
description = DomUtils.tag_value(item, "description", default="")
if not title and not link:
logger.warn(f"条目标题和链接均为空,无法处理")
continue
rss_info['title'] = title
rss_info['link'] = link
doubanid = re.findall(r"/(\d+)/", link)
if doubanid:
doubanid = doubanid[0]
if doubanid and not str(doubanid).isdigit():
logger.warn(f"解析的豆瓣ID格式不正确{doubanid}")
continue
rss_info['doubanid'] = doubanid
# 匹配4位独立数字1900-2099年
year = re.findall(r"\b(19\d{2}|20\d{2})\b", description)
if year:
rss_info['year'] = year[0]
# 返回对象
ret_array.append({
'title': title,
'link': link,
'doubanid': doubanid
})
ret_array.append(rss_info)
except Exception as e1:
logger.error("解析RSS条目失败" + str(e1))
continue

View File

@@ -6,6 +6,7 @@ from typing import Optional, Any, List, Dict, Tuple
import pytz
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.schemas.types import MediaType
from app.chain.download import DownloadChain
from app.chain.search import SearchChain
@@ -30,7 +31,7 @@ class DoubanSync(_PluginBase):
# 插件图标
plugin_icon = "douban.png"
# 插件版本
plugin_version = "1.1"
plugin_version = "1.2"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -475,6 +476,9 @@ class DoubanSync(_PluginBase):
continue
# 识别媒体信息
meta = MetaInfo(title=title)
douban_info = self.chain.douban_info(doubanid=douban_id)
meta.year = douban_info.get("year")
meta.type = MediaType.MOVIE if douban_info.get("type") == "movie" else MediaType.TV
mediainfo = self.chain.recognize_media(meta=meta, doubanid=douban_id)
if not mediainfo:
logger.warn(f'未识别到媒体信息,标题:{title}豆瓣ID{douban_id}')

View File

@@ -1,3 +1,4 @@
import threading
from datetime import datetime, timedelta
from pathlib import Path
from threading import Event as ThreadEvent
@@ -16,6 +17,8 @@ from app.schemas import TransferInfo
from app.schemas.types import EventType
from app.utils.system import SystemUtils
ffmpeg_lock = threading.Lock()
class FFmpegThumb(_PluginBase):
# 插件名称
@@ -25,7 +28,7 @@ class FFmpegThumb(_PluginBase):
# 插件图标
plugin_icon = "ffmpeg.png"
# 插件版本
plugin_version = "1.0"
plugin_version = "1.2"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -328,18 +331,18 @@ class FFmpegThumb(_PluginBase):
"""
处理一个文件
"""
try:
thumb_path = file_path.with_name(file_path.stem + "-thumb.jpg")
if thumb_path.exists():
logger.info(f"缩略图已存在:{thumb_path}")
return
if FfmpegHelper.get_thumb(video_path=str(file_path),
image_path=str(thumb_path), frames=self._timeline):
logger.info(f"{file_path} 缩略图已生成:{thumb_path}")
else:
logger.warn(f"{file_path} 缩略图生成失败!")
except Exception as err:
logger.error(f"FFmpeg处理文件 {file_path} 时发生错误:{str(err)}")
# 单线程处理
with ffmpeg_lock:
try:
thumb_path = file_path.with_name(file_path.stem + "-thumb.jpg")
if thumb_path.exists():
logger.info(f"缩略图已存在:{thumb_path}")
return
if FfmpegHelper.get_thumb(video_path=str(file_path),
image_path=str(thumb_path), frames=self._timeline):
logger.info(f"{file_path} 缩略图生成{thumb_path}")
except Exception as err:
logger.error(f"FFmpeg处理文件 {file_path} 时发生错误:{str(err)}")
def stop_service(self):
"""

View File

@@ -8,8 +8,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from app.core.config import settings
from app.core.meta import MetaBase
from app.core.metainfo import MetaInfo
from app.core.metainfo import MetaInfoPath
from app.db.transferhistory_oper import TransferHistoryOper
from app.helper.nfo import NfoReader
from app.log import logger
@@ -26,7 +25,7 @@ class LibraryScraper(_PluginBase):
# 插件图标
plugin_icon = "scraper.png"
# 插件版本
plugin_version = "1.1"
plugin_version = "1.2"
# 插件作者
plugin_author = "jxxghp"
# 作者主页
@@ -214,7 +213,7 @@ class LibraryScraper(_PluginBase):
'model': 'scraper_paths',
'label': '削刮路径',
'rows': 5,
'placeholder': '每一行一个目录,需配置到媒体文件的上级目录,即开了二级分类时需要配置到二级分类目录'
'placeholder': '每一行一个目录'
}
}
]
@@ -243,27 +242,6 @@ class LibraryScraper(_PluginBase):
}
]
},
{
'component': 'VRow',
'content': [
{
'component': 'VCol',
'props': {
'cols': 12,
},
'content': [
{
'component': 'VAlert',
'props': {
'type': 'info',
'variant': 'tonal',
'text': '刮削路径要配置到二级分类路径如果配置了LIBRARY_CATEGORY=true开启插件后默认会实时处理增量整理的媒体文件需要处理存量媒体文件时才需开启定时。'
}
}
]
}
]
},
{
'component': 'VRow',
'content': [
@@ -325,8 +303,9 @@ class LibraryScraper(_PluginBase):
logger.warning(f"媒体库刮削路径不存在:{path}")
continue
logger.info(f"开始刮削媒体库:{path} {mtype} ...")
# 遍历一层文件
for sub_path in scraper_path.iterdir():
# 遍历所有文件
files = SystemUtils.list_files(scraper_path, settings.RMT_MEDIAEXT)
for file_path in files:
if self._event.is_set():
logger.info(f"媒体库刮削服务停止")
return
@@ -334,115 +313,71 @@ class LibraryScraper(_PluginBase):
exclude_flag = False
for exclude_path in exclude_paths:
try:
if sub_path.is_relative_to(Path(exclude_path)):
if file_path.is_relative_to(Path(exclude_path)):
exclude_flag = True
break
except Exception as err:
print(str(err))
if exclude_flag:
logger.debug(f"{sub_path} 在排除目录中,跳过 ...")
logger.debug(f"{file_path} 在排除目录中,跳过 ...")
continue
# 开始刮削目录
if sub_path.is_dir():
# 判断目录是不是媒体目录
dir_meta = MetaInfo(sub_path.name)
if not dir_meta.name or not dir_meta.year:
logger.warn(f"{sub_path} 可能不是媒体目录,请检查刮削目录配置,跳过 ...")
continue
logger.info(f"开始刮削目录:{sub_path} ...")
self.__scrape_dir(path=sub_path, dir_meta=dir_meta, mtype=mtype)
logger.info(f"目录 {sub_path} 刮削完成")
# 开始刮削文件
self.__scrape_file(file=file_path, mtype=mtype)
logger.info(f"媒体库 {path} 刮削完成")
def __scrape_dir(self, path: Path, dir_meta: MetaBase, mtype: MediaType = None):
def __scrape_file(self, file: Path, mtype: MediaType = None):
"""
削刮一个目录,该目录必须是媒体文件目录
"""
# 识别元数据
meta_info = MetaInfoPath(file)
# 强制指定类型
if mtype:
meta_info.type = mtype
# 媒体信息
mediainfo = None
# 是否刮削
force_nfo = self._mode in ["force_all", "force_nfo"]
force_img = self._mode in ["force_all", "force_image"]
# 查找目录下所有的文件
files = SystemUtils.list_files(path, settings.RMT_MEDIAEXT)
for file in files:
if self._event.is_set():
logger.info(f"媒体库刮削服务停止")
return
# 优先读取本地nfo文件
tmdbid = None
if meta_info.type == MediaType.MOVIE:
# 电影
movie_nfo = file.parent / "movie.nfo"
if movie_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(movie_nfo)
file_nfo = file.with_suffix(".nfo")
if not tmdbid and file_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(file_nfo)
else:
# 电视剧
tv_nfo = file.parent.parent / "tvshow.nfo"
if tv_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(tv_nfo)
if tmdbid:
# 按TMDBID识别
logger.info(f"读取到本地nfo文件的tmdbid{tmdbid}")
mediainfo = self.chain.recognize_media(tmdbid=tmdbid, mtype=meta_info.type)
else:
# 按名称识别
mediainfo = self.chain.recognize_media(meta=meta_info)
if not mediainfo:
logger.warn(f"未识别到媒体信息:{file}")
return
# 识别元数据
meta_info = MetaInfo(file.stem)
# 合并
meta_info.merge(dir_meta)
# 强制指定类型
if mtype:
meta_info.type = mtype
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
if not settings.SCRAP_FOLLOW_TMDB:
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id,
mtype=mediainfo.type.value)
if transfer_history:
mediainfo.title = transfer_history.title
# 是否刮削
scrap_metadata = settings.SCRAP_METADATA
# 没有媒体信息或者名字出现变化时,需要重新识别
if not mediainfo \
or meta_info.name != dir_meta.name:
# 优先读取本地nfo文件
tmdbid = None
if meta_info.type == MediaType.MOVIE:
# 电影
movie_nfo = file.parent / "movie.nfo"
if movie_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(movie_nfo)
file_nfo = file.with_suffix(".nfo")
if not tmdbid and file_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(file_nfo)
else:
# 电视剧
tv_nfo = file.parent.parent / "tvshow.nfo"
if tv_nfo.exists():
tmdbid = self.__get_tmdbid_from_nfo(tv_nfo)
if tmdbid:
# 按TMDBID识别
logger.info(f"读取到本地nfo文件的tmdbid{tmdbid}")
mediainfo = self.chain.recognize_media(tmdbid=tmdbid, mtype=meta_info.type)
else:
# 按名称识别
mediainfo = self.chain.recognize_media(meta=meta_info)
if not mediainfo:
logger.warn(f"未识别到媒体信息:{file}")
continue
# 如果未开启新增已入库媒体是否跟随TMDB信息变化则根据tmdbid查询之前的title
if not settings.SCRAP_FOLLOW_TMDB:
transfer_history = self.transferhis.get_by_type_tmdbid(tmdbid=mediainfo.tmdb_id,
mtype=mediainfo.type.value)
if transfer_history:
mediainfo.title = transfer_history.title
# 覆盖模式时提前删除nfo
if self._mode in ["force_all", "force_nfo"]:
scrap_metadata = True
nfo_files = SystemUtils.list_files(path, [".nfo"])
for nfo_file in nfo_files:
try:
logger.warn(f"删除nfo文件{nfo_file}")
nfo_file.unlink()
except Exception as err:
print(str(err))
# 覆盖模式时,提前删除图片文件
if self._mode in ["force_all", "force_image"]:
scrap_metadata = True
image_files = SystemUtils.list_files(path, [".jpg", ".png"])
for image_file in image_files:
if ".actors" in str(image_file):
continue
try:
logger.warn(f"删除图片文件:{image_file}")
image_file.unlink()
except Exception as err:
print(str(err))
# 刮削单个文件
if scrap_metadata:
self.chain.scrape_metadata(path=file, mediainfo=mediainfo, transfer_type=settings.TRANSFER_TYPE)
# 刮削
self.chain.scrape_metadata(path=file,
mediainfo=mediainfo,
transfer_type=settings.TRANSFER_TYPE,
force_nfo=force_nfo,
force_img=force_img)
@staticmethod
def __get_tmdbid_from_nfo(file_path: Path):