luckyrobots 0.1.72__py3-none-any.whl → 0.1.73__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.
@@ -149,103 +149,127 @@ unitreego1:
149
149
  - locomotion
150
150
  action_space:
151
151
  actuator_names:
152
- - FR_hip
153
- - FR_thigh
154
- - FR_calf
155
- - FL_hip
156
- - FL_thigh
157
- - FL_calf
158
- - RR_hip
159
- - RR_thigh
160
- - RR_calf
161
- - RL_hip
162
- - RL_thigh
163
- - RL_calf
152
+ - FR_hip_joint
153
+ - FR_thigh_joint
154
+ - FR_calf_joint
155
+ - FL_hip_joint
156
+ - FL_thigh_joint
157
+ - FL_calf_joint
158
+ - RR_hip_joint
159
+ - RR_thigh_joint
160
+ - RR_calf_joint
161
+ - RL_hip_joint
162
+ - RL_thigh_joint
163
+ - RL_calf_joint
164
164
  actuator_limits:
165
- - name: FR_hip
165
+ - name: FR_hip_joint
166
166
  lower: -0.863
167
167
  upper: 0.863
168
- - name: FR_thigh
168
+ default: 0.1
169
+ - name: FR_thigh_joint
169
170
  lower: -0.686
170
171
  upper: 4.501
171
- - name: FR_calf
172
+ default: 0.9
173
+ - name: FR_calf_joint
172
174
  lower: -2.818
173
175
  upper: -0.888
174
- - name: FL_hip
176
+ default: -1.8
177
+ - name: FL_hip_joint
175
178
  lower: -0.863
176
179
  upper: 0.863
177
- - name: FL_thigh
180
+ default: -0.1
181
+ - name: FL_thigh_joint
178
182
  lower: -0.686
179
183
  upper: 4.501
180
- - name: FL_calf
184
+ default: 0.9
185
+ - name: FL_calf_joint
181
186
  lower: -2.818
182
187
  upper: -0.888
183
- - name: RR_hip
188
+ default: -1.8
189
+ - name: RR_hip_joint
184
190
  lower: -0.863
185
191
  upper: 0.863
186
- - name: RR_thigh
192
+ default: 0.1
193
+ - name: RR_thigh_joint
187
194
  lower: -0.686
188
195
  upper: 4.501
189
- - name: RR_calf
196
+ default: 0.9
197
+ - name: RR_calf_joint
190
198
  lower: -2.818
191
199
  upper: -0.888
192
- - name: RL_hip
200
+ default: -1.8
201
+ - name: RL_hip_joint
193
202
  lower: -0.863
194
203
  upper: 0.863
195
- - name: RL_thigh
204
+ default: -0.1
205
+ - name: RL_thigh_joint
196
206
  lower: -0.686
197
207
  upper: 4.501
198
- - name: RL_calf
208
+ default: 0.9
209
+ - name: RL_calf_joint
199
210
  lower: -2.818
200
211
  upper: -0.888
212
+ default: -1.8
201
213
  observation_space:
202
214
  actuator_names:
203
- - FR_hip
204
- - FR_thigh
205
- - FR_calf
206
- - FL_hip
207
- - FL_thigh
208
- - FL_calf
209
- - RR_hip
210
- - RR_thigh
211
- - RR_calf
212
- - RL_hip
213
- - RL_thigh
214
- - RL_calf
215
+ - FR_hip_joint
216
+ - FR_thigh_joint
217
+ - FR_calf_joint
218
+ - FL_hip_joint
219
+ - FL_thigh_joint
220
+ - FL_calf_joint
221
+ - RR_hip_joint
222
+ - RR_thigh_joint
223
+ - RR_calf_joint
224
+ - RL_hip_joint
225
+ - RL_thigh_joint
226
+ - RL_calf_joint
215
227
  actuator_limits:
216
- - name: FR_hip
228
+ - name: FR_hip_joint
217
229
  lower: -0.863
218
230
  upper: 0.863
219
- - name: FR_thigh
231
+ default: 0.1
232
+ - name: FR_thigh_joint
220
233
  lower: -0.686
221
234
  upper: 4.501
222
- - name: FR_calf
235
+ default: 0.9
236
+ - name: FR_calf_joint
223
237
  lower: -2.818
224
238
  upper: -0.888
