Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Lyrics 2.xcdatamodel</string>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24299" systemVersion="25A5349a" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
<entity name="IDToColor" representedClassName="IDToColor" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String"/>
<attribute name="songColor" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="PersistentIDToSpotify" representedClassName="PersistentIDToSpotify" syncable="YES" codeGenerationType="class">
<attribute name="persistentID" optional="YES" attributeType="String"/>
<attribute name="spotifyID" optional="YES" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="persistentID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SongObject" representedClassName="SongObject" syncable="YES">
<attribute name="downloadDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="id" attributeType="String"/>
<attribute name="language" optional="YES" attributeType="String"/>
<attribute name="lyricsTimestamps" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[TimeInterval]"/>
<attribute name="lyricsWords" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[String]"/>
<attribute name="title" attributeType="String"/>
<attribute name="sourceProvider" optional="YES" attributeType="String"/>
<attribute name="userPicked" optional="NO" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="SongToLocale" representedClassName="SongToLocale" syncable="YES" codeGenerationType="class">
<attribute name="id" attributeType="String"/>
<attribute name="locale" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="id"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
</model>
2 changes: 2 additions & 0 deletions LyricFever/Models/CoreData/SongObjectExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

}

Expand Down
48 changes: 44 additions & 4 deletions LyricFever/ViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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 {
Expand All @@ -383,6 +387,7 @@ import MediaRemoteAdapter
print("Caught exception on \(networkLyricProvider.providerName): \(error)")
}
}
saveCoreData()
return NetworkFetchReturn(lyrics: [], colorData: nil)
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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> = 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> = SongObject.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "id == %@", trackID) // Replace trackID with the desired value
Expand Down
14 changes: 13 additions & 1 deletion LyricFever/Views/MenubarWindowView/MenubarWindowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 3 additions & 1 deletion LyricFever/Views/SearchView/SearchWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down