From 97d2d86506816e940064348a36d459ca237c4542 Mon Sep 17 00:00:00 2001 From: Kartik Venugopal Date: Tue, 14 Jan 2025 23:56:52 +0100 Subject: [PATCH] Menu item to remove downloaded lyrics, fixed lyrics precedence --- Aural.xcodeproj/project.pbxproj | 4 +++ Source/Core/Constants/FilesAndPaths.swift | 1 - .../TrackInfoUpdatedNotification.swift | 5 +++- .../LyricsNotifications.swift | 1 + .../Model/MetadataPersistentState.swift | 2 ++ .../Delegate/PlayQueueDelegate+Init.swift | 4 +-- Source/Core/TrackIO/Model/FileMetadata.swift | 5 ++++ .../Core/TrackIO/Model/Track+Metadata.swift | 2 +- Source/Core/TrackIO/TrackInitializer.swift | 22 ++++++++++++++++ Source/Core/TrackIO/TrackReader+Lyrics.swift | 25 ++++++++++++++++-- .../Core/Utils/Extensions/URLExtensions.swift | 12 +++++++++ Source/UI/Lyrics/Lyrics.xib | 2 +- .../Lyrics/LyricsViewController+Timed.swift | 4 +++ Source/UI/Lyrics/LyricsViewController.swift | 4 +++ Source/UI/Menus/Base.lproj/MainMenu.xib | 7 +++++ Source/UI/Menus/MetadataMenuController.swift | 18 +++++++++++++ .../ModularPlayerWindowController.swift | 3 ++- Source/UI/Player/PlayerViewController.swift | 26 +++++++------------ .../UnifiedPlayerWindowController.swift | 3 ++- 19 files changed, 123 insertions(+), 27 deletions(-) create mode 100644 Source/Core/TrackIO/TrackInitializer.swift diff --git a/Aural.xcodeproj/project.pbxproj b/Aural.xcodeproj/project.pbxproj index 945948d41..b82f20268 100644 --- a/Aural.xcodeproj/project.pbxproj +++ b/Aural.xcodeproj/project.pbxproj @@ -844,6 +844,7 @@ 3E9E590E25DB6B150064EB5F /* MenuBarAppModeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E58FF25DB6B150064EB5F /* MenuBarAppModeController.swift */; }; 3E9E590F25DB6B150064EB5F /* AppModeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E590025DB6B150064EB5F /* AppModeManager.swift */; }; 3E9E591025DB6B150064EB5F /* ModularAppModeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9E590125DB6B150064EB5F /* ModularAppModeController.swift */; }; + 3E9F0AE72D370A2600A23BDE /* TrackInitializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E9F0AE62D370A2600A23BDE /* TrackInitializer.swift */; }; 3EA0102925E2725900A80DAC /* FontSchemeHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA0102825E2725900A80DAC /* FontSchemeHistory.swift */; }; 3EA0102D25E28C4C00A80DAC /* FontSchemeChangeContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA0102C25E28C4C00A80DAC /* FontSchemeChangeContext.swift */; }; 3EA07ACA2D27D22B001A5124 /* LyricsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EA07AC92D27D22B001A5124 /* LyricsPreferences.swift */; }; @@ -1972,6 +1973,7 @@ 3E9E58FF25DB6B150064EB5F /* MenuBarAppModeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MenuBarAppModeController.swift; sourceTree = ""; }; 3E9E590025DB6B150064EB5F /* AppModeManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppModeManager.swift; sourceTree = ""; }; 3E9E590125DB6B150064EB5F /* ModularAppModeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModularAppModeController.swift; sourceTree = ""; }; + 3E9F0AE62D370A2600A23BDE /* TrackInitializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackInitializer.swift; sourceTree = ""; }; 3E9F692126B9CE26001FAFA3 /* PlaylistControlsContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistControlsContainer.swift; sourceTree = ""; }; 3EA0102825E2725900A80DAC /* FontSchemeHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSchemeHistory.swift; sourceTree = ""; }; 3EA0102C25E28C4C00A80DAC /* FontSchemeChangeContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSchemeChangeContext.swift; sourceTree = ""; }; @@ -3087,6 +3089,7 @@ 3E0217782C23490E00865AC2 /* FileReaderProtocol.swift */, 3E0217792C23490E00865AC2 /* TrackReader.swift */, 3E28A5782D33DCCB007331AF /* TrackReader+Lyrics.swift */, + 3E9F0AE62D370A2600A23BDE /* TrackInitializer.swift */, ); path = TrackIO; sourceTree = ""; @@ -6170,6 +6173,7 @@ 3E0218D52C23490E00865AC2 /* PlaybackContextProtocol.swift in Sources */, 3E9DA1972D342B080094D6DE /* TextAndImageButtonCell.swift in Sources */, 3E7E9F4E2C66B1AF0011DE8E /* WaveformWindowController.swift in Sources */, + 3E9F0AE72D370A2600A23BDE /* TrackInitializer.swift in Sources */, 3E02189A2C23490E00865AC2 /* MenuBarPlayerUIPersistentState.swift in Sources */, 3E02195C2C23490E00865AC2 /* SearchResults.swift in Sources */, 3E6EFFA42815309500C813AC /* TextColorSchemeViewController.swift in Sources */, diff --git a/Source/Core/Constants/FilesAndPaths.swift b/Source/Core/Constants/FilesAndPaths.swift index 6488bd378..6ba786371 100644 --- a/Source/Core/Constants/FilesAndPaths.swift +++ b/Source/Core/Constants/FilesAndPaths.swift @@ -19,7 +19,6 @@ struct FilesAndPaths { // Default user's music directory (default place to look in, when opening/saving files) static let musicDir: URL = homeDir.appendingPathComponent("/Music", isDirectory: true).resolvedURL -// static let musicDir: URL = URL(fileURLWithPath: "/Volumes/MBP-Ext-4TB/Projects/Aural-Test/Aural-Music") static let baseDir: URL = musicDir.appendingPathComponent("aural", isDirectory: true) static let metadataDir: URL = baseDir.appendingPathComponent("metadata", isDirectory: true) diff --git a/Source/Core/Messaging/NotificationDefinitions/TrackInfoUpdatedNotification.swift b/Source/Core/Messaging/NotificationDefinitions/TrackInfoUpdatedNotification.swift index 5961d31ee..5ffdefbc9 100644 --- a/Source/Core/Messaging/NotificationDefinitions/TrackInfoUpdatedNotification.swift +++ b/Source/Core/Messaging/NotificationDefinitions/TrackInfoUpdatedNotification.swift @@ -23,10 +23,13 @@ struct TrackInfoUpdatedNotification: NotificationPayload { // The track info fields that have been updated. Different UI components may display different fields. let updatedFields: Set - init(updatedTrack: Track, updatedFields: UpdatedTrackInfoField...) { + let destructiveUpdate: Bool + + init(updatedTrack: Track, updatedFields: UpdatedTrackInfoField..., destructiveUpdate: Bool = false) { self.updatedTrack = updatedTrack self.updatedFields = Set(updatedFields) + self.destructiveUpdate = destructiveUpdate } } diff --git a/Source/Core/Messaging/NotificationNames/LyricsNotifications.swift b/Source/Core/Messaging/NotificationNames/LyricsNotifications.swift index aa7f1ae78..4fb08897f 100644 --- a/Source/Core/Messaging/NotificationNames/LyricsNotifications.swift +++ b/Source/Core/Messaging/NotificationNames/LyricsNotifications.swift @@ -17,6 +17,7 @@ extension Notification.Name { static let loadFromFile = Notification.Name("lyrics_loadFromFile") static let addLyricsFile = Notification.Name("lyrics_addLyricsFile") static let searchForLyricsOnline = Notification.Name("lyrics_searchForLyricsOnline") + static let removeDownloadedLyrics = Notification.Name("lyrics_removeDownloadedLyrics") static let karaokeModePreferenceUpdated = Notification.Name("lyrics_karaokeModePreferenceUpdated") } } diff --git a/Source/Core/Persistence/Model/MetadataPersistentState.swift b/Source/Core/Persistence/Model/MetadataPersistentState.swift index e4a49ad29..12e63a775 100644 --- a/Source/Core/Persistence/Model/MetadataPersistentState.swift +++ b/Source/Core/Persistence/Model/MetadataPersistentState.swift @@ -50,6 +50,7 @@ struct FileMetadataPersistentState: Codable { let timedLyrics: TimedLyricsPersistentState? let externalLyricsFile: URL? + let lyricsDownloaded: Bool? let nonEssentialMetadata: [String: MetadataEntry] @@ -100,6 +101,7 @@ struct FileMetadataPersistentState: Codable { } self.externalLyricsFile = metadata.externalLyricsFile + self.lyricsDownloaded = metadata.lyricsDownloaded self.nonEssentialMetadata = metadata.nonEssentialMetadata diff --git a/Source/Core/PlayQueue/Delegate/PlayQueueDelegate+Init.swift b/Source/Core/PlayQueue/Delegate/PlayQueueDelegate+Init.swift index fde491a30..0cc70c526 100644 --- a/Source/Core/PlayQueue/Delegate/PlayQueueDelegate+Init.swift +++ b/Source/Core/PlayQueue/Delegate/PlayQueueDelegate+Init.swift @@ -40,8 +40,8 @@ extension PlayQueueDelegate { case .rememberFromLastAppLaunch: - if let tracks = persistentState?.tracks, tracks.isNonEmpty { - loadTracks(from: tracks, params: pqParmsWithAutoplayAndNoHistory) + if let urls = persistentState?.tracks, urls.isNonEmpty { + loadTracks(from: urls, params: pqParmsWithAutoplayAndNoHistory) } case .loadPlaylistFile: diff --git a/Source/Core/TrackIO/Model/FileMetadata.swift b/Source/Core/TrackIO/Model/FileMetadata.swift index c1d2d4c05..68aef51f5 100644 --- a/Source/Core/TrackIO/Model/FileMetadata.swift +++ b/Source/Core/TrackIO/Model/FileMetadata.swift @@ -69,6 +69,7 @@ class FileMetadata { var externalLyricsFile: URL? var externalTimedLyrics: TimedLyrics? + var lyricsDownloaded: Bool = false var nonEssentialMetadata: [String: MetadataEntry] = [:] @@ -140,6 +141,10 @@ class FileMetadata { self.externalLyricsFile = persistentState.externalLyricsFile + if let lyricsDownloaded = persistentState.lyricsDownloaded { + self.lyricsDownloaded = lyricsDownloaded + } + self.nonEssentialMetadata = persistentState.nonEssentialMetadata self.art = persistentCoverArt diff --git a/Source/Core/TrackIO/Model/Track+Metadata.swift b/Source/Core/TrackIO/Model/Track+Metadata.swift index bc1b08809..0f8495020 100644 --- a/Source/Core/TrackIO/Model/Track+Metadata.swift +++ b/Source/Core/TrackIO/Model/Track+Metadata.swift @@ -156,7 +156,7 @@ extension Track { } var hasExternalLyrics: Bool { - metadata.externalLyricsFile != nil || metadata.externalTimedLyrics != nil + metadata.externalTimedLyrics != nil } // Non-essential metadata diff --git a/Source/Core/TrackIO/TrackInitializer.swift b/Source/Core/TrackIO/TrackInitializer.swift new file mode 100644 index 000000000..1f655e6be --- /dev/null +++ b/Source/Core/TrackIO/TrackInitializer.swift @@ -0,0 +1,22 @@ +// +// TrackInitializer.swift +// Aural +// +// Copyright © 2025 Kartik Venugopal. All rights reserved. +// +// This software is licensed under the MIT software license. +// See the file "LICENSE" in the project root directory for license terms. +// + +import Foundation + +class TrackInitializer { + + +} + +enum TrackInitPriority: Int { + + case trackList = 0 + case menu = 1 +} diff --git a/Source/Core/TrackIO/TrackReader+Lyrics.swift b/Source/Core/TrackIO/TrackReader+Lyrics.swift index e9c7245ee..e3d43c87f 100644 --- a/Source/Core/TrackIO/TrackReader+Lyrics.swift +++ b/Source/Core/TrackIO/TrackReader+Lyrics.swift @@ -19,7 +19,7 @@ extension TrackReader { func loadExternalLyrics(for track: Track, immediate: Bool) { - guard !track.hasLyrics else {return} + guard !track.hasExternalLyrics else {return} // Load lyrics from previously assigned external file if let externalLyricsFile = track.metadata.externalLyricsFile, externalLyricsFile.exists, @@ -43,7 +43,7 @@ extension TrackReader { } // Online search - guard onlineSearchEnabled, track.title != nil && track.artist != nil else {return} + guard !track.hasLyrics, onlineSearchEnabled, track.title != nil && track.artist != nil else {return} Task.detached(priority: immediate ? .userInitiated : .utility) { @@ -55,7 +55,9 @@ extension TrackReader { Messenger.publish(TrackInfoUpdatedNotification(updatedTrack: track, updatedFields: .lyrics)) if let cachedLyricsFile = bestLyrics.persistToFile(track.defaultDisplayName) { + track.metadata.externalLyricsFile = cachedLyricsFile + track.metadata.lyricsDownloaded = true } } } @@ -132,7 +134,26 @@ extension TrackReader { } if let cachedLyricsFile = bestLyrics.persistToFile(track.defaultDisplayName) { + track.metadata.externalLyricsFile = cachedLyricsFile + track.metadata.lyricsDownloaded = true + } + } + } + + func removeDownloadedLyrics(for track: Track) { + + if let extLyricsFile = track.metadata.externalLyricsFile, + extLyricsFile.exists, extLyricsFile.parentDir == FilesAndPaths.lyricsDir { + + extLyricsFile.moveToTrash() + + track.metadata.externalLyricsFile = nil + track.metadata.externalTimedLyrics = nil + track.metadata.lyricsDownloaded = false + + if appModeManager.isShowingLyrics || appModeManager.isShowingTrackInfo { + Messenger.publish(TrackInfoUpdatedNotification(updatedTrack: track, updatedFields: .lyrics, destructiveUpdate: true)) } } } diff --git a/Source/Core/Utils/Extensions/URLExtensions.swift b/Source/Core/Utils/Extensions/URLExtensions.swift index ff46549fc..bd47dead2 100644 --- a/Source/Core/Utils/Extensions/URLExtensions.swift +++ b/Source/Core/Utils/Extensions/URLExtensions.swift @@ -210,6 +210,18 @@ extension URL { } } + func moveToTrash() { + + guard exists else {return} + + do { + try fileManager.trashItem(at: self, resultingItemURL: nil) + + } catch let error as NSError { + NSLog("Error moving file '%@' to trash: %@", self.path, error.description) + } + } + // Renames this file func rename(to target: URL) { diff --git a/Source/UI/Lyrics/Lyrics.xib b/Source/UI/Lyrics/Lyrics.xib index fa55d60a2..30643f1b4 100644 --- a/Source/UI/Lyrics/Lyrics.xib +++ b/Source/UI/Lyrics/Lyrics.xib @@ -104,7 +104,7 @@ - + diff --git a/Source/UI/Lyrics/LyricsViewController+Timed.swift b/Source/UI/Lyrics/LyricsViewController+Timed.swift index 620692fea..5402f5457 100644 --- a/Source/UI/Lyrics/LyricsViewController+Timed.swift +++ b/Source/UI/Lyrics/LyricsViewController+Timed.swift @@ -66,6 +66,10 @@ extension LyricsViewController { } @IBAction func searchForLyricsOnlineButtonAction(_ sender: NSButton) { + searchForLyricsOnline() + } + + func searchForLyricsOnline() { guard let track else {return} diff --git a/Source/UI/Lyrics/LyricsViewController.swift b/Source/UI/Lyrics/LyricsViewController.swift index 800058d98..aae6300e9 100644 --- a/Source/UI/Lyrics/LyricsViewController.swift +++ b/Source/UI/Lyrics/LyricsViewController.swift @@ -54,6 +54,7 @@ class LyricsViewController: NSViewController { super.viewDidLoad() view.wantsLayer = true + changeCornerRadius(to: playerUIState.cornerRadius) fontSchemesManager.registerObserver(self) @@ -68,6 +69,9 @@ class LyricsViewController: NSViewController { }) messenger.subscribe(to: .Lyrics.loadFromFile, handler: loadLyrics(fromFile:)) + messenger.subscribe(to: .Lyrics.searchForLyricsOnline, handler: searchForLyricsOnline, filter: { + appModeManager.isShowingLyrics + }) messenger.subscribe(to: .View.changeWindowCornerRadius, handler: changeCornerRadius(to:)) } diff --git a/Source/UI/Menus/Base.lproj/MainMenu.xib b/Source/UI/Menus/Base.lproj/MainMenu.xib index 96a747ede..065e9e5e6 100644 --- a/Source/UI/Menus/Base.lproj/MainMenu.xib +++ b/Source/UI/Menus/Base.lproj/MainMenu.xib @@ -104,6 +104,7 @@ + @@ -807,6 +808,12 @@ CA + + + + + + diff --git a/Source/UI/Menus/MetadataMenuController.swift b/Source/UI/Menus/MetadataMenuController.swift index c67519393..febab1111 100644 --- a/Source/UI/Menus/MetadataMenuController.swift +++ b/Source/UI/Menus/MetadataMenuController.swift @@ -15,11 +15,25 @@ class MetadataMenuController: NSObject, NSMenuDelegate { @IBOutlet weak var detailedInfoMenuItem: NSMenuItem! @IBOutlet weak var addLyricsFileMenuItem: NSMenuItem! @IBOutlet weak var searchForLyricsOnlineMenuItem: NSMenuItem! + @IBOutlet weak var removeOnlineLyricsMenuItem: NSMenuItem! func menuNeedsUpdate(_ menu: NSMenu) { let isPlayingOrPaused = playbackInfoDelegate.state.isPlayingOrPaused [detailedInfoMenuItem, addLyricsFileMenuItem, searchForLyricsOnlineMenuItem].forEach {$0?.enableIf(isPlayingOrPaused)} + + var enableRemoveLyricsMenuItem = false + + if let playingTrack = playbackInfoDelegate.playingTrack, playingTrack.metadata.lyricsDownloaded { + + if let extLyricsFile = playingTrack.metadata.externalLyricsFile, + extLyricsFile.exists, extLyricsFile.parentDir == FilesAndPaths.lyricsDir { + + enableRemoveLyricsMenuItem = true + } + } + + removeOnlineLyricsMenuItem.showIf(enableRemoveLyricsMenuItem) } @IBAction func moreInfoAction(_ sender: AnyObject) { @@ -33,4 +47,8 @@ class MetadataMenuController: NSObject, NSMenuDelegate { @IBAction func searchForLyricsOnlineAction(_ sender: AnyObject) { Messenger.publish(.Lyrics.searchForLyricsOnline) } + + @IBAction func removeOnlineLyricsAction(_ sender: AnyObject) { + Messenger.publish(.Lyrics.removeDownloadedLyrics) + } } diff --git a/Source/UI/ModularPlayer/ModularPlayerWindowController.swift b/Source/UI/ModularPlayer/ModularPlayerWindowController.swift index e46e808c4..1ed537169 100644 --- a/Source/UI/ModularPlayer/ModularPlayerWindowController.swift +++ b/Source/UI/ModularPlayer/ModularPlayerWindowController.swift @@ -87,7 +87,7 @@ class ModularPlayerWindowController: NSWindowController { messenger.subscribeAsync(to: .Player.trackTransitioned, handler: trackTransitioned(notif:)) messenger.subscribeAsync(to: .Player.trackInfoUpdated, handler: lyricsLoaded(notif:), filter: {notif in - notif.updatedFields.contains(.lyrics) + notif.updatedFields.contains(.lyrics) && !notif.destructiveUpdate }) colorSchemesManager.registerSchemeObserver(self) @@ -168,6 +168,7 @@ class ModularPlayerWindowController: NSWindowController { if preferences.metadataPreferences.lyrics.showWindowWhenPresent.value, playbackInfoDelegate.playingTrack == notif.updatedTrack, + notif.updatedTrack.hasLyrics, !appModeManager.isShowingLyrics { windowLayoutsManager.showWindow(withId: .lyrics) diff --git a/Source/UI/Player/PlayerViewController.swift b/Source/UI/Player/PlayerViewController.swift index 655811680..0eda0b97e 100644 --- a/Source/UI/Player/PlayerViewController.swift +++ b/Source/UI/Player/PlayerViewController.swift @@ -446,6 +446,7 @@ class PlayerViewController: NSViewController { messenger.subscribe(to: .Lyrics.addLyricsFile, handler: addLyricsFile) messenger.subscribe(to: .Lyrics.searchForLyricsOnline, handler: searchForLyricsOnline) + messenger.subscribe(to: .Lyrics.removeDownloadedLyrics, handler: removeDownloadedLyrics) } func previousTrack() { @@ -579,26 +580,17 @@ class PlayerViewController: NSViewController { func searchForLyricsOnline() { - guard let track = playbackInfoDelegate.playingTrack else {return} - - guard track.title != nil || track.artist != nil else { + if !appModeManager.isShowingLyrics { - NSAlert.showError(withTitle: "Online lyrics search not possible", andText: "The playing track does not have artist/title metadata.") - return - } - - let uiUpdateBlock = {(lyrics: TimedLyrics?) in - - if lyrics != nil { - Messenger.publish(TrackInfoUpdatedNotification(updatedTrack: track, updatedFields: .lyrics)) - - } else { - NSAlert.showError(withTitle: "Lyrics not loaded", andText: "No lyrics found online for the playing track.") - } + Messenger.publish(.View.toggleLyrics) + Messenger.publish(.Lyrics.searchForLyricsOnline) } + } + + func removeDownloadedLyrics() { - Task.detached(priority: .userInitiated) { - await trackReader.searchForLyricsOnline(for: track, uiUpdateBlock: uiUpdateBlock) + if let playingTrack = playbackInfoDelegate.playingTrack { + trackReader.removeDownloadedLyrics(for: playingTrack) } } diff --git a/Source/UI/UnifiedPlayer/UnifiedPlayerWindowController.swift b/Source/UI/UnifiedPlayer/UnifiedPlayerWindowController.swift index e9c763ec2..df0b5a9b8 100644 --- a/Source/UI/UnifiedPlayer/UnifiedPlayerWindowController.swift +++ b/Source/UI/UnifiedPlayer/UnifiedPlayerWindowController.swift @@ -99,7 +99,7 @@ class UnifiedPlayerWindowController: NSWindowController { messenger.subscribe(to: .Player.trackTransitioned, handler: trackTransitioned(notif:)) messenger.subscribeAsync(to: .Player.trackInfoUpdated, handler: lyricsLoaded(notif:), filter: {notif in - notif.updatedFields.contains(.lyrics) + notif.updatedFields.contains(.lyrics) && !notif.destructiveUpdate }) messenger.subscribe(to: .Application.willExit, handler: preApplicationExit) @@ -329,6 +329,7 @@ class UnifiedPlayerWindowController: NSWindowController { if preferences.metadataPreferences.lyrics.showWindowWhenPresent.value, playbackInfoDelegate.playingTrack == notif.updatedTrack, + notif.updatedTrack.hasLyrics, !appModeManager.isShowingLyrics { showLyrics()