222 lines
10 KiB
HTML
222 lines
10 KiB
HTML
{% 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>
|
|
|
|
{% if message %}
|
|
<div class="alert alert-{{ message_type or 'info' }}" role="alert">
|
|
{{ message }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card mb-4">
|
|
<div class="card-body">
|
|
<div class="d-flex flex-column flex-xl-row align-items-xl-center gap-3 mb-3">
|
|
<div>
|
|
<h5 class="card-title mb-0">选择同步策略</h5>
|
|
<small class="text-body-secondary">根据需要选择单向覆盖或双向合并策略</small>
|
|
</div>
|
|
<form class="ms-xl-auto w-100 w-xl-auto" method="post" action="/sync">
|
|
<div class="row gy-2 gx-2 align-items-end">
|
|
<div class="col-12 col-lg-8">
|
|
<label class="form-label mb-1">同步模式</label>
|
|
<select class="form-select" name="mode">
|
|
{% for option in sync_modes %}
|
|
<option value="{{ option.value }}" {% if selected_mode == option.value %}selected{% endif %}>
|
|
{{ option.label }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-auto">
|
|
<label class="form-label d-none d-lg-block"> </label>
|
|
<div>
|
|
<button class="btn btn-primary" type="submit">
|
|
<i class="bi bi-play-fill"></i>
|
|
<span class="ms-1">开始同步</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" name="local_path" value="{{ local_path }}">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<ul class="mb-0 text-body-secondary small ps-3">
|
|
<li><strong>完全本地优先</strong>:本地列表直接覆盖云端,顺序以本地为准(无 diff)。</li>
|
|
<li><strong>完全云端优先</strong>:云端列表直接覆盖本地,顺序以云端为准(无 diff)。</li>
|
|
<li><strong>双向合并(本地优先)</strong>:三方合并,冲突时选本地。</li>
|
|
<li><strong>双向合并(云端优先)</strong>:三方合并,冲突时选云端。</li>
|
|
</ul>
|
|
{% if sync_result %}
|
|
<div class="alert alert-info mt-3 mb-0" role="alert">
|
|
<div class="fw-semibold">{{ sync_result.mode_label }}</div>
|
|
<div class="small mb-1">播放列表:{{ sync_result.playlist_count }},删除:{{ sync_result.delete_count }},合并曲目:{{ sync_result.merged_count }},冲突数:{{ sync_result.conflict_count }}</div>
|
|
<div class="small text-body-secondary">已写入:{{ sync_result.output_dir }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</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>
|
|
|
|
<div class="card mt-4">
|
|
<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>
|
|
</div>
|
|
<form id="pathRuleForm" method="post" action="/path-rules">
|
|
<input type="hidden" name="local_path" value="{{ local_path }}">
|
|
<div id="ruleList" class="d-flex flex-column gap-2 mb-3"></div>
|
|
<div class="d-flex align-items-center gap-3 mb-3">
|
|
<button class="btn btn-outline-primary rounded-circle rule-add-btn" type="button" id="addRuleBtn"
|
|
title="添加新的正则规则">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
<div class="text-body-secondary small">从上到下依次匹配,左侧填写正则表达式,右侧填写替换结果。</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<button class="btn btn-success" type="submit">
|
|
<i class="bi bi-save"></i>
|
|
<span class="ms-1">保存规则</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<template id="ruleRowTemplate">
|
|
<div class="input-group rule-row">
|
|
<span class="input-group-text">匹配</span>
|
|
<input type="text" class="form-control" name="pattern" placeholder="正则表达式" required>
|
|
<span class="input-group-text">替换为</span>
|
|
<input type="text" class="form-control" name="replacement" placeholder="替换文本">
|
|
<button class="btn btn-outline-danger" type="button" title="删除此规则">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
const existingRules = {{ path_rules | tojson | safe }};
|
|
const ruleList = document.getElementById('ruleList');
|
|
const addRuleBtn = document.getElementById('addRuleBtn');
|
|
const template = document.getElementById('ruleRowTemplate');
|
|
|
|
function bindRemoveButton(row) {
|
|
const removeBtn = row.querySelector('button');
|
|
removeBtn.addEventListener('click', () => {
|
|
row.remove();
|
|
});
|
|
}
|
|
|
|
function addRuleRow(pattern = '', replacement = '') {
|
|
const clone = template.content.cloneNode(true);
|
|
const row = clone.querySelector('.rule-row');
|
|
const [patternInput, replacementInput] = row.querySelectorAll('input');
|
|
patternInput.value = pattern;
|
|
replacementInput.value = replacement;
|
|
bindRemoveButton(row);
|
|
ruleList.appendChild(clone);
|
|
}
|
|
|
|
addRuleBtn.addEventListener('click', () => addRuleRow());
|
|
|
|
if (existingRules.length) {
|
|
existingRules.forEach(rule => addRuleRow(rule.pattern || '', rule.replacement || ''));
|
|
}
|
|
|
|
if (!ruleList.children.length) {
|
|
addRuleRow();
|
|
}
|
|
</script>
|
|
{% endblock %}
|