Files
MoviePilot/app/db/oper/site.py
T

423 lines
14 KiB
Python

from datetime import datetime
from typing import Any, List, Mapping, Tuple, Optional
from sqlalchemy import delete as sqlalchemy_delete
from app.db.base import DbOper
from app.db.models.site import Site
from app.db.models.siteicon import SiteIcon
from app.db.models.sitestatistic import SiteStatistic
from app.db.models.siteuserdata import SiteUserData
class SiteOper(DbOper):
"""
站点管理
"""
def add(self, **kwargs) -> Tuple[bool, str]:
"""
新增站点
"""
site = Site(**kwargs)
if not site.get_by_domain(self._db, kwargs.get("domain")):
self._stage_create(site)
return True, "新增站点成功"
return False, "站点已存在"
def get(self, sid: int) -> Optional[Site]:
"""
查询单个站点
"""
return Site.get(self._db, sid)
async def async_get(self, sid: int) -> Optional[Site]:
"""
异步查询单个站点
"""
return await Site.async_get(self._db, sid)
async def get_by_id(self, site_id: int) -> Optional[Site]:
"""读取站点写用例需要的目标站点。"""
return await self.async_get(site_id)
async def stage_create(self, payload: Mapping[str, Any]) -> None:
"""暂存新增站点,不由仓储自行提交。"""
values = dict(payload)
values.pop("id", None)
self._db.add(Site(**values))
async def stage_update(
self,
site_id: int,
payload: Mapping[str, Any],
) -> bool:
"""暂存站点字段更新,不由模型装饰器提前提交。"""
site = await self.async_get(site_id)
if not site:
return False
for key, value in payload.items():
if key != "id":
setattr(site, key, value)
return True
async def stage_delete(self, site_id: int) -> None:
"""暂存站点删除,由请求级 UnitOfWork 统一提交。"""
await self._db.execute(
sqlalchemy_delete(Site).where(Site.id == site_id)
)
async def stage_priorities(self, priorities: list[dict]) -> None:
"""暂存批量优先级更新,避免逐行独立提交。"""
for priority in priorities:
site_id = priority.get("id")
site = await self.async_get(site_id) if site_id else None
if site:
site.pri = priority.get("pri")
def list(self) -> List[Site]:
"""
获取站点列表
"""
return Site.list(self._db)
async def async_list(self) -> List[Site]:
"""
异步获取站点列表
"""
return await Site.async_list(self._db)
async def async_list_order_by_pri(self) -> List[Site]:
"""异步按优先级获取站点,供站点查询应用服务使用。"""
return await Site.async_list_order_by_pri(self._db)
def list_order_by_pri(self) -> List[Site]:
"""
获取站点列表
"""
return Site.list_order_by_pri(self._db)
def list_active(self) -> List[Site]:
"""
按状态获取站点列表
"""
return Site.get_actives(self._db)
async def async_list_active(self) -> List[Site]:
"""
异步按状态获取站点列表
"""
return await Site.async_get_actives(self._db)
def delete(self, sid: int):
"""
删除站点
"""
self._stage_delete(Site, sid)
def reset(self) -> None:
"""清空站点表;兼容入口的事务由组合根统一持有。"""
self._execute_sync_write(Site.reset)
async def stage_reset(self) -> None:
"""暂存清空站点表,由应用事务统一提交。"""
await self._db.execute(sqlalchemy_delete(Site))
def update(self, sid: int, payload: dict) -> Optional[Site]:
"""
更新站点
"""
site = Site.get(self._db, sid)
if not site:
return None
self._stage_update(site, payload)
return site
async def async_update(self, sid: int, payload: dict) -> Optional[Site]:
"""
异步更新站点。
"""
site = await self.async_get(sid)
if site:
await self._stage_async_update(site, payload)
return site
def get_by_domain(self, domain: str) -> Optional[Site]:
"""
按域名获取站点
"""
return Site.get_by_domain(self._db, domain)
async def async_get_by_domain(self, domain: str) -> Optional[Site]:
"""
异步按域名获取站点
"""
return await Site.async_get_by_domain(self._db, domain)
async def async_get_by_name(self, name: str) -> Optional[Site]:
"""
异步按名称获取站点
"""
return await Site.async_get_by_name(self._db, name)
def get_domains_by_ids(self, ids: List[int]) -> List[Optional[str]]:
"""
按ID获取站点域名
"""
return Site.get_domains_by_ids(self._db, ids)
def exists(self, domain: str) -> bool:
"""
判断站点是否存在
"""
return Site.get_by_domain(self._db, domain) is not None
def update_cookie(self, domain: str, cookies: str) -> Tuple[bool, str]:
"""
更新站点Cookie
"""
site = Site.get_by_domain(self._db, domain)
if not site:
return False, "站点不存在"
self._stage_update(site, {
"cookie": cookies
})
return True, "更新站点Cookie成功"
def update_rss(self, domain: str, rss: str) -> Tuple[bool, str]:
"""
更新站点rss
"""
site = Site.get_by_domain(self._db, domain)
if not site:
return False, "站点不存在"
self._stage_update(site, {
"rss": rss
})
return True, "更新站点RSS地址成功"
def update_userdata(self, domain: str, name: str, payload: dict) -> Tuple[bool, str]:
"""
更新站点用户数据
"""
# 当前系统日期
current_day = datetime.now().strftime('%Y-%m-%d')
current_time = datetime.now().strftime('%H:%M:%S')
payload.update({
"domain": domain,
"name": name,
"updated_day": current_day,
"updated_time": current_time,
"err_msg": payload.get("err_msg") or ""
})
# 按站点+天判断是否存在数据
siteuserdatas = SiteUserData.get_by_domain(self._db, domain=domain, workdate=current_day)
if siteuserdatas:
# 存在则更新
if not payload.get("err_msg"):
self._stage_update(siteuserdatas[0], payload)
else:
# 不存在则插入
self._stage_create(SiteUserData(**payload))
return True, "更新站点用户数据成功"
def get_userdata(self) -> List[SiteUserData]:
"""
获取站点用户数据
"""
return SiteUserData.list(self._db)
def get_userdata_by_domain(self, domain: str, workdate: Optional[str] = None) -> List[SiteUserData]:
"""
获取站点用户数据
"""
return SiteUserData.get_by_domain(self._db, domain=domain, workdate=workdate)
async def async_get_userdata_by_domain(
self, domain: str, workdate: Optional[str] = None
) -> List[SiteUserData]:
"""
异步获取站点用户数据。
"""
return await SiteUserData.async_get_by_domain(
self._db, domain=domain, workdate=workdate
)
async def async_get_userdata_latest(self) -> List[SiteUserData]:
"""异步获取各站点最新用户数据。"""
return await SiteUserData.async_get_latest(self._db)
async def async_get_icon_by_domain(self, domain: str) -> Optional[SiteIcon]:
"""异步按域名获取站点图标。"""
return await SiteIcon.async_get_by_domain(self._db, domain)
async def async_get_statistic_by_domain(
self,
domain: str,
) -> Optional[SiteStatistic]:
"""异步按域名获取站点统计。"""
return await SiteStatistic.async_get_by_domain(self._db, domain)
async def async_list_statistics(self) -> List[SiteStatistic]:
"""异步获取所有站点统计。"""
return await SiteStatistic.async_list(self._db)
def get_userdata_by_date(self, date: str) -> List[SiteUserData]:
"""
获取站点用户数据
"""
return SiteUserData.get_by_date(self._db, date)
def get_userdata_latest(self) -> List[SiteUserData]:
"""
获取站点最新数据
"""
return SiteUserData.get_latest(self._db)
def get_icon_by_domain(self, domain: str) -> Optional[SiteIcon]:
"""
按域名获取站点图标
"""
return SiteIcon.get_by_domain(self._db, domain)
def update_icon(self, name: str, domain: str, icon_url: str, icon_base64: str) -> bool:
"""
更新站点图标
"""
icon_base64 = f"data:image/ico;base64,{icon_base64}" if icon_base64 else ""
siteicon = self.get_icon_by_domain(domain)
if not siteicon:
self._stage_create(
SiteIcon(name=name, domain=domain, url=icon_url, base64=icon_base64)
)
elif icon_base64:
self._stage_update(siteicon, {
"url": icon_url,
"base64": icon_base64
})
return True
def success(self, domain: str, seconds: Optional[int] = None):
"""
站点访问成功
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = SiteStatistic.get_by_domain(self._db, domain)
if sta:
# 使用深复制确保 note 是全新的字典对象
note = dict(sta.note) if sta.note else {}
avg_seconds = None
if seconds is not None:
note[lst_date] = seconds or 1
avg_times = len(note.keys())
if avg_times > 10:
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
avg_seconds = sum([v for v in note.values()]) // avg_times
self._stage_update(sta, {
"success": sta.success + 1,
"seconds": avg_seconds or sta.seconds,
"lst_state": 0,
"lst_mod_date": lst_date,
"note": note
})
else:
note = {}
if seconds is not None:
note = {
lst_date: seconds or 1
}
self._stage_create(SiteStatistic(
domain=domain,
success=1,
fail=0,
seconds=seconds or 1,
lst_state=0,
lst_mod_date=lst_date,
note=note
))
def fail(self, domain: str):
"""
站点访问失败
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = SiteStatistic.get_by_domain(self._db, domain)
if sta:
self._stage_update(sta, {
"fail": sta.fail + 1,
"lst_state": 1,
"lst_mod_date": lst_date
})
else:
self._stage_create(SiteStatistic(
domain=domain,
success=0,
fail=1,
lst_state=1,
lst_mod_date=lst_date
))
async def async_success(self, domain: str, seconds: Optional[int] = None):
"""
异步站点访问成功
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
if sta:
# 使用深复制确保 note 是全新的字典对象
note = dict(sta.note) if sta.note else {}
avg_seconds = None
if seconds is not None:
note[lst_date] = seconds or 1
avg_times = len(note.keys())
if avg_times > 10:
note = dict(sorted(note.items(), key=lambda x: x[0], reverse=True)[:10])
avg_seconds = sum([v for v in note.values()]) // avg_times
await self._stage_async_update(sta, {
"success": sta.success + 1,
"seconds": avg_seconds or sta.seconds,
"lst_state": 0,
"lst_mod_date": lst_date,
"note": note
})
else:
note = {}
if seconds is not None:
note = {
lst_date: seconds or 1
}
await self._stage_async_create(SiteStatistic(
domain=domain,
success=1,
fail=0,
seconds=seconds or 1,
lst_state=0,
lst_mod_date=lst_date,
note=note
))
async def async_fail(self, domain: str):
"""
异步站点访问失败
"""
lst_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
sta = await SiteStatistic.async_get_by_domain(self._db, domain)
if sta:
await self._stage_async_update(sta, {
"fail": sta.fail + 1,
"lst_state": 1,
"lst_mod_date": lst_date
})
else:
await self._stage_async_create(SiteStatistic(
domain=domain,
success=0,
fail=1,
lst_state=1,
lst_mod_date=lst_date
))