trailsnap-cli 0.1.0__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.
- trailsnap_cli-0.1.0/PKG-INFO +81 -0
- trailsnap_cli-0.1.0/README.md +74 -0
- trailsnap_cli-0.1.0/pyproject.toml +17 -0
- trailsnap_cli-0.1.0/setup.cfg +4 -0
- trailsnap_cli-0.1.0/trailsnap/cli.py +43 -0
- trailsnap_cli-0.1.0/trailsnap/commands/__init__.py +12 -0
- trailsnap_cli-0.1.0/trailsnap/commands/albums.py +27 -0
- trailsnap_cli-0.1.0/trailsnap/commands/config.py +14 -0
- trailsnap_cli-0.1.0/trailsnap/commands/folders.py +17 -0
- trailsnap_cli-0.1.0/trailsnap/commands/locations.py +50 -0
- trailsnap_cli-0.1.0/trailsnap/commands/medias.py +50 -0
- trailsnap_cli-0.1.0/trailsnap/commands/people.py +35 -0
- trailsnap_cli-0.1.0/trailsnap/commands/photos.py +111 -0
- trailsnap_cli-0.1.0/trailsnap/commands/tags.py +24 -0
- trailsnap_cli-0.1.0/trailsnap/utils.py +86 -0
- trailsnap_cli-0.1.0/trailsnap_cli.egg-info/PKG-INFO +81 -0
- trailsnap_cli-0.1.0/trailsnap_cli.egg-info/SOURCES.txt +18 -0
- trailsnap_cli-0.1.0/trailsnap_cli.egg-info/dependency_links.txt +1 -0
- trailsnap_cli-0.1.0/trailsnap_cli.egg-info/entry_points.txt +2 -0
- trailsnap_cli-0.1.0/trailsnap_cli.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trailsnap-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TrailSnap Command Line Interface
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# TrailSnap CLI
|
|
9
|
+
|
|
10
|
+
Command Line Interface for TrailSnap.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Via pip
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install trailsnap-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Via npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g trailsnap-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Local Development / Testing
|
|
27
|
+
|
|
28
|
+
If you want to test or develop the CLI locally before publishing:
|
|
29
|
+
|
|
30
|
+
### 1. Test via Python (Recommended for Development)
|
|
31
|
+
|
|
32
|
+
Use pip's editable mode to install the package so your code changes take effect immediately without reinstalling:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd package/trailsnap-cli
|
|
36
|
+
pip install -e .
|
|
37
|
+
|
|
38
|
+
# Test if the command works
|
|
39
|
+
trailsnap --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Test via npm (Testing the Node.js Wrapper)
|
|
43
|
+
|
|
44
|
+
Since the npm package relies on downloading pre-built binaries from GitHub Releases (which don't exist locally), you must build the binary manually and skip the download script.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd package/trailsnap-cli
|
|
48
|
+
|
|
49
|
+
# Install pyinstaller
|
|
50
|
+
pip install pyinstaller
|
|
51
|
+
|
|
52
|
+
# Build the standalone executable
|
|
53
|
+
pyinstaller --onefile --name trailsnap --paths trailsnap trailsnap/cli.py
|
|
54
|
+
|
|
55
|
+
# Create bin directory and move the executable (Windows example)
|
|
56
|
+
mkdir bin
|
|
57
|
+
cp dist/trailsnap.exe bin/
|
|
58
|
+
# For Mac/Linux: cp dist/trailsnap bin/
|
|
59
|
+
|
|
60
|
+
# Install globally via npm, skipping the postinstall download script
|
|
61
|
+
npm install -g . --ignore-scripts
|
|
62
|
+
|
|
63
|
+
# Test if the wrapper works
|
|
64
|
+
trailsnap --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To uninstall local versions:
|
|
68
|
+
- `pip uninstall trailsnap-cli`
|
|
69
|
+
- `npm uninstall -g trailsnap-cli`
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
trailsnap --help
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
获取TrailSnap的API URL和Token
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
trailsnap config set --url <url> --token <token>
|
|
81
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# TrailSnap CLI
|
|
2
|
+
|
|
3
|
+
Command Line Interface for TrailSnap.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Via pip
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install trailsnap-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Via npm
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g trailsnap-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Local Development / Testing
|
|
20
|
+
|
|
21
|
+
If you want to test or develop the CLI locally before publishing:
|
|
22
|
+
|
|
23
|
+
### 1. Test via Python (Recommended for Development)
|
|
24
|
+
|
|
25
|
+
Use pip's editable mode to install the package so your code changes take effect immediately without reinstalling:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd package/trailsnap-cli
|
|
29
|
+
pip install -e .
|
|
30
|
+
|
|
31
|
+
# Test if the command works
|
|
32
|
+
trailsnap --help
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2. Test via npm (Testing the Node.js Wrapper)
|
|
36
|
+
|
|
37
|
+
Since the npm package relies on downloading pre-built binaries from GitHub Releases (which don't exist locally), you must build the binary manually and skip the download script.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd package/trailsnap-cli
|
|
41
|
+
|
|
42
|
+
# Install pyinstaller
|
|
43
|
+
pip install pyinstaller
|
|
44
|
+
|
|
45
|
+
# Build the standalone executable
|
|
46
|
+
pyinstaller --onefile --name trailsnap --paths trailsnap trailsnap/cli.py
|
|
47
|
+
|
|
48
|
+
# Create bin directory and move the executable (Windows example)
|
|
49
|
+
mkdir bin
|
|
50
|
+
cp dist/trailsnap.exe bin/
|
|
51
|
+
# For Mac/Linux: cp dist/trailsnap bin/
|
|
52
|
+
|
|
53
|
+
# Install globally via npm, skipping the postinstall download script
|
|
54
|
+
npm install -g . --ignore-scripts
|
|
55
|
+
|
|
56
|
+
# Test if the wrapper works
|
|
57
|
+
trailsnap --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To uninstall local versions:
|
|
61
|
+
- `pip uninstall trailsnap-cli`
|
|
62
|
+
- `npm uninstall -g trailsnap-cli`
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
trailsnap --help
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
获取TrailSnap的API URL和Token
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
trailsnap config set --url <url> --token <token>
|
|
74
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trailsnap-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TrailSnap Command Line Interface"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = []
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
trailsnap = "trailsnap.cli:main"
|
|
15
|
+
|
|
16
|
+
[tool.setuptools]
|
|
17
|
+
packages = ["trailsnap", "trailsnap.commands"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
7
|
+
|
|
8
|
+
from commands import config, photos, tags, albums, locations, people, folders, medias
|
|
9
|
+
|
|
10
|
+
VERSION = "0.1.0"
|
|
11
|
+
|
|
12
|
+
def main():
|
|
13
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
14
|
+
sys.stdout.reconfigure(errors="replace")
|
|
15
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
16
|
+
sys.stderr.reconfigure(errors="replace")
|
|
17
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
18
|
+
parser = argparse.ArgumentParser(description="TrailSnap CLI 命令行工具")
|
|
19
|
+
subparsers = parser.add_subparsers(dest="command", help="可用命令")
|
|
20
|
+
subparsers.required = True
|
|
21
|
+
|
|
22
|
+
parser.add_argument("-v", "--version", action="version", version=VERSION)
|
|
23
|
+
|
|
24
|
+
# 注册各个子命令
|
|
25
|
+
config.setup_parser(subparsers)
|
|
26
|
+
photos.setup_parser(subparsers)
|
|
27
|
+
tags.setup_parser(subparsers)
|
|
28
|
+
albums.setup_parser(subparsers)
|
|
29
|
+
locations.setup_parser(subparsers)
|
|
30
|
+
people.setup_parser(subparsers)
|
|
31
|
+
folders.setup_parser(subparsers)
|
|
32
|
+
medias.setup_parser(subparsers)
|
|
33
|
+
|
|
34
|
+
args = parser.parse_args()
|
|
35
|
+
|
|
36
|
+
# 执行对应的命令处理函数
|
|
37
|
+
if hasattr(args, "func"):
|
|
38
|
+
args.func(args)
|
|
39
|
+
else:
|
|
40
|
+
parser.print_help()
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("albums", help="管理和查询相册")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
list_parser = sub_subparsers.add_parser("list", help="查询相册列表")
|
|
10
|
+
list_parser.add_argument("--skip", type=int, default=0, help="跳过 N 张相册")
|
|
11
|
+
list_parser.add_argument("--limit", type=int, default=100, help="限制返回 N 张相册")
|
|
12
|
+
list_parser.set_defaults(func=execute_list)
|
|
13
|
+
|
|
14
|
+
def execute_list(args):
|
|
15
|
+
data = make_request("/albums", {"skip": args.skip, "limit": args.limit})
|
|
16
|
+
if data:
|
|
17
|
+
albums = [{
|
|
18
|
+
"id": album["id"],
|
|
19
|
+
"name": album["name"],
|
|
20
|
+
"count": album["num_photos"],
|
|
21
|
+
"description": album["description"],
|
|
22
|
+
"condition": album["condition"],
|
|
23
|
+
"type": album["type"]
|
|
24
|
+
} for album in data]
|
|
25
|
+
print(json.dumps(albums, indent=2, ensure_ascii=False))
|
|
26
|
+
else:
|
|
27
|
+
print("未查询到相册记录")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from utils import save_env
|
|
2
|
+
|
|
3
|
+
def setup_parser(subparsers):
|
|
4
|
+
parser = subparsers.add_parser("config", help="配置 CLI")
|
|
5
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
6
|
+
sub_subparsers.required = True
|
|
7
|
+
|
|
8
|
+
set_parser = sub_subparsers.add_parser("set", help="配置 API URL 和 Token")
|
|
9
|
+
set_parser.add_argument("--url", help="API 基础地址 (例如: http://localhost:8000)", required=True)
|
|
10
|
+
set_parser.add_argument("--token", help="API Token (Bearer 凭证)", required=True)
|
|
11
|
+
set_parser.set_defaults(func=execute_set)
|
|
12
|
+
|
|
13
|
+
def execute_set(args):
|
|
14
|
+
save_env(args.url, args.token)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("folders", help="管理和查询挂载的存储目录")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
list_parser = sub_subparsers.add_parser("list", help="查询挂载的存储目录")
|
|
10
|
+
list_parser.set_defaults(func=execute_list)
|
|
11
|
+
|
|
12
|
+
def execute_list(args):
|
|
13
|
+
data = make_request("/settings/directories")
|
|
14
|
+
if data is not None:
|
|
15
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
16
|
+
else:
|
|
17
|
+
print("未查询到存储目录信息")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("locations", help="管理和查询位置")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
list_parser = sub_subparsers.add_parser("list", help="查询位置分布,不含时间信息(地点名,照片数量)")
|
|
10
|
+
list_parser.add_argument("--level", choices=["city", "province", "district", "scene"], default="city", help="分组级别,默认 city, 可选值:city,province,district,scene(5A景区) 中的一个")
|
|
11
|
+
list_parser.add_argument("--skip", type=int, default=0, help="跳过 N 个位置")
|
|
12
|
+
list_parser.add_argument("--limit", type=int, default=100, help="限制返回 N 个位置")
|
|
13
|
+
# start_date, end_date 过滤时间范围
|
|
14
|
+
list_parser.add_argument("--start-date", help="可选,开始日期,格式 YYYY-MM-DD")
|
|
15
|
+
list_parser.add_argument("--end-date", help="可选,结束日期,格式 YYYY-MM-DD")
|
|
16
|
+
list_parser.set_defaults(func=execute_list)
|
|
17
|
+
|
|
18
|
+
timeline_parser = sub_subparsers.add_parser("timeline", help="查询足迹时间轴列表,按时间和地点分组(开始日期,结束日期,地点名,照片数量)")
|
|
19
|
+
timeline_parser.add_argument("--level", choices=["city", "province", "district", "scene"], default="city", help="分组级别,默认 city, 可选值:city,province,district,scene(5A景区) 中的一个")
|
|
20
|
+
timeline_parser.add_argument("--skip", type=int, default=0, help="跳过 N 个位置")
|
|
21
|
+
timeline_parser.add_argument("--limit", type=int, default=100, help="限制返回 N 个位置")
|
|
22
|
+
# start_date, end_date 过滤时间范围
|
|
23
|
+
timeline_parser.add_argument("--start-date", help="可选,开始日期,格式 YYYY-MM-DD")
|
|
24
|
+
timeline_parser.add_argument("--end-date", help="可选,结束日期,格式 YYYY-MM-DD")
|
|
25
|
+
timeline_parser.set_defaults(func=execute_timeline)
|
|
26
|
+
|
|
27
|
+
def execute_timeline(args):
|
|
28
|
+
data = make_request("/locations/timeline", {"start_date": args.start_date, "end_date": args.end_date, "skip": args.skip, "limit": args.limit, "level": args.level})
|
|
29
|
+
if data:
|
|
30
|
+
timelines = [{
|
|
31
|
+
"startDate": timeline["startDate"],
|
|
32
|
+
"endDate": timeline["endDate"],
|
|
33
|
+
"locationName": timeline["locationName"],
|
|
34
|
+
"count": timeline["photoCount"]
|
|
35
|
+
} for timeline in data["nodes"]]
|
|
36
|
+
print(json.dumps(timelines, indent=2, ensure_ascii=False))
|
|
37
|
+
else:
|
|
38
|
+
print("未查询到位置足迹时间轴数据")
|
|
39
|
+
|
|
40
|
+
def execute_list(args):
|
|
41
|
+
data = make_request("/locations", {"level": args.level, "skip": args.skip, "limit": args.limit, "start_date": args.start_date, "end_date": args.end_date})
|
|
42
|
+
if data:
|
|
43
|
+
# 映射字段并添加 count 字段
|
|
44
|
+
locations = [{
|
|
45
|
+
"name": location["name"],
|
|
46
|
+
"count": location["count"]
|
|
47
|
+
} for location in data]
|
|
48
|
+
print(json.dumps(locations, indent=2, ensure_ascii=False))
|
|
49
|
+
else:
|
|
50
|
+
print("未查询到位置记录")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from utils import make_request,load_env
|
|
4
|
+
|
|
5
|
+
def setup_parser(subparsers):
|
|
6
|
+
parser = subparsers.add_parser("medias", help="获取和管理媒体文件")
|
|
7
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
8
|
+
sub_subparsers.required = True
|
|
9
|
+
|
|
10
|
+
get_parser = sub_subparsers.add_parser("get", help="获取媒体文件")
|
|
11
|
+
get_parser.add_argument("--photo-id", type=str, default=100, help="照片ID")
|
|
12
|
+
get_parser.add_argument("--size", type=str, default="medium", help="照片质量,默认 medium,可选值:small,medium,large")
|
|
13
|
+
# 输出格式,默认 URL
|
|
14
|
+
get_parser.add_argument("--format", type=str, default="url", help="输出格式,默认 URL,可选值:url,base64,file")
|
|
15
|
+
# 输出文件路径,默认不保存
|
|
16
|
+
get_parser.add_argument("--output", type=str, default=None, help="输出文件路径,默认不保存,仅当format为file时有效")
|
|
17
|
+
get_parser.set_defaults(func=execute_get)
|
|
18
|
+
|
|
19
|
+
def execute_get(args):
|
|
20
|
+
env = load_env()
|
|
21
|
+
photo_id = args.photo_id
|
|
22
|
+
size = args.size
|
|
23
|
+
if size not in ["small", "medium", "large"]:
|
|
24
|
+
print("错误:size参数值必须为 small,medium,large 中的一个")
|
|
25
|
+
return
|
|
26
|
+
format = args.format
|
|
27
|
+
if format not in ["url", "base64", "file"]:
|
|
28
|
+
print("错误:format参数值必须为 url,base64,file 中的一个")
|
|
29
|
+
return
|
|
30
|
+
output = args.output
|
|
31
|
+
if format == "url":
|
|
32
|
+
base_url = env.get("TRAILSNAP_API_URL", "")
|
|
33
|
+
if not base_url:
|
|
34
|
+
print("错误:TRAILSNAP_API_URL环境变量未设置")
|
|
35
|
+
return
|
|
36
|
+
if size == 'large':
|
|
37
|
+
print(base_url + f"/medias/{photo_id}/file")
|
|
38
|
+
else:
|
|
39
|
+
print(base_url + f"/medias/{photo_id}/thumbnail?size={size}")
|
|
40
|
+
elif format == "base64":
|
|
41
|
+
data = make_request(f"/medias/{photo_id}/thumbnail?size={size}&format=base64")
|
|
42
|
+
print(data["base64"])
|
|
43
|
+
elif format == "file":
|
|
44
|
+
if not output:
|
|
45
|
+
print("错误:output参数值不能为空")
|
|
46
|
+
return
|
|
47
|
+
data = make_request(f"/medias/{photo_id}/file", method="GET", response_type="bytes")
|
|
48
|
+
with open(output, "wb") as f:
|
|
49
|
+
f.write(data)
|
|
50
|
+
print(f"将文件保存到 {output}")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("people", help="管理和查询人物(面部识别)")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
list_parser = sub_subparsers.add_parser("list", help="查询人物列表")
|
|
10
|
+
list_parser.add_argument("--limit", type=int, default=100, help="返回的记录数,默认 100")
|
|
11
|
+
# types: named,unnamed,hidden(允许多个值)
|
|
12
|
+
list_parser.add_argument("--types", type=str, default="named", help="查询类型,默认 named, 可选值:named,unnamed,hidden 中的一个或多个,逗号分隔")
|
|
13
|
+
list_parser.set_defaults(func=execute_list)
|
|
14
|
+
|
|
15
|
+
def execute_list(args):
|
|
16
|
+
# 验证types参数值是否有效
|
|
17
|
+
valid_types = {"named", "unnamed", "hidden"}
|
|
18
|
+
if not set(args.types.split(",")) <= valid_types:
|
|
19
|
+
print("错误:types参数值必须为 named,unnamed,hidden 中的一个或多个")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
# 转换types参数值为列表
|
|
23
|
+
args.types = args.types.split(",")
|
|
24
|
+
data = make_request("/faces/identities", {"page": 1, "limit": args.limit, "types": args.types})
|
|
25
|
+
if data:
|
|
26
|
+
identities = [{
|
|
27
|
+
"id": identity["id"],
|
|
28
|
+
"name": identity["identity_name"],
|
|
29
|
+
"tags": identity["tags"],
|
|
30
|
+
"description": identity["description"],
|
|
31
|
+
"face_count": identity["face_count"]
|
|
32
|
+
} for identity in data]
|
|
33
|
+
print(json.dumps(identities, indent=2, ensure_ascii=False))
|
|
34
|
+
else:
|
|
35
|
+
print("未查询到人物记录")
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request,load_env
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("photos", help="管理和查询照片")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
# list subcommand
|
|
10
|
+
list_parser = sub_subparsers.add_parser("list", help="查询照片列表")
|
|
11
|
+
list_parser.add_argument("--skip", type=int, default=0, help="跳过 N 张照片")
|
|
12
|
+
list_parser.add_argument("--limit", type=int, default=10, help="限制返回 N 张照片")
|
|
13
|
+
list_parser.add_argument("--order_by", type=str, default="memory_score", help="排序字段,默认按值得回忆评分排序,可选值:quality_score,memory_score,photo_time")
|
|
14
|
+
list_parser.add_argument("--image-type", help="按图片类型过滤照片,多个类型用逗号分隔,可选值:Camera,Screenshot,Other")
|
|
15
|
+
# start_time 过滤照片
|
|
16
|
+
list_parser.add_argument("--start-time", help="按开始时间过滤照片,格式为 YYYY-MM-DD HH:MM:SS")
|
|
17
|
+
# end_time 过滤照片
|
|
18
|
+
list_parser.add_argument("--end-time", help="按结束时间过滤照片,格式为 YYYY-MM-DD HH:MM:SS")
|
|
19
|
+
list_parser.add_argument("--album-id", help="按相册 ID 过滤,多个 ID 用逗号分隔")
|
|
20
|
+
list_parser.add_argument("--people-id", help="按人物 ID 过滤,多个 ID 用逗号分隔")
|
|
21
|
+
list_parser.add_argument("--tag-id", help="按标签 ID 过滤,多个 ID 用逗号分隔")
|
|
22
|
+
list_parser.add_argument("--city", help="按城市过滤,多个城市用逗号分隔")
|
|
23
|
+
list_parser.add_argument("--province", help="按省份过滤,多个省份用逗号分隔")
|
|
24
|
+
list_parser.add_argument("--scene", help="按景区过滤,多个景区用逗号分隔")
|
|
25
|
+
list_parser.add_argument("--make", help="按相机品牌过滤,多个品牌用逗号分隔")
|
|
26
|
+
list_parser.add_argument("--model", help="按相机型号过滤,多个型号用逗号分隔")
|
|
27
|
+
list_parser.set_defaults(func=execute_list)
|
|
28
|
+
|
|
29
|
+
# info subcommand
|
|
30
|
+
info_parser = sub_subparsers.add_parser("info", help="获取单张照片信息")
|
|
31
|
+
info_parser.add_argument("--photo-id", required=True, help="照片ID")
|
|
32
|
+
info_parser.set_defaults(func=execute_info)
|
|
33
|
+
|
|
34
|
+
# delete subcommand
|
|
35
|
+
delete_parser = sub_subparsers.add_parser("delete", help="删除照片")
|
|
36
|
+
delete_parser.add_argument("--photo-id", required=True, help="照片ID")
|
|
37
|
+
delete_parser.set_defaults(func=execute_delete)
|
|
38
|
+
|
|
39
|
+
def execute_list(args):
|
|
40
|
+
params = {
|
|
41
|
+
"skip": args.skip,
|
|
42
|
+
"limit": args.limit,
|
|
43
|
+
"start_time": args.start_time,
|
|
44
|
+
"end_time": args.end_time,
|
|
45
|
+
"image_types": args.image_type.split(",") if args.image_type else [],
|
|
46
|
+
"scenes": args.scene.split(",") if args.scene else [],
|
|
47
|
+
"album_ids": args.album_id.split(",") if args.album_id else [],
|
|
48
|
+
"cities": args.city.split(",") if args.city else [],
|
|
49
|
+
"provinces": args.province.split(",") if args.province else [],
|
|
50
|
+
"makes": args.make.split(",") if args.make else [],
|
|
51
|
+
"models": args.model.split(",") if args.model else [],
|
|
52
|
+
"face_ids": args.people_id.split(",") if args.people_id else [],
|
|
53
|
+
"tag_ids": args.tag_id.split(",") if args.tag_id else [],
|
|
54
|
+
"order_by": args.order_by
|
|
55
|
+
}
|
|
56
|
+
data = make_request("/photos/detail", params)
|
|
57
|
+
env = load_env()
|
|
58
|
+
base_url = env.get("TRAILSNAP_API_URL", "")
|
|
59
|
+
|
|
60
|
+
if data:
|
|
61
|
+
photos = []
|
|
62
|
+
for photo in data:
|
|
63
|
+
metadata = photo.get("metadata_info", {})
|
|
64
|
+
if not metadata:
|
|
65
|
+
metadata = {}
|
|
66
|
+
image_description = photo.get("image_description", {})
|
|
67
|
+
if not image_description:
|
|
68
|
+
image_description = {}
|
|
69
|
+
photos.append({
|
|
70
|
+
"id": photo["id"],
|
|
71
|
+
"url": f"{base_url}/medias/{photo['id']}/file",
|
|
72
|
+
"filename": photo["filename"],
|
|
73
|
+
"file_type": photo["file_type"],
|
|
74
|
+
"photo_time": photo["photo_time"],
|
|
75
|
+
"address": metadata.get("address", ""),
|
|
76
|
+
"description": {
|
|
77
|
+
"description": image_description.get("description", ""),
|
|
78
|
+
"tags": image_description.get("tags", []),
|
|
79
|
+
"memory_score": image_description.get("memory_score", 0),
|
|
80
|
+
"quality_score": image_description.get("quality_score", 0),
|
|
81
|
+
"narrative": image_description.get("narrative", "")
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
print(json.dumps(photos, indent=2, ensure_ascii=False))
|
|
85
|
+
else:
|
|
86
|
+
print("没有查询到照片列表")
|
|
87
|
+
|
|
88
|
+
def execute_info(args):
|
|
89
|
+
data = make_request(f"/photos/{args.photo_id}/metadata")
|
|
90
|
+
description_data = make_request(f"/photos/{args.photo_id}/description")
|
|
91
|
+
if not description_data:
|
|
92
|
+
description_data = {}
|
|
93
|
+
if data:
|
|
94
|
+
info = {
|
|
95
|
+
# "file_path": data["file_path"],
|
|
96
|
+
"address": data["address"],
|
|
97
|
+
"albums": data["albums"],
|
|
98
|
+
"tags": data["tags"],
|
|
99
|
+
"faces_identities": data["faces_identities"],
|
|
100
|
+
"description": description_data
|
|
101
|
+
}
|
|
102
|
+
print(json.dumps(info, indent=2, ensure_ascii=False))
|
|
103
|
+
else:
|
|
104
|
+
print("未查询到照片信息")
|
|
105
|
+
|
|
106
|
+
def execute_delete(args):
|
|
107
|
+
data = make_request(f"/photos/{args.photo_id}", method="DELETE")
|
|
108
|
+
if data:
|
|
109
|
+
print(f"照片 {args.photo_id} 删除成功")
|
|
110
|
+
else:
|
|
111
|
+
print("照片删除失败或不存在")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from utils import make_request
|
|
3
|
+
|
|
4
|
+
def setup_parser(subparsers):
|
|
5
|
+
parser = subparsers.add_parser("tags", help="管理和查询分类标签")
|
|
6
|
+
sub_subparsers = parser.add_subparsers(dest="subcommand", help="可用操作")
|
|
7
|
+
sub_subparsers.required = True
|
|
8
|
+
|
|
9
|
+
list_parser = sub_subparsers.add_parser("list", help="查询分类标签")
|
|
10
|
+
list_parser.add_argument("--skip", type=int, default=0, help="跳过 N 个记录")
|
|
11
|
+
list_parser.add_argument("--limit", type=int, default=100, help="限制返回 N 个记录")
|
|
12
|
+
list_parser.set_defaults(func=execute_list)
|
|
13
|
+
|
|
14
|
+
def execute_list(args):
|
|
15
|
+
data = make_request("/tags", {"skip": args.skip, "limit": args.limit})
|
|
16
|
+
if data:
|
|
17
|
+
tags = [{
|
|
18
|
+
"id": tag["id"],
|
|
19
|
+
"name": tag["tag_name"],
|
|
20
|
+
"count": tag["count"]
|
|
21
|
+
} for tag in data]
|
|
22
|
+
print(json.dumps(tags, indent=2, ensure_ascii=False))
|
|
23
|
+
else:
|
|
24
|
+
print("未查询到分类标签记录")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import urllib.request
|
|
3
|
+
from urllib.error import URLError, HTTPError
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
# 获取用户目录(永久目录,不会消失)
|
|
11
|
+
if os.name == "nt": # Windows
|
|
12
|
+
CONFIG_DIR = Path(os.getenv("APPDATA")) / "trailsnap"
|
|
13
|
+
else: # Mac/Linux
|
|
14
|
+
CONFIG_DIR = Path.home() / ".config" / "trailsnap"
|
|
15
|
+
|
|
16
|
+
# 确保目录存在
|
|
17
|
+
CONFIG_DIR.mkdir(exist_ok=True)
|
|
18
|
+
|
|
19
|
+
# .env 配置文件 永久保存在这里
|
|
20
|
+
ENV_FILE = CONFIG_DIR / ".env"
|
|
21
|
+
|
|
22
|
+
def load_env():
|
|
23
|
+
if not ENV_FILE.exists():
|
|
24
|
+
return {}
|
|
25
|
+
env = {}
|
|
26
|
+
with open(ENV_FILE, "r", encoding="utf-8") as f:
|
|
27
|
+
for line in f:
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if line and not line.startswith("#") and "=" in line:
|
|
30
|
+
key, val = line.split("=", 1)
|
|
31
|
+
env[key.strip()] = val.strip()
|
|
32
|
+
return env
|
|
33
|
+
|
|
34
|
+
def save_env(url, token):
|
|
35
|
+
with open(ENV_FILE, "w", encoding="utf-8") as f:
|
|
36
|
+
f.write(f"TRAILSNAP_API_URL={url}\n")
|
|
37
|
+
f.write(f"TRAILSNAP_API_TOKEN={token}\n")
|
|
38
|
+
print(f"配置已保存到 {ENV_FILE}")
|
|
39
|
+
|
|
40
|
+
def make_request(endpoint, params=None, method="GET", response_type="json"):
|
|
41
|
+
env = load_env()
|
|
42
|
+
base_url = env.get("TRAILSNAP_API_URL")
|
|
43
|
+
token = env.get("TRAILSNAP_API_TOKEN")
|
|
44
|
+
|
|
45
|
+
if not base_url or not token:
|
|
46
|
+
print("错误: API URL 和 Token 未配置,请先运行 'config' 命令。")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
url = f"{base_url.rstrip('/')}{endpoint}"
|
|
50
|
+
if params:
|
|
51
|
+
# 过滤掉 None 值
|
|
52
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
53
|
+
if params:
|
|
54
|
+
query_string = urlencode(params, doseq=True)
|
|
55
|
+
url = f"{url}?{query_string}"
|
|
56
|
+
|
|
57
|
+
headers = {
|
|
58
|
+
"Authorization": f"Bearer {token}",
|
|
59
|
+
"Content-Type": "application/json"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
req = urllib.request.Request(url, headers=headers)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
with urllib.request.urlopen(req) as response:
|
|
66
|
+
if response_type == "json":
|
|
67
|
+
return json.loads(response.read().decode("utf-8"))
|
|
68
|
+
elif response_type == "text":
|
|
69
|
+
return response.read().decode("utf-8")
|
|
70
|
+
if response_type == "bytes":
|
|
71
|
+
return response.read()
|
|
72
|
+
else:
|
|
73
|
+
print(f"未知的响应类型: {response_type}")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
return response.read().decode("utf-8")
|
|
76
|
+
except HTTPError as e:
|
|
77
|
+
print(f"HTTP 错误: {e.code} - {e.read().decode('utf-8')}")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
except URLError as e:
|
|
80
|
+
print(f"URL 错误: {e.reason}")
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
print(f"错误: {str(e)}")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
load_env()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trailsnap-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TrailSnap Command Line Interface
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
|
|
8
|
+
# TrailSnap CLI
|
|
9
|
+
|
|
10
|
+
Command Line Interface for TrailSnap.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Via pip
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install trailsnap-cli
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Via npm
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g trailsnap-cli
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Local Development / Testing
|
|
27
|
+
|
|
28
|
+
If you want to test or develop the CLI locally before publishing:
|
|
29
|
+
|
|
30
|
+
### 1. Test via Python (Recommended for Development)
|
|
31
|
+
|
|
32
|
+
Use pip's editable mode to install the package so your code changes take effect immediately without reinstalling:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd package/trailsnap-cli
|
|
36
|
+
pip install -e .
|
|
37
|
+
|
|
38
|
+
# Test if the command works
|
|
39
|
+
trailsnap --help
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 2. Test via npm (Testing the Node.js Wrapper)
|
|
43
|
+
|
|
44
|
+
Since the npm package relies on downloading pre-built binaries from GitHub Releases (which don't exist locally), you must build the binary manually and skip the download script.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd package/trailsnap-cli
|
|
48
|
+
|
|
49
|
+
# Install pyinstaller
|
|
50
|
+
pip install pyinstaller
|
|
51
|
+
|
|
52
|
+
# Build the standalone executable
|
|
53
|
+
pyinstaller --onefile --name trailsnap --paths trailsnap trailsnap/cli.py
|
|
54
|
+
|
|
55
|
+
# Create bin directory and move the executable (Windows example)
|
|
56
|
+
mkdir bin
|
|
57
|
+
cp dist/trailsnap.exe bin/
|
|
58
|
+
# For Mac/Linux: cp dist/trailsnap bin/
|
|
59
|
+
|
|
60
|
+
# Install globally via npm, skipping the postinstall download script
|
|
61
|
+
npm install -g . --ignore-scripts
|
|
62
|
+
|
|
63
|
+
# Test if the wrapper works
|
|
64
|
+
trailsnap --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
To uninstall local versions:
|
|
68
|
+
- `pip uninstall trailsnap-cli`
|
|
69
|
+
- `npm uninstall -g trailsnap-cli`
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
trailsnap --help
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
获取TrailSnap的API URL和Token
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
trailsnap config set --url <url> --token <token>
|
|
81
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
trailsnap/cli.py
|
|
4
|
+
trailsnap/utils.py
|
|
5
|
+
trailsnap/commands/__init__.py
|
|
6
|
+
trailsnap/commands/albums.py
|
|
7
|
+
trailsnap/commands/config.py
|
|
8
|
+
trailsnap/commands/folders.py
|
|
9
|
+
trailsnap/commands/locations.py
|
|
10
|
+
trailsnap/commands/medias.py
|
|
11
|
+
trailsnap/commands/people.py
|
|
12
|
+
trailsnap/commands/photos.py
|
|
13
|
+
trailsnap/commands/tags.py
|
|
14
|
+
trailsnap_cli.egg-info/PKG-INFO
|
|
15
|
+
trailsnap_cli.egg-info/SOURCES.txt
|
|
16
|
+
trailsnap_cli.egg-info/dependency_links.txt
|
|
17
|
+
trailsnap_cli.egg-info/entry_points.txt
|
|
18
|
+
trailsnap_cli.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
trailsnap
|