Skip to content

Commit

Permalink
External metadata load func
Browse files Browse the repository at this point in the history
  • Loading branch information
kartik-venugopal committed Jan 12, 2025
1 parent a40cd23 commit 42942af
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 137 deletions.
4 changes: 4 additions & 0 deletions Aural.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@
3E25B65627F7875700D10A5F /* PlayQueueSimpleView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E25B65527F7875700D10A5F /* PlayQueueSimpleView.xib */; };
3E25B65927F78C0B00D10A5F /* TrackListTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E25B65827F78C0B00D10A5F /* TrackListTableViewController.swift */; };
3E25B65B27F78EE100D10A5F /* PlayQueueSimpleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E25B65A27F78EE100D10A5F /* PlayQueueSimpleViewController.swift */; };
3E28A5792D33DCCB007331AF /* TrackReader+Lyrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E28A5782D33DCCB007331AF /* TrackReader+Lyrics.swift */; };
3E2976132629473500459272 /* WindowCornerRadiusMenuItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E2976122629473500459272 /* WindowCornerRadiusMenuItemView.swift */; };
3E2C4BD02B38D46400FC65DD /* Search.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E2C4BCF2B38D46400FC65DD /* Search.xib */; };
3E2C4BD42B38DD0400FC65DD /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E2C4BD32B38DD0400FC65DD /* SearchViewController.swift */; };
Expand Down Expand Up @@ -1577,6 +1578,7 @@
3E25EA952B979D5100D76006 /* FavoritePlaylistFilesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritePlaylistFilesViewController.swift; sourceTree = "<group>"; };
3E25EA972B97A12000D76006 /* FavoriteFolders.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FavoriteFolders.xib; sourceTree = "<group>"; };
3E25EA992B97A12700D76006 /* FavoriteFoldersViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteFoldersViewController.swift; sourceTree = "<group>"; };
3E28A5782D33DCCB007331AF /* TrackReader+Lyrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrackReader+Lyrics.swift"; sourceTree = "<group>"; };
3E2976122629473500459272 /* WindowCornerRadiusMenuItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCornerRadiusMenuItemView.swift; sourceTree = "<group>"; };
3E2A2A052809B68A000F0539 /* PlaylistNamesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistNamesTableViewController.swift; sourceTree = "<group>"; };
3E2A2A0A280A032F000F0539 /* PlaylistTracksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistTracksViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3078,6 +3080,7 @@
3E0217772C23490E00865AC2 /* FileReader.swift */,
3E0217782C23490E00865AC2 /* FileReaderProtocol.swift */,
3E0217792C23490E00865AC2 /* TrackReader.swift */,
3E28A5782D33DCCB007331AF /* TrackReader+Lyrics.swift */,
);
path = TrackIO;
sourceTree = "<group>";
Expand Down Expand Up @@ -6216,6 +6219,7 @@
3E0218382C23490E00865AC2 /* FFmpegString.swift in Sources */,
3EB805DD2B793FE7005E464A /* MenuBarPlayQueueContainer.swift in Sources */,
3E47D2352815A5BD00938F20 /* ControlStatesColorSchemeViewController.swift in Sources */,
3E28A5792D33DCCB007331AF /* TrackReader+Lyrics.swift in Sources */,
3E25B65B27F78EE100D10A5F /* PlayQueueSimpleViewController.swift in Sources */,
3E6C12B325CEBE5800BF0D07 /* FavoritesMenuController.swift in Sources */,
3E5F942E25E57190002DEF80 /* FFT.swift in Sources */,
Expand Down
4 changes: 2 additions & 2 deletions Source/Core/Playback/Delegates/PlaybackDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ class PlaybackDelegate: PlaybackDelegateProtocol {
messenger.publish(TrackTransitionNotification(beginTrack: currentTrack, beginState: .stopped,
endTrack: firstTrack, endState: player.state))

trackReader.loadArtAsync(for: firstTrack, immediate: true)
trackReader.loadExternalMetadataAsync(for: firstTrack, immediate: true)
}