225
- - name: FL_hip
239
+ default: -1.8
240
+ - name: FL_hip_joint
226
241
  lower: -0.863
227
242
  upper: 0.863
228
- - name: FL_thigh
243
+ default: -0.1
244
+ - name: FL_thigh_joint
229
245
  lower: -0.686
230
246
  upper: 4.501
231
- - name: FL_calf
247
+ default: 0.9
248
+ - name: FL_calf_joint
232
249
  lower: -2.818
233
250
  upper: -0.888
234
- - name: RR_hip
251
+ default: -1.8
252
+ - name: RR_hip_joint
235
253
  lower: -0.863
236
254
  upper: 0.863
237
- - name: RR_thigh
255
+ default: 0.1
256
+ - name: RR_thigh_joint
238
257
  lower: -0.686
239
258
  upper: 4.501
240
- - name: RR_calf
259
+ default: 0.9
260
+ - name: RR_calf_joint
241
261
  lower: -2.818
242
262
  upper: -0.888
243
- - name: RL_hip
263
+ default: -1.8
264
+ - name: RL_hip_joint
244
265
  lower: -0.863
245
266
  upper: 0.863
246
- - name: RL_thigh
267
+ default: -0.1
268
+ - name: RL_thigh_joint
247
269
  lower: -0.686
248
270
  upper: 4.501
249
- - name: RL_calf
271
+ default: 0.9
272
+ - name: RL_calf_joint
250
273
  lower: -2.818
251
274
  upper: -0.888
275
+ default: -1.8
@@ -1,23 +1,8 @@
1
1
  """Engine lifecycle management for LuckyEngine."""
2
2
 
