Compare commits
2 Commits
9ff74550a2
...
f80fac4ce5
| Author | SHA1 | Date | |
|---|---|---|---|
| f80fac4ce5 | |||
| 61794b8db9 |
+65
-7
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
from typing import Tuple
|
||||
from app.utils.config import server_config
|
||||
from fastapi import FastAPI, Request, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
@@ -6,6 +7,7 @@ from fastapi.templating import Jinja2Templates
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from app.utils.plex_client import plex_client
|
||||
from app.utils.logger import logger
|
||||
from app.utils.local_playlist import scan_local_playlists
|
||||
|
||||
app = FastAPI()
|
||||
templates = Jinja2Templates(
|
||||
@@ -21,20 +23,76 @@ app.mount(
|
||||
)
|
||||
|
||||
|
||||
def _get_cloud_playlists() -> Tuple[list[dict], str, dict]:
|
||||
"""Fetch playlists and connection state from the remote Plex server."""
|
||||
|
||||
server_config.load()
|
||||
playlists: list[dict] = []
|
||||
status = "unset"
|
||||
server_info = {
|
||||
"name": "未设置",
|
||||
"domain": server_config.url or "未设置",
|
||||
}
|
||||
|
||||
# no server url configured
|
||||
if not server_config.url:
|
||||
return playlists, status, server_info
|
||||
|
||||
status = "failed"
|
||||
try:
|
||||
plex_client.connect(
|
||||
token=server_config.token,
|
||||
scheme=server_config.scheme,
|
||||
url=server_config.url,
|
||||
port=server_config.port,
|
||||
)
|
||||
status = "connected" if plex_client.connected else "failed"
|
||||
server_info.update(
|
||||
{
|
||||
"name": getattr(plex_client.server, "friendlyName", "未命名服务器"),
|
||||
"domain": server_config.url,
|
||||
}
|
||||
)
|
||||
|
||||
for playlist in plex_client.server.playlists():
|
||||
track_count = getattr(playlist, "itemCount", None)
|
||||
if track_count is None:
|
||||
try:
|
||||
track_count = len(playlist.items())
|
||||
except Exception:
|
||||
track_count = 0
|
||||
playlists.append(
|
||||
{
|
||||
"name": playlist.title,
|
||||
"track_count": track_count,
|
||||
}
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning(f"Failed to fetch cloud playlists: {exc}")
|
||||
status = "failed"
|
||||
|
||||
playlists.sort(key=lambda item: item["name"].lower())
|
||||
return playlists, status, server_info
|
||||
|
||||
|
||||
# 显示主页
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request):
|
||||
async def home(request: Request, local_path: str = "playlist"):
|
||||
server_config.load()
|
||||
local_playlists = scan_local_playlists(local_path)
|
||||
cloud_playlists, connection_status, server_info = _get_cloud_playlists()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"login.html",
|
||||
"home.html",
|
||||
{
|
||||
"request": request,
|
||||
"theme": server_config.theme,
|
||||
"path": "/login",
|
||||
"scheme": server_config.scheme,
|
||||
"token": server_config.token,
|
||||
"server_url": server_config.url,
|
||||
"port": server_config.port,
|
||||
"path": "/",
|
||||
"local_playlists": local_playlists,
|
||||
"local_path": local_path,
|
||||
"cloud_playlists": cloud_playlists,
|
||||
"connection_status": connection_status,
|
||||
"server_info": server_info,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -18,3 +18,23 @@
|
||||
.glass-toast-info {
|
||||
background-color: rgba(var(--bs-info-rgb), 0.75) !important;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: var(--bs-success);
|
||||
}
|
||||
|
||||
.status-failed {
|
||||
background-color: var(--bs-danger);
|
||||
}
|
||||
|
||||
.status-unset {
|
||||
background-color: var(--bs-secondary);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,13 @@
|
||||
</a>
|
||||
<hr class="d-none d-md-block">
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item">
|
||||
<a href="/" class="nav-link {% if path == '/' %}active{% endif %}">
|
||||
<!-- home icon -->
|
||||
<i class="bi bi-house-door-fill"></i>
|
||||
<span class="d-none d-md-inline ms-2">主页</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="/login" class="nav-link {% if path == '/login' %}active{% endif %}">
|
||||
<!-- login icon -->
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}主页 - Plex Sync{% endblock %}
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column flex-md-row align-items-md-center mb-4 gap-2">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">播放列表概览</h1>
|
||||
<p class="text-body-secondary mb-0">浏览本地与云端同步的播放列表,随时刷新最新状态。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-lg-row align-items-lg-center gap-3 mb-3">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">本地播放列表</h5>
|
||||
<small class="text-body-secondary">输入目录并刷新,读取挂载的播放列表文件</small>
|
||||
</div>
|
||||
<form class="ms-lg-auto w-100 w-lg-auto" method="get" action="/">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="local_path" value="{{ local_path }}" placeholder="playlist">
|
||||
<button class="btn btn-outline-secondary" type="submit" title="刷新本地播放列表">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
<span class="d-none d-sm-inline ms-1">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if local_playlists %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for playlist in local_playlists %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ playlist.name }}</div>
|
||||
<div class="text-body-secondary small">曲目数</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-primary">{{ playlist.track_count }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="text-body-secondary">未发现播放列表,请确认目录或点击刷新。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-xl-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-body">
|
||||
<div class="d-flex flex-column flex-lg-row align-items-lg-center gap-3 mb-3">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">云端播放列表</h5>
|
||||
<small class="text-body-secondary">来自已连接的 Plex 服务器</small>
|
||||
</div>
|
||||
<form class="ms-lg-auto" method="get" action="/">
|
||||
<input type="hidden" name="local_path" value="{{ local_path }}">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-2 text-body-secondary small">
|
||||
<span class="status-dot status-{{ connection_status }}"></span>
|
||||
<span>
|
||||
{{ server_info.name }}
|
||||
<span class="text-body-secondary">· {{ server_info.domain }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary" type="submit" title="刷新云端播放列表">
|
||||
<i class="bi bi-arrow-clockwise"></i>
|
||||
<span class="d-none d-sm-inline ms-1">刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if cloud_playlists %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for playlist in cloud_playlists %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ playlist.name }}</div>
|
||||
<div class="text-body-secondary small">曲目数</div>
|
||||
</div>
|
||||
<span class="badge rounded-pill text-bg-success">{{ playlist.track_count }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="text-body-secondary">暂无云端播放列表,检查连接或刷新重试。</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from typing import List
|
||||
from app.utils.logger import logger
|
||||
|
||||
@@ -31,4 +32,37 @@ def load_local_playlist(playlist_path: str) -> List[str]:
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"An error occurred while loading the playlist: {e}")
|
||||
return []
|
||||
return []
|
||||
|
||||
|
||||
def scan_local_playlists(base_path: str) -> list[dict]:
|
||||
"""Scan a directory for playlist files and return their basic info.
|
||||
|
||||
Args:
|
||||
base_path: Directory that contains playlist files.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries with ``name`` and ``track_count`` keys.
|
||||
"""
|
||||
|
||||
playlists: list[dict] = []
|
||||
if not base_path:
|
||||
logger.warning("No base path provided for local playlists scan.")
|
||||
return playlists
|
||||
|
||||
absolute_path = os.path.abspath(base_path)
|
||||
if not os.path.isdir(absolute_path):
|
||||
logger.warning(f"Playlist path does not exist or is not a directory: {absolute_path}")
|
||||
return playlists
|
||||
|
||||
for entry in os.scandir(absolute_path):
|
||||
if not entry.is_file():
|
||||
continue
|
||||
if not entry.name.lower().endswith((".m3u", ".m3u8")):
|
||||
continue
|
||||
tracks = load_local_playlist(entry.path)
|
||||
playlists.append({"name": entry.name, "track_count": len(tracks)})
|
||||
|
||||
playlists.sort(key=lambda item: item["name"].lower())
|
||||
logger.info(f"Found {len(playlists)} playlists under {absolute_path}.")
|
||||
return playlists
|
||||
Reference in New Issue
Block a user