]> git.r.bdr.sh - rbdr/lyricli/commitdiff
Merge branch 'release/0.1.0' 0.1.0
authorBen Beltran <redacted>
Sat, 20 May 2017 14:52:30 +0000 (09:52 -0500)
committerBen Beltran <redacted>
Sat, 20 May 2017 14:52:30 +0000 (09:52 -0500)
19 files changed:
.gitignore
.travis.yml [new file with mode: 0644]
CHANGELOG.md [new file with mode: 0644]
CONTRIBUTING.md [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
Package.pins [new file with mode: 0644]
Package.swift [new file with mode: 0644]
README.md
Scripts/install_sourcekitten.sh [new file with mode: 0755]
Scripts/install_swiftlint.sh [new file with mode: 0755]
Sources/arguments_source.swift [new file with mode: 0644]
Sources/configuration.swift [new file with mode: 0644]
Sources/lyricli.swift [new file with mode: 0644]
Sources/lyrics_engine.swift [new file with mode: 0644]
Sources/main.swift
Sources/source_manager.swift [new file with mode: 0644]
Sources/source_protocol.swift [new file with mode: 0644]
Sources/track.swift [new file with mode: 0644]

index 02c087533d1ecf6e6cd20888338045a340fc9737..2deb6d7f914d7b49b51e7769d71774e044854247 100644 (file)
@@ -2,3 +2,4 @@
 /.build
 /Packages
 /*.xcodeproj
+docs
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..46250e5
--- /dev/null
@@ -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 (file)
index 0000000..029adac
--- /dev/null
@@ -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 (file)
index 0000000..966e0e1
--- /dev/null
@@ -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 (file)
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 (file)
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 (file)
index 0000000..4bb7a96
--- /dev/null
@@ -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 (file)
index 0000000..5027708
--- /dev/null
@@ -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)
+    ]
+)
index 6713a7d8d4c33d3d885767ec78cb086f07242fa2..5e7c9951e48d0f52e7cb60bf13ed5e0140735366 100644 (file)
--- 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] <artist_name> <song_name>
+```
+
+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 <source>` enables a source
+* `lrc -d` or `lrc --disable <source>` disables a source
+* `lrc -r` or `lrc --reset-source <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 (executable)
index 0000000..9420a26
--- /dev/null
@@ -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 (executable)
index 0000000..6a2f250
--- /dev/null
@@ -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 (file)
index 0000000..9615318
--- /dev/null
@@ -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 (file)
index 0000000..f1a1ee1
--- /dev/null
@@ -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 (file)
index 0000000..7d77a51
--- /dev/null
@@ -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 (file)
index 0000000..85e4735
--- /dev/null
@@ -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")
+    }
+}
index f7cf60e14f9a9e9805e0463e7fa33b6c91204c4d..9d46e926fef8b7e1d9bea7721df6405a9b9a9b4f 100644 (file)
@@ -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 (file)
index 0000000..5ee1305
--- /dev/null
@@ -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 (file)
index 0000000..0885994
--- /dev/null
@@ -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 (file)
index 0000000..ead4359
--- /dev/null
@@ -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
+    }
+}