Skip to content

Commit f23f855

Browse files
committed
Fix pairing
1 parent dab63ac commit f23f855

4 files changed

Lines changed: 86 additions & 73 deletions

File tree

internal/airplay/credentials.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ func (cs *CredentialStore) Lookup(deviceID string) *SavedCredentials {
114114
return cs.devices[""]
115115
}
116116

117+
// Len returns the number of stored credential entries.
118+
func (cs *CredentialStore) Len() int {
119+
cs.mu.Lock()
120+
defer cs.mu.Unlock()
121+
return len(cs.devices)
122+
}
123+
124+
// Import adds a credential entry without persisting (used for legacy migration at startup).
125+
func (cs *CredentialStore) Import(deviceID string, creds *SavedCredentials) {
126+
cs.mu.Lock()
127+
defer cs.mu.Unlock()
128+
cs.devices[deviceID] = creds
129+
}
130+
117131
// Save stores credentials for a device and persists to disk.
118132
func (cs *CredentialStore) Save(deviceID string, pairingID string, pub ed25519.PublicKey, priv ed25519.PrivateKey) error {
119133
cs.mu.Lock()

internal/daemon/daemon.go

Lines changed: 67 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ func New(cfg Config) (*Daemon, error) {
110110
if err != nil {
111111
return nil, fmt.Errorf("load credentials: %w", err)
112112
}
113+
114+
// If the credential store is empty, try importing from the legacy file
115+
if cs.Len() == 0 && cfg.CredFile != "" {
116+
legacyCreds, lerr := airplay.LoadCredentials(cfg.CredFile)
117+
if lerr == nil && legacyCreds != nil {
118+
cs.Import("", legacyCreds)
119+
log.Printf("[daemon] imported legacy credentials from %s", cfg.CredFile)
120+
}
121+
}
122+
113123
return &Daemon{
114124
cfg: cfg,
115125
state: StateIdle,
@@ -404,43 +414,72 @@ func (d *Daemon) connectAndStream(ctx context.Context, target string, port int,
404414

405415
log.Printf("[daemon] connected to %s (model: %s, deviceID: %s)", info.Name, info.Model, deviceID)
406416

407-
// Try saved credentials by DeviceID
417+
// If a PIN was provided, skip credential lookup and go straight to PIN pairing.
418+
// The PIN was displayed during the previous connection attempt; trying pair-verify
419+
// or transient pairing first would reset the device's pairing state and invalidate it.
408420
paired := false
409-
savedCreds := d.credStore.Lookup(deviceID)
410-
if savedCreds != nil {
411-
pub, priv := savedCreds.Ed25519Keys()
412-
client.PairingID = savedCreds.PairingID
413-
client.PairKeys = &airplay.PairKeys{
414-
Ed25519Public: pub,
415-
Ed25519Private: priv,
421+
if pin != "" {
422+
if err := client.Pair(ctx, pin); err != nil {
423+
setErr(fmt.Sprintf("pairing failed: %v", err))
424+
return
416425
}
417-
if err := client.PairVerify(ctx); err != nil {
418-
log.Printf("[daemon] pair-verify with saved creds failed: %v", err)
419-
// Reconnect for fresh pairing attempt
420-
client.Close()
421-
client = airplay.NewAirPlayClient(target, port)
422-
if err := client.Connect(ctx); err != nil {
423-
setErr(fmt.Sprintf("reconnect failed: %v", err))
424-
return
426+
paired = true
427+
// Save the new credentials
428+
if client.PairKeys != nil {
429+
if err := d.credStore.Save(deviceID, client.PairingID,
430+
client.PairKeys.Ed25519Public, client.PairKeys.Ed25519Private); err != nil {
431+
log.Printf("[daemon] warning: failed to save credentials: %v", err)
432+
} else {
433+
log.Printf("[daemon] credentials saved for %s (deviceID: %s)", info.Name, deviceID)
434+
}
435+
}
436+
}
437+
438+
// Try saved credentials by DeviceID
439+
if !paired {
440+
savedCreds := d.credStore.Lookup(deviceID)
441+
if savedCreds != nil {
442+
pub, priv := savedCreds.Ed25519Keys()
443+
client.PairingID = savedCreds.PairingID
444+
client.PairKeys = &airplay.PairKeys{
445+
Ed25519Public: pub,
446+
Ed25519Private: priv,
425447
}
426-
if _, err := client.GetInfo(); err != nil {
427-
setErr(fmt.Sprintf("get info after reconnect failed: %v", err))
428-
return
448+
if err := client.PairVerify(ctx); err != nil {
449+
log.Printf("[daemon] pair-verify with saved creds failed: %v, trying transient pairing", err)
450+
// Reconnect for fresh pairing attempt
451+
client.Close()
452+
client = airplay.NewAirPlayClient(target, port)
453+
if err := client.Connect(ctx); err != nil {
454+
setErr(fmt.Sprintf("reconnect failed: %v", err))
455+
return
456+
}
457+
if _, err := client.GetInfo(); err != nil {
458+
setErr(fmt.Sprintf("get info after reconnect failed: %v", err))
459+
return
460+
}
461+
// Try transient (no-PIN) pairing as fallback
462+
if err := client.Pair(ctx, ""); err != nil {
463+
log.Printf("[daemon] transient pairing also failed: %v", err)
464+
} else {
465+
paired = true
466+
log.Printf("[daemon] transient pairing succeeded for %s", info.Name)
467+
}
468+
} else {
469+
paired = true
470+
log.Printf("[daemon] pair-verify succeeded for %s", info.Name)
429471
}
430-
} else {
431-
paired = true
432-
log.Printf("[daemon] pair-verify succeeded for %s", info.Name)
433472
}
434473
}
435474

436-
// If not paired, we need a PIN
475+
// If still not paired, try transient pairing (no saved creds)
437476
if !paired {
438-
if pin == "" {
439-
// Trigger PIN display on the device and ask the user
477+
if err := client.Pair(ctx, ""); err != nil {
478+
log.Printf("[daemon] transient pairing failed: %v", err)
479+
// Last resort: ask for PIN
440480
if err := client.StartPINDisplay(); err != nil {
441481
log.Printf("[daemon] start PIN display failed: %v", err)
442482
}
443-
// Pause connection — wait for PIN via a subsequent connect command
444483
client.Close()
445484
d.mu.Lock()
446485
d.state = StatePINRequired
@@ -451,21 +490,8 @@ func (d *Daemon) connectAndStream(ctx context.Context, target string, port int,
451490
log.Printf("[daemon] PIN required for %s — waiting for user input", info.Name)
452491
return
453492
}
454-
455-
// PIN was provided — do full pair-setup
456-
if err := client.Pair(ctx, pin); err != nil {
457-
setErr(fmt.Sprintf("pairing failed: %v", err))
458-
return
459-
}
460-
// Save the new credentials
461-
if client.PairKeys != nil {
462-
if err := d.credStore.Save(deviceID, client.PairingID,
463-
client.PairKeys.Ed25519Public, client.PairKeys.Ed25519Private); err != nil {
464-
log.Printf("[daemon] warning: failed to save credentials: %v", err)
465-
} else {
466-
log.Printf("[daemon] credentials saved for %s (deviceID: %s)", info.Name, deviceID)
467-
}
468-
}
493+
paired = true
494+
log.Printf("[daemon] transient pairing succeeded for %s", info.Name)
469495
}
470496

471497
// FairPlay setup

plasmoid/README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ sudo install -m755 doubletake doubletake-ctl /usr/local/bin/
1919

2020
# Install the plasmoid
2121
kpackagetool6 -t Plasma/Applet -i plasmoid/
22-
# Or for development (symlink, auto-reloads):
23-
kpackagetool6 -t Plasma/Applet -i plasmoid/ -p
2422
```
2523

2624
To update after changes:
@@ -80,14 +78,14 @@ systemctl --user enable --now doubletake.service
8078

8179
```
8280
┌─────────────────┐ JSON/Unix socket ┌──────────────────┐
83-
│ Plasma Applet │ ──── doubletake-ctl ─────▶ │ doubletake │
84-
│ (QML/JS) │ │ (Go) │
85-
└─────────────────┘ └──────────────────┘
81+
│ Plasma Applet │ ──── doubletake-ctl ────> │ doubletake
82+
│ (QML/JS) │ │ (Go) │
83+
└─────────────────┘ └──────────────────┘
8684
87-
85+
v
8886
AirPlay protocol
8987
(mDNS, RTSP, FairPlay,
90-
H.264 streaming)
88+
H.264 streaming)
9189
```
9290

9391
The applet runs `doubletake-ctl` as a subprocess to communicate with the daemon.

plasmoid/contents/ui/main.qml

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -163,31 +163,6 @@ PlasmoidItem {
163163
anchors.fill: parent
164164
spacing: 0
165165

166-
// Header bar: title + refresh button
167-
RowLayout {
168-
Layout.fillWidth: true
169-
Layout.leftMargin: Kirigami.Units.smallSpacing
170-
Layout.rightMargin: Kirigami.Units.smallSpacing
171-
Layout.topMargin: Kirigami.Units.smallSpacing
172-
173-
PlasmaExtras.Heading {
174-
Layout.fillWidth: true
175-
level: 4
176-
text: "AirPlay Devices"
177-
}
178-
179-
Controls.ToolButton {
180-
icon.name: "view-refresh"
181-
display: Controls.ToolButton.IconOnly
182-
Controls.ToolTip.text: "Refresh devices"
183-
Controls.ToolTip.visible: hovered
184-
enabled: !root.isBusy
185-
onClicked: {
186-
root.runCtl(["discover"], "discover")
187-
}
188-
}
189-
}
190-
191166
Kirigami.Separator {
192167
Layout.fillWidth: true
193168
}

0 commit comments

Comments
 (0)