]> git.r.bdr.sh - rbdr/lyricli/commitdiff
Add macos sources
authorRuben Beltran del Rio <redacted>
Fri, 15 Mar 2024 21:10:06 +0000 (22:10 +0100)
committerRuben Beltran del Rio <redacted>
Fri, 15 Mar 2024 21:10:06 +0000 (22:10 +0100)
25 files changed:
.build.yml [new file with mode: 0644]
Cargo.lock
Cargo.toml
Makefile
Package.resolved [deleted file]
Package.swift [deleted file]
Sources/lyricli/configuration.swift [deleted file]
Sources/lyricli/errors/configuration_could_not_be_read.swift [deleted file]
Sources/lyricli/errors/source_could_not_be_disabled.swift [deleted file]
Sources/lyricli/errors/source_could_not_be_enabled.swift [deleted file]
Sources/lyricli/errors/source_could_not_be_reset.swift [deleted file]
Sources/lyricli/errors/source_not_available.swift [deleted file]
Sources/lyricli/lyricli.swift [deleted file]
Sources/lyricli/lyricli_command.swift [deleted file]
Sources/lyricli/lyrics_engine.swift [deleted file]
Sources/lyricli/source_manager.swift [deleted file]
Sources/lyricli/sources/apple_music_source.swift [deleted file]
Sources/lyricli/sources/source_protocol.swift [deleted file]
Sources/lyricli/track.swift [deleted file]
man/lrc.1 [new file with mode: 0644]
src/configuration.rs [new file with mode: 0644]
src/main.rs
src/sources/apple_music.rs [new file with mode: 0644]
src/sources/mod.rs
src/sources/spotify.rs [new file with mode: 0644]

diff --git a/.build.yml b/.build.yml
new file mode 100644 (file)
index 0000000..1123577
--- /dev/null
@@ -0,0 +1,28 @@
+image: archlinux
+packages:
+  - make
+  - rsync
+  - coreutils
+  - clang
+  - lld
+  - rustup
+  - aarch64-linux-gnu-gcc
+  - tar
+  - gzip
+sources:
+  - git@git.sr.ht:~rbdr/lyricli
+environment:
+  GPG_TTY: /dev/pts/0
+secrets:
+  - 89d3b676-25d6-4942-8231-38b73aa62bf6
+  - 0b0d3e5e-fbdc-41d0-97ed-ee654fe797ff
+tasks:
+  - set_rust: |
+      cd lyricli
+      make set_rust
+  - install_builders: |
+      cargo install cargo-generate-rpm
+      cargo install cargo-deb
+  - package: |
+      cd lyricli
+      make ci
index 5a286a0b0ca2da345a20a944a32f760cb4822c21..95c6f9130273130ef527ec6059a47d916c6926ad 100644 (file)
@@ -117,6 +117,12 @@ version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
 
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+
 [[package]]
 name = "bumpalo"
 version = "3.15.4"
@@ -187,6 +193,36 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
 
+[[package]]
+name = "cocoa"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "cocoa-foundation",
+ "core-foundation",
+ "core-graphics",
+ "foreign-types 0.5.0",
+ "libc",
+ "objc",
+]
+
+[[package]]
+name = "cocoa-foundation"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
+dependencies = [
+ "bitflags 1.3.2",
+ "block",
+ "core-foundation",
+ "core-graphics-types",
+ "libc",
+ "objc",
+]
+
 [[package]]
 name = "colorchoice"
 version = "1.0.0"
@@ -209,6 +245,30 @@ version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
+[[package]]
+name = "core-graphics"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-graphics-types",
+ "foreign-types 0.5.0",
+ "libc",
+]
+
+[[package]]
+name = "core-graphics-types"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "libc",
+]
+
 [[package]]
 name = "cssparser"
 version = "0.31.2"
@@ -307,7 +367,28 @@ version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
 dependencies = [
- "foreign-types-shared",
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
+dependencies = [
+ "foreign-types-macros",
+ "foreign-types-shared 0.3.1",
+]
+
+[[package]]
+name = "foreign-types-macros"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
 ]
 
 [[package]]
