plugin-jm-server 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/PKG-INFO +11 -2
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/README.md +10 -1
- plugin_jm_server-0.2.2/src/plugin_jm_server/__init__.py +3 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/app.py +70 -2
- plugin_jm_server-0.2.2/src/plugin_jm_server/driver.py +88 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/files.py +20 -15
- plugin_jm_server-0.2.2/src/plugin_jm_server/message.py +135 -0
- plugin_jm_server-0.2.2/src/plugin_jm_server/static/css/h5/m_message.css +476 -0
- plugin_jm_server-0.2.2/src/plugin_jm_server/static/css/message.css +516 -0
- plugin_jm_server-0.2.2/src/plugin_jm_server/static/js/common.js +195 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/index.js +7 -3
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/index_spa.js +4 -1
- plugin_jm_server-0.2.2/src/plugin_jm_server/static/js/message.js +301 -0
- plugin_jm_server-0.2.1/src/plugin_jm_server/templates/m_index.html → plugin_jm_server-0.2.2/src/plugin_jm_server/templates/index.html +37 -8
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/index_spa.html +27 -4
- plugin_jm_server-0.2.1/src/plugin_jm_server/templates/index.html → plugin_jm_server-0.2.2/src/plugin_jm_server/templates/m_index.html +27 -4
- plugin_jm_server-0.2.2/src/plugin_jm_server/templates/m_message.html +84 -0
- plugin_jm_server-0.2.2/src/plugin_jm_server/templates/message.html +91 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server.egg-info/PKG-INFO +11 -2
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server.egg-info/SOURCES.txt +6 -0
- plugin_jm_server-0.2.1/src/plugin_jm_server/__init__.py +0 -3
- plugin_jm_server-0.2.1/src/plugin_jm_server/driver.py +0 -41
- plugin_jm_server-0.2.1/src/plugin_jm_server/static/js/common.js +0 -78
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/setup.cfg +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/setup.py +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/all.min.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/bootstrap.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/download_error.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/h5/m_download_error.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/h5/m_index.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/h5/m_login.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/h5/m_upload.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/h5/normalize.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/index.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/index_spa.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/login.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/style_photo.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/css/upload.css +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/img/bg.jpg +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/img/m_bg.jpeg +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/h5/m_index.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/h5/m_upload.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/jquery-3.7.1.min.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/jquery.album-0.2.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/jquery.lazyload-2.0.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/jquery.photo-0.3.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/js/upload.js +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/webfonts/fa-regular-400.woff2 +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/static/webfonts/fa-solid-900.woff2 +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/download_error.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/jm_view.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/login.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/m_download_error.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/m_jm_view.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/m_login.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/m_upload.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server/templates/upload.html +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server.egg-info/dependency_links.txt +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server.egg-info/requires.txt +0 -0
- {plugin_jm_server-0.2.1 → plugin_jm_server-0.2.2}/src/plugin_jm_server.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plugin_jm_server
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: plugin_jm_server, a plugin for jmcomic that can be used to view comics in a web browser.
|
|
5
5
|
Home-page: https://github.com/hect0x7/plugin-jm-server
|
|
6
6
|
Author: hect0x7
|
|
@@ -53,7 +53,6 @@ Dynamic: summary
|
|
|
53
53
|
|
|
54
54
|
### 文件夹列表页
|
|
55
55
|

|
|
56
|
-

|
|
57
56
|
|
|
58
57
|
### 看本模式
|
|
59
58
|

|
|
@@ -72,6 +71,9 @@ Dynamic: summary
|
|
|
72
71
|
### 看本模式
|
|
73
72
|

|
|
74
73
|
|
|
74
|
+
### 局域网消息(方便手机和电脑互传消息)
|
|
75
|
+

