Skip to content

feat(tunein): dynamic TuneIn stream resolution + remove preset overwrite confirmation#117

Merged
scheilch merged 30 commits intomainfrom
feat/tunein-support
May 5, 2026
Merged

feat(tunein): dynamic TuneIn stream resolution + remove preset overwrite confirmation#117
scheilch merged 30 commits intomainfrom
feat/tunein-support

Conversation

@scheilch
Copy link
Copy Markdown
Owner

@scheilch scheilch commented May 2, 2026

Summary

feat(tunein): Resolve TuneIn streams dynamically at playback time

TuneIn stations provide stream URLs that expire or change over time. Previously, the stream URL was resolved once at preset-save time and stored statically. This caused playback failures after the URL expired.

Fix: TuneIn station presets now store only the station UUID ( uneinId). At playback time, the Orion adapter detects the uneinId and resolves a fresh stream URL from the TuneIn API on demand.

Changed files:

  • \bmx/stream_utils.py\ — extract \convert_https_to_http()\ as shared utility (was duplicated in
    outes.py)
  • \bmx/tunein.py\ — apply HTTPS→HTTP conversion during TuneIn stream fetch
  • \bmx/routes.py\ — \custom_stream_playback\ reads \ uneinId\ from Orion payload; delegates to _resolve_tunein_for_orion()\ when no \streamUrl\ is present
  • \devices/client.py\ + \client_adapter.py\ — add \station_uuid\ param to \store_preset(); TuneIn stations without stream URL write \ uneinId\ into Orion JSON payload
  • \presets/service.py\ — pass \station_uuid\ through to \store_preset()\

feat(presets): Remove overwrite confirmation for occupied presets

The confirmation dialog when overwriting an occupied preset slot added friction without meaningful benefit — users already selected a station and a preset slot intentionally. Dialog removed; occupied slots are overwritten directly.

Changed file:

  • \frontend/src/pages/RadioPresets.tsx\ — remove \if (presets[assigningPreset])\ guard that triggered \setPendingStation\

fix(e2e): Add missing intercepts to wizard tests

Wizard E2E and UX screenshot tests were missing \detect-strategy\ and \server-info\ intercepts, causing intermittent failures in CI.

Changed files:

  • \tests/e2e/wizard-full-flow.cy.ts\
  • \tests/e2e/ux/ux-wizard-screenshots.cy.ts\

Test Coverage

  • \test_stream_utils.py\ — new, covers \convert_https_to_http\
  • \test_tunein.py\ — extended with HTTPS→HTTP conversion cases
  • \test_orion_adapter.py\ — extended (+95 lines) covering \ uneinId\ path in \store_preset\

Breaking Changes

None. TuneIn stations saved before this change may need to be re-assigned to pick up the \ uneinId-based storage format.

TuneIn stations have empty stream URLs (resolved lazily via Tune.ashx).
Previously, the empty URL was stored in the preset base64 payload,
causing the Bose device to load forever.

Now store tuneinId in preset data and resolve fresh stream URLs
via TuneIn API when the device requests playback. Also apply
HTTPS-to-HTTP conversion on TuneIn stream URLs (Bose limitation).

Extract convert_https_to_http into shared bmx/stream_utils module.
@scheilch scheilch force-pushed the feat/tunein-support branch from 9817edc to 30d420c Compare May 2, 2026 23:38
oct-support added 23 commits May 3, 2026 01:55
… issues

- fix(security): remove user-controlled data from logger calls in client_adapter.py
- fix(a11y): replace role=dialog div with native dialog element in BugReportModal
- fix(a11y): add readonly props to BugReportModal interface
- refactor(multiroom): use useZoneBuilder hook with i18n messages to eliminate duplication
- test: add coverage for bugReport API, logBuffer, BugReportModal, useZoneBuilder
- chore: add html2canvas mock for test environment
- fix(security): add NOSONAR comment for intentional HTTPS-to-HTTP in stream_utils.py
- fix(a11y): remove onClick from non-interactive overlay div in BugReportModal
- test: expand BugReportModal tests to 13 (submit flow, error handling, toggleDevice)
…closure)

msgs strings are destructured as primitives before callbacks so ESLint
exhaustive-deps can verify them and closures always use current values.