private func changeGaplessTrack(mustStopIfNoTrack: Bool, trackProducer: TrackProducer) {
Expand Down Expand Up @@ -531,7 +531,7 @@ class PlaybackDelegate: PlaybackDelegateProtocol {
// print("\(Date.nowTimestampString) - subsequentTrack: \(subsequentTrack)")

session.track = subsequentTrack
trackReader.loadArtAsync(for: subsequentTrack, immediate: true)
trackReader.loadExternalMetadataAsync(for: subsequentTrack, immediate: true)
}

messenger.publish(TrackTransitionNotification(beginTrack: beginTrack, beginState: beginState,
Expand Down
1 change: 1 addition & 0 deletions Source/Core/TrackIO/Model/Track.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Track: Hashable, PlayableItem {

let fileSystemInfo: FileSystemInfo
var metadata: FileMetadata
var externalMetadataLoaded: AtomicBool = AtomicBool(value: false)
var playbackContext: PlaybackContextProtocol?

init(_ file: URL, primaryMetadata: PrimaryMetadata? = nil, cueSheetMetadata: CueSheetMetadata? = nil) {
Expand Down
109 changes: 109 additions & 0 deletions Source/Core/TrackIO/TrackReader+Lyrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// TrackReader+Lyrics.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
import LyricsCore
import LyricsService

extension TrackReader {

func loadExternalLyrics(for track: Track) {

// Load lyrics from previously assigned external file
if let externalLyricsFile = track.metadata.externalLyricsFile, externalLyricsFile.exists,
let lyrics = loadLyricsFromFile(at: externalLyricsFile) {

track.metadata.externalTimedLyrics = TimedLyrics(from: lyrics, trackDuration: track.duration)
return
}

// Look for lyrics in candidate directories
let lyricsFolder = preferences.metadataPreferences.lyrics.lyricsFilesDirectory.value

for dir in [lyricsFolder, track.file.parentDir, FilesAndPaths.lyricsDir].compactMap({$0}) {

if loadLyricsFromDirectory(dir, for: track) {
return
}
}
}

/// Loads lyrics from a specified directory by searching for .lrc or .lrcx files
///
/// - Parameter directory: The directory to search for lyrics files
/// - Returns: A Lyrics object if found and successfully loaded, nil otherwise
///
private func loadLyricsFromDirectory(_ directory: URL, for track: Track) -> Bool {

let possibleFiles = SupportedTypes.lyricsFileExtensions.map {
directory.appendingPathComponent(track.defaultDisplayName).appendingPathExtension($0)
}

if let lyricsFile = possibleFiles.first(where: {$0.exists}) {
return loadTimedLyricsFromFile(at: lyricsFile, for: track)
}

return false
}

func loadTimedLyricsFromFile(at url: URL, for track: Track) -> Bool {

guard let lyrics = loadLyricsFromFile(at: url) else {return false}

track.metadata.externalTimedLyrics = TimedLyrics(from: lyrics, trackDuration: track.duration)
track.metadata.externalLyricsFile = url
return true
}

/// Loads lyrics content from a file at the specified URL
///
/// - Parameter url: The URL of the lyrics file
/// - Returns: A Lyrics object if successfully loaded, nil otherwise
///
private func loadLyricsFromFile(at url: URL) -> LyricsCore.Lyrics? {

do {

let lyricsText = try String(contentsOf: url, encoding: .utf8)
return LyricsCore.Lyrics(lyricsText)

} catch {

print("Failed to read lyrics file at \(url.path): \(error.localizedDescription)")
return nil
}
}

private var onlineSearchEnabled: Bool {
preferences.metadataPreferences.lyrics.enableOnlineSearch.value
}

func searchForLyricsOnline(for track: Track, using searchService: LyricsSearchService, uiUpdateBlock: @escaping (TimedLyrics) -> Void) async {

guard onlineSearchEnabled else {return}

Task.detached(priority: .userInitiated) {

guard let bestLyrics = await searchService.searchLyrics(for: track) else {return}

let timedLyrics = TimedLyrics(from: bestLyrics, trackDuration: track.duration)
track.metadata.externalTimedLyrics = timedLyrics

// Update the UI
await MainActor.run {
uiUpdateBlock(timedLyrics)
}

if let cachedLyricsFile = bestLyrics.persistToFile(track.defaultDisplayName) {
track.metadata.externalLyricsFile = cachedLyricsFile
}
}
}
}
162 changes: 41 additions & 121 deletions Source/Core/TrackIO/TrackReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
//

import Foundation
import LyricsCore
import LyricsService

typealias TrackIOCompletionHandler = () -> Void

Expand Down Expand Up @@ -42,8 +40,6 @@ class TrackReader {
track.metadata = cachedMetadata

doLoadMetadata(for: track, onQueue: opQueue)
loadExternalLyrics(for: track, onQueue: opQueue)

return
}

Expand All @@ -59,7 +55,6 @@ class TrackReader {
}

self.doLoadMetadata(for: track, onQueue: opQueue)
self.loadExternalLyrics(for: track, onQueue: opQueue)

} catch {

Expand All @@ -86,105 +81,6 @@ class TrackReader {
}
}

private func loadExternalLyrics(for track: Track, onQueue opQueue: OperationQueue) {

opQueue.addOperation {

// Load lyrics from previously assigned external file
if let externalLyricsFile = track.metadata.externalLyricsFile, externalLyricsFile.exists,
let lyrics = self.loadLyricsFromFile(at: externalLyricsFile) {

track.metadata.externalTimedLyrics = TimedLyrics(from: lyrics, trackDuration: track.duration)
return
}

// Look for lyrics in candidate directories
let lyricsFolder = preferences.metadataPreferences.lyrics.lyricsFilesDirectory.value

for dir in [lyricsFolder, track.file.parentDir, FilesAndPaths.lyricsDir].compactMap({$0}) {

if self.loadLyricsFromDirectory(dir, for: track) {
return
}
}
}
}

/// Loads lyrics from a specified directory by searching for .lrc or .lrcx files
///
/// - Parameter directory: The directory to search for lyrics files
/// - Returns: A Lyrics object if found and successfully loaded, nil otherwise
///
private func loadLyricsFromDirectory(_ directory: URL, for track: Track) -> Bool {

let possibleFiles = SupportedTypes.lyricsFileExtensions.map {
directory.appendingPathComponent(track.defaultDisplayName).appendingPathExtension($0)
}

if let lyricsFile = possibleFiles.first(where: {$0.exists}) {
return loadTimedLyricsFromFile(at: lyricsFile, for: track)
}

return false
}

func loadTimedLyricsFromFile(at url: URL, for track: Track) -> Bool {

if let lyrics = loadLyricsFromFile(at: url) {

track.metadata.externalTimedLyrics = TimedLyrics(from: lyrics, trackDuration: track.duration)
track.metadata.externalLyricsFile = url
return true
}

return false
}

/// Loads lyrics content from a file at the specified URL
///
/// - Parameter url: The URL of the lyrics file
/// - Returns: A Lyrics object if successfully loaded, nil otherwise
///
private func loadLyricsFromFile(at url: URL) -> LyricsCore.Lyrics? {

do {

let lyricsText = try String(contentsOf: url, encoding: .utf8)
return LyricsCore.Lyrics(lyricsText)

} catch {

print("Failed to read lyrics file at \(url.path): \(error.localizedDescription)")
return nil
}
}

private var onlineSearchEnabled: Bool {
preferences.metadataPreferences.lyrics.enableOnlineSearch.value
}

func searchForLyricsOnline(for track: Track, using searchService: LyricsSearchService, uiUpdateBlock: @escaping (TimedLyrics) -> Void) async {

guard onlineSearchEnabled else {return}

Task.detached(priority: .userInitiated) {

guard let bestLyrics = await searchService.searchLyrics(for: track) else {return}

let timedLyrics = TimedLyrics(from: bestLyrics, trackDuration: track.duration)
track.metadata.externalTimedLyrics = timedLyrics

// Update the UI
await MainActor.run {
uiUpdateBlock(timedLyrics)
}

if let cachedLyricsFile = bestLyrics.persistToFile(track.defaultDisplayName) {
track.metadata.externalLyricsFile = cachedLyricsFile
}
}
}

func computeAccurateDuration(forTrack track: Track, onQueue opQueue: OperationQueue) {

opQueue.addOperation {
Expand Down Expand Up @@ -253,8 +149,8 @@ class TrackReader {
try track.playbackContext?.open()
}

// Load cover art for display in the player.
loadArtAsync(for: track, immediate: immediate)
// Load cover art / lyrics for display in the player.
loadExternalMetadataAsync(for: track, immediate: immediate)

} catch {

Expand Down Expand Up @@ -306,26 +202,51 @@ class TrackReader {
}
}

func loadExternalMetadataAsync(for track: Track, immediate: Bool = true) {

guard track.externalMetadataLoaded.isFalse else {return}

track.externalMetadataLoaded.setTrue()

DispatchQueue.global(qos: immediate ? .userInteractive : .utility).async {

self.loadArt(for: track)
self.loadExternalLyrics(for: track)
}
}

///
/// Loads cover art for a track, asynchronously. This is useful when
/// cover art is not required immediately, and a short delay is acceptable.
/// (eg. when preparing for playback)
///
func loadArtAsync(for track: Track, immediate: Bool = true) {
func loadArt(for track: Track) {

if track.art?.originalImage == nil {
doLoadArt(for: track)
}
}

private func doLoadArt(for track: Track) {

if track.art?.originalImage != nil {return}
guard let art = coverArtReader.getCoverArt(forTrack: track) else {return}

DispatchQueue.global(qos: immediate ? .userInteractive : .utility).async {

guard let art = coverArtReader.getCoverArt(forTrack: track) else {return}
if let existingArt = track.art {
existingArt.merge(withOther: art)
} else {
track.metadata.art = art
}

Messenger.publish(TrackInfoUpdatedNotification(updatedTrack: track, updatedFields: .art))
}

func loadArtAsync(for track: Track, immediate: Bool = true) {

if track.art?.originalImage == nil {

if let existingArt = track.art {
existingArt.merge(withOther: art)
} else {
track.metadata.art = art
DispatchQueue.global(qos: immediate ? .userInteractive : .utility).async {
self.doLoadArt(for: track)
}

Messenger.publish(TrackInfoUpdatedNotification(updatedTrack: track, updatedFields: .art))
}
}

Expand All @@ -334,10 +255,9 @@ class TrackReader {
///
func loadAuxiliaryMetadata(for track: Track) {


track.audioInfo.replayGainFromMetadata = track.replayGain
loadArtAsync(for: track)

track.audioInfo.replayGainFromMetadata = track.replayGain
track.audioInfo.replayGainFromAnalysis = replayGainScanner.cachedReplayGainData(forTrack: track)

loadExternalMetadataAsync(for: track)
}
}
Loading

0 comments on commit 42942af

Please sign in to comment.