From: Ruben Beltran del Rio Date: Mon, 10 Apr 2023 11:02:51 +0000 (+0200) Subject: Update code, add source management config X-Git-Tag: 2.0.0~4 X-Git-Url: https://git.r.bdr.sh/rbdr/lyricli/commitdiff_plain/c53df649c817f770ae1750bb6d11113e0bcd2b18?ds=inline Update code, add source management config --- diff --git a/Package.resolved b/Package.resolved index a2a49b3..4d93f8f 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,34 +1,14 @@ { - "object": { - "pins": [ - { - "package": "Bariloche", - "repositoryURL": "https://github.com/Subito-it/Bariloche", - "state": { - "branch": null, - "revision": "507f4121d2a7479522908dabf83aed78a6e5e268", - "version": "1.0.4" - } - }, - { - "package": "Rainbow", - "repositoryURL": "https://github.com/onevcat/Rainbow", - "state": { - "branch": null, - "revision": "9c52c1952e9b2305d4507cf473392ac2d7c9b155", - "version": "3.1.5" - } - }, - { - "package": "HTMLEntities", - "repositoryURL": "https://github.com/IBM-Swift/swift-html-entities.git", - "state": { - "branch": null, - "revision": "dc15f4d8eba5be23280a561c698fc36ab4fb6c76", - "version": "3.0.12" - } + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a", + "version" : "1.2.2" } - ] - }, - "version": 1 + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 9adbd39..2f49d10 100644 --- a/Package.swift +++ b/Package.swift @@ -1,19 +1,16 @@ -// swift-tools-version:5.0 +// swift-tools-version:5.8 import PackageDescription let package = Package( name: "lyricli", dependencies: [ - /// 🔡 Tools for working with HTML entities - .package(url: "https://github.com/IBM-Swift/swift-html-entities.git", from: "3.0.11"), - /// 🚩 Command Line Arguments - .package(url: "https://github.com/Subito-it/Bariloche", from: "1.0.4") + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2") ], targets: [ - .target( + .executableTarget( name: "lyricli", - dependencies: ["HTMLEntities", "Bariloche"]) + dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]) ] ) diff --git a/README.md b/README.md index 57dff9a..6d6046a 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,7 @@ A command line tool to show the lyrics of your current song. ## Usage -Lyricli can be invoked with the command `lrc`. It can be invoked without -arguments, with an artist and song or with a special command: +Lyricli can be invoked with the command `lrc`. ``` $ lrc [-t] @@ -26,20 +25,24 @@ song and artist names before the lyrics. ### Commands -In order to configure +In order to configure sources, lyricli provides a few commands: -* `lrc -l` or `lrc --list` lists the available sources. Enabled +* `lrc -l` or `lrc --list-sources` lists the available sources. Enabled sourcess will have a `*` -* `lrc -e` or `lrc --enable ` enables a source -* `lrc -d` or `lrc --disable ` disables a source -* `lrc -r` or `lrc --reset ` resets the configuration for - a source and disables it. +* `lrc -e` or `lrc --enable-source ` enables a source +* `lrc -d` or `lrc --disable-source ` disables a source without + resetting its configuration. +* `lrc -r` or `lrc --reset-source ` resets the configuration + for a source and disables it. + +And you can print the help or the version: + * `lrc -v` or `lrc --version` prints the version * `lrc -h` or `lrc --help` display built-in help ## Building -The build has only been tested on OSX using Swift 5.0.1. Building defaults +The build has only been tested on OSX using Swift 5.8 Building defaults to the debug configuration. ``` 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/itunes_source.swift b/Sources/lyricli/sources/apple_music_source.swift similarity index 72% rename from Sources/lyricli/sources/itunes_source.swift rename to Sources/lyricli/sources/apple_music_source.swift index 92b9969..5eee588 100644 --- a/Sources/lyricli/sources/itunes_source.swift +++ b/Sources/lyricli/sources/apple_music_source.swift @@ -1,35 +1,35 @@ import ScriptingBridge import Foundation -// Protocol to obtain the track from iTunes -@objc protocol iTunesTrack { +// 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 iTunes -@objc protocol iTunesApplication { - @objc optional var currentTrack: iTunesTrack? {get} +// Protocol to interact with Apple Music +@objc protocol AppleMusicApplication { + @objc optional var currentTrack: AppleMusicTrack? {get} @objc optional var currentStreamTitle: String? {get} } -extension SBApplication: iTunesApplication {} +extension SBApplication: AppleMusicApplication {} // Source that reads track artist and name from current itunes track -class ItunesSource: Source { +class AppleMusicSource: 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 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 = iTunes.currentStreamTitle { + if let currentStreamTitle = appleMusic.currentStreamTitle { if let track = currentStreamTitle { let trackComponents = track.split(separator: "-").map(String.init) @@ -45,7 +45,7 @@ class ItunesSource: Source { } // Attempt to fetch the title from a song - if let currentTrack = iTunes.currentTrack { + if let currentTrack = appleMusic.currentTrack { if let track = currentTrack { if let name = track.name { if let artist = track.artist { @@ -74,4 +74,7 @@ class ItunesSource: Source { 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/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 } }