2 Commits

7 changed files with 153 additions and 80 deletions
+3 -2
View File
@@ -7,7 +7,6 @@
"timeout": 9, "timeout": 9,
"library_name": "", "library_name": "",
"sync_mode": "local_force", "sync_mode": "local_force",
"local_path": "playlists",
"path_rules": [], "path_rules": [],
"path_mapping": { "path_mapping": {
"mode": "SIMPLE", "mode": "SIMPLE",
@@ -24,5 +23,7 @@
"schedule_daily_time": "00:00", "schedule_daily_time": "00:00",
"schedule_weekly_days": [0], "schedule_weekly_days": [0],
"schedule_weekly_time": "00:00", "schedule_weekly_time": "00:00",
"schedule_auto_watch": false "schedule_auto_watch": false,
"backup_enabled": false,
"backup_retention_count": 5
} }
+19 -17
View File
@@ -13,7 +13,7 @@ from pydantic import BaseModel, Field
from app.utils.config import server_config from app.utils.config import server_config
from app.utils.local_playlist import load_local_playlist, scan_local_playlists from app.utils.local_playlist import load_local_playlist, scan_local_playlists
from app.utils.logger import logger from app.utils.logger import logger
from app.utils.playlist_merge import SyncMode, TEST_PLAYLIST_DIR, sync_all_playlists from app.utils.playlist_merge import SyncMode, SYNC_ARTIFACTS_DIR, sync_all_playlists
from app.utils.plex_client import plex_client from app.utils.plex_client import plex_client
from app.utils.scheduler import start_scheduler, update_scheduler_job, get_next_run_time, validate_cron_expression from app.utils.scheduler import start_scheduler, update_scheduler_job, get_next_run_time, validate_cron_expression
from app.utils.sync_manager import sync_manager from app.utils.sync_manager import sync_manager
@@ -499,8 +499,8 @@ async def api_connect(payload: ConnectRequest):
async def api_playlists(server: str = Query(..., pattern="(?i)^(local|cloud)$"), local_path: str | None = None): async def api_playlists(server: str = Query(..., pattern="(?i)^(local|cloud)$"), local_path: str | None = None):
server_type = server.lower() server_type = server.lower()
if server_type == "local": if server_type == "local":
resolved_path = local_path or server_config.local_path # local_path is intentionally fixed; ignore query overrides.
server_config.set_and_save_config(local_path=resolved_path) resolved_path = server_config.local_path
playlists = _scan_local_playlists_with_meta(resolved_path) playlists = _scan_local_playlists_with_meta(resolved_path)
return {"playlists": [item.model_dump() for item in playlists]} return {"playlists": [item.model_dump() for item in playlists]}
@@ -542,7 +542,8 @@ async def api_sync(payload: SyncRequest):
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) raise HTTPException(status_code=400, detail=str(exc))
local_dir = payload.local_path or server_config.local_path # local_path is intentionally fixed; ignore request overrides.
local_dir = server_config.local_path
# Use sync_manager to execute sync, ensuring state is updated # Use sync_manager to execute sync, ensuring state is updated
try: try:
@@ -567,7 +568,7 @@ async def api_sync(payload: SyncRequest):
"conflict_count": conflict_count, "conflict_count": conflict_count,
"delete_count": deleted_count, "delete_count": deleted_count,
"playlist_count": len(results), "playlist_count": len(results),
"output_dir": TEST_PLAYLIST_DIR, "output_dir": SYNC_ARTIFACTS_DIR,
} }
@@ -611,23 +612,24 @@ def _build_home_context(
# 显示主页 # 显示主页
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def home(request: Request, local_path: str = "playlist"): async def home(request: Request, local_path: str = "playlists"):
index_path = os.path.join(FRONTEND_DIST_PATH, "index.html") index_path = os.path.join(FRONTEND_DIST_PATH, "index.html")
if os.path.exists(index_path): if os.path.exists(index_path):
return FileResponse(index_path) return FileResponse(index_path)
context = _build_home_context(request, local_path or server_config.local_path) # local_path is intentionally fixed; ignore query overrides.
context = _build_home_context(request, server_config.local_path)
return templates.TemplateResponse("home.html", context) return templates.TemplateResponse("home.html", context)
@app.post("/sync", response_class=HTMLResponse) @app.post("/sync", response_class=HTMLResponse)
async def trigger_sync(request: Request, mode: str = Form(...), local_path: str = Form("playlist")): async def trigger_sync(request: Request, mode: str = Form(...), local_path: str = Form("playlists")):
try: try:
sync_mode = SyncMode(mode) sync_mode = SyncMode(mode)
except ValueError: except ValueError:
context = _build_home_context( context = _build_home_context(
request, request,
local_path, server_config.local_path,
message=f"未知的同步策略:{mode}", message=f"未知的同步策略:{mode}",
message_type="danger", message_type="danger",
selected_mode=mode, selected_mode=mode,
@@ -636,17 +638,17 @@ async def trigger_sync(request: Request, mode: str = Form(...), local_path: str
try: try:
results = sync_all_playlists( results = sync_all_playlists(
local_dir=local_path, local_dir=server_config.local_path,
mode=sync_mode, mode=sync_mode,
test_folder=TEST_PLAYLIST_DIR, test_folder=SYNC_ARTIFACTS_DIR,
) )
merged_count = sum(len(item.merged_paths) for item in results) merged_count = sum(len(item.merged_paths) for item in results)
conflict_count = sum(len(item.conflicts) for item in results) conflict_count = sum(len(item.conflicts) for item in results)
deleted_count = sum(1 for item in results if item.action == "deleted") deleted_count = sum(1 for item in results if item.action == "deleted")
context = _build_home_context( context = _build_home_context(
request, request,
local_path, server_config.local_path,
message="同步完成,输出已写入测试目录用于验证", message="同步完成,输出已写入同步工作目录(Artifacts",
message_type="success", message_type="success",
sync_result={ sync_result={
"mode": sync_mode.value, "mode": sync_mode.value,
@@ -658,7 +660,7 @@ async def trigger_sync(request: Request, mode: str = Form(...), local_path: str
"conflict_count": conflict_count, "conflict_count": conflict_count,
"delete_count": deleted_count, "delete_count": deleted_count,
"playlist_count": len(results), "playlist_count": len(results),
"output_dir": TEST_PLAYLIST_DIR, "output_dir": SYNC_ARTIFACTS_DIR,
}, },
selected_mode=sync_mode.value, selected_mode=sync_mode.value,
) )
@@ -667,7 +669,7 @@ async def trigger_sync(request: Request, mode: str = Form(...), local_path: str
logger.warning(f"Sync failed: {exc}") logger.warning(f"Sync failed: {exc}")
context = _build_home_context( context = _build_home_context(
request, request,
local_path, server_config.local_path,
message=f"同步失败:{exc}", message=f"同步失败:{exc}",
message_type="danger", message_type="danger",
selected_mode=sync_mode.value, selected_mode=sync_mode.value,
@@ -678,7 +680,7 @@ async def trigger_sync(request: Request, mode: str = Form(...), local_path: str
@app.post("/path-rules", response_class=HTMLResponse) @app.post("/path-rules", response_class=HTMLResponse)
async def save_path_rules( async def save_path_rules(
request: Request, request: Request,
local_path: str = Form("playlist"), local_path: str = Form("playlists"),
pattern: list[str] | None = Form(None), pattern: list[str] | None = Form(None),
replacement: list[str] | None = Form(None), replacement: list[str] | None = Form(None),
): ):
@@ -696,7 +698,7 @@ async def save_path_rules(
context = _build_home_context( context = _build_home_context(
request, request,
local_path, server_config.local_path,
message="正则规则已保存并会在同步前应用。", message="正则规则已保存并会在同步前应用。",
message_type="success", message_type="success",
) )
+8 -2
View File
@@ -7,11 +7,17 @@ from app.utils.config import server_config
from app.utils.local_playlist import load_local_playlist from app.utils.local_playlist import load_local_playlist
from app.utils.plex_client import plex_client from app.utils.plex_client import plex_client
# Default backup directory # Default backup directory (repo root /backups)
BACKUP_DIR = os.path.abspath( DEFAULT_BACKUP_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "backups") os.path.join(os.path.dirname(__file__), "..", "..", "backups")
) )
# Allow Docker / users to relocate backups for centralized host backup.
# Example: /app/data/backup
BACKUP_DIR = os.path.abspath(
os.environ.get("PLEXPLAYLISTSYNC_BACKUP_DIR", DEFAULT_BACKUP_DIR)
)
def ensure_backup_dir(): def ensure_backup_dir():
"""Ensure the backup directory exists.""" """Ensure the backup directory exists."""
+23 -13
View File
@@ -3,6 +3,7 @@ import os
from app.utils.logger import logger from app.utils.logger import logger
DEFAULT_SYNC_MODE = "merge_local_primary" DEFAULT_SYNC_MODE = "merge_local_primary"
LOCAL_PLAYLISTS_FOLDER = "playlists"
DEFAULT_PATH_MAPPING = { DEFAULT_PATH_MAPPING = {
"mode": "SIMPLE", "mode": "SIMPLE",
"simple": [], "simple": [],
@@ -14,10 +15,22 @@ DEFAULT_PATH_MAPPING = {
} }
} }
CONFIG_PATH = os.path.abspath( DEFAULT_CONFIG_PATH = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "config.json") os.path.join(os.path.dirname(__file__), "..", "config.json")
) )
# Allow Docker / users to relocate config for backup convenience.
# Example: /app/data/config/config.json
CONFIG_PATH = os.path.abspath(
os.environ.get("PLEXPLAYLISTSYNC_CONFIG_PATH", DEFAULT_CONFIG_PATH)
)
def _ensure_parent_dir(file_path: str) -> None:
parent = os.path.dirname(os.path.abspath(file_path))
if parent and not os.path.isdir(parent):
os.makedirs(parent, exist_ok=True)
class ServerConfig: class ServerConfig:
@@ -30,16 +43,18 @@ class ServerConfig:
self.timeout = 9 self.timeout = 9
self.library_name = "" self.library_name = ""
self.sync_mode = DEFAULT_SYNC_MODE self.sync_mode = DEFAULT_SYNC_MODE
self.local_path = "playlist" # Local playlists folder is intentionally fixed and not part of config.
# Docker volume should mount host ./playlists -> container /app/playlists.
self.local_path = LOCAL_PLAYLISTS_FOLDER
self.path_rules: list[dict[str, str]] = [] # Legacy field for backward compatibility self.path_rules: list[dict[str, str]] = [] # Legacy field for backward compatibility
self.path_mapping: dict = DEFAULT_PATH_MAPPING.copy() self.path_mapping: dict = DEFAULT_PATH_MAPPING.copy()
self.schedule_mode = "DISABLED" self.schedule_mode = "DISABLED"
self.schedule_cron = "" self.schedule_cron = ""
self.schedule_daily_time = "02:00" self.schedule_daily_time = "00:00"
self.schedule_weekly_days = [0] self.schedule_weekly_days = [0]
self.schedule_weekly_time = "03:00" self.schedule_weekly_time = "00:00"
self.schedule_auto_watch = False self.schedule_auto_watch = False
self.backup_enabled = False self.backup_enabled = True
self.backup_retention_count = 5 self.backup_retention_count = 5
self.load() self.load()
@@ -66,7 +81,8 @@ class ServerConfig:
self.timeout = config.get("timeout", 9) self.timeout = config.get("timeout", 9)
self.library_name = config.get("library_name", "") self.library_name = config.get("library_name", "")
self.sync_mode = config.get("sync_mode", DEFAULT_SYNC_MODE) self.sync_mode = config.get("sync_mode", DEFAULT_SYNC_MODE)
self.local_path = config.get("local_path", "playlist") # local_path is fixed by design and not configurable.
self.local_path = LOCAL_PLAYLISTS_FOLDER
self.path_rules = config.get("path_rules", []) or [] self.path_rules = config.get("path_rules", []) or []
# Load path_mapping with default fallback # Load path_mapping with default fallback
@@ -97,6 +113,7 @@ class ServerConfig:
logger.debug(f"Current server config: {self.__dict__}") logger.debug(f"Current server config: {self.__dict__}")
def save(self): def save(self):
_ensure_parent_dir(CONFIG_PATH)
config = { config = {
"theme": self.theme, "theme": self.theme,
"token": self.token, "token": self.token,
@@ -106,7 +123,6 @@ class ServerConfig:
"timeout": self.timeout, "timeout": self.timeout,
"library_name": self.library_name, "library_name": self.library_name,
"sync_mode": self.sync_mode, "sync_mode": self.sync_mode,
"local_path": self.local_path,
"path_rules": self.path_rules, "path_rules": self.path_rules,
"path_mapping": self.path_mapping, "path_mapping": self.path_mapping,
"schedule_mode": self.schedule_mode, "schedule_mode": self.schedule_mode,
@@ -144,9 +160,6 @@ class ServerConfig:
def set_sync_mode(self, sync_mode: str) -> None: def set_sync_mode(self, sync_mode: str) -> None:
self.sync_mode = sync_mode self.sync_mode = sync_mode
def set_local_path(self, local_path: str) -> None:
self.local_path = local_path or "playlist"
def set_theme(self, theme: str) -> None: def set_theme(self, theme: str) -> None:
# check theme is valid # check theme is valid
if theme not in ["auto", "dark", "light"]: if theme not in ["auto", "dark", "light"]:
@@ -208,7 +221,6 @@ class ServerConfig:
timeout: int | None = None, timeout: int | None = None,
library_name: str | None = None, library_name: str | None = None,
sync_mode: str | None = None, sync_mode: str | None = None,
local_path: str | None = None,
path_rules: list[dict[str, str]] | None = None, path_rules: list[dict[str, str]] | None = None,
path_mapping: dict | None = None, path_mapping: dict | None = None,
) -> None: ) -> None:
@@ -228,8 +240,6 @@ class ServerConfig:
self.set_library(library_name) self.set_library(library_name)
if sync_mode is not None: if sync_mode is not None:
self.set_sync_mode(sync_mode) self.set_sync_mode(sync_mode)
if local_path is not None:
self.set_local_path(local_path)
if path_rules is not None: if path_rules is not None:
self.set_path_rules(path_rules) self.set_path_rules(path_rules)
if path_mapping is not None: if path_mapping is not None:
+89 -39
View File
@@ -12,10 +12,13 @@ from app.utils.plex_client import plex_client
from merge3 import Merge3 from merge3 import Merge3
TEST_PLAYLIST_DIR = os.path.abspath( SYNC_ARTIFACTS_DIR = os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "test_playlists") os.path.join(os.path.dirname(__file__), "..", "..", "data", "sync_artifacts")
) )
# Backward-compat alias (older API / logs used this name).
TEST_PLAYLIST_DIR = SYNC_ARTIFACTS_DIR
class ConflictResolutionStrategy(str, Enum): class ConflictResolutionStrategy(str, Enum):
LOCAL_PRIORITY = "local_priority" LOCAL_PRIORITY = "local_priority"
@@ -159,9 +162,37 @@ class MergeResult:
conflicts: list[dict] conflicts: list[dict]
def _ensure_test_dir(folder: str = TEST_PLAYLIST_DIR) -> str: def _ensure_dir(path: str) -> str:
os.makedirs(folder, exist_ok=True) os.makedirs(path, exist_ok=True)
return folder return path
def _safe_folder_name(name: str, max_len: int = 120) -> str:
"""Make a filesystem-safe folder name (especially for Windows hosts).
This is used for artifacts folders persisted to the host.
"""
if not name:
return "(unnamed)"
# Windows-disallowed characters plus path separators.
invalid = set('<>:"/\\|?*')
cleaned = "".join(("_" if ch in invalid else ch) for ch in name).strip()
if not cleaned:
cleaned = "(unnamed)"
if len(cleaned) > max_len:
cleaned = cleaned[:max_len].rstrip()
return cleaned
def _playlist_artifact_dir(artifacts_root: str, playlist_name: str) -> str:
return os.path.join(artifacts_root, _safe_folder_name(playlist_name))
def _artifact_file(playlist_folder: str, category: str, filename: str) -> str:
category_dir = _ensure_dir(os.path.join(playlist_folder, category))
return os.path.join(category_dir, filename)
def _read_text_if_exists(path: str) -> tuple[str, bool]: def _read_text_if_exists(path: str) -> tuple[str, bool]:
@@ -174,26 +205,27 @@ def _read_text_if_exists(path: str) -> tuple[str, bool]:
return "", False return "", False
def _save_playlist_to_folder(filename: str, paths: Sequence[str], folder: str) -> str: def _save_playlist_text(path: str, text: str) -> str:
_ensure_test_dir(folder) """Write text if changed (avoid triggering unnecessary file events)."""
file_path = os.path.join(folder, filename)
logger.info(f"Saving playlist to: {file_path}")
new_content = save_paths(paths) _ensure_dir(os.path.dirname(path))
# Check if content has changed before writing to avoid triggering unnecessary file events if os.path.exists(path):
if os.path.exists(file_path):
try: try:
with open(file_path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as file:
current_content = f.read() if file.read() == text:
if current_content == new_content: return path
return file_path
except OSError: except OSError:
pass pass
with open(file_path, "w", encoding="utf-8") as file: with open(path, "w", encoding="utf-8") as file:
file.write(new_content) file.write(text)
return file_path return path
def _save_playlist_paths(path: str, paths: Sequence[str]) -> str:
logger.info(f"Saving playlist to: {path}")
return _save_playlist_text(path, save_paths(paths))
def _normalize_inputs( def _normalize_inputs(
@@ -205,9 +237,9 @@ def _normalize_inputs(
local_paths = load_paths(local_text) local_paths = load_paths(local_text)
remote_paths = load_paths(remote_text) remote_paths = load_paths(remote_text)
_save_playlist_to_folder("base_playlist.m3u8", base_paths, folder) _save_playlist_paths(_artifact_file(folder, "base", "base_prev.m3u8"), base_paths)
_save_playlist_to_folder("local_input.m3u8", local_paths, folder) _save_playlist_paths(_artifact_file(folder, "inputs", "local_input.m3u8"), local_paths)
_save_playlist_to_folder("remote_input.m3u8", remote_paths, folder) _save_playlist_paths(_artifact_file(folder, "inputs", "remote_input.m3u8"), remote_paths)
return base_paths, local_paths, remote_paths return base_paths, local_paths, remote_paths
@@ -275,14 +307,13 @@ def _write_results(
else: else:
remote_lines = list(merged_lines) remote_lines = list(merged_lines)
_save_playlist_to_folder("local_result.m3u8", local_lines, folder) _save_playlist_paths(_artifact_file(folder, "outputs", "local_result.m3u8"), local_lines)
_save_playlist_to_folder("remote_result.m3u8", remote_lines, folder) _save_playlist_paths(_artifact_file(folder, "outputs", "remote_result.m3u8"), remote_lines)
_save_playlist_to_folder("base_next.m3u8", merged_lines, folder) _save_playlist_paths(_artifact_file(folder, "base", "base_next.m3u8"), merged_lines)
def _write_delete_marker(playlist: str, folder: str) -> str: def _write_delete_marker(playlist: str, folder: str) -> str:
_ensure_test_dir(folder) marker_path = _artifact_file(folder, "meta", "delete.txt")
marker_path = os.path.join(folder, "delete.txt")
with open(marker_path, "w", encoding="utf-8") as file: with open(marker_path, "w", encoding="utf-8") as file:
file.write(f"delete playlist {playlist}") file.write(f"delete playlist {playlist}")
return marker_path return marker_path
@@ -418,7 +449,7 @@ def merge_playlists(
local_text: str, local_text: str,
remote_text: str, remote_text: str,
strategy: ConflictResolutionStrategy = ConflictResolutionStrategy.LOCAL_PRIORITY, strategy: ConflictResolutionStrategy = ConflictResolutionStrategy.LOCAL_PRIORITY,
test_folder: str = TEST_PLAYLIST_DIR, test_folder: str = SYNC_ARTIFACTS_DIR,
compiled_rules: CompiledRegexRules | None = None, compiled_rules: CompiledRegexRules | None = None,
) -> MergeResult: ) -> MergeResult:
"""Merge playlists using diff3 and resolve conflicts per strategy. """Merge playlists using diff3 and resolve conflicts per strategy.
@@ -494,19 +525,42 @@ def _load_local_playlists(local_dir: str) -> dict[str, str]:
def _load_playlist_snapshots(playlist: str, folder: str) -> tuple[str, str, str, bool, bool]: def _load_playlist_snapshots(playlist: str, folder: str) -> tuple[str, str, str, bool, bool]:
"""Load base/local/remote texts for a playlist from its test folder.""" """Load base/local/remote texts for a playlist from its test folder."""
playlist_folder = os.path.join(folder, playlist) playlist_folder = _playlist_artifact_dir(folder, playlist)
# Prefer new organized layout.
base_text, base_exists = _read_text_if_exists( base_text, base_exists = _read_text_if_exists(
os.path.join(playlist_folder, "base_next.m3u8") os.path.join(playlist_folder, "base", "base_next.m3u8")
) )
if not base_text: if not base_text:
alt_text, _ = _read_text_if_exists( alt_text, alt_exists = _read_text_if_exists(
os.path.join(playlist_folder, "base_playlist.m3u8") os.path.join(playlist_folder, "base", "base_prev.m3u8")
) )
base_text = base_text or alt_text base_text = base_text or alt_text
base_exists = base_exists or alt_exists
# Backward-compat: legacy flat file layout.
if not base_text:
legacy_text, legacy_exists = _read_text_if_exists(
os.path.join(playlist_folder, "base_next.m3u8")
)
base_text = base_text or legacy_text
base_exists = base_exists or legacy_exists
if not base_text:
legacy_text, legacy_exists = _read_text_if_exists(
os.path.join(playlist_folder, "base_playlist.m3u8")
)
base_text = base_text or legacy_text
base_exists = base_exists or legacy_exists
remote_text, remote_exists = _read_text_if_exists( remote_text, remote_exists = _read_text_if_exists(
os.path.join(playlist_folder, "inputs", "remote_input.m3u8")
)
if not remote_text:
legacy_remote, legacy_exists = _read_text_if_exists(
os.path.join(playlist_folder, "remote_input.m3u8") os.path.join(playlist_folder, "remote_input.m3u8")
) )
remote_text = remote_text or legacy_remote
remote_exists = remote_exists or legacy_exists
return base_text, remote_text, playlist_folder, remote_exists, base_exists return base_text, remote_text, playlist_folder, remote_exists, base_exists
@@ -748,7 +802,7 @@ def _compile_simple_mapping_rules(simple_mappings: list[dict]) -> CompiledRegexR
def sync_all_playlists( def sync_all_playlists(
local_dir: str, mode: SyncMode, test_folder: str = TEST_PLAYLIST_DIR local_dir: str, mode: SyncMode, test_folder: str = SYNC_ARTIFACTS_DIR
) -> list[PlaylistSyncResult]: ) -> list[PlaylistSyncResult]:
"""Synchronize all playlists that can be matched by name. """Synchronize all playlists that can be matched by name.
@@ -795,18 +849,14 @@ def sync_all_playlists(
legacy_compiled_rules = _compile_regex_rules(server_config.path_rules) legacy_compiled_rules = _compile_regex_rules(server_config.path_rules)
logger.info("Using legacy path_rules for preprocessing") logger.info("Using legacy path_rules for preprocessing")
_ensure_test_dir(test_folder) _ensure_dir(test_folder)
logger.info(f"Syncing playlists to test folder: {test_folder}") logger.info(f"Sync artifacts folder: {test_folder}")
local_playlists = _load_local_playlists(local_dir) local_playlists = _load_local_playlists(local_dir)
remote_playlists = _fetch_remote_playlists() remote_playlists = _fetch_remote_playlists()
playlist_names: set[str] = set(local_playlists.keys()) playlist_names: set[str] = set(local_playlists.keys())
playlist_names.update(remote_playlists.keys()) playlist_names.update(remote_playlists.keys())
for entry in os.scandir(test_folder):
if entry.is_dir():
playlist_names.add(entry.name)
results: list[PlaylistSyncResult] = [] results: list[PlaylistSyncResult] = []
for playlist in sorted(playlist_names): for playlist in sorted(playlist_names):
+2 -2
View File
@@ -136,7 +136,7 @@ class SyncManager:
try: try:
if action == "synced": if action == "synced":
# 1. Write Local # 1. Write Local
local_result_path = os.path.join(output_dir, "local_result.m3u8") local_result_path = os.path.join(output_dir, "outputs", "local_result.m3u8")
if os.path.exists(local_result_path): if os.path.exists(local_result_path):
tracks = load_local_playlist(local_result_path) tracks = load_local_playlist(local_result_path)
dest_path = os.path.join(server_config.local_path, f"{playlist_name}.m3u8") dest_path = os.path.join(server_config.local_path, f"{playlist_name}.m3u8")
@@ -145,7 +145,7 @@ class SyncManager:
write_local_playlist(dest_path, tracks) write_local_playlist(dest_path, tracks)
# 2. Write Remote (Plex) # 2. Write Remote (Plex)
remote_result_path = os.path.join(output_dir, "remote_result.m3u8") remote_result_path = os.path.join(output_dir, "outputs", "remote_result.m3u8")
if os.path.exists(remote_result_path): if os.path.exists(remote_result_path):
tracks = load_local_playlist(remote_result_path) tracks = load_local_playlist(remote_result_path)
if server_config.library_name: if server_config.library_name:
+5 -1
View File
@@ -6,10 +6,14 @@ services:
- "8888:8080" - "8888:8080"
volumes: volumes:
- PATH_TO_YOUR_PLAYLISTS:/app/playlists - PATH_TO_YOUR_PLAYLISTS:/app/playlists
- PATH_TO_YOUR_BACKUP:/app/backup - PATH_TO_DATA/backup:/app/data/backup
- PATH_TO_DATA/config:/app/data/config
- PATH_TO_DATA/sync_artifacts:/app/data/sync_artifacts
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
- PYTHONDONTWRITEBYTECODE=1 - PYTHONDONTWRITEBYTECODE=1
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
- TZ=${TZ:-Asia/Tokyo} - TZ=${TZ:-Asia/Tokyo}
- PLEXPLAYLISTSYNC_CONFIG_PATH=/app/data/config/config.json
- PLEXPLAYLISTSYNC_BACKUP_DIR=/app/data/backup
restart: unless-stopped restart: unless-stopped