@@ -1211,41 +1264,147 @@
});
});
- // ── 保存并重启 ──
- function saveAndRestart() {
+ // ── 下载队列 ──
+ let downloadQueue = [];
+
+ function escapeHtml(s) {
+ return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
+ '&':'&','<':'<','>':'>','"':'"',"'":'''
+ })[c]);
+ }
+
+ function buildFilterFromForm(startDate, endDate) {
+ const custom = filterExpression || '';
+ if (custom) return custom;
+ if (!startDate) return '';
+ return 'message_date >= ' + startDate + ' 00:00:00' +
+ (endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : '');
+ }
+
+ function renderQueue() {
+ const card = document.getElementById('queue-card');
+ const list = document.getElementById('queue-list');
+ const count = document.getElementById('queue-count');
+ if (!downloadQueue.length) { card.style.display = 'none'; return; }
+ card.style.display = '';
+ count.textContent = downloadQueue.length + ' 个';
+ list.innerHTML = downloadQueue.map((q, i) => `
+
+
${i + 1}
+
+
${escapeHtml(q.chat_title || q.chat_id)}
+
${escapeHtml(q.download_filter || '无过滤条件')}
+
+
+
+ `).join('');
+ }
+
+ function addToQueue() {
const channelId = parseChannelId(document.getElementById('channel-input').value);
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
if (!channelId) { toast('请输入有效的频道', 'err'); return; }
if (!startDate) { toast('请选择开始日期', 'err'); return; }
+ if (downloadQueue.some(q => q.chat_id === channelId)) {
+ toast('该频道已在队列中', 'err'); return;
+ }
- const doSave = (chatTitle, chatType) => {
- const btn = document.getElementById('save-btn');
- btn.disabled = true; btn.innerHTML = '
重启中…';
- const submittedFilter = filterExpression || '';
- const builtFilter = startDate
- ? ('message_date >= ' + startDate + ' 00:00:00' + (endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : ''))
- : '';
- fetch('/api/save_and_restart', {
- method:'POST', headers:{'Content-Type':'application/json'},
- body: JSON.stringify({ chat_id:channelId, start_date:startDate, end_date:endDate, chat_title:chatTitle, chat_type:chatType||'', download_filter:submittedFilter })
- }).then(r => r.json()).then(d => {
- if (d.success) {
- currentFilter = submittedFilter || builtFilter;
- updateBannerFilter();
- toast('✅ ' + d.message + ',5秒后刷新…'); setTimeout(() => location.reload(), 5000);
- } else { toast('失败: ' + (d.error||''), 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
- }).catch(e => { toast('操作失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
+ const doAdd = (chatTitle, chatType) => {
+ downloadQueue.push({
+ chat_id: channelId,
+ chat_title: chatTitle || channelId,
+ chat_type: chatType || '',
+ download_filter: buildFilterFromForm(startDate, endDate),
+ start_date: startDate,
+ end_date: endDate,
+ });
+ renderQueue();
+ // 清空表单以便继续添加下一个频道
+ document.getElementById('channel-input').value = '';
+ validatedChannel = null;
+ const valBox = document.getElementById('val-result');
+ if (valBox) valBox.className = 'val-drop';
+ toast('已加入队列: ' + (chatTitle || channelId));
};
- if (validatedChannel) {
- doSave(validatedChannel.chat_title, validatedChannel.chat_type);
+ const btn = document.getElementById('add-queue-btn');
+ if (validatedChannel && validatedChannel.chat_id === channelId) {
+ doAdd(validatedChannel.chat_title, validatedChannel.chat_type);
} else {
- const btn = document.getElementById('save-btn');
- btn.disabled = true; btn.innerHTML = '
验证中…';
+ btn.disabled = true;
fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
.then(r => r.json()).then(d => {
- if (d.valid) { showVal(d); validatedChannel = d; doSave(d.chat_title, d.chat_type); }
+ btn.disabled = false;
+ if (d.valid) { showVal(d); validatedChannel = d; doAdd(d.chat_title, d.chat_type); }
+ else { showVal(d); toast('频道验证失败', 'err'); }
+ }).catch(e => { btn.disabled = false; toast('验证失败: ' + e.message, 'err'); });
+ }
+ }
+
+ function removeFromQueue(idx) {
+ downloadQueue.splice(idx, 1);
+ renderQueue();
+ }
+
+ function clearQueue() {
+ if (!downloadQueue.length) return;
+ if (!confirm('确认清空队列里的 ' + downloadQueue.length + ' 个频道?')) return;
+ downloadQueue = [];
+ renderQueue();
+ }
+
+ // ── 保存并重启(队列 + 表单一起提交) ──
+ function saveAndRestart() {
+ const channelId = parseChannelId(document.getElementById('channel-input').value);
+ const startDate = document.getElementById('start-date').value;
+ const endDate = document.getElementById('end-date').value;
+
+ const hasForm = !!channelId;
+ const hasQueue = downloadQueue.length > 0;
+ if (!hasForm && !hasQueue) { toast('请输入频道或先加入队列', 'err'); return; }
+ if (hasForm && !startDate) { toast('请选择开始日期', 'err'); return; }
+
+ const btn = document.getElementById('save-btn');
+ btn.disabled = true; btn.innerHTML = '
重启中…';
+
+ const submitAll = (formItem) => {
+ const items = downloadQueue.slice();
+ if (formItem && !items.some(q => q.chat_id === formItem.chat_id)) items.push(formItem);
+ fetch('/api/save_and_restart_multi', {
+ method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({ items })
+ }).then(r => r.json()).then(d => {
+ if (d.success) {
+ toast('✅ ' + d.message + ',5秒后刷新…');
+ setTimeout(() => location.reload(), 5000);
+ } else {
+ toast('失败: ' + (d.error||''), 'err');
+ btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
+ }
+ }).catch(e => {
+ toast('操作失败: ' + e.message, 'err');
+ btn.disabled = false; btn.textContent = '🔄 保存并重启下载';
+ });
+ };
+
+ if (!hasForm) { submitAll(null); return; }
+
+ const buildFormItem = (chatTitle, chatType) => ({
+ chat_id: channelId,
+ chat_title: chatTitle || channelId,
+ chat_type: chatType || '',
+ download_filter: buildFilterFromForm(startDate, endDate),
+ start_date: startDate,
+ end_date: endDate,
+ });
+
+ if (validatedChannel && validatedChannel.chat_id === channelId) {
+ submitAll(buildFormItem(validatedChannel.chat_title, validatedChannel.chat_type));
+ } else {
+ fetch('/api/validate_chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ chat_id: channelId }) })
+ .then(r => r.json()).then(d => {
+ if (d.valid) { showVal(d); validatedChannel = d; submitAll(buildFormItem(d.chat_title, d.chat_type)); }
else { showVal(d); toast('频道验证失败', 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; }
}).catch(e => { toast('验证失败: ' + e.message, 'err'); btn.disabled = false; btn.textContent = '🔄 保存并重启下载'; });
}
diff --git a/module/web.py b/module/web.py
index aa311b1..e8218e7 100644
--- a/module/web.py
+++ b/module/web.py
@@ -646,6 +646,58 @@ def _update_chat_config(chat_id: str, download_filter: str, update_memory: bool
return {"success": False, "error": str(e)}
+def _update_chat_configs_multi(items: list, update_memory: bool = True) -> dict:
+ """
+ 多频道版本:把 items 数组写入 config["chat"],完整覆盖原数组。
+ 每个 item 形如 {"chat_id": "xxx", "download_filter": "xxx"}。
+ 同步更新内存里的 _app.chat_download_config。
+ """
+ global _app
+ if not items:
+ return {"success": False, "error": "队列为空"}
+
+ try:
+ yaml = YAML()
+ yaml.preserve_quotes = True
+ with open(CONFIG_FILE_PATH, "r", encoding="utf-8") as f:
+ config = yaml.load(f)
+
+ # 按队列顺序重建 chat 数组
+ new_chat_list = []
+ for item in items:
+ chat_id = str(item.get("chat_id", "")).strip()
+ download_filter = str(item.get("download_filter", "") or "").strip()
+ if not chat_id:
+ continue
+ entry = {"chat_id": chat_id, "last_read_message_id": 0}
+ if download_filter:
+ entry["download_filter"] = download_filter
+ new_chat_list.append(entry)
+
+ if not new_chat_list:
+ return {"success": False, "error": "队列中没有有效的频道"}
+
+ config["chat"] = new_chat_list
+
+ with open(CONFIG_FILE_PATH, "w", encoding="utf-8") as f:
+ yaml.dump(config, f)
+
+ # 同步内存配置
+ if update_memory and _app:
+ _app.config["chat"] = new_chat_list
+ _app.chat_download_config.clear()
+ for entry in new_chat_list:
+ c = ChatDownloadConfig()
+ c.last_read_message_id = 0
+ c.download_filter = entry.get("download_filter", "")
+ _app.chat_download_config[entry["chat_id"]] = c
+
+ return {"success": True, "count": len(new_chat_list)}
+ except Exception as e:
+ logger.exception(f"Error updating multi chat config: {e}")
+ return {"success": False, "error": str(e)}
+
+
async def _validate_chat(chat_id: str) -> dict:
"""
Validate chat/channel and get its info.
@@ -804,6 +856,62 @@ def api_save_and_restart():
return jsonify({"success": False, "error": str(e)})
+@_flask_app.route("/api/save_and_restart_multi", methods=["POST"])
+@login_required
+def api_save_and_restart_multi():
+ """多频道版本的保存并重启。接收 items 数组,每项含 chat_id + download_filter。"""
+ global _app
+ try:
+ data = request.get_json() or {}
+ items = data.get("items", [])
+ if not isinstance(items, list) or not items:
+ return jsonify({"success": False, "error": "队列为空"})
+
+ # 规范化 items:每项必须有 chat_id;download_filter 可由 start_date/end_date 构建
+ normalized = []
+ for it in items:
+ cid = str(it.get("chat_id", "") or "").strip()
+ if not cid:
+ continue
+ flt = str(it.get("download_filter", "") or "").strip()
+ if not flt:
+ sd = str(it.get("start_date", "") or "").strip()
+ ed = str(it.get("end_date", "") or "").strip()
+ if sd:
+ flt = _build_download_filter(sd, ed)
+ normalized.append({
+ "chat_id": cid,
+ "download_filter": flt,
+ "chat_title": it.get("chat_title", "") or "",
+ "chat_type": it.get("chat_type", "") or "",
+ })
+
+ if not normalized:
+ return jsonify({"success": False, "error": "没有有效的频道"})
+
+ result = _update_chat_configs_multi(normalized, update_memory=True)
+ if not result.get("success"):
+ return jsonify(result)
+
+ # 历史记录更新
+ for it in normalized:
+ _add_to_channel_history(it["chat_id"], it["chat_title"], it["chat_type"])
+
+ # 触发重启
+ if _app:
+ _app.restart_program = True
+ logger.info(f"Restart flag set, {len(normalized)} chats queued")
+
+ return jsonify({
+ "success": True,
+ "count": len(normalized),
+ "message": f"已保存 {len(normalized)} 个频道任务,正在重启..."
+ })
+ except Exception as e:
+ logger.exception(f"Error in api_save_and_restart_multi: {e}")
+ return jsonify({"success": False, "error": str(e)})
+
+
@_flask_app.route("/api/start_download", methods=["POST"])
@login_required
def api_start_download():