diff --git a/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/.xccurrentversion b/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/.xccurrentversion
new file mode 100644
index 0000000..1424570
--- /dev/null
+++ b/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+
+
+
+
+ _XCCurrentVersionName
+ Lyrics 2.xcdatamodel
+
+
diff --git a/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/Lyrics 2.xcdatamodel/contents b/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/Lyrics 2.xcdatamodel/contents
new file mode 100644
index 0000000..9c45a3a
--- /dev/null
+++ b/LyricFever/Models/CoreData/Lyrics.xcdatamodeld/Lyrics 2.xcdatamodel/contents
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LyricFever/Models/CoreData/SongObjectExtensions.swift b/LyricFever/Models/CoreData/SongObjectExtensions.swift
index 54e9007..0719cbf 100644
--- a/LyricFever/Models/CoreData/SongObjectExtensions.swift
+++ b/LyricFever/Models/CoreData/SongObjectExtensions.swift
@@ -22,6 +22,8 @@ extension SongObject {
@NSManaged public var language: String
@NSManaged public var lyricsWords: [String]
@NSManaged public var lyricsTimestamps: [TimeInterval]
+ @NSManaged public var sourceProvider: String?
+ @NSManaged public var userPicked: Bool
}
diff --git a/LyricFever/ViewModel.swift b/LyricFever/ViewModel.swift
index 7eb6658..ea235ff 100644
--- a/LyricFever/ViewModel.swift
+++ b/LyricFever/ViewModel.swift
@@ -24,7 +24,8 @@ import MediaRemoteAdapter
static let shared = ViewModel()
// Apple Music Tahoe broken AppleScript workaround
- let musicController = MediaController(bundleIdentifier: "com.apple.Music")
+ // bundleIdentifier param removed from MediaController.init in adapter b8ce5d1
+ let musicController = MediaController()
// var appleMusicUniqueIdentifier: String?
var currentlyPlaying: String?
@@ -272,7 +273,8 @@ import MediaRemoteAdapter
var localFileUploadProvider = LocalFileUploadProvider()
#endif
@ObservationIgnored lazy var allNetworkLyricProviders: [LyricProvider] = [spotifyLyricProvider, lRCLyricProvider, netEaseLyricProvider]
-
+
+
// custom order because LRCLIB is tweaking for the time being
@ObservationIgnored lazy var allNetworkLyricProvidersForSearch: [LyricProvider] = [spotifyLyricProvider, netEaseLyricProvider, lRCLyricProvider]
@@ -370,7 +372,9 @@ import MediaRemoteAdapter
amplitude.track(eventType: "\(networkLyricProvider.providerName) Fetch")
print("FetchAllNetworkLyrics: returning lyrics from \(networkLyricProvider.providerName)")
// thats how i save to coredata
- let _ = SongObject(from: lyrics.lyrics, with: coreDataContainer.viewContext, trackID: currentlyPlaying, trackName: currentlyPlayingName)
+ let song = SongObject(from: lyrics.lyrics, with: coreDataContainer.viewContext, trackID: currentlyPlaying, trackName: currentlyPlayingName)
+ song.sourceProvider = networkLyricProvider.providerName
+ song.userPicked = false
saveCoreData()
return lyrics
} else if networkLyricProvider is SpotifyLyricProvider {
@@ -383,6 +387,7 @@ import MediaRemoteAdapter
print("Caught exception on \(networkLyricProvider.providerName): \(error)")
}
}
+ saveCoreData()
return NetworkFetchReturn(lyrics: [], colorData: nil)
}
@@ -921,7 +926,24 @@ import MediaRemoteAdapter
func fetchLyrics(for trackID: String, _ trackName: String, checkCoreDataFirst: Bool) async throws -> [LyricLine] {
let initiatingTrackID = trackID
- if checkCoreDataFirst, let lyrics = fetchFromCoreData(for: trackID) {
+ // Sticky check: if the user previously picked lyrics manually (via search window),
+ // skip the network chain entirely and return the cached lyrics as-is.
+ if checkCoreDataFirst {
+ let request = SongObject.fetchRequest()
+ request.predicate = NSPredicate(format: "id == %@", trackID)
+ if let existing = try? coreDataContainer.viewContext.fetch(request).first,
+ existing.userPicked, !existing.lyricsWords.isEmpty {
+ let lyrics = zip(existing.lyricsTimestamps, existing.lyricsWords).map { LyricLine(startTime: $0, words: $1) }
+ try Task.checkCancellation()
+ amplitude.track(eventType: "CoreData Fetch (userPicked sticky)")
+ if initiatingTrackID != self.currentlyPlaying {
+ throw FetchError.staleTrack
+ }
+ return lyrics
+ }
+ }
+
+ if checkCoreDataFirst, let lyrics = fetchFromCoreData(for: trackID), !lyrics.isEmpty {
print("ViewModel FetchLyrics: got lyrics from core data :D \(trackID) \(trackName)")
try Task.checkCancellation()
amplitude.track(eventType: "CoreData Fetch")
@@ -992,6 +1014,24 @@ import MediaRemoteAdapter
}
}
+ /// Deletes the CoreData entry for the current track (including userPicked and
+ /// none_found entries) and resets in-memory state so the next tick re-runs
+ /// the full lyric-fetch chain from scratch.
+ func resetLyricsForCurrentTrack() {
+ guard let trackID = currentlyPlaying else { return }
+ let ctx = coreDataContainer.viewContext
+ let request: NSFetchRequest = SongObject.fetchRequest()
+ request.predicate = NSPredicate(format: "id == %@", trackID)
+ if let existing = try? ctx.fetch(request).first {
+ ctx.delete(existing)
+ saveCoreData()
+ }
+ // Reset in-memory state; the next player-change tick will re-fetch.
+ currentlyPlayingLyrics = []
+ currentFetchTask?.cancel()
+ setCurrentProperties()
+ }
+
func fetchFromCoreData(for trackID: String) -> [LyricLine]? {
let fetchRequest: NSFetchRequest = SongObject.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", trackID) // Replace trackID with the desired value
diff --git a/LyricFever/Views/MenubarWindowView/MenubarWindowView.swift b/LyricFever/Views/MenubarWindowView/MenubarWindowView.swift
index d27c66a..7a12654 100644
--- a/LyricFever/Views/MenubarWindowView/MenubarWindowView.swift
+++ b/LyricFever/Views/MenubarWindowView/MenubarWindowView.swift
@@ -380,7 +380,14 @@ struct MenubarWindowView: View {
}
return .clickable
}
-
+
+ var resetState: ButtonState {
+ guard viewmodel.userDefaultStorage.hasOnboarded else {
+ return .disabled
+ }
+ return .clickable
+ }
+
var deleteOrUploadState: ButtonState {
guard viewmodel.userDefaultStorage.hasOnboarded else {
return .disabled
@@ -433,6 +440,11 @@ struct MenubarWindowView: View {
currentHoveredItem = .none
}
}
+ SmallMenubarButton(buttonText: "", imageText: "arrow.counterclockwise", buttonState: resetState) {
+ viewmodel.resetLyricsForCurrentTrack()
+ }
+ .help("Reset lyrics for this track (clears cache, re-runs chain)")
+ .disabled(resetState == .disabled)
Menu {
translationAndRomanizationView
} label: {
diff --git a/LyricFever/Views/SearchView/SearchWindow.swift b/LyricFever/Views/SearchView/SearchWindow.swift
index 7acc283..94df4c9 100644
--- a/LyricFever/Views/SearchView/SearchWindow.swift
+++ b/LyricFever/Views/SearchView/SearchWindow.swift
@@ -74,7 +74,9 @@ struct SearchWindow: View {
return
}
// thats how i save to coredata
- let _ = SongObject(from: cleanLyrics, with: viewmodel.coreDataContainer.viewContext, trackID: spotifyID, trackName: trackName)
+ let song = SongObject(from: cleanLyrics, with: viewmodel.coreDataContainer.viewContext, trackID: spotifyID, trackName: trackName)
+ song.userPicked = true
+ song.sourceProvider = "user_picked"
viewmodel.saveCoreData()
lyricsAreApplied = true
} label: {