@@ -316,6 +397,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
+[[package]]
+name = "foreign-types-shared"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.1"
@@ -611,8 +698,10 @@ name = "lyricli"
 version = "3.0.0"
 dependencies = [
  "clap",
+ "cocoa",
  "libc",
  "objc",
+ "objc_id",
  "percent-encoding",
  "reqwest",
  "scraper",
@@ -725,6 +814,15 @@ dependencies = [
  "malloc_buf",
 ]
 
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+dependencies = [
+ "objc",
+]
+
 [[package]]
 name = "object"
 version = "0.32.2"
@@ -748,7 +846,7 @@ checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
 dependencies = [
  "bitflags 2.4.2",
  "cfg-if",
- "foreign-types",
+ "foreign-types 0.3.2",
  "libc",
  "once_cell",
  "openssl-macros",
index 89aacadfe18862eed5a816f1fadf8c49bd6a8de6..ba316a9b3ac910cc5da6a524636d7522131c23f2 100644 (file)
@@ -13,8 +13,6 @@ path = "src/main.rs"
 
 [dependencies]
 clap = { version = "4.5.2", features = ["derive"] }
-libc = "0.2.153"
-objc = "0.2.7"
 percent-encoding = "2.3.1"
 reqwest = { version = "0.11", features = ["json"] }
 scraper = "0.19.0"
@@ -22,6 +20,12 @@ serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.114"
 tokio = { version = "1", features = ["full"] }
 
+[target.'cfg(target_os = "macos")'.dependencies]
+libc = "0.2.153"
+objc = "0.2.7"
+cocoa = "0.25.0"
+objc_id = "0.1.1"
+
 [profile.release]
 strip = true
 lto = true
index 3416abdc3cec4bb5307847c69520dcc30ddee660..4d414c84ac6a80ed4a98ac14b9fd309494d215a1 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 profile := dev
 target = $(shell rustc -vV | grep host | awk '{print $$2}')
 architectures := x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu
-app_name := blog
+app_name := lrc
 
 default: build
 
diff --git a/Package.resolved b/Package.resolved
deleted file mode 100644 (file)
index 54140ee..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-{
-  "pins" : [
-    {
-      "identity" : "swift-argument-parser",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/apple/swift-argument-parser",
-      "state" : {
-        "revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
-        "version" : "1.2.2"
-      }
-    },
-    {
-      "identity" : "swiftsoup",
-      "kind" : "remoteSourceControl",
-      "location" : "https://github.com/scinfu/SwiftSoup.git",
-      "state" : {
-        "revision" : "f707b8680cddb96dc1855632340a572ef37bbb98",
-        "version" : "2.5.3"
-      }
-    }
-  ],
-  "version" : 2
-}
diff --git a/Package.swift b/Package.swift
deleted file mode 100644 (file)
index 508b5fb..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-// swift-tools-version:5.8
-
-import PackageDescription
-
-let package = Package(
-    name: "lyricli",
-    dependencies: [
-        /// HTML Parsing
-        .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.5.3"),
-
-        /// ðŸš© Command Line Arguments
-        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.2")
-    ],
-    targets: [
-        .executableTarget(
-            name: "lyricli",
-            dependencies: [
-                .product(name: "SwiftSoup", package: "SwiftSoup"),
-                .product(name: "ArgumentParser", package: "swift-argument-parser")
-            ])
-    ]
-)
diff --git a/Sources/lyricli/configuration.swift b/Sources/lyricli/configuration.swift
deleted file mode 100644 (file)
index b2defc1..0000000
+++ /dev/null
@@ -1,70 +0,0 @@
-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": ["apple_music", "spotify"]
-    ]
-
-    // 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/errors/configuration_could_not_be_read.swift b/Sources/lyricli/errors/configuration_could_not_be_read.swift
deleted file mode 100644 (file)
index fb2b61a..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-struct ConfigurationCouldNotBeRead: Error {
-    var localizedDescription = "The configuration could not be read, check ~/.lyricli.conf"
-}
diff --git a/Sources/lyricli/errors/source_could_not_be_disabled.swift b/Sources/lyricli/errors/source_could_not_be_disabled.swift
deleted file mode 100644 (file)
index b19f3c5..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-// Future improvement: At the moment the sources don't need any special
-// configuration. Once we do have more operations it would make sense
-// to throw more descriptive errors.
-struct SourceCouldNotBeDisabled: Error {
-    var localizedDescription = "The selected source failed while disabling"
-}
diff --git a/Sources/lyricli/errors/source_could_not_be_enabled.swift b/Sources/lyricli/errors/source_could_not_be_enabled.swift
deleted file mode 100644 (file)
index 7797478..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-// Future improvement: At the moment the sources don't need any special
-// configuration. Once we do have more operations it would make sense
-// to throw more descriptive errors.
-struct SourceCouldNotBeEnabled: Error {
-    var localizedDescription = "The selected source failed while enabling"
-}
diff --git a/Sources/lyricli/errors/source_could_not_be_reset.swift b/Sources/lyricli/errors/source_could_not_be_reset.swift
deleted file mode 100644 (file)
index beecf54..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-// Future improvement: At the moment the sources don't need any special
-// configuration. Once we do have more operations it would make sense
-// to throw more descriptive errors.
-struct SourceCouldNotBeReset: Error {
-    var localizedDescription = "The selected source failed while resetting"
-}
diff --git a/Sources/lyricli/errors/source_not_available.swift b/Sources/lyricli/errors/source_not_available.swift
deleted file mode 100644 (file)
index e3d598d..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-struct SourceNotAvailable: Error {
-    var localizedDescription = "The selected source wasn't available"
-}
diff --git a/Sources/lyricli/lyricli.swift b/Sources/lyricli/lyricli.swift
deleted file mode 100644 (file)
index d5e5b91..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-// The main class, handles all the actions that the executable will call
-class Lyricli {
-
-    // Version of the application
-    static var version = "2.0.1"
-
-    // 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 {
-            printLyrics(currentTrack)
-        } else {
-            print("No Artist/Song could be found :(")
-        }
-    }
-
-    // fetches the lyrics from an engine and prints them
-    static func printLyrics(_ currentTrack: Track) {
-        let engine = LyricsEngine(withTrack: currentTrack)
-
-        if showTitle {
-            printTitle(currentTrack)
-        }
-        if let lyrics = engine.lyrics {
-            print(lyrics)
-        } else {
-            print("Lyrics not found :(")
-        }
-    }
-
-    // Print the currently available sources
-    static func printSources() {
-        let sourceManager = SourceManager()
-        for (sourceName, _) in sourceManager.availableSources {
-            if (Configuration.shared["enabled_sources"] as? [String] ?? []).contains(sourceName) {
-                print("\(sourceName) (enabled)")
-            } else {
-                print(sourceName)
-            }
-        }
-    }
-
-    // Runs the enable method of a source and writes the configuration to set it
-    // as enabled
-    static func enableSource(_ sourceName: String) throws {
-        let sourceManager = SourceManager()
-        if let source = sourceManager.availableSources[sourceName] {
-            if let enabledSources = Configuration.shared["enabled_sources"] as? [String] {
-                if source.enable() == false {
-                    throw SourceCouldNotBeEnabled()
-                }
-                if !enabledSources.contains(sourceName) {
-                    Configuration.shared["enabled_sources"] = enabledSources + [sourceName]
-                }
-                return
-            }
-            throw ConfigurationCouldNotBeRead()
-        }
-        throw SourceNotAvailable()
-    }
-
-    // Remove a source from the enabled sources configuration
-    static func disableSource(_ sourceName: String) throws {
-        let sourceManager = SourceManager()
-        if let source = sourceManager.availableSources[sourceName] {
-            if let enabledSources = Configuration.shared["enabled_sources"] as? [String] {
-                if source.disable() == false {
-                    throw SourceCouldNotBeDisabled()
-                }
-                Configuration.shared["enabled_sources"] = enabledSources.filter { $0 != sourceName }
-                return
-            }
-            throw ConfigurationCouldNotBeRead()
-        }
-        throw SourceNotAvailable()
-    }
-
-    // Removes any configuration for a source, and disables it
-    static func resetSource(_ sourceName: String) throws {
-        let sourceManager = SourceManager()
-        if let source = sourceManager.availableSources[sourceName] {
-            if source.reset() == false {
-                throw SourceCouldNotBeReset()
-            }
-            try disableSource(sourceName)
-            return
-        }
-        throw SourceNotAvailable()
-    }
-
-    // Prints the track artist and name
-    private static func printTitle(_ track: Track) {
-        print("\(track.artist) - \(track.name)")
-    }
-}
diff --git a/Sources/lyricli/lyricli_command.swift b/Sources/lyricli/lyricli_command.swift
deleted file mode 100644 (file)
index 872ab6e..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-import Darwin
-import ArgumentParser
-
-@main
-struct LyricliCommand: ParsableCommand {
-
-    // Positional Arguments
-    @Argument var artist: String?
-    @Argument var trackName: String?
-
-    // Flags
-    @Flag(name: .shortAndLong, help: "Prints the version.")
-    var version = false
-
-    @Flag(name: [.long, .customShort("t")], help: "Shows title of track if true")
-    var showTitle = false
-
-    @Flag(name: .shortAndLong, help: "Lists all sources")
-    var listSources = false
-
-    // Named Arguments
-    @Option(name: .shortAndLong, help: ArgumentHelp("Enables a source", valueName: "source"))
-    var enableSource: String?
-
-    @Option(name: .shortAndLong, help: ArgumentHelp("Disables a source", valueName: "source"))
-    var disableSource: String?
-
-    @Option(name: .shortAndLong, help: ArgumentHelp("Resets a source", valueName: "source"))
-    var resetSource: String?
-
-    mutating func run() throws {
-
-        // Handle the version flag
-        if version {
-            print(Lyricli.version)
-            Darwin.exit(0)
-        }
-
-        // Handle the list sources flag
-        if listSources {
-            Lyricli.printSources()
-            Darwin.exit(0)
-        }
-
-        // Handle the enable source option
-        if let source = enableSource {
-            do {
-                try Lyricli.enableSource(source)
-            } catch let error {
-                handleErrorAndQuit(error)
-            }
-            Darwin.exit(0)
-        }
-
-        // Handle the disable source option
-        if let source = disableSource {
-            do {
-                try Lyricli.disableSource(source)
-            } catch let error {
-                handleErrorAndQuit(error)
-            }
-            Darwin.exit(0)
-        }
-
-        // Handle the reset source flag
-        if let source = resetSource {
-            do {
-                try Lyricli.resetSource(source)
-            } catch let error {
-                handleErrorAndQuit(error)
-            }
-            Darwin.exit(0)
-        }
-
-        Lyricli.showTitle = showTitle
-
-        if let artist {
-            let currentTrack: Track
-            if let trackName {
-                currentTrack = Track(withName: trackName, andArtist: artist)
-            } else {
-                currentTrack = Track(withName: "", andArtist: artist)
-            }
-            Lyricli.printLyrics(currentTrack)
-            Darwin.exit(0)
-        }
-
-        Lyricli.printLyrics()
-    }
-
-    private func handleErrorAndQuit(_ error: Error) {
-        fputs(error.localizedDescription, stderr)
-        Darwin.exit(1)
-    }
-}
diff --git a/Sources/lyricli/lyrics_engine.swift b/Sources/lyricli/lyrics_engine.swift
deleted file mode 100644 (file)
index e0752d7..0000000
+++ /dev/null
@@ -1,142 +0,0 @@
-import Foundation
-import SwiftSoup
-
-// Given a track, attempts to fetch the lyrics from lyricswiki
-class LyricsEngine {
-
-    private let clientToken = <GENIUS_CLIENT_TOKEN>
-
-    // URL of the API endpoint to use
-    private let apiURL = "https://api.genius.com/search"
-
-    // Method used to call the API
-    private let apiMethod = "GET"
-
-    // 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 = track.name.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
-
-                if let url = URL(string: "\(apiURL)?access_token=\(clientToken)&q=\(artist)%20\(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
-
-                    searchLyricsUsingAPI(withURL: url, completionHandler: {lyricsResult -> Void in
-                        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 searchLyricsUsingAPI(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 response = jsonResponse["response"] as? [String: Any] {
-                            if let hits = response["hits"] as? [[String: Any]] {
-                                let filteredHits = hits.filter { $0["type"] as? String == "song" }
-                                if filteredHits.count > 0 {
-                                    let firstHit = hits[0]
-                                    if let firstHitData = firstHit["result"] as? [String: Any] {
-                                        if let lyricsUrlString = firstHitData["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) {
-
-        do {
-            let document: Document = try SwiftSoup.parse(body)
-            let lyricsBox = try document.select("div[data-lyrics-container=\"true\"]")
-            try lyricsBox.select("br").after("\\n")
-            let lyrics = try lyricsBox.text()
-            completionHandler(lyrics.replacingOccurrences(of: "\\n", with: "\r\n"))
-        } catch {
-            completionHandler(nil)
-        }
-    }
-}
diff --git a/Sources/lyricli/source_manager.swift b/Sources/lyricli/source_manager.swift
deleted file mode 100644 (file)
index 481d327..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-// Collect and manage the available and enabled source
-class SourceManager {
-
-    // List of sources enabled for the crurent platform
-    var availableSources: [String: Source] = [
-        "apple_music": AppleMusicSource(),
-        "spotify": SpotifySource()
-    ]
-
-    // 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/lyricli/sources/apple_music_source.swift b/Sources/lyricli/sources/apple_music_source.swift
deleted file mode 100644 (file)
index 5eee588..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-import ScriptingBridge
-import Foundation
-
-// Protocol to obtain the track from 
-@objc protocol AppleMusicTrack {
-    @objc optional var name: String {get}
-    @objc optional var artist: String {get}
-}
-
-// Protocol to interact with Apple Music
-@objc protocol AppleMusicApplication {
-    @objc optional var currentTrack: AppleMusicTrack? {get}
-    @objc optional var currentStreamTitle: String? {get}
-}
-
-extension SBApplication: AppleMusicApplication {}
-
-// Source that reads track artist and name from current itunes track
-class AppleMusicSource: Source {
-
-    // Calls the spotify API and returns the current track
-    var currentTrack: Track? {
-
-        if let appleMusic: AppleMusicApplication = SBApplication(bundleIdentifier: bundleIdentifier) {
-            if let application = appleMusic as? SBApplication {
-              if !application.isRunning {
-                return nil
-              }
-            }
-
-            // Attempt to fetch the title from a stream
-            if let currentStreamTitle = appleMusic.currentStreamTitle {
-                if let track = currentStreamTitle {
-
-                    let trackComponents = track.split(separator: "-").map(String.init)
-
-                    if trackComponents.count == 2 {
-                        let artist = trackComponents[0].trimmingCharacters(in: .whitespaces)
-                        let name = trackComponents[1].trimmingCharacters(in: .whitespaces)
-
-                        return Track(withName: name, andArtist: artist)
-                    }
-
-                }
-            }
-
-            // Attempt to fetch the title from a song
-            if let currentTrack = appleMusic.currentTrack {
-                if let track = currentTrack {
-                    if let name = track.name {
-                        if let artist = track.artist {
-
-                            // track properties are empty strings if itunes is closed
-                            if name == "" || artist == "" {
-                                return nil
-                            }
-                            return Track(withName: name, andArtist: artist)
-                        }
-                    }
-                }
-            }
-        }
-
-        return nil
-    }
-
-    private var bundleIdentifier: String {
-        if ProcessInfo().isOperatingSystemAtLeast(
-          OperatingSystemVersion(majorVersion: 10, minorVersion: 15, patchVersion: 0)
-          ) {
-          return "com.apple.Music"
-        }
-
-        return "com.apple.iTunes"
-    }
-
-    func enable() -> Bool { return true }
-    func disable() -> Bool { return true }
-    func reset() -> Bool { return true }
-}
diff --git a/Sources/lyricli/sources/source_protocol.swift b/Sources/lyricli/sources/source_protocol.swift
deleted file mode 100644 (file)
index 8d52603..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-// 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 }
-
-    func disable() -> Bool
-    func enable() -> Bool
-    func reset() -> Bool
-}
diff --git a/Sources/lyricli/track.swift b/Sources/lyricli/track.swift
deleted file mode 100644 (file)
index ead4359..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-// 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
-    }
-}
diff --git a/man/lrc.1 b/man/lrc.1
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/configuration.rs b/src/configuration.rs
new file mode 100644 (file)
index 0000000..f678461
--- /dev/null
@@ -0,0 +1,98 @@
+use std::env;
+use std::fs::{create_dir_all, write, File};
+use std::io::{Read, Result};
+use std::path::PathBuf;
+use serde::{Deserialize, Serialize};
+use serde_json;
+
+const CONFIG_ENV_VARIABLE: &str = "LYRICLI_CONFIG_DIRECTORY";
+const CONFIG_DEFAULT_LOCATION: &str = "XDG_CONFIG_HOME";
+const CONFIG_FALLBACK_LOCATION: &str = ".config";
+const CONFIG_SUBDIRECTORY: &str = ".config";
+const CONFIG_FILENAME: &str = "lyricli.conf";
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Configuration {
+    enabled_sources: Vec<String>
+}
+
+impl Configuration {
+
+    pub fn new() -> Self  {
+
+        if let Some(configuration) = Configuration::read() {
+            return configuration;
+        }
+
+        let configuration = Configuration::default();
+
+        // Write the defaults.
+        Configuration::write(&configuration).ok();
+        configuration
+    }
+
+    // Public API
+
+    pub fn is_enabled(&self, source_name: &String) -> bool {
+        self.enabled_sources.contains(source_name)
+    }
+
+    pub fn enable_source(&mut self, source_name: &String) -> Result<()> {
+        if !self.enabled_sources.contains(source_name) {
+            self.enabled_sources.push(source_name.to_owned())
+        }
+        Configuration::write(self)
+    }
+
+    pub fn disable_source(&mut self, source_name: &String) -> Result<()> {
+        self.enabled_sources.retain(|source| source != source_name);
+        Configuration::write(self)
+    }
+
+    // Helpers
+
+    fn default() -> Configuration {
+        Configuration {
+            enabled_sources: vec![
+                "apple_music".to_string(),
+                "spotify".to_string()
+            ]
+        }
+    }
+
+    fn read() -> Option<Configuration> {
+        let config_file_path = Configuration::file_path();
+
+        let mut config_file = File::open(&config_file_path).ok()?;
+        let mut config_contents = String::new();
+        config_file.read_to_string(&mut config_contents).ok()?;
+        serde_json::from_str(&config_contents).ok()?
+    }
+
+    fn write(configuration: &Configuration) -> Result<()> {
+        let config_file_path = Configuration::file_path();
+        if let Ok(serialized_configuration) = serde_json::to_string(&configuration) {
+            write(&config_file_path, serialized_configuration)?;
+        }
+        Ok(())
+    }
+
+    fn file_path() -> PathBuf {
+        let config_directory = Configuration::directory();
+        create_dir_all(&config_directory).ok();
+        config_directory.join(CONFIG_FILENAME)
+    }
+
+    fn directory() -> PathBuf {
+        match env::var(CONFIG_ENV_VARIABLE) {
+            Ok(directory) => PathBuf::from(directory),
+            Err(_) => match env::var(CONFIG_DEFAULT_LOCATION) {
+                Ok(directory) => PathBuf::from(directory),
+                Err(_) => match env::var("HOME") {
+                    Ok(directory) => PathBuf::from(directory).join(CONFIG_FALLBACK_LOCATION),
+                    Err(_) => panic!("Could not find required directory, {} or {} should be set and readable.", CONFIG_ENV_VARIABLE, CONFIG_DEFAULT_LOCATION),
+                },
+            },
+        }.join(CONFIG_SUBDIRECTORY)
+    }
+}
index 13b7e37903f13ff278f2165a37718ffbdb6fe19c..a6119266f25581e98b17ff7530816455372b4d2e 100644 (file)
@@ -1,10 +1,11 @@
-// mod configuration;
+mod configuration;
 mod lyrics_engine;
 mod sources;
 
 use std::io::{Result, Error, ErrorKind::Other};
-use clap::{Parser};
+use clap::Parser;
 
+use configuration::Configuration;
 use sources::{enable, disable, get_track, reset, list};
 use lyrics_engine::print_lyrics;
 
@@ -45,22 +46,37 @@ pub struct Track {
 
 #[tokio::main]
 async fn main() -> Result<()> {
+    let mut configuration = Configuration::new();
     let arguments = Arguments::parse();
 
     if arguments.list_sources {
-        return list();
+        let sources = list();
+        for source in sources {
+            print!("{}", source);
+            if configuration.is_enabled(&source) {
+                print!(" (enabled)");
+            }
+            println!("");
+        }
+        return Ok(());
     }
 
     if let Some(source_name) = arguments.enable_source {
-        return enable(source_name);
+        if !configuration.is_enabled(&source_name) {
+            enable(&source_name)?;
+        }
+        return configuration.enable_source(&source_name);
     }
 
     if let Some(source_name) = arguments.disable_source {
-        return disable(source_name);
+        if configuration.is_enabled(&source_name) {
+            disable(&source_name)?;
+        }
+        return configuration.disable_source(&source_name);
     }
 
     if let Some(source_name) = arguments.reset_source {
-        return reset(source_name);
+        return reset(&source_name);
     }
 
     let current_track: Track;
diff --git a/src/sources/apple_music.rs b/src/sources/apple_music.rs
new file mode 100644 (file)
index 0000000..cb36f5c
--- /dev/null
@@ -0,0 +1,71 @@
+use std::ffi::CStr;
+use std::io::Result;
+
+use cocoa::{base::nil, foundation::NSString};
+use objc::{class, msg_send, sel, sel_impl, runtime::Object};
+use objc_id::Id;
+
+use crate::Track;
+
+use super::LyricsSource;
+
+pub struct AppleMusic;
+
+impl AppleMusic {
+    pub fn new() -> Self {
+        AppleMusic
+    }
+}
+
+impl LyricsSource for AppleMusic {
+
+    fn name(&self) -> String {
+        "apple_music".to_string()
+    }
+
+    fn current_track(&self) -> Option<Track> {
+        unsafe {
+            let app: Id<Object> = {
+                let cls = class!(SBApplication);
+                let bundle_identifier = NSString::alloc(nil).init_str("com.apple.Music"); 
+                let app: *mut Object = msg_send![cls, applicationWithBundleIdentifier:bundle_identifier];
+                Id::from_ptr(app)
+            };
+
+            if msg_send![app, isRunning] {
+                let current_track: *mut Object = msg_send![app, currentTrack];
+                if !current_track.is_null() {
+                    let name_raw: *mut Object = msg_send![current_track, name];
+                    let artist_raw: *mut Object = msg_send![current_track, artist];
+
+                    let name_ptr: *const i8 = msg_send![name_raw, UTF8String];
+                    let artist_ptr: *const i8 = msg_send![artist_raw, UTF8String];
+
+                    let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned();
+                    let artist = CStr::from_ptr(artist_ptr).to_string_lossy().into_owned();
+
+
+                    return Some(Track {
+                        name,
+                        artist
+                    })
+                }
+            }
+        }
+        None
+    }
+
+    fn disable(&self) -> Result<()> {
+        Ok(())
+    }
+
+    fn enable(&self) -> Result<()> {
+        Ok(())
+    }
+
+    fn reset(&self) -> Result<()> {
+        Ok(())
+    }
+}
+
+
index fb7af8fa88b353f3b35682b133407c0bfe580711..78f749994ca5ce76725639e27ee15560e6b330b6 100644 (file)
@@ -1,9 +1,10 @@
-use std::io::Result;
+use std::io::{Result, Error, ErrorKind::Other};
+
+#[cfg(target_os = "macos")]
+mod apple_music;
+#[cfg(target_os = "macos")]
+mod spotify;
 
-// #[cfg(target_os = "macos")]
-// mod applie_music;
-// #[cfg(target_os = "macos")]
-// mod spotify;
 // #[cfg(not(target_os = "macos"))]
 // mod rhythmbox;
 // #[cfg(not(target_os = "macos"))]
@@ -13,10 +14,10 @@ use std::io::Result;
 // #[cfg(not(target_os = "macos"))]
 // mod tauon;
 
-// #[cfg(target_os = "macos")]
-// use apple_music::AppleMusic;
-// #[cfg(target_os = "macos")]
-// use spotify::Spotify;
+#[cfg(target_os = "macos")]
+use apple_music::AppleMusic;
+#[cfg(target_os = "macos")]
+use spotify::Spotify;
 
 // #[cfg(not(target_os = "macos"))]
 // use rhythmbox::Rhythmbox;
@@ -39,26 +40,47 @@ pub trait LyricsSource {
     fn reset(&self) -> Result<()>;
 }
 
-pub fn list() -> Result<()> {
-    Ok(())
+pub fn list() -> Vec<String> {
+    available_sources().into_iter().map(|source| source.name()).collect()
 }
 
-pub fn enable(source_name: String) -> Result<()> {
-    println!("Enabling {}", source_name);
-    Ok(())
+pub fn enable(source_name: &String) -> Result<()> {
+    let sources = available_sources();
+    for source in sources {
+        if &source.name() == source_name {
+            return source.enable()
+        }
+    }
+    Err(Error::new(Other, "No such source was available."))
 }
 
-pub fn disable(source_name: String) -> Result<()> {
-    println!("Disabling {}", source_name);
-    Ok(())
+pub fn disable(source_name: &String) -> Result<()> {
+    let sources = available_sources();
+    for source in sources {
+        if &source.name() == source_name {
+            return source.disable()
+        }
+    }
+    Err(Error::new(Other, "No such source was available."))
 }
 
-pub fn reset(source_name: String) -> Result<()> {
-    println!("Reset {}", source_name);
-    Ok(())
+pub fn reset(source_name: &String) -> Result<()> {
+    let sources = available_sources();
+    for source in sources {
+        if &source.name() == source_name {
+            return source.reset()
+        }
+    }
+    Err(Error::new(Other, "No such source was available."))
 }
 
 pub fn get_track() -> Option<Track> {
+    let sources = available_sources();
+    for source in sources {
+        if let Some(track) = source.current_track() {
+            return Some(track);
+        }
+    }
     return None
 }
 
@@ -66,8 +88,8 @@ pub fn available_sources() -> Vec<Box<dyn LyricsSource>> {
     let mut sources: Vec<Box<dyn LyricsSource>> = Vec::new();
     #[cfg(target_os = "macos")]
     {
-        // sources.push(Box::new(AppleMusic::new()));
-        // sources.push(Box::new(Spotify::new()));
+        sources.push(Box::new(AppleMusic::new()));
+        sources.push(Box::new(Spotify::new()));
     }
 
     #[cfg(not(target_os = "macos"))]
diff --git a/src/sources/spotify.rs b/src/sources/spotify.rs
new file mode 100644 (file)
index 0000000..b23ea73
--- /dev/null
@@ -0,0 +1,69 @@
+use std::ffi::CStr;
+use std::io::Result;
+
+use cocoa::{base::nil, foundation::NSString};
+use objc::{class, msg_send, sel, sel_impl, runtime::Object};
+use objc_id::Id;
+
+use crate::Track;
+
+use super::LyricsSource;
+
+pub struct Spotify;
+
+impl Spotify {
+    pub fn new() -> Self {
+        Spotify
+    }
+}
+
+impl LyricsSource for Spotify {
+
+    fn name(&self) -> String {
+        "spotify".to_string()
+    }
+
+    fn current_track(&self) -> Option<Track> {
+        unsafe {
+            let app: Id<Object> = {
+                let cls = class!(SBApplication);
+                let bundle_identifier = NSString::alloc(nil).init_str("com.spotify.Client"); 
+                let app: *mut Object = msg_send![cls, applicationWithBundleIdentifier:bundle_identifier];
+                Id::from_ptr(app)
+            };
+
+            if msg_send![app, isRunning] {
+                let current_track: *mut Object = msg_send![app, currentTrack];
+                if !current_track.is_null() {
+                    let name_raw: *mut Object = msg_send![current_track, name];
+                    let artist_raw: *mut Object = msg_send![current_track, artist];
+
+                    let name_ptr: *const i8 = msg_send![name_raw, UTF8String];
+                    let artist_ptr: *const i8 = msg_send![artist_raw, UTF8String];
+
+                    let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned();
+                    let artist = CStr::from_ptr(artist_ptr).to_string_lossy().into_owned();
+
+
+                    return Some(Track {
+                        name,
+                        artist
+                    })
+                }
+            }
+        }
+        None
+    }
+
+    fn disable(&self) -> Result<()> {
+        Ok(())
+    }
+
+    fn enable(&self) -> Result<()> {
+        Ok(())
+    }
+
+    fn reset(&self) -> Result<()> {
+        Ok(())
+    }
+}