├── include ├── terminal-notifier.app │ ├── Icon │ └── Contents │ │ ├── PkgInfo │ │ ├── MacOS │ │ └── terminal-notifier │ │ ├── Resources │ │ ├── Spotify.icns │ │ └── en.lproj │ │ │ ├── MainMenu.nib │ │ │ ├── InfoPlist.strings │ │ │ └── Credits.rtf │ │ └── Info.plist ├── normalize ├── images │ ├── dash.png │ ├── info.png │ ├── album.png │ ├── artist.png │ ├── error.png │ ├── folder.png │ ├── paused.png │ ├── search.png │ ├── single.png │ ├── track.png │ ├── checked.png │ ├── disabled.png │ ├── playing.png │ ├── playlist.png │ ├── psd │ │ ├── album.psd │ │ ├── dash.psd │ │ ├── error.psd │ │ ├── info.psd │ │ ├── track.psd │ │ ├── artist.psd │ │ ├── checked.psd │ │ ├── folder.psd │ │ ├── paused.psd │ │ ├── playing.psd │ │ ├── search.psd │ │ ├── disabled.psd │ │ ├── playlist.psd │ │ ├── unchecked.psd │ │ ├── remote │ │ │ ├── next.psd │ │ │ ├── previous.psd │ │ │ ├── search.psd │ │ │ ├── playpause.psd │ │ │ ├── volup.svg │ │ │ └── voldown.svg │ │ ├── configuration.psd │ │ └── controls │ │ │ ├── next.psd │ │ │ ├── previous.psd │ │ │ └── playpause.psd │ ├── unchecked.png │ ├── compilation.png │ ├── remote │ │ ├── next.png │ │ ├── search.png │ │ ├── voldown.png │ │ ├── volup.png │ │ ├── playpause.png │ │ └── previous.png │ ├── commands │ │ ├── next.png │ │ ├── playpause.png │ │ └── previous.png │ └── configuration.png ├── setup │ ├── img │ │ ├── save.png │ │ ├── redirect.png │ │ ├── client_info.png │ │ ├── create_an_app_name.png │ │ └── create_an_app_button.png │ ├── link.php │ ├── client_submit.php │ ├── style │ │ ├── style.css │ │ └── normalize.css │ └── index.php └── screenshots │ ├── controls.png │ ├── hero_shot.png │ ├── install.png │ ├── install.psd │ ├── loading.png │ ├── main_menu.png │ ├── settings.png │ ├── compilation.png │ ├── compilation.psd │ └── compilation_small.png ├── icon.png ├── spotifious.sublime-project ├── .gitignore ├── dist └── Spotifious.alfredworkflow ├── .editorconfig ├── _remote ├── images │ ├── items │ │ ├── 3277740D-DEEE-4C08-8B1D-E7B962742990.png │ │ ├── 3A89A720-DC62-42BE-9FB6-2BFEA717BCE9.png │ │ ├── 5A4E8FCB-5384-41CC-8B7C-97A263163633.png │ │ ├── 9AF9D5D1-FA74-4419-9B9C-C075BEC1AD75.png │ │ ├── B476C144-0CAF-4BC7-8609-BD97EBFCCF3F.png │ │ └── D113E787-2890-471A-B3E8-126F562439DC.png │ └── pages │ │ └── 396A6C7D-DEF5-4580-8F15-927EFB5D6BFC.png └── pages │ ├── pages.data │ └── BF8D740B-C9D0-4622-8695-FC0CCA97B37F.data ├── src └── citelao │ ├── Spotifious │ ├── Menus │ │ ├── Menu.php │ │ ├── Helper.php │ │ ├── Detail.php │ │ ├── Control.php │ │ ├── SetupCountryCode.php │ │ ├── DetailArtist.php │ │ ├── DetailAlbum.php │ │ ├── Settings.php │ │ ├── DetailPlaylist.php │ │ ├── Setup.php │ │ ├── Main.php │ │ └── Search.php │ ├── Actions │ │ ├── IAction.php │ │ └── Applescript.php │ └── Spotifious.php │ └── OhAlfred │ ├── Exceptions │ ├── IStatefulException.php │ └── StatefulException.php │ ├── HTTP │ ├── JsonParser.php │ ├── JsonFetcher.php │ └── Fetcher.php │ ├── Command │ ├── Command.php │ └── Timeout.php │ ├── Applescript │ ├── Applescript.php │ └── ApplicationApplescript.php │ └── OhAlfred.php ├── composer.json ├── script ├── travis_install.sh └── check_version.php ├── action.php ├── main.php ├── Makefile ├── .travis.yml ├── callback.php ├── LICENSE.md ├── composer.lock ├── doc └── spec.md ├── CHANGELOG.md ├── README.md └── info.plist /include/terminal-notifier.app/Icon : -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /include/terminal-notifier.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/icon.png -------------------------------------------------------------------------------- /include/normalize: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/normalize -------------------------------------------------------------------------------- /spotifious.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": "." 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /include/images/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/dash.png -------------------------------------------------------------------------------- /include/images/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/info.png -------------------------------------------------------------------------------- /include/images/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/album.png -------------------------------------------------------------------------------- /include/images/artist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/artist.png -------------------------------------------------------------------------------- /include/images/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/error.png -------------------------------------------------------------------------------- /include/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/folder.png -------------------------------------------------------------------------------- /include/images/paused.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/paused.png -------------------------------------------------------------------------------- /include/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/search.png -------------------------------------------------------------------------------- /include/images/single.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/single.png -------------------------------------------------------------------------------- /include/images/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/track.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # my private data 2 | *.sublime-workspace 3 | 4 | # build cruft 5 | build/ 6 | 7 | # vendor 8 | vendor/ -------------------------------------------------------------------------------- /include/images/checked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/checked.png -------------------------------------------------------------------------------- /include/images/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/disabled.png -------------------------------------------------------------------------------- /include/images/playing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/playing.png -------------------------------------------------------------------------------- /include/images/playlist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/playlist.png -------------------------------------------------------------------------------- /include/images/psd/album.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/album.psd -------------------------------------------------------------------------------- /include/images/psd/dash.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/dash.psd -------------------------------------------------------------------------------- /include/images/psd/error.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/error.psd -------------------------------------------------------------------------------- /include/images/psd/info.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/info.psd -------------------------------------------------------------------------------- /include/images/psd/track.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/track.psd -------------------------------------------------------------------------------- /include/images/unchecked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/unchecked.png -------------------------------------------------------------------------------- /include/setup/img/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/setup/img/save.png -------------------------------------------------------------------------------- /dist/Spotifious.alfredworkflow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/dist/Spotifious.alfredworkflow -------------------------------------------------------------------------------- /include/images/compilation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/compilation.png -------------------------------------------------------------------------------- /include/images/psd/artist.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/artist.psd -------------------------------------------------------------------------------- /include/images/psd/checked.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/checked.psd -------------------------------------------------------------------------------- /include/images/psd/folder.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/folder.psd -------------------------------------------------------------------------------- /include/images/psd/paused.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/paused.psd -------------------------------------------------------------------------------- /include/images/psd/playing.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/playing.psd -------------------------------------------------------------------------------- /include/images/psd/search.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/search.psd -------------------------------------------------------------------------------- /include/images/remote/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/next.png -------------------------------------------------------------------------------- /include/setup/img/redirect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/setup/img/redirect.png -------------------------------------------------------------------------------- /include/images/commands/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/commands/next.png -------------------------------------------------------------------------------- /include/images/configuration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/configuration.png -------------------------------------------------------------------------------- /include/images/psd/disabled.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/disabled.psd -------------------------------------------------------------------------------- /include/images/psd/playlist.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/playlist.psd -------------------------------------------------------------------------------- /include/images/psd/unchecked.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/unchecked.psd -------------------------------------------------------------------------------- /include/images/remote/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/search.png -------------------------------------------------------------------------------- /include/images/remote/voldown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/voldown.png -------------------------------------------------------------------------------- /include/images/remote/volup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/volup.png -------------------------------------------------------------------------------- /include/screenshots/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/controls.png -------------------------------------------------------------------------------- /include/screenshots/hero_shot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/hero_shot.png -------------------------------------------------------------------------------- /include/screenshots/install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/install.png -------------------------------------------------------------------------------- /include/screenshots/install.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/install.psd -------------------------------------------------------------------------------- /include/screenshots/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/loading.png -------------------------------------------------------------------------------- /include/screenshots/main_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/main_menu.png -------------------------------------------------------------------------------- /include/screenshots/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/settings.png -------------------------------------------------------------------------------- /include/setup/img/client_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/setup/img/client_info.png -------------------------------------------------------------------------------- /include/images/psd/remote/next.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/remote/next.psd -------------------------------------------------------------------------------- /include/images/remote/playpause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/playpause.png -------------------------------------------------------------------------------- /include/images/remote/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/remote/previous.png -------------------------------------------------------------------------------- /include/screenshots/compilation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/compilation.png -------------------------------------------------------------------------------- /include/screenshots/compilation.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/compilation.psd -------------------------------------------------------------------------------- /include/images/commands/playpause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/commands/playpause.png -------------------------------------------------------------------------------- /include/images/commands/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/commands/previous.png -------------------------------------------------------------------------------- /include/images/psd/configuration.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/configuration.psd -------------------------------------------------------------------------------- /include/images/psd/controls/next.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/controls/next.psd -------------------------------------------------------------------------------- /include/images/psd/remote/previous.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/remote/previous.psd -------------------------------------------------------------------------------- /include/images/psd/remote/search.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/remote/search.psd -------------------------------------------------------------------------------- /include/images/psd/controls/previous.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/controls/previous.psd -------------------------------------------------------------------------------- /include/images/psd/remote/playpause.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/remote/playpause.psd -------------------------------------------------------------------------------- /include/setup/img/create_an_app_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/setup/img/create_an_app_name.png -------------------------------------------------------------------------------- /include/images/psd/controls/playpause.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/images/psd/controls/playpause.psd -------------------------------------------------------------------------------- /include/screenshots/compilation_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/screenshots/compilation_small.png -------------------------------------------------------------------------------- /include/setup/img/create_an_app_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/setup/img/create_an_app_button.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | [*.php] 6 | indent_style = tab 7 | 8 | [Makefile] 9 | indent_style = tab -------------------------------------------------------------------------------- /_remote/images/items/3277740D-DEEE-4C08-8B1D-E7B962742990.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/3277740D-DEEE-4C08-8B1D-E7B962742990.png -------------------------------------------------------------------------------- /_remote/images/items/3A89A720-DC62-42BE-9FB6-2BFEA717BCE9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/3A89A720-DC62-42BE-9FB6-2BFEA717BCE9.png -------------------------------------------------------------------------------- /_remote/images/items/5A4E8FCB-5384-41CC-8B7C-97A263163633.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/5A4E8FCB-5384-41CC-8B7C-97A263163633.png -------------------------------------------------------------------------------- /_remote/images/items/9AF9D5D1-FA74-4419-9B9C-C075BEC1AD75.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/9AF9D5D1-FA74-4419-9B9C-C075BEC1AD75.png -------------------------------------------------------------------------------- /_remote/images/items/B476C144-0CAF-4BC7-8609-BD97EBFCCF3F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/B476C144-0CAF-4BC7-8609-BD97EBFCCF3F.png -------------------------------------------------------------------------------- /_remote/images/items/D113E787-2890-471A-B3E8-126F562439DC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/items/D113E787-2890-471A-B3E8-126F562439DC.png -------------------------------------------------------------------------------- /_remote/images/pages/396A6C7D-DEF5-4580-8F15-927EFB5D6BFC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/_remote/images/pages/396A6C7D-DEF5-4580-8F15-927EFB5D6BFC.png -------------------------------------------------------------------------------- /include/terminal-notifier.app/Contents/MacOS/terminal-notifier: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/terminal-notifier.app/Contents/MacOS/terminal-notifier -------------------------------------------------------------------------------- /include/terminal-notifier.app/Contents/Resources/Spotify.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/citelao/Spotify-for-Alfred/HEAD/include/terminal-notifier.app/Contents/Resources/Spotify.icns -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Menu.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | pages 6 | 7 | BF8D740B-C9D0-4622-8695-FC0CCA97B37F 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/citelao/OhAlfred/HTTP/JsonParser.php: -------------------------------------------------------------------------------- 1 | $json)); 13 | 14 | return $json; 15 | } 16 | } -------------------------------------------------------------------------------- /src/citelao/OhAlfred/Command/Command.php: -------------------------------------------------------------------------------- 1 | script = $command; 10 | } 11 | 12 | public function run() { 13 | $unused; 14 | return exec($this->script, $unused, $this->return_status); 15 | } 16 | 17 | public function status() { 18 | return $this->return_status; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/citelao/OhAlfred/Applescript/Applescript.php: -------------------------------------------------------------------------------- 1 | script = $script; 17 | } 18 | 19 | public function run() { 20 | return exec($this->script); 21 | } 22 | } -------------------------------------------------------------------------------- /src/citelao/OhAlfred/Applescript/ApplicationApplescript.php: -------------------------------------------------------------------------------- 1 | /dev/null 2>/dev/null &"; 16 | 17 | echo($script); 18 | 19 | $this->script = $script; 20 | } 21 | 22 | public function run() { 23 | return exec($this->script); 24 | } 25 | } -------------------------------------------------------------------------------- /include/images/psd/remote/volup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /include/images/psd/remote/voldown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/citelao/OhAlfred/HTTP/JsonFetcher.php: -------------------------------------------------------------------------------- 1 | url = $url; 14 | $this->fetcher = new Fetcher($url); 15 | } 16 | 17 | public function run() { 18 | $json = $this->fetcher->run(); 19 | 20 | if(empty($json)) 21 | throw new StatefulException("No JSON returned from " . $this->url); 22 | 23 | return JsonParser::parse($json); 24 | } 25 | } -------------------------------------------------------------------------------- /include/terminal-notifier.app/Contents/Resources/en.lproj/Credits.rtf: -------------------------------------------------------------------------------- 1 | {\rtf0\ansi{\fonttbl\f0\fswiss Helvetica;} 2 | {\colortbl;\red255\green255\blue255;} 3 | \paperw9840\paperh8400 4 | \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural 5 | 6 | \f0\b\fs24 \cf0 Engineering: 7 | \b0 \ 8 | Some people\ 9 | \ 10 | 11 | \b Human Interface Design: 12 | \b0 \ 13 | Some other people\ 14 | \ 15 | 16 | \b Testing: 17 | \b0 \ 18 | Hopefully not nobody\ 19 | \ 20 | 21 | \b Documentation: 22 | \b0 \ 23 | Whoever\ 24 | \ 25 | 26 | \b With special thanks to: 27 | \b0 \ 28 | Mom\ 29 | } 30 | -------------------------------------------------------------------------------- /script/travis_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://docs.travis-ci.com/user/multi-os/ 4 | if [[ $TRAVIS_OS_NAME == 'osx' ]]; then 5 | # update & tap PHP! 6 | brew update 7 | brew tap homebrew/php 8 | 9 | case "${PHPENV}" in 10 | php55) 11 | brew install php55 12 | ;; 13 | php56) 14 | brew install php56 15 | ;; 16 | php70) 17 | brew install php70 18 | ;; 19 | esac 20 | 21 | # install Composer 22 | # https://github.com/phpmyadmin/phpmyadmin/commit/9ecda4175b6c19c781cf23254da151bfa15eb81a#diff-354f30a63fb0907d4ad57269548329e3 23 | curl https://getcomposer.org/installer | php 24 | ln -s $PWD/composer.phar /usr/local/bin/composer 25 | fi 26 | -------------------------------------------------------------------------------- /include/setup/link.php: -------------------------------------------------------------------------------- 1 | options('spotify_client_id'), $alfred->options('spotify_secret'), 'http://localhost:11114/callback.php'); 11 | 12 | $scopes = explode(', ', $alfred->options('desired_scopes')); 13 | 14 | $authorizeUrl = $session->getAuthorizeUrl(array( 15 | 'scope' => $scopes 16 | )); 17 | 18 | header('Location: ' . $authorizeUrl); 19 | die(); -------------------------------------------------------------------------------- /action.php: -------------------------------------------------------------------------------- 1 | process($action); 17 | 18 | // For debugging 19 | // print_r($action); 20 | // print("\n"); 21 | 22 | print_r($results); 23 | } catch(Exception $e) { 24 | $alfred->exceptionify($e, false); 25 | throw $e; 26 | } -------------------------------------------------------------------------------- /main.php: -------------------------------------------------------------------------------- 1 | 14 | * an Alfred extension by Ben Stolovitz 15 | **/ 16 | 17 | $alfred = new OhAlfred(); 18 | 19 | set_exception_handler(array($alfred, 'exceptionify')); 20 | try { 21 | $spotifious = new Spotifious($alfred); 22 | 23 | $query = $argv[1]; 24 | $results = $spotifious->run($query); 25 | 26 | $alfred->alfredify($results); 27 | } catch(Exception $e) { 28 | $alfred->exceptionify($e); 29 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Actions/Applescript.php: -------------------------------------------------------------------------------- 1 | command)) { 15 | throw new StatefulException("You have to have a command to run!"); 16 | } 17 | 18 | if(isset($options->application)) { 19 | $this->action = new ApplicationApplescript($options->application, $options->command); 20 | } else { 21 | $this->action = new ApplescriptHandler($options->command); 22 | } 23 | 24 | } 25 | 26 | public function run() { 27 | $this->action->run(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/citelao/OhAlfred/Exceptions/StatefulException.php: -------------------------------------------------------------------------------- 1 | throwState = array_diff_key($all_vars, array_flip($this->forbidden)); // Take out all private things. 15 | 16 | parent::__construct($message); 17 | } 18 | 19 | function getState() { 20 | return $this->throwState; 21 | } 22 | 23 | function setState(array $state) { 24 | $this->throwState = $state; 25 | return $this; 26 | } 27 | } -------------------------------------------------------------------------------- /src/citelao/OhAlfred/HTTP/Fetcher.php: -------------------------------------------------------------------------------- 1 | and Robin Enhorn 7 | class Fetcher { 8 | protected $url; 9 | 10 | public function __construct($url) { 11 | $this->url = $url; 12 | } 13 | 14 | public function run() { 15 | $ch = curl_init($this->url); 16 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 17 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 18 | curl_setopt($ch, CURLOPT_TIMEOUT, 5); 19 | $page = curl_exec($ch); 20 | $info = curl_getinfo($ch); 21 | curl_close($ch); 22 | 23 | if($info['http_code'] != '200') { 24 | if ($info['http_code'] == '0') { 25 | throw new StatefulException("Could not access $this->url. Try searching again"); 26 | } 27 | 28 | throw new StatefulException("fetch() failed; error code: " . $info['http_code']); 29 | } 30 | 31 | return $page; 32 | } 33 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Helper.php: -------------------------------------------------------------------------------- 1 | diff($ago); 15 | 16 | $diff->w = floor($diff->d / 7); 17 | $diff->d -= $diff->w * 7; 18 | 19 | $string = array( 20 | 'y' => 'year', 21 | 'm' => 'month', 22 | 'w' => 'week', 23 | 'd' => 'day', 24 | 'h' => 'hour', 25 | 'i' => 'minute', 26 | 's' => 'second', 27 | ); 28 | foreach ($string as $k => &$v) { 29 | if ($diff->$k) { 30 | $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); 31 | } else { 32 | unset($string[$k]); 33 | } 34 | } 35 | 36 | if (!$full) $string = array_slice($string, 0, 1); 37 | return $string ? implode(', ', $string) . ' ago' : 'just now'; 38 | } 39 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | all: clean build 3 | 4 | .PHONY: help 5 | help: 6 | @echo "Make Spotifious!" 7 | @echo "" 8 | @echo "Available commands:" 9 | @$(MAKE) -f $(lastword $(MAKEFILE_LIST)) list 10 | 11 | # https://stackoverflow.com/questions/4219255/how-do-you-get-the-list-of-targets-in-a-makefile 12 | .PHONY: list 13 | list: 14 | @$(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' 15 | 16 | .PHONY: clean 17 | clean: 18 | -rm -r dist/ 19 | -rm -r build/ 20 | 21 | .PHONY: init 22 | init: vendor 23 | 24 | # https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile 25 | COMPOSER_INSTALLED := $(shell command -v composer 2> /dev/null) 26 | vendor: 27 | ifndef COMPOSER_INSTALLED 28 | $(error Please install composer globally. https://getcomposer.org/download/) 29 | endif 30 | composer install 31 | 32 | .PHONY: build.intermediates 33 | build.intermediates: 34 | mkdir build/ 35 | @echo "TODO BUILD IN BUILD DIRECTORY" 36 | 37 | .PHONY: build.images 38 | build.images: 39 | @echo "TODO COMPILE IMAGES" 40 | 41 | .PHONY: build 42 | build: init check_version build.intermediates build.images 43 | mkdir dist/ 44 | zip -q -x '*.git*' '*include/images/psd*' '*include/screenshots*' '*.psd' -r dist/Spotifious.alfredworkflow ./ 45 | 46 | .PHONY: check_version 47 | check_version: 48 | php script/check_version.php 49 | -------------------------------------------------------------------------------- /script/check_version.php: -------------------------------------------------------------------------------- 1 | workflow() . "CHANGELOG.md"; 14 | $changelog = file_get_contents($changelog_filename); 15 | $count = preg_match('/##\W*v?([\d.]*)/', $changelog, $matches); 16 | $changelog_version = $matches[1]; 17 | 18 | // Check `info.plist` 19 | $plist_version = $alfred->plist($alfred->workflow() . "info", "version"); 20 | 21 | // Check README 22 | $readme_filename = $alfred->workflow() . "README.md"; 23 | $readme = file_get_contents($readme_filename); 24 | $count = preg_match_all('/(\d+(?:\.\d+){2})/', $readme, $matches); 25 | $potential_readme_version = $matches[0]; 26 | 27 | // Compare results 28 | if($changelog_version != $plist_version || !in_array($changelog_version, $potential_readme_version)) { 29 | fwrite(STDERR, "Version mismatch! One or more versions are not in sync:\n"); 30 | fwrite(STDERR, "\t" . $changelog_version . " - CHANGELOG (expected version)\n"); 31 | fwrite(STDERR, "\t" . $plist_version . " - plist latest version\n"); 32 | foreach ($potential_readme_version as $ver) { 33 | fwrite(STDERR, "\t" . $ver . " - potential README version\n"); 34 | } 35 | exit(1); 36 | } 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | # Travis and OSX aren't friends: 3 | # https://github.com/orls/php-spx/blob/80bcf8252502380de73ab271feb6840db29b4ab7/.travis.yml 4 | matrix: 5 | include: 6 | # `php: ..` in here is just descriptive/for UI, they don't affect installed ver. 7 | - os: osx 8 | php: "5.5" 9 | language: generic 10 | env: PHPENV=php55 11 | - os: osx 12 | php: "5.6" 13 | language: generic 14 | env: PHPENV=php56 15 | - os: osx 16 | php: "7.0" 17 | language: generic 18 | env: PHPENV=php70 19 | before_install: 20 | - sh ./script/travis_install.sh 21 | install: make clean init 22 | script: make build 23 | deploy: 24 | provider: releases 25 | api_key: 26 | secure: jeJ1Dw4sMhttDclMPJ7bzWgVhn8NPF8QdGS2b85RICTU8+Z6rDsN9xwwhzbfyHVT16cmlB6GSPkX8Vhxw6BM8IiFFxN07S3uK6wNvzunUKkZqAh/o3hvpniRmmOmmTZfOCFhEZRuyYeng+NnNaHAU3bvH3tnRMuJUq9tKUqusw7Vbp7driQu2J5hvr7rgIu/mw+4SI16q6jshx6Vsc4owAyFGjt3vtTf+jke8aEqZOiweeRYJY7lDTf3jlSHiqeOOZm204M6v3FY32TIOIX3NQEFqjmAyGcSBXN89n31ufLAKOageTZOEqdffiPXgYfO8YAT5n/FupFcW6MaAH55PQUHVWhccYsRL80ix7h/xQ+PUFXS55AZR4B1AGVzkTZhrn5eJTtMyP+JevKX3SgjmK0uxfV4ckEEPSNPHIHpImP6gkUts3JlcxKhWH3htN7dh8XEfhQ4fK3zvhxVk/syS8QQ5eXOHrhkcjsnmx0BhlNyjLEeO0bxa0AvQOltEfsrNbicEr9z5i+GcI98HJeVWWUE4ZncLKK8wQffs3ONtrZVcCcLNhvIJrhjg27/Jr953DMi1SexgFyrDax0gOAmjwU3J4XhmNSKgNNxZlpZE5OcVSO5wKz7aZUTbOSfp6JcfpsU/uX+P+348Ve2X68gQWvrZPU/Wwuu7JB/w43+aeI= 27 | file: dist/Spotifious.alfredworkflow 28 | skip_cleanup: true 29 | on: 30 | branch: master 31 | tags: true 32 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Detail.php: -------------------------------------------------------------------------------- 1 | search = $options['search']; 15 | 16 | $this->currentURI = $options['URIs'][$options['depth'] - 1]; 17 | $explodedURI = explode(":", $this->currentURI); 18 | $this->type = $explodedURI[count($explodedURI) - 2]; 19 | 20 | $constructedOptions = array( 21 | 'currentURI' => $this->currentURI, 22 | 'search' => $options['search'], 23 | 'id' => $explodedURI[count($explodedURI) - 1], 24 | 'originalQuery' => $options['query'], 25 | 'query' => implode(" ⟩", $options['args']) 26 | ); 27 | 28 | if($this->type == "artist") { 29 | $this->submenu = new DetailArtist($constructedOptions, $alfred, $api); 30 | } else if($this->type == "album") { 31 | $this->submenu = new DetailAlbum($constructedOptions, $alfred, $api); 32 | } else if($this->type == "playlist") { 33 | $this->submenu = new DetailPlaylist($constructedOptions, $alfred, $api); 34 | } else { 35 | throw new StatefulException("Unknown detail type: '${$this->type}'"); 36 | 37 | } 38 | } 39 | 40 | public function output() { 41 | return $this->submenu->output(); 42 | } 43 | 44 | // protected function contains($stack, $needle) { 45 | // return (strpos($stack, $needle) !== false); 46 | // } 47 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Control.php: -------------------------------------------------------------------------------- 1 | 'Next track', 15 | 'keys' => 'skip', 16 | 'icon' => 'include/images/commands/next.png', 17 | 'action' => 'next⟩returnControls' 18 | ), 19 | 20 | array( 21 | 'name' => 'Previous track', 22 | 'keys' => 'back', 23 | 'icon' => 'include/images/commands/previous.png', 24 | 'action' => 'previous⟩returnControls' 25 | ), 26 | 27 | array( 28 | 'name' => 'Play/pause', 29 | 'keys' => 'playpause', 30 | 'icon' => 'include/images/commands/playpause.png', 31 | 'action' => 'playpause⟩returnControls' 32 | ) 33 | ); 34 | 35 | public function __construct($query, $alfred=null, $api=null) { 36 | $this->query = $query; 37 | $this->search = mb_substr($query, 1); 38 | } 39 | 40 | public function output() { 41 | $results = array(); 42 | foreach ($this->commands as $command) { 43 | if($this->search != null && 44 | !@mb_stristr($command['name'] . ' ' . $command['keys'], $this->search)) 45 | continue; 46 | 47 | $results[] = array( 48 | 'title' => $command['name'], 49 | 'arg' => $command['action'], 50 | 'icon' => array('path' => $command['icon']) 51 | ); 52 | } 53 | 54 | $results[] = array( 55 | 'title' => 'Controls', 56 | 'subtitle' => 'Access this menu at any time by typing `c`', 57 | 'icon' => array('path' => 'include/images/info.png'), 58 | 'valid' => false, 59 | 'autocomplete' => $this->query 60 | ); 61 | 62 | return $results; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /callback.php: -------------------------------------------------------------------------------- 1 | options('spotify_client_id'), $alfred->options('spotify_secret'), 'http://localhost:11114/callback.php'); 11 | 12 | // Request a access token using the code from Spotify 13 | $success = !array_key_exists('error', $_GET); 14 | 15 | if($success) { 16 | $session->requestAccessToken($_GET['code']); 17 | 18 | // Save the tokens 19 | $alfred->options("spotify_access_token", $session->getAccessToken()); 20 | $alfred->options("spotify_refresh_token", $session->getRefreshToken()); 21 | $alfred->options("spotify_access_token_expires", $session->getTokenExpiration()); 22 | 23 | $alfred->options('registered_scopes', $alfred->options('desired_scopes')); 24 | } 25 | 26 | ?> 27 | 28 | 29 | Spotifious Setup 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 |
39 |

Spotifious should be setup :)

40 |

41 | You should be able to start using Spotifious now! 42 |

43 | 44 |

(enjoy)

45 |
46 | 47 |
48 |

Spotifious didn't setup correctly.

49 |

50 | Error: 51 |

52 | 53 |

You'll need to try logging in again.

54 |
55 | 56 |
57 | 58 | -------------------------------------------------------------------------------- /include/terminal-notifier.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 13F34 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | terminal-notifier 11 | CFBundleIconFile 12 | Spotify 13 | CFBundleIdentifier 14 | nl.superalloy.oss.terminal-notifier.citelao-icon 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | terminal-notifier 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 1.6.1 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | 12 27 | DTCompiler 28 | com.apple.compilers.llvm.clang.1_0 29 | DTPlatformBuild 30 | 6A317 31 | DTPlatformVersion 32 | GM 33 | DTSDKBuild 34 | 13F26 35 | DTSDKName 36 | macosx10.9 37 | DTXcode 38 | 0600 39 | DTXcodeBuild 40 | 6A317 41 | LSMinimumSystemVersion 42 | 10.8 43 | LSUIElement 44 | 45 | NSHumanReadableCopyright 46 | Copyright © 2012 Eloy Durán. All rights reserved. 47 | NSMainNibFile 48 | MainMenu 49 | NSPrincipalClass 50 | NSApplication 51 | 52 | 53 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## Main code 4 | 5 | You may do what you want with this program and its source subject to these restrictions: 6 | 7 | - You MUST reference me (Ben Stolovitz) as the author of the original version 8 | - You MUST provide a link to this original version 9 | - You MUST respect the licenses for the software below 10 | - You MUST NOT misrepresent this software as yours. 11 | 12 | Furthermore, I am not responsible or liable for any problems that might occur from you using this software. I provide no warranty or guarantee on this software, and I am not liable for any damages, claims, or liabilities of *any* sort that might arise from this software in any way. 13 | 14 | ## Entypo 15 | 16 | Entypo pictograms by Daniel Bruce — www.entypo.com 17 | 18 | I created album.png, if you were wondering. The other images are Entypo's. 19 | 20 | ## Terminal Notifier 21 | 22 | All the works are available under the MIT license. **Except** for 23 | ‘Terminal.icns’, which is a copy of Apple’s Terminal.app icon and as such is 24 | copyright of Apple. 25 | 26 | Copyright (C) 2012-2013 Eloy Durán 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy of 29 | this software and associated documentation files (the "Software"), to deal in 30 | the Software without restriction, including without limitation the rights to 31 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 32 | of the Software, and to permit persons to whom the Software is furnished to do 33 | so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in all 36 | copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 44 | SOFTWARE. -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "ce7c888d471b1dcfc2d10f288bbd23ee", 8 | "packages": [ 9 | { 10 | "name": "jwilsson/spotify-web-api-php", 11 | "version": "1.12.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/jwilsson/spotify-web-api-php.git", 15 | "reference": "ecda858d53b2d4b04a44f4f1095ab0a6a3161a1c" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/jwilsson/spotify-web-api-php/zipball/ecda858d53b2d4b04a44f4f1095ab0a6a3161a1c", 20 | "reference": "ecda858d53b2d4b04a44f4f1095ab0a6a3161a1c", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-curl": "*", 25 | "php": "^5.5 || ^7.0" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^4.8 || ^5.7", 29 | "satooshi/php-coveralls": "dev-master", 30 | "squizlabs/php_codesniffer": "^3.0" 31 | }, 32 | "type": "library", 33 | "autoload": { 34 | "psr-4": { 35 | "SpotifyWebAPI\\": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "authors": [ 43 | { 44 | "name": "Jonathan Wilsson", 45 | "email": "jonathan.wilsson@gmail.com" 46 | } 47 | ], 48 | "description": "A PHP wrapper for Spotify's Web API.", 49 | "homepage": "http://jwilsson.github.io/spotify-web-api-php/", 50 | "keywords": [ 51 | "spotify" 52 | ], 53 | "time": "2017-07-26T14:40:41+00:00" 54 | } 55 | ], 56 | "packages-dev": [], 57 | "aliases": [], 58 | "minimum-stability": "stable", 59 | "stability-flags": [], 60 | "prefer-stable": false, 61 | "prefer-lowest": false, 62 | "platform": [], 63 | "platform-dev": [] 64 | } 65 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/SetupCountryCode.php: -------------------------------------------------------------------------------- 1 | 13 | $url = "https://raw.githubusercontent.com/lukes/ISO-3166-Countries-with-Regional-Codes/master/all/all.json"; 14 | 15 | $fetcher = new JsonFetcher($url); 16 | $json = $fetcher->run(); 17 | 18 | $this->search = mb_substr($query, mb_strlen('Country Code ⟩')); 19 | 20 | foreach ($json as $key => $value) { 21 | $currentCountry = array( 22 | 'name' => $value->name, 23 | 'code' => $value->{'alpha-2'} 24 | ); 25 | 26 | $this->countries[] = $currentCountry; 27 | } 28 | 29 | $this->countries[] = array( 30 | 'name' => "I'd rather not give a country!", 31 | 'code' => 'not-given' 32 | ); 33 | } 34 | 35 | public function output() { 36 | $results = array(); 37 | 38 | foreach($this->countries as $country) { 39 | if($this->search != null && 40 | !@mb_stristr($country['name'] . ' ' . $country['code'], $this->search)) 41 | continue; 42 | 43 | $currentResult = array( 44 | 'title' => $country['name'], 45 | 'subtitle' => "Set your country to “{$country['code']}.”", 46 | 'arg' => 'country⟩' . $country['code'], 47 | 'autocomplete' => 'Country Code ⟩' . $country['name'], 48 | 'icon' => array('path' => 'include/images/dash.png') 49 | ); 50 | 51 | $results[] = $currentResult; 52 | } 53 | 54 | if($results == null) { 55 | $results[] = array( 56 | 'title' => 'Could not find country', 57 | 'subtitle' => 'We are looking for a country called "' . $this->search . '"', 58 | 'autocomplete' => 'Country Code ⟩', 59 | 'valid' => false 60 | ); 61 | } else { 62 | usort($results, array($this,'countrySort')); 63 | } 64 | 65 | return $results; 66 | } 67 | 68 | protected function countrySort($a, $b) { 69 | // Give priority to common countries. 70 | // Arbitrarily decided by me. 71 | $common = array( 72 | "United States of America", // America first hehehe 73 | "United Kingdom of Great Britain and Northern Ireland", 74 | "Canada", 75 | "Australia", 76 | "I'd rather not give a country!" 77 | ); 78 | 79 | if(in_array($a['title'], $common)) { 80 | if (in_array($b['title'], $common)) { 81 | return array_search($a['title'], $common) - array_search($b['title'], $common); 82 | } else { 83 | return -1; 84 | } 85 | } 86 | 87 | if(in_array($b['title'], $common)) 88 | return 1; 89 | 90 | // If neither is common, do a simple alphabetical order. 91 | return strcmp($a['title'], $b['title']); 92 | } 93 | } -------------------------------------------------------------------------------- /include/setup/client_submit.php: -------------------------------------------------------------------------------- 1 | options('spotify_app_opt_out', 'true'); 17 | 18 | ?> 19 | 20 | 21 | Spotifious Setup 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |

Setup complete!

31 |

You've successfully opted out of using a Spotify app.

32 |

33 | You can always opt back in using the settings menu in 34 | Spotifious. Just type s in Spotifious. 35 |

36 |

You can now reopen Spotifious to continue setup.

37 |
38 |
39 | 40 | 41 | 42 | requestCredentialsToken($scopes); 63 | } catch(SpotifyWebAPI\SpotifyWebAPIException $e) { 64 | $response["status"] = "error"; 65 | 66 | $response["message"] = "Invalid data - "; 67 | if ($e->getMessage() == "Invalid client") { 68 | $response["message"] .= "invalid ID"; 69 | } elseif($e->getMessage() == "Invalid client secret") { 70 | $response["message"] .= "incorrect secret"; 71 | } else { 72 | $response["message"] .= $e->getMessage(); 73 | } 74 | 75 | echo json_encode($response); 76 | exit(); 77 | } 78 | 79 | // Save data 80 | $alfred->options('spotify_client_id', $_GET["id"]); 81 | $alfred->options('spotify_secret', $_GET["secret"]); 82 | $alfred->options('spotify_access_token', ''); 83 | $alfred->options('spotify_access_token_expires', ''); 84 | $alfred->options('spotify_refresh_token', ''); 85 | $alfred->options('spotify_app_opt_out', 'false'); 86 | 87 | $response["status"] = "success"; 88 | $response["message"] = "Saved your information! Make sure to do step 8 :)"; 89 | echo json_encode($response); 90 | exit(); -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/DetailArtist.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 22 | $locale = $this->alfred->options('country'); 23 | 24 | $this->currentURI = $options['currentURI']; 25 | $this->query = $options['query']; 26 | $this->originalQuery = $options['originalQuery']; 27 | $this->search = $options['search']; 28 | 29 | if($api) { 30 | $artistJson = $api->getArtist($options['id']); 31 | } else { 32 | $artistFetcher = new JsonFetcher("https://api.spotify.com/v1/artists/{$options['id']}"); 33 | $artistJson = $artistFetcher->run(); 34 | } 35 | 36 | $this->name = $artistJson->name; 37 | $this->type = $artistJson->type; 38 | 39 | if($api) { 40 | if($locale == 'not-given') { 41 | $locale = ''; 42 | } 43 | $albumsJson = $api->getArtistAlbums($options['id'], [$locale]); 44 | } else { 45 | $url = "https://api.spotify.com/v1/artists/{$options['id']}/albums"; 46 | if($locale != 'not-given') { 47 | $url .= "?market=$locale"; 48 | } 49 | $albumFetcher = new JsonFetcher($url); 50 | $albumsJson = $albumFetcher->run(); 51 | } 52 | 53 | $this->albums = array(); 54 | foreach ($albumsJson->items as $key => $value) { 55 | $this->albums[] = array( 56 | 'uri' => $value->uri, 57 | 'name' => $value->name, 58 | 'type' => $value->album_type 59 | ); 60 | } 61 | 62 | // TODO search for more! 63 | } 64 | 65 | public function output() { 66 | $results = array(); 67 | 68 | foreach ($this->albums as $key => $current) { 69 | $currentResult = array( 70 | 'title' => $current['name'], 71 | 'subtitle' => "Browse this {$current['type']}", 72 | 'valid' => false, 73 | 'autocomplete' => "{$this->currentURI} ⟩ {$current['uri']} ⟩ {$this->query} ⟩{$this->search}⟩", 74 | 'copy' => $this->currentURI, 75 | 'icon' => array('path' => "include/images/album.png") 76 | ); 77 | 78 | if($this->search != '' && !mb_stristr($currentResult['title'], $this->search)) 79 | continue; 80 | 81 | $results[] = $currentResult; 82 | } 83 | 84 | $scope['title'] = $this->name; 85 | $scope['subtitle'] = "Browse this {$this->type} in Spotify"; 86 | $scope['arg'] = "spotify⟩activate (open location \"{$this->currentURI}\")"; 87 | $scope['autocomplete'] = $this->originalQuery; 88 | $scope['copy'] = $this->currentURI; 89 | $scope['icon'] = array('path' => "include/images/{$this->type}.png"); 90 | 91 | if ($this->search == null) { 92 | array_unshift($results, $scope); 93 | } else { 94 | array_push($results, $scope); 95 | } 96 | 97 | return $results; 98 | } 99 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/DetailAlbum.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 21 | $locale = $this->alfred->options('country'); 22 | 23 | $this->currentURI = $options['currentURI']; 24 | $this->query = $options['query']; 25 | $this->originalQuery = $options['originalQuery']; 26 | $this->search = $options['search']; 27 | 28 | if(!$api) { 29 | $artistFetcher = new JsonFetcher("https://api.spotify.com/v1/albums/{$options['id']}"); 30 | $json = $artistFetcher->run(); 31 | } else { 32 | $json = $api->getAlbum($options['id']); 33 | } 34 | 35 | $this->name = $json->name; 36 | $this->type = $json->type; 37 | 38 | $this->tracks = array(); 39 | foreach ($json->tracks->items as $key => $value) { 40 | $this->tracks[] = array( 41 | 'uri' => $value->uri, 42 | 'name' => $value->name, 43 | 'type' => $value->type, 44 | 'number' => $value->track_number, 45 | 'duration' => $value->duration_ms, 46 | 'explicit' => ($value->explicit == 'true') 47 | ); 48 | } 49 | } 50 | 51 | public function output() { 52 | $results = array(); 53 | 54 | foreach ($this->tracks as $key => $current) { 55 | $explicit = $current['explicit'] ? " (explicit)" : ""; 56 | 57 | $currentResult = array( 58 | 'title' => "{$current['number']}. {$current['name']}", 59 | 'subtitle' => $this->prettifyTime($current['duration']) . $explicit, 60 | 'valid' => true, 61 | 'arg' => "spotify⟩play track \"{$current['uri']}\" in context \"{$this->currentURI}\"", 62 | 'copy' => $current['uri'], 63 | 'icon' => array('path' => "include/images/track.png") 64 | ); 65 | 66 | if($this->search != '' && !mb_stristr($currentResult['title'], $this->search)) 67 | continue; 68 | 69 | $results[] = $currentResult; 70 | } 71 | 72 | $scope['title'] = $this->name; 73 | $scope['subtitle'] = "Browse this {$this->type} in Spotify"; 74 | $scope['arg'] = "spotify⟩activate (open location \"{$this->currentURI}\")"; 75 | $scope['autocomplete'] = $this->originalQuery; 76 | $scope['copy'] = $this->currentURI; 77 | $scope['icon'] = array('path' => "include/images/{$this->type}.png"); 78 | 79 | if ($this->search == null) { 80 | array_unshift($results, $scope); 81 | } else { 82 | array_push($results, $scope); 83 | } 84 | 85 | return $results; 86 | } 87 | 88 | protected function prettifyTime($time_ms) { 89 | $secondsForm = $time_ms / 1000; 90 | 91 | $seconds = $secondsForm % 60; 92 | $minutes = floor($secondsForm / 60); 93 | 94 | $seconds_string = ($seconds < 10) 95 | ? "0" . $seconds 96 | : $seconds; 97 | 98 | return $minutes . ":" . $seconds_string; 99 | } 100 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Settings.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 19 | $this->api = $api; 20 | 21 | $this->trackNotificationsEnabled = ($this->alfred->options('track_notifications') == 'true'); 22 | $this->playlists_cache_date = $this->alfred->options('playlists_cache_date'); 23 | $this->countryCode = $this->alfred->options('country'); 24 | $this->optedOut = ($this->alfred->options('spotify_app_opt_out') == 'true'); 25 | } 26 | 27 | public function output() { 28 | $results[] = array( 29 | 'title' => 'Settings', 30 | 'subtitle' => 'Here you can configure any options you want.', 31 | 'icon' => array('path' => 'include/images/configuration.png'), 32 | 'valid' => false 33 | ); 34 | 35 | $results[] = array( 36 | 'title' => 'Track notifications', 37 | 'subtitle' => "Check this if you'd like to enable track change notifications.", 38 | 'icon' => array('path' => $this->trackNotificationsEnabled ? 'include/images/checked.png' : 'include/images/unchecked.png'), 39 | 'arg' => 'togglenotifications⟩' 40 | ); 41 | 42 | if($this->api) { 43 | $last_update = Helper::human_ago($this->playlists_cache_date); 44 | $results[] = array( 45 | 'title' => 'Update playlists cache', 46 | 'subtitle' => "Last updated $last_update. If your playlists are not appearing, try this.", 47 | 'icon' => array('path' => 'include/images/dash.png'), 48 | 'arg' => 'update_playlists_cache⟩' 49 | ); 50 | } 51 | 52 | $results[] = array( 53 | 'title' => 'Configure country code', 54 | 'subtitle' => 'Currently set to "' . $this->countryCode . '."', 55 | 'autocomplete' => 'Country Code ⟩', 56 | 'valid' => false, 57 | 'icon' => array('path' => 'include/images/dash.png') 58 | ); 59 | 60 | $results[] = array( 61 | 'title' => ($this->optedOut) ? 62 | 'Link a Spotify app' : 63 | 'Change my linked Spotify application', 64 | 'subtitle' => ($this->optedOut) ? 65 | "Opt-in to using a Spotify app. The dark side's not so bad!" : 66 | 'If you want to link a new Spotify app or update key information. You will need to login again.', 67 | 'arg' => 'appsetup⟩', 68 | 'icon' => array('path' => 'include/images/dash.png') 69 | ); 70 | 71 | if(!$this->optedOut) { 72 | $results[] = array( 73 | 'title' => 'Login again to my Spotify application', 74 | 'subtitle' => 'If you want to login again, do it!', 75 | 'arg' => 'applink⟩', 76 | 'icon' => array('path' => 'include/images/dash.png') 77 | ); 78 | } 79 | 80 | $results[] = array( 81 | 'title' => 'You can access settings easily.', 82 | 'subtitle' => 'Type `s` from the main menu.', 83 | 'icon' => array('path' => 'include/images/info.png'), 84 | 'valid' => false 85 | ); 86 | 87 | return $results; 88 | } 89 | } -------------------------------------------------------------------------------- /doc/spec.md: -------------------------------------------------------------------------------- 1 | # How Spotifious should work # 2 | 3 | There are four specific types of output: 4 | 5 | 1. Main Menu — first activation 6 | 4. Generic Menu — list of options, used for settings menu. 7 | 2. Search Menu — user typing 8 | 3. Detail Menu — autocompleted text with Spotify URL 9 | 10 | ## Main Menu ## 11 | 12 | `^⌘⏎` should show the main menu. 13 | 14 | It should check for updates every so often and display an update bar occasionally. 15 | 16 | 1. ► Crooked Teeth ⏎ `playpause` 17 | - ??? 18 | 2. Plans ⏎ album detail 19 | - More from this album... 20 | 3. Death Cab for Cutie ⏎ artist detail 21 | - More from this artist... 22 | 4. Search for music... 23 | - Begin typing to search 24 | 25 | ## Generic Menu ## 26 | 27 | We use this for the settings & setup menus. 28 | 29 | ### Setup menu ### 30 | 31 | These are the options required for initial operation. 32 | 33 | - Country Code 34 | - should trigger a list of countries. 35 | - Create a Spotify application 36 | - should trigger the application-creation web server. 37 | - Link Spotify application 38 | - should open the Spotify login page for the new app. 39 | 40 | ### Settings Menu ### 41 | 42 | These are all the original setup options, plus some additional ones. 43 | 44 | - Track Notifications 45 | - toggle notifications of next playing tracks 46 | 47 | ## Search Menu ## 48 | 49 | After activation, any typing (3+ chars should work) should show the search menu, unless the query requires a detail menu (see below). The results should be weighted so artists>albums>songs— but only if the query is completely contained by the result. `Lady Danv` should return Lady Danville, then Lady Danville EP, then songs. Otherwise sort by popularity; use a unique id for each query so popular searches follow Alfred's smart order. 50 | 51 | If the query starts with a `c`, include control items, like play, pause, shuffle, etc. 52 | 53 | 1. Lady Danville ⏎ artist detail 54 | - ★★★★☆ Artist 55 | 2. Lady Danville EP ⏎ album detail 56 | - ★★★☆☆ Album by Lady Danville 57 | 3. Love to Love ⏎ `open location <song>` 58 | - ★★☆☆☆ Lady Danville EP by Lady Danville 59 | 60 | ## Detail Menu ## 61 | 62 | I want to maintain easily navigable menus & submenus, but need to provide the Spotify URL in order to perform an artist or album lookup. To that end, the detail menu uses a smart syntax, separated by some unicode glyphs (`►` & `⟩` maybe). 63 | 64 | The query should have all URLs in hierarchical order, than all the queries in hierarchical order, followed by a closing separator: 65 | 66 | `a menu url⟩a submenu url⟩another submenu url►query used in menu⟩query used in submenu⟩query used in final submenu⟩` 67 | 68 | The different separator should be a sign that the query is parsable. If it isn't present, ignore the query. 69 | 70 | This syntax will not be compatible with the old syntax. 71 | 72 | ### Playlist Detail ### 73 | 74 | 1. Liked from Radio ⏎ view in Spotify 75 | - View playlist in Spotify 76 | 2. Sticking it to Myself ⏎ `open location <song>` 77 | - Jonathan Coulton - Artificial Heart 2:20 ★★★☆☆ 78 | 79 | ### Artist Detail ### 80 | 81 | 1. Jonathan Coulton ⏎ view in Spotify 82 | - View artist in Spotify 83 | 2. One Christmas at a Time ⏎ album detail 84 | - Open this album... 85 | 3. ... 86 | 87 | ### Album Detail ### 88 | 89 | 1. Artificial Heart ⏎ view in Spotify 90 | - View album in Spotify 91 | 2. 1. Sticking it to Myself ⏎ `open location <song>` 92 | - Jonathan Coulton 2:20 ★★★☆☆ 93 | 3. ... -------------------------------------------------------------------------------- /include/setup/style/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | *:focus { 6 | -webkit-animation: bounce 0.25s 0s; 7 | -moz-animation: bounce 0.25s 0s; 8 | -o-animation: bounce 0.25s 0s; 9 | animation: bounce 0.25s 0s; 10 | } 11 | 12 | *:target { 13 | background: #93FF74; 14 | -webkit-animation: bounce 0.25s 0s; 15 | -moz-animation: bounce 0.25s 0s; 16 | -o-animation: bounce 0.25s 0s; 17 | animation: bounce 0.25s 0s; 18 | } 19 | 20 | a, 21 | input[type=submit] { 22 | position: relative; 23 | bottom: 0; 24 | -webkit-transition: all 0.1s ease-in; 25 | } 26 | 27 | a:hover, 28 | input[type=submit]:hover { 29 | position: relative; 30 | bottom: .1em; 31 | -webkit-transition: none; 32 | } 33 | 34 | body { 35 | color: #333; 36 | line-height: 1.5; 37 | } 38 | 39 | .clear { 40 | clear: both; 41 | } 42 | 43 | .wrapper { 44 | width: 40em; 45 | margin: 0 auto; 46 | } 47 | 48 | code, kbd { 49 | background: #ddd; 50 | padding: .5em; 51 | 52 | border: .1em solid #999; 53 | border-radius: .5em; 54 | } 55 | 56 | figure { 57 | margin: 1em 0; 58 | padding: 0; 59 | } 60 | 61 | figcaption { 62 | text-align: center; 63 | padding: .5em; 64 | 65 | background: #333; 66 | color: #fff; 67 | } 68 | 69 | img { 70 | max-width: 100%; 71 | border: .5em solid #333; 72 | } 73 | 74 | table { 75 | width: 100%; 76 | } 77 | 78 | td + td { 79 | padding-left: .5em; 80 | } 81 | 82 | label { 83 | color: #999; 84 | } 85 | 86 | input { 87 | padding: .5em; 88 | width: 100%; 89 | border: .1em solid #999; 90 | } 91 | 92 | input[type=submit] { 93 | width: auto; 94 | float: right; 95 | margin: .5em 0; 96 | padding: .5em 2em; 97 | 98 | background: rgb(81, 37, 132); 99 | color: #fff; 100 | border: 0; 101 | border-radius: 1em; 102 | } 103 | 104 | input[disabled] { 105 | color: #999; 106 | } 107 | 108 | .cancel { 109 | float: right; 110 | display: inline-block; 111 | margin: .5rem 2rem; 112 | 113 | color: #627699; 114 | font-size: 0.75em; 115 | } 116 | 117 | .response, .info { 118 | text-align: center; 119 | padding: 1em 0; 120 | } 121 | .response { 122 | display: none; 123 | } 124 | 125 | .error { 126 | background: #E52C48; 127 | color: #fff; 128 | } 129 | 130 | .success { 131 | background: #6CD19B; 132 | } 133 | 134 | .note { 135 | background: #80A0FF; 136 | } 137 | 138 | .warning { 139 | background: #93FF74; 140 | } 141 | 142 | @-webkit-keyframes bounce { 143 | 0% { -webkit-transform: scale(1); transform: scale(1);} 144 | 60% { -webkit-transform: scale(1.4); transform: scale(1.4); } 145 | 90% { -webkit-transform: scale(0.95); transform: scale(0.95); } 146 | 100% { -webkit-transform: scale(1); transform: scale(1); } 147 | } 148 | 149 | @-moz-keyframes bounce { 150 | 0% { -moz-transform: scale(1); transform: scale(1);} 151 | 60% { -moz-transform: scale(1.4); transform: scale(1.4); } 152 | 90% { -moz-transform: scale(0.95); transform: scale(0.95); } 153 | 100% { -moz-transform: scale(1); transform: scale(1); } 154 | } 155 | 156 | @-o-keyframes bounce { 157 | 0% { -o-transform: scale(1); transform: scale(1);} 158 | 60% { -o-transform: scale(1.4); transform: scale(1.4); } 159 | 90% { -o-transform: scale(0.95); transform: scale(0.95); } 160 | 100% { -o-transform: scale(1); transform: scale(1); } 161 | } 162 | 163 | @keyframes bounce { 164 | 0% { transform: scale(1); } 165 | 60% { transform: scale(1.4); } 166 | 90% { transform: scale(0.95); } 167 | 100% { transform: scale(1); } 168 | } -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/DetailPlaylist.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 22 | $locale = $this->alfred->options('country'); 23 | 24 | $this->currentURI = $options['currentURI']; 25 | $this->query = $options['query']; 26 | $this->originalQuery = $options['originalQuery']; 27 | $this->search = $options['search']; 28 | 29 | $explodedURI = explode(":", $this->currentURI); 30 | $user = $explodedURI[count($explodedURI) - 3]; 31 | 32 | if(!$api) { 33 | throw new StatefulException("You should have an API at this point"); 34 | } else { 35 | $json = $api->getUserPlaylist($user, $options['id']); 36 | } 37 | 38 | $this->name = $json->name; 39 | $this->type = $json->type; 40 | 41 | $this->tracks = array(); 42 | foreach ($json->tracks->items as $key => $value) { 43 | $this->tracks[] = array( 44 | 'uri' => $value->track->uri, 45 | 'name' => $value->track->name, 46 | 'type' => $value->track->type, 47 | 'number' => $key + 1, 48 | 'duration' => $value->track->duration_ms, 49 | 'explicit' => ($value->track->explicit == 'true') 50 | ); 51 | } 52 | 53 | $this->overflow = false; 54 | if($json->tracks->total > $json->tracks->limit) { 55 | $this->overflow = true; 56 | $this->overflow_count = $json->tracks->total - $json->tracks->limit; 57 | } 58 | } 59 | 60 | public function output() { 61 | $results = array(); 62 | 63 | foreach ($this->tracks as $key => $current) { 64 | $explicit = $current['explicit'] ? " (explicit)" : ""; 65 | 66 | $currentResult = array( 67 | 'title' => "{$current['number']}. {$current['name']}", 68 | 'subtitle' => $this->prettifyTime($current['duration']) . $explicit, 69 | 'valid' => true, 70 | 'arg' => "spotify⟩play track \"{$current['uri']}\" in context \"{$this->currentURI}\"", 71 | 'copy' => $current['uri'], 72 | 'icon' => array('path' => "include/images/track.png") 73 | ); 74 | 75 | if($this->search != '' && !mb_stristr($currentResult['title'], $this->search)) 76 | continue; 77 | 78 | $results[] = $currentResult; 79 | } 80 | 81 | if($this->overflow) { 82 | $overflow = array( 83 | 'title' => 'This is a large playlist', 84 | 'subtitle' => ($this->overflow_count === 1) 85 | ? "1 song could not be displayed; you can view it in Spotify." 86 | : "$this->overflow_count songs could not be displayed; you can view them in Spotify.", 87 | 'arg' => "spotify⟩activate (open location \"{$this->currentURI}\")", 88 | 'autocomplete' => $this->originalQuery, 89 | 'copy' => $this->currentURI, 90 | 'icon' => array('path' => "include/images/info.png") 91 | ); 92 | } 93 | 94 | $scope['title'] = $this->name; 95 | $scope['subtitle'] = "Browse this {$this->type} in Spotify"; 96 | $scope['arg'] = "spotify⟩activate (open location \"{$this->currentURI}\")"; 97 | $scope['autocomplete'] = $this->originalQuery; 98 | $scope['copy'] = $this->currentURI; 99 | $scope['icon'] = array('path' => "include/images/{$this->type}.png"); 100 | 101 | if ($this->search == null) { 102 | if($this->overflow) { 103 | array_unshift($results, $overflow); 104 | } 105 | array_unshift($results, $scope); 106 | } else { 107 | array_push($results, $scope); 108 | if($this->overflow) { 109 | array_push($results, $overflow); 110 | } 111 | } 112 | 113 | return $results; 114 | } 115 | 116 | protected function prettifyTime($time_ms) { 117 | $secondsForm = $time_ms / 1000; 118 | 119 | $seconds = $secondsForm % 60; 120 | $minutes = floor($secondsForm / 60); 121 | 122 | $seconds_string = ($seconds < 10) 123 | ? "0" . $seconds 124 | : $seconds; 125 | 126 | return $minutes . ":" . $seconds_string; 127 | } 128 | } -------------------------------------------------------------------------------- /_remote/pages/BF8D740B-C9D0-4622-8695-FC0CCA97B37F.data: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | backgroundcolor 6 | 7 | 0.64910906553268433 8 | 0.56339049339294434 9 | 0.66290569305419922 10 | 11 | iconid 12 | 396A6C7D-DEF5-4580-8F15-927EFB5D6BFC 13 | iconisuserspecified 14 | 15 | items 16 | 17 | 18 | actionconfig 19 | 20 | autoworkflowuid 21 | 22 | triggerid 23 | com.citelao.spotifious.previous 24 | 25 | actionuid 26 | remote.alfred.runworkflow 27 | buttoniconid 28 | 5A4E8FCB-5384-41CC-8B7C-97A263163633 29 | buttonlabel 30 | Previous Track 31 | itemuid 32 | 3E137BFB-62B6-49F5-881F-753A2A7A3523 33 | 34 | 35 | actionconfig 36 | 37 | autoworkflowuid 38 | 39 | triggerid 40 | com.citelao.spotifious.playpause 41 | 42 | actionuid 43 | remote.alfred.runworkflow 44 | buttoniconid 45 | 3277740D-DEEE-4C08-8B1D-E7B962742990 46 | buttonlabel 47 | Play/Pause 48 | itemuid 49 | 60010EB8-95D0-414B-8732-2DEABA8D7D0E 50 | 51 | 52 | actionconfig 53 | 54 | autoworkflowuid 55 | 56 | triggerid 57 | com.citelao.spotifious.search 58 | 59 | actionuid 60 | remote.alfred.runworkflow 61 | buttoniconid 62 | B476C144-0CAF-4BC7-8609-BD97EBFCCF3F 63 | buttonlabel 64 | Search 65 | itemuid 66 | D1B96059-3586-42DD-8CBB-13A9AA19574D 67 | 68 | 69 | actionconfig 70 | 71 | autoworkflowuid 72 | 73 | triggerid 74 | com.citelao.spotifious.next 75 | 76 | actionuid 77 | remote.alfred.runworkflow 78 | buttoniconid 79 | D113E787-2890-471A-B3E8-126F562439DC 80 | buttonlabel 81 | Next Track 82 | itemuid 83 | FC6BDC0F-E523-4849-A970-1F8026B4B40B 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | actionconfig 95 | 96 | autoworkflowuid 97 | 98 | triggerid 99 | com.citelao.spotifious.voldown 100 | 101 | actionuid 102 | remote.alfred.runworkflow 103 | buttoniconid 104 | 3A89A720-DC62-42BE-9FB6-2BFEA717BCE9 105 | buttonlabel 106 | Volume Down 107 | itemuid 108 | 45153A8C-C2BC-4DF3-85C3-DA09FD30EE7D 109 | 110 | 111 | 112 | 113 | actionconfig 114 | 115 | autoworkflowuid 116 | 117 | triggerid 118 | com.citelao.spotifious.volup 119 | 120 | actionuid 121 | remote.alfred.runworkflow 122 | buttoniconid 123 | 9AF9D5D1-FA74-4419-9B9C-C075BEC1AD75 124 | buttonlabel 125 | Volume Up 126 | itemuid 127 | FBE348BE-7495-4997-BC2C-4FA298E552FF 128 | 129 | 130 | subtitle 131 | Control Spotify 132 | title 133 | Spotifious 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Setup.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 18 | 19 | $this->wasOptOut = $this->alfred->options('spotify_app_opt_out') == 'true'; 20 | $this->hasInstalledSpotify = $this->alfred->options('has_installed_spotify') == 'true'; 21 | 22 | $this->countryCodeConfigured = !($this->alfred->options('country') == ''); 23 | $this->applicationCreated = !($this->alfred->options('spotify_client_id') == '' || $this->alfred->options('spotify_secret') == ''); 24 | $this->applicationLinked = !($this->alfred->options('spotify_access_token') == '' || 25 | $this->alfred->options('spotify_refresh_token') == '' || 26 | $this->alfred->options('spotify_access_token_expires') == '' || 27 | $this->alfred->options('desired_scopes') != $this->alfred->options('registered_scopes')); 28 | 29 | $this->applicationPreviouslyLinked = !($this->alfred->options('spotify_access_token') == ''); 30 | } 31 | 32 | public function output() { 33 | if($this->wasOptOut) { 34 | $results[] = array( 35 | 'title' => 'Sorry, but the Spotify API changed.', 36 | 'subtitle' => 'You must now create a Spotify application to use Spotifious.', 37 | 'icon' => array('path' => 'include/images/configuration.png'), 38 | 'valid' => false 39 | ); 40 | } else { 41 | $results[] = array( 42 | 'title' => 'Welcome to Spotifious!', 43 | 'subtitle' => 'You need to configure a few more things before you can use Spotifious.', 44 | 'icon' => array('path' => 'include/images/configuration.png'), 45 | 'valid' => false 46 | ); 47 | } 48 | 49 | $results[] = array( 50 | 'title' => '1. Download & install Spotify', 51 | 'subtitle' => 'Spotifious only works with the Spotify desktop app.', 52 | 'icon' => array('path' => $this->hasInstalledSpotify ? 'include/images/checked.png' : 'include/images/unchecked.png'), 53 | 'arg' => '{"action":"command", "options": {"command": "open https://www.spotify.com/us/download/mac/"}}', 54 | ); 55 | 56 | $results[] = array( 57 | 'title' => '2. Set your country code', 58 | 'subtitle' => 'Choosing the correct country code makes sure you can play songs you select.', 59 | 'icon' => array('path' => $this->countryCodeConfigured ? 'include/images/checked.png' : 'include/images/unchecked.png'), 60 | 'autocomplete' => 'Country Code ⟩', 61 | 'valid' => false 62 | ); 63 | 64 | $results[] = array( 65 | 'title' => '3. Create a Spotify application', 66 | 'subtitle' => 'Set up a Spotify application so you can search playlists!', 67 | 'icon' => array('path' => $this->applicationCreated ? 'include/images/checked.png' : 'include/images/unchecked.png'), 68 | 'arg' => 'appsetup⟩' 69 | ); 70 | 71 | if($this->applicationPreviouslyLinked) { 72 | $results[] = array( 73 | 'title' => '4. Relink your Spotify application', 74 | 'subtitle' => "We've added new features to Spotifious, but you need to login again to use them.", 75 | 'icon' => array('path' => $this->applicationCreated ? $this->applicationLinked ? 'include/images/checked.png' : 'include/images/unchecked.png' : 'include/images/disabled.png'), 76 | 'arg' => 'applink⟩', 77 | 'valid' => $this->applicationCreated ? true : false 78 | ); 79 | 80 | } else { 81 | $results[] = array( 82 | 'title' => '4. Link your Spotify application', 83 | 'subtitle' => 'Connect your Spotify application to Spotifious to search your playlists.', 84 | 'icon' => array('path' => $this->applicationCreated ? $this->applicationLinked ? 'include/images/checked.png' : 'include/images/unchecked.png' : 'include/images/disabled.png'), 85 | 'arg' => 'applink⟩', 86 | 'valid' => $this->applicationCreated ? true : false 87 | ); 88 | } 89 | 90 | $results[] = array( 91 | 'title' => 'You can access settings easily.', 92 | 'subtitle' => 'Type `s` from the main menu.', 93 | 'icon' => array('path' => 'include/images/info.png'), 94 | 'valid' => false 95 | ); 96 | 97 | return $results; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Main.php: -------------------------------------------------------------------------------- 1 | now(); 17 | 18 | $this->currentTrack = $current[0]; 19 | $this->currentAlbum = $current[1]; 20 | $this->currentArtist = $current[2]; 21 | $this->currentURL = $current[3]; 22 | $this->currentStatus = ($current[4] == 'playing') ? "include/images/paused.png" : "include/images/playing.png"; 23 | } 24 | 25 | public function output() { 26 | $results[0] = array( 27 | 'title' => "$this->currentTrack", 28 | 'subtitle' => "$this->currentAlbum by $this->currentArtist", 29 | 'arg' => "playpause⟩", 30 | 'copy' => $this->currentURL, 31 | 'icon' => array( 32 | 'path' => $this->currentStatus 33 | ), 34 | // 'mods' => array( 35 | // 'alt' => array( 36 | // 'subtitle' => "Browse to artist ($this->currentArtist)...", 37 | // 'arg' => '{"action": "spotifious", "options": { "command": "artist:' . $this->currentArtist . '" }}' 38 | // ), 39 | // 'ctrl' => array( 40 | // 'subtitle' => "Browse to album ($this->currentAlbum)..." 41 | // ), 42 | // 'cmd' => array( 43 | // 'subtitle' => 'Queue this song' 44 | // ), 45 | // ) 46 | ); 47 | 48 | $results[1] = array( 49 | 'title' => "$this->currentAlbum", 50 | 'subtitle' => "More from this album...", 51 | 'autocomplete' => "artist:$this->currentArtist album:$this->currentAlbum", // TODO change to albumdetail 52 | 'copy' => "$this->currentAlbum", // TODO change to albumdetail 53 | 'valid' => false, 54 | 'icon' => array( 55 | 'path' => 'include/images/album.png' 56 | ), 57 | // 'mods' => array( 58 | // 'alt' => array( 59 | // 'subtitle' => "Browse to artist ($this->currentArtist)..." 60 | // ), 61 | // 'cmd' => array( 62 | // 'subtitle' => 'Queue this album' 63 | // ), 64 | // ) 65 | ); 66 | 67 | $results[2] = array( 68 | 'title' => "$this->currentArtist", 69 | 'subtitle' => "More by this artist...", 70 | 'autocomplete' => "artist:$this->currentArtist", // TODO change to artistdetail 71 | 'copy' => $this->currentArtist, // TODO change to artistdetail 72 | 'valid' => false, 73 | 'icon' => array('path' => 'include/images/artist.png') 74 | ); 75 | 76 | $results[3] = array( 77 | 'title' => "Search for music...", 78 | 'subtitle' => "Begin typing to search", 79 | 'valid' => false, 80 | 'icon' => array('path' => "include/images/search.png"), 81 | 'mods' => array( 82 | 'ctrl' => array( 83 | 'valid' => true, 84 | 'subtitle' => 'Open controls...', 85 | 'arg' => '{"action": "spotifious", "options": { "command": "c" }}', 86 | 'variables' => array( 87 | 'keepalive' => 'true' 88 | ) 89 | ), 90 | 'cmd' => array( 91 | 'valid' => true, 92 | 'subtitle' => 'Open settings...', 93 | 'arg' => '{"action": "spotifious", "options": { "command": "s" }}', 94 | 'icon' => array('path' => "include/images/configuration.png"), 95 | 'variables' => array( 96 | 'keepalive' => 'true' 97 | ) 98 | ), 99 | 'shift' => array( 100 | 'valid' => true, 101 | 'subtitle' => 'Activate Spotify', 102 | 'arg' => '{"action": "applescript", "options": { "application": "Spotify", "command": "activate" }}' 103 | ) 104 | ) 105 | ); 106 | 107 | // Overrides for no track 108 | if($this->currentTrack == "No track playing") { 109 | $results[0]['subtitle'] = ""; 110 | 111 | $results[1]['subtitle'] = ""; 112 | $results[1]['autocomplete'] = ""; 113 | $results[1]['copy'] = ""; 114 | 115 | $results[2]['subtitle'] = ""; 116 | $results[2]['autocomplete'] = ""; 117 | $results[2]['copy'] = ""; 118 | } 119 | 120 | return $results; 121 | } 122 | 123 | protected function now() { 124 | $spotQuery = new ApplicationApplescript('Spotify', 'return name of current track & "✂" & album of current track & "✂" & artist of current track & "✂" & spotify url of current track & "✂" & player state'); 125 | 126 | $data = $spotQuery->run(); 127 | 128 | $array = explode("✂", $data); 129 | 130 | if($array[0] == "") { 131 | $array[0] = "No track playing"; 132 | $array[1] = "No album"; 133 | $array[2] = "No artist"; 134 | $array[3] = ""; 135 | $array[4] = "paused"; 136 | } 137 | 138 | return $array; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog # 2 | 3 | ## v0.13.9 ## 4 | - Fixed: Deprecation error thrown in newer versions of PHP for unparenthesized 5 | left-associative ternary operator in `Spotifious.php` 6 | - Changed: Updated `spotifious.sublime-project` configuration to use relative 7 | path of the workflow directory 8 | 9 | ## v0.13.8 ## 10 | - Fixed: Support new Spotify playlist syntax (without `user:` prefix). 11 | 12 | ## v0.13.7 ## 13 | - Fixed: Bump up the Spotify API version so Spotifious works again 14 | 15 | ## v0.13.6 ## 16 | - Added: Version checker added to buildscript so I stop shipping incorrect versions numbers 17 | - Fixed: Addressed a syntax error that prevented Spotify from returning properly 18 | 19 | ## v0.13.5 ## 20 | - Added: Now checks for installed Spotify on startup 21 | - Added: New hotkeys for playing albums and artists directly from search 22 | - Changed: @philihp clarrified where build location is 23 | 24 | ## v0.13.4 ## 25 | - Fixed: Compilation albums now have an icon 26 | - Fixed: Can now add playlists with `\` in their titles 27 | - Fixed: Removed light-colored edge of some icons 28 | 29 | ## v0.13.3.2 ## 30 | - Fixed: Alfred variables are now more reliably passed to the workflow. Should 31 | fix several people's configuration bugs 32 | 33 | ## v0.13.3.1 ## 34 | - Added: there's a small install guide inside the Workflow menu 35 | - Fixed: Small playlists now appear correctly 36 | - Fixed: Playlist cache correctly on first run (thanks, @chrsblck!) 37 | 38 | ## v0.13.3 ## 39 | - Added: Alfred-native hotkey activation means a huge speedboost to hotkeys 40 | - Added: Cache your playlists to add them to search 41 | - Fixed: API search now uses Authentication all the time 42 | - Changed: Do not source control vendor/ 43 | - Changed: Use a newer version of JWilsson's API. 44 | - Removed: Opt-out does not work with the Spotify API anymore, so it's removed (thanks @atabbott!) 45 | 46 | ## v0.13.2.1 ## 47 | - Fixed: Setup bug where Spotifious expects client ID before it exists 48 | 49 | ## v0.13.2 ## 50 | - Added: Use JSON output when possible 51 | - Added: Exceptions appear in debugger for actions 52 | - Changed: Search uses API with auth 53 | - Changed: Album pages use API with auth 54 | - Changed: Album pages load faster 55 | - Changed: Info loads much faster (but may break in edge cases) 56 | - Changed: Popularity boxes look pretty now (thanks, danielma!) 57 | - Changed: Build size is much smaller 58 | - Fixed: Single-digit seconds are now prefixed with a 0 59 | - Fixed: Add Homebrew to path if needed (thanks, mieubrisse!) 60 | - Fixed: Setup in Alfred 3 works 61 | - Fixed: The UK shows up at the top of country selection 62 | 63 | ## v0.13.1 ## 64 | - Fixed: Alfred 3 compatibility 65 | - Fixed: America appears first in country selection 66 | 67 | ## v0.13 ## 68 | - Added: preliminary scaffolding for web API (playlists, starring, all of it is coming) 69 | - Added: new settings menu. 70 | - Added: you can now *disable* track notifications 71 | - Fixed: track notifications say the right thing in Spotify 1.0 72 | - Fixed: control panel stays open properly 73 | - Fixed: weird looping volume changes 74 | 75 | ## v0.12 ## 76 | - Added: compatiblity with Alfred Remote 77 | 78 | ## v0.11.2 ## 79 | - Added: opt-out feature for location information 80 | - Changed: better installation guide 81 | 82 | ## v0.11.1 ## 83 | - Fixed: non-responsiveness after some requests to Spotify 84 | 85 | ## v0.11 ## 86 | - Added: hotkeys, configurable via Alfred 87 | - Added: notifications 88 | - Added: controls menu (type `c`) 89 | - Changed: using Spotify's faster web API! 90 | - Changed: non-unicode popularity glyphs 91 | 92 | ## v0.10 ## 93 | - Added: Settings menu (accessible using "s") 94 | - Added: Location-based filtering of search results 95 | - Changed: Actions route through `action.php` now, instead of direct Applescript execution. 96 | 97 | ## v0.9.4 ## 98 | - Changed: more compatibility changes for PHP v5.3. These did not get included in v0.9.3. 99 | 100 | ## v0.9.3 ## 101 | - Changed: more compatibility changes for PHP v5.3 102 | 103 | ## v0.9.2 ## 104 | - Changed: compatibility changes for PHP v5.3 105 | 106 | ## v0.9.1 ## 107 | - Changed: prevent breakage if no track playing. 108 | 109 | ## v0.9 ## 110 | - Added: created changelog 111 | - Added: error reporting 112 | - Added: context-based searching 113 | - Added: README in Alfred. 114 | - Added: Packal support 115 | - Changed: restructured to use Composer 116 | - Changed: divided Alfred output code into `OhAlfred` 117 | - Changed: uses more extensible menu system. 118 | - Removed: Alleyoop support 119 | - Removed: album art (as I never use it and it was becoming increasingly untested) 120 | 121 | ## v0.8 ## 122 | - Added: new icons; set them as default 123 | 124 | ## v0.7 ## 125 | - Added: artist- and album-specific menus 126 | - Changed: general code clean-up 127 | 128 | ## v0.6 ## 129 | - Added: Alleyoop support 130 | 131 | ## v0.5 ## 132 | - Added: menu system 133 | 134 | ## v0.1 ## 135 | - Added: initial search system 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spotifious # 2 | ## a natural Spotify controller for Alfred ## 3 | 4 | ![The magical interface](include/screenshots/hero_shot.png) 5 | 6 | So, you've heard of **Spotifious**, eh? *A natural Spotify controller for 7 | Alfred*? Searches Spotify, controls your music, generally rocks? 8 | 9 | It's built off a lot of other people's work— 10 | [PHPFunk](https://github.com/phpfunk/alfred-spotify-controls) and 11 | [David Ferguson](http://jdfwarrior.tumblr.com/) especially— and tries to match 12 | the functionality of Alfred's integrated iTunes player. It's not perfect, but it does the job. And I think it's pretty cool. 13 | 14 | ## Features ## 15 | 16 | A quick rundown of its coolness: 17 | 18 | ### A controller, not a workflow ### 19 | 20 | Spotifious just *feels* fun to use, like a real plugin for Alfred, not a workflow. It 21 | can do in-depth, album-scouring searches and gives you useful information even if something goes wrong. Filter by artist or album, search for specific tracks or just plain see what's playing. It's all there. 22 | 23 | Not unlike the iTunes Mini Player. 24 | 25 | ### Alfred-like ### 26 | 27 | Spotifious is also especially Alfred-like. Just start typing to scour Spotify's servers for music. Browse through artists and albums like nobody's business and drill down through Queen's huge anthologies with a snappy context-based search and filtering. Your frequent searches rise to the top, and your controls pop up with the letter `c`. 28 | 29 | ### Smart ### 30 | 31 | Spotifious uses Packal to make sure you always have the latest version. It gives you a heads-up about what's going on with Spotify *the moment you start it*. And don't worry about errors— we'll handle those. All you have to do is choose how you want to pop it up on screen. 32 | 33 | ## Download & Install ## 34 | 35 | Latest version: [v0.13.9](https://github.com/citelao/Spotify-for-Alfred/archive/master.zip) | Latest dev build: [v0.13.9](https://github.com/citelao/Spotify-for-Alfred/archive/dev.zip) 36 | 37 | An in-depth [installation guide](http://ben.stolovitz.com/Spotify-for-Alfred/download/) is available on the Spotifious website. 38 | 39 | 1. [Download](https://github.com/citelao/Spotify-for-Alfred/archive/master.zip) 40 | this repository. 41 | 2. Open `Spotifious.alfredworkflow` in the `dist` folder by double-clicking it or dragging it into 42 | Alfred. 43 | 3. Double-click the first thingy marked `Hotkey`. 44 | 4. Click the textfield labeled `Hotkey` and press `^⌘⏎`. 45 | 5. Click `Save` to store the binding. 46 | 6. Bind the other hotkeys as you wish. Their actions are visible in the `Text` field, under `Argument`. 47 | 7. Pop up Spotifious with the key command you set— I would have `^⌘⏎`. 48 | 8. Follow the instructions on the set-up menu. 49 | 1. You'll be asked to choose your country (for better search results— you can opt out). 50 | 2. You'll create a Spotify application (or not, you can opt out). 51 | 3. You'll link the new app to Spotifious. 52 | 10. Continue on with your merry day— but now with Spotifious. 53 | 54 | ![A visual install guide](include/screenshots/install.png) 55 | 56 | ## How to Use ## 57 | 58 | So let's assume you've downloaded and installed the workflow. Now what? 59 | 60 | 1. Press `^⌘⏎`. 61 | 62 | Good! You should briefly see a loading entry, then the main menu: 63 | 64 | ![Loading...](include/screenshots/loading.png) 65 | ![Main Menu](include/screenshots/main_menu.png) 66 | 67 | You can action the song title (press `⏎`) to play or pause the song, action 68 | the album or artist to search for that album or artist, or just start typing to 69 | search for cool music. 70 | 71 | ![Some cool screenshots](include/screenshots/compilation.png) 72 | 73 | Once you've searched for something, you can continue to browse albums and arists through Spotifious. Actioning an artist will bring up a list of their albums, and actioning an album will bring up the track list. 74 | 75 | If you're looking through a really long album or a particularly prodigious artist, you can also narrow your search by typing a little more— Spotifious will search for results within the album or artist. 76 | 77 | **Note:** You can always leave a menu and go back just by pressing `⌫`. 78 | 79 | You can access a small controls menu by typing `c` at the main menu. 80 | 81 | ![A preview of the controls menu](include/screenshots/controls.png) 82 | 83 | You can change your settings by typing `s` at the main menu. 84 | 85 | ![The settings menu](include/screenshots/settings.png) 86 | 87 | ## Acknowledgements ## 88 | 89 | A lot of people helped out on this: 90 | 91 | - **vdesabou** and I trade ideas a ton 92 | - **PHPfunk** and his PHP Alfred workflow inspired much of `OhAlfred` 93 | - **David Ferguson** helped with some iffy Alfred glitches 94 | - **jwilsson**'s [Spotify PHP web API](http://jwilsson.github.io/spotify-web-api-php/). 95 | - a slightly modified version of [Entypo](http://www.entypo.com/) icon font face for icons. 96 | - and many other people mentioned in the code. 97 | 98 | Thanks to everyone who helped, and thanks to everyone who uses my work! 99 | Oh, and thanks for reading this whole README. I hope you enjoy Spotifious! 100 | 101 | ## Development ## 102 | 103 | You must install dependencies manually. Install 104 | [Composer](https://getcomposer.org/) and then run `make init`. Then you can 105 | proceed with development! (if you do not want to do a global install, run 106 | `php composer.phar install` from this directory) 107 | 108 | Command line development for Alfred 3 is currently not working, since we 109 | depend on several environment variables. Eventually, if you are developing for 110 | Alfred version 3 and want to run these files from 111 | the command line (`php -f main.php -- "args"`), you will need to run in debug 112 | mode : `DEBUG=true php -f main.php -- "args"`. 113 | 114 | ### Deployment ### 115 | 116 | 1. Bump version here 117 | 2. Update `CHANGELOG` 118 | 3. Update `.alfredworkflow` version 119 | 4. Update Alfred forum 120 | 5. Update Packal 121 | 6. Update GitHub Pages site 122 | 123 | ## TODO ## 124 | 125 | - Allow `⎇`, `^`, and `⌘` to function as modifiers (Open in Spotify, other things?). 126 | - More in-depth Spotify control (radio, queuing, playlists) 127 | - Make main menu links go directly to menus. 128 | 129 | ## Changelog ## 130 | 131 | This project uses pseudo-SemVer. It will use SemVer when we hit v1.0.0. 132 | 133 | See `CHANGELOG.md`. 134 | 135 | ## License ## 136 | 137 | This code is licensed as written in the LICENSE.md file. 138 | -------------------------------------------------------------------------------- /include/setup/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | Spotifious Setup 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Spotifious Setup

13 |

14 | Hi! In order to fully use Spotifious (search playlists, star 15 | tracks, etc.), you need to generate a Spotify API key by 16 | creating a Spotify app. 17 |

18 |

19 | NOTE: this server will automatically close in 5 20 | minutes. If you get errors like "page not found," just reopen 21 | Spotifious. 22 |

23 |
24 | 25 |
26 |

Instructions

27 |

28 | Setting up the app is not very hard, but it has a couple 29 | specific steps. Just follow these instructions, and you'll be 30 | fine! 31 |

32 |

33 | If you're worried about privacy, 34 | here’s what I have to say. 35 |

36 |

37 | If you already know how to make a Spotify app, you can enter the 38 | ID and secret here. Please set the callback 39 | URL correctly, though (step 8). 40 |

41 | 42 |
    43 |
  1. Open up the Spotify application manager page.
  2. 44 |
  3. Login, if prompted.
  4. 45 |
  5. 46 | Click "Create an app." 47 |
    48 | A screenshot of the Spotify Developer website, focusesed on the 'Create an app button' 51 |
    The 'Create an app' button
    52 |
    53 |
  6. 54 |
  7. 55 | Enter an application name and description. They can be anything you want. 56 |
    57 | A screenshot of the Spotify Developer application creation page, with example data entered in the 'Application Name' and 'Application Description' fields. 60 |
    This data will do nicely.
    61 |
    62 |
  8. 63 |
  9. Click "Create"!
  10. 64 |
  11. 65 | Note the "Client ID" and "Client Secret." You'll enter them 66 | later. 67 |
  12. 68 |
  13. Click the "Add URI" button to add a new Redirect URI.
  14. 69 |
  15. 70 | Type http://localhost:11114/callback.php and click "Add" 71 |
    72 | A screenshot of the Spotify Developer application data page, with our redirect page ready to be submitted. 75 |
    Just type it in!
    76 |
    77 |
  16. 78 |
  17. 79 | Save those changes! 80 |
    81 | A screenshot of the Spotify Developer application data page showing the save button. 84 |
    The green one, please.
    85 |
    86 |
  18. 87 |
  19. Copy and paste your Client ID and Client Secret below: 88 |
    89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
    Response here
    102 | 103 | 104 | privacy notice 105 |
    106 |
    107 |
    108 | A screenshot of the Spotify Developer application data page showing the locations of the client ID and secret. 111 |
    This will not be a blur on your screen.
    112 |
    113 |
  20. 114 |
  21. Go back to Spotifious and finish your setup!
  22. 115 |
116 |
117 | 118 |
119 |

Privacy

120 |

121 | Spotifious merely reads and writes your keys from and to your 122 | hard drive. They are never used in a way that could compromise 123 | their integrity other than in the Spotify query, as required by 124 | the API. Spotifious itself never sends the data 125 | anywhere else, for any reason, at any time. 126 |

127 |

128 | You do not need to set up a Spotify app to use 129 | Spotifious, but you lose a lot of functionality. If you would 130 | prefer not to use this part of Spotify, 131 | click here. 133 |

134 |
135 |
136 | 137 | 138 | 179 | 180 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Menus/Search.php: -------------------------------------------------------------------------------- 1 | query = $query; 17 | $this->alfred = $alfred; 18 | 19 | $locale = $this->alfred->options('country'); 20 | 21 | // Use the API to fetch, if possible. 22 | $json = ""; 23 | if($api) { 24 | $options = array(); 25 | if($locale != 'not-given') { 26 | $options['market'] = $locale; 27 | } 28 | $json = $api->search($query, ['artist', 'album', 'track'], $options); 29 | } else { 30 | /* Fetch and parse the search results. */ 31 | $urlQuery = urlencode($query); 32 | $url = "https://api.spotify.com/v1/search?q=$urlQuery&type=artist,album,track"; 33 | if($locale != 'not-given') { 34 | $url .= "&market=$locale"; 35 | } 36 | 37 | $fetcher = new JsonFetcher($url); 38 | $json = $fetcher->run(); 39 | } 40 | 41 | // Albums do not include artist data. 42 | // Grab all the album ids, and find their artists 43 | $albumIDs = array(); 44 | foreach ($json->albums->items as $key => $value) { 45 | $albumIDs[] = $value->id; 46 | } 47 | 48 | if(sizeof($albumIDs) != 0) 49 | { 50 | $albumsJson = null; 51 | if($api) { 52 | $albumsJson = $api->getAlbums($albumIDs); 53 | } else { 54 | $urlQuery = str_replace("%3A", ":", urlencode(join(',', $albumIDs))); 55 | $url = "https://api.spotify.com/v1/albums?ids=$urlQuery"; 56 | 57 | $albumFetcher = new JsonFetcher($url); 58 | $albumsJson = $albumFetcher->run(); 59 | } 60 | 61 | $albums = array(); 62 | foreach ($albumsJson->albums as $key => $value) { 63 | $albums[] = array( 64 | 'artist' => $value->artists[0]->name, 65 | 'popularity' => $value->popularity 66 | ); 67 | } 68 | } 69 | 70 | // Build the search results 71 | // for each query type 72 | foreach (array('artist', 'album', 'track') as $type) { 73 | // Create the search results array 74 | foreach ($json->{$type . "s"}->items as $key => $value) { 75 | // Weight popularity 76 | if($type == 'album') { 77 | $popularity = $albums[$key]['popularity']; 78 | } else { 79 | $popularity = $value->popularity; 80 | } 81 | 82 | if($type == 'artist') 83 | $popularity += 50; 84 | 85 | if($type == 'album') 86 | $popularity += 25; 87 | 88 | if ($type == 'track') { 89 | $currentRaw['album'] = $value->album->name; 90 | $currentRaw['album_uri'] = $value->album->uri; 91 | $currentRaw['artist'] = $value->artists[0]->name; 92 | } elseif ($type == 'album') { 93 | $currentRaw['artist'] = $albums[$key]['artist']; 94 | } 95 | 96 | if($type == 'album') { 97 | $currentRaw['type'] = $value->album_type; 98 | } else { 99 | $currentRaw['type'] = $value->type; 100 | } 101 | 102 | $currentRaw['title'] = $value->name; 103 | $currentRaw['popularity'] = $popularity; 104 | $currentRaw['uri'] = $value->uri; 105 | 106 | $this->search[] = $currentRaw; 107 | } 108 | } 109 | 110 | // Add playlists to result 111 | $playlists_json = $this->alfred->options('playlists'); 112 | if($playlists_json !== '') { 113 | $playlists = json_decode($playlists_json); 114 | 115 | foreach ($playlists as $playlist) { 116 | if(!@mb_stristr(mb_strtoupper($playlist->name), mb_strtoupper($query))) { 117 | continue; 118 | } 119 | 120 | $this->search[] = array( 121 | 'type' => 'playlist', 122 | 'uri' => $playlist->uri, 123 | 'title' => $playlist->name, 124 | 'owner' => $playlist->owner, 125 | 'popularity' => 80 126 | ); 127 | } 128 | } 129 | 130 | if(!empty($this->search)) 131 | usort($this->search, array($this, 'popularitySort')); 132 | } 133 | 134 | public function output() { 135 | if(!empty($this->search)) { 136 | foreach ($this->search as $key => $current) { 137 | $popularity = Helper::floatToBars($current['popularity']); 138 | 139 | if ($current['type'] == 'track') { 140 | $subtitle = "$popularity {$current['album']} by {$current['artist']}"; 141 | } elseif ($current['type'] == 'album') { 142 | $subtitle = "$popularity Album by {$current['artist']}"; 143 | } elseif ($current['type'] == 'single') { 144 | $subtitle = "$popularity Single by {$current['artist']}"; 145 | } elseif ($current['type'] == 'playlist') { 146 | $subtitle = "Playlist by {$current['owner']}"; 147 | } else { 148 | $subtitle = "$popularity " . ucfirst($current['type']); 149 | } 150 | 151 | if ($current['type'] == 'track') { 152 | $valid = true; 153 | $arg = "spotify⟩play track \"{$current['uri']}\""; 154 | $autocomplete = ''; 155 | 156 | // $album_json = '{ "action":"spotifious", "command":"test" }'; 157 | 158 | $mods = array( 159 | // 'alt' => array( 160 | // 'subtitle' => "Browse to artist ({$current['artist']})..." 161 | // ), 162 | // 'ctrl' => array( 163 | // 'subtitle' => "Browse to album ({$current['album']})...", 164 | // 'arg' => $album_json, 165 | // ), 166 | 'shift' => array( 167 | 'subtitle' => 'Reveal in Spotify', 168 | 'arg' => "spotify⟩activate (open location \"{$current['uri']}\")" 169 | ) 170 | ); 171 | } else { 172 | $valid = false; 173 | $arg = ''; 174 | $autocomplete = "{$current['uri']} ⟩ {$this->query} ⟩"; 175 | } 176 | 177 | // Modifiers :D 178 | if($current['type'] == 'playlist') { 179 | $mods = array( 180 | 'ctrl' => array( 181 | 'subtitle' => 'Play this playlist', 182 | 'valid' => true, 183 | 'arg' => "spotify⟩play track \"{$current['uri']}\"", 184 | 'autocomplete' => '' 185 | ), 186 | // 'alt' => array(), 187 | // 'cmd' => array( 188 | // 'subtitle' => 'Queue playlist' 189 | // ) 190 | ); 191 | } else if($current['type'] == 'artist') { 192 | $mods = array( 193 | 'ctrl' => array( 194 | 'subtitle' => 'Play this artist', 195 | 'valid' => true, 196 | 'arg' => "spotify⟩play track \"{$current['uri']}\"", 197 | 'autocomplete' => '' 198 | ) 199 | // 'cmd' => array( 'subtitle' => 'Queue this artist' ), 200 | // 'shift' => array( 'subtitle' => 'Open in Spotify' ) 201 | ); 202 | } else if($current['type'] == 'album') { 203 | $mods = array( 204 | 'ctrl' => array( 205 | 'subtitle' => 'Play this album', 206 | 'valid' => true, 207 | 'arg' => "spotify⟩play track \"{$current['uri']}\"", 208 | 'autocomplete' => '' 209 | ) 210 | // 'cmd' => array( 'subtitle' => 'Queue this album' ), 211 | // 'shift' => array( 'subtitle' => 'Open in Spotify' ), 212 | // 'alt' => array('subtitle' => 'Browse to artist'), 213 | ); 214 | } else if($current['type'] == 'single') { 215 | $mods = array( 216 | 'ctrl' => array( 217 | 'subtitle' => 'Play this single', 218 | 'valid' => true, 219 | 'arg' => "spotify⟩play track \"{$current['uri']}\"", 220 | 'autocomplete' => '' 221 | ) 222 | // 'cmd' => array( 'subtitle' => 'Queue this single' ), 223 | // 'shift' => array( 'subtitle' => 'Open in Spotify' ), 224 | // 'alt' => array('subtitle' => 'Browse to artist'), 225 | ); 226 | } else if($current['type'] == 'track') { 227 | // Unimplemented since we have no good way of changing 228 | // autocomplete strings. 229 | $mods = array( 230 | // 'cmd' => array( 'subtitle' => 'Queue this song' ), 231 | // 'shift' => array( 'subtitle' => 'Open in Spotify' ), 232 | // 'alt' => array('subtitle' => 'Browse to artist'), 233 | // 'ctrl' => array( 'subtitle' => 'Browse to album' ), 234 | ); 235 | } 236 | 237 | $currentResult['title'] = $current['title']; 238 | $currentResult['subtitle'] = $subtitle; 239 | $currentResult['uid'] = "bs-spotify-{$this->query}-{$current['type']}-{$current['title']}"; 240 | $currentResult['valid'] = $valid; 241 | $currentResult['arg'] = $arg; 242 | $currentResult['autocomplete'] = $autocomplete; 243 | $currentResult['copy'] = $current['uri']; 244 | $currentResult['icon'] = array('path' => "include/images/{$current['type']}.png"); 245 | if(isset($mods)) { 246 | $currentResult['mods'] = $mods; 247 | } 248 | 249 | $results[] = $currentResult; 250 | } 251 | } 252 | 253 | /* Give the option to continue searching in Spotify because even I know my limits. */ 254 | $results[] = array( 255 | 'title' => "Search for {$this->query}", 256 | 'subtitle' => "Continue this search in Spotify…", 257 | 'uid' => "bs-spotify-{$this->query}-more", 258 | 'arg' => "spotify⟩activate (open location \"spotify:search:{$this->query}\")", 259 | 'icon' => array('path' => 'include/images/search.png') 260 | ); 261 | 262 | return $results; 263 | } 264 | 265 | protected function popularitySort($a, $b) { 266 | if($a['popularity'] == $b['popularity']) 267 | return 0; 268 | 269 | return ($a['popularity'] < $b['popularity']) ? 1 : -1; 270 | } 271 | 272 | protected function contains($stack, $needle) { 273 | return (strpos($stack, $needle) !== false); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /include/setup/style/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /src/citelao/OhAlfred/OhAlfred.php: -------------------------------------------------------------------------------- 1 | name == null) 30 | $this->name = $this->defaults('bundleid'); 31 | 32 | return $this->name; 33 | } 34 | 35 | // Get the current Alfred version 36 | public function version() 37 | { 38 | if($this->version == null) { 39 | if(!getenv("alfred_version")) { 40 | $this->version = "2"; 41 | } else { 42 | $this->version = getenv("alfred_version"); 43 | } 44 | } 45 | 46 | return $this->version; 47 | } 48 | 49 | // Helper for Alfred 3 50 | public function isAlfred3() 51 | { 52 | return $this->version()[0] === "3"; 53 | } 54 | 55 | // Get the user's home directory. 56 | public function home() 57 | { 58 | if($this->home == null) 59 | $this->home = exec('printf "$HOME"'); 60 | 61 | return $this->home; 62 | } 63 | 64 | // Get the workflow directory. 65 | public function workflow() 66 | { 67 | if($this->workflow == null) 68 | $this->workflow = dirname(dirname(dirname(__DIR__))) . '/'; // Because I keep OhAlfred in the src/citelao/OhAlfred directory. 69 | // TODO make portable 70 | 71 | return $this->workflow; 72 | } 73 | 74 | // Get the cache directory 75 | public function cache() { 76 | if($this->cache == null) { 77 | if(getenv('alfred_workflow_cache')) { 78 | $this->cache = getenv('alfred_workflow_cache') . "/"; 79 | } else { 80 | $this->cache = $this->home() . "/Library/Caches/com.runningwithcrayons.Alfred-2/Workflow Data/" . $this->name() . "/"; 81 | } 82 | } 83 | 84 | if (!file_exists($this->cache)) { 85 | mkdir($this->cache); 86 | } 87 | 88 | return $this->cache; 89 | } 90 | 91 | // Get the storage directory 92 | public function storage() { 93 | if($this->storage == null) { 94 | if(getenv('alfred_workflow_data')) { 95 | $this->storage = getenv('alfred_workflow_data') . "/"; 96 | } else { 97 | $this->storage = $this->home() . "/Library/Application Support/Alfred 2/Workflow Data/" . $this->name() . "/"; 98 | } 99 | } 100 | 101 | if (!file_exists($this->storage)) { 102 | mkdir($this->storage); 103 | } 104 | 105 | return $this->storage; 106 | } 107 | 108 | /** 109 | * Both `defaults` and `options` are inspired by jdfwarrior's PHP workflow for Alfred. 110 | * Though I cited him at the beginning of this class, the plist method of setting 111 | * storage I pulled from his workflow. 112 | **/ 113 | 114 | // Read an arbitrary plist setting. 115 | public function plist($plist, $setting, $value = -1) { 116 | if ($value === -1) { 117 | if(!array_key_exists($plist, $this->plists)) { 118 | $json = exec("plutil -convert json -o - '{$plist}.plist'"); 119 | $decoded = json_decode($json); 120 | 121 | if(json_last_error() !== JSON_ERROR_NONE) { 122 | return false; 123 | } 124 | 125 | $this->plists[$plist] = $decoded; 126 | } 127 | 128 | if(!property_exists($this->plists[$plist], $setting)) { 129 | return false; 130 | } 131 | 132 | return $this->plists[$plist]->{$setting}; 133 | } 134 | 135 | // Uncache plist on write 136 | unset($this->plists[$plist]); 137 | 138 | if(!is_string($value)) { 139 | $value = '"' . addslashes(json_encode($value)) . '"'; 140 | } 141 | 142 | $escaped_plist = escapeshellarg($plist); 143 | $escaped_setting = escapeshellarg($setting); 144 | $escaped_value = escapeshellarg($value); 145 | // print("defaults write $escaped_plist $escaped_setting $escaped_value"); 146 | return exec("defaults write $escaped_plist $escaped_setting $escaped_value"); 147 | } 148 | 149 | // Read the workflow .plist file. 150 | public function defaults($setting, $value = -1) { 151 | return $this->plist($this->workflow() . "info", $setting, $value); 152 | } 153 | 154 | // Read a custom workflow options .plist file. 155 | public function options($setting, $value = -1) { 156 | $options = $this->storage() . "settings"; 157 | $optionsFile = $options . ".plist"; 158 | 159 | if(!file_exists($optionsFile)) 160 | touch($optionsFile); 161 | 162 | return $this->plist($options, $setting, $value); 163 | } 164 | 165 | // Create and return the XML output as a string. 166 | public function alfredify($r = null) { 167 | if($r == null) 168 | $r = $this->results; 169 | 170 | $output = ""; 171 | if($this->isAlfred3()) { 172 | $output = $this->jsonify($r); 173 | } else { 174 | $output = $this->xmlify($r); 175 | } 176 | 177 | print $output; 178 | } 179 | 180 | protected function jsonify($results) { 181 | $output = array('items' => []); 182 | 183 | foreach ($results as $result) { 184 | $item = $result; 185 | 186 | // Meantime checks 187 | if(isset($item['icon']) && is_string($item['icon'])) { 188 | $icon = array('path' => $item['icon']); 189 | $item['icon'] = $icon; 190 | throw new StatefulException("Expected array for icon, not string"); 191 | } 192 | 193 | if(isset($item['valid']) && is_string($item['valid'])) { 194 | $item['valid'] = $item['valid'] === 'yes'; 195 | throw new StatefulException("Expected boolean for validity, not string"); 196 | } 197 | 198 | if(isset($item['copy']) && is_string($item['copy'])) { 199 | $item['text'] = array('copy' => $item['copy']); 200 | // TODO purge the copy tag! 201 | } 202 | 203 | $output['items'][] = $item; 204 | } 205 | 206 | return json_encode($output); 207 | } 208 | 209 | protected function xmlify($results) { 210 | $output = "\r\n"; 211 | 212 | foreach($results as $result) { 213 | if(!isset($result['arg'])) 214 | $result['arg'] = 'null'; 215 | 216 | if(!isset($result['title'])) 217 | $result['title'] = 'null'; 218 | 219 | if(!isset($result['icon'])) 220 | $result['icon'] = 'icon.png'; 221 | 222 | if(!is_string($result['icon'])) { 223 | $icon = $result['icon']['path']; 224 | $result['icon'] = $icon; 225 | } 226 | 227 | if(!isset($result['valid'])) 228 | $result['valid'] = 'yes'; 229 | 230 | if(!is_string($result['valid'])) { 231 | $result['valid'] = ($result['valid']) ? 'yes' : 'no'; 232 | } 233 | 234 | if(!isset($result['uid'])) 235 | $result['uid'] = time() . "-" . $result['title']; 236 | 237 | if(!isset($result['autocomplete'])) 238 | $result['autocomplete'] = ''; 239 | 240 | if(!isset($result['subtitle'])) 241 | $result['subtitle'] = ''; 242 | 243 | $output .= "\r\n\r\n"; 244 | $output .= " \r\n"; 245 | $output .= " " . $result['arg'] . "\r\n"; 246 | $output .= " " . $this->escapeQuery($result['title']) . "\r\n"; 247 | $output .= " " . $this->escapeQuery($result['subtitle']) . "\r\n"; 248 | $output .= " " . $this->escapeQuery($result['icon']) . "\r\n"; 249 | 250 | if(isset($result['copy'])) { 251 | $output .= " " . $this->escapeQuery($result['copy']) . "\r\n"; 252 | } 253 | 254 | $output .= " \r\n"; 255 | } 256 | 257 | $output .= ""; 258 | return $output; 259 | } 260 | 261 | // Replace some symbols that confuse Alfred. 262 | public function escapeQuery($text) { 263 | $text = str_replace("&", "&", $text); 264 | $text = str_replace("'", "'", $text); 265 | 266 | return $text; 267 | } 268 | 269 | // Change an exception into Alfred-displayable XML. 270 | public function exceptionify($error, $should_die = true) { 271 | if(method_exists($error, 'getState')) { 272 | $state = array_merge($error->getState(), $error->getTrace()); 273 | } else { 274 | $state = $error->getTrace(); 275 | } 276 | 277 | $this->errorify(get_class($error), $error->getMessage(), $error->getFile(), $error->getLine(), $state, $should_die); 278 | } 279 | 280 | // Change an error into Alfred-displayable XML 281 | public function errorify($number, $message, $file, $line, $context, $should_die = true) { 282 | $titles = array('Aw, jeez!', 'Dagnabit!', 'Crud!', 'Whoops!', 'Oh, snap!', 'Aw, fiddlesticks!', 'Goram it!'); 283 | 284 | $fdir = $this->loggifyError($number, $message, $file, $line, $context); 285 | 286 | $results = array( 287 | array( 288 | 'title' => $titles[array_rand($titles)], 289 | 'subtitle' => "Something went haywire. You can continue using Spotifious.", 290 | 'valid' => false, 291 | 'icon' => array('path' => 'include/images/error.png') 292 | ), 293 | 294 | array( 295 | 'title' => $message, 296 | 'subtitle' => "Line " . $line . ", " . $file, 297 | 'valid' => false, 298 | 'icon' => array('path' => 'include/images/info.png') 299 | ), 300 | 301 | array( 302 | 'title' => "View log", 303 | 'subtitle' => "Open new Finder window with .log file.", 304 | 'icon' => array('path' => 'include/images/folder.png'), 305 | 'arg' => $fdir 306 | ) 307 | ); 308 | 309 | $this->alfredify($results); 310 | 311 | if($should_die) { 312 | die(); 313 | } 314 | } 315 | 316 | // Write a log file of an error. 317 | protected function loggifyError($number, $message, $file, $line, $context) { 318 | // Write contents of log file. 319 | $fcontents = "# Error Log # \n"; 320 | 321 | $fcontents .= "## Error Info ## \n"; 322 | $fcontents .= $message . "\n"; 323 | $fcontents .= "Line " . $line . ", " . $file . "\n\n"; 324 | 325 | $fcontents .= "## Symbols ## \n"; 326 | $fcontents .= print_r($context, true) . "\n"; 327 | $fcontents .= "\n\n"; 328 | 329 | // Delay storing of error 'till contents are fully generated. 330 | $errordir = $this->cache(); 331 | $fname = date('Y-m-d h-m-s') . " Spotifious.log"; 332 | $fdir = $errordir . $fname; 333 | 334 | $log = fopen($fdir, "w"); 335 | fwrite($log, $fcontents); 336 | fclose($log); 337 | 338 | return $fdir; 339 | } 340 | 341 | public function notify($message, $title = '', $subtitle = '', $appIcon = '', $contentImage = '', $open = '') { 342 | if($this->options('track_notifications') != 'true') { 343 | return; 344 | } 345 | 346 | $command = "include/terminal-notifier.app/Contents/MacOS/terminal-notifier "; 347 | 348 | $command .= "-message " . $this->escapeNotify($message) . " "; 349 | $command .= "-group ohAlfredNotifications "; 350 | 351 | if($title) 352 | $command .= "-title " . $this->escapeNotify($title) . " "; 353 | 354 | if($subtitle) 355 | $command .= "-subtitle " . $this->escapeNotify($subtitle) . " "; 356 | 357 | if($appIcon) 358 | $command .= "-appIcon " . $this->escapeNotify($appIcon) . " "; 359 | 360 | if($contentImage) 361 | $command .= "-contentImage " . $this->escapeNotify($contentImage) . " "; 362 | 363 | if($open) 364 | $command .= "-open " . $this->escapeNotify($open) . " "; 365 | 366 | exec($command); 367 | } 368 | 369 | protected function escapeNotify($string) { 370 | if(!preg_match('/[a-zA-Z0-9]/', $string[0])) { 371 | $string = '\\' . $string; 372 | } 373 | 374 | return escapeshellarg($string); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/citelao/Spotifious/Spotifious.php: -------------------------------------------------------------------------------- 1 | alfred = $alfred; 29 | } 30 | 31 | public function run($query) { 32 | // Correct for old Spotifious queries 33 | $q = str_replace("►", "⟩", $query); 34 | 35 | // Set defaults if they've not been set before 36 | if($this->alfred->options('track_notifications') == '') { 37 | $this->alfred->options('track_notifications', 'true'); 38 | } 39 | 40 | $scopes = 'playlist-read-private'; 41 | if($this->alfred->options('desired_scopes') != $scopes) { 42 | $this->alfred->options('desired_scopes', $scopes); 43 | } 44 | 45 | if($this->alfred->options('lookup_current_song') == '') { 46 | $this->alfred->options('lookup_current_song', 'true'); 47 | } 48 | 49 | if($this->alfred->options('has_installed_spotify') == '' || $this->alfred->options('has_installed_spotify') == 'false') { 50 | $detector = new Command('open -Ra "Spotify"'); 51 | $detector->run(); 52 | $spotify_installed = !$detector->status(); 53 | $this->alfred->options('has_installed_spotify', $spotify_installed); 54 | } 55 | 56 | // Display the setup menu if the app isn't setup. 57 | // Or the "options" menu if the S key is pressed 58 | if($this->alfred->options('country') == '' || 59 | $this->alfred->options('has_installed_spotify') == 'false' || 60 | $this->alfred->options('spotify_client_id') == '' || 61 | $this->alfred->options('spotify_secret') == '' || 62 | $this->optedOut() || 63 | $this->alfred->options('spotify_access_token') == '' || 64 | $this->alfred->options('spotify_access_token_expires') == '' || 65 | $this->alfred->options('spotify_refresh_token') == '' || 66 | $this->alfred->options('desired_scopes') != $this->alfred->options('registered_scopes') || 67 | $this->contains($query, "Country Code ⟩")) { 68 | 69 | // Check version first 70 | if(version_compare(phpversion(), "5.4") < 0) { 71 | throw new StatefulException("Your PHP version is too low", 72 | array( 73 | "Your version" => phpversion(), 74 | "Required version" => "5.4" 75 | )); 76 | } 77 | 78 | // If we are trying to configure country code 79 | if($this->contains($query, "Country Code ⟩")) { 80 | $menu = new SetupCountryCode($query); 81 | return $menu->output(); 82 | } 83 | 84 | $menu = new Setup($query, $this->alfred); 85 | return $menu->output(); 86 | } 87 | 88 | // Don't bother connecting if we've opted out :). 89 | $api = null; 90 | if(!$this->optedOut()) { 91 | $api = $this->getSpotifyApi(); 92 | } 93 | // TODO 94 | // if expired 95 | // attempt refresh 96 | // if failed, prompt for relogin 97 | 98 | // Fetch playlists on a first run... 99 | if($api && $this->alfred->options('playlists') == '') { 100 | $this->update_playlists_cache($api); 101 | } 102 | 103 | if (mb_strlen($query) <= 3) { 104 | if(mb_strlen($query) > 0 && ($query[0] == "c" || $query[0] == "C")) { 105 | $menu = new Control($query); 106 | return $menu->output(); 107 | } elseif(mb_strlen($query) > 0 && ($query[0] == "s" || $query[0] == "S")) { 108 | $menu = new Settings($query, $this->alfred, $api); 109 | return $menu->output(); 110 | } else { 111 | $menu = new Main($query); 112 | return $menu->output(); 113 | } 114 | 115 | } elseif ($this->contains($query, '⟩')) { 116 | // if the query contains any machine-generated text 117 | // (the unicode `⟩` is untypeable so we check for it) 118 | // we need to parse the query and extract the URLs. 119 | 120 | // So split based on the delimeter `⟩` and excise the delimeter and blanks. 121 | $splitQuery = array_filter(str_replace("⟩", "", explode("⟩", $query))); 122 | array_walk($splitQuery, array($this, 'trim_value')); 123 | 124 | $URIs = array_filter($splitQuery, array($this, 'is_spotify_uri')); 125 | $args = array_diff($splitQuery, $URIs); 126 | 127 | // Find which URI to use (by count, not by array index). 128 | // Arrows should be twice the number of URIs for the last URI. 129 | // For every one arrow fewer, traverse one URI backwards. 130 | $arrows = mb_substr_count($query, "⟩"); 131 | $depth = count($URIs) - (2 * count($URIs) - $arrows); // equiv to $arrows - count($URIs). 132 | 133 | $options = array( 134 | 'depth' => $depth, 135 | 'URIs' => $URIs, 136 | 'args' => $args, 137 | 'search' => '', 138 | 'query' => $query 139 | ); 140 | 141 | if (mb_substr($query, -1) == "⟩") { // Machine-generated 142 | $menu = new Detail($options, $this->alfred, $api); 143 | return $menu->output(); 144 | 145 | } elseif($depth > 0) { 146 | $search = array_pop($args); 147 | $options['search'] = $search; 148 | $options['args'] = $args; 149 | 150 | $menu = new Detail($options, $this->alfred, $api); 151 | return $menu->output(); 152 | 153 | } else { 154 | $menu = new Search(end($args), $this->alfred, $api); 155 | return $menu->output(); 156 | } 157 | 158 | } else { 159 | $menu = new Search($query, $this->alfred, $api); 160 | $results = $menu->output(); 161 | 162 | if(mb_strlen($query) > 0 && ($query[0] == "c" || $query[0] == "C")) { 163 | $controlMenu = new Control($query); 164 | $results = array_merge($controlMenu->output(), $results); 165 | } 166 | 167 | return $results; 168 | } 169 | } 170 | 171 | public function process($action) { 172 | $api = null; 173 | if($this->alfred->options('spotify_access_token') && !$this->optedOut()) { 174 | $api = $this->getSpotifyApi(); 175 | } 176 | // TODO refresh token 177 | // this is identical code to run() 178 | // if expired 179 | // attempt refresh 180 | // if failed, prompt for relogin 181 | 182 | // Handle JSON if given 183 | 184 | // print_r("GGGG"); 185 | if($action[0] == "{") { 186 | $json = JsonParser::parse($action); 187 | $options = (isset($json->options)) 188 | ? $json->options 189 | : new \stdClass(); 190 | 191 | $action = null; 192 | if($json->action == "applescript") { 193 | $action = new Applescript($options, $this->alfred, $api); 194 | } else if($json->action == "command") { 195 | if(!isset($options->command)) { 196 | throw new StatefulException("You have to have a command to run!"); 197 | } 198 | $action = new Command($options->command); 199 | } else if($json->action == "spotifious") { 200 | $v = $this->alfred->version()[0]; 201 | $command = ($options->command) 202 | ? $options->command 203 | : ''; 204 | $passed_options = (object) array( 205 | 'application' => "Alfred $v", 206 | 'command' => 'run trigger "search" in workflow "com.citelao.spotifious" with argument "' . $command . '"' 207 | ); 208 | $action = new Applescript($passed_options, $this->alfred, $api); 209 | } else { 210 | throw new StatefulException("Could not process command", array('json' => $json)); 211 | } 212 | $action->run(); 213 | return; 214 | } 215 | 216 | if($this->contains($action, '⟩')) { 217 | $splitAction = explode('⟩', $action); 218 | $command = array_shift($splitAction); 219 | 220 | if($command == 'country') { 221 | $this->alfred->options('country', $splitAction[0]); 222 | 223 | $v = $this->alfred->version()[0]; 224 | $as = new ApplicationApplescript("Alfred $v", 'run trigger "search" in workflow "com.citelao.spotifious"'); 225 | $as->run(); 226 | 227 | } else if($command == 'appsetup') { 228 | // Autokill server in 10 minutes 229 | $server = new Timeout(10 * 60, "php -S localhost:11114 & open 'http://localhost:11114/include/setup/index.php'"); 230 | $server->run(); 231 | 232 | } else if($command == 'applink') { 233 | // Autokill server in 10 minutes 234 | $server = new Timeout(10 * 60, "php -S localhost:11114 & open 'http://localhost:11114/include/setup/link.php'"); 235 | $server->run(); 236 | 237 | } else if($command == 'togglenotifications') { 238 | $current = $this->alfred->options('track_notifications'); 239 | 240 | if($current == 'true') { 241 | $this->alfred->options('track_notifications', 'false'); 242 | } else { 243 | $this->alfred->options('track_notifications', 'true'); 244 | } 245 | 246 | } else if($command == 'update_playlists_cache') { 247 | if($api) { 248 | $this->update_playlists_cache($api); 249 | } 250 | 251 | } else if($command == 'next') { 252 | $song = $this->respondingSpotifyQuery('next track'); 253 | 254 | $this->alfred->notify( 255 | $song['album'] . " — " . $song['artist'], 256 | $song['title'], 257 | // $song['url'], 258 | "", 259 | "", 260 | "", 261 | $song['url']); 262 | 263 | } else if($command == 'previous') { 264 | $song = $this->respondingSpotifyQuery('previous track'); 265 | 266 | $this->alfred->notify( 267 | $song['album'] . " — " . $song['artist'], 268 | $song['title'], 269 | // $song['url'], 270 | "", 271 | "", 272 | "", 273 | $song['url']); 274 | 275 | } else if($command == 'playpause') { 276 | $song = $this->respondingSpotifyQuery('playpause'); 277 | 278 | $icon = ($song['state'] == "playing") ? "▶" : "‖"; 279 | $this->alfred->notify( 280 | $song['album'] . " — " . $song['artist'], 281 | $icon . " " . $song['title'], 282 | // $song['url'], 283 | "", 284 | "", 285 | "", 286 | $song['url']); 287 | 288 | } else if($command == 'volup') { 289 | $as = new ApplicationApplescript("Spotify", "if sound volume < 90 then \n set sound volume to sound volume + 10 \n else \n set sound volume to 100 \n end if"); 290 | $as->run(); 291 | 292 | } else if($command == 'voldown') { 293 | $as = new ApplicationApplescript("Spotify", "if sound volume > 10 then \n set sound volume to sound volume - 10 \n else \n set sound volume to 0 \n end if"); 294 | $as->run(); 295 | 296 | } else if($command == 'spotify') { 297 | $as = new ApplicationApplescript("Spotify", $splitAction[0]); 298 | $as->run(); 299 | } else if($command == 'respond') { 300 | $song = $this->splitSpotifyResponse($splitAction[0]); 301 | 302 | $icon = ($song['state'] == "playing") ? "▶" : "‖"; 303 | $this->alfred->notify( 304 | $song['album'] . " — " . $song['artist'], 305 | $icon . " " . $song['title'], 306 | // $song['url'], 307 | "", 308 | "", 309 | "", 310 | $song['url']); 311 | } 312 | 313 | $v = $this->alfred->version()[0]; 314 | if($splitAction[0] && $splitAction[0] == 'return') { 315 | $as = new ApplicationApplescript("Alfred $v", 'run trigger "search" in workflow "com.citelao.spotifious"'); 316 | $as->run(); 317 | } elseif($splitAction[0] && $splitAction[0] == 'returnControls') { 318 | $as = new ApplicationApplescript("Alfred $v", 'run trigger "search" in workflow "com.citelao.spotifious" with argument "c"'); 319 | $as->run(); 320 | 321 | } 322 | } else { 323 | return "Could not process command!"; 324 | } 325 | } 326 | 327 | protected function contains($stack, $needle) { 328 | return (strpos($stack, $needle) !== false); 329 | } 330 | 331 | protected function trim_value(&$value) { 332 | $value = trim($value); 333 | } 334 | 335 | // TODO cite 336 | protected function is_spotify_uri($item) { 337 | $regex = '/^(spotify:(?:album|artist|track|(?:user:[^:]+:)?playlist):[a-zA-Z0-9]+)$/x'; 338 | 339 | return preg_match($regex, $item); 340 | } 341 | 342 | protected function respondingSpotifyQuery($query) { 343 | $as = new ApplicationApplescript("Spotify", $query . " \n return name of current track & \"✂\" & album of current track & \"✂\" & artist of current track & \"✂\" & spotify url of current track & \"✂\" & player state"); 344 | $result = $as->run(); 345 | 346 | return $this->splitSpotifyResponse($result); 347 | } 348 | 349 | protected function splitSpotifyResponse($resp) { 350 | $array = explode("✂", $resp); 351 | if($array[0] == "") { 352 | $array[0] = "No track playing"; 353 | $array[1] = "No album"; 354 | $array[2] = "No artist"; 355 | $array[3] = ""; 356 | $array[4] = "paused"; 357 | } 358 | 359 | $data = array( 360 | 'title' => $array[0], 361 | 'album' => $array[1], 362 | 'artist' => $array[2], 363 | 'url' => $array[3], 364 | 'state' => $array[4] 365 | ); 366 | 367 | return $data; 368 | } 369 | 370 | protected function getSpotifyApi() { 371 | if($this->optedOut()) { 372 | throw new StatefulException("Trying to get API for opted out user. My bad."); 373 | } 374 | 375 | $api = new SpotifyWebAPI(); 376 | 377 | // If the access token has expired :( 378 | if ($this->alfred->options('spotify_access_token_expires') < time() + 60*60*3) { 379 | $session = new Session($this->alfred->options('spotify_client_id'), $this->alfred->options('spotify_secret'), 'http://localhost:11114/callback.php'); 380 | $session->refreshAccessToken($this->alfred->options('spotify_refresh_token')); 381 | 382 | $this->alfred->options('spotify_access_token_expires', $session->getTokenExpiration()); 383 | $this->alfred->options('spotify_access_token', $session->getAccessToken()); 384 | } 385 | 386 | $api->setAccessToken($this->alfred->options('spotify_access_token')); 387 | 388 | return $api; 389 | } 390 | 391 | protected function optedOut() { 392 | return $this->alfred->options('spotify_app_opt_out') == 'true'; 393 | } 394 | 395 | protected function update_playlists_cache($api) { 396 | $playlists = $api->getMyPlaylists(); 397 | $search_data = array(); 398 | foreach ($playlists->items as $playlist) { 399 | $search_data[] = array( 400 | 'id' => $playlist->id, 401 | 'name' => $playlist->name, 402 | 'uri' => $playlist->uri, 403 | 'owner' => ( 404 | property_exists($playlist->owner, 'display_name') 405 | ? $playlist->owner->display_name 406 | : property_exists($playlist->owner, 'id') 407 | ) ? $playlist->owner->id : 'unknown' 408 | ); 409 | } 410 | $datetime = new \DateTime("now"); 411 | $this->alfred->options('playlists_cache_date', $datetime->format("@U")); 412 | $this->alfred->options('playlists', $search_data); 413 | } 414 | } 415 | -------------------------------------------------------------------------------- /info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | bundleid 6 | com.citelao.spotifious 7 | category 8 | Productivity 9 | connections 10 | 11 | 18496AC4-EA42-444C-A906-642E38259C96 12 | 13 | 14 | destinationuid 15 | C8A768AF-C5D9-47A1-B499-8B0D3C844A99 16 | modifiers 17 | 0 18 | modifiersubtext 19 | 20 | vitoclose 21 | 22 | 23 | 24 | destinationuid 25 | 6B1397DB-E737-4607-8181-27D9DFE70E2C 26 | modifiers 27 | 0 28 | modifiersubtext 29 | 30 | vitoclose 31 | 32 | 33 | 34 | destinationuid 35 | 2EA4FBDD-4717-4E37-9067-C93FE29A48F4 36 | modifiers 37 | 0 38 | modifiersubtext 39 | 40 | vitoclose 41 | 42 | 43 | 44 | 2EA4FBDD-4717-4E37-9067-C93FE29A48F4 45 | 46 | 47 | destinationuid 48 | 605E53E5-EBF3-4D6E-A934-DA94EC49C1CF 49 | modifiers 50 | 0 51 | modifiersubtext 52 | 53 | vitoclose 54 | 55 | 56 | 57 | 30E2AA18-937F-4B5D-B685-7852B9717F34 58 | 59 | 60 | destinationuid 61 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 62 | modifiers 63 | 0 64 | modifiersubtext 65 | 66 | vitoclose 67 | 68 | 69 | 70 | 4BB22960-369A-4674-AF29-00CCE1603B53 71 | 72 | 73 | destinationuid 74 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 75 | modifiers 76 | 0 77 | modifiersubtext 78 | 79 | vitoclose 80 | 81 | 82 | 83 | 5610E60C-68BF-4445-BAE3-CB55DEA70D99 84 | 85 | 86 | destinationuid 87 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 88 | modifiers 89 | 0 90 | modifiersubtext 91 | 92 | vitoclose 93 | 94 | 95 | 96 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 97 | 98 | 99 | destinationuid 100 | 6EECFF75-AE32-44F5-97CB-3AE51C7E6A22 101 | modifiers 102 | 0 103 | modifiersubtext 104 | 105 | vitoclose 106 | 107 | 108 | 109 | 695822FE-205D-4684-86EC-3F7B7E8606DC 110 | 111 | 112 | destinationuid 113 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 114 | modifiers 115 | 0 116 | modifiersubtext 117 | 118 | vitoclose 119 | 120 | 121 | 122 | 6B1397DB-E737-4607-8181-27D9DFE70E2C 123 | 124 | 6EECFF75-AE32-44F5-97CB-3AE51C7E6A22 125 | 126 | 127 | destinationuid 128 | DBF28452-DD3D-4A25-AB98-89E268566DD5 129 | modifiers 130 | 0 131 | modifiersubtext 132 | 133 | vitoclose 134 | 135 | 136 | 137 | 73FAFE96-4B16-4AB5-B181-229B4BB79E25 138 | 139 | 140 | destinationuid 141 | EC859C21-FCFF-4949-B767-726A7B37DAE8 142 | modifiers 143 | 0 144 | modifiersubtext 145 | 146 | vitoclose 147 | 148 | 149 | 150 | 7858554D-2173-462A-B03D-1078EC7059D6 151 | 152 | 153 | destinationuid 154 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 155 | modifiers 156 | 0 157 | modifiersubtext 158 | 159 | vitoclose 160 | 161 | 162 | 163 | 8020A500-17C9-43BA-B17A-00D35E460EB6 164 | 165 | 166 | destinationuid 167 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 168 | modifiers 169 | 0 170 | modifiersubtext 171 | 172 | vitoclose 173 | 174 | 175 | 176 | 86EAA0E6-6086-4BC4-8CE7-0673F392C77F 177 | 178 | 179 | destinationuid 180 | 18496AC4-EA42-444C-A906-642E38259C96 181 | modifiers 182 | 0 183 | modifiersubtext 184 | 185 | vitoclose 186 | 187 | 188 | 189 | AE6160D1-56EA-4416-991E-CF12B874FFD3 190 | 191 | 192 | destinationuid 193 | 18496AC4-EA42-444C-A906-642E38259C96 194 | modifiers 195 | 0 196 | modifiersubtext 197 | 198 | vitoclose 199 | 200 | 201 | 202 | B02E0659-E11A-4746-ADB7-2C2C0A8BD8BB 203 | 204 | 205 | destinationuid 206 | 695822FE-205D-4684-86EC-3F7B7E8606DC 207 | modifiers 208 | 0 209 | modifiersubtext 210 | 211 | vitoclose 212 | 213 | 214 | 215 | BFFC58C1-626F-4091-A0A4-61034DDD10D0 216 | 217 | 218 | destinationuid 219 | 695822FE-205D-4684-86EC-3F7B7E8606DC 220 | modifiers 221 | 0 222 | modifiersubtext 223 | 224 | vitoclose 225 | 226 | 227 | 228 | C5E43F66-AD1D-4D8D-9E4A-D7855C8F5AE2 229 | 230 | 231 | destinationuid 232 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 233 | modifiers 234 | 0 235 | modifiersubtext 236 | 237 | vitoclose 238 | 239 | 240 | 241 | EC859C21-FCFF-4949-B767-726A7B37DAE8 242 | 243 | 244 | destinationuid 245 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 246 | modifiers 247 | 0 248 | modifiersubtext 249 | 250 | vitoclose 251 | 252 | 253 | 254 | F1B61D14-D686-468D-9373-AB4FC3EEE3C8 255 | 256 | 257 | destinationuid 258 | EC859C21-FCFF-4949-B767-726A7B37DAE8 259 | modifiers 260 | 0 261 | modifiersubtext 262 | 263 | vitoclose 264 | 265 | 266 | 267 | F4A2A879-4C5A-4532-BF35-02BAAD863581 268 | 269 | 270 | destinationuid 271 | 18496AC4-EA42-444C-A906-642E38259C96 272 | modifiers 273 | 0 274 | modifiersubtext 275 | 276 | vitoclose 277 | 278 | 279 | 280 | 281 | createdby 282 | Ben Stolovitz 283 | description 284 | A natural Spotify controller for Alfred 285 | disabled 286 | 287 | name 288 | Spotifious 289 | objects 290 | 291 | 292 | config 293 | 294 | argument 295 | '{query}', {allvars} 296 | cleardebuggertext 297 | 298 | processoutputs 299 | 300 | 301 | type 302 | alfred.workflow.utility.debug 303 | uid 304 | 7986333C-6D0E-4D95-8EC9-D3AEECD2D2FA 305 | version 306 | 1 307 | 308 | 309 | config 310 | 311 | argument 312 | 313 | argumenttype 314 | 0 315 | triggerid 316 | com.citelao.spotifious.search 317 | triggername 318 | Search 319 | workflowonly 320 | 321 | 322 | type 323 | alfred.workflow.trigger.remote 324 | uid 325 | 86EAA0E6-6086-4BC4-8CE7-0673F392C77F 326 | version 327 | 1 328 | 329 | 330 | type 331 | alfred.workflow.action.revealfile 332 | uid 333 | C8A768AF-C5D9-47A1-B499-8B0D3C844A99 334 | version 335 | 1 336 | 337 | 338 | config 339 | 340 | alfredfiltersresults 341 | 342 | alfredfiltersresultsmatchmode 343 | 0 344 | argumenttrimmode 345 | 0 346 | argumenttype 347 | 1 348 | escaping 349 | 62 350 | keyword 351 | spotifious 352 | queuedelaycustom 353 | 3 354 | queuedelayimmediatelyinitially 355 | 356 | queuedelaymode 357 | 1 358 | queuemode 359 | 2 360 | runningsubtext 361 | Getting Spotifious data... 362 | script 363 | # Add Homebrew to path for those who need :) 364 | export PATH="/usr/local/bin:${PATH}" 365 | 366 | php -f main.php -- "{query}" 367 | 368 | # In case you are experiencing unsolveable issues, 369 | # you can generate a log file to help me. Modify the 370 | # first line of this script so it looks like this: 371 | # php -f main.php -- "{query}" | tee ~/Desktop/Spotifious.log 372 | scriptargtype 373 | 0 374 | scriptfile 375 | 376 | subtext 377 | Search for artists, tracks, or albums 378 | title 379 | Search Spotify for tracks, artists, and albums 380 | type 381 | 0 382 | withspace 383 | 384 | 385 | type 386 | alfred.workflow.input.scriptfilter 387 | uid 388 | 18496AC4-EA42-444C-A906-642E38259C96 389 | version 390 | 2 391 | 392 | 393 | config 394 | 395 | action 396 | 0 397 | argument 398 | 0 399 | focusedappvariable 400 | 401 | focusedappvariablename 402 | 403 | hotkey 404 | 36 405 | hotmod 406 | 1310720 407 | hotstring 408 | 409 | leftcursor 410 | 411 | modsmode 412 | 0 413 | relatedAppsMode 414 | 0 415 | 416 | type 417 | alfred.workflow.trigger.hotkey 418 | uid 419 | AE6160D1-56EA-4416-991E-CF12B874FFD3 420 | version 421 | 2 422 | 423 | 424 | config 425 | 426 | concurrently 427 | 428 | escaping 429 | 4 430 | script 431 | # Add Homebrew to path for those who need :) 432 | export PATH="/usr/local/bin:${PATH}" 433 | 434 | php -f action.php -- "{query}" 435 | 436 | # In case you are experiencing unsolveable issues, 437 | # you can generate a log file to help me. Modify the 438 | # first line of this script so it looks like this: 439 | # php -f action.php -- "{query}" | tee ~/Desktop/SpotifiousAction.log 440 | scriptargtype 441 | 0 442 | scriptfile 443 | 444 | type 445 | 0 446 | 447 | type 448 | alfred.workflow.action.script 449 | uid 450 | 6B1397DB-E737-4607-8181-27D9DFE70E2C 451 | version 452 | 2 453 | 454 | 455 | config 456 | 457 | triggerid 458 | search 459 | 460 | type 461 | alfred.workflow.trigger.external 462 | uid 463 | F4A2A879-4C5A-4532-BF35-02BAAD863581 464 | version 465 | 1 466 | 467 | 468 | config 469 | 470 | inputstring 471 | {var:keepalive} 472 | matchcasesensitive 473 | 474 | matchmode 475 | 1 476 | matchstring 477 | true 478 | 479 | type 480 | alfred.workflow.utility.filter 481 | uid 482 | 2EA4FBDD-4717-4E37-9067-C93FE29A48F4 483 | version 484 | 1 485 | 486 | 487 | type 488 | alfred.workflow.utility.hidealfred 489 | uid 490 | 605E53E5-EBF3-4D6E-A934-DA94EC49C1CF 491 | version 492 | 1 493 | 494 | 495 | config 496 | 497 | applescript 498 | on alfred_script(q) 499 | return run script "tell application \"Spotify\"" & return & q & return & "delay 0.05" & return & "name of current track & \"✂\" & album of current track & \"✂\" & artist of current track & \"✂\" & spotify url of current track & \"✂\" & player state" & return & "end tell" 500 | end alfred_script 501 | cachescript 502 | 503 | 504 | type 505 | alfred.workflow.action.applescript 506 | uid 507 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 508 | version 509 | 1 510 | 511 | 512 | config 513 | 514 | argument 515 | playpause 516 | argumenttype 517 | 3 518 | triggerid 519 | com.citelao.spotifious.playpause 520 | triggername 521 | Play/Pause 522 | workflowonly 523 | 524 | 525 | type 526 | alfred.workflow.trigger.remote 527 | uid 528 | 8020A500-17C9-43BA-B17A-00D35E460EB6 529 | version 530 | 1 531 | 532 | 533 | config 534 | 535 | concurrently 536 | 537 | escaping 538 | 102 539 | script 540 | # Add Homebrew to path for those who need :) 541 | export PATH="/usr/local/bin:${PATH}" 542 | 543 | php -f action.php -- "respond⟩{query}" 544 | 545 | # In case you are experiencing unsolveable issues, 546 | # you can generate a log file to help me. Modify the 547 | # first line of this script so it looks like this: 548 | # php -f action.php -- "{query}" | tee ~/Desktop/SpotifiousAction.log 549 | scriptargtype 550 | 0 551 | scriptfile 552 | 553 | type 554 | 0 555 | 556 | type 557 | alfred.workflow.action.script 558 | uid 559 | DBF28452-DD3D-4A25-AB98-89E268566DD5 560 | version 561 | 2 562 | 563 | 564 | config 565 | 566 | inputstring 567 | {var:respond} 568 | matchcasesensitive 569 | 570 | matchmode 571 | 1 572 | matchstring 573 | false 574 | 575 | type 576 | alfred.workflow.utility.filter 577 | uid 578 | 6EECFF75-AE32-44F5-97CB-3AE51C7E6A22 579 | version 580 | 1 581 | 582 | 583 | config 584 | 585 | action 586 | 0 587 | argument 588 | 3 589 | argumenttext 590 | playpause 591 | focusedappvariable 592 | 593 | focusedappvariablename 594 | 595 | hotkey 596 | 49 597 | hotmod 598 | 1310720 599 | hotstring 600 | Space 601 | leftcursor 602 | 603 | modsmode 604 | 0 605 | relatedAppsMode 606 | 0 607 | 608 | type 609 | alfred.workflow.trigger.hotkey 610 | uid 611 | 7858554D-2173-462A-B03D-1078EC7059D6 612 | version 613 | 2 614 | 615 | 616 | config 617 | 618 | argument 619 | next track 620 | argumenttype 621 | 3 622 | triggerid 623 | com.citelao.spotifious.next 624 | triggername 625 | Next Track 626 | workflowonly 627 | 628 | 629 | type 630 | alfred.workflow.trigger.remote 631 | uid 632 | 4BB22960-369A-4674-AF29-00CCE1603B53 633 | version 634 | 1 635 | 636 | 637 | config 638 | 639 | action 640 | 0 641 | argument 642 | 3 643 | argumenttext 644 | next track 645 | focusedappvariable 646 | 647 | focusedappvariablename 648 | 649 | hotkey 650 | 124 651 | hotmod 652 | 11796480 653 | hotstring 654 | 655 | leftcursor 656 | 657 | modsmode 658 | 0 659 | relatedAppsMode 660 | 0 661 | 662 | type 663 | alfred.workflow.trigger.hotkey 664 | uid 665 | 5610E60C-68BF-4445-BAE3-CB55DEA70D99 666 | version 667 | 2 668 | 669 | 670 | config 671 | 672 | argument 673 | previous track 674 | argumenttype 675 | 3 676 | triggerid 677 | com.citelao.spotifious.previous 678 | triggername 679 | Previous Track 680 | workflowonly 681 | 682 | 683 | type 684 | alfred.workflow.trigger.remote 685 | uid 686 | C5E43F66-AD1D-4D8D-9E4A-D7855C8F5AE2 687 | version 688 | 1 689 | 690 | 691 | config 692 | 693 | action 694 | 0 695 | argument 696 | 3 697 | argumenttext 698 | previous track 699 | focusedappvariable 700 | 701 | focusedappvariablename 702 | 703 | hotkey 704 | 123 705 | hotmod 706 | 11796480 707 | hotstring 708 | 709 | leftcursor 710 | 711 | modsmode 712 | 0 713 | relatedAppsMode 714 | 0 715 | 716 | type 717 | alfred.workflow.trigger.hotkey 718 | uid 719 | 30E2AA18-937F-4B5D-B685-7852B9717F34 720 | version 721 | 2 722 | 723 | 724 | config 725 | 726 | argument 727 | volup⟩ 728 | argumenttype 729 | 3 730 | triggerid 731 | com.citelao.spotifious.volup 732 | triggername 733 | Volume Up 734 | workflowonly 735 | 736 | 737 | type 738 | alfred.workflow.trigger.remote 739 | uid 740 | B02E0659-E11A-4746-ADB7-2C2C0A8BD8BB 741 | version 742 | 1 743 | 744 | 745 | config 746 | 747 | argument 748 | if sound volume < 90 then 749 | set sound volume to sound volume + 10 750 | else 751 | set sound volume to 100 752 | end if 753 | variables 754 | 755 | respond 756 | false 757 | 758 | 759 | type 760 | alfred.workflow.utility.argument 761 | uid 762 | 695822FE-205D-4684-86EC-3F7B7E8606DC 763 | version 764 | 1 765 | 766 | 767 | config 768 | 769 | action 770 | 0 771 | argument 772 | 3 773 | argumenttext 774 | volup⟩ 775 | focusedappvariable 776 | 777 | focusedappvariablename 778 | 779 | hotkey 780 | 126 781 | hotmod 782 | 11796480 783 | hotstring 784 | 785 | leftcursor 786 | 787 | modsmode 788 | 0 789 | relatedAppsMode 790 | 0 791 | 792 | type 793 | alfred.workflow.trigger.hotkey 794 | uid 795 | BFFC58C1-626F-4091-A0A4-61034DDD10D0 796 | version 797 | 2 798 | 799 | 800 | config 801 | 802 | argument 803 | voldown⟩ 804 | argumenttype 805 | 3 806 | triggerid 807 | com.citelao.spotifious.voldown 808 | triggername 809 | Volume Down 810 | workflowonly 811 | 812 | 813 | type 814 | alfred.workflow.trigger.remote 815 | uid 816 | F1B61D14-D686-468D-9373-AB4FC3EEE3C8 817 | version 818 | 1 819 | 820 | 821 | config 822 | 823 | argument 824 | if sound volume > 10 then 825 | set sound volume to sound volume - 10 826 | else 827 | set sound volume to 0 828 | end if 829 | variables 830 | 831 | respond 832 | false 833 | 834 | 835 | type 836 | alfred.workflow.utility.argument 837 | uid 838 | EC859C21-FCFF-4949-B767-726A7B37DAE8 839 | version 840 | 1 841 | 842 | 843 | config 844 | 845 | action 846 | 0 847 | argument 848 | 3 849 | argumenttext 850 | voldown⟩ 851 | focusedappvariable 852 | 853 | focusedappvariablename 854 | 855 | hotkey 856 | 125 857 | hotmod 858 | 11796480 859 | hotstring 860 | 861 | leftcursor 862 | 863 | modsmode 864 | 0 865 | relatedAppsMode 866 | 0 867 | 868 | type 869 | alfred.workflow.trigger.hotkey 870 | uid 871 | 73FAFE96-4B16-4AB5-B181-229B4BB79E25 872 | version 873 | 2 874 | 875 | 876 | readme 877 | # Spotifious 878 | 879 | I’ve worked a long time on it to make it as good as it can be, and if you have any ideas to make it better or if you need to complain, leave me a note on my GitHub page or on the forum threads, linked below. 880 | 881 | GitHub: https://github.com/citelao/Spotify-for-Alfred/ 882 | Forum Link: http://www.alfredforum.com/topic/1644-spotifious-a-natural-spotify-controller-for-alfred/ 883 | 884 | A lot of people helped out on this: 885 | - vdesabou and I both trade ideas a ton (sockets were his idea) 886 | - PHPfunk’s PHP Alfred workflow inspired much of OhAlfred 887 | - and many more people are referenced in the code itself. 888 | Thank you everyone! Y’all made Spotifious as awesome as it is. 889 | 890 | Also thank you for using Spotifious and reading the README; I hope you enjoy it. 891 | 892 | This code’s license can be found in LICENSE.md 893 | uidata 894 | 895 | 18496AC4-EA42-444C-A906-642E38259C96 896 | 897 | xpos 898 | 300 899 | ypos 900 | 10 901 | 902 | 2EA4FBDD-4717-4E37-9067-C93FE29A48F4 903 | 904 | note 905 | (If we ask to keep the window alive, don't hide) 906 | xpos 907 | 520 908 | ypos 909 | 290 910 | 911 | 30E2AA18-937F-4B5D-B685-7852B9717F34 912 | 913 | xpos 914 | 100 915 | ypos 916 | 1230 917 | 918 | 4BB22960-369A-4674-AF29-00CCE1603B53 919 | 920 | xpos 921 | 100 922 | ypos 923 | 870 924 | 925 | 5610E60C-68BF-4445-BAE3-CB55DEA70D99 926 | 927 | xpos 928 | 100 929 | ypos 930 | 980 931 | 932 | 605E53E5-EBF3-4D6E-A934-DA94EC49C1CF 933 | 934 | xpos 935 | 630 936 | ypos 937 | 290 938 | 939 | 6792E8E4-6892-467C-A207-2FBF2877E3C6 940 | 941 | note 942 | Responding Spotify query (much faster) 943 | xpos 944 | 490 945 | ypos 946 | 610 947 | 948 | 695822FE-205D-4684-86EC-3F7B7E8606DC 949 | 950 | xpos 951 | 280 952 | ypos 953 | 1410 954 | 955 | 6B1397DB-E737-4607-8181-27D9DFE70E2C 956 | 957 | note 958 | Action! 959 | xpos 960 | 500 961 | ypos 962 | 130 963 | 964 | 6EECFF75-AE32-44F5-97CB-3AE51C7E6A22 965 | 966 | note 967 | (only respond when we want it) 968 | xpos 969 | 680 970 | ypos 971 | 640 972 | 973 | 73FAFE96-4B16-4AB5-B181-229B4BB79E25 974 | 975 | xpos 976 | 100 977 | ypos 978 | 1740 979 | 980 | 7858554D-2173-462A-B03D-1078EC7059D6 981 | 982 | note 983 | 2. Bind remaining hotkeys... 984 | xpos 985 | 100 986 | ypos 987 | 720 988 | 989 | 7986333C-6D0E-4D95-8EC9-D3AEECD2D2FA 990 | 991 | note 992 | Setup guide! 993 | 994 | Spotifious works out of the box (just type `spotifious` in Alfred), but works best when configured with hotkeys. 995 | 996 | 1. Bind the top-most hotkey (the one labeled with the `1`) to whatever you'd like to activate Spotifious. 997 | 998 | 2. Bind the remaining "hotkey" items for simple, quick-access shortcuts. 999 | 1000 | 3. Activate Spotifious and finish the 3-step configuration. 1001 | xpos 1002 | 850 1003 | ypos 1004 | 10 1005 | 1006 | 8020A500-17C9-43BA-B17A-00D35E460EB6 1007 | 1008 | xpos 1009 | 100 1010 | ypos 1011 | 610 1012 | 1013 | 86EAA0E6-6086-4BC4-8CE7-0673F392C77F 1014 | 1015 | xpos 1016 | 100 1017 | ypos 1018 | 10 1019 | 1020 | AE6160D1-56EA-4416-991E-CF12B874FFD3 1021 | 1022 | note 1023 | 1. Bind this hotkey to activate Spotifious! 1024 | xpos 1025 | 100 1026 | ypos 1027 | 120 1028 | 1029 | B02E0659-E11A-4746-ADB7-2C2C0A8BD8BB 1030 | 1031 | xpos 1032 | 100 1033 | ypos 1034 | 1380 1035 | 1036 | BFFC58C1-626F-4091-A0A4-61034DDD10D0 1037 | 1038 | xpos 1039 | 100 1040 | ypos 1041 | 1490 1042 | 1043 | C5E43F66-AD1D-4D8D-9E4A-D7855C8F5AE2 1044 | 1045 | xpos 1046 | 100 1047 | ypos 1048 | 1120 1049 | 1050 | C8A768AF-C5D9-47A1-B499-8B0D3C844A99 1051 | 1052 | xpos 1053 | 500 1054 | ypos 1055 | 10 1056 | 1057 | DBF28452-DD3D-4A25-AB98-89E268566DD5 1058 | 1059 | note 1060 | Handle responding queries 1061 | xpos 1062 | 790 1063 | ypos 1064 | 610 1065 | 1066 | EC859C21-FCFF-4949-B767-726A7B37DAE8 1067 | 1068 | xpos 1069 | 280 1070 | ypos 1071 | 1660 1072 | 1073 | F1B61D14-D686-468D-9373-AB4FC3EEE3C8 1074 | 1075 | xpos 1076 | 100 1077 | ypos 1078 | 1630 1079 | 1080 | F4A2A879-4C5A-4532-BF35-02BAAD863581 1081 | 1082 | xpos 1083 | 100 1084 | ypos 1085 | 270 1086 | 1087 | 1088 | version 1089 | 0.13.9 1090 | webaddress 1091 | https://github.com/citelao/Spotify-for-Alfred 1092 | 1093 | 1094 | --------------------------------------------------------------------------------