mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Save tokens
This commit is contained in:
187
CLAUDE.md
Normal file
187
CLAUDE.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
bonob is a Sonos SMAPI (Sonos Music API) implementation that bridges Subsonic API clones (like Navidrome and Gonic) with Sonos devices. It acts as a middleware service that translates between the Subsonic API and Sonos's proprietary music service protocol, allowing users to stream their personal music libraries to Sonos speakers.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Building and Running
|
||||||
|
```bash
|
||||||
|
# Build TypeScript to JavaScript
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Development mode with auto-reload (requires environment variables)
|
||||||
|
npm run dev
|
||||||
|
# OR with auto-registration
|
||||||
|
npm run devr
|
||||||
|
|
||||||
|
# Register bonob service with Sonos devices
|
||||||
|
npm run register-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# Run tests in watch mode
|
||||||
|
npm run testw
|
||||||
|
|
||||||
|
# Set custom test timeout (default: 5000ms)
|
||||||
|
JEST_TIMEOUT=10000 npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables for Development
|
||||||
|
When running locally, you need to set several environment variables:
|
||||||
|
- `BNB_DEV_HOST_IP`: Your machine's IP address (so Sonos can reach bonob)
|
||||||
|
- `BNB_DEV_SONOS_DEVICE_IP`: IP address of a Sonos device for discovery
|
||||||
|
- `BNB_DEV_SUBSONIC_URL`: URL of your Subsonic API server (e.g., Navidrome)
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
**`src/app.ts`** - Application entry point
|
||||||
|
- Reads configuration from environment variables
|
||||||
|
- Initializes all services (Subsonic, Sonos, authentication)
|
||||||
|
- Wires together the Express server with appropriate dependencies
|
||||||
|
- Handles SIGTERM for graceful shutdown
|
||||||
|
|
||||||
|
**`src/server.ts`** - Express HTTP server
|
||||||
|
- Serves web UI for service registration and login
|
||||||
|
- Handles music streaming (`/stream/track/:id`)
|
||||||
|
- Generates icons and cover art (`/icon/...`, `/art/...`)
|
||||||
|
- Serves Sonos-specific XML files (strings, presentation map)
|
||||||
|
- Binds SOAP service for Sonos SMAPI communication
|
||||||
|
|
||||||
|
**`src/smapi.ts`** - Sonos SMAPI SOAP implementation (1200+ lines)
|
||||||
|
- Implements the Sonos Music API Protocol via SOAP/XML
|
||||||
|
- Core operations: `getMetadata`, `getMediaURI`, `search`, `getExtendedMetadata`
|
||||||
|
- Handles authentication flow with link codes and device auth tokens
|
||||||
|
- Manages token refresh and session management
|
||||||
|
- Maps music library concepts to Sonos browse hierarchy
|
||||||
|
|
||||||
|
**`src/subsonic.ts`** - Subsonic API client
|
||||||
|
- Implements `MusicService` and `MusicLibrary` interfaces
|
||||||
|
- Handles authentication with Subsonic servers using token-based auth
|
||||||
|
- Translates between Subsonic data models and bonob's domain types
|
||||||
|
- Supports custom player configurations for transcoding
|
||||||
|
- Special handling for Navidrome (bearer token authentication)
|
||||||
|
- Implements artist image fetching with optional caching
|
||||||
|
|
||||||
|
**`src/music_service.ts`** - Core domain types and interfaces
|
||||||
|
- Defines `MusicService` interface (auth and login)
|
||||||
|
- Defines `MusicLibrary` interface (browsing, search, streaming, rating, scrobbling)
|
||||||
|
- Domain types: `Artist`, `Album`, `Track`, `Playlist`, `Genre`, `Rating`, etc.
|
||||||
|
- Uses `fp-ts` for functional programming patterns (`TaskEither`, `Option`, `Either`)
|
||||||
|
|
||||||
|
**`src/sonos.ts`** - Sonos device discovery and registration
|
||||||
|
- Discovers Sonos devices on the network using SSDP/UPnP
|
||||||
|
- Registers/unregisters bonob as a music service with Sonos systems
|
||||||
|
- Supports both auto-discovery and seed-host based discovery
|
||||||
|
- Uses `@svrooij/sonos` library for device communication
|
||||||
|
|
||||||
|
**`src/smapi_auth.ts`** - Authentication token management
|
||||||
|
- Implements JWT-based SMAPI tokens (token + key pairs)
|
||||||
|
- Handles token verification and expiry
|
||||||
|
- Token refresh flow using `fp-ts` `TaskEither`
|
||||||
|
|
||||||
|
**`src/config.ts`** - Configuration management
|
||||||
|
- Reads and validates environment variables (all prefixed with `BNB_`)
|
||||||
|
- Legacy environment variable support (BONOB_ prefix)
|
||||||
|
- Type-safe configuration with defaults
|
||||||
|
|
||||||
|
### Key Abstractions
|
||||||
|
|
||||||
|
**BUrn (Bonob URN)** - Resource identifier system (`src/burn.ts`)
|
||||||
|
- Format: `{ system: string, resource: string }`
|
||||||
|
- Systems: `subsonic` (for cover art), `external` (for URLs like Spotify images)
|
||||||
|
- Used for abstracting art/image sources across different backends
|
||||||
|
|
||||||
|
**URL Builder** (`src/url_builder.ts`)
|
||||||
|
- Wraps URL manipulation with a builder pattern
|
||||||
|
- Handles context path for reverse proxy deployments
|
||||||
|
- Used throughout for generating URLs that Sonos devices can access
|
||||||
|
|
||||||
|
**Custom Players** (`src/subsonic.ts`)
|
||||||
|
- Allows mime-type specific transcoding configurations
|
||||||
|
- Maps source mime types to transcoded types
|
||||||
|
- Creates custom "client" names in Subsonic (e.g., "bonob+audio/flac")
|
||||||
|
- Example: `BNB_SUBSONIC_CUSTOM_CLIENTS="audio/flac>audio/mp3"`
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
1. **Sonos App Request** → SOAP endpoint (`/ws/sonos`)
|
||||||
|
2. **SOAP Service** → Verifies auth token, calls `MusicLibrary` methods
|
||||||
|
3. **MusicLibrary** → Makes Subsonic API calls, transforms data
|
||||||
|
4. **SOAP Response** → Returns XML formatted for Sonos
|
||||||
|
|
||||||
|
For streaming:
|
||||||
|
1. **Sonos Device** → `GET /stream/track/:id` with custom headers (bnbt, bnbk)
|
||||||
|
2. **Stream Handler** → Verifies token, calls `MusicLibrary.stream()`
|
||||||
|
3. **Subsonic Stream** → Proxies audio with proper mime-type handling
|
||||||
|
4. **Response** → Streams audio to Sonos, reports "now playing"
|
||||||
|
|
||||||
|
### Icon System (`src/icon.ts`)
|
||||||
|
- SVG-based icon generation with dynamic colors
|
||||||
|
- Supports foreground/background color customization via `BNB_ICON_FOREGROUND_COLOR` and `BNB_ICON_BACKGROUND_COLOR`
|
||||||
|
- Genre-specific icons
|
||||||
|
- Text overlay support (e.g., year icons like "1984")
|
||||||
|
- Holiday/festival decorations (auto-applied based on date)
|
||||||
|
- Legacy mode: renders to 80x80 PNG for older Sonos systems
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
1. Sonos app requests link code via `getAppLink()`
|
||||||
|
2. User visits login URL with link code
|
||||||
|
3. User enters Subsonic credentials
|
||||||
|
4. bonob validates with Subsonic, generates service token
|
||||||
|
5. bonob associates link code with service token
|
||||||
|
6. Sonos polls `getDeviceAuthToken()` with link code
|
||||||
|
7. bonob returns SMAPI token (JWT) to Sonos
|
||||||
|
8. Subsequent requests use SMAPI token, which maps to service token
|
||||||
|
|
||||||
|
### Testing Philosophy
|
||||||
|
- Jest with ts-jest preset
|
||||||
|
- In-memory implementations for `LinkCodes`, `APITokens` for testing
|
||||||
|
- Mocking with `ts-mockito`
|
||||||
|
- Test helpers in `tests/` directory
|
||||||
|
- Console.log suppressed in tests (see `tests/setup.js`)
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Use `fp-ts` `TaskEither<AuthFailure, T>` for async operations that can fail with auth errors
|
||||||
|
- SOAP faults for Sonos-specific errors (see SMAPI_FAULT_* constants)
|
||||||
|
- Promise-based error handling with `.catch()` for most async operations
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
- Strict TypeScript (`strict: true`, `noImplicitAny: true`, `noUncheckedIndexedAccess: true`)
|
||||||
|
- Extensive use of discriminated unions
|
||||||
|
- Interface-based design for pluggable services
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
- Winston-based logger (`src/logger.ts`)
|
||||||
|
- Log level controlled by `BNB_LOG_LEVEL`
|
||||||
|
- Request logging optional via `BNB_SERVER_LOG_REQUESTS`
|
||||||
|
|
||||||
|
### Functional Programming
|
||||||
|
- Heavy use of `fp-ts` for `Option`, `Either`, `TaskEither`
|
||||||
|
- Pipe-based composition (`pipe(data, fn1, fn2, ...)`)
|
||||||
|
- Immutable data transformations
|
||||||
|
|
||||||
|
## File Organization
|
||||||
|
- `src/` - TypeScript source code
|
||||||
|
- `tests/` - Jest test files (mirrors src/ structure)
|
||||||
|
- `build/` - Compiled JavaScript (gitignored)
|
||||||
|
- `web/` - HTML templates (Eta templating) and static assets
|
||||||
|
- `typings/` - Custom TypeScript definitions
|
||||||
|
|
||||||
|
## Important Constraints
|
||||||
|
- bonob must be accessible from Sonos devices at `BNB_URL`
|
||||||
|
- `BNB_URL` cannot contain "localhost" (validation error)
|
||||||
|
- Sonos requires specific XML formats (SMAPI WSDL v1.19.6)
|
||||||
|
- Streaming must handle HTTP range requests for seek functionality
|
||||||
|
- Token lifetime (`BNB_AUTH_TIMEOUT`) should be less than Subsonic session timeout
|
||||||
62
log.txt
Normal file
62
log.txt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{"level":"info","message":"Starting bonob with config {\"port\":8200,\"bonobUrl\":{\"url\":\"https://bonob.famkulhanek.com/\"},\"secret\":\"*******\",\"authTimeout\":\"1h\",\"icons\":{\"foregroundColor\":\"black\",\"backgroundColor\":\"#65d7f4\"},\"logRequests\":true,\"sonos\":{\"serviceName\":\"Kulhanek\",\"discovery\":{\"enabled\":false},\"autoRegister\":false,\"sid\":114316248},\"subsonic\":{\"url\":{\"url\":\"https://music.famkulhanek.com/\"}},\"scrobbleTracks\":true,\"reportNowPlaying\":true}","service":"bonob","timestamp":"2025-10-16 09:45:13"}
|
||||||
|
{"level":"info","message":"Listening on 8200 available @ https://bonob.famkulhanek.com/","service":"bonob","timestamp":"2025-10-16 09:45:13"}
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:46"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getAppLink xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><hardware>iPhone14,3</hardware><osVersion>Version 26.0.1 (Build 23A355)</osVersion><sonosAppName>ICRU_iPhone14,3</sonosAppName><callbackPath>sonos-2://x-callback-url/addAccount?state=intId%3Dcom%2Efamkulhanek%2Emusic</callbackPath></getAppLink></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:46"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:46"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:46"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><getAppLinkResponse xmlns=\"http://www.sonos.com/Services/1.1\"><getAppLinkResult><authorizeAccount><appUrlStringId>AppLinkMessage</appUrlStringId><deviceLink><regUrl>https://bonob.famkulhanek.com/login?linkCode=42504097-231c-4abb-a878-cecffcc0666f</regUrl><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode><showLinkCode>false</showLinkCode></deviceLink></authorizeAccount></getAppLinkResult></getAppLinkResponse></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:46"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:45:46 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":"/login (req[accept-language]=de-DE,de;q=0.9)","service":"bonob","timestamp":"2025-10-16 09:45:49"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:45:49 +0000] "GET /login?linkCode=42504097-231c-4abb-a878-cecffcc0666f HTTP/1.1" 200 1240 "-" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0.1 Mobile/15E148 Safari/604.1"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getDeviceAuthToken xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode></getDeviceAuthToken></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
{"level":"info","message":"Client not linked, awaiting user to associate account with link code by logging in.","service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><soap:Fault><faultcode>Client.NOT_LINKED_RETRY</faultcode><faultstring>Link Code not found yet, sonos app will keep polling until you log in to bonob</faultstring><detail><ExceptionInfo>NOT_LINKED_RETRY</ExceptionInfo><SonosError>5</SonosError></detail></soap:Fault></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:51"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:45:51 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getDeviceAuthToken xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode></getDeviceAuthToken></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
{"level":"info","message":"Client not linked, awaiting user to associate account with link code by logging in.","service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><soap:Fault><faultcode>Client.NOT_LINKED_RETRY</faultcode><faultstring>Link Code not found yet, sonos app will keep polling until you log in to bonob</faultstring><detail><ExceptionInfo>NOT_LINKED_RETRY</ExceptionInfo><SonosError>5</SonosError></detail></soap:Fault></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:45:57"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:45:57 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getDeviceAuthToken xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode></getDeviceAuthToken></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
{"level":"info","message":"Client not linked, awaiting user to associate account with link code by logging in.","service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><soap:Fault><faultcode>Client.NOT_LINKED_RETRY</faultcode><faultstring>Link Code not found yet, sonos app will keep polling until you log in to bonob</faultstring><detail><ExceptionInfo>NOT_LINKED_RETRY</ExceptionInfo><SonosError>5</SonosError></detail></soap:Fault></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:02"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:02 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getDeviceAuthToken xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode></getDeviceAuthToken></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
{"level":"info","message":"Client not linked, awaiting user to associate account with link code by logging in.","service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><soap:Fault><faultcode>Client.NOT_LINKED_RETRY</faultcode><faultstring>Link Code not found yet, sonos app will keep polling until you log in to bonob</faultstring><detail><ExceptionInfo>NOT_LINKED_RETRY</ExceptionInfo><SonosError>5</SonosError></detail></soap:Fault></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:07"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:07 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":"/login (req[accept-language]=de-DE,de;q=0.9)","service":"bonob","timestamp":"2025-10-16 09:46:08"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:09 +0000] "POST /login HTTP/1.1" 200 817 "https://bonob.famkulhanek.com/login?linkCode=42504097-231c-4abb-a878-cecffcc0666f" "Mozilla/5.0 (iPhone; CPU iPhone OS 18_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0.1 Mobile/15E148 Safari/604.1"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider></credentials></s:Header><s:Body><getDeviceAuthToken xmlns=\"http://www.sonos.com/Services/1.1\"><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId><linkCode>42504097-231c-4abb-a878-cecffcc0666f</linkCode></getDeviceAuthToken></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
{"level":"debug","message":"Adding token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0 {\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0\",\"key\":\"64a930fd-475d-4285-85b1-185f1d358453\"}","service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><getDeviceAuthTokenResponse xmlns=\"http://www.sonos.com/Services/1.1\"><getDeviceAuthTokenResult><authToken>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0</authToken><privateKey>64a930fd-475d-4285-85b1-185f1d358453</privateKey><userInfo><nickname>wolfgang</nickname><userIdHashCode>86c12ba6737d0873c383445f01db4c6c691579efc0110dd4537bc34b7f5e3e6d</userIdHashCode></userInfo></getDeviceAuthTokenResult></getDeviceAuthTokenResponse></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:13"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:13 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
{"level":"debug","message":{"data":"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceId>00-00-00-00-00-00:0</deviceId><deviceProvider>Sonos</deviceProvider><loginToken><token>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0</token><key>64a930fd-475d-4285-85b1-185f1d358453</key><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId></loginToken></credentials></s:Header><s:Body><reportAccountAction xmlns=\"http://www.sonos.com/Services/1.1\"><type>addAccount</type></reportAccountAction></s:Body></s:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
{"level":"info","message":"Sonos reportAccountAction: {\"type\":\"addAccount\"} Headers: {\"host\":\"bonob.famkulhanek.com\",\"user-agent\":\"Linux UPnP/1.0 Sonos/91.0-70070\",\"content-length\":\"1242\",\"accept\":\"text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2\",\"accept-encoding\":\"gzip\",\"cache-control\":\"no-cache\",\"content-type\":\"text/xml; charset=utf-8\",\"pragma\":\"no-cache\",\"soapaction\":\"\\\"http://www.sonos.com/Services/1.1#reportAccountAction\\\"\",\"x-forwarded-for\":\"44.205.206.64\",\"x-forwarded-host\":\"bonob.famkulhanek.com\",\"x-forwarded-port\":\"443\",\"x-forwarded-proto\":\"https\",\"x-forwarded-server\":\"ea7d24592c56\",\"x-real-ip\":\"44.205.206.64\"}","service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><reportAccountActionResponse xmlns=\"http://www.sonos.com/Services/1.1\"></reportAccountActionResponse></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:17"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:17 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "Linux UPnP/1.0 Sonos/91.0-70070"
|
||||||
|
{"level":"debug","message":{"data":"Handling POST on /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":{"data":"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns=\"http://www.sonos.com/Services/1.1\"><soap:Header><credentials xmlns=\"http://www.sonos.com/Services/1.1\"><deviceProvider>Sonos</deviceProvider><loginToken><token>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0</token><householdId>Sonos_wG6xlFpEtv2adIteHFXs7nRgw1_e81df8b1</householdId></loginToken></credentials></soap:Header><soap:Body><ns:getMetadata xmlns=\"http://www.sonos.com/Services/1.1\"><id>root</id><index>0</index><count>100</count></ns:getMetadata></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":{"data":"Attempting to bind to /ws/sonos","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":{"data":"Trying SonosSoap from path /Test/TestService.php","level":"info"},"service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":"getCredentialsForToken called with: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0","service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":"Current tokens: {\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0\":{\"token\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlVG9rZW4iOiJleUoxYzJWeWJtRnRaU0k2SW5kdmJHWm5ZVzVuSWl3aWNHRnpjM2R2Y21RaU9pSlRkSEpoYm1kc1pTMUNZV2RtZFd3d0xVeGxaMmRwYm1keklpd2lZbVZoY21WeUlqb2laWGxLYUdKSFkybFBhVXBKVlhwSk1VNXBTWE5KYmxJMVkwTkpOa2xyY0ZoV1EwbzVMbVY1U21oYVJ6QnBUMjFhYUdKSVRteE1RMHBzWlVoQmFVOXFSVE5PYWtFelRucE5NVTVxYTNOSmJXeG9aRU5KTmsxVVl6Sk5SRmwzVFVSak1rOVRkMmxoV0U1NlNXcHZhVlJyVVdsTVEwcDZaRmRKYVU5cFNqTmlNbmh0V2pKR2RWcDVTWE5KYmxad1drTkpOa2xxVWxKU1YxbDVVakpTZVdOV1VsSmpNVkp0WTFVMWVXTnFTVEZTVlRocFpsRXVhbVZ2VkdGcFMxRkdMVXhLYVVvdFNtVnlUR1owZVcxVWRVbEhWRTVUZWtFNVFWVkZhM2hOUzA5RFJTSXNJblI1Y0dVaU9pSnVZWFpwWkhKdmJXVWlmUT09IiwiaWF0IjoxNzYwNjAwNzczLCJleHAiOjE3NjA2MDQzNzN9.CizS-PP_uS-wLSnBm7fiIXGg_HXHcaQQHCtLnXsMdE0\",\"key\":\"64a930fd-475d-4285-85b1-185f1d358453\"}}","service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
{"level":"debug","message":{"data":"<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:tns=\"http://www.sonos.com/Services/1.1\"><soap:Body><soap:Fault><faultcode>Client.LoginUnauthorized</faultcode><faultstring>Failed to authenticate, try Re-Authorising your account in the sonos app</faultstring></soap:Fault></soap:Body></soap:Envelope>","level":"debug"},"service":"bonob","timestamp":"2025-10-16 09:46:23"}
|
||||||
|
::ffff:10.88.0.1 - - [16/Oct/2025:07:46:23 +0000] "POST /ws/sonos HTTP/1.1" 200 - "-" "com.sonos.SonosController2/80.30 iPhone14,3 iOS/26.0.1 CFNetwork/1.0 Darwin/25.0.0 (ICRU_iPhone14,3) (Sonos/Universal-Content-Service 1.1.998)"
|
||||||
25
package-lock.json
generated
25
package-lock.json
generated
@@ -3024,31 +3024,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/encoding": {
|
|
||||||
"version": "0.1.13",
|
|
||||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
|
|
||||||
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"iconv-lite": "^0.6.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/encoding/node_modules/iconv-lite": {
|
|
||||||
"version": "0.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
||||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import sonos, { bonobService } from "./sonos";
|
|||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_service";
|
||||||
import { SystemClock } from "./clock";
|
import { SystemClock } from "./clock";
|
||||||
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
import { JWTSmapiLoginTokens } from "./smapi_auth";
|
||||||
|
import { FileSmapiTokenStore } from "./smapi_token_store";
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
const clock = SystemClock;
|
const clock = SystemClock;
|
||||||
@@ -95,7 +96,8 @@ const app = server(
|
|||||||
logRequests: config.logRequests,
|
logRequests: config.logRequests,
|
||||||
version,
|
version,
|
||||||
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
smapiAuthTokens: new JWTSmapiLoginTokens(clock, config.secret, config.authTimeout),
|
||||||
externalImageResolver: artistImageFetcher
|
externalImageResolver: artistImageFetcher,
|
||||||
|
smapiTokenStore: new FileSmapiTokenStore("/config/tokens.json")
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import {
|
|||||||
JWTSmapiLoginTokens,
|
JWTSmapiLoginTokens,
|
||||||
SmapiAuthTokens,
|
SmapiAuthTokens,
|
||||||
} from "./smapi_auth";
|
} from "./smapi_auth";
|
||||||
|
import { SmapiTokenStore, InMemorySmapiTokenStore } from "./smapi_token_store";
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ export type ServerOpts = {
|
|||||||
version: string;
|
version: string;
|
||||||
smapiAuthTokens: SmapiAuthTokens;
|
smapiAuthTokens: SmapiAuthTokens;
|
||||||
externalImageResolver: ImageFetcher;
|
externalImageResolver: ImageFetcher;
|
||||||
|
smapiTokenStore: SmapiTokenStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
const DEFAULT_SERVER_OPTS: ServerOpts = {
|
||||||
@@ -108,6 +110,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = {
|
|||||||
"1m"
|
"1m"
|
||||||
),
|
),
|
||||||
externalImageResolver: axiosImageFetcher,
|
externalImageResolver: axiosImageFetcher,
|
||||||
|
smapiTokenStore: new InMemorySmapiTokenStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function server(
|
function server(
|
||||||
@@ -607,7 +610,8 @@ function server(
|
|||||||
apiTokens,
|
apiTokens,
|
||||||
clock,
|
clock,
|
||||||
i8n,
|
i8n,
|
||||||
serverOpts.smapiAuthTokens
|
serverOpts.smapiAuthTokens,
|
||||||
|
serverOpts.smapiTokenStore
|
||||||
);
|
);
|
||||||
|
|
||||||
if (serverOpts.applyContextPath) {
|
if (serverOpts.applyContextPath) {
|
||||||
|
|||||||
24
src/smapi.ts
24
src/smapi.ts
@@ -40,6 +40,7 @@ import {
|
|||||||
} from "./smapi_auth";
|
} from "./smapi_auth";
|
||||||
import { InvalidTokenError } from "./smapi_auth";
|
import { InvalidTokenError } from "./smapi_auth";
|
||||||
import { IncomingHttpHeaders } from "http2";
|
import { IncomingHttpHeaders } from "http2";
|
||||||
|
import { SmapiTokenStore } from "./smapi_token_store";
|
||||||
|
|
||||||
export const LOGIN_ROUTE = "/login";
|
export const LOGIN_ROUTE = "/login";
|
||||||
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
|
export const CREATE_REGISTRATION_ROUTE = "/registration/add";
|
||||||
@@ -164,20 +165,20 @@ class SonosSoap {
|
|||||||
bonobUrl: URLBuilder;
|
bonobUrl: URLBuilder;
|
||||||
smapiAuthTokens: SmapiAuthTokens;
|
smapiAuthTokens: SmapiAuthTokens;
|
||||||
clock: Clock;
|
clock: Clock;
|
||||||
tokens: {[tokenKey:string]:SmapiToken};
|
tokenStore: SmapiTokenStore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
bonobUrl: URLBuilder,
|
bonobUrl: URLBuilder,
|
||||||
linkCodes: LinkCodes,
|
linkCodes: LinkCodes,
|
||||||
smapiAuthTokens: SmapiAuthTokens,
|
smapiAuthTokens: SmapiAuthTokens,
|
||||||
clock: Clock
|
clock: Clock,
|
||||||
|
tokenStore: SmapiTokenStore
|
||||||
) {
|
) {
|
||||||
this.bonobUrl = bonobUrl;
|
this.bonobUrl = bonobUrl;
|
||||||
this.linkCodes = linkCodes;
|
this.linkCodes = linkCodes;
|
||||||
this.smapiAuthTokens = smapiAuthTokens;
|
this.smapiAuthTokens = smapiAuthTokens;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
this.tokens = {};
|
this.tokenStore = tokenStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
getAppLink(): GetAppLinkResult {
|
getAppLink(): GetAppLinkResult {
|
||||||
@@ -244,17 +245,17 @@ class SonosSoap {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
getCredentialsForToken(token: string): SmapiToken {
|
getCredentialsForToken(token: string): SmapiToken | undefined {
|
||||||
logger.debug("getCredentialsForToken called with: " + token);
|
logger.debug("getCredentialsForToken called with: " + token);
|
||||||
logger.debug("Current tokens: " + JSON.stringify(this.tokens));
|
logger.debug("Current tokens: " + JSON.stringify(this.tokenStore.getAll()));
|
||||||
return this.tokens[token]!;
|
return this.tokenStore.get(token);
|
||||||
}
|
}
|
||||||
associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) {
|
associateCredentialsForToken(token: string, fullSmapiToken: SmapiToken, oldToken?:string) {
|
||||||
logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken));
|
logger.debug("Adding token: " + token + " " + JSON.stringify(fullSmapiToken));
|
||||||
if(oldToken) {
|
if(oldToken) {
|
||||||
delete this.tokens[oldToken];
|
this.tokenStore.delete(oldToken);
|
||||||
}
|
}
|
||||||
this.tokens[token] = fullSmapiToken;
|
this.tokenStore.set(token, fullSmapiToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,9 +404,10 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
apiKeys: APITokens,
|
apiKeys: APITokens,
|
||||||
clock: Clock,
|
clock: Clock,
|
||||||
i8n: I8N,
|
i8n: I8N,
|
||||||
smapiAuthTokens: SmapiAuthTokens
|
smapiAuthTokens: SmapiAuthTokens,
|
||||||
|
tokenStore: SmapiTokenStore
|
||||||
) {
|
) {
|
||||||
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock);
|
const sonosSoap = new SonosSoap(bonobUrl, linkCodes, smapiAuthTokens, clock, tokenStore);
|
||||||
|
|
||||||
const urlWithToken = (accessToken: string) =>
|
const urlWithToken = (accessToken: string) =>
|
||||||
bonobUrl.append({
|
bonobUrl.append({
|
||||||
|
|||||||
96
src/smapi_token_store.ts
Normal file
96
src/smapi_token_store.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import logger from "./logger";
|
||||||
|
import { SmapiToken } from "./smapi_auth";
|
||||||
|
|
||||||
|
export interface SmapiTokenStore {
|
||||||
|
get(token: string): SmapiToken | undefined;
|
||||||
|
set(token: string, fullSmapiToken: SmapiToken): void;
|
||||||
|
delete(token: string): void;
|
||||||
|
getAll(): { [tokenKey: string]: SmapiToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemorySmapiTokenStore implements SmapiTokenStore {
|
||||||
|
private tokens: { [tokenKey: string]: SmapiToken } = {};
|
||||||
|
|
||||||
|
get(token: string): SmapiToken | undefined {
|
||||||
|
return this.tokens[token];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(token: string, fullSmapiToken: SmapiToken): void {
|
||||||
|
this.tokens[token] = fullSmapiToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(token: string): void {
|
||||||
|
delete this.tokens[token];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): { [tokenKey: string]: SmapiToken } {
|
||||||
|
return this.tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileSmapiTokenStore implements SmapiTokenStore {
|
||||||
|
private tokens: { [tokenKey: string]: SmapiToken } = {};
|
||||||
|
private readonly filePath: string;
|
||||||
|
|
||||||
|
constructor(filePath: string) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.loadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadFromFile(): void {
|
||||||
|
try {
|
||||||
|
// Ensure the directory exists
|
||||||
|
const dir = path.dirname(this.filePath);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
logger.info(`Created token storage directory: ${dir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing tokens if file exists
|
||||||
|
if (fs.existsSync(this.filePath)) {
|
||||||
|
const data = fs.readFileSync(this.filePath, "utf8");
|
||||||
|
this.tokens = JSON.parse(data);
|
||||||
|
logger.info(
|
||||||
|
`Loaded ${Object.keys(this.tokens).length} token(s) from ${this.filePath}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.info(`No existing token file found at ${this.filePath}, starting fresh`);
|
||||||
|
this.tokens = {};
|
||||||
|
this.saveToFile();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to load tokens from ${this.filePath}`, { error });
|
||||||
|
this.tokens = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveToFile(): void {
|
||||||
|
try {
|
||||||
|
const data = JSON.stringify(this.tokens, null, 2);
|
||||||
|
fs.writeFileSync(this.filePath, data, "utf8");
|
||||||
|
logger.debug(`Saved ${Object.keys(this.tokens).length} token(s) to ${this.filePath}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to save tokens to ${this.filePath}`, { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(token: string): SmapiToken | undefined {
|
||||||
|
return this.tokens[token];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(token: string, fullSmapiToken: SmapiToken): void {
|
||||||
|
this.tokens[token] = fullSmapiToken;
|
||||||
|
this.saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(token: string): void {
|
||||||
|
delete this.tokens[token];
|
||||||
|
this.saveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): { [tokenKey: string]: SmapiToken } {
|
||||||
|
return this.tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user