--- /dev/null
+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
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"
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"
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"
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]]
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"
version = "3.0.0"
dependencies = [
"clap",
+ "cocoa",
"libc",
"objc",
+ "objc_id",
"percent-encoding",
"reqwest",
"scraper",
"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"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
- "foreign-types",
+ "foreign-types 0.3.2",
"libc",
"once_cell",
"openssl-macros",
[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"
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
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
+++ /dev/null
-{
- "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
-}
+++ /dev/null
-// 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")
- ])
- ]
-)
+++ /dev/null
-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()
- }
- }
-}
+++ /dev/null
-struct ConfigurationCouldNotBeRead: Error {
- var localizedDescription = "The configuration could not be read, check ~/.lyricli.conf"
-}
+++ /dev/null
-// 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"
-}
+++ /dev/null
-// 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"
-}
+++ /dev/null
-// 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"
-}
+++ /dev/null
-struct SourceNotAvailable: Error {
- var localizedDescription = "The selected source wasn't available"
-}
+++ /dev/null
-// 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)")
- }
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-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)
- }
- }
-}
+++ /dev/null
-// 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) {
- }
-}
+++ /dev/null
-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 }
-}
+++ /dev/null
-// 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
-}
+++ /dev/null
-// 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
- }
-}
--- /dev/null
+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)
+ }
+}
-// 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;
#[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;
--- /dev/null
+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(())
+ }
+}
+
+
-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"))]
// #[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;
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
}
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"))]
--- /dev/null
+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(())
+ }
+}