From c53df649c817f770ae1750bb6d11113e0bcd2b18 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 10 Apr 2023 13:02:51 +0200 Subject: Update code, add source management config --- Sources/lyricli/configuration.swift | 2 +- .../errors/configuration_could_not_be_read.swift | 3 + .../errors/source_could_not_be_disabled.swift | 6 ++ .../errors/source_could_not_be_enabled.swift | 6 ++ .../lyricli/errors/source_could_not_be_reset.swift | 6 ++ Sources/lyricli/errors/source_not_available.swift | 3 + Sources/lyricli/lyricli.swift | 60 +++++++++-- Sources/lyricli/lyricli_command.swift | 117 +++++++++++++++------ Sources/lyricli/lyrics_engine.swift | 11 +- Sources/lyricli/main.swift | 107 ------------------- Sources/lyricli/source_manager.swift | 4 +- Sources/lyricli/sources/apple_music_source.swift | 80 ++++++++++++++ Sources/lyricli/sources/itunes_source.swift | 77 -------------- Sources/lyricli/sources/source_protocol.swift | 4 + Sources/lyricli/sources/spotify_source.swift | 3 + 15 files changed, 256 insertions(+), 233 deletions(-) create mode 100644 Sources/lyricli/errors/configuration_could_not_be_read.swift create mode 100644 Sources/lyricli/errors/source_could_not_be_disabled.swift create mode 100644 Sources/lyricli/errors/source_could_not_be_enabled.swift create mode 100644 Sources/lyricli/errors/source_could_not_be_reset.swift create mode 100644 Sources/lyricli/errors/source_not_available.swift delete mode 100644 Sources/lyricli/main.swift create mode 100644 Sources/lyricli/sources/apple_music_source.swift delete mode 100644 Sources/lyricli/sources/itunes_source.swift (limited to 'Sources') diff --git a/Sources/lyricli/configuration.swift b/Sources/lyricli/configuration.swift index 1b01034..b2defc1 100644 --- a/Sources/lyricli/configuration.swift +++ b/Sources/lyricli/configuration.swift @@ -8,7 +8,7 @@ class Configuration { // Default options, will be automatically written to the global config if // not found. private var configuration: [String: Any] = [ - "enabled_sources": ["itunes", "spotify"] + "enabled_sources": ["apple_music", "spotify"] ] // The shared instance of the object diff --git a/Sources/lyricli/errors/configuration_could_not_be_read.swift b/Sources/lyricli/errors/configuration_could_not_be_read.swift new file mode 100644 index 0000000..fb2b61a --- /dev/null +++ b/Sources/lyricli/errors/configuration_could_not_be_read.swift @@ -0,0 +1,3 @@ +struct ConfigurationCouldNotBeRead: Error { + var localizedDescription = "The configuration could not be read, check ~/.lyricli.conf" +} diff --git a/Sources/lyricli/errors/source_could_not_be_disabled.swift b/Sources/lyricli/errors/source_could_not_be_disabled.swift new file mode 100644 index 0000000..b19f3c5 --- /dev/null +++ b/Sources/lyricli/errors/source_could_not_be_disabled.swift @@ -0,0 +1,6 @@ +// Future improvement: At the moment the sources don't need any special +// configuration. Once we do have more operations it would make sense +// to throw more descriptive errors. +struct SourceCouldNotBeDisabled: Error { + var localizedDescription = "The selected source failed while disabling" +} diff --git a/Sources/lyricli/errors/source_could_not_be_enabled.swift b/Sources/lyricli/errors/source_could_not_be_enabled.swift new file mode 100644 index 0000000..7797478 --- /dev/null +++ b/Sources/lyricli/errors/source_could_not_be_enabled.swift @@ -0,0 +1,6 @@ +// Future improvement: At the moment the sources don't need any special +// configuration. Once we do have more operations it would make sense +// to throw more descriptive errors. +struct SourceCouldNotBeEnabled: Error { + var localizedDescription = "The selected source failed while enabling" +} diff --git a/Sources/lyricli/errors/source_could_not_be_reset.swift b/Sources/lyricli/errors/source_could_not_be_reset.swift new file mode 100644 index 0000000..beecf54 --- /dev/null +++ b/Sources/lyricli/errors/source_could_not_be_reset.swift @@ -0,0 +1,6 @@ +// Future improvement: At the moment the sources don't need any special +// configuration. Once we do have more operations it would make sense +// to throw more descriptive errors. +struct SourceCouldNotBeReset: Error { + var localizedDescription = "The selected source failed while resetting" +} diff --git a/Sources/lyricli/errors/source_not_available.swift b/Sources/lyricli/errors/source_not_available.swift new file mode 100644 index 0000000..e3d598d --- /dev/null +++ b/Sources/lyricli/errors/source_not_available.swift @@ -0,0 +1,3 @@ +struct SourceNotAvailable: Error { + var localizedDescription = "The selected source wasn't available" +} diff --git a/Sources/lyricli/lyricli.swift b/Sources/lyricli/lyricli.swift index 043c004..ad4bb86 100644 --- a/Sources/lyricli/lyricli.swift +++ b/Sources/lyricli/lyricli.swift @@ -25,11 +25,10 @@ class Lyricli { static func printLyrics(_ currentTrack: Track) { let engine = LyricsEngine(withTrack: currentTrack) + if showTitle { + printTitle(currentTrack) + } if let lyrics = engine.lyrics { - if showTitle { - printTitle(currentTrack) - } - print(lyrics) } else { print("Lyrics not found :(") @@ -38,23 +37,62 @@ class Lyricli { // Print the currently available sources static func printSources() { - print("Listing Sources: Not yet implemented") + let sourceManager = SourceManager() + for (sourceName, _) in sourceManager.availableSources { + if (Configuration.shared["enabled_sources"] as? [String] ?? []).contains(sourceName) { + print("\(sourceName) (enabled)") + } else { + print(sourceName) + } + } } // Runs the enable method of a source and writes the configuration to set it // as enabled - static func enableSource(_ sourceName: String) { - print("Enable source \(sourceName): Not yet implemented") + static func enableSource(_ sourceName: String) throws { + let sourceManager = SourceManager() + if let source = sourceManager.availableSources[sourceName] { + if let enabledSources = Configuration.shared["enabled_sources"] as? [String] { + if source.enable() == false { + throw SourceCouldNotBeEnabled() + } + if !enabledSources.contains(sourceName) { + Configuration.shared["enabled_sources"] = enabledSources + [sourceName] + } + return + } + throw ConfigurationCouldNotBeRead() + } + throw SourceNotAvailable() } // Remove a source from the enabled sources configuration - static func disableSource(_ sourceName: String) { - print("Disable source \(sourceName): Not yet implemented") + static func disableSource(_ sourceName: String) throws { + let sourceManager = SourceManager() + if let source = sourceManager.availableSources[sourceName] { + if let enabledSources = Configuration.shared["enabled_sources"] as? [String] { + if source.disable() == false { + throw SourceCouldNotBeDisabled() + } + Configuration.shared["enabled_sources"] = enabledSources.filter { $0 != sourceName } + return + } + throw ConfigurationCouldNotBeRead() + } + throw SourceNotAvailable() } // Removes any configuration for a source, and disables it - static func resetSource(_ sourceName: String) { - print("Reset source \(sourceName): Not yet implemented") + static func resetSource(_ sourceName: String) throws { + let sourceManager = SourceManager() + if let source = sourceManager.availableSources[sourceName] { + if source.reset() == false { + throw SourceCouldNotBeReset() + } + try disableSource(sourceName) + return + } + throw SourceNotAvailable() } // Prints the track artist and name diff --git a/Sources/lyricli/lyricli_command.swift b/Sources/lyricli/lyricli_command.swift index 88f9c7b..872ab6e 100644 --- a/Sources/lyricli/lyricli_command.swift +++ b/Sources/lyricli/lyricli_command.swift @@ -1,38 +1,95 @@ -import Bariloche +import Darwin +import ArgumentParser -class LyricliCommand: Command { - let usage: String? = "Fetch the lyrics for current playing track or the one specified via arguments" +@main +struct LyricliCommand: ParsableCommand { + + // Positional Arguments + @Argument var artist: String? + @Argument var trackName: String? // Flags - let version = Flag(short: "v", long: "version", help: "Prints the version.") - let showTitle = Flag(short: "t", long: "title", help: "Shows title of song if true") - let listSources = Flag(short: "l", long: "list", help: "Lists all sources") + @Flag(name: .shortAndLong, help: "Prints the version.") + var version = false + + @Flag(name: [.long, .customShort("t")], help: "Shows title of track if true") + var showTitle = false + + @Flag(name: .shortAndLong, help: "Lists all sources") + var listSources = false // Named Arguments - let enableSource = Argument(name: "source", - kind: .named(short: "e", long: "enable"), - optional: true, - help: "Enables a source") - let disableSource = Argument(name: "source", - kind: .named(short: "d", long: "disable"), - optional: true, - help: "Disables a source") - let resetSource = Argument(name: "source", - kind: .named(short: "r", long: "reset"), - optional: true, - help: "Resets a source") + @Option(name: .shortAndLong, help: ArgumentHelp("Enables a source", valueName: "source")) + var enableSource: String? - // Positional Arguments - let artist = Argument(name: "artist", - kind: .positional, - optional: true, - help: "The name of the artist") - let trackName = Argument(name: "trackName", - kind: .positional, - optional: true, - help: "The name of the track") - - func run() -> Bool { - return true + @Option(name: .shortAndLong, help: ArgumentHelp("Disables a source", valueName: "source")) + var disableSource: String? + + @Option(name: .shortAndLong, help: ArgumentHelp("Resets a source", valueName: "source")) + var resetSource: String? + + mutating func run() throws { + + // Handle the version flag + if version { + print(Lyricli.version) + Darwin.exit(0) + } + + // Handle the list sources flag + if listSources { + Lyricli.printSources() + Darwin.exit(0) + } + + // Handle the enable source option + if let source = enableSource { + do { + try Lyricli.enableSource(source) + } catch let error { + handleErrorAndQuit(error) + } + Darwin.exit(0) + } + + // Handle the disable source option + if let source = disableSource { + do { + try Lyricli.disableSource(source) + } catch let error { + handleErrorAndQuit(error) + } + Darwin.exit(0) + } + + // Handle the reset source flag + if let source = resetSource { + do { + try Lyricli.resetSource(source) + } catch let error { + handleErrorAndQuit(error) + } + Darwin.exit(0) + } + + Lyricli.showTitle = showTitle + + if let artist { + let currentTrack: Track + if let trackName { + currentTrack = Track(withName: trackName, andArtist: artist) + } else { + currentTrack = Track(withName: "", andArtist: artist) + } + Lyricli.printLyrics(currentTrack) + Darwin.exit(0) + } + + Lyricli.printLyrics() + } + + private func handleErrorAndQuit(_ error: Error) { + fputs(error.localizedDescription, stderr) + Darwin.exit(1) } } diff --git a/Sources/lyricli/lyrics_engine.swift b/Sources/lyricli/lyrics_engine.swift index 1a182d0..9ca0cad 100644 --- a/Sources/lyricli/lyrics_engine.swift +++ b/Sources/lyricli/lyrics_engine.swift @@ -1,5 +1,4 @@ import Foundation -import HTMLEntities // Given a track, attempts to fetch the lyrics from lyricswiki class LyricsEngine { @@ -82,7 +81,10 @@ class LyricsEngine { if let lyricsUrl = URL(string: lyricsUrlString) { // At this point we have a valid wiki url - self.fetchLyricsFromPage(withURL: lyricsUrl, completionHandler: completionHandler) + self.fetchLyricsFromPage( + withURL: lyricsUrl, + completionHandler: completionHandler + ) return } } @@ -145,10 +147,9 @@ class LyricsEngine { completionHandler(nil) } - // Escapes the HTML entities + // Escapes the HTML entities and HTML private func decodeLyrics(_ lyrics: String) -> String { - let unescapedLyrics = lyrics.htmlUnescape() - return unescapedLyrics.replacingOccurrences(of: "
", with: "\n") + return lyrics } } diff --git a/Sources/lyricli/main.swift b/Sources/lyricli/main.swift deleted file mode 100644 index be1c933..0000000 --- a/Sources/lyricli/main.swift +++ /dev/null @@ -1,107 +0,0 @@ -import Foundation -import Bariloche - -// Entry point of the application. This is the main executable -private func main() { - - // Bariloche assumes at least one argument, so bypass - // if that's the case. - if CommandLine.arguments.count > 1 { - let parser = Bariloche(command: LyricliCommand()) - let result = parser.parse() - - if result.count == 0 { - exit(EX_USAGE) - } - - if let lyricliCommand = result[0] as? LyricliCommand { - // Flags - checkVersionFlag(lyricliCommand) - checkListSourcesFlag(lyricliCommand) - checkTitleFlag(lyricliCommand) - - // String Options - - checkEnableSourceFlag(lyricliCommand) - checkDisableSourceFlag(lyricliCommand) - checkResetSourceFlag(lyricliCommand) - - checkPositionalArguments(lyricliCommand) - - } - } - - // Run Lyricli - Lyricli.printLyrics() -} - -// Handle the version flag - -private func checkVersionFlag(_ command: LyricliCommand) { - if command.version.value { - print(Lyricli.version) - exit(0) - } -} - -// Handle the list sources flag - -private func checkListSourcesFlag(_ command: LyricliCommand) { - if command.listSources.value { - Lyricli.printSources() - exit(0) - } -} - -// Handle the title flag - -private func checkTitleFlag(_ command: LyricliCommand) { - Lyricli.showTitle = command.showTitle.value -} - -// Handle the enable source flag - -private func checkEnableSourceFlag(_ command: LyricliCommand) { - if let source = command.enableSource.value { - Lyricli.enableSource(source) - exit(0) - } -} - -// Handle the disable source flag - -private func checkDisableSourceFlag(_ command: LyricliCommand) { - if let source = command.disableSource.value { - Lyricli.disableSource(source) - exit(0) - } -} - -// Handle the reset source flag - -private func checkResetSourceFlag(_ command: LyricliCommand) { - if let source = command.resetSource.value { - Lyricli.resetSource(source) - exit(0) - } -} - -// Handle the positional arguments - -private func checkPositionalArguments(_ command: LyricliCommand) { - if let artist = command.artist.value { - - let currentTrack: Track - - if let trackName = command.trackName.value { - currentTrack = Track(withName: trackName, andArtist: artist) - } else { - currentTrack = Track(withName: "", andArtist: artist) - } - - Lyricli.printLyrics(currentTrack) - exit(0) - } -} - -main() diff --git a/Sources/lyricli/source_manager.swift b/Sources/lyricli/source_manager.swift index 2f0b8f4..481d327 100644 --- a/Sources/lyricli/source_manager.swift +++ b/Sources/lyricli/source_manager.swift @@ -2,8 +2,8 @@ class SourceManager { // List of sources enabled for the crurent platform - private var availableSources: [String: Source] = [ - "itunes": ItunesSource(), + var availableSources: [String: Source] = [ + "apple_music": AppleMusicSource(), "spotify": SpotifySource() ] diff --git a/Sources/lyricli/sources/apple_music_source.swift b/Sources/lyricli/sources/apple_music_source.swift new file mode 100644 index 0000000..5eee588 --- /dev/null +++ b/Sources/lyricli/sources/apple_music_source.swift @@ -0,0 +1,80 @@ +import ScriptingBridge +import Foundation + +// Protocol to obtain the track from +@objc protocol AppleMusicTrack { + @objc optional var name: String {get} + @objc optional var artist: String {get} +} + +// Protocol to interact with Apple Music +@objc protocol AppleMusicApplication { + @objc optional var currentTrack: AppleMusicTrack? {get} + @objc optional var currentStreamTitle: String? {get} +} + +extension SBApplication: AppleMusicApplication {} + +// Source that reads track artist and name from current itunes track +class AppleMusicSource: Source { + + // Calls the spotify API and returns the current track + var currentTrack: Track? { + + if let appleMusic: AppleMusicApplication = SBApplication(bundleIdentifier: bundleIdentifier) { + if let application = appleMusic as? SBApplication { + if !application.isRunning { + return nil + } + } + + // Attempt to fetch the title from a stream + if let currentStreamTitle = appleMusic.currentStreamTitle { + if let track = currentStreamTitle { + + let trackComponents = track.split(separator: "-").map(String.init) + + if trackComponents.count == 2 { + let artist = trackComponents[0].trimmingCharacters(in: .whitespaces) + let name = trackComponents[1].trimmingCharacters(in: .whitespaces) + + return Track(withName: name, andArtist: artist) + } + + } + } + + // Attempt to fetch the title from a song + if let currentTrack = appleMusic.currentTrack { + if let track = currentTrack { + if let name = track.name { + if let artist = track.artist { + + // track properties are empty strings if itunes is closed + if name == "" || artist == "" { + return nil + } + return Track(withName: name, andArtist: artist) + } + } + } + } + } + + return nil + } + + private var bundleIdentifier: String { + if ProcessInfo().isOperatingSystemAtLeast( + OperatingSystemVersion(majorVersion: 10, minorVersion: 15, patchVersion: 0) + ) { + return "com.apple.Music" + } + + return "com.apple.iTunes" + } + + func enable() -> Bool { return true } + func disable() -> Bool { return true } + func reset() -> Bool { return true } +} diff --git a/Sources/lyricli/sources/itunes_source.swift b/Sources/lyricli/sources/itunes_source.swift deleted file mode 100644 index 92b9969..0000000 --- a/Sources/lyricli/sources/itunes_source.swift +++ /dev/null @@ -1,77 +0,0 @@ -import ScriptingBridge -import Foundation - -// Protocol to obtain the track from iTunes -@objc protocol iTunesTrack { - @objc optional var name: String {get} - @objc optional var artist: String {get} -} - -// Protocol to interact with iTunes -@objc protocol iTunesApplication { - @objc optional var currentTrack: iTunesTrack? {get} - @objc optional var currentStreamTitle: String? {get} -} - -extension SBApplication: iTunesApplication {} - -// Source that reads track artist and name from current itunes track -class ItunesSource: Source { - - // Calls the spotify API and returns the current track - var currentTrack: Track? { - - if let iTunes: iTunesApplication = SBApplication(bundleIdentifier: bundleIdentifier) { - if let application = iTunes as? SBApplication { - if !application.isRunning { - return nil - } - } - - // Attempt to fetch the title from a stream - if let currentStreamTitle = iTunes.currentStreamTitle { - if let track = currentStreamTitle { - - let trackComponents = track.split(separator: "-").map(String.init) - - if trackComponents.count == 2 { - let artist = trackComponents[0].trimmingCharacters(in: .whitespaces) - let name = trackComponents[1].trimmingCharacters(in: .whitespaces) - - return Track(withName: name, andArtist: artist) - } - - } - } - - // Attempt to fetch the title from a song - if let currentTrack = iTunes.currentTrack { - if let track = currentTrack { - if let name = track.name { - if let artist = track.artist { - - // track properties are empty strings if itunes is closed - if name == "" || artist == "" { - return nil - } - return Track(withName: name, andArtist: artist) - } - } - } - } - } - - return nil - } - - private var bundleIdentifier: String { - if ProcessInfo().isOperatingSystemAtLeast( - OperatingSystemVersion(majorVersion: 10, minorVersion: 15, patchVersion: 0) - ) { - return "com.apple.Music" - } - - return "com.apple.iTunes" - } - -} diff --git a/Sources/lyricli/sources/source_protocol.swift b/Sources/lyricli/sources/source_protocol.swift index 0885994..8d52603 100644 --- a/Sources/lyricli/sources/source_protocol.swift +++ b/Sources/lyricli/sources/source_protocol.swift @@ -2,4 +2,8 @@ // property will return a track if the conditions are met protocol Source { var currentTrack: Track? { get } + + func disable() -> Bool + func enable() -> Bool + func reset() -> Bool } diff --git a/Sources/lyricli/sources/spotify_source.swift b/Sources/lyricli/sources/spotify_source.swift index 9c516c4..863a399 100644 --- a/Sources/lyricli/sources/spotify_source.swift +++ b/Sources/lyricli/sources/spotify_source.swift @@ -43,4 +43,7 @@ class SpotifySource: Source { return nil } + func enable() -> Bool { return true } + func disable() -> Bool { return true } + func reset() -> Bool { return true } } -- cgit