ci: update sonarqube-scan-action v5 -> v6 (security patch)
Cypress 15's built-in webpack/ts-loader sets downlevelIteration:true which
TypeScript 6.0 treats as error TS5101 (deprecated option), crashing all
14 e2e specs. Fix: use @cypress/webpack-preprocessor with transpileOnly:true
to bypass TypeScript type-checker during test bundling. Full type safety
is preserved via tsc --noEmit in the Lint Code step.
In CI (Ubuntu headless Chrome), navigator.language='en' so the app loaded
in English while tests assert German text (weiter, Zurück, Willkommen, etc).
Fix: set localStorage 'oct-lang'='de' before every page load via the global
Cypress window:before:load handler.

wizard-i18n.cy.ts English tests opt out by setting Cypress.env('e2e_locale', 'en')
in their beforeEach, which the global handler respects.
…ish tests

The global window:before:load hook sets 'de' locale for all tests (CI defaults
to English which broke German text assertions). wizard-i18n.cy.ts English tests
use a visitEn() helper that overrides to 'en' per-visit, avoiding the
Cypress.env() incompatibility with allowCypressEnv:false.
Remove unreliable global window:before:load hook (cy.visit onBeforeLoad
cannot override it). Instead, each spec that asserts German text uses a
visitDe() helper that sets oct-lang=de via onBeforeLoad per-visit.

localStorage persists within a test, so only the first visit per test
needs onBeforeLoad; subsequent cy.visit calls inherit the locale.

Affected specs: device-offline, manual-ip-configuration,
wizard-device-persistence, wizard-full-flow.

wizard-i18n.cy.ts reverted to plain cy.visit (CI default is English,
German switch is done via UI in the German describe block).
…viceOffline=true

useNowPlaying polls the real mock device (192.168.1.x) which is unreachable
in CI, causing markDeviceOffline(). The error-message div has condition
{error && !deviceOffline} so it never renders when device is offline.
Fix: intercept now-playing and volume in Error Handling beforeEach.
- wizard-ui-rendering: fix TS syntax error (replace ASCII 0x22 with \\u201D
  in MOJIBAKE_PATTERNS for 🔌 exact pattern); fix all URLs from
  /setup/wizard?step=N to /setup-wizard?step=N; add cy.wait('@getDevices')
- ux-wizard-screenshots: add visitDe() helper and use it for all wizard
  cy.visit calls so CI Chrome gets German locale (fixes /weiter/i not found)
- device-offline: add cy.reload() before cy.wait('@nowPlaying503') in
  'should show device name' test so 503 intercept is active on initial
  component mount (avoids polling-cycle race condition)
- preset-management-advanced: add scrollIntoView() and increase timeout
  to 10000ms on error-message visibility assertions
- wizard-i18n: replace UI-click language switch in German describe block
  with visitDe() helper (selector not present on wizard pages, unreliable)
- wizard-ui-rendering: add now-playing/volume mocks to setupBasicMocks
  preventing 503→offline cascade that blocked Preset Search Modal tests
- wizard-i18n: fix German nav test (Presets label same in DE/EN, check
  Zonen/Einstellungen instead), update dropdown from 4 to 10 languages,
  add now-playing/volume/presets mocks to setupWizardMocks
- preset-management-advanced: add scrollIntoView + increase timeout for
  dismiss error messages test (same overflow:hidden fix pattern)
- Change /api/presets* to /api/presets/* so it matches the actual API
  path /api/presets/{deviceId} (minimatch * does not match /)
- Move cy.wait(@getPresets) into beforeEach since the root page loads
  presets on mount, remove redundant cy.contains(Presets).click() nav
  clicks from each test
- Add __OCT_EXT_RESOLVER__ Vite define flag (default: true)
- Create capabilities.ts as single source of truth
- Gate RadioSearch provider chips, CloudBadge, NowPlaying badge
- Add backend OCT_EXTENDED_RESOLVER env var guard on /api/radio/search
- Dead-code elimination removes all TuneIn code when flag=false
- Add tests for both flag states (frontend + backend)

Co-authored-by: Christian Scheil <christian@scheils.de>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 5, 2026

@scheilch scheilch merged commit 5bfa594 into main May 5, 2026
17 of 20 checks passed
@scheilch scheilch deleted the feat/tunein-support branch May 5, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants