From: Ben Beltran Date: Sat, 20 May 2017 14:52:30 +0000 (-0500) Subject: Merge branch 'release/0.1.0' X-Git-Tag: 0.1.0^0 X-Git-Url: https://git.r.bdr.sh/rbdr/lyricli/commitdiff_plain/ee966aa755acd8eea53c982e43e13aef4f193984?hp=bcd37f402a2586d051ac10a6a4982c8c226b2cce Merge branch 'release/0.1.0' --- diff --git a/.gitignore b/.gitignore index 02c0875..2deb6d7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.build /Packages /*.xcodeproj +docs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..46250e5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: + - swift + +osx_image: xcode8.3 + +install: + - gem install jazzy + - "./Scripts/install_sourcekitten.sh" + - "./Scripts/install_swiftlint.sh" + +script: + - make lint + - make build + +before_deploy: + - make document + +deploy: + provider: pages + skip_cleanup: true + github_token: "$GITHUB_TOKEN" + local_dir: docs + on: + branch: master + +env: + global: + secure: EyOzJFSGY2ifBVqnQz7Xc0sDcg9maLb7VDKWIC2+1n2RsMHGptsxDfJf9r/bOc2kJN9mCzw19eA3XTkypeHKgIgPZ+boLPTDqiiNcD+0iVkYxqw/Q0v5et1+pJaOUo93cKfl2WLWXvISU1MYuzbjGwmnjPDUmujTwGZH1SFvhOKynqx9V/PiL4ZF+CurU2far+diLDhJXUPT4mDV6lDfiALUBvfj50AplM928Vwc6xr71SFii4fE+1GGGGI23ZyXmhnYIJBfQ/9d2wzW6szSRz+q0Gq8jQFJ2cZmBQPnfPY6/xARkDIf5H55HIxLg8pqA7Yn+WDT6/a8uoFLY6OzI8B/TTZ/pX4LXhkK0gbmXeeigRjxN3Dcsb++n9e5+3/Bq0y/Vm+Ufy+TtEvExvU6vdzDu8YZQaE0T2Loyqaw3BQBMoCunv4i7z0crXTLyNYNuc3zDGDmjkR3laxX8lcEZ85zTRTuYqxmvQxkxWUHKYQOvGy7SfkD1xc73f1XvCqpx45utZX0U/OzIxRflWFNy4mlgLvo23h5T0b44LGBBBWEVkjt5YduOuSo9L1wtOrADcDYyxSciIby2SHd4B2fGOb059KyCIUcX/qgOS6FJlmPeC963NCAuZB6DyscaoT6DrJto9nuZW2wNYdo7dvCC2E4ZqHnRPl2zux/RTmeuCU= diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..029adac --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/) +and this project adheres to [Semantic Versioning](http://semver.org/). + +## 0.1.0 - 2017-05-20 +### Added +- Documentation with Jazzy / SourceKitten +- Apache License +- Documentation to help use lyricli and contribute +- Makefile to help build and install +- CI integration +- Lyrics engine to fetch the lyrics from lyricswiki +- Arguments source to read song and artist from command line +- Parsing of options to match legacy lyricli +- Placeholder for the library with expected endpoints + +[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/master...develop diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..966e0e1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# Contributing to Lyricli + +At this moment this is a really small project that I used to revive an +old ruby project and learn swift, it probably has a lot of bad code that +is not in "the swift way", it has no tests and can always be extended to +support more sources. + +## The ~~Roadmap~~ ~~Streetmap~~ ~~Pathmap~~ Corridormap + +* Writing sources (They're used to automatically obtain artist and song + name from +* Writing tests +* Improving code to match idiomatic swift +* Improving the documentation +* Extending the lyrics engine to support different lyrics sources +* Improving the build system + +## How to contribute + +Be nice, always, to others and yourself: use welcoming and inclusive language, +be patient and respectful of others' opinions and experiences. + +Do not insult others, use sexualized language, publish others' personal +information without their consent. Do not harass others. + +To report unacceptabe behavior, send an [e-mail][email] + +## Sending Pull Requests + +* Run [swiftlint][swiftlint] on the Source directory and make sure there are no errors +* There should be no warnings on compilation +* [CI][ci] should be green +* Make the PRs according to [Git Flow][gitflow]: (features go to + develop, hotfixes go to master) + +[gitflow]: https://github.com/nvie/gitflow +[swiftlint]: https://github.com/realm/SwiftLint +[email]: mailto:ben@nsovocal.com +[ci]: https://travis-ci.org/lyricli-app/lyricli diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0f57bd9 --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +configuration = debug +build_path = .build + +# These are used to rename the executable to lrc without renaming the package +source_binary_name = lyricli +target_binary_name = lrc +install_path = /usr/local/bin +source_binary_path = $(build_path)/$(configuration)/$(source_binary_name) +install_binary_path = $(install_path)/$(target_binary_name) + +# Default to release configuration on install +install: configuration = release + +default: build + +build: + swift build --build-path $(build_path) --configuration $(configuration) + +install: build + cp $(source_binary_path) $(install_binary_path) + +test: build + swift test + +lint: + cd Sources && swiftlint + +document: build + sourcekitten doc --spm-module $(source_binary_name) > $(build_path)/$(source_binary_name).json + jazzy \ + -s $(build_path)/$(source_binary_name).json \ + --readme README.md \ + --clean \ + --author Lyricli \ + --author_url https://github.com/lyricli-app \ + --github_url https://github.com/lyricli-app/lyricli \ + --module-version 0.1.0 \ + --module Lyricli \ + +clean: + swift build --clean + +.PHONY: build install test clean lint diff --git a/Package.pins b/Package.pins new file mode 100644 index 0000000..4bb7a96 --- /dev/null +++ b/Package.pins @@ -0,0 +1,18 @@ +{ + "autoPin": true, + "pins": [ + { + "package": "CommandeLineKit", + "reason": null, + "repositoryURL": "https://github.com/rbdr/CommandLineKit", + "version": "4.0.0" + }, + { + "package": "HTMLEntities", + "reason": null, + "repositoryURL": "https://github.com/IBM-Swift/swift-html-entities.git", + "version": "3.0.3" + } + ], + "version": 1 +} \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..5027708 --- /dev/null +++ b/Package.swift @@ -0,0 +1,9 @@ +import PackageDescription + +let package = Package( + name: "lyricli", + dependencies: [ + .Package(url: "https://github.com/rbdr/CommandLineKit", majorVersion: 4, minor: 0), + .Package(url: "https://github.com/IBM-Swift/swift-html-entities.git", majorVersion: 3, minor: 0) + ] +) diff --git a/README.md b/README.md index 6713a7d..5e7c995 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,85 @@ -#Lyricli (lrc) +# Lyricli (lrc) -A command line tool to show the lyrics of your current song +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: + +``` +$ lrc [-t] +``` + +When you run it without arguments, it will look in the available source +to try to find a playing song and extract the lyrics. If you include the +`-t` flag, it will show the song and artist names before the lyrics. + +``` +$ lrc [-t] +``` + +When you run it with arguments, it will use them to search for the +lyrics. This won't work if you manually disable the arguments source in +your configuration file. If you include the `-t` flag, it will show the +song and artist names before the lyrics. + +### Commands + +In order to configure + +* `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-source ` resets the configuration for + a source and disables it. +* `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 3.1. Building defaults +to the debug configuration. + +``` +make +``` + +## Installing from source + +Builds lyricli in release configuration and copies the executable as +`lrc` to `/usr/local/bin` + +``` +make install +``` + +### Installing to a custom directory + +This can be done by overriding the `install_path` variable + +``` +make install install_path=/opt/bin +``` + +## Linting and Generating Documentation + +We use [swiftlint][swiftlint] to lint, and `make lint` to run it. +We use [jazzy][jazzy] and [SourceKitten][sourcekitten] to document, and +`make document` to generate it. + +## Running tests + +No tests at the moment 😬... but the makefile is mapped to run the swift +tests. + +``` +make test +``` + +[![Build Status](https://travis-ci.org/lyricli-app/lyricli.svg?branch=master)](https://travis-ci.org/lyricli-app/lyricli) + +[swiftlint]: https://github.com/realm/SwiftLint +[jazzy]: https://github.com/realm/jazzy +[sourcekitten]: https://github.com/jpsim/SourceKitten diff --git a/Scripts/install_sourcekitten.sh b/Scripts/install_sourcekitten.sh new file mode 100755 index 0000000..9420a26 --- /dev/null +++ b/Scripts/install_sourcekitten.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Taken from: https://alexplescan.com/posts/2016/03/03/setting-up-swiftlint-on-travis-ci/ +# And adapted for sourcekitten + +# Installs the SourceKitten package. +# Tries to get the precompiled .pkg file from Github, but if that +# fails just recompiles from source. + +set -e + +SOURCEKITTEN_PKG_PATH="/tmp/SourceKitten.pkg" +SOURCEKITTEN_PKG_URL="https://github.com/jpsim/SourceKitten/releases/download/0.17.3/SourceKitten.pkg" + +wget --output-document=$SOURCEKITTEN_PKG_PATH $SOURCEKITTEN_PKG_URL + +if [ -f $SOURCEKITTEN_PKG_PATH ]; then + echo "SourceKitten package exists! Installing it..." + sudo installer -pkg $SOURCEKITTEN_PKG_PATH -target / +else + echo "SourceKitten package doesn't exist. Compiling from source..." && + git clone https://github.com/jspim/SourceKitten.git /tmp/SourceKitten && + cd /tmp/SourceKitten && + sudo make install +fi diff --git a/Scripts/install_swiftlint.sh b/Scripts/install_swiftlint.sh new file mode 100755 index 0000000..6a2f250 --- /dev/null +++ b/Scripts/install_swiftlint.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# Taken from: https://alexplescan.com/posts/2016/03/03/setting-up-swiftlint-on-travis-ci/ + +# Installs the SwiftLint package. +# Tries to get the precompiled .pkg file from Github, but if that +# fails just recompiles from source. + +set -e + +SWIFTLINT_PKG_PATH="/tmp/SwiftLint.pkg" +SWIFTLINT_PKG_URL="https://github.com/realm/SwiftLint/releases/download/0.18.1/SwiftLint.pkg" + +wget --output-document=$SWIFTLINT_PKG_PATH $SWIFTLINT_PKG_URL + +if [ -f $SWIFTLINT_PKG_PATH ]; then + echo "SwiftLint package exists! Installing it..." + sudo installer -pkg $SWIFTLINT_PKG_PATH -target / +else + echo "SwiftLint package doesn't exist. Compiling from source..." && + git clone https://github.com/realm/SwiftLint.git /tmp/SwiftLint && + cd /tmp/SwiftLint && + git submodule update --init --recursive && + sudo make install +fi 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 + 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'>(.+) 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: "
", 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) [ ]" + 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 + } +}