diff --git a/crosspoint_reader/config.py b/crosspoint_reader/config.py index e23a15a..2765dcf 100644 --- a/crosspoint_reader/config.py +++ b/crosspoint_reader/config.py @@ -5,6 +5,7 @@ from qt.core import ( QDialogButtonBox, QFormLayout, QHBoxLayout, + QLabel, QLineEdit, QPlainTextEdit, QPushButton, @@ -50,6 +51,12 @@ class CrossPointConfigWidget(QWidget): layout.addRow('Host', self.host) layout.addRow('Port', self.port) + + notice = QLabel('Host and port settings are fallback values used only when the device is not auto-discoverable by UDP broadcast.') + notice.setWordWrap(True) + notice.setStyleSheet('color: gray; font-style: italic;') + layout.addRow('', notice) + layout.addRow('Upload path', self.path) layout.addRow('Chunk size', self.chunk_size) layout.addRow('', self.debug) diff --git a/crosspoint_reader/driver.py b/crosspoint_reader/driver.py index 22b08d1..4dcd78f 100644 --- a/crosspoint_reader/driver.py +++ b/crosspoint_reader/driver.py @@ -21,7 +21,7 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): description = 'CrossPoint Reader wireless device' supported_platforms = ['windows', 'osx', 'linux'] author = 'CrossPoint Reader' - version = (0, 2, 0) + version = (0, 1, 3) # Invalid USB vendor info to avoid USB scans matching. VENDOR_ID = [0xFFFF] @@ -136,7 +136,8 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): def _http_post_form(self, path, data, timeout=5): url = self._http_base() + path body = urllib.parse.urlencode(data).encode('utf-8') - req = urllib.request.Request(url, data=body, method='POST') + req = urllib.request.Request(url, data=body, method='POST', + headers={'Content-Type': 'application/x-www-form-urlencoded'}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: return resp.status, resp.read().decode('utf-8', 'ignore') @@ -250,7 +251,8 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): """ url = self._http_base() + '/mkdir' body = urllib.parse.urlencode({'name': name, 'path': path}).encode('utf-8') - req = urllib.request.Request(url, data=body, method='POST') + req = urllib.request.Request(url, data=body, method='POST', + headers={'Content-Type': 'application/x-www-form-urlencoded'}) try: with urllib.request.urlopen(req, timeout=5) as resp: resp.read() @@ -353,11 +355,22 @@ class CrossPointDevice(DeviceConfig, DevicePlugin): def delete_books(self, paths, end_session=True): - for path in paths: - status, body = self._http_post_form('/delete', {'path': path, 'type': 'file'}) - if status != 200: - raise ControlError(desc=f'Delete failed for {path}: {body}') - self._log(f'[CrossPoint] deleted {path}') + import json as _json + self._log(f'[CrossPoint] deleting {len(paths)} books: {paths}') + url = self._http_base() + '/delete' + # Server expects form field 'paths' containing a JSON array string + body = urllib.parse.urlencode({'paths': _json.dumps(list(paths))}).encode('utf-8') + req = urllib.request.Request(url, data=body, method='POST', + headers={'Content-Type': 'application/x-www-form-urlencoded'}) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + self._log(f'[CrossPoint] delete OK: {resp.status}') + except urllib.error.HTTPError as exc: + err_body = exc.read().decode('utf-8', 'ignore') if exc.fp else '' + self._log(f'[CrossPoint] delete error {exc.code}: {err_body}') + raise ControlError(desc=f'Delete failed: {exc.code} {err_body}') + except Exception as exc: + raise ControlError(desc=f'Delete failed: {exc}') def remove_books_from_metadata(self, paths, booklists): def norm(p): diff --git a/crosspoint_reader/ws_client.py b/crosspoint_reader/ws_client.py index d87fa0b..288ef02 100644 --- a/crosspoint_reader/ws_client.py +++ b/crosspoint_reader/ws_client.py @@ -189,6 +189,21 @@ def _broadcast_from_host(host): return '.'.join(parts) +def _local_broadcast_addrs(): + addrs = set() + try: + for info in socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET): + ip = info[4][0] + if ip.startswith('127.'): + continue + bcast = _broadcast_from_host(ip) + if bcast: + addrs.add(bcast) + except Exception: + pass + return addrs + + def discover_device(timeout=2.0, debug=False, logger=None, extra_hosts=None): ports = [8134, 54982, 48123, 39001, 44044, 59678] local_port = 0 @@ -210,6 +225,11 @@ def discover_device(timeout=2.0, debug=False, logger=None, extra_hosts=None): pass targets = [] + for bcast in _local_broadcast_addrs(): + _log(logger, debug, f'[CrossPoint WS] discovery subnet broadcast {bcast}') + for port in ports: + targets.append((bcast, port)) + # 255.255.255.255 as fallback — works on Linux/Windows, silently fails on macOS for port in ports: targets.append(('255.255.255.255', port)) for host in extra_hosts or []: @@ -240,6 +260,9 @@ def discover_device(timeout=2.0, debug=False, logger=None, extra_hosts=None): text = data.decode('utf-8', 'ignore') except Exception: continue + if not text.startswith('crosspoint'): + _log(logger, debug, f'[CrossPoint WS] discovery ignoring non-crosspoint response: {text}') + continue semi = text.find(';') port = 81 if semi != -1: