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;
+ }
+}