From 15bb043fe8c2e969fa8e8d891e914debe80baa9a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 10 Jul 2023 16:42:20 +0800 Subject: [PATCH] add dashboard apis --- app/api/apiv1.py | 3 +- app/api/endpoints/dashboard.py | 46 +++++++++++++++++ app/chain/dashboard.py | 13 +++++ app/modules/emby/__init__.py | 14 ++++++ app/modules/jellyfin/__init__.py | 14 ++++++ app/modules/plex/__init__.py | 13 +++++ app/schemas/__init__.py | 1 + app/schemas/dashboard.py | 38 ++++++++++++++ app/utils/system.py | 86 +++++++++++++++++++++++++++++--- requirements.txt | 3 +- 10 files changed, 223 insertions(+), 8 deletions(-) create mode 100644 app/api/endpoints/dashboard.py create mode 100644 app/chain/dashboard.py create mode 100644 app/schemas/dashboard.py diff --git a/app/api/apiv1.py b/app/api/apiv1.py index 412b1864..a504ae89 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -1,7 +1,7 @@ from fastapi import APIRouter 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.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(plugin.router, prefix="/plugin", tags=["plugin"]) api_router.include_router(download.router, prefix="/download", tags=["download"]) +api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"]) diff --git a/app/api/endpoints/dashboard.py b/app/api/endpoints/dashboard.py new file mode 100644 index 00000000..c6d2bbc0 --- /dev/null +++ b/app/api/endpoints/dashboard.py @@ -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() diff --git a/app/chain/dashboard.py b/app/chain/dashboard.py new file mode 100644 index 00000000..cb03e3d7 --- /dev/null +++ b/app/chain/dashboard.py @@ -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") diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 41fc8660..ee589cd6 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Optional, Tuple, Union, Any +from app import schemas from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase @@ -83,3 +84,16 @@ class EmbyModule(_ModuleBase): ) ] 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 + ) diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index c1883e99..560f5889 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -2,6 +2,7 @@ import json from pathlib import Path from typing import Optional, Tuple, Union, Any +from app import schemas from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase @@ -75,3 +76,16 @@ class JellyfinModule(_ModuleBase): :return: 成功或失败 """ 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 + ) diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index b8311dc8..1abb98fa 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Optional, Tuple, Union, Any +from app import schemas from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase @@ -73,3 +74,15 @@ class PlexModule(_ModuleBase): ) ] 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 + ) diff --git a/app/schemas/__init__.py b/app/schemas/__init__.py index 78c6571e..06cfac75 100644 --- a/app/schemas/__init__.py +++ b/app/schemas/__init__.py @@ -7,3 +7,4 @@ from .context import * from .servarr import * from .plugin import * from .history import * +from .dashboard import * diff --git a/app/schemas/dashboard.py b/app/schemas/dashboard.py new file mode 100644 index 00000000..27ea1133 --- /dev/null +++ b/app/schemas/dashboard.py @@ -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 diff --git a/app/utils/system.py b/app/utils/system.py index f8b3b7f5..a0ff172d 100644 --- a/app/utils/system.py +++ b/app/utils/system.py @@ -1,9 +1,12 @@ +import datetime import os import platform import re import shutil from pathlib import Path -from typing import List +from typing import List, Union, Tuple +import psutil +from app import schemas class SystemUtils: @@ -39,7 +42,7 @@ class SystemUtils: return True if platform.system() == 'Darwin' else False @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) @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) @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) @staticmethod - def softlink(src: Path, dest: Path): + def softlink(src: Path, dest: Path) -> Tuple[int, str]: """ 软链接 """ @@ -105,7 +108,7 @@ class SystemUtils: return files @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 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 diff --git a/requirements.txt b/requirements.txt index bb1daf9d..12ad2a5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -42,4 +42,5 @@ chardet~=4.0.0 starlette~=0.27.0 PyVirtualDisplay~=3.0 Cython~=0.29.35 -tvdb_api~=3.1 \ No newline at end of file +tvdb_api~=3.1 +psutil==5.9.4 \ No newline at end of file