-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.py
More file actions
197 lines (153 loc) · 6.35 KB
/
Copy pathserver.py
File metadata and controls
197 lines (153 loc) · 6.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
#!/usr/bin/env python3
"""
server.py
Local web server that wraps media_crawler.MediaCrawler in a controllable
background job, and serves a browser dashboard (static/index.html) to
start/stop crawls and watch live progress.
Run:
python3 server.py
Then open:
http://127.0.0.1:5000
Only one crawl runs at a time in this v1 -- starting a new one while one
is running returns an error. Downloaded files are saved under
./runs/<timestamp>/media/ and served back to the browser at /files/...
so the dashboard can show real thumbnails.
"""
import os
import threading
import time
from datetime import datetime
from flask import Flask, jsonify, request, send_from_directory, abort
from media_crawler import MediaCrawler, DEFAULT_USER_AGENT
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RUNS_DIR = os.path.join(BASE_DIR, "runs")
STATIC_DIR = os.path.join(BASE_DIR, "static")
os.makedirs(RUNS_DIR, exist_ok=True)
app = Flask(__name__, static_folder=STATIC_DIR, static_url_path="/static")
class CrawlJobManager:
"""Holds at most one active/most-recent crawl job and exposes thread-safe
control (start/stop) and read (status/manifest) operations for the API."""
def __init__(self):
self.lock = threading.Lock()
self.crawler = None
self.thread = None
self.stop_event = None
self.run_id = None
def is_running(self):
with self.lock:
return self.crawler is not None and self.crawler.status == "running"
def start(self, params):
with self.lock:
if self.crawler is not None and self.crawler.status == "running":
raise RuntimeError("A crawl is already running. Stop it first.")
run_id = datetime.now().strftime("%Y%m%d-%H%M%S")
output_dir = os.path.join(RUNS_DIR, run_id)
allowed_domains = "any" if params.get("no_domain_restriction") else params.get("allowed_domains")
self.stop_event = threading.Event()
self.crawler = MediaCrawler(
seeds=params["seeds"],
output_dir=output_dir,
max_depth=params.get("max_depth", 2),
max_pages=params.get("max_pages", 200),
max_files=params.get("max_files") or None,
allowed_domains=allowed_domains,
max_domains=params.get("max_domains") or None,
respect_robots=not params.get("ignore_robots", False),
delay=params.get("delay", 0.5),
timeout=params.get("timeout", 15),
user_agent=params.get("user_agent") or DEFAULT_USER_AGENT,
verbose=True,
stop_event=self.stop_event,
)
self.run_id = run_id
self.thread = threading.Thread(target=self.crawler.crawl, daemon=True)
self.thread.start()
return run_id
def stop(self):
with self.lock:
if self.crawler is None or self.crawler.status != "running":
raise RuntimeError("No crawl is currently running.")
self.stop_event.set()
def status(self):
with self.lock:
if self.crawler is None:
return {"status": "idle"}
s = self.crawler.get_status()
s["run_id"] = self.run_id
return s
def manifest(self):
with self.lock:
if self.crawler is None:
return []
return self.crawler.get_manifest()
def media_dir(self):
with self.lock:
if self.crawler is None:
return None
return self.crawler.output_dir
job = CrawlJobManager()
# ---------------------------------------------------------------------------
# API routes
# ---------------------------------------------------------------------------
@app.route("/")
def index():
return send_from_directory(STATIC_DIR, "index.html")
@app.route("/api/start", methods=["POST"])
def api_start():
body = request.get_json(force=True, silent=True) or {}
raw_seeds = body.get("seeds", "")
if isinstance(raw_seeds, str):
seeds = [s.strip() for s in raw_seeds.replace(",", "\n").splitlines() if s.strip()]
else:
seeds = [s.strip() for s in raw_seeds if s.strip()]
if not seeds:
return jsonify({"error": "At least one seed URL is required."}), 400
for s in seeds:
if not (s.startswith("http://") or s.startswith("https://")):
return jsonify({"error": f"Seed URL must start with http:// or https://: {s}"}), 400
raw_domains = body.get("allowed_domains", "")
if isinstance(raw_domains, str):
allowed_domains = [d.strip() for d in raw_domains.replace(",", "\n").splitlines() if d.strip()]
else:
allowed_domains = [d.strip() for d in (raw_domains or []) if d.strip()]
params = {
"seeds": seeds,
"max_depth": int(body.get("max_depth", 2)),
"max_pages": int(body.get("max_pages", 200)),
"max_files": int(body["max_files"]) if body.get("max_files") not in (None, "") else None,
"allowed_domains": allowed_domains or None,
"no_domain_restriction": bool(body.get("no_domain_restriction", False)),
"max_domains": int(body["max_domains"]) if body.get("max_domains") not in (None, "") else None,
"delay": float(body.get("delay", 0.5)),
"ignore_robots": bool(body.get("ignore_robots", False)),
}
try:
run_id = job.start(params)
except RuntimeError as e:
return jsonify({"error": str(e)}), 409
except Exception as e:
return jsonify({"error": f"Could not start crawl: {e}"}), 400
return jsonify({"run_id": run_id})
@app.route("/api/stop", methods=["POST"])
def api_stop():
try:
job.stop()
except RuntimeError as e:
return jsonify({"error": str(e)}), 409
return jsonify({"ok": True})
@app.route("/api/status")
def api_status():
return jsonify(job.status())
@app.route("/api/manifest")
def api_manifest():
return jsonify(job.manifest())
@app.route("/files/<path:subpath>")
def serve_downloaded_file(subpath):
media_dir = job.media_dir()
if media_dir is None:
abort(404)
# media_dir is an absolute path under RUNS_DIR; send_from_directory
# guards against path traversal outside of it.
return send_from_directory(media_dir, subpath)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False, threaded=True)