dy-cli 0.2.0__py3-none-any.whl
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.
- dy_cli/__init__.py +3 -0
- dy_cli/commands/__init__.py +0 -0
- dy_cli/commands/account.py +103 -0
- dy_cli/commands/analytics.py +120 -0
- dy_cli/commands/auth.py +159 -0
- dy_cli/commands/config_cmd.py +67 -0
- dy_cli/commands/download.py +212 -0
- dy_cli/commands/init.py +200 -0
- dy_cli/commands/interact.py +140 -0
- dy_cli/commands/live.py +141 -0
- dy_cli/commands/profile.py +78 -0
- dy_cli/commands/publish.py +123 -0
- dy_cli/commands/search.py +131 -0
- dy_cli/commands/trending.py +82 -0
- dy_cli/engines/__init__.py +0 -0
- dy_cli/engines/api_client.py +665 -0
- dy_cli/engines/playwright_client.py +836 -0
- dy_cli/main.py +144 -0
- dy_cli/utils/__init__.py +0 -0
- dy_cli/utils/config.py +99 -0
- dy_cli/utils/envelope.py +49 -0
- dy_cli/utils/export.py +68 -0
- dy_cli/utils/index_cache.py +83 -0
- dy_cli/utils/output.py +283 -0
- dy_cli/utils/signature.py +183 -0
- dy_cli-0.2.0.dist-info/METADATA +376 -0
- dy_cli-0.2.0.dist-info/RECORD +34 -0
- dy_cli-0.2.0.dist-info/WHEEL +4 -0
- dy_cli-0.2.0.dist-info/entry_points.txt +2 -0
- dy_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- scripts/chrome_launcher.py +71 -0
- scripts/douyin_analytics.py +99 -0
- scripts/douyin_login.py +64 -0
- scripts/douyin_publisher.py +199 -0
dy_cli/main.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dy — 抖音命令行工具主入口。
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
dy search "关键词" 搜索视频
|
|
6
|
+
dy trending 抖音热榜
|
|
7
|
+
dy download URL 无水印下载
|
|
8
|
+
dy publish -t 标题 -c 描述 -v 视频 发布视频
|
|
9
|
+
dy detail AWEME_ID 视频详情
|
|
10
|
+
dy comments AWEME_ID 查看评论
|
|
11
|
+
dy like AWEME_ID 点赞
|
|
12
|
+
dy comment AWEME_ID -c "内容" 评论
|
|
13
|
+
dy favorite AWEME_ID 收藏
|
|
14
|
+
dy follow SEC_USER_ID 关注
|
|
15
|
+
dy live info ROOM_ID 直播信息
|
|
16
|
+
dy live record ROOM_ID 录制直播
|
|
17
|
+
dy analytics 数据看板
|
|
18
|
+
dy notifications 通知消息
|
|
19
|
+
dy me 我的信息
|
|
20
|
+
dy profile SEC_USER_ID 用户主页
|
|
21
|
+
dy login 登录
|
|
22
|
+
dy logout 退出登录
|
|
23
|
+
dy status 登录状态
|
|
24
|
+
dy account list|add|remove|default 多账号管理
|
|
25
|
+
dy config show|set|get|reset 配置管理
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import click
|
|
30
|
+
|
|
31
|
+
from dy_cli import __version__
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
BANNER = r"""
|
|
35
|
+
╔═══════════════════════════════╗
|
|
36
|
+
║ 🎬 dy-cli v{version} ║
|
|
37
|
+
║ 抖音命令行工具 ║
|
|
38
|
+
╚═══════════════════════════════╝
|
|
39
|
+
""".format(version=__version__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AliasGroup(click.Group):
|
|
43
|
+
"""支持命令别名的 Click Group。"""
|
|
44
|
+
|
|
45
|
+
ALIASES = {
|
|
46
|
+
"pub": "publish",
|
|
47
|
+
"s": "search",
|
|
48
|
+
"dl": "download",
|
|
49
|
+
"t": "trending",
|
|
50
|
+
"r": "detail",
|
|
51
|
+
"read": "detail",
|
|
52
|
+
"fav": "favorite",
|
|
53
|
+
"noti": "notifications",
|
|
54
|
+
"stat": "status",
|
|
55
|
+
"acc": "account",
|
|
56
|
+
"cfg": "config",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def get_command(self, ctx, cmd_name):
|
|
60
|
+
resolved = self.ALIASES.get(cmd_name, cmd_name)
|
|
61
|
+
return super().get_command(ctx, resolved)
|
|
62
|
+
|
|
63
|
+
def format_help(self, ctx, formatter):
|
|
64
|
+
formatter.write(BANNER)
|
|
65
|
+
super().format_help(ctx, formatter)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@click.group(cls=AliasGroup, invoke_without_command=True)
|
|
69
|
+
@click.version_option(version=__version__, prog_name="dy-cli")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
def cli(ctx):
|
|
72
|
+
"""🎬 抖音命令行工具 — 搜索、下载、发布、互动、热榜、直播、数据分析"""
|
|
73
|
+
if ctx.invoked_subcommand is None:
|
|
74
|
+
click.echo(ctx.get_help())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# 注册所有命令
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
# 初始化
|
|
82
|
+
from dy_cli.commands.init import init
|
|
83
|
+
cli.add_command(init)
|
|
84
|
+
|
|
85
|
+
# 认证
|
|
86
|
+
from dy_cli.commands.auth import login, logout, auth_status
|
|
87
|
+
cli.add_command(login)
|
|
88
|
+
cli.add_command(logout)
|
|
89
|
+
cli.add_command(auth_status, "status")
|
|
90
|
+
|
|
91
|
+
# 搜索 & 详情
|
|
92
|
+
from dy_cli.commands.search import search, detail
|
|
93
|
+
cli.add_command(search)
|
|
94
|
+
cli.add_command(detail)
|
|
95
|
+
|
|
96
|
+
# 下载
|
|
97
|
+
from dy_cli.commands.download import download
|
|
98
|
+
cli.add_command(download)
|
|
99
|
+
|
|
100
|
+
# 发布
|
|
101
|
+
from dy_cli.commands.publish import publish
|
|
102
|
+
cli.add_command(publish)
|
|
103
|
+
|
|
104
|
+
# 互动
|
|
105
|
+
from dy_cli.commands.interact import like, favorite, comment, comments, follow
|
|
106
|
+
cli.add_command(like)
|
|
107
|
+
cli.add_command(favorite)
|
|
108
|
+
cli.add_command(comment)
|
|
109
|
+
cli.add_command(comments)
|
|
110
|
+
cli.add_command(follow)
|
|
111
|
+
|
|
112
|
+
# 热榜
|
|
113
|
+
from dy_cli.commands.trending import trending
|
|
114
|
+
cli.add_command(trending)
|
|
115
|
+
|
|
116
|
+
# 直播
|
|
117
|
+
from dy_cli.commands.live import live_group
|
|
118
|
+
cli.add_command(live_group, "live")
|
|
119
|
+
|
|
120
|
+
# 数据分析
|
|
121
|
+
from dy_cli.commands.analytics import analytics, notifications
|
|
122
|
+
cli.add_command(analytics)
|
|
123
|
+
cli.add_command(notifications)
|
|
124
|
+
|
|
125
|
+
# 用户
|
|
126
|
+
from dy_cli.commands.profile import me, profile
|
|
127
|
+
cli.add_command(me)
|
|
128
|
+
cli.add_command(profile)
|
|
129
|
+
|
|
130
|
+
# 账号管理
|
|
131
|
+
from dy_cli.commands.account import account_group
|
|
132
|
+
cli.add_command(account_group, "account")
|
|
133
|
+
|
|
134
|
+
# 配置管理
|
|
135
|
+
from dy_cli.commands.config_cmd import config_group
|
|
136
|
+
cli.add_command(config_group, "config")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def main():
|
|
140
|
+
cli()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|
dy_cli/utils/__init__.py
ADDED
|
File without changes
|
dy_cli/utils/config.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
全局配置管理 — ~/.dy/config.json
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
CONFIG_DIR = os.path.expanduser("~/.dy")
|
|
11
|
+
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
|
12
|
+
COOKIES_DIR = os.path.join(CONFIG_DIR, "cookies")
|
|
13
|
+
|
|
14
|
+
DEFAULT_CONFIG: dict[str, Any] = {
|
|
15
|
+
"api": {
|
|
16
|
+
"cookie_file": os.path.join(COOKIES_DIR, "default.json"),
|
|
17
|
+
"proxy": "",
|
|
18
|
+
"timeout": 30,
|
|
19
|
+
},
|
|
20
|
+
"playwright": {
|
|
21
|
+
"headless": False,
|
|
22
|
+
"chromium_path": "",
|
|
23
|
+
"slow_mo": 0,
|
|
24
|
+
},
|
|
25
|
+
"default": {
|
|
26
|
+
"account": "default",
|
|
27
|
+
"engine": "auto", # auto | api | playwright
|
|
28
|
+
"output": "table", # table | json
|
|
29
|
+
"download_dir": os.path.expanduser("~/Downloads/douyin"),
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _ensure_dir():
|
|
35
|
+
os.makedirs(CONFIG_DIR, exist_ok=True)
|
|
36
|
+
os.makedirs(COOKIES_DIR, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_config() -> dict[str, Any]:
|
|
40
|
+
"""加载配置文件,不存在则返回默认配置。"""
|
|
41
|
+
if os.path.exists(CONFIG_FILE):
|
|
42
|
+
try:
|
|
43
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
44
|
+
user_cfg = json.load(f)
|
|
45
|
+
return _deep_merge(DEFAULT_CONFIG, user_cfg)
|
|
46
|
+
except Exception:
|
|
47
|
+
pass
|
|
48
|
+
return dict(DEFAULT_CONFIG)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def save_config(cfg: dict[str, Any]):
|
|
52
|
+
"""保存配置到文件。"""
|
|
53
|
+
_ensure_dir()
|
|
54
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(cfg, f, ensure_ascii=False, indent=2)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get(key_path: str, default: Any = None) -> Any:
|
|
59
|
+
"""获取嵌套配置值。key_path 格式: 'api.proxy', 'default.engine'"""
|
|
60
|
+
cfg = load_config()
|
|
61
|
+
keys = key_path.split(".")
|
|
62
|
+
current = cfg
|
|
63
|
+
for k in keys:
|
|
64
|
+
if isinstance(current, dict) and k in current:
|
|
65
|
+
current = current[k]
|
|
66
|
+
else:
|
|
67
|
+
return default
|
|
68
|
+
return current
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def set_value(key_path: str, value: Any):
|
|
72
|
+
"""设置嵌套配置值。"""
|
|
73
|
+
cfg = load_config()
|
|
74
|
+
keys = key_path.split(".")
|
|
75
|
+
current = cfg
|
|
76
|
+
for k in keys[:-1]:
|
|
77
|
+
if k not in current or not isinstance(current[k], dict):
|
|
78
|
+
current[k] = {}
|
|
79
|
+
current = current[k]
|
|
80
|
+
current[keys[-1]] = value
|
|
81
|
+
save_config(cfg)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_cookie_file(account: str | None = None) -> str:
|
|
85
|
+
"""获取指定账号的 Cookie 文件路径。"""
|
|
86
|
+
_ensure_dir()
|
|
87
|
+
account = account or load_config()["default"]["account"]
|
|
88
|
+
return os.path.join(COOKIES_DIR, f"{account}.json")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
92
|
+
"""递归合并字典。"""
|
|
93
|
+
result = dict(base)
|
|
94
|
+
for k, v in override.items():
|
|
95
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
96
|
+
result[k] = _deep_merge(result[k], v)
|
|
97
|
+
else:
|
|
98
|
+
result[k] = v
|
|
99
|
+
return result
|
dy_cli/utils/envelope.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
统一输出信封 — Agent 友好的结构化输出。
|
|
3
|
+
|
|
4
|
+
所有 --json / --yaml 输出使用此信封格式:
|
|
5
|
+
成功: {ok: true, schema_version: "1", data: ...}
|
|
6
|
+
失败: {ok: false, schema_version: "1", error: {code: ..., message: ...}}
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
SCHEMA_VERSION = "1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def success_envelope(data: Any) -> dict:
|
|
18
|
+
return {"ok": True, "schema_version": SCHEMA_VERSION, "data": data}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def error_envelope(code: str, message: str) -> dict:
|
|
22
|
+
return {
|
|
23
|
+
"ok": False,
|
|
24
|
+
"schema_version": SCHEMA_VERSION,
|
|
25
|
+
"error": {"code": code, "message": message},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def emit(envelope: dict, fmt: str = "auto") -> None:
|
|
30
|
+
"""输出信封到 stdout。
|
|
31
|
+
|
|
32
|
+
fmt: "json" | "yaml" | "auto"
|
|
33
|
+
auto: TTY → 不输出(由 Rich 处理), 非 TTY → yaml
|
|
34
|
+
"""
|
|
35
|
+
if fmt == "auto":
|
|
36
|
+
if not sys.stdout.isatty():
|
|
37
|
+
fmt = "yaml"
|
|
38
|
+
else:
|
|
39
|
+
return # TTY 模式由 Rich 处理
|
|
40
|
+
|
|
41
|
+
if fmt == "yaml":
|
|
42
|
+
try:
|
|
43
|
+
import yaml
|
|
44
|
+
sys.stdout.write(yaml.dump(envelope, allow_unicode=True, default_flow_style=False, sort_keys=False))
|
|
45
|
+
except ImportError:
|
|
46
|
+
# fallback to json if pyyaml not installed
|
|
47
|
+
sys.stdout.write(json.dumps(envelope, ensure_ascii=False, indent=2) + "\n")
|
|
48
|
+
else:
|
|
49
|
+
sys.stdout.write(json.dumps(envelope, ensure_ascii=False, indent=2) + "\n")
|
dy_cli/utils/export.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
导出工具 — 将列表数据导出为 JSON / CSV 文件。
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import csv
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from dy_cli.utils.output import success, info
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def export_data(data: list[dict], output_path: str) -> None:
|
|
15
|
+
"""根据文件扩展名导出数据。"""
|
|
16
|
+
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
|
17
|
+
ext = os.path.splitext(output_path)[1].lower()
|
|
18
|
+
|
|
19
|
+
if ext == ".csv":
|
|
20
|
+
_export_csv(data, output_path)
|
|
21
|
+
elif ext in (".json", ".jsonl"):
|
|
22
|
+
_export_json(data, output_path)
|
|
23
|
+
elif ext in (".yaml", ".yml"):
|
|
24
|
+
_export_yaml(data, output_path)
|
|
25
|
+
else:
|
|
26
|
+
_export_json(data, output_path)
|
|
27
|
+
|
|
28
|
+
success(f"已导出 {len(data)} 条到 {output_path}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _export_json(data: list[dict], path: str) -> None:
|
|
32
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
33
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _export_csv(data: list[dict], path: str) -> None:
|
|
37
|
+
if not data:
|
|
38
|
+
return
|
|
39
|
+
# Flatten nested dicts for CSV
|
|
40
|
+
flat = [_flatten(item) for item in data]
|
|
41
|
+
keys = list(flat[0].keys())
|
|
42
|
+
with open(path, "w", newline="", encoding="utf-8-sig") as f:
|
|
43
|
+
writer = csv.DictWriter(f, fieldnames=keys, extrasaction="ignore")
|
|
44
|
+
writer.writeheader()
|
|
45
|
+
writer.writerows(flat)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _export_yaml(data: list[dict], path: str) -> None:
|
|
49
|
+
try:
|
|
50
|
+
import yaml
|
|
51
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
52
|
+
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
|
|
53
|
+
except ImportError:
|
|
54
|
+
_export_json(data, path)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _flatten(d: dict, prefix: str = "") -> dict:
|
|
58
|
+
"""将嵌套 dict 展平为单层 (用于 CSV)。"""
|
|
59
|
+
items: dict[str, Any] = {}
|
|
60
|
+
for k, v in d.items():
|
|
61
|
+
key = f"{prefix}{k}" if not prefix else f"{prefix}.{k}"
|
|
62
|
+
if isinstance(v, dict):
|
|
63
|
+
items.update(_flatten(v, key))
|
|
64
|
+
elif isinstance(v, list):
|
|
65
|
+
items[key] = str(v)[:100]
|
|
66
|
+
else:
|
|
67
|
+
items[key] = v
|
|
68
|
+
return items
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
短索引导航 — 列表命令自动缓存结果,后续命令用数字引用。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
dy search "美食" → 缓存搜索结果
|
|
6
|
+
dy read 1 → 读取第 1 条
|
|
7
|
+
dy download 3 → 下载第 3 条
|
|
8
|
+
dy detail 2 → 查看第 2 条详情
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from dy_cli.utils.config import CONFIG_DIR
|
|
17
|
+
|
|
18
|
+
INDEX_FILE = os.path.join(CONFIG_DIR, "index_cache.json")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def save_index(items: list[dict[str, str]]) -> None:
|
|
22
|
+
"""保存列表结果的索引。每条记录需要 aweme_id。"""
|
|
23
|
+
os.makedirs(os.path.dirname(INDEX_FILE), exist_ok=True)
|
|
24
|
+
entries = []
|
|
25
|
+
for item in items:
|
|
26
|
+
aweme_id = item.get("aweme_id", "")
|
|
27
|
+
if aweme_id:
|
|
28
|
+
entries.append({
|
|
29
|
+
"aweme_id": str(aweme_id),
|
|
30
|
+
"desc": item.get("desc", "")[:60],
|
|
31
|
+
"author": item.get("author", {}).get("nickname", "") if isinstance(item.get("author"), dict) else str(item.get("author", "")),
|
|
32
|
+
"sec_uid": item.get("author", {}).get("sec_uid", "") if isinstance(item.get("author"), dict) else "",
|
|
33
|
+
})
|
|
34
|
+
with open(INDEX_FILE, "w", encoding="utf-8") as f:
|
|
35
|
+
json.dump(entries, f, ensure_ascii=False, indent=2)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_by_index(index: int) -> dict[str, str] | None:
|
|
39
|
+
"""用 1-based 索引获取缓存条目。"""
|
|
40
|
+
if index <= 0:
|
|
41
|
+
return None
|
|
42
|
+
if not os.path.isfile(INDEX_FILE):
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
with open(INDEX_FILE, "r", encoding="utf-8") as f:
|
|
46
|
+
data = json.load(f)
|
|
47
|
+
if not isinstance(data, list) or index > len(data):
|
|
48
|
+
return None
|
|
49
|
+
return data[index - 1]
|
|
50
|
+
except (json.JSONDecodeError, OSError):
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def resolve_id(id_or_index: str) -> str:
|
|
55
|
+
"""解析 aweme_id: 短数字(≤999)视为索引,长数字视为 ID,含字母/URL 原样返回。"""
|
|
56
|
+
if not id_or_index.isdigit():
|
|
57
|
+
return id_or_index
|
|
58
|
+
|
|
59
|
+
n = int(id_or_index)
|
|
60
|
+
# 短索引: 1-999; aweme_id 通常 > 15 位
|
|
61
|
+
if n <= 999:
|
|
62
|
+
entry = get_by_index(n)
|
|
63
|
+
if entry:
|
|
64
|
+
return entry["aweme_id"]
|
|
65
|
+
# 索引不存在时给出明确提示,不要把 "1" 当 aweme_id
|
|
66
|
+
count = get_index_count()
|
|
67
|
+
if count == 0:
|
|
68
|
+
raise ValueError("没有缓存的搜索结果,请先执行 dy search")
|
|
69
|
+
raise ValueError(f"索引 {n} 超出范围 (共 {count} 条)")
|
|
70
|
+
|
|
71
|
+
return id_or_index
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_index_count() -> int:
|
|
75
|
+
"""获取缓存的条目数量。"""
|
|
76
|
+
if not os.path.isfile(INDEX_FILE):
|
|
77
|
+
return 0
|
|
78
|
+
try:
|
|
79
|
+
with open(INDEX_FILE, "r", encoding="utf-8") as f:
|
|
80
|
+
data = json.load(f)
|
|
81
|
+
return len(data) if isinstance(data, list) else 0
|
|
82
|
+
except (json.JSONDecodeError, OSError):
|
|
83
|
+
return 0
|