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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,12 @@
1
+ from . import config, photos, tags, albums, locations, people, folders, medias
2
+
3
+ __all__ = [
4
+ "config",
5
+ "photos",
6
+ "tags",
7
+ "albums",
8
+ "locations",
9
+ "people",
10
+ "folders",
11
+ "medias"
12
+ ]
@@ -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,2 @@
1
+ [console_scripts]
2
+ trailsnap = trailsnap.cli:main
@@ -0,0 +1 @@
1
+ trailsnap