|
|
76
|
+
|
|
75
77
|
|
|
76
78
|
# 使用方式
|
|
77
79
|
|
|
@@ -178,6 +180,13 @@ op.wait_all_plugins_finish()
|
|
|
178
180
|
* **浮动菜单**:提供“回到顶部”、“加载全部图片”及“在资源管理器打开”等便捷操作。
|
|
179
181
|
* **键盘支持**:支持常用快捷键进行页面滚动和翻页。
|
|
180
182
|
|
|
183
|
+
### 4. 局域网消息中心 (v0.2.2+ New)
|
|
184
|
+
本项目最新版本引入了即时通讯与增强的局域网接入支持。
|
|
185
|
+
* **实时互动**:支持服务器与内网设备之间互发即时消息,采用暗色毛玻璃视觉风格。
|
|
186
|
+
* **全站通知**:后台实时轮询新消息,支持浏览器原生桌面通知及精美的网页侧边栏滑入提醒。
|
|
187
|
+
* **接入优化**:自动识别服务器在局域网内的真实物理 IP,主页提供一键复制功能。
|
|
188
|
+
* **非侵入式反馈**:全站移除 alert 阻塞弹窗,所有交互操作结果均通过通知条反馈。
|
|
189
|
+
|
|
181
190
|
# 想法起源
|
|
182
191
|
|
|
183
192
|
想法起源:https://github.com/hect0x7/JMComic-Crawler-Python/issues/192
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
### 文件夹列表页
|
|
19
19
|

|
|
20
|
-

|
|
21
20
|
|
|
22
21
|
### 看本模式
|
|
23
22
|

|
|
@@ -36,6 +35,9 @@
|
|
|
36
35
|
### 看本模式
|
|
37
36
|

|
|
38
37
|
|
|
38
|
+
### 局域网消息(方便手机和电脑互传消息)
|
|
39
|
+

