mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 03:48:03 +08:00
99 lines
3.3 KiB
Python
99 lines
3.3 KiB
Python
"""
|
|
图片域名归一化与站点白名单快照,不缓存 DNS 安全判定。
|
|
"""
|
|
|
|
import asyncio
|
|
import threading
|
|
from time import monotonic
|
|
from typing import Optional
|
|
from urllib.parse import urlsplit
|
|
from weakref import WeakValueDictionary
|
|
|
|
from app.application.site.query import SiteQueryService
|
|
|
|
|
|
def normalize_image_domain(address: Optional[str]) -> Optional[str]:
|
|
"""
|
|
提取 HTTP(S) 主机及端口,保留协议以兼容 URL 白名单解析。
|
|
"""
|
|
address = (address or "").strip()
|
|
if not address:
|
|
return None
|
|
try:
|
|
parsed = urlsplit(address if "://" in address else f"https://{address}")
|
|
if parsed.scheme in {"http", "https"} and parsed.hostname and parsed.username is None:
|
|
return f"{parsed.scheme}://{parsed.netloc.lower()}"
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
class SiteImageDomainCache:
|
|
"""
|
|
缓存站点图片域名,同一事件循环 合并查询,写入失效后禁止旧查询回填。
|
|
"""
|
|
|
|
def __init__(self, ttl: float = 60) -> None:
|
|
"""
|
|
设置短期兜底有效期;锁只保护内存,不跨 await 持有线程锁。
|
|
"""
|
|
self._ttl = ttl
|
|
self._lock = threading.Lock()
|
|
self._inflight: WeakValueDictionary[asyncio.AbstractEventLoop, asyncio.Lock] = WeakValueDictionary()
|
|
self._generation = 0
|
|
self._source: Optional[SiteQueryService] = None
|
|
self._domains: frozenset[str] = frozenset()
|
|
self._expires = 0.0
|
|
|
|
def invalidate(self) -> None:
|
|
"""
|
|
提交后立即撤销旧域名;递增代际防止在途查询恢复已删除的授权。
|
|
"""
|
|
with self._lock:
|
|
self._generation += 1
|
|
self._expires = 0.0
|
|
self._source = None
|
|
self._domains = frozenset()
|
|
|
|
async def get(self, source: SiteQueryService) -> frozenset[str]:
|
|
"""
|
|
返回不可变快照,命中时不查库;配置服务更换后自动重建。
|
|
"""
|
|
loop = asyncio.get_running_loop()
|
|
with self._lock:
|
|
if self._source is source and monotonic() < self._expires:
|
|
return self._domains
|
|
inflight = self._inflight.get(loop)
|
|
if inflight is None:
|
|
inflight = asyncio.Lock()
|
|
self._inflight[loop] = inflight
|
|
async with inflight:
|
|
return await self._refresh(source)
|
|
|
|
async def _refresh(self, source: SiteQueryService) -> frozenset[str]:
|
|
"""
|
|
锁内复查并加载,失效与查询交错时丢弃旧结果后重新读取。
|
|
"""
|
|
while True:
|
|
with self._lock:
|
|
if self._source is source and monotonic() < self._expires:
|
|
return self._domains
|
|
generation = self._generation
|
|
sites = await source.list()
|
|
domains = frozenset(
|
|
domain
|
|
for site in sites
|
|
for address in (site.domain, site.url)
|
|
if (domain := normalize_image_domain(address))
|
|
)
|
|
with self._lock:
|
|
if generation != self._generation:
|
|
continue
|
|
self._source = source
|
|
self._domains = domains
|
|
self._expires = monotonic() + self._ttl
|
|
return domains
|
|
|
|
|
|
site_image_domains = SiteImageDomainCache()
|