diff options
| author | Ben Beltran <ben@nsovocal.com> | 2017-05-20 09:52:30 -0500 |
|---|---|---|
| committer | Ben Beltran <ben@nsovocal.com> | 2017-05-20 09:52:30 -0500 |
| commit | ee966aa755acd8eea53c982e43e13aef4f193984 (patch) | |
| tree | 5df111da5948ea198c5db1ca8f62e6c6ebe51178 /Sources | |
| parent | bcd37f402a2586d051ac10a6a4982c8c226b2cce (diff) | |
| parent | 0d4485a445dd387235c141afca7764481eac0311 (diff) | |
Merge branch 'release/0.1.0'0.1.0
Diffstat (limited to 'Sources')
| -rw-r--r-- | Sources/arguments_source.swift | 18 | ||||
| -rw-r--r-- | Sources/configuration.swift | 70 | ||||
| -rw-r--r-- | Sources/lyricli.swift | 60 | ||||
| -rw-r--r-- | Sources/lyrics_engine.swift | 148 | ||||
| -rw-r--r-- | Sources/main.swift | 152 | ||||
| -rw-r--r-- | Sources/source_manager.swift | 50 | ||||
| -rw-r--r-- | Sources/source_protocol.swift | 5 | ||||
| -rw-r--r-- | Sources/track.swift | 15 |
8 files changed, 517 insertions, 1 deletions
diff --git a/Sources/arguments_source.swift b/Sources/arguments_source.swift new file mode 100644 index 0000000..9615318 --- /dev/null +++ b/Sources/arguments_source.swift @@ -0,0 +1,18 @@ +// Source that reads track artist and name from the command line +class ArgumentsSource: Source { + + // Returns a track based on the arguments. It assumes the track artist + // will be the first argument, and the name will be the second, excluding + // any flags. + var currentTrack: Track? { + + if CommandLine.arguments.count >= 3 { + // expected usage: $ ./lyricli <artist> <name> + let trackName: String = CommandLine.arguments[2] + let trackArtist: String = CommandLine.arguments[1] + + return Track(withName: trackName, andArtist: trackArtist) + } + return nil + } +} diff --git a/Sources/configuration.swift b/Sources/configuration.swift new file mode 100644 index 0000000..f1a1ee1 --- /dev/null +++ b/Sources/configuration.swift @@ -0,0 +1,70 @@ +import Foundation + +// Reads and writes the configuration. Config keys are accessed as a dictionary. +class Configuration { + // Location of the global configuration file + private let configurationPath = NSString(string: "~/.lyricli.conf").expandingTildeInPath + + // Default options, will be automatically written to the global config if + // not found. + private var configuration: [String: Any] = [ + "enabled_sources": ["arguments"] + ] + + // The shared instance of the object + static let shared: Configuration = Configuration() + + private init() { + + // Read the config file and attempt to set any of the values. Otherwise + // don't do anything. + + if let data = try? NSData(contentsOfFile: configurationPath) as Data { + if let parsedConfig = try? JSONSerialization.jsonObject(with: data) { + if let parsedConfig = parsedConfig as? [String: Any] { + for (key, value) in parsedConfig { + + if key == "enabled_sources" { + if let value = value as? [String] { + configuration[key] = value + } + } else { + if let value = value as? String { + configuration[key] = value + } + } + } + } + } + } + + writeConfiguration() + } + + // Write the configuration back to the file + private func writeConfiguration() { + + var error: NSError? + + if let outputStream = OutputStream(toFileAtPath: configurationPath, append: false) { + outputStream.open() + JSONSerialization.writeJSONObject(configuration, + to: outputStream, + options: [JSONSerialization.WritingOptions.prettyPrinted], + error: &error) + outputStream.close() + } + } + + // Allow access to the config properties as a dictionary + subscript(index: String) -> Any? { + get { + return configuration[index] + } + + set(newValue) { + configuration[index] = newValue + writeConfiguration() + } + } +} diff --git a/Sources/lyricli.swift b/Sources/lyricli.swift new file mode 100644 index 0000000..7d77a51 --- /dev/null +++ b/Sources/lyricli.swift @@ -0,0 +1,60 @@ +// The main class, handles all the actions that the executable will call +class Lyricli { + + // Version of the application + static var version = "0.1.0" + + // Flag that controls whether we should show the track artist and name before + // the lyrics + static var showTitle = false + + // Obtains the name of the current track from a source, fetches the lyrics + // from an engine and prints them + static func printLyrics() { + + let sourceManager = SourceManager() + + if let currentTrack = sourceManager.currentTrack { + let engine = LyricsEngine(withTrack: currentTrack) + + if let lyrics = engine.lyrics { + if showTitle { + printTitle(currentTrack) + } + + print(lyrics) + } else { + print("Lyrics not found :(") + } + + } else { + print("No Artist/Song could be found :(") + } + } + + // Print the currently available sources + static func printSources() { + print("Listing Sources: Not yet implemented") + } + + // 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") + } + + // Remove a source from the enabled sources configuration + static func disableSource(_ sourceName: String) { + print("Disable source \(sourceName): Not yet implemented") + } + + // Removes any configuration for a source, and disables it + static func resetSource(_ sourceName: String) { + print("Reset source \(sourceName): Not yet implemented") + } + + // Prints the track artist and name + private static func printTitle(_ track: Track) { + print("\(track.artist) - \(track.name)") + } +} diff --git a/Sources/lyrics_engine.swift b/Sources/lyrics_engine.swift new file mode 100644 index 0000000..85e4735 --- /dev/null +++ b/Sources/lyrics_engine.swift @@ -0,0 +1,148 @@ +import Foundation +import HTMLEntities + +// Given a track, attempts to fetch the lyrics from lyricswiki +class LyricsEngine { + + // URL of the API endpoint to use + private let apiURL = "https://lyrics.wikia.com/api.php?action=lyrics&func=getSong&fmt=realjson" + + // Method used to call the API + private let apiMethod = "GET" + + // Regular expxression used to find the lyrics in the lyricswiki HTML + private let lyricsMatcher = "class='lyricbox'>(.+)<div" + + // The track we'll be looking for + private let track: Track + + // Fetches the lyrics and returns if found + var lyrics: String? { + + var lyrics: String? + + // Encode the track artist and name and finish building the API call URL + + if let artist = track.artist.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + if let name: String = track.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + if let url = URL(string: "\(apiURL)&artist=\(artist)&song=\(name)") { + + // We'll lock until the async call is finished + + var requestFinished = false + let asyncLock = NSCondition() + asyncLock.lock() + + // Call the API and unlock when you're done + + fetchLyricsFromAPI(withURL: url, completionHandler: {lyricsResult -> Void in + if let lyricsResult = lyricsResult { + lyrics = lyricsResult + requestFinished = true + asyncLock.signal() + } + }) + + while !requestFinished { + asyncLock.wait() + } + asyncLock.unlock() + } + } + } + + return lyrics + } + + // Initializes with a track + init(withTrack targetTrack: Track) { + + track = targetTrack + } + + // Fetch the lyrics URL from the API, triggers the request to fetch the + // lyrics page + private func fetchLyricsFromAPI(withURL url: URL, completionHandler: @escaping (String?) -> Void) { + + var apiRequest = URLRequest(url: url) + apiRequest.httpMethod = "GET" + + let task = URLSession.shared.dataTask(with: apiRequest, completionHandler: {data, _, _ -> Void in + + // If the response is parseable JSON, and has a url, we'll look for + // the lyrics in there + + if let data = data { + if let jsonResponse = try? JSONSerialization.jsonObject(with: data) { + if let jsonResponse = jsonResponse as? [String: Any] { + if let lyricsUrlString = jsonResponse["url"] as? String { + if let lyricsUrl = URL(string: lyricsUrlString) { + + // At this point we have a valid wiki url + self.fetchLyricsFromPage(withURL: lyricsUrl, completionHandler: completionHandler) + return + } + } + } + } + } + + completionHandler(nil) + }) + task.resume() + } + + // Fetch the lyrics from the page and send it to the parser + private func fetchLyricsFromPage(withURL url: URL, completionHandler: @escaping (String?) -> Void) { + + var pageRequest = URLRequest(url: url) + pageRequest.httpMethod = "GET" + + let task = URLSession.shared.dataTask(with: pageRequest, completionHandler: {data, _, _ -> Void in + + // If the response is parseable JSON, and has a url, we'll look for + // the lyrics in there + + if let data = data { + if let htmlBody = String(data: data, encoding: String.Encoding.utf8) { + self.parseHtmlBody(htmlBody, completionHandler: completionHandler) + return + } + } + + completionHandler(nil) + }) + task.resume() + } + + // Parses the wiki to find the lyrics, decodes the lyrics object + private func parseHtmlBody(_ body: String, completionHandler: @escaping (String?) -> Void) { + + // Look for the lyrics lightbox + + if let regex = try? NSRegularExpression(pattern: lyricsMatcher) { + let matches = regex.matches(in: body, range: NSRange(location: 0, length: body.characters.count)) + + for match in matches { + + let nsBody = body as NSString + let range = match.rangeAt(1) + let encodedLyrics = nsBody.substring(with: range) + + let decodedLyrics = decodeLyrics(encodedLyrics) + + completionHandler(decodedLyrics) + return + } + } + + completionHandler(nil) + } + + // Escapes the HTML entities + private func decodeLyrics(_ lyrics: String) -> String { + + let unescapedLyrics = lyrics.htmlUnescape() + return unescapedLyrics.replacingOccurrences(of: "<br />", with: "\n") + } +} diff --git a/Sources/main.swift b/Sources/main.swift index f7cf60e..9d46e92 100644 --- a/Sources/main.swift +++ b/Sources/main.swift @@ -1 +1,151 @@ -print("Hello, world!") +import CommandLineKit +import Foundation + +// Entry point of the application. This is the main executable +private func main() { + let (flags, parser) = createParser() + + do { + try parser.parse() + } catch { + parser.printUsage(error) + exit(EX_USAGE) + } + + // Boolean Options + + checkHelpFlag(flags["help"], withParser: parser) + checkVersionFlag(flags["version"], withParser: parser) + checkListSourcesFlag(flags["listSources"], withParser: parser) + checkTitleFlag(flags["title"], withParser: parser) + + // String Options + + checkEnableSourceFlag(flags["enableSource"], withParser: parser) + checkDisableSourceFlag(flags["disableSource"], withParser: parser) + checkResetSourceFlag(flags["resetSource"], withParser: parser) + + // Remove any flags so anyone after this gets the unprocessed values + + let programName: [String] = [CommandLine.arguments[0]] + CommandLine.arguments = programName + parser.unparsedArguments + + // Run Lyricli + + Lyricli.printLyrics() +} + +/// Sets up and returns a new options parser +/// +/// - Returns: A Dictionary of Options, and a new CommandLineKit instance +private func createParser() -> ([String:Option], CommandLineKit) { + let parser = CommandLineKit() + var flags: [String:Option] = [:] + + flags["help"] = BoolOption(shortFlag: "h", longFlag: "help", helpMessage: "Prints a help message.") + flags["version"] = BoolOption(shortFlag: "v", longFlag: "version", helpMessage: "Prints the version.") + + flags["enableSource"] = StringOption(shortFlag: "e", longFlag: "enable-source", helpMessage: "Enables a source") + flags["disableSource"] = StringOption(shortFlag: "d", longFlag: "disable-source", helpMessage: "Disables a source") + flags["resetSource"] = StringOption(shortFlag: "r", longFlag: "reset-source", helpMessage: "Resets a source") + flags["listSources"] = BoolOption(shortFlag: "l", longFlag: "list-sources", helpMessage: "Lists all sources") + + flags["title"] = BoolOption(shortFlag: "t", longFlag: "title", helpMessage: "Shows title of song if true") + + parser.addOptions(Array(flags.values)) + + parser.formatOutput = {parseString, type in + + var formattedString: String + + switch type { + case .About: + formattedString = "\(parseString) [<artist_name> <song_name>]" + break + default: + formattedString = parseString + } + + return parser.defaultFormat(formattedString, type: type) + } + + return (flags, parser) +} + +// Handle the Help flag + +private func checkHelpFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let helpFlag = flag as? BoolOption { + if helpFlag.value { + parser.printUsage() + exit(0) + } + } +} + +// Handle the version flag + +private func checkVersionFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let versionFlag = flag as? BoolOption { + if versionFlag.value { + print(Lyricli.version) + exit(0) + } + } +} + +// Handle the list sources flag + +private func checkListSourcesFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let listSourcesFlag = flag as? BoolOption { + if listSourcesFlag.value { + Lyricli.printSources() + exit(0) + } + } +} + +// Handle the title flag + +private func checkTitleFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let titleFlag = flag as? BoolOption { + if titleFlag.value { + Lyricli.showTitle = true + } + } +} + +// Handle the enable source flag + +private func checkEnableSourceFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let enableSourceFlag = flag as? StringOption { + if let source = enableSourceFlag.value { + Lyricli.enableSource(source) + exit(0) + } + } +} + +// Handle the disable source flag + +private func checkDisableSourceFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let disableSourceFlag = flag as? StringOption { + if let source = disableSourceFlag.value { + Lyricli.disableSource(source) + exit(0) + } + } +} + +// Handle the reset source flag + +private func checkResetSourceFlag(_ flag: Option?, withParser parser: CommandLineKit) { + if let resetSourceFlag = flag as? StringOption { + if let source = resetSourceFlag.value { + Lyricli.resetSource(source) + exit(0) + } + } +} + +main() diff --git a/Sources/source_manager.swift b/Sources/source_manager.swift new file mode 100644 index 0000000..5ee1305 --- /dev/null +++ b/Sources/source_manager.swift @@ -0,0 +1,50 @@ +// Collect and manage the available and enabled source +class SourceManager { + + // List of sources enabled for the crurent platform + private var availableSources: [String: Source] = [ + "arguments": ArgumentsSource() + ] + + // Iterate over the sources until we find a track or run out of sources + var currentTrack: Track? { + for source in enabledSources { + if let currentTrack = source.currentTrack { + return currentTrack + } + } + + return nil + } + + // Return the list of enabled sources based on the configuration + var enabledSources: [Source] { + + // Checks the config and returns an array of sources based on the + // enabled and available ones + + var sources = [Source]() + + if let sourceNames = Configuration.shared["enabled_sources"] as? [String] { + for sourceName in sourceNames { + if let source = availableSources[sourceName] { + sources.append(source) + } + } + } + + return sources + } + + // Given a source name, it will enable it and add it to the enabled sources config + func enable(sourceName: String) { + } + + // Given a source name, it will remove it from the enabled sources config + func disable(sourceName: String) { + } + + // Given a source name, it removes any stored configuration and disables it + func reset(sourceName: String) { + } +} diff --git a/Sources/source_protocol.swift b/Sources/source_protocol.swift new file mode 100644 index 0000000..0885994 --- /dev/null +++ b/Sources/source_protocol.swift @@ -0,0 +1,5 @@ +// All sources should comply with this protocol. The currentTrack computed +// property will return a track if the conditions are met +protocol Source { + var currentTrack: Track? { get } +} diff --git a/Sources/track.swift b/Sources/track.swift new file mode 100644 index 0000000..ead4359 --- /dev/null +++ b/Sources/track.swift @@ -0,0 +1,15 @@ +// Holds the name and artist of a track +class Track { + + // The name of the track to search for + let name: String + + // The name of the artist + let artist: String + + init(withName trackName: String, andArtist trackArtist: String) { + + name = trackName + artist = trackArtist + } +} |