|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
# 使用方式
|
|
41
43
|
|
|
@@ -142,6 +144,13 @@ op.wait_all_plugins_finish()
|
|
|
142
144
|
* **浮动菜单**:提供“回到顶部”、“加载全部图片”及“在资源管理器打开”等便捷操作。
|
|
143
145
|
* **键盘支持**:支持常用快捷键进行页面滚动和翻页。
|
|
144
146
|
|
|
147
|
+
### 4. 局域网消息中心 (v0.2.2+ New)
|
|
148
|
+
本项目最新版本引入了即时通讯与增强的局域网接入支持。
|
|
149
|
+
* **实时互动**:支持服务器与内网设备之间互发即时消息,采用暗色毛玻璃视觉风格。
|
|
150
|
+
* **全站通知**:后台实时轮询新消息,支持浏览器原生桌面通知及精美的网页侧边栏滑入提醒。
|
|
151
|
+
* **接入优化**:自动识别服务器在局域网内的真实物理 IP,主页提供一键复制功能。
|
|
152
|
+
* **非侵入式反馈**:全站移除 alert 阻塞弹窗,所有交互操作结果均通过通知条反馈。
|
|
153
|
+
|
|
145
154
|
# 想法起源
|
|
146
155
|
|
|
147
156
|
想法起源:https://github.com/hect0x7/JMComic-Crawler-Python/issues/192
|
|
@@ -11,6 +11,8 @@ from flask import render_template, send_from_directory
|
|
|
11
11
|
from flask import request, session, redirect, flash, jsonify
|
|
12
12
|
|
|
13
13
|
from .files import FileManager
|
|
14
|
+
from .message import MessageManager
|
|
15
|
+
from .driver import get_lan_ip
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
# noinspection PyMethodMayBeStatic
|
|
@@ -65,6 +67,9 @@ class JmServer:
|
|
|
65
67
|
for k, v in env.items():
|
|
66
68
|
os.environ[k] = v
|
|
67
69
|
|
|
70
|
+
# 初始化消息管理器
|
|
71
|
+
self.message_manager = MessageManager()
|
|
72
|
+
|
|
68
73
|
def __hook_jm_logging(self):
|
|
69
74
|
import jmcomic
|
|
70
75
|
def executor_log(topic: str, msg: str):
|
|
@@ -142,7 +147,9 @@ class JmServer:
|
|
|
142
147
|
data={
|
|
143
148
|
'currentPath': path,
|
|
144
149
|
'defaultPath': self.file_manager.default_path,
|
|
145
|
-
'drivers': self.file_manager.DRIVERS_LIST
|
|
150
|
+
'drivers': self.file_manager.DRIVERS_LIST,
|
|
151
|
+
'lan_ip': get_lan_ip(),
|
|
152
|
+
'port': self.extra.get('port', self.DEFAULT_PORT)
|
|
146
153
|
},
|
|
147
154
|
randomArg=self.url_random_arg())
|
|
148
155
|
|
|
@@ -273,7 +280,9 @@ class JmServer:
|
|
|
273
280
|
"files": self.file_manager.get_files_data(path),
|
|
274
281
|
"drivers": self.file_manager.DRIVERS_LIST,
|
|
275
282
|
"currentPath": path,
|
|
276
|
-
"defaultPath": self.file_manager.default_path
|
|
283
|
+
"defaultPath": self.file_manager.default_path,
|
|
284
|
+
"lan_ip": get_lan_ip(),
|
|
285
|
+
"port": self.extra.get('port', self.DEFAULT_PORT)
|
|
277
286
|
},
|
|
278
287
|
randomArg=self.url_random_arg())
|
|
279
288
|
|
|
@@ -406,6 +415,60 @@ class JmServer:
|
|
|
406
415
|
subprocess.Popen(f'explorer /select,"{path}"')
|
|
407
416
|
return ''
|
|
408
417
|
|
|
418
|
+
# ===== 消息功能 =====
|
|
419
|
+
|
|
420
|
+
def message_page(self):
|
|
421
|
+
"""
|
|
422
|
+
消息页面(支持 PC/移动端自适应)
|
|
423
|
+
"""
|
|
424
|
+
if not self.verify():
|
|
425
|
+
return redirect('/login')
|
|
426
|
+
|
|
427
|
+
return render_template(
|
|
428
|
+
self.url_format(self.mobile_check(), 'message.html'),
|
|
429
|
+
randomArg=self.url_random_arg()
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def api_get_messages(self):
|
|
433
|
+
"""
|
|
434
|
+
API: 获取消息列表(支持增量拉取)
|
|
435
|
+
"""
|
|
436
|
+
if not self.verify():
|
|
437
|
+
return abort(403)
|
|
438
|
+
|
|
439
|
+
since_id = request.args.get('since_id', 0, type=int)
|
|
440
|
+
limit = request.args.get('limit', 50, type=int)
|
|
441
|
+
|
|
442
|
+
messages = self.message_manager.get_messages(since_id=since_id, limit=limit)
|
|
443
|
+
return jsonify({'messages': messages})
|
|
444
|
+
|
|
445
|
+
def api_send_message(self):
|
|
446
|
+
"""
|
|
447
|
+
API: 发送消息
|
|
448
|
+
"""
|
|
449
|
+
if not self.verify():
|
|
450
|
+
return abort(403)
|
|
451
|
+
|
|
452
|
+
data = request.get_json(silent=True)
|
|
453
|
+
if not data or not data.get('content'):
|
|
454
|
+
return jsonify({'error': '消息内容不能为空'}), 400
|
|
455
|
+
|
|
456
|
+
sender = data.get('sender', '').strip() or '匿名'
|
|
457
|
+
content = data.get('content', '').strip()
|
|
458
|
+
sender_ip = request.remote_addr or ''
|
|
459
|
+
|
|
460
|
+
# 判断是否为服务器本机发送
|
|
461
|
+
is_local = sender_ip in ('127.0.0.1', '::1', 'localhost')
|
|
462
|
+
if is_local and sender == '服务器':
|
|
463
|
+
msg = self.message_manager.send_server_message(content)
|
|
464
|
+
else:
|
|
465
|
+
msg = self.message_manager.send_message(sender, content, sender_ip)
|
|
466
|
+
|
|
467
|
+
if msg:
|
|
468
|
+
return jsonify({'status': 'ok', 'message': msg})
|
|
469
|
+
else:
|
|
470
|
+
return jsonify({'error': '发送失败'}), 400
|
|
471
|
+
|
|
409
472
|
def register_routes(self):
|
|
410
473
|
# 添加路由
|
|
411
474
|
self.app.add_url_rule('/jm_view', 'jm_view', self.jm_view, methods=['GET'])
|
|
@@ -424,6 +487,11 @@ class JmServer:
|
|
|
424
487
|
self.app.add_url_rule("/api/album_images", 'api_album_images', self.api_album_images, methods=['GET'])
|
|
425
488
|
self.app.add_url_rule("/api/open_file", 'api_open_file', self.api_open_file, methods=['GET'])
|
|
426
489
|
|
|
490
|
+
# 消息功能路由
|
|
491
|
+
self.app.add_url_rule("/message", 'message_page', self.message_page, methods=['GET'])
|
|
492
|
+
self.app.add_url_rule("/api/messages", 'api_get_messages', self.api_get_messages, methods=['GET'])
|
|
493
|
+
self.app.add_url_rule("/api/messages", 'api_send_message', self.api_send_message, methods=['POST'])
|
|
494
|
+
|
|
427
495
|
def run(self, **kwargs):
|
|
428
496
|
kwargs.setdefault('port', self.DEFAULT_PORT)
|
|
429
497
|
self.register_routes()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_winDriver():
|
|
6
|
+
"""
|
|
7
|
+
Windows操作系统下,返回全部驱动器卷标['C:\','D:\']
|
|
8
|
+
"""
|
|
9
|
+
import psutil
|
|
10
|
+
# 返回驱动器卷标列表
|
|
11
|
+
driver_list = sorted([driver.device for driver in psutil.disk_partitions(True)])
|
|
12
|
+
|
|
13
|
+
i = 0
|
|
14
|
+
num = len(driver_list)
|
|
15
|
+
while num != 0:
|
|
16
|
+
# 重新格式化分隔符
|
|
17
|
+
driver_name = driver_list[i]
|
|
18
|
+
driver_name = driver_name.strip('\\')
|
|
19
|
+
driver_name += '/'
|
|
20
|
+
|
|
21
|
+
# 测试各驱动器是否可访问,目的是筛除未就绪驱动器,如空光驱
|
|
22
|
+
try:
|
|
23
|
+
os.listdir(driver_name)
|
|
24
|
+
driver_list[i] = driver_name
|
|
25
|
+
i += 1
|
|
26
|
+
except PermissionError as e:
|
|
27
|
+
del driver_list[i]
|
|
28
|
+
mobj = re.match(r'\[WinError (\d+)\]', str(e))
|
|
29
|
+
# ERROR_NOT_READY, ERROR_ACCESS_DENIED
|
|
30
|
+
if mobj is not None and mobj.group(1) not in {'21', '5'}:
|
|
31
|
+
print(f'Drive {driver_name} unexpectedly unavailable: {e}')
|
|
32
|
+
finally:
|
|
33
|
+
num -= 1
|
|
34
|
+
|
|
35
|
+
# 返回列表
|
|
36
|
+
return driver_list
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_lan_ip():
|
|
40
|
+
"""
|
|
41
|
+
通过本地接口获取局域网 IP 地址(无需外部连接)
|
|
42
|
+
优先使用 psutil 扫描活动网卡
|
|
43
|
+
"""
|
|
44
|
+
import socket
|
|
45
|
+
try:
|
|
46
|
+
import psutil
|
|
47
|
+
|
|
48
|
+
# 获取所有网卡信息
|
|
49
|
+
addrs = psutil.net_if_addrs()
|
|
50
|
+
stats = psutil.net_if_stats()
|
|
51
|
+
|
|
52
|
+
# 定义私有地址段前缀
|
|
53
|
+
private_prefixes = ('192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.',
|
|
54
|
+
'172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.',
|
|
55
|
+
'172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.')
|
|
56
|
+
|
|
57
|
+
# 1. 优先寻找处于 up 状态且有私有 IP 的物理网卡
|
|
58
|
+
for interface, snics in addrs.items():
|
|
59
|
+
# 跳过禁用的网卡
|
|
60
|
+
if interface in stats and not stats[interface].isup:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
for snic in snics:
|
|
64
|
+
# 必须是 IPv4
|
|
65
|
+
if snic.family == socket.AF_INET:
|
|
66
|
+
ip = snic.address
|
|
67
|
+
# 排除回环地址,且必须符合私有地址段
|
|
68
|
+
if not ip.startswith('127.') and any(ip.startswith(prefix) for prefix in private_prefixes):
|
|
69
|
+
return ip
|
|
70
|
+
|
|
71
|
+
# 2. 如果没找到私有地址,尝试获取主机名的 IP
|
|
72
|
+
host_ip = socket.gethostbyname(socket.gethostname())
|
|
73
|
+
if not host_ip.startswith('127.'):
|
|
74
|
+
return host_ip
|
|
75
|
+
|
|
76
|
+
except Exception:
|
|
77
|
+
# 最后的保底兜底逻辑
|
|
78
|
+
try:
|
|
79
|
+
return socket.gethostbyname(socket.gethostname())
|
|
80
|
+
except:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
return '127.0.0.1'
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
paths = get_winDriver()
|
|
88
|
+
print(paths)
|
|
@@ -21,9 +21,8 @@ class FileManager:
|
|
|
21
21
|
return ['/']
|
|
22
22
|
|
|
23
23
|
def get_jm_view_images(self, path):
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
give_up_sort = False
|
|
24
|
+
sortable_images = []
|
|
25
|
+
unsortable_images = []
|
|
27
26
|
|
|
28
27
|
for f in self.files_of_dir_safe(path):
|
|
29
28
|
if not self.is_image_file(f):
|
|
@@ -32,20 +31,22 @@ class FileManager:
|
|
|
32
31
|
name = common.of_file_name(f)
|
|
33
32
|
try:
|
|
34
33
|
index = int(name[:name.index('.')])
|
|
34
|
+
sortable_images.append({
|
|
35
|
+
'filename': name,
|
|
36
|
+
'data_original': f'/view_file?path={f}',
|
|
37
|
+
'index': index,
|
|
38
|
+
})
|
|
35
39
|
except ValueError:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'data_original': f'/view_file?path={f}',
|
|
42
|
-
'index': index,
|
|
43
|
-
})
|
|
40
|
+
unsortable_images.append({
|
|
41
|
+
'filename': name,
|
|
42
|
+
'data_original': f'/view_file?path={f}',
|
|
43
|
+
'index': None,
|
|
44
|
+
})
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
sortable_images.sort(key=lambda item: item['index'])
|
|
47
|
+
unsortable_images.sort(key=lambda item: item['filename'])
|
|
47
48
|
|
|
48
|
-
return
|
|
49
|
+
return sortable_images + unsortable_images
|
|
49
50
|
|
|
50
51
|
@staticmethod
|
|
51
52
|
def is_image_file(filename):
|
|
@@ -92,10 +93,14 @@ class FileManager:
|
|
|
92
93
|
target_path = self.get_target_path(file_path)
|
|
93
94
|
return self.build_one_path_info(target_path, 'Link')
|
|
94
95
|
|
|
96
|
+
quoted_path = quote(file_path)
|
|
97
|
+
quoted_name = quote(name)
|
|
98
|
+
|
|
95
99
|
return {
|
|
96
100
|
"name": name,
|
|
97
101
|
'path': file_path,
|
|
98
|
-
'
|
|
102
|
+
'quoted_path': quoted_path,
|
|
103
|
+
'href': f'/?path={quoted_path}' if the_type == 'dir' else f'./download_file/{quoted_name}',
|
|
99
104
|
"size": size,
|
|
100
105
|
"ctime": time_str,
|
|
101
106
|
"type": the_type,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
消息管理模块
|
|
3
|
+
使用 JSON 文件持久化存储聊天消息
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import threading
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageManager:
|
|
13
|
+
"""
|
|
14
|
+
消息管理器 —— 负责消息的读写与存储
|
|
15
|
+
数据以 JSON 文件形式保存在指定目录
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, data_dir=None):
|
|
19
|
+
"""
|
|
20
|
+
初始化消息管理器
|
|
21
|
+
:param data_dir: 数据目录路径,默认使用程序同级目录下的 .jm_messages
|
|
22
|
+
"""
|
|
23
|
+
if data_dir is None:
|
|
24
|
+
data_dir = os.path.join(os.path.expanduser('~'), '.jm_messages')
|
|
25
|
+
|
|
26
|
+
self.data_dir = data_dir
|
|
27
|
+
self.messages_file = os.path.join(data_dir, 'messages.json')
|
|
28
|
+
self._lock = threading.Lock()
|
|
29
|
+
self._ensure_data_dir()
|
|
30
|
+
|
|
31
|
+
def _ensure_data_dir(self):
|
|
32
|
+
"""确保数据目录存在"""
|
|
33
|
+
os.makedirs(self.data_dir, exist_ok=True)
|
|
34
|
+
if not os.path.exists(self.messages_file):
|
|
35
|
+
self._save_messages([])
|
|
36
|
+
|
|
37
|
+
def _load_messages(self) -> list:
|
|
38
|
+
"""从文件加载所有消息"""
|
|
39
|
+
try:
|
|
40
|
+
with open(self.messages_file, 'r', encoding='utf-8') as f:
|
|
41
|
+
return json.load(f)
|
|
42
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
def _save_messages(self, messages: list):
|
|
46
|
+
"""将所有消息保存到文件"""
|
|
47
|
+
with open(self.messages_file, 'w', encoding='utf-8') as f:
|
|
48
|
+
json.dump(messages, f, ensure_ascii=False, indent=2)
|
|
49
|
+
|
|
50
|
+
def send_message(self, sender: str, content: str, sender_ip: str = '') -> dict:
|
|
51
|
+
"""
|
|
52
|
+
发送一条消息
|
|
53
|
+
:param sender: 发送者昵称
|
|
54
|
+
:param content: 消息内容
|
|
55
|
+
:param sender_ip: 发送者 IP
|
|
56
|
+
:return: 新创建的消息对象
|
|
57
|
+
"""
|
|
58
|
+
content = content.strip()
|
|
59
|
+
if not content:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
# 限制单条消息最大长度
|
|
63
|
+
if len(content) > 5000:
|
|
64
|
+
content = content[:5000]
|
|
65
|
+
|
|
66
|
+
now = time.time()
|
|
67
|
+
msg = {
|
|
68
|
+
'id': int(now * 1000),
|
|
69
|
+
'sender': sender or '匿名',
|
|
70
|
+
'content': content,
|
|
71
|
+
'sender_ip': sender_ip,
|
|
72
|
+
'timestamp': now,
|
|
73
|
+
'time_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'),
|
|
74
|
+
'is_server': False,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
with self._lock:
|
|
78
|
+
messages = self._load_messages()
|
|
79
|
+
messages.append(msg)
|
|
80
|
+
# 保留最近 500 条消息
|
|
81
|
+
if len(messages) > 500:
|
|
82
|
+
messages = messages[-500:]
|
|
83
|
+
self._save_messages(messages)
|
|
84
|
+
|
|
85
|
+
return msg
|
|
86
|
+
|
|
87
|
+
def send_server_message(self, content: str) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
服务器端发送一条消息
|
|
90
|
+
:param content: 消息内容
|
|
91
|
+
:return: 新创建的消息对象
|
|
92
|
+
"""
|
|
93
|
+
content = content.strip()
|
|
94
|
+
if not content:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
now = time.time()
|
|
98
|
+
msg = {
|
|
99
|
+
'id': int(now * 1000),
|
|
100
|
+
'sender': '服务器',
|
|
101
|
+
'content': content,
|
|
102
|
+
'sender_ip': '127.0.0.1',
|
|
103
|
+
'timestamp': now,
|
|
104
|
+
'time_str': datetime.fromtimestamp(now).strftime('%Y-%m-%d %H:%M:%S'),
|
|
105
|
+
'is_server': True,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
with self._lock:
|
|
109
|
+
messages = self._load_messages()
|
|
110
|
+
messages.append(msg)
|
|
111
|
+
if len(messages) > 500:
|
|
112
|
+
messages = messages[-500:]
|
|
113
|
+
self._save_messages(messages)
|
|
114
|
+
|
|
115
|
+
return msg
|
|
116
|
+
|
|
117
|
+
def get_messages(self, since_id: int = 0, limit: int = 50) -> list:
|
|
118
|
+
"""
|
|
119
|
+
获取消息列表
|
|
120
|
+
:param since_id: 从该 ID 之后的消息开始获取(用于增量拉取)
|
|
121
|
+
:param limit: 最多返回条数
|
|
122
|
+
:return: 消息列表
|
|
123
|
+
"""
|
|
124
|
+
with self._lock:
|
|
125
|
+
messages = self._load_messages()
|
|
126
|
+
|
|
127
|
+
if since_id > 0:
|
|
128
|
+
messages = [m for m in messages if m['id'] > since_id]
|
|
129
|
+
|
|
130
|
+
return messages[-limit:]
|
|
131
|
+
|
|
132
|
+
def get_all_messages(self) -> list:
|
|
133
|
+
"""获取全部消息"""
|
|
134
|
+
with self._lock:
|
|
135
|
+
return self._load_messages()
|