bug fixes

This commit is contained in:
Dallas Groot 2026-04-10 17:02:17 -07:00
parent 2bdac607b4
commit caea96547a
6 changed files with 130 additions and 4 deletions

View file

@ -89,11 +89,32 @@ class WatchConnectivityManager: NSObject, ObservableObject {
wcLog("Started file transfer: \(transfer.file.fileURL.lastPathComponent)")
}
/// Notify the watch that a song's ID3 tags were updated via the Companion API.
/// The watch updates its offline store metadata in-place and invalidates stale caches.
/// Uses sendMessage if reachable, transferUserInfo as fallback (queued delivery on next activation).
func notifyTagsUpdated(songId: String, title: String?, artist: String?,
album: String?, albumArtist: String?) {
guard let session, session.isPaired, session.isWatchAppInstalled else { return }
var payload: [String: Any] = ["type": "tagsUpdated", "songId": songId]
if let v = title { payload["title"] = v }
if let v = artist { payload["artist"] = v }
if let v = album { payload["album"] = v }
if let v = albumArtist { payload["albumArtist"] = v }
if session.isReachable {
session.sendMessage(payload, replyHandler: nil, errorHandler: { [weak self] err in
self?.wcLog("tagsUpdated sendMessage failed — queuing: \(err.localizedDescription)")
session.transferUserInfo(payload)
})
} else {
session.transferUserInfo(payload)
}
wcLog("notifyTagsUpdated: \(songId) title=\(title ?? "-")")
}
/// High-level: send a downloaded song to the watch for offline playback.
/// Always transcodes to MP3 192kbps via the server's stream endpoint for fast transfer.
@discardableResult
func sendSongToWatch(_ song: Song) -> Bool {
wcLog("sendSongToWatch: \(song.title) (id: \(song.id))")
func sendSongToWatch(_ song: Song) -> Bool { wcLog("sendSongToWatch: \(song.title) (id: \(song.id))")
guard let session = session else {
wcLog("FAIL: WCSession is nil")

View file

@ -215,7 +215,20 @@ struct BatchAlbumEditorSheet: View {
await MainActor.run { progress = 0.3 }
let result = try await api.batchEditMetadata(request)
// Notify the watch for each song in the batch
if let songs = album.song {
for song in songs {
WatchConnectivityManager.shared.notifyTagsUpdated(
songId: song.id,
title: nil, // batch edit never changes titles
artist: applyArtistToAll ? (artist.isEmpty ? nil : artist) : nil,
album: albumName.isEmpty ? nil : albumName,
albumArtist: albumArtist.isEmpty ? nil : albumArtist
)
}
}
await MainActor.run {
progress = 1.0
isSaving = false
@ -250,3 +263,7 @@ struct BatchAlbumEditorSheet: View {
.keyboardType(keyboard)
.multilineTextAlignment(.trailing)
.keyboardDoneButton()
}
.font(.system(size: 14))
}
}

View file

@ -296,7 +296,20 @@ struct MultiAlbumEditorSheet: View {
await MainActor.run { progress = 0.3 }
let result = try await api.batchEditMetadata(request)
// Notify the watch for every song across all albums
for album in albums {
for song in album.song ?? [] {
WatchConnectivityManager.shared.notifyTagsUpdated(
songId: song.id,
title: nil,
artist: applyArtistToAll ? (artist.isEmpty ? nil : artist) : nil,
album: albumName.isEmpty ? nil : albumName,
albumArtist: albumArtist.isEmpty ? nil : albumArtist
)
}
}
await MainActor.run {
progress = 1.0
isSaving = false
@ -330,6 +343,7 @@ struct MultiAlbumEditorSheet: View {
TextField(label, text: text)
.keyboardType(keyboard)
.multilineTextAlignment(.trailing)
.keyboardDoneButton()
}
.font(.system(size: 14))
}

View file

@ -250,6 +250,15 @@ struct TrackEditorView: View {
try await api.editMetadata(request)
// Notify the watch so it can update its offline store metadata
WatchConnectivityManager.shared.notifyTagsUpdated(
songId: song.id,
title: request.title,
artist: request.artist,
album: request.album,
albumArtist: request.albumArtist
)
await MainActor.run {
isSaving = false
withAnimation { showSuccess = true }
@ -289,6 +298,7 @@ struct TrackEditorView: View {
.foregroundColor(isEnabled.wrappedValue ? .white : .gray.opacity(0.5))
.disabled(!isEnabled.wrappedValue)
.keyboardDoneButton()
}
.font(.system(size: 13))
}
}

View file

@ -59,6 +59,39 @@ class WatchOfflineStore: NSObject, ObservableObject, URLSessionDownloadDelegate
totalSize += fileSize
saveCatalog()
}
/// Update the embedded Song metadata for an offline song in-place.
/// Called when iOS notifies the watch of tag changes via WatchConnectivity.
func updateSongMetadata(id: String, title: String?, artist: String?,
album: String?, albumArtist: String?) {
guard let idx = songs.firstIndex(where: { $0.id == id }) else { return }
let old = songs[idx]
let updated = Song(
id: old.song.id,
parent: old.song.parent, isDir: old.song.isDir,
title: title ?? old.song.title,
album: album ?? old.song.album,
artist: artist ?? old.song.artist,
track: old.song.track, year: old.song.year, genre: old.song.genre,
coverArt: old.song.coverArt,
size: old.song.size, contentType: old.song.contentType,
suffix: old.song.suffix,
transcodedContentType: old.song.transcodedContentType,
transcodedSuffix: old.song.transcodedSuffix,
duration: old.song.duration, bitRate: old.song.bitRate,
path: old.song.path, playCount: old.song.playCount,
discNumber: old.song.discNumber, created: old.song.created,
albumId: old.song.albumId, artistId: old.song.artistId,
type: old.song.type, starred: old.song.starred,
bpm: old.song.bpm, musicBrainzId: old.song.musicBrainzId
)
songs[idx] = WatchOfflineSong(
id: old.id, song: updated,
localPath: old.localPath, fileSize: old.fileSize, dateAdded: old.dateAdded
)
saveCatalog()
print("[Watch] Updated metadata for \(id): title=\(title ?? "-")")
}
func removeSong(_ songId: String) {
guard let idx = songs.firstIndex(where: { $0.id == songId }) else { return }

View file

@ -239,6 +239,33 @@ class WatchSessionManager: NSObject, ObservableObject {
await MainActor.run { isSyncing = false }
}
// MARK: - Tag Update Handler
private func handleTagsUpdated(_ message: [String: Any]) {
guard let songId = message["songId"] as? String else { return }
let title = message["title"] as? String
let artist = message["artist"] as? String
let album = message["album"] as? String
let albumArtist = message["albumArtist"] as? String
DispatchQueue.main.async {
// Update offline store if this song is downloaded on the watch
WatchOfflineStore.shared.updateSongMetadata(
id: songId, title: title, artist: artist,
album: album, albumArtist: albumArtist
)
// Invalidate stale library caches next fetch will repopulate from server
for key in ["all_albums", "recent", "playlists"] {
UserDefaults.standard.removeObject(forKey: self.cachePrefix + key)
}
// Refresh now-playing display if this song is currently playing
if let t = title { self.phoneNowPlaying?.title = t }
if let a = artist { self.phoneNowPlaying?.artist = a }
if let b = album { self.phoneNowPlaying?.album = b }
print("[Watch] Tags updated for \(songId)")
}
}
// MARK: - Server-Direct Download Handler
/// Called when iPhone tells us to download a song directly from server.
@ -376,6 +403,8 @@ extension WatchSessionManager: WCSessionDelegate {
coverArtId: message["coverArtId"] as? String
)
}
} else if type == "tagsUpdated" {
handleTagsUpdated(message)
} else if type == "downloadSong" {
handleDownloadCommand(message)
}
@ -439,6 +468,8 @@ extension WatchSessionManager: WCSessionDelegate {
print("[Watch] Wake signal received — app is active")
} else if type == "downloadSong" {
handleDownloadCommand(userInfo)
} else if type == "tagsUpdated" {
handleTagsUpdated(userInfo)
}
}
}