diff --git a/Spotifious.alfredworkflow b/Spotifious.alfredworkflow index 5579cc4..57eadd7 100644 Binary files a/Spotifious.alfredworkflow and b/Spotifious.alfredworkflow differ diff --git a/artwork/index.htm b/artwork/index.htm deleted file mode 100644 index e79b3f0..0000000 --- a/artwork/index.htm +++ /dev/null @@ -1 +0,0 @@ -so github caches \ No newline at end of file diff --git a/clear.php b/clear.php deleted file mode 100644 index e0fb425..0000000 --- a/clear.php +++ /dev/null @@ -1,16 +0,0 @@ -; this is identical to his `clear.php` - -$folder = 'artwork'; -$bytes = 0; -$total = 0; -if ($handle = opendir($folder)) { - - while (false !== ($file = readdir($handle))) { - if (stristr($file, '.png')) { - unlink($folder . '/' . $file); - } - } - - closedir($handle); -} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..5a0d8a4 --- /dev/null +++ b/composer.json @@ -0,0 +1,8 @@ +{ + "autoload": { + "psr-4": { + "OhAlfred\\": "src/citelao/OhAlfred/", + "Spotifious\\": "src/citelao/Spotifious/" + } + } +} diff --git a/include/images/checked.png b/include/images/checked.png new file mode 100644 index 0000000..8838814 Binary files /dev/null and b/include/images/checked.png differ diff --git a/include/images/configuration.png b/include/images/configuration.png new file mode 100644 index 0000000..b55417c Binary files /dev/null and b/include/images/configuration.png differ diff --git a/include/images/error.png b/include/images/error.png new file mode 100644 index 0000000..9fe8db2 Binary files /dev/null and b/include/images/error.png differ diff --git a/include/images/folder.png b/include/images/folder.png new file mode 100644 index 0000000..6833915 Binary files /dev/null and b/include/images/folder.png differ diff --git a/include/images/info.png b/include/images/info.png new file mode 100644 index 0000000..f9a3e89 Binary files /dev/null and b/include/images/info.png differ diff --git a/include/images/psd/checked.psd b/include/images/psd/checked.psd new file mode 100644 index 0000000..9056fd5 Binary files /dev/null and b/include/images/psd/checked.psd differ diff --git a/include/images/psd/configuration.psd b/include/images/psd/configuration.psd new file mode 100644 index 0000000..57226f6 Binary files /dev/null and b/include/images/psd/configuration.psd differ diff --git a/include/images/psd/error.psd b/include/images/psd/error.psd new file mode 100644 index 0000000..4ebdaec Binary files /dev/null and b/include/images/psd/error.psd differ diff --git a/include/images/psd/folder.psd b/include/images/psd/folder.psd new file mode 100644 index 0000000..6155df1 Binary files /dev/null and b/include/images/psd/folder.psd differ diff --git a/include/images/psd/info.psd b/include/images/psd/info.psd new file mode 100644 index 0000000..7c72ec0 Binary files /dev/null and b/include/images/psd/info.psd differ diff --git a/include/images/psd/unchecked.psd b/include/images/psd/unchecked.psd new file mode 100644 index 0000000..58de92a Binary files /dev/null and b/include/images/psd/unchecked.psd differ diff --git a/include/images/unchecked.png b/include/images/unchecked.png new file mode 100644 index 0000000..b6c7755 Binary files /dev/null and b/include/images/unchecked.png differ diff --git a/info.plist b/info.plist index 18fed37..e4aa4d8 100644 --- a/info.plist +++ b/info.plist @@ -16,12 +16,9 @@ modifiersubtext - - 36606F70-E12F-4D1D-81A0-3F3FFE4EA650 - destinationuid - 26447A5D-A2C6-4DD3-9467-8A7C9422E330 + CBDD35D9-C95A-4A15-BB2C-86A429FDA6CF modifiers 0 modifiersubtext @@ -56,7 +53,7 @@ escaping 50 script - tell application "Spotify" + tell application "Spotify" run script {query} end tell @@ -103,12 +100,11 @@ end tell escaping 62 keyword - spot + spotifious runningsubtext - Harnessing Spotify's terrible API... + Getting Spotifious data... script - SHOWIMAGES="no" # 'yes' or 'no' -php -f main.php -- $SHOWIMAGES "{query}" + php -f main.php -- "{query}" subtext Search for artists, tracks, or albums title @@ -127,39 +123,11 @@ php -f main.php -- $SHOWIMAGES "{query}" config - - argumenttype - 2 - keyword - spot cleanup - subtext - Clear cached artwork - text - Cleanup Spotifious - withspace - - + type - alfred.workflow.input.keyword + alfred.workflow.action.browseinalfred uid - 36606F70-E12F-4D1D-81A0-3F3FFE4EA650 - version - 0 - - - config - - escaping - 63 - script - php -f clear.php - type - 0 - - type - alfred.workflow.action.script - uid - 26447A5D-A2C6-4DD3-9467-8A7C9422E330 + CBDD35D9-C95A-4A15-BB2C-86A429FDA6CF version 0 @@ -173,21 +141,16 @@ php -f main.php -- $SHOWIMAGES "{query}" ypos 10 - 26447A5D-A2C6-4DD3-9467-8A7C9422E330 + AE6160D1-56EA-4416-991E-CF12B874FFD3 ypos - 130 + 10 - 36606F70-E12F-4D1D-81A0-3F3FFE4EA650 + CBDD35D9-C95A-4A15-BB2C-86A429FDA6CF ypos 130 - AE6160D1-56EA-4416-991E-CF12B874FFD3 - - ypos - 10 - E16D7A45-5212-4A70-AE85-36C6BF0450E7 ypos diff --git a/main.php b/main.php index 0a2bd2a..bcf616d 100644 --- a/main.php +++ b/main.php @@ -1,7 +1,11 @@ **/ +$alfred = new OhAlfred(); +$spotifious = new Spotifious(); -/* Parse the query. */ -$results = array(); -$showImages = ($argv[1] == 'yes') ? true : false; -$rawQuery = normalize($argv[2]); -$imgdResults = 6; -$maxResults = 15; - -$queryBits = str_replace("►", "", explode("►", $rawQuery)); - array_walk($queryBits, 'trim_value'); -$query = $queryBits[count($queryBits)-1]; - -if(mb_strlen($rawQuery) < 3) { - /* If the query is tiny, show the main menu. */ - - /* Get now-playing info. */ - $current = now(); - $currentTrack = $current[0]; - $currentAlbum = $current[1]; - $currentArtist = $current[2]; - $currentURL = $current[3]; - $currentStatus = ($current[4] == 'playing') ? "include/images/paused.png" : "include/images/playing.png"; - - if($showImages) { - $currentArtistArtwork = getArtistArtwork($currentArtist); // TODO use API to query artist URL? or just use plaintext from now on? - $currentAlbumArtwork = getTrackArtwork($currentURL); - } - - /* Output now-playing info. */ - $results[0][title] = "$currentTrack"; - $results[0][subtitle] = "$currentAlbum by $currentArtist"; - $results[0][arg] = "playpause"; - $results[0][icon] = $currentStatus; - - $results[1][title] = "$currentAlbum"; - $results[1][subtitle] = "More from this album..."; - $results[1][autocomplete] = "$currentAlbum"; // TODO change to albumdetail - $results[1][valid] = "no"; - $results[1][icon] = (!file_exists($currentAlbumArtwork)) ? 'include/images/album.png' : $currentAlbumArtwork; - - $results[2][title] = "$currentArtist"; - $results[2][subtitle] = "More by this artist..."; - $results[2][autocomplete] = $currentArtist; // TODO change to artistdetail - $results[2][valid] = "no"; - $results[2][icon] = (!file_exists($currentArtistArtwork)) ? 'include/images/artist.png' : $currentArtistArtwork; - - $results[3][title] = "Search for music..."; - $results[3][subtitle] = "Begin typing to search"; - $results[3][valid] = "no"; - $results[3][icon] = "include/images/search.png"; -} elseif(mb_substr($rawQuery, -1, 1) == "►") { - // If the query is an unmodified machine-generated one, generate a detail menu. - - // If the query is two levels deep, generate the detail menu of the second - // URL. Otherwise generate a detail menu based on the first (or only) URL. - - /* Do additional query-parsing. */ - $detailURL = (mb_substr($rawQuery, -2, 1) == "►") ? $queryBits[1] : $queryBits[0]; - $detailBits = explode(":", $detailURL); - $type = $detailBits[1]; - $provided = ($detailBits[1] == "artist") ? "album" : "track"; - $query = $queryBits[count($queryBits)-2]; - - /* Fetch and parse the details. */ - $json = fetch("http://ws.spotify.com/lookup/1/.json?uri=$detailURL&extras=$provided" . "detail"); - - if(empty($json)) - alfredify(array(array('title' => 'Sorry, there was an error', 'subtitle' => 'Please try again'))); // TODO better error - - $json = json_decode($json); - - /* Output the details. */ - $results[0][title] = $json->$type->name; - $results[0][subtitle] = "View $type in Spotify"; - $results[0][arg] = 'activate (open location "' . $detailURL . '")'; - - if($showImages) { - $results[0][icon] = getTrackArtwork($detailURL); - } else { - $results[0][icon] = "include/images/$type.png"; - } - - if($provided == "album") { - $currentResultNumber = 1; - $albums = array(); - foreach ($json->$type->{$provided . "s"} as $key => $value) { - if($currentResultNumber > $maxResults) - continue; - - $value = $value->$provided; - - if(in_array($value->name, $albums)) - continue; - - $currentResult[title] = $value->name; - $currentResult[subtitle] = "Open this $provided..."; - $currentResult[valid] = "no"; - $currentResult[autocomplete] = "$detailURL ► $value->href ► $query ►►"; - - if($showImages && $currentResultNumber <= $imgdResults) { - $currentResult[icon] = getTrackArtwork($value->href); - } else { - $currentResult[icon] = "include/images/album.png"; - } - - $results[] = $currentResult; - $albums[] = "$value->name"; - $currentResultNumber++; - } - } else { - $currentResultNumber = 1; - foreach ($json->$type->{$provided . "s"} as $key => $value) { - $starString = floatToStars($value->popularity); - - $currentResult[title] = "$currentResultNumber. $value->name"; - $currentResult[subtitle] = "$starString " . beautifyTime($value->length); - $currentResult[arg] = 'open location "' . $value->href . '"'; - $currentResult[icon] = "include/images/track.png"; - - $results[] = $currentResult; - $currentResultNumber++; - } - } - - -} else { - // If the query is completely user-generated, or the user has modified it, show the search menu. - - // Run the search using all three types of API queries - foreach (array('artist','album','track') as $type) { - /* Fetch and parse the search results. */ - $json = fetch("http://ws.spotify.com/search/1/$type.json?q=" . str_replace("%3A", ":", urlencode($queryBits[count($queryBits)-1]))); - - if(empty($json)) - continue; // TODO output a better error. - - $json = json_decode($json); - - /* Output the results. */ - $currentResultNumber = 1; - foreach ($json->{$type . "s"} as $key => $value) { - if($currentResultNumber > $maxResults / 3) - continue; - - /* Weight popularity. */ - $popularity = $value->popularity; - - if($type == 'artist') - $popularity += .5; - if($type == 'album') - $popularity += .15; - - /* Convert popularity to stars. */ - $starString = floatToStars($popularity); - - if($type == 'track') { - $subtitle = "$starString " . $value->album->name . " by " . $value->artists[0]->name; - $genericResultArtwork = "include/images/track.png"; - } elseif($type == 'album') { - $subtitle = "$starString Album by " . $value->artists[0]->name; - $genericResultArtwork = "include/images/album.png"; - } else { - $subtitle = "$starString " . ucfirst($type); - $genericResultArtwork = "include/images/artist.png"; - } - - $currentResult[title] = $value->name; - $currentResult[subtitle] = $subtitle; - - $currentResult[uid] = "bs-spotify-$query-$type"; - $currentResult[popularity] = $popularity; - - // `arg` is only used if item is valid, likewise `autocomplete` is - // only used if item is not valid. Tracks run an action, everything - // else autocompletes. - $currentResult[valid] = ($type == 'track') ? 'yes' : 'no'; - $currentResult[arg] = "open location \"$value->href\""; - $currentResult[autocomplete] = "$value->href ► $query ►"; - - if($showImages && $currentResultNumber <= $imgdResults / 3) { - $currentResult[icon] = getTrackArtwork($value->href); - } else { - $currentResult[icon] = $genericResultArtwork; - } - - $results[] = $currentResult; - $currentResultNumber++; - } - } - - /* Sort results by popularity. */ - if(!empty($results)) - usort($results, "popularitySort"); -} +set_exception_handler(array($alfred, 'exceptionify')); +set_error_handler(array($alfred, 'errorify'), E_ALL); -alfredify($results); +$query = $argv[1]; +$results = $spotifious->run($query); -?> \ No newline at end of file +$alfred->alfredify($results); \ No newline at end of file diff --git a/src/citelao/OhAlfred/Applescript/Applescript.php b/src/citelao/OhAlfred/Applescript/Applescript.php new file mode 100644 index 0000000..28ca8be --- /dev/null +++ b/src/citelao/OhAlfred/Applescript/Applescript.php @@ -0,0 +1,22 @@ +script = $script; + } + + public function run() { + return exec($this->script); + } +} \ No newline at end of file diff --git a/src/citelao/OhAlfred/Applescript/ApplicationApplescript.php b/src/citelao/OhAlfred/Applescript/ApplicationApplescript.php new file mode 100644 index 0000000..167c6d2 --- /dev/null +++ b/src/citelao/OhAlfred/Applescript/ApplicationApplescript.php @@ -0,0 +1,16 @@ +throwState = get_defined_vars(); + } else { + $vars = array_diff_key($vars, array_flip($this->forbidden)); // Take out all private things. + $this->throwState = $vars; + } + + parent::__construct($message); + } + + function getState() { + return $this->throwState; + } + + function setState(array $state) { + $this->throwState = $state; + return $this; + } +} \ No newline at end of file diff --git a/src/citelao/OhAlfred/HTTP/Fetcher.php b/src/citelao/OhAlfred/HTTP/Fetcher.php new file mode 100644 index 0000000..961522a --- /dev/null +++ b/src/citelao/OhAlfred/HTTP/Fetcher.php @@ -0,0 +1,32 @@ + and Robin Enhorn +class Fetcher { + protected $url; + + public function __construct($url) { + $this->url = $url; + } + + public function run() { + $ch = curl_init($this->url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $page = curl_exec($ch); + $info = curl_getinfo($ch); + curl_close($ch); + + if($info['http_code'] != '200') { + if ($info['http_code'] == '0') + throw new StatefulException("Could not access Spotify API. Try searching again"); + + throw new StatefulException("fetch() failed; error code: " . $info['http_code']); + } + + return $page; + } +} \ No newline at end of file diff --git a/src/citelao/OhAlfred/HTTP/JsonFetcher.php b/src/citelao/OhAlfred/HTTP/JsonFetcher.php new file mode 100644 index 0000000..29353cb --- /dev/null +++ b/src/citelao/OhAlfred/HTTP/JsonFetcher.php @@ -0,0 +1,27 @@ +fetcher = new Fetcher($url); + } + + public function run() { + $json = $this->fetcher->run(); + + if(empty($json)) + throw new StatefulException("No JSON returned from Spotify web search"); + + $json = json_decode($json); + + if($json == null) + throw new StatefulException("JSON error: " . json_last_error()); + + return $json; + } +} \ No newline at end of file diff --git a/src/citelao/OhAlfred/OhAlfred.php b/src/citelao/OhAlfred/OhAlfred.php new file mode 100644 index 0000000..f0c19c6 --- /dev/null +++ b/src/citelao/OhAlfred/OhAlfred.php @@ -0,0 +1,210 @@ +name == null) + $this->name = $this->defaults('bundleid'); + + return $this->name; + } + + public function home() + { + if($this->home == null) + $this->home = exec('printf "$HOME"'); + + return $this->home; + } + + public function workflow() + { + if($this->workflow == null) + $this->workflow = dirname(dirname(dirname(__DIR__))); // Because I keep OhAlfred in the src/citelao/OhAlfred directory. + // TODO make portable + + return $this->workflow; + } + + public function cache() { + if($this->cache == null) + $this->cache = $this->home() . "/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/" . $this->name() . "/"; + + if (!file_exists($this->cache)) + mkdir($this->cache); + + return $this->cache; + } + + public function storage() { + if($this->storage == null) + $this->storage = $this->home() . "/Library/Application Support/Alfred 2/Workflow Data/" . $this->name() . "/"; + + if (!file_exists($this->storage)) + mkdir($this->storage); + + return $this->storage; + } + + /** + * Both `defaults` and `options` are inspired by jdfwarrior's PHP workflow for Alfred. + * Though I cited him at the beginning of this class, the plist method of setting + * storage I pulled from his workflow. + **/ + + // Read an arbitrary plist setting. + public function plist($plist, $setting, $value = '') { + if ($value == '') { + return exec("defaults read '$plist' '$setting'"); + } + + return exec("defaults write '$plist' '$setting' '$value'"); + } + + // Read the workflow .plist file. + public function defaults($setting, $value = '') { + return $this->plist($this->workflow() . "/info", $setting, $value); + } + + // Read a custom workflow options .plist file. + public function options($setting, $value = '') { + $options = $this->storage() . "/settings"; + $optionsFile = $options . ".plist"; + + if(!file_exists($optionsFile)) + touch($optionsFile); + + return $this->plist($options, $setting, $value); + } + + public function alfredify($r = null) { + if($r == null) + $r = $this->results; + + print "\r\n"; + + foreach($r as $result) { + if(!isset($result['arg'])) + $result['arg'] = 'null'; + + if(!isset($result['title'])) + $result['title'] = 'null'; + + if(!isset($result['icon'])) + $result['icon'] = 'icon.png'; + + if(!isset($result['valid'])) + $result['valid'] = 'yes'; + + if(!isset($result['uid'])) + $result['uid'] = time() . "-" . $result['title']; + + if(!isset($result['autocomplete'])) + $result['autocomplete'] = ''; + + if(!isset($result['subtitle'])) + $result['subtitle'] = ''; + + print "\r\n\r\n"; + print " \r\n"; + print " " . $this->escapeQuery($result['title']) . "\r\n"; + print " " . $this->escapeQuery($result['subtitle']) . "\r\n"; + print " " . $this->escapeQuery($result['icon']) . "\r\n"; + print " \r\n"; + } + + print ""; + } + + public function escapeQuery($text) { + $text = str_replace("&", "&", $text); + $text = str_replace("'", "'", $text); + + return $text; + } + + public function exceptionify($error) { + // $this->errorify(0, $error->getMessage(), $error->getFile(), $error->getLine()); + } + + public function errorify($number, $message, $file, $line, $context) { + $titles = ['Aw, jeez!', 'Dagnabit!', 'Crud!', 'Whoops!', 'Oh, snap!', 'Aw, fiddlesticks!', 'Goram it!']; + + $fdir = $this->loggifyError($number, $message, $file, $line, $context); + + $results = [ + [ + 'title' => $titles[array_rand($titles)], + 'subtitle' => "Something went haywire. You can continue using Spotifious.", + 'valid' => "no", + 'icon' => 'include/images/error.png' + ], + + [ + 'title' => $message, + 'subtitle' => "Line " . $line . ", " . $file, + 'valid' => "no", + 'icon' => 'include/images/info.png' + ], + + [ + 'title' => "View log", + 'subtitle' => "Open new Finder window with .log file.", + 'icon' => 'include/images/folder.png', + 'arg' => $fdir + ] + ]; + + $this->alfredify($results); + return true; + // exit(); + } + + // TODO + protected function loggifyError($number, $message, $file, $line, $context) { + // Write contents of log file. + $fcontents = "# Error Log # \n"; + + $fcontents .= "## Error Info ## \n"; + $fcontents .= $message . "\n"; + $fcontents .= "Line " . $line . ", " . $file . "\n\n"; + + $fcontents .= "## Symbols ## \n"; // TODO rewrite + if(!is_a($error, "StatefulException") && !is_a($error, "OhAlfred\StatefulException")) { + $fcontents .= "This is not an Alfred-parsable exception. \n"; + $fcontents .= "This is a " . get_class($error); + } else { + $fcontents .= print_r($context, true) . "\n"; + } + $fcontents .= "\n\n"; + + $fcontents .= "## Stack Trace ## \n"; // tODO + // $fcontents .= print_r($error->getTrace(), true) . "\n"; + + // Delay storing of error 'till contents are fully generated. + $errordir = $this->cache(); + $fname = date('Y-m-d h-m-s') . " Spotifious.log"; + $fdir = $errordir . $fname; + + $log = fopen($fdir, "w"); + fwrite($log, $fcontents); + fclose($log); + + return $fdir; + } +} \ No newline at end of file diff --git a/src/citelao/Spotifious/Menus/Detail.php b/src/citelao/Spotifious/Menus/Detail.php new file mode 100644 index 0000000..fc462de --- /dev/null +++ b/src/citelao/Spotifious/Menus/Detail.php @@ -0,0 +1,110 @@ +search = $options['search']; + + $this->currentURI = $options['URIs'][$options['depth'] - 1]; + $explodedURI = explode(":", $this->currentURI); + $this->type = $explodedURI[1]; + $this->rawType = ($this->type == "artist") ? "album" : "track"; + + $fetcher = new JsonFetcher("http://ws.spotify.com/lookup/1/.json?uri={$this->currentURI}&extras={$this->rawType}detail"); + $json = $fetcher->run(); + + $this->title = $json->{$this->type}->name; + $this->raw = array(); + + if($this->rawType == "album") { + $albums = array(); + $this->query = implode(" ⟩", $options['args']); + + foreach ($json->artist->albums as $key => $value) { + $value = $value->album; + + if(in_array($value->name, $albums)) + continue; + + $currentResult['title'] = $value->name; + $currentResult['type'] = 'album'; + $currentResult['href'] = $value->href; + + if($this->search != '' && !mb_stristr($currentResult['title'], $this->search)) + continue; + + $this->raw[] = $currentResult; + $albums[] = $value->name; + } + } else { + foreach ($json->album->tracks as $key => $value) { + $currentResult['title'] = $value->name; + $currentResult['type'] = 'track'; + $currentResult['href'] = $value->href; + + $currentResult['number'] = $value->{'track-number'}; + $currentResult['popularity'] = $value->popularity; + $currentResult['length'] = $value->length; + + if($this->search != '' && !mb_stristr($currentResult['title'], $this->search)) + continue; + + $this->raw[] = $currentResult; + } + } + } + + public function output() { + $results = array(); + + if(!empty($this->raw)) { + foreach ($this->raw as $key => $current) { + $currentResult = array(); + if ($current['type'] == 'track') { + $currentResult['title'] = "{$current['number']}. {$current['title']}"; + $currentResult['subtitle'] = Helper::floatToBars($current['popularity'], 12); + $currentResult['arg'] = "play track \"{$current['href']}\" in context \"{$this->currentURI}\""; + $currentResult['valid'] = "yes"; + $currentResult['icon'] = "include/images/track.png"; + } else { + $currentResult['title'] = $current['title']; + $currentResult['subtitle'] = "Browse this {$current['type']}"; + $currentResult['valid'] = "no"; + $currentResult['autocomplete'] = "{$this->currentURI} ⟩ {$current['href']} ⟩ {$this->query} ⟩{$this->search}⟩"; + $currentResult['icon'] = "include/images/album.png"; + } + + $results[] = $currentResult; + } + } + + $scope['title'] = $this->title; + $scope['subtitle'] = "Browse this {$this->type} in Spotify"; + $scope['arg'] = "activate (open location \"{$this->currentURI}\")"; + $scope['icon'] = "include/images/{$this->type}.png"; + + if ($this->search == null) { + array_unshift($results, $scope); + } else { + array_push($results, $scope); + } + + return $results; + } +} \ No newline at end of file diff --git a/src/citelao/Spotifious/Menus/Helper.php b/src/citelao/Spotifious/Menus/Helper.php new file mode 100644 index 0000000..529ca3e --- /dev/null +++ b/src/citelao/Spotifious/Menus/Helper.php @@ -0,0 +1,9 @@ +now(); + $this->currentTrack = $current[0]; + $this->currentAlbum = $current[1]; + $this->currentArtist = $current[2]; + $this->currentURL = $current[3]; + $this->currentStatus = ($current[4] == 'playing') ? "include/images/paused.png" : "include/images/playing.png"; + } + + public function output() { + $results[0]['title'] = "$this->currentTrack"; + $results[0]['subtitle'] = "$this->currentAlbum by $this->currentArtist"; + $results[0]['arg'] = "playpause"; + $results[0]['icon'] = $this->currentStatus; + + $results[1]['title'] = "$this->currentAlbum"; + $results[1]['subtitle'] = "More from this album..."; + $results[1]['autocomplete'] = "$this->currentAlbum"; // TODO change to albumdetail + $results[1]['valid'] = "no"; + $results[1]['icon'] = 'include/images/album.png'; + + $results[2]['title'] = "$this->currentArtist"; + $results[2]['subtitle'] = "More by this artist..."; + $results[2]['autocomplete'] = $this->currentArtist; // TODO change to artistdetail + $results[2]['valid'] = "no"; + $results[2]['icon'] = 'include/images/artist.png'; + + $results[3]['title'] = "Search for music..."; + $results[3]['subtitle'] = "Begin typing to search"; + $results[3]['valid'] = "no"; + $results[3]['icon'] = "include/images/search.png"; + + return $results; + } + + protected function now() { + $spotQuery = new ApplicationApplescript('Spotify', 'return name of current track & "✂" & album of current track & "✂" & artist of current track & "✂" & spotify url of current track & "✂" & player state'); + + $data = $spotQuery->run(); + + return explode("✂", $data); + } +} \ No newline at end of file diff --git a/src/citelao/Spotifious/Menus/Menu.php b/src/citelao/Spotifious/Menus/Menu.php new file mode 100644 index 0000000..54f6009 --- /dev/null +++ b/src/citelao/Spotifious/Menus/Menu.php @@ -0,0 +1,7 @@ +query = $query; + + // Build the search results + // for each query type + foreach (array('artist', 'album', 'track') as $type) { + /* Fetch and parse the search results. */ + $urlQuery = str_replace("%3A", ":", urlencode($query)); + $url = "http://ws.spotify.com/search/1/$type.json?q=$urlQuery"; + + $fetcher = new JsonFetcher($url); + $json = $fetcher->run(); + + // Create the search results array + foreach ($json->{$type . "s"} as $key => $value) { + // TODO check region availability + + // Weight popularity + $popularity = $value->popularity; + + if($type == 'artist') + $popularity += .5; + + if($type == 'album') + $popularity += .15; + + if ($type == 'track') { + $currentRaw['album'] = $value->album->name; + $currentRaw['artist'] = $value->artists[0]->name; + } elseif ($type == 'album') { + $currentRaw['artist'] = $value->artists[0]->name; + } + + $currentRaw['type'] = $type; + $currentRaw['title'] = $value->name; + $currentRaw['popularity'] = $popularity; + $currentRaw['href'] = $value->href; + + $this->search[] = $currentRaw; + } + } + + if(!empty($this->search)) + usort($this->search, array($this, 'popularitySort')); + } + + public function output() { + if(!empty($this->search)) { + foreach ($this->search as $key => $current) { + $popularity = Helper::floatToBars($current['popularity']); + + if ($current['type'] == 'track') { + $subtitle = "$popularity {$current['album']} by {$current['artist']}"; + } elseif ($current['type'] == 'album') { + $subtitle = "$popularity Album by {$current['artist']}"; + } else { + $subtitle = "$popularity " . ucfirst($current['type']); + } + + if ($current['type'] == 'track') { + $valid = 'yes'; + $arg = "play track \"{$current['href']}\""; + $autocomplete = ''; + } else { + $valid = 'no'; + $arg = ''; + $autocomplete = "{$current['href']} ⟩ {$this->query} ⟩"; + } + + $currentResult['title'] = $current['title']; + $currentResult['subtitle'] = $subtitle; + $currentResult['uid'] = "bs-spotify-{$this->query}-{$current['type']}-{$current['title']}"; + $currentResult['valid'] = $valid; + $currentResult['arg'] = $arg; + $currentResult['autocomplete'] = $autocomplete; + $currentResult['icon'] = "include/images/{$current['type']}.png"; + + $results[] = $currentResult; + } + } + + /* Give the option to continue searching in Spotify because even I know my limits. */ + $results[] = [ + 'title' => "Search for {$this->query}", + 'subtitle' => "Continue this search in Spotify…", + 'uid' => "bs-spotify-{$this->query}-more", + 'arg' => "activate (open location \"spotify:search:{$this->query}\")", + 'icon' => 'include/images/search.png' + ]; + + return $results; + } + + protected function popularitySort($a, $b) { + if($a['popularity'] == $b['popularity']) + return 0; + + return ($a['popularity'] < $b['popularity']) ? 1 : -1; + } +} \ No newline at end of file diff --git a/src/citelao/Spotifious/Spotifious.php b/src/citelao/Spotifious/Spotifious.php new file mode 100644 index 0000000..4a711b0 --- /dev/null +++ b/src/citelao/Spotifious/Spotifious.php @@ -0,0 +1,79 @@ +output(); + + } elseif ($this->contains($query, '⟩')) { + // if the query contains any machine-generated text + // (the unicode `⟩` is untypeable so we check for it) + // we need to parse the query and extract the URLs. + + // So split based on the delimeter `⟩` and excise the delimeter and blanks. + $splitQuery = array_filter(str_replace("⟩", "", explode("⟩", $query))); + array_walk($splitQuery, array($this, 'trim_value')); + + $URIs = array_filter($splitQuery, array($this, 'is_spotify_uri')); + $args = array_diff($splitQuery, $URIs); + + // Find which URI to use (by count, not by array index). + // Arrows should be twice the number of URIs for the last URI. + // For every one arrow fewer, traverse one URI backwards. + $arrows = mb_substr_count($query, "⟩"); + $depth = count($URIs) - (2 * count($URIs) - $arrows); // equiv to $arrows - count($URIs). + + $options = array( + 'depth' => $depth, + 'URIs' => $URIs, + 'args' => $args, + 'search' => '' + ); + + if (mb_substr($query, -1) == "⟩") { // Machine-generated + $menu = new Detail($options); + return $menu->output(); + + } elseif($depth > 0) { + $search = array_pop($args); + $options['search'] = $search; + $options['args'] = $args; + + $menu = new Detail($options); + return $menu->output(); + + } else { + $menu = new Search(end($args)); + return $menu->output(); + } + + } else { + $menu = new Search($query); + return $menu->output(); + + } + } + + protected function contains($stack, $needle) { + return (strpos($stack, $needle) !== false); + } + + protected function trim_value(&$value) { + $value = trim($value); + } + + protected function is_spotify_uri($item) { + $regex = '/^(spotify:(?:album|artist|track|user:[^:]+:playlist):[a-zA-Z0-9]+)$/x'; + + return preg_match($regex, $item); + } +} \ No newline at end of file diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..6c35dd2 --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0 class loader + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-0.md + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + + public function getPrefixes() + { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-0 base directories + * @param bool $prepend Whether to prepend the directories + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + */ + public function setPsr4($prefix, $paths) { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + include $file; + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 + if ('\\' == $class[0]) { + $class = substr($class, 1); + } + + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php'; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { + if (0 === strpos($class, $prefix)) { + foreach ($this->prefixDirsPsr4[$prefix] as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . '.php'; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + // Remember that this class does not exist. + return $this->classMap[$class] = false; + } +} diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..7a91153 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,9 @@ + array($baseDir . '/src/citelao/Spotifious'), + 'OhAlfred\\' => array($baseDir . '/src/citelao/OhAlfred'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..a9fbc91 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,48 @@ + $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + + $loader->register(true); + + return $loader; + } +}