add dashboard apis
This commit is contained in:
parent
90002b6223
commit
15bb043fe8
@ -1,7 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
||||||
media, douban, search, plugin, tmdb, history, system, download
|
media, douban, search, plugin, tmdb, history, system, download, dashboard
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(login.router, tags=["login"])
|
api_router.include_router(login.router, tags=["login"])
|
||||||
@ -18,3 +18,4 @@ api_router.include_router(history.router, prefix="/history", tags=["history"])
|
|||||||
api_router.include_router(system.router, prefix="/system", tags=["system"])
|
api_router.include_router(system.router, prefix="/system", tags=["system"])
|
||||||
api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"])
|
api_router.include_router(plugin.router, prefix="/plugin", tags=["plugin"])
|
||||||
api_router.include_router(download.router, prefix="/download", tags=["download"])
|
api_router.include_router(download.router, prefix="/download", tags=["download"])
|
||||||
|
api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"])
|
||||||
|
46
app/api/endpoints/dashboard.py
Normal file
46
app/api/endpoints/dashboard.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.chain.dashboard import DashboardChain
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.security import verify_token
|
||||||
|
from app.utils.system import SystemUtils
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/statistic", summary="媒体数量统计", response_model=schemas.Statistic)
|
||||||
|
def statistic(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
查询媒体数量统计信息
|
||||||
|
"""
|
||||||
|
media_statistic = DashboardChain().media_statistic()
|
||||||
|
return schemas.Statistic(
|
||||||
|
movie_count=media_statistic.movie_count,
|
||||||
|
tv_count=media_statistic.tv_count,
|
||||||
|
episode_count=media_statistic.episode_count,
|
||||||
|
user_count=media_statistic.user_count
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/storage", summary="存储空间", response_model=schemas.Storage)
|
||||||
|
def storage(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
查询存储空间信息
|
||||||
|
"""
|
||||||
|
total_storage, used_storage = SystemUtils.space_usage(Path(settings.LIBRARY_PATH))
|
||||||
|
return schemas.Storage(
|
||||||
|
total_storage=total_storage,
|
||||||
|
used_storage=used_storage
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/processes", summary="进程信息", response_model=List[schemas.ProcessInfo])
|
||||||
|
def processes(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
进程信息
|
||||||
|
"""
|
||||||
|
return SystemUtils.processes()
|
13
app/chain/dashboard.py
Normal file
13
app/chain/dashboard.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from app import schemas
|
||||||
|
from app.chain import ChainBase
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardChain(ChainBase):
|
||||||
|
"""
|
||||||
|
各类仪表板统计处理链
|
||||||
|
"""
|
||||||
|
def media_statistic(self) -> schemas.Statistic:
|
||||||
|
"""
|
||||||
|
媒体数量统计
|
||||||
|
"""
|
||||||
|
return self.run_module("media_statistic")
|
@ -1,6 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union, Any
|
from typing import Optional, Tuple, Union, Any
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
@ -83,3 +84,16 @@ class EmbyModule(_ModuleBase):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
return self.emby.refresh_library_by_items(items)
|
return self.emby.refresh_library_by_items(items)
|
||||||
|
|
||||||
|
def media_statistic(self) -> schemas.Statistic:
|
||||||
|
"""
|
||||||
|
媒体数量统计
|
||||||
|
"""
|
||||||
|
media_statistic = self.emby.get_medias_count()
|
||||||
|
user_count = self.emby.get_user_count()
|
||||||
|
return schemas.Statistic(
|
||||||
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
|
user_count=user_count or 0
|
||||||
|
)
|
||||||
|
@ -2,6 +2,7 @@ import json
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union, Any
|
from typing import Optional, Tuple, Union, Any
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
@ -75,3 +76,16 @@ class JellyfinModule(_ModuleBase):
|
|||||||
:return: 成功或失败
|
:return: 成功或失败
|
||||||
"""
|
"""
|
||||||
return self.jellyfin.refresh_root_library()
|
return self.jellyfin.refresh_root_library()
|
||||||
|
|
||||||
|
def media_statistic(self) -> schemas.Statistic:
|
||||||
|
"""
|
||||||
|
媒体数量统计
|
||||||
|
"""
|
||||||
|
media_statistic = self.jellyfin.get_medias_count()
|
||||||
|
user_count = self.jellyfin.get_user_count()
|
||||||
|
return schemas.Statistic(
|
||||||
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
|
user_count=user_count or 0
|
||||||
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Tuple, Union, Any
|
from typing import Optional, Tuple, Union, Any
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.modules import _ModuleBase
|
from app.modules import _ModuleBase
|
||||||
@ -73,3 +74,15 @@ class PlexModule(_ModuleBase):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
return self.plex.refresh_library_by_items(items)
|
return self.plex.refresh_library_by_items(items)
|
||||||
|
|
||||||
|
def media_statistic(self) -> schemas.Statistic:
|
||||||
|
"""
|
||||||
|
媒体数量统计
|
||||||
|
"""
|
||||||
|
media_statistic = self.plex.get_medias_count()
|
||||||
|
return schemas.Statistic(
|
||||||
|
movie_count=media_statistic.get("MovieCount") or 0,
|
||||||
|
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||||
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
|
user_count=1
|
||||||
|
)
|
||||||
|
@ -7,3 +7,4 @@ from .context import *
|
|||||||
from .servarr import *
|
from .servarr import *
|
||||||
from .plugin import *
|
from .plugin import *
|
||||||
from .history import *
|
from .history import *
|
||||||
|
from .dashboard import *
|
||||||
|
38
app/schemas/dashboard.py
Normal file
38
app/schemas/dashboard.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Statistic(BaseModel):
|
||||||
|
# 电影
|
||||||
|
movie_count: Optional[int] = 0
|
||||||
|
# 电视剧数量
|
||||||
|
tv_count: Optional[int] = 0
|
||||||
|
# 集数量
|
||||||
|
episode_count: Optional[int] = 0
|
||||||
|
# 用户数量
|
||||||
|
user_count: Optional[int] = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Storage(BaseModel):
|
||||||
|
# 总存储空间
|
||||||
|
total_storage: Optional[float] = 0
|
||||||
|
# 已使用空间
|
||||||
|
used_storage: Optional[float] = 0
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessInfo(BaseModel):
|
||||||
|
# 进程ID
|
||||||
|
pid: Optional[int] = 0
|
||||||
|
# 进程名称
|
||||||
|
name: Optional[str] = None
|
||||||
|
# 进程状态
|
||||||
|
status: Optional[str] = None
|
||||||
|
# 进程占用CPU
|
||||||
|
cpu: Optional[float] = 0.0
|
||||||
|
# 进程占用内存 MB
|
||||||
|
memory: Optional[float] = 0.0
|
||||||
|
# 进程创建时间
|
||||||
|
create_time: Optional[float] = 0.0
|
||||||
|
# 进程运行时间 秒
|
||||||
|
run_time: Optional[float] = 0.0
|
@ -1,9 +1,12 @@
|
|||||||
|
import datetime
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List, Union, Tuple
|
||||||
|
import psutil
|
||||||
|
from app import schemas
|
||||||
|
|
||||||
|
|
||||||
class SystemUtils:
|
class SystemUtils:
|
||||||
@ -39,7 +42,7 @@ class SystemUtils:
|
|||||||
return True if platform.system() == 'Darwin' else False
|
return True if platform.system() == 'Darwin' else False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy(src: Path, dest: Path):
|
def copy(src: Path, dest: Path) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
复制
|
复制
|
||||||
"""
|
"""
|
||||||
@ -51,7 +54,7 @@ class SystemUtils:
|
|||||||
return -1, str(err)
|
return -1, str(err)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def move(src: Path, dest: Path):
|
def move(src: Path, dest: Path) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
移动
|
移动
|
||||||
"""
|
"""
|
||||||
@ -63,7 +66,7 @@ class SystemUtils:
|
|||||||
return -1, str(err)
|
return -1, str(err)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def link(src: Path, dest: Path):
|
def link(src: Path, dest: Path) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
硬链接
|
硬链接
|
||||||
"""
|
"""
|
||||||
@ -75,7 +78,7 @@ class SystemUtils:
|
|||||||
return -1, str(err)
|
return -1, str(err)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def softlink(src: Path, dest: Path):
|
def softlink(src: Path, dest: Path) -> Tuple[int, str]:
|
||||||
"""
|
"""
|
||||||
软链接
|
软链接
|
||||||
"""
|
"""
|
||||||
@ -105,7 +108,7 @@ class SystemUtils:
|
|||||||
return files
|
return files
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_directory_size(path: Path):
|
def get_directory_size(path: Path) -> float:
|
||||||
"""
|
"""
|
||||||
计算目录的大小
|
计算目录的大小
|
||||||
|
|
||||||
@ -125,3 +128,74 @@ class SystemUtils:
|
|||||||
total_size += path.stat().st_size
|
total_size += path.stat().st_size
|
||||||
|
|
||||||
return total_size
|
return total_size
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def space_usage(dir_list: Union[Path, List[Path]]) -> Tuple[float, float]:
|
||||||
|
"""
|
||||||
|
计算多个目录的总可用空间/剩余空间(单位:Byte),并去除重复磁盘
|
||||||
|
"""
|
||||||
|
if not dir_list:
|
||||||
|
return 0.0, 0.0
|
||||||
|
if not isinstance(dir_list, list):
|
||||||
|
dir_list = [dir_list]
|
||||||
|
# 存储不重复的磁盘
|
||||||
|
disk_set = set()
|
||||||
|
# 存储总剩余空间
|
||||||
|
total_free_space = 0.0
|
||||||
|
# 存储总空间
|
||||||
|
total_space = 0.0
|
||||||
|
for dir_path in dir_list:
|
||||||
|
if not dir_path:
|
||||||
|
continue
|
||||||
|
if not dir_path.exists():
|
||||||
|
continue
|
||||||
|
# 获取目录所在磁盘
|
||||||
|
if os.name == "nt":
|
||||||
|
disk = dir_path.drive
|
||||||
|
else:
|
||||||
|
disk = os.stat(dir_path).st_dev
|
||||||
|
# 如果磁盘未出现过,则计算其剩余空间并加入总剩余空间中
|
||||||
|
if disk not in disk_set:
|
||||||
|
disk_set.add(disk)
|
||||||
|
total_space += SystemUtils.total_space(dir_path)
|
||||||
|
total_free_space += SystemUtils.free_space(dir_path)
|
||||||
|
return total_space, total_free_space
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def free_space(path: Path) -> float:
|
||||||
|
"""
|
||||||
|
获取指定路径的剩余空间(单位:Byte)
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return 0.0
|
||||||
|
return psutil.disk_usage(str(path)).free
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def total_space(path: Path) -> float:
|
||||||
|
"""
|
||||||
|
获取指定路径的总空间(单位:Byte)
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return 0.0
|
||||||
|
return psutil.disk_usage(str(path)).total
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def processes() -> List[schemas.ProcessInfo]:
|
||||||
|
"""
|
||||||
|
获取所有进程
|
||||||
|
"""
|
||||||
|
processes = []
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'create_time', 'memory_info', 'status']):
|
||||||
|
try:
|
||||||
|
if proc.status() != psutil.STATUS_ZOMBIE:
|
||||||
|
runtime = datetime.datetime.now() - datetime.datetime.fromtimestamp(
|
||||||
|
int(getattr(proc, 'create_time', 0)()))
|
||||||
|
mem_info = getattr(proc, 'memory_info', None)()
|
||||||
|
if mem_info is not None:
|
||||||
|
mem_mb = round(mem_info.rss / (1024 * 1024), 1)
|
||||||
|
processes.append(schemas.ProcessInfo(
|
||||||
|
pid=proc.pid, name=proc.name(), run_time=runtime.seconds, memory=mem_mb
|
||||||
|
))
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||||
|
pass
|
||||||
|
return processes
|
||||||
|
@ -43,3 +43,4 @@ starlette~=0.27.0
|
|||||||
PyVirtualDisplay~=3.0
|
PyVirtualDisplay~=3.0
|
||||||
Cython~=0.29.35
|
Cython~=0.29.35
|
||||||
tvdb_api~=3.1
|
tvdb_api~=3.1
|
||||||
|
psutil==5.9.4
|
Loading…
x
Reference in New Issue
Block a user