TikLocal 0.5.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.
tiklocal-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 ChanMo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: TikLocal
3
+ Version: 0.5.0
4
+ Summary: A local media server that combines the features of TikTok and Pinterest
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: tiklocal,tiktok,douyin,jellyfin,vlc
8
+ Author: ChanMo
9
+ Author-email: chan.mo@outlook.com
10
+ Requires-Python: >=3.10,<4.0
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Dist: flask (>=3.1.0,<4.0.0)
19
+ Requires-Dist: pyyaml (>=6.0,<7.0)
20
+ Requires-Dist: waitress (>=3.0.2,<4.0.0)
21
+ Project-URL: Homepage, https://github.com/ChanMo/TikLocal
22
+ Project-URL: Repository, https://github.com/ChanMo/TikLocal
23
+ Description-Content-Type: text/markdown
24
+
25
+ # TikLocal
26
+
27
+ **TikLocal** is a **mobile and tablet** **web application** built on **Flask**. It allows you to browse and manage your local videos and images in a way similar to TikTok and Pinterest.
28
+
29
+ [中文](./README_zh.md)
30
+
31
+ ## Introduction
32
+
33
+ TikLocal's main features include:
34
+
35
+ * **A TikTok-like swipe-up browsing experience** that allows you to easily and quickly browse local video files.
36
+ * **A file manager-like directory browsing** feature that allows you to easily find and manage local video files.
37
+ * **A Pinterest-like grid layout** feature that allows you to enjoy local images.
38
+ * **Support for light and dark modes** to suit your personal preferences.
39
+
40
+ ## Use cases
41
+
42
+ TikLocal is suitable for the following use cases:
43
+
44
+ * You don't trust TikTok's teen mode and want to provide your child with completely controllable video content.
45
+ * You want to browse and manage your local videos and images locally, but don't want to use third-party cloud services.
46
+ * You want to use a TikTok-style video browsing experience on your phone or tablet.
47
+ * You want to use a Pinterest-style image browsing experience on your phone or tablet.
48
+
49
+ ## How to use
50
+
51
+ ### Installation
52
+
53
+ TikLocal is a Python application that you can install using the following command:
54
+
55
+ ```
56
+ pip install tiklocal
57
+ ```
58
+
59
+ ### Usage
60
+
61
+ Starting TikLocal is very simple, just run the following command:
62
+
63
+ ```
64
+ tiklocal ~/Videos/
65
+ ```
66
+
67
+ You can specify any media folder.
68
+
69
+ To close, press `Ctrl + C`.
70
+
71
+ ### Configuration
72
+
73
+ TikLocal provides some configuration options that you can adjust to your needs.
74
+
75
+ * **Light and dark modes:** You can choose to use light or dark mode.
76
+ * **Video playback speed:** You can adjust the video playback speed.
77
+
78
+
79
+ ## TODO
80
+
81
+ * [ ] Add search
82
+ * [ ] Add more management operations, such as moving files and creating folders
83
+ * [ ] Add basic login control
84
+ * [ ] Add a bookmarking feature
85
+ * [ ] Add a Docker image
86
+ * [ ] Add a tagging feature
87
+ * [ ] Use recommendation algorithms
88
+
89
+ ## Contribution
90
+
91
+ TikLocal is an open source project that you can contribute to in the following ways:
92
+
93
+ * Submit code or documentation improvements.
94
+ * Report bugs.
95
+ * Suggest new features.
96
+
97
+ ## Contact us
98
+
99
+ If you have any questions or suggestions, you can contact us in the following ways:
100
+
101
+ * GitHub project page: [https://github.com/ChanMo/TikLocal/](https://github.com/ChanMo/TikLocal/)
102
+ * Email: [chan.mo@outlook.com]
103
+
@@ -0,0 +1,78 @@
1
+ # TikLocal
2
+
3
+ **TikLocal** is a **mobile and tablet** **web application** built on **Flask**. It allows you to browse and manage your local videos and images in a way similar to TikTok and Pinterest.
4
+
5
+ [中文](./README_zh.md)
6
+
7
+ ## Introduction
8
+
9
+ TikLocal's main features include:
10
+
11
+ * **A TikTok-like swipe-up browsing experience** that allows you to easily and quickly browse local video files.
12
+ * **A file manager-like directory browsing** feature that allows you to easily find and manage local video files.
13
+ * **A Pinterest-like grid layout** feature that allows you to enjoy local images.
14
+ * **Support for light and dark modes** to suit your personal preferences.
15
+
16
+ ## Use cases
17
+
18
+ TikLocal is suitable for the following use cases:
19
+
20
+ * You don't trust TikTok's teen mode and want to provide your child with completely controllable video content.
21
+ * You want to browse and manage your local videos and images locally, but don't want to use third-party cloud services.
22
+ * You want to use a TikTok-style video browsing experience on your phone or tablet.
23
+ * You want to use a Pinterest-style image browsing experience on your phone or tablet.
24
+
25
+ ## How to use
26
+
27
+ ### Installation
28
+
29
+ TikLocal is a Python application that you can install using the following command:
30
+
31
+ ```
32
+ pip install tiklocal
33
+ ```
34
+
35
+ ### Usage
36
+
37
+ Starting TikLocal is very simple, just run the following command:
38
+
39
+ ```
40
+ tiklocal ~/Videos/
41
+ ```
42
+
43
+ You can specify any media folder.
44
+
45
+ To close, press `Ctrl + C`.
46
+
47
+ ### Configuration
48
+
49
+ TikLocal provides some configuration options that you can adjust to your needs.
50
+
51
+ * **Light and dark modes:** You can choose to use light or dark mode.
52
+ * **Video playback speed:** You can adjust the video playback speed.
53
+
54
+
55
+ ## TODO
56
+
57
+ * [ ] Add search
58
+ * [ ] Add more management operations, such as moving files and creating folders
59
+ * [ ] Add basic login control
60
+ * [ ] Add a bookmarking feature
61
+ * [ ] Add a Docker image
62
+ * [ ] Add a tagging feature
63
+ * [ ] Use recommendation algorithms
64
+
65
+ ## Contribution
66
+
67
+ TikLocal is an open source project that you can contribute to in the following ways:
68
+
69
+ * Submit code or documentation improvements.
70
+ * Report bugs.
71
+ * Suggest new features.
72
+
73
+ ## Contact us
74
+
75
+ If you have any questions or suggestions, you can contact us in the following ways:
76
+
77
+ * GitHub project page: [https://github.com/ChanMo/TikLocal/](https://github.com/ChanMo/TikLocal/)
78
+ * Email: [chan.mo@outlook.com]
@@ -0,0 +1,28 @@
1
+ [tool.poetry]
2
+ name = "TikLocal"
3
+ version = "0.5.0"
4
+ description = "A local media server that combines the features of TikTok and Pinterest"
5
+ authors = ["ChanMo <chan.mo@outlook.com>"]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ homepage = "https://github.com/ChanMo/TikLocal"
9
+ repository = "https://github.com/ChanMo/TikLocal"
10
+ keywords = ["tiklocal", "tiktok", "douyin", "jellyfin", "vlc"]
11
+
12
+ [tool.poetry.scripts]
13
+ tiklocal = 'tiklocal.run:main'
14
+
15
+ [tool.poetry.dependencies]
16
+ python = ">=3.10,<4.0"
17
+ flask = "^3.1.0"
18
+ waitress = "^3.0.2"
19
+ pyyaml = "^6.0"
20
+
21
+ [[tool.poetry.source]]
22
+ name = "aliyun"
23
+ url = "https://mirrors.aliyun.com/pypi/simple"
24
+ priority = "primary"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core"]
28
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,267 @@
1
+ import os
2
+ import json
3
+ import random
4
+ import datetime
5
+ from urllib.parse import quote, unquote
6
+ from importlib.metadata import version, PackageNotFoundError
7
+ from pathlib import Path
8
+
9
+ from flask import Flask, render_template, send_from_directory, request, redirect, send_file
10
+
11
+ # Service Imports
12
+ from tiklocal.services import LibraryService, FavoriteService, RecommendService
13
+ from tiklocal.services.thumbnail import ThumbnailService
14
+
15
+ try:
16
+ app_version = version("tiklocal")
17
+ except PackageNotFoundError:
18
+ app_version = '1.0.0'
19
+
20
+ def create_app(test_config=None):
21
+ app = Flask(__name__, instance_relative_config=True)
22
+ app.config.from_prefixed_env()
23
+ app.config.from_mapping(
24
+ SECRET_KEY = 'dev',
25
+ MEDIA_ROOT = Path(os.environ.get('MEDIA_ROOT', '.'))
26
+ )
27
+ app.config.from_pyfile('config.py', silent=True)
28
+ try:
29
+ os.makedirs(app.instance_path)
30
+ except OSError:
31
+ pass
32
+
33
+ # Initialize Services
34
+ media_root_str = str(app.config['MEDIA_ROOT'])
35
+ library_service = LibraryService(media_root_str)
36
+ favorite_service = FavoriteService(media_root_str)
37
+ recommend_service = RecommendService(library_service, favorite_service)
38
+ thumbnail_service = ThumbnailService(Path(media_root_str))
39
+
40
+ # --- Template Filters ---
41
+ @app.template_filter('timestamp_to_date')
42
+ def timestamp_to_date(timestamp):
43
+ try:
44
+ return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
45
+ except (ValueError, OSError):
46
+ return '未知时间'
47
+
48
+ @app.template_filter('filesizeformat')
49
+ def filesizeformat(num_bytes):
50
+ if num_bytes is None: return '0 B'
51
+ for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
52
+ if num_bytes < 1024.0:
53
+ return f"{int(num_bytes) if unit == 'B' else f'{num_bytes:.1f}'} {unit}"
54
+ num_bytes /= 1024.0
55
+ return f"{num_bytes:.1f} PB"
56
+
57
+
58
+ # --- Web Routes ---
59
+ @app.route('/')
60
+ def tiktok():
61
+ """Immersive Video Feed"""
62
+ return render_template('tiktok.html', menu='index')
63
+
64
+ @app.route('/gallery')
65
+ def gallery():
66
+ """Immersive Image Discovery"""
67
+ return render_template('gallery.html', menu='gallery')
68
+
69
+ @app.route('/browse')
70
+ def browse():
71
+ """Video Library List"""
72
+ # Using scan_videos instead of recursive get_files
73
+ videos = library_service.scan_videos()
74
+
75
+ # Filter Logic
76
+ size_mode = request.args.get('size', 'all')
77
+ min_mb = int(request.args.get('min_mb', 50))
78
+
79
+ if size_mode == 'big':
80
+ threshold = min_mb * 1024 * 1024
81
+ videos = [v for v in videos if v.stat().st_size >= threshold]
82
+
83
+ # Pagination
84
+ count = len(videos)
85
+ page = int(request.args.get('page', 1))
86
+ length = 20
87
+ offset = length * (page - 1)
88
+
89
+ # Convert to relative strings for template
90
+ sliced_videos = [library_service.get_relative_path(v) for v in videos[offset:offset+length]]
91
+
92
+ return render_template(
93
+ 'browse.html',
94
+ page=page,
95
+ count=count,
96
+ length=length,
97
+ files=sliced_videos,
98
+ menu='browse',
99
+ size_mode=size_mode,
100
+ min_mb=min_mb,
101
+ has_min_mb=request.args.get('min_mb') is not None,
102
+ has_previous=page > 1,
103
+ has_next=len(videos) > offset + length
104
+ )
105
+
106
+ @app.route('/settings/')
107
+ def settings_view():
108
+ video_count = len(library_service.scan_videos())
109
+ return render_template('settings.html', menu='settings', version=app_version, videos=video_count)
110
+
111
+ @app.route('/library')
112
+ def library_redirect():
113
+ return redirect('/browse')
114
+
115
+
116
+ # --- Detail & Action Routes ---
117
+
118
+ @app.route('/detail/<path:name>')
119
+ def detail_view(name):
120
+ target = library_service.resolve_path(name)
121
+ if not target or not target.exists():
122
+ return "File not found", 404
123
+
124
+ # Context navigation (prev/next)
125
+ # Note: Re-scanning every request is inefficient for large libraries,
126
+ # but keeps state stateless. Optimization: Cache this.
127
+ videos = library_service.scan_videos()
128
+ video_names = [library_service.get_relative_path(v) for v in videos]
129
+
130
+ try:
131
+ index = video_names.index(name)
132
+ prev_item = video_names[index-1] if index > 0 else None
133
+ next_item = video_names[index+1] if index < len(video_names)-1 else None
134
+ except ValueError:
135
+ prev_item = next_item = None
136
+
137
+ return render_template(
138
+ 'detail.html',
139
+ file=name,
140
+ mtime=datetime.datetime.fromtimestamp(target.stat().st_mtime).strftime('%Y-%m-%d %H:%M'),
141
+ size=target.stat().st_size,
142
+ previous_item=prev_item,
143
+ next_item=next_item
144
+ )
145
+
146
+ @app.route('/image')
147
+ def image_view():
148
+ uri = request.args.get('uri')
149
+ if not uri: return "Missing uri", 400
150
+
151
+ target = library_service.resolve_path(uri)
152
+ if not target or not target.exists(): return "File not found", 404
153
+
154
+ return render_template('image_detail.html', image=target, uri=uri, stat=target.stat())
155
+
156
+ @app.route("/delete/<path:name>", methods=['POST', 'GET'])
157
+ def delete_view(name):
158
+ target = library_service.resolve_path(name)
159
+ if request.method == 'POST':
160
+ if target and target.exists():
161
+ try:
162
+ target.unlink()
163
+ # Thumbnails are handled by OS or periodic cleanup, but ideally Service should handle it
164
+ except Exception as e:
165
+ return f"Error deleting file: {e}", 500
166
+ return redirect('/browse')
167
+
168
+ return render_template('delete_confirm.html', file=name)
169
+
170
+ @app.route("/delete", methods=['POST', 'GET'])
171
+ def delete_confirm_legacy():
172
+ # Legacy support for query param style
173
+ uri = request.args.get('uri')
174
+ if not uri: return redirect('/browse')
175
+ return redirect(f"/delete/{quote(uri)}")
176
+
177
+ @app.route('/favorite')
178
+ def favorite_view():
179
+ return render_template('favorite.html', files=list(favorite_service.load()))
180
+
181
+
182
+ # --- Media Serving Routes ---
183
+
184
+ @app.route("/media/<path:filename>")
185
+ def serve_media(filename):
186
+ # Consolidated media serving
187
+ try:
188
+ return send_from_directory(app.config["MEDIA_ROOT"], filename)
189
+ except Exception:
190
+ return "File not found", 404
191
+
192
+ @app.route("/media")
193
+ def serve_media_legacy():
194
+ # Legacy support for /media?uri=...
195
+ uri = request.args.get('uri')
196
+ if not uri: return "Missing uri", 400
197
+ return redirect(f"/media/{uri}")
198
+
199
+ @app.route('/thumb')
200
+ def thumb_view():
201
+ uri = request.args.get('uri')
202
+ if not uri: return send_file(io.BytesIO(thumbnail_service.placeholder), mimetype='image/png')
203
+
204
+ path, mimetype = thumbnail_service.get_thumbnail(unquote(uri))
205
+ if isinstance(path, bytes):
206
+ return send_file(io.BytesIO(path), mimetype=mimetype)
207
+ return send_file(path, mimetype=mimetype)
208
+
209
+
210
+ # --- API Routes ---
211
+
212
+ @app.route('/api/videos')
213
+ def api_videos():
214
+ # Clean JSON API
215
+ selected = recommend_service.get_weighted_selection(file_type='video', limit=20)
216
+ return json.dumps(selected)
217
+
218
+ @app.route('/api/random-images')
219
+ def api_random_images():
220
+ page = int(request.args.get('page', 1))
221
+ size = int(request.args.get('size', 30))
222
+ seed = request.args.get('seed') or str(random.randint(1, 999999))
223
+
224
+ # Get recommended images (all of them, weighted)
225
+ # Note: RecommendService currently returns a list. For true scale, we'd paginate inside Service.
226
+ # For now, consistent with previous behavior, we get all and slice.
227
+ all_images = recommend_service.get_weighted_selection(file_type='image', limit=99999, seed=seed)
228
+
229
+ total = len(all_images)
230
+ start = (page - 1) * size
231
+ end = start + size
232
+ page_images = all_images[start:end]
233
+
234
+ return {
235
+ 'images': page_images,
236
+ 'page': page,
237
+ 'total': total,
238
+ 'has_more': end < total,
239
+ 'seed': seed
240
+ }
241
+
242
+ @app.route('/api/favorite/<path:name>', methods=['GET', 'POST'])
243
+ def api_favorite(name):
244
+ if request.method == 'GET':
245
+ return {'favorite': favorite_service.is_favorite(name)}
246
+
247
+ new_state = favorite_service.toggle(name)
248
+ return {'success': True, 'favorite': new_state}
249
+
250
+ @app.route('/api/thumbnail/<path:name>', methods=['POST'])
251
+ def api_set_thumbnail(name):
252
+ target = library_service.resolve_path(name)
253
+ if not target: return {'success': False, 'error': 'Invalid path'}, 400
254
+
255
+ payload = request.get_json(silent=True) or {}
256
+ ts = payload.get('time')
257
+
258
+ # This logic is a bit specific to app.py still, ideally move to ThumbnailService
259
+ # But for now, we just need to regen the thumb
260
+ thumb_path = thumbnail_service._get_thumb_path(name)
261
+ success = thumbnail_service._generate(target, thumb_path, timestamp=float(ts) if ts else None)
262
+
263
+ if success:
264
+ return {'success': True, 'url': f"/thumb?uri={quote(name)}&v={int(datetime.datetime.now().timestamp())}"}
265
+ return {'success': False, 'error': 'Failed to generate'}, 500
266
+
267
+ return app
File without changes
@@ -0,0 +1,26 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ def get_data_dir() -> Path:
6
+ """Return the global data directory for TikLocal.
7
+ Default to ~/.tiklocal unless TIKLOCAL_INSTANCE is set.
8
+ """
9
+ base = os.environ.get('TIKLOCAL_INSTANCE')
10
+ if base:
11
+ p = Path(base).expanduser()
12
+ else:
13
+ p = Path.home() / '.tiklocal'
14
+ p.mkdir(parents=True, exist_ok=True)
15
+ return p
16
+
17
+
18
+ def get_thumbnails_dir() -> Path:
19
+ d = get_data_dir() / 'thumbnails'
20
+ d.mkdir(parents=True, exist_ok=True)
21
+ return d
22
+
23
+
24
+ def get_thumbs_map_path() -> Path:
25
+ return get_data_dir() / 'thumbs.json'
26
+
@@ -0,0 +1,139 @@
1
+ import os
2
+ import sys
3
+ import argparse
4
+ from pathlib import Path
5
+ from waitress import serve
6
+ from tiklocal.app import create_app
7
+ from tiklocal.thumbs import generate_thumbnails
8
+ from tiklocal.paths import get_data_dir
9
+
10
+ try:
11
+ import yaml
12
+ except ImportError:
13
+ yaml = None
14
+
15
+
16
+ def load_config():
17
+ """从配置文件加载配置"""
18
+ config = {}
19
+
20
+ # 尝试读取配置文件
21
+ config_paths = [
22
+ Path.home() / '.config' / 'tiklocal' / 'config.yaml',
23
+ Path.home() / '.tiklocal' / 'config.yaml',
24
+ ]
25
+
26
+ for config_path in config_paths:
27
+ if config_path.exists():
28
+ if yaml is None:
29
+ print(f"警告: 找到配置文件 {config_path} 但未安装 PyYAML,跳过配置文件", file=sys.stderr)
30
+ break
31
+ try:
32
+ with open(config_path, 'r', encoding='utf-8') as f:
33
+ config = yaml.safe_load(f) or {}
34
+ break
35
+ except Exception as e:
36
+ print(f"警告: 读取配置文件 {config_path} 失败: {e}", file=sys.stderr)
37
+
38
+ return config
39
+
40
+
41
+ def main():
42
+ # 读取配置文件
43
+ config = load_config()
44
+
45
+ # 预处理 argv,支持以下形式:
46
+ # 1) tiklocal -> serve
47
+ # 2) tiklocal /path -> serve /path
48
+ # 3) tiklocal --port 9000 -> serve --port 9000
49
+ # 4) tiklocal thumbs /path -> thumbs /path
50
+ # 5) tiklocal /path thumbs -> thumbs /path
51
+ argv = sys.argv[1:]
52
+ if '-h' not in argv and '--help' not in argv:
53
+ if 'thumbs' in argv:
54
+ idx = argv.index('thumbs')
55
+ if idx != 0:
56
+ argv.pop(idx)
57
+ argv.insert(0, 'thumbs')
58
+ elif len(argv) == 0 or argv[0] not in ('serve', 'thumbs'):
59
+ # 默认回退 serve(空参数或第一个不是已知子命令)
60
+ argv.insert(0, 'serve')
61
+
62
+ # 解析命令行参数(支持子命令)
63
+ parser = argparse.ArgumentParser(
64
+ description='TikLocal - 本地媒体服务器',
65
+ formatter_class=argparse.RawDescriptionHelpFormatter,
66
+ epilog='''
67
+ 示例:
68
+ tiklocal # 启动服务(默认)
69
+ tiklocal /path/to/media # 指定媒体目录
70
+ tiklocal --port 9000 # 使用指定端口
71
+ tiklocal serve /path --port 9000 # 显式使用 serve 子命令
72
+ tiklocal thumbs /path --overwrite # 批量生成缩略图
73
+ '''
74
+ )
75
+
76
+ subparsers = parser.add_subparsers(dest='command')
77
+
78
+ # serve 子命令
79
+ serve_parser = subparsers.add_parser('serve', help='启动服务器')
80
+ serve_parser.add_argument('media_root', nargs='?', help='媒体文件根目录路径')
81
+ serve_parser.add_argument('--host', default=None, help='服务器监听地址 (默认: 0.0.0.0)')
82
+ serve_parser.add_argument('--port', type=int, default=None, help='服务器端口 (默认: 8000)')
83
+
84
+ # thumbs 子命令
85
+ thumbs_parser = subparsers.add_parser('thumbs', help='批量生成视频缩略图')
86
+ thumbs_parser.add_argument('media_root', nargs='?', help='媒体文件根目录路径(可省略以使用环境变量/配置文件)')
87
+ thumbs_parser.add_argument('--overwrite', action='store_true', help='存在时覆盖重建')
88
+ thumbs_parser.add_argument('--limit', type=int, default=0, help='最多处理多少个(0 表示全部)')
89
+
90
+ args = parser.parse_args(argv)
91
+
92
+ # 判断命令类型(无子命令时视为 serve)
93
+ cmd = args.command or 'serve'
94
+
95
+ if cmd == 'thumbs':
96
+ media_root = args.media_root or os.environ.get('MEDIA_ROOT') or config.get('media_root')
97
+ if not media_root:
98
+ parser.error('必须指定媒体目录:\n - tiklocal thumbs /path/to/media\n - 或设置环境变量: MEDIA_ROOT=/path/to/media')
99
+ media_path = Path(media_root)
100
+ if not media_path.exists() or not media_path.is_dir():
101
+ print(f"错误: 媒体目录不可用: {media_root}", file=sys.stderr)
102
+ sys.exit(1)
103
+ print(f"数据目录: {get_data_dir()}")
104
+ stats = generate_thumbnails(media_path, overwrite=getattr(args, 'overwrite', False), limit=getattr(args, 'limit', 0), show_progress=True)
105
+ # 完成后退出
106
+ return
107
+
108
+ # serve 路径
109
+ media_root = args.media_root or os.environ.get('MEDIA_ROOT') or config.get('media_root')
110
+ host = args.host or os.environ.get('TIKLOCAL_HOST') or config.get('host', '0.0.0.0')
111
+ port = args.port or int(os.environ.get('TIKLOCAL_PORT', 0)) or config.get('port', 8000)
112
+
113
+ # 验证媒体目录
114
+ if not media_root:
115
+ parser.error('必须指定媒体目录:\n - 通过命令行参数: tiklocal /path/to/media\n - 通过环境变量: MEDIA_ROOT=/path/to/media\n - 通过配置文件: ~/.config/tiklocal/config.yaml')
116
+
117
+ media_path = Path(media_root)
118
+ if not media_path.exists():
119
+ print(f"错误: 媒体目录不存在: {media_root}", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ if not media_path.is_dir():
123
+ print(f"错误: 路径不是目录: {media_root}", file=sys.stderr)
124
+ sys.exit(1)
125
+
126
+ # 设置环境变量供 Flask 使用
127
+ os.environ['MEDIA_ROOT'] = str(media_path.absolute())
128
+
129
+ # 启动服务器
130
+ print(f"启动 TikLocal 服务器...")
131
+ print(f"媒体目录: {media_path.absolute()}")
132
+ print(f"数据目录: {get_data_dir()}")
133
+ print(f"访问地址: http://{host}:{port}")
134
+
135
+ serve(create_app(), host=host, port=port)
136
+
137
+
138
+ if __name__ == '__main__':
139
+ main()