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: {