3
- from .check_updates import check_updates
4
- from .download import apply_changes, get_base_url, get_os_type
5
- from .manager import (
6
- find_luckyengine_executable,
7
- is_luckyengine_running,
8
- launch_luckyengine,
9
- stop_luckyengine,
3
+ from luckyrobots.engine.manager import (
4
+ find_luckyengine_executable as find_luckyengine_executable,
10
5
  )
11
-
12
- __all__ = [
13
- # Manager functions
14
- "launch_luckyengine",
15
- "stop_luckyengine",
16
- "is_luckyengine_running",
17
- "find_luckyengine_executable",
18
- # Update functions
19
- "check_updates",
20
- "apply_changes",
21
- "get_base_url",
22
- "get_os_type",
23
- ]
6
+ from luckyrobots.engine.manager import is_luckyengine_running as is_luckyengine_running
7
+ from luckyrobots.engine.manager import launch_luckyengine as launch_luckyengine
8
+ from luckyrobots.engine.manager import stop_luckyengine as stop_luckyengine
@@ -1,15 +1,3 @@
1
- """
2
- Pydantic models for LuckyRobots.
3
- """
1
+ """Pydantic models for LuckyRobots."""
4
2
 
5
- from .observation import ObservationResponse, StateSnapshot
6
- from .camera import CameraData, CameraShape
7
- from .randomization import DomainRandomizationConfig
8
-
9
- __all__ = [
10
- "ObservationResponse",
11
- "StateSnapshot",
12
- "CameraData",
13
- "CameraShape",
14
- "DomainRandomizationConfig",
15
- ]
3
+ from luckyrobots.models.observation import ObservationResponse as ObservationResponse
@@ -1,12 +1,6 @@
1
- """
2
- RL observation models for LuckyRobots.
1
+ """RL observation models for LuckyRobots."""
3
2
 
4
- These are the primary return types for the simplified API:
5
- - ObservationResponse: returned by get_observation()
6
- - StateSnapshot: returned by get_state()
7
- """
8
-
9
- from typing import Any, Dict, List, Optional
3
+ from typing import Dict, List, Optional
10
4
  from pydantic import BaseModel, Field, ConfigDict
11
5
 
12
6
 
@@ -14,11 +8,11 @@ class ObservationResponse(BaseModel):
14
8
  """RL observation data from an agent.
15
9
 
16
10
  This is the return type for LuckyEngineClient.get_observation() and
17
- LuckyRobots.get_observation(). It contains the RL observation vector
11
+ LuckyEngineClient.step(). It contains the RL observation vector
18
12
  with optional named access for debugging.
19
13
 
20
14
  Usage:
21
- obs = client.get_observation()
15
+ obs = client.step(actions)
22
16
 
23
17
  # Flat vector for RL training
24
18
  obs.observation # [0.1, 0.2, 0.3, ...]
@@ -110,26 +104,3 @@ class ObservationResponse(BaseModel):
110
104
  if self.action_names is not None:
111
105
  return dict(zip(self.action_names, self.actions))
112
106
  return {f"action_{i}": v for i, v in enumerate(self.actions)}
113
-
114
-
115
- class StateSnapshot(BaseModel):
116
- """Bundled snapshot of multiple data sources.
117
-
118
- Use LuckyEngineClient.get_state() to get a bundled snapshot when you need
119
- multiple data types in one efficient call. For streaming data like telemetry,
120
- use the dedicated streaming methods instead.
121
- """
122
-
123
- model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
124
-
125
- observation: Optional[ObservationResponse] = Field(
126
- default=None, description="RL observation data (if include_observation=True)"
127
- )
128
- joint_state: Optional[Any] = Field(
129
- default=None, description="Joint positions/velocities (if include_joint_state=True)"
130
- )
131
- camera_frames: Optional[List[Any]] = Field(
132
- default=None, description="Camera frames (if camera_names provided)"
133
- )
134
- timestamp_ms: int = Field(default=0, description="Wall-clock timestamp in milliseconds")
135
- frame_number: int = Field(default=0, description="Monotonic frame counter")
luckyrobots/utils.py CHANGED
@@ -1,49 +1,7 @@
1
- """
2
- Utility functions and classes for LuckyRobots.
3
- """
1
+ """Utility functions for LuckyRobots."""
4
2
 
5
- import time
6
3
  import yaml
7
4
  import importlib.resources
8
- from collections import deque
9
-
10
-
11
- class FPS:
12
- """Utility for measuring frames per second with a rolling window.
13
-
14
- Usage:
15
- fps = FPS(frame_window=30)
16
- while running:
17
- # ... do work ...
18
- current_fps = fps.measure()
19
- """
20
-
21
- def __init__(self, frame_window: int = 30):
22
- """Initialize FPS counter.
23
-
24
- Args:
25
- frame_window: Number of frames to average over.
26
- """
27
- self.frame_window = frame_window
28
- self.frame_times: deque[float] = deque(maxlen=frame_window)
29
- self.last_frame_time = time.perf_counter()
30
-
31
- def measure(self) -> float:
32
- """Record a frame and return current FPS.
33
-
34
- Returns:
35
- Current frames per second (averaged over window).
36
- """
37
- current_time = time.perf_counter()
38
- frame_delta = current_time - self.last_frame_time
39
- self.last_frame_time = current_time
40
-
41
- self.frame_times.append(frame_delta)
42
-
43
- if len(self.frame_times) >= 2:
44
- avg_frame_time = sum(self.frame_times) / len(self.frame_times)
45
- return 1.0 / avg_frame_time if avg_frame_time > 0 else 0.0
46
- return 0.0
47
5
 
48
6
 
49
7
  def get_robot_config(robot: str = None) -> dict:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: luckyrobots
3
- Version: 0.1.72
3
+ Version: 0.1.73
4
4
  Summary: Robotics-AI Training in Hyperrealistic Game Environments
5
5
  Project-URL: Homepage, https://github.com/luckyrobots/luckyrobots
6
6
  Project-URL: Documentation, https://luckyrobots.readthedocs.io
@@ -1,12 +1,10 @@
1
- luckyrobots/__init__.py,sha256=VWewiYhYbYCs1ZSD0KxozmG2jH2uLlBxsVgFIRivKTM,824
2
- luckyrobots/client.py,sha256=3F0p46FndJv02D_pSZkOn4j2tkjV6RX1ziCFFEaJsr4,36129
1
+ luckyrobots/__init__.py,sha256=cjma4V6iH-1QWWkt9n9Y9JD8LZJgmTIXP7eE9RT2ylw,469
2
+ luckyrobots/client.py,sha256=_OSK5WgMMcSUtSHwJRdmytB-WIJHYMazrwhkV5LSO0c,17667
3
3
  luckyrobots/luckyrobots.py,sha256=-XkIt9h7si4GK_j5Wpknb7_h56CCWi8_d1woVMuIvxU,8957
4
- luckyrobots/utils.py,sha256=62BqpSRBFykVRMZ5Ti2F-yKRDoyU3UNHeesDF_PVCFc,2958
4
+ luckyrobots/utils.py,sha256=deh0OV3I6Tr6V1s0yWs4jtchsZK1vKySpUohLjqOF1I,1769
5
5
  luckyrobots/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- luckyrobots/config/robots.yaml,sha256=Wu2AgwhZxo9F-RLcqPJXHnqzjT8a0wR-rFBcecck_eE,5164
7
- luckyrobots/engine/__init__.py,sha256=-3r7aLueDF7eyumqIhTvYqS8ZNnVSJUJEHG4ybsKrjc,546
8
- luckyrobots/engine/check_updates.py,sha256=KGQHrj1LB0_vexktO01xx-ZvPOYKkto4VBb3H1KV7uI,7593
9
- luckyrobots/engine/download.py,sha256=vIb5T40Jk2BnfjPo0f4KgW14Om6P4_mvPpZ9JAlkVvg,4029
6
+ luckyrobots/config/robots.yaml,sha256=TevKlMzsZw-8k3PDf1aOG8zs18LedquS-ayVZeqyYdU,5968
7
+ luckyrobots/engine/__init__.py,sha256=OsyOgAnLtOfpRDHXa3Gzc1fRhJA8EfKvRkzijF6iRc8,403
10
8
  luckyrobots/engine/manager.py,sha256=8vxSevaoXo-lw2G6y74lVc-Ms8xIELK0yLswydMiOQY,13388
11
9
  luckyrobots/grpc/__init__.py,sha256=hPh-X_FEus2sxrfQB2fQjD754X5bMaUFTWm3mZmUhe0,153
12
10
  luckyrobots/grpc/generated/__init__.py,sha256=u69M4E_2LwRQSD_n4wvQyMX7dryBCrIdsho59c52Crk,732
@@ -37,11 +35,9 @@ luckyrobots/grpc/proto/mujoco.proto,sha256=b7tBvYqqvcSjmqrEax6_75-P3fIHHZq63A1HV
37
35
  luckyrobots/grpc/proto/scene.proto,sha256=ZypWsUzpgplGsQPP0FXJpyY4rSxgWYGsniM4vofhnak,3075
38
36
  luckyrobots/grpc/proto/telemetry.proto,sha256=PZiUoA0bpwuvsBUdoQQdBNGWOuhlBMKAiBpwqPZiJgY,1481
39
37
  luckyrobots/grpc/proto/viewport.proto,sha256=gunF8cIrVgvoAX8FNER6gw_u-IlRWAR1Q6ts0zUuqr8,1324
40
- luckyrobots/models/__init__.py,sha256=assqJIKZXvDGwCVM0grNLJCBWyHtKRBAaxdJb2dON10,332
41
- luckyrobots/models/camera.py,sha256=e-CftAndHASfokhtUtU7ujGb2q_kcXZ6-2KKvY42CKg,3682
42
- luckyrobots/models/observation.py,sha256=jbRiUTjOP2ZiSobsnNcks3aMPAJA6oPoKqiXwvrNQK0,4758
43
- luckyrobots/models/randomization.py,sha256=AicqHovzG97Ge33wtNQE-_EvKxndjEHguTjCiHfhAV8,2753
44
- luckyrobots-0.1.72.dist-info/METADATA,sha256=-liPc6bNv78N5A-PgTJ8YeiD5YIIU8BTO60zE-zV87Y,8379
45
- luckyrobots-0.1.72.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
46
- luckyrobots-0.1.72.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
47
- luckyrobots-0.1.72.dist-info/RECORD,,
38
+ luckyrobots/models/__init__.py,sha256=h_PFZ1OgJnHXfQBqO8_ULB8szCKrGA4lMfmBGZgtIDc,126
39
+ luckyrobots/models/observation.py,sha256=H45zargSOMR4c-zPupbXNplWBTBfGdG-oQbib203eCY,3620
40
+ luckyrobots-0.1.73.dist-info/METADATA,sha256=GJY8BZbJzlwk_1VRViO_DmCZKoBxQjOxsAst7baVY38,8379
41
+ luckyrobots-0.1.73.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
42
+ luckyrobots-0.1.73.dist-info/licenses/LICENSE,sha256=xsPYvRJPH_fW_sofTUknI_KvZOsD4-BqjSOTZqI6Nmw,1069
43
+ luckyrobots-0.1.73.dist-info/RECORD,,
@@ -1,264 +0,0 @@
1
- """
2
- Check for LuckyEngine executable updates.
3
-
4
- This module compares local and remote file structures to detect changes.
5
- """
6
-
7
- import json
8
- import logging
9
- import mimetypes
10
- import os
11
- import platform
12
- import re
13
- import sys
14
- import zlib
15
- from typing import Optional
16
- from urllib.parse import urljoin
17
-
18
- import requests
19
-
20
- logger = logging.getLogger("luckyrobots.engine.check_updates")
21
-
22
- BASE_URL = "https://builds.luckyrobots.xyz/"
23
-
24
-
25
- def get_os_type() -> str:
26
- """
27
- Get the operating system type as a string.
28
-
29
- Returns:
30
- "mac", "win", or "linux"
31
-
32
- Raises:
33
- ValueError: If the OS is not supported.
34
- """
35
- os_type = platform.system().lower()
36
- if os_type == "darwin":
37
- return "mac"
38
- elif os_type == "windows":
39
- return "win"
40
- elif os_type == "linux":
41
- return "linux"
42
- else:
43
- raise ValueError(f"Unsupported operating system: {os_type}")
44
-
45
-
46
- def calculate_crc32(file_path: str) -> int:
47
- """
48
- Calculate CRC32 checksum for a file.
49
-
50
- Args:
51
- file_path: Path to the file.
52
-
53
- Returns:
54
- CRC32 checksum as unsigned 32-bit integer.
55
- """
56
- crc32 = 0
57
- with open(file_path, "rb") as file:
58
- while True:
59
- data = file.read(65536) # Read in 64kb chunks
60
- if not data:
61
- break
62
- crc32 = zlib.crc32(data, crc32)
63
- return crc32 & 0xFFFFFFFF # Ensure unsigned 32-bit integer
64
-
65
-
66
- def scan_directory(root_path: str) -> list[dict]:
67
- """
68
- Scan a directory and create a file structure representation.
69
-
70
- Args:
71
- root_path: Root directory to scan.
72
-
73
- Returns:
74
- List of dictionaries containing file/directory metadata.
75
- """
76
- file_structure = []
77
-
78
- for dirpath, dirnames, filenames in os.walk(root_path):
79
- # Add directories
80
- for dirname in dirnames:
81
- dir_path = os.path.join(dirpath, dirname)
82
- relative_path = os.path.relpath(dir_path, root_path)
83
- file_structure.append(
84
- {
85
- "path": relative_path,
86
- "type": "directory",
87
- "size": 0,
88
- "mtime": os.path.getmtime(dir_path),
89
- "crc32": 0,
90
- "mime_type": "directory",
91
- }
92
- )
93
-
94
- # Add files
95
- for filename in filenames:
96
- file_path = os.path.join(dirpath, filename)
97
- relative_path = os.path.relpath(file_path, root_path)
98
- file_stat = os.stat(file_path)
99
-
100
- # Guess the file type using mimetypes
101
- file_type, _ = mimetypes.guess_type(file_path)
102
- if file_type is None:
103
- file_type = "application/octet-stream"
104
-
105
- file_structure.append(
106
- {
107
- "path": relative_path,
108
- "crc32": calculate_crc32(file_path),
109
- "size": file_stat.st_size,
110
- "mtime": file_stat.st_mtime,
111
- "type": "file",
112
- "mime_type": file_type,
113
- }
114
- )
115
-
116
- return file_structure
117
-
118
-
119
- def save_json(data: dict, filename: str) -> None:
120
- """Save data to a JSON file."""
121
- with open(filename, "w") as f:
122
- json.dump(data, f, indent=2)
123
-
124
-
125
- def load_json(filename: str) -> dict:
126
- """Load data from a JSON file."""
127
- with open(filename, "r") as f:
128
- return json.load(f)
129
-
130
-
131
- def clean_path(path: str) -> tuple[str, Optional[str]]:
132
- """
133
- Clean and parse a path string.
134
-
135
- Args:
136
- path: Path string to clean.
137
-
138
- Returns:
139
- Tuple of (directory_path, attribute_name).
140
- """
141
- # Remove ['children'] and quotes, replace '][' with '/'
142
- cleaned = re.sub(r"\['children'\]", "", path).replace("']['", "/").strip("[]'")
143
- # Replace remaining single quotes with nothing
144
- cleaned = cleaned.replace("'", "")
145
- # Split the path and the attribute that changed
146
- parts = cleaned.rsplit("/", 1)
147
- return parts[0], parts[1] if len(parts) > 1 else None
148
-
149
-
150
- def compare_structures(
151
- client_structure: list[dict], server_structure: list[dict]
152
- ) -> list[dict]:
153
- """
154
- Compare two file structures and return list of changes.
155
-
156
- Args:
157
- client_structure: Local file structure.
158
- server_structure: Remote file structure.
159
-
160
- Returns:
161
- List of changes (new, modified, deleted files).
162
- """
163
- dict1 = {item["path"]: item for item in client_structure}
164
- dict2 = {item["path"]: item for item in server_structure}
165
-
166
- result = []
167
-
168
- # Check for new and modified items
169
- for path, item in dict2.items():
170
- if path not in dict1:
171
- item["change_type"] = "new_file"
172
- result.append(item)
173
- elif item["type"] == "file" and item["crc32"] != dict1[path]["crc32"]:
174
- item["change_type"] = "modified"
175
- result.append(item)
176
-
177
- # Check for deleted items
178
- for path, item in dict1.items():
179
- if path not in dict2:
180
- item["change_type"] = "deleted"
181
- result.append(item)
182
-
183
- # Remove hashmap.json from changes (it's metadata)
184
- result = [item for item in result if item["path"] != "hashmap.json"]
185
-
186
- return result
187
-
188
-
189
- def scan_server(server_path: str) -> None:
190
- """
191
- Scan server directory structure and save hashmap files.
192
-
193
- Args:
194
- server_path: Path to server root directory.
195
- """
196
- mac_path = os.path.join(server_path, "mac")
197
- win_path = os.path.join(server_path, "win")
198
- linux_path = os.path.join(server_path, "linux")
199
-
200
- mac_structure = scan_directory(mac_path)
201
- win_structure = scan_directory(win_path)
202
- linux_structure = scan_directory(linux_path)
203
-
204
- save_json(mac_structure, os.path.join(server_path, "mac/hashmap.json"))
205
- save_json(win_structure, os.path.join(server_path, "win/hashmap.json"))
206
- save_json(linux_structure, os.path.join(server_path, "linux/hashmap.json"))
207
-
208
-
209
- def check_updates(root_path: str, base_url: Optional[str] = None) -> list[dict]:
210
- """
211
- Check for updates by comparing local and remote file structures.
212
-
213
- Args:
214
- root_path: Local root directory to check.
215
- base_url: Base URL for remote server (uses default if None).
216
-
217
- Returns:
218
- List of changes detected.
219
- """
220
- if base_url is None:
221
- base_url = BASE_URL
222
-
223
- os_type = get_os_type()
224
- url = urljoin(base_url, f"{os_type}/hashmap.json")
225
-
226
- # Download the JSON file
227
- try:
228
- response = requests.get(url, timeout=10)
229
- response.raise_for_status()
230
- server_structure = response.json()
231
- except requests.RequestException as e:
232
- logger.warning(f"Error downloading JSON file: {e}")
233
- server_structure = None
234
-
235
- if server_structure is None:
236
- logger.info("Using local scan as no remote file could be downloaded.")
237
- server_structure = {}
238
-
239
- # Scan the directory and create a new file structure
240
- client_structure = scan_directory(root_path)
241
-
242
- # Compare and return changes
243
- if server_structure:
244
- changes = compare_structures(client_structure, server_structure)
245
- return changes
246
- else:
247
- logger.info("No server structure available for comparison.")
248
- return []
249
-
250
-
251
- if __name__ == "__main__":
252
- # This is used as a cron job on the main server to keep file structures up to date
253
- lr_server_root = None
254
-
255
- for arg in sys.argv:
256
- if arg.startswith("--lr-server-root="):
257
- lr_server_root = arg.split("=", 1)[1]
258
- break
259
-
260
- if lr_server_root:
261
- logger.info(f"Scanning server at {lr_server_root}")
262
- scan_server(lr_server_root)
263
- else:
264
- logger.error("--lr-server-root argument required")