mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
First integration with Last.fm
Koel can now integrate and use the rich information from Last.fm. Now whenever a song is played, its album and artist information will be queried from Last.fm and cached for later use. What's better, if an album has no cover, Koel will try to update its cover if one is found on Last.fm. In order to use this feature, users only need to provide valid Last.fm API credentials (namely LASTFM_API_KEY and LASTFM_API_SECRET) in .env. A npm and gulp rebuild is also required - just like with every update.
This commit is contained in:
parent
e536ff6d35
commit
cf27ed713d
37 changed files with 1654 additions and 299 deletions
|
@ -17,6 +17,10 @@ APP_MAX_SCAN_TIME=600
|
|||
# See https://github.com/phanan/koel/wiki#streaming-music for more information.
|
||||
STREAMING_METHOD=php
|
||||
|
||||
# If you want Koel to integrate with Last.fm, set the API details here.
|
||||
LASTFM_API_KEY=
|
||||
LASTFM_API_SECRET=
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
|
|
|
@ -15,6 +15,8 @@ env:
|
|||
ADMIN_EMAIL: koel@example.com
|
||||
ADMIN_NAME: Koel
|
||||
ADMIN_PASSWORD: SoSecureK0el
|
||||
LASTFM_API_KEY: foo
|
||||
LASTFM_API_SECRET: bar
|
||||
|
||||
branches:
|
||||
only:
|
||||
|
|
13
app/Facades/Lastfm.php
Normal file
13
app/Facades/Lastfm.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Lastfm extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'Lastfm';
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ use App\Models\Artist;
|
|||
use App\Models\Interaction;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\Setting;
|
||||
use App\Models\Song;
|
||||
use App\Models\User;
|
||||
|
||||
class DataController extends Controller
|
||||
|
@ -32,6 +31,7 @@ class DataController extends Controller
|
|||
'interactions' => Interaction::byCurrentUser()->get(),
|
||||
'users' => auth()->user()->is_admin ? User::all() : [],
|
||||
'user' => auth()->user(),
|
||||
'useLastfm' => env('LASTFM_API_KEY') && env('LASTFM_SECRET'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,4 +39,22 @@ class SongController extends Controller
|
|||
{
|
||||
return response()->json(Song::findOrFail($id)->lyrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra information about a song via Last.fm
|
||||
*
|
||||
* @param string $id
|
||||
*
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getInfo($id)
|
||||
{
|
||||
$song = Song::with('album.artist')->findOrFail($id);
|
||||
|
||||
return response()->json([
|
||||
'lyrics' => $song->lyrics,
|
||||
'album_info' => $song->album->getInfo(),
|
||||
'artist_info' => $song->album->artist->getInfo(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,7 @@ Route::group(['prefix' => 'api', 'middleware' => 'auth', 'namespace' => 'API'],
|
|||
post('settings', 'SettingController@save');
|
||||
|
||||
get('{id}/play', 'SongController@play')->where('id', '[a-f0-9]{32}');
|
||||
|
||||
get('{id}/lyrics', 'SongController@getLyrics')->where('id', '[a-f0-9]{32}');
|
||||
get('{id}/info', 'SongController@getInfo')->where('id', '[a-f0-9]{32}');
|
||||
|
||||
post('interaction/play', 'InteractionController@play');
|
||||
post('interaction/like', 'InteractionController@like');
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Facades\Lastfm;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property string cover The path to the album's cover
|
||||
* @property bool has_cover If the album has a cover image
|
||||
* @property int id
|
||||
* @property string name Name of the album
|
||||
* @property Artist artist The album's artist
|
||||
*/
|
||||
class Album extends Model
|
||||
{
|
||||
|
@ -49,6 +52,32 @@ class Album extends Model
|
|||
return $album;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra information about the album from Last.fm.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getInfo()
|
||||
{
|
||||
if ($this->id === self::UNKNOWN_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = Lastfm::getAlbumInfo($this->name, $this->artist->name);
|
||||
|
||||
// If our current album has no cover, and Last.fm has one, why don't we steal it?
|
||||
// Great artists steal for their great albums!
|
||||
if (!$this->has_cover &&
|
||||
is_string($image = array_get($info, 'image')) &&
|
||||
ini_get('allow_url_fopen')
|
||||
) {
|
||||
$extension = explode('.', $image);
|
||||
$this->writeCoverFile(file_get_contents($image), last($extension));
|
||||
}
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cover from provided data.
|
||||
*
|
||||
|
@ -68,10 +97,24 @@ class Album extends Model
|
|||
public function generateCover(array $cover)
|
||||
{
|
||||
$extension = explode('/', $cover['image_mime']);
|
||||
$fileName = uniqid().'.'.strtolower($extension[1]);
|
||||
$extension = empty($extension[1]) ? 'png' : $extension[1];
|
||||
|
||||
$this->writeCoverFile($cover['data'], $extension);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a cover image file with binary data and update the Album with the new cover file.
|
||||
*
|
||||
* @param string $binaryData
|
||||
* @param string $extension The file extension
|
||||
*/
|
||||
private function writeCoverFile($binaryData, $extension)
|
||||
{
|
||||
$extension = trim(strtolower($extension), '. ');
|
||||
$fileName = uniqid().".$extension";
|
||||
$coverPath = app()->publicPath().'/public/img/covers/'.$fileName;
|
||||
|
||||
file_put_contents($coverPath, $cover['data']);
|
||||
file_put_contents($coverPath, $binaryData);
|
||||
|
||||
$this->update(['cover' => $fileName]);
|
||||
}
|
||||
|
@ -101,6 +144,8 @@ class Album extends Model
|
|||
* This makes sure they are always sane.
|
||||
*
|
||||
* @param $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNameAttribute($value)
|
||||
{
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Facades\Lastfm;
|
||||
use App\Facades\Util;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @property int id The model ID
|
||||
* @property int id The model ID
|
||||
* @property string name The artist name
|
||||
*/
|
||||
class Artist extends Model
|
||||
{
|
||||
|
@ -54,4 +56,22 @@ class Artist extends Model
|
|||
|
||||
return self::firstOrCreate(compact('name'), compact('name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get extra information about the artist from Last.fm.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getInfo()
|
||||
{
|
||||
if ($this->id === self::UNKNOWN_ID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = Lastfm::getArtistInfo($this->name);
|
||||
|
||||
// TODO: Copy the artist's image for our local use.
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
||||
|
|
31
app/Providers/LastfmServiceProvider.php
Normal file
31
app/Providers/LastfmServiceProvider.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\Lastfm;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class LastfmServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
app()->singleton('Lastfm', function () {
|
||||
return new Lastfm();
|
||||
});
|
||||
}
|
||||
}
|
170
app/Services/Lastfm.php
Normal file
170
app/Services/Lastfm.php
Normal file
|
@ -0,0 +1,170 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Log;
|
||||
|
||||
class Lastfm extends RESTfulService
|
||||
{
|
||||
/**
|
||||
* Specify the response format, since Last.fm only returns XML.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $responseFormat = 'xml';
|
||||
|
||||
/**
|
||||
* Override the key param, since, again, Lastfm wants to be different.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyParam = 'api_key';
|
||||
|
||||
/**
|
||||
* Construct an instance of Lastfm service.
|
||||
*
|
||||
* @param string $key Last.fm API key.
|
||||
* @param string $secret Last.fm API shared secret.
|
||||
* @param Client $client The Guzzle HTTP client.
|
||||
*/
|
||||
public function __construct($key = null, $secret = null, Client $client = null)
|
||||
{
|
||||
parent::__construct(
|
||||
$key ?: env('LASTFM_API_KEY'),
|
||||
$secret ?: env('LASTFM_API_SECRET'),
|
||||
'https://ws.audioscrobbler.com/2.0',
|
||||
$client ?: new Client()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an artist.
|
||||
*
|
||||
* @param $name string Name of the artist
|
||||
*
|
||||
* @return object|false
|
||||
*/
|
||||
public function getArtistInfo($name)
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = urlencode($name);
|
||||
|
||||
try {
|
||||
$cacheKey = md5("lastfm_artist_$name");
|
||||
|
||||
if ($response = Cache::get($cacheKey)) {
|
||||
$response = simplexml_load_string($response);
|
||||
} else {
|
||||
if ($response = $this->get("?method=artist.getInfo&autocorrect=1&artist=$name")) {
|
||||
Cache::put($cacheKey, $response->asXML(), 24 * 60 * 7);
|
||||
}
|
||||
}
|
||||
|
||||
$response = json_decode(json_encode($response), true);
|
||||
|
||||
if (!$response || !$artist = array_get($response, 'artist')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'url' => array_get($artist, 'url'),
|
||||
'image' => count($artist['image']) > 3 ? $artist['image'][3] : $artist['image'][0],
|
||||
'bio' => [
|
||||
'summary' => $this->formatText(array_get($artist, 'bio.summary')),
|
||||
'full' => $this->formatText(array_get($artist, 'bio.content')),
|
||||
],
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly format a string returned by Last.fm.
|
||||
*
|
||||
* @param string $str
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function formatText($str)
|
||||
{
|
||||
if (!$str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return trim(str_replace('Read more on Last.fm', '', nl2br(strip_tags(html_entity_decode($str)))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about an album.
|
||||
*
|
||||
* @param string $name Name of the album
|
||||
* @param string $artistName Name of the artist
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getAlbumInfo($name, $artistName)
|
||||
{
|
||||
if (!$this->enabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = urlencode($name);
|
||||
$artistName = urlencode($artistName);
|
||||
|
||||
try {
|
||||
$cacheKey = md5("lastfm_album_{$name}_{$artistName}");
|
||||
|
||||
if ($response = Cache::get($cacheKey)) {
|
||||
$response = simplexml_load_string($response);
|
||||
} else {
|
||||
if ($response = $this->get("?method=album.getInfo&autocorrect=1&album=$name&artist=$artistName")) {
|
||||
Cache::put($cacheKey, $response->asXML(), 24 * 60 * 7);
|
||||
}
|
||||
}
|
||||
|
||||
$response = json_decode(json_encode($response), true);
|
||||
|
||||
if (!$response || !$album = array_get($response, 'album')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'url' => array_get($album, 'url'),
|
||||
'image' => count($album['image']) > 3 ? $album['image'][3] : $album['image'][0],
|
||||
'wiki' => [
|
||||
'summary' => $this->formatText(array_get($album, 'wiki.summary')),
|
||||
'full' => $this->formatText(array_get($album, 'wiki.content')),
|
||||
],
|
||||
'tracks' => array_map(function ($track) {
|
||||
return [
|
||||
'title' => $track['name'],
|
||||
'length' => (int) $track['duration'],
|
||||
'url' => $track['url'],
|
||||
];
|
||||
}, array_get($album, 'tracks.track', [])),
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if Last.fm integration is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function enabled()
|
||||
{
|
||||
return $this->getKey() && $this->getSecret();
|
||||
}
|
||||
}
|
208
app/Services/RESTfulService.php
Normal file
208
app/Services/RESTfulService.php
Normal file
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class RESTfulService.
|
||||
*
|
||||
* @method object get($uri)
|
||||
* @method object post($uri, array $data = [])
|
||||
* @method object put($uri, array $data = [])
|
||||
* @method object patch($uri, array $data = [])
|
||||
* @method object head($uri, array $data = [])
|
||||
* @method object delete($uri)
|
||||
*/
|
||||
class RESTfulService
|
||||
{
|
||||
protected $responseFormat = 'json';
|
||||
|
||||
/**
|
||||
* The API endpoint.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $endpoint = null;
|
||||
|
||||
/**
|
||||
* The GuzzleHttp client to talk to the API.
|
||||
*
|
||||
* @var Client;
|
||||
*/
|
||||
protected $client;
|
||||
|
||||
/**
|
||||
* The API key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $key;
|
||||
|
||||
/**
|
||||
* The query parameter name for the key.
|
||||
* For example, Last.fm use api_key, like this:
|
||||
* https://ws.audioscrobbler.com/2.0?method=artist.getInfo&artist=Kamelot&api_key=API_KEY.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyParam = 'key';
|
||||
|
||||
/**
|
||||
* The API secret.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $secret;
|
||||
|
||||
public function __construct($key, $secret, $endpoint, Client $client)
|
||||
{
|
||||
$this->setKey($key);
|
||||
$this->setSecret($secret);
|
||||
$this->setEndpoint($endpoint);
|
||||
$this->setClient($client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request to the API.
|
||||
*
|
||||
* @param string $verb The HTTP verb
|
||||
* @param string $uri The API URI (segment)
|
||||
* @param array $params An array of parameters
|
||||
*
|
||||
* @return object The JSON response.
|
||||
*/
|
||||
public function request($verb, $uri, $params = [])
|
||||
{
|
||||
try {
|
||||
$body = (string) $this->getClient()
|
||||
->$verb($this->buildUrl($uri), ['form_params' => $params])
|
||||
->getBody();
|
||||
|
||||
if ($this->responseFormat === 'json') {
|
||||
return json_decode($body);
|
||||
}
|
||||
|
||||
if ($this->responseFormat === 'xml') {
|
||||
return simplexml_load_string($body);
|
||||
}
|
||||
|
||||
return $body;
|
||||
} catch (ClientException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an HTTP call to the external resource.
|
||||
*
|
||||
* @param string $method The HTTP method
|
||||
* @param array $args An array of parameters
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
if (count($args) < 1) {
|
||||
throw new InvalidArgumentException('Magic request methods require a URI and optional options array');
|
||||
}
|
||||
|
||||
$uri = $args[0];
|
||||
$opts = isset($args[1]) ? $args[1] : [];
|
||||
|
||||
return $this->request($method, $uri, $opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turn a URI segment into a full API URL.
|
||||
*
|
||||
* @param string $uri
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function buildUrl($uri)
|
||||
{
|
||||
if (!starts_with($uri, ['http://', 'https://'])) {
|
||||
if ($uri[0] != '/') {
|
||||
$uri = "/$uri";
|
||||
}
|
||||
|
||||
$uri = $this->endpoint.$uri;
|
||||
}
|
||||
|
||||
// Append the API key.
|
||||
if (parse_url($uri, PHP_URL_QUERY)) {
|
||||
$uri .= "&{$this->keyParam}=".$this->getKey();
|
||||
} else {
|
||||
$uri .= "?{$this->keyParam}=".$this->getKey();
|
||||
}
|
||||
|
||||
return $uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Client
|
||||
*/
|
||||
public function getClient()
|
||||
{
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Client $client
|
||||
*/
|
||||
public function setClient($client)
|
||||
{
|
||||
$this->client = $client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*/
|
||||
public function setKey($key)
|
||||
{
|
||||
$this->key = $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSecret()
|
||||
{
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $secret
|
||||
*/
|
||||
public function setSecret($secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEndpoint()
|
||||
{
|
||||
return $this->endpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $endpoint
|
||||
*/
|
||||
public function setEndpoint($endpoint)
|
||||
{
|
||||
$this->endpoint = $endpoint;
|
||||
}
|
||||
}
|
|
@ -9,7 +9,8 @@
|
|||
"laravel/framework": "5.1.*",
|
||||
"james-heinrich/getid3": "^1.9",
|
||||
"phanan/cascading-config": "~2.0",
|
||||
"barryvdh/laravel-ide-helper": "^2.1"
|
||||
"barryvdh/laravel-ide-helper": "^2.1",
|
||||
"guzzlehttp/guzzle": "^6.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fzaninotto/faker": "~1.4",
|
||||
|
|
648
composer.lock
generated
648
composer.lock
generated
|
@ -4,9 +4,72 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"hash": "d6bc7d442de58681377bb1c28e216590",
|
||||
"content-hash": "6ae1cb335a5047084101d16a3b864ac8",
|
||||
"hash": "17203bc34b6f1fe1e3f418afa6e101f5",
|
||||
"content-hash": "19b8169471239898bb337b97f362bbbf",
|
||||
"packages": [
|
||||
{
|
||||
"name": "barryvdh/laravel-ide-helper",
|
||||
"version": "v2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
|
||||
"reference": "83999f8467374adcb8893f566c9171c9d9691f50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/83999f8467374adcb8893f566c9171c9d9691f50",
|
||||
"reference": "83999f8467374adcb8893f566c9171c9d9691f50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "5.0.x|5.1.x",
|
||||
"illuminate/filesystem": "5.0.x|5.1.x",
|
||||
"illuminate/support": "5.0.x|5.1.x",
|
||||
"php": ">=5.4.0",
|
||||
"phpdocumentor/reflection-docblock": "2.0.4",
|
||||
"symfony/class-loader": "~2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "~2.3"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\LaravelIdeHelper\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
|
||||
"keywords": [
|
||||
"autocomplete",
|
||||
"codeintel",
|
||||
"helper",
|
||||
"ide",
|
||||
"laravel",
|
||||
"netbeans",
|
||||
"phpdoc",
|
||||
"phpstorm",
|
||||
"sublime"
|
||||
],
|
||||
"time": "2015-08-13 11:40:00"
|
||||
},
|
||||
{
|
||||
"name": "classpreloader/classpreloader",
|
||||
"version": "3.0.0",
|
||||
|
@ -217,6 +280,177 @@
|
|||
],
|
||||
"time": "2015-11-06 14:35:42"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/guzzle",
|
||||
"version": "6.1.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/guzzle.git",
|
||||
"reference": "c6851d6e48f63b69357cbfa55bca116448140e0c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/c6851d6e48f63b69357cbfa55bca116448140e0c",
|
||||
"reference": "c6851d6e48f63b69357cbfa55bca116448140e0c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"guzzlehttp/promises": "~1.0",
|
||||
"guzzlehttp/psr7": "~1.1",
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"phpunit/phpunit": "~4.0",
|
||||
"psr/log": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "6.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Guzzle is a PHP HTTP client library",
|
||||
"homepage": "http://guzzlephp.org/",
|
||||
"keywords": [
|
||||
"client",
|
||||
"curl",
|
||||
"framework",
|
||||
"http",
|
||||
"http client",
|
||||
"rest",
|
||||
"web service"
|
||||
],
|
||||
"time": "2015-11-23 00:47:50"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/promises",
|
||||
"version": "1.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/promises.git",
|
||||
"reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/promises/zipball/b1e1c0d55f8083c71eda2c28c12a228d708294ea",
|
||||
"reference": "b1e1c0d55f8083c71eda2c28c12a228d708294ea",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\Promise\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "Guzzle promises library",
|
||||
"keywords": [
|
||||
"promise"
|
||||
],
|
||||
"time": "2015-10-15 22:28:00"
|
||||
},
|
||||
{
|
||||
"name": "guzzlehttp/psr7",
|
||||
"version": "1.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/guzzle/psr7.git",
|
||||
"reference": "4d0bdbe1206df7440219ce14c972aa57cc5e4982"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/guzzle/psr7/zipball/4d0bdbe1206df7440219ce14c972aa57cc5e4982",
|
||||
"reference": "4d0bdbe1206df7440219ce14c972aa57cc5e4982",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.4.0",
|
||||
"psr/http-message": "~1.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/http-message-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"GuzzleHttp\\Psr7\\": "src/"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Michael Dowling",
|
||||
"email": "mtdowling@gmail.com",
|
||||
"homepage": "https://github.com/mtdowling"
|
||||
}
|
||||
],
|
||||
"description": "PSR-7 message implementation",
|
||||
"keywords": [
|
||||
"http",
|
||||
"message",
|
||||
"stream",
|
||||
"uri"
|
||||
],
|
||||
"time": "2015-11-03 01:34:55"
|
||||
},
|
||||
{
|
||||
"name": "jakub-onderka/php-console-color",
|
||||
"version": "0.1",
|
||||
|
@ -878,6 +1112,152 @@
|
|||
],
|
||||
"time": "2015-12-10 14:48:13"
|
||||
},
|
||||
{
|
||||
"name": "phanan/cascading-config",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phanan/cascading-config.git",
|
||||
"reference": "02efc75ae964f63f0c2a40a22654111fecea895c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phanan/cascading-config/zipball/02efc75ae964f63f0c2a40a22654111fecea895c",
|
||||
"reference": "02efc75ae964f63f0c2a40a22654111fecea895c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "~5.1",
|
||||
"laravel/lumen-framework": "~5.1.6",
|
||||
"phpunit/phpunit": "~5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhanAn\\CascadingConfig\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phan An",
|
||||
"email": "me@phanan.net",
|
||||
"homepage": "http://phanan.net"
|
||||
}
|
||||
],
|
||||
"description": "Bringing the cascading configuration system back to Laravel 5.",
|
||||
"homepage": "https://github.com/phanan/cascading-config",
|
||||
"keywords": [
|
||||
"cascade",
|
||||
"cascading",
|
||||
"config",
|
||||
"configuration",
|
||||
"laravel",
|
||||
"laravel 5"
|
||||
],
|
||||
"time": "2015-11-16 17:01:33"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "2.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"dflydev/markdown": "~1.0",
|
||||
"erusev/parsedown": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"phpDocumentor": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "mike.vanriel@naenius.com"
|
||||
}
|
||||
],
|
||||
"time": "2015-02-03 12:10:50"
|
||||
},
|
||||
{
|
||||
"name": "psr/http-message",
|
||||
"version": "1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/http-message.git",
|
||||
"reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
|
||||
"reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Http\\Message\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "http://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for HTTP messages",
|
||||
"keywords": [
|
||||
"http",
|
||||
"http-message",
|
||||
"psr",
|
||||
"psr-7",
|
||||
"request",
|
||||
"response"
|
||||
],
|
||||
"time": "2015-05-04 20:22:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "1.0.0",
|
||||
|
@ -1041,6 +1421,58 @@
|
|||
],
|
||||
"time": "2015-06-06 14:19:39"
|
||||
},
|
||||
{
|
||||
"name": "symfony/class-loader",
|
||||
"version": "v2.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/class-loader.git",
|
||||
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/class-loader/zipball/51f83451bf0ddfc696e47e4642d6cd10fcfce160",
|
||||
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/finder": "~2.0,>=2.0.5|~3.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.8-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\ClassLoader\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony ClassLoader Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-11-26 07:00:59"
|
||||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v2.7.7",
|
||||
|
@ -1911,69 +2343,6 @@
|
|||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "barryvdh/laravel-ide-helper",
|
||||
"version": "v2.1.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
|
||||
"reference": "83999f8467374adcb8893f566c9171c9d9691f50"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/83999f8467374adcb8893f566c9171c9d9691f50",
|
||||
"reference": "83999f8467374adcb8893f566c9171c9d9691f50",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/console": "5.0.x|5.1.x",
|
||||
"illuminate/filesystem": "5.0.x|5.1.x",
|
||||
"illuminate/support": "5.0.x|5.1.x",
|
||||
"php": ">=5.4.0",
|
||||
"phpdocumentor/reflection-docblock": "2.0.4",
|
||||
"symfony/class-loader": "~2.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "~2.3"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Barryvdh\\LaravelIdeHelper\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
|
||||
"keywords": [
|
||||
"autocomplete",
|
||||
"codeintel",
|
||||
"helper",
|
||||
"ide",
|
||||
"laravel",
|
||||
"netbeans",
|
||||
"phpdoc",
|
||||
"phpstorm",
|
||||
"sublime"
|
||||
],
|
||||
"time": "2015-08-13 11:40:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/instantiator",
|
||||
"version": "1.0.5",
|
||||
|
@ -2190,103 +2559,6 @@
|
|||
],
|
||||
"time": "2015-04-02 19:54:00"
|
||||
},
|
||||
{
|
||||
"name": "phanan/cascading-config",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phanan/cascading-config.git",
|
||||
"reference": "02efc75ae964f63f0c2a40a22654111fecea895c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phanan/cascading-config/zipball/02efc75ae964f63f0c2a40a22654111fecea895c",
|
||||
"reference": "02efc75ae964f63f0c2a40a22654111fecea895c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"laravel/framework": "~5.1",
|
||||
"laravel/lumen-framework": "~5.1.6",
|
||||
"phpunit/phpunit": "~5.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"PhanAn\\CascadingConfig\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Phan An",
|
||||
"email": "me@phanan.net",
|
||||
"homepage": "http://phanan.net"
|
||||
}
|
||||
],
|
||||
"description": "Bringing the cascading configuration system back to Laravel 5.",
|
||||
"homepage": "https://github.com/phanan/cascading-config",
|
||||
"keywords": [
|
||||
"cascade",
|
||||
"cascading",
|
||||
"config",
|
||||
"configuration",
|
||||
"laravel",
|
||||
"laravel 5"
|
||||
],
|
||||
"time": "2015-11-16 17:01:33"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
"version": "2.0.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~4.0"
|
||||
},
|
||||
"suggest": {
|
||||
"dflydev/markdown": "~1.0",
|
||||
"erusev/parsedown": "~1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"phpDocumentor": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mike van Riel",
|
||||
"email": "mike.vanriel@naenius.com"
|
||||
}
|
||||
],
|
||||
"time": "2015-02-03 12:10:50"
|
||||
},
|
||||
{
|
||||
"name": "phpspec/php-diff",
|
||||
"version": "v1.0.2",
|
||||
|
@ -3198,58 +3470,6 @@
|
|||
"homepage": "https://github.com/sebastianbergmann/version",
|
||||
"time": "2015-06-21 13:59:46"
|
||||
},
|
||||
{
|
||||
"name": "symfony/class-loader",
|
||||
"version": "v2.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/class-loader.git",
|
||||
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/class-loader/zipball/51f83451bf0ddfc696e47e4642d6cd10fcfce160",
|
||||
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.9"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/finder": "~2.0,>=2.0.5|~3.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.8-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\ClassLoader\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony ClassLoader Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2015-11-26 07:00:59"
|
||||
},
|
||||
{
|
||||
"name": "symfony/yaml",
|
||||
"version": "v3.0.0",
|
||||
|
|
|
@ -150,6 +150,7 @@ return [
|
|||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\MediaServiceProvider::class,
|
||||
App\Providers\UtilServiceProvider::class,
|
||||
App\Providers\LastfmServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
|
@ -202,6 +203,7 @@ return [
|
|||
|
||||
'Media' => App\Facades\Media::class,
|
||||
'Util' => App\Facades\Util::class,
|
||||
'Lastfm' => App\Facades\Lastfm::class,
|
||||
|
||||
],
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPreferencesToUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->text('preferences')->after('is_admin')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('preferences');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -24,5 +24,7 @@
|
|||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="LASTFM_API_KEY" value="foo"/>
|
||||
<env name="LASTFM_API_SECRET" value="bar"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
|
@ -239,6 +239,7 @@
|
|||
<style lang="sass">
|
||||
@import "resources/assets/sass/partials/_vars.scss";
|
||||
@import "resources/assets/sass/partials/_mixins.scss";
|
||||
@import "resources/assets/sass/partials/_shared.scss";
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
|
|
124
resources/assets/js/components/main-wrapper/extra/album-info.vue
Normal file
124
resources/assets/js/components/main-wrapper/extra/album-info.vue
Normal file
|
@ -0,0 +1,124 @@
|
|||
<template>
|
||||
<article v-if="song" id="albumInfo">
|
||||
<h1>
|
||||
<span>{{ song.album.name }}</span>
|
||||
|
||||
<a class="shuffle" @click.prevent="shuffleAll"><i class="fa fa-random"></i></a>
|
||||
</h1>
|
||||
|
||||
<div v-if="song.album.info">
|
||||
<img v-if="song.album.info.image" :src="song.album.info.image"
|
||||
title=""
|
||||
class="cover">
|
||||
|
||||
<div class="wiki" v-if="song.album.info.wiki && song.album.info.wiki.summary">
|
||||
<div class="summary" v-show="!showingFullWiki">{{{ song.album.info.wiki.summary }}}</div>
|
||||
<div class="full" v-show="showingFullWiki">{{{ song.album.info.wiki.full }}}</div>
|
||||
|
||||
<button class="more" v-show="!showingFullWiki" @click.prevent="showingFullWiki = !showingFullWiki">
|
||||
Full Wiki
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="track-listing" v-if="song.album.info.tracks.length">
|
||||
<h1>Track Listing</h1>
|
||||
<ul class="tracks">
|
||||
<li v-for="track in song.album.info.tracks">
|
||||
<span class="no">{{ $index + 1 }}</span>
|
||||
<span class="title">{{ track.title }}</span>
|
||||
<span class="length">{{ track.fmtLength }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer>Data © <a target="_blank" href="{{{ song.album.info.url }}}">Last.fm</a></footer>
|
||||
</div>
|
||||
|
||||
<p class="none" v-else>
|
||||
No album information found. At all.
|
||||
</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playback from '../../../services/playback';
|
||||
|
||||
export default {
|
||||
replace: false,
|
||||
|
||||
data() {
|
||||
return {
|
||||
song: null,
|
||||
showingFullWiki: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetState() {
|
||||
this.song = null;
|
||||
this.showingFullWiki = false;
|
||||
},
|
||||
|
||||
shuffleAll() {
|
||||
playback.playAllInAlbum(this.song.album);
|
||||
}
|
||||
},
|
||||
|
||||
events: {
|
||||
'song:info-loaded': function (song) {
|
||||
this.song = song;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "resources/assets/sass/partials/_vars.scss";
|
||||
@import "resources/assets/sass/partials/_mixins.scss";
|
||||
|
||||
#albumInfo {
|
||||
img.cover {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.wiki {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.track-listing {
|
||||
margin-top: 16px;
|
||||
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.no {
|
||||
flex: 0 0 24px;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.length {
|
||||
flex: 0 0 48px;
|
||||
text-align: right;
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<article v-if="song" id="artistInfo">
|
||||
<h1>
|
||||
<span>{{ song ? song.album.artist.name : '' }}</span>
|
||||
|
||||
<a class="shuffle" @click.prevent="shuffleAll"><i class="fa fa-random"></i></a>
|
||||
</h1>
|
||||
|
||||
<div v-if="song.album.artist.info">
|
||||
<img v-if="song.album.artist.info.image" :src="song.album.artist.info.image"
|
||||
title="They see me posin, they hatin"
|
||||
class="cool-guys-posing cover">
|
||||
|
||||
<div class="bio" v-if="song.album.artist.info.bio.summary">
|
||||
<div class="summary" v-show="!showingFullBio">{{{ song.album.artist.info.bio.summary }}}</div>
|
||||
<div class="full" v-show="showingFullBio">{{{ song.album.artist.info.bio.full }}}</div>
|
||||
|
||||
<button class="more" v-show="!showingFullBio" @click.prevent="showingFullBio = !showingFullBio">
|
||||
Full Bio
|
||||
</button>
|
||||
</div>
|
||||
<p class="none" v-else>This artist has no Last.fm biography – yet.</p>
|
||||
|
||||
<footer>Data © <a target="_blank" href="{{{ song.album.artist.info.url }}}">Last.fm</a></footer>
|
||||
</div>
|
||||
|
||||
<p class="none" v-else>Nothing can be found. This artist is a mystery.</p>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playback from '../../../services/playback';
|
||||
|
||||
export default {
|
||||
replace: false,
|
||||
|
||||
data() {
|
||||
return {
|
||||
song: null,
|
||||
showingFullBio: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
resetState() {
|
||||
this.song = null;
|
||||
this.showingFullBio = false;
|
||||
},
|
||||
|
||||
shuffleAll() {
|
||||
playback.playAllByArtist(this.song.album.artist);
|
||||
},
|
||||
},
|
||||
|
||||
events: {
|
||||
'song:info-loaded': function (song) {
|
||||
this.song = song;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "resources/assets/sass/partials/_vars.scss";
|
||||
@import "resources/assets/sass/partials/_mixins.scss";
|
||||
|
||||
#artistInfo {
|
||||
img.cool-guys-posing {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.bio {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,25 +1,41 @@
|
|||
<template>
|
||||
<section id="extra" :class="{ showing: prefs.showExtraPanel }">
|
||||
<h1>Lyrics</h1>
|
||||
<div class="tabs">
|
||||
<div class="header clear">
|
||||
<a @click.prevent="currentView = 'lyrics'"
|
||||
:class="{ active: currentView == 'lyrics' }">Lyrics</a>
|
||||
<a @click.prevent="currentView = 'artistInfo'"
|
||||
:class="{ active: currentView == 'artistInfo' }">Artist</a>
|
||||
<a @click.prevent="currentView = 'albumInfo'"
|
||||
:class="{ active: currentView == 'albumInfo' }">Album</a>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<lyrics></lyrics>
|
||||
<div class="panes">
|
||||
<lyrics v-ref:lyrics v-show="currentView == 'lyrics'"></lyrics>
|
||||
<artist-info v-ref:artist-info v-show="currentView == 'artistInfo'"></artist-info>
|
||||
<album-info v-ref:album-info v-show="currentView == 'albumInfo'"></album-info>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import isMobile from 'ismobilejs';
|
||||
import _ from 'lodash';
|
||||
|
||||
import lyrics from './lyrics.vue';
|
||||
import artistInfo from './artist-info.vue'
|
||||
import albumInfo from './album-info.vue'
|
||||
import preferenceStore from '../../../stores/preference';
|
||||
import songStore from '../../../stores/song';
|
||||
|
||||
export default {
|
||||
components: { lyrics },
|
||||
components: { lyrics, artistInfo, albumInfo },
|
||||
|
||||
data() {
|
||||
return {
|
||||
prefs: preferenceStore.state,
|
||||
currentView: 'lyrics',
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -39,6 +55,19 @@
|
|||
this.prefs.showExtraPanel = false;
|
||||
}
|
||||
},
|
||||
|
||||
'song:play': function (song) {
|
||||
// Reset all applicable child components' states
|
||||
_.each(this.$refs, child => {
|
||||
if (typeof child.resetState === 'function') {
|
||||
child.resetState();
|
||||
}
|
||||
});
|
||||
|
||||
songStore.getInfo(song, () => {
|
||||
this.$broadcast('song:info-loaded', song);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -49,11 +78,12 @@
|
|||
|
||||
#extra {
|
||||
flex: 0 0 334px;
|
||||
padding: 16px 16px $footerHeight;
|
||||
padding: 24px 16px $footerHeight;
|
||||
background: $colorExtraBgr;
|
||||
max-height: calc(100vh - #{$headerHeight + $footerHeight});
|
||||
overflow: auto;
|
||||
display: none;
|
||||
color: $color2ndText;
|
||||
|
||||
&.showing {
|
||||
display: block;
|
||||
|
@ -61,9 +91,77 @@
|
|||
|
||||
h1 {
|
||||
font-weight: $fontWeight_UltraThin;
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 64px;
|
||||
line-height: 36px;
|
||||
|
||||
@include vertical-center();
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 14px;
|
||||
|
||||
&:hover {
|
||||
color: $colorHighlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
.header {
|
||||
border-bottom: 1px solid $colorHighlight;
|
||||
|
||||
a {
|
||||
padding: 4px 12px;
|
||||
margin-left: 4px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
opacity: .4;
|
||||
border: 1px solid $colorHighlight;
|
||||
margin-bottom: -1px;
|
||||
float: left;
|
||||
|
||||
&.active {
|
||||
border-bottom: 1px solid $colorExtraBgr;
|
||||
color: $colorHighlight;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panes {
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-top: 8px;
|
||||
border-radius: 3px;
|
||||
background: $colorBlue;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 24px;
|
||||
font-size: 90%;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
font-weight: $fontWeight_Normal;
|
||||
|
||||
&:hover {
|
||||
color: #b90000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
<template>
|
||||
<div id="lyrics">{{{ lyrics }}}</div>
|
||||
<article id="lyrics">
|
||||
<div class="content">
|
||||
<div v-if="lyrics">{{{ lyrics }}}</div>
|
||||
<p class="none" v-else>No lyrics found. Are you not listening to Bach?</p>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import songStore from '../../../stores/song';
|
||||
|
||||
export default {
|
||||
replace: false,
|
||||
|
||||
data() {
|
||||
return {
|
||||
lyrics: '',
|
||||
};
|
||||
},
|
||||
|
||||
events: {
|
||||
/**
|
||||
* Whenever a song is played, get its lyrics from store to display.
|
||||
*
|
||||
* @param object song The currently played song
|
||||
*
|
||||
* @return true
|
||||
*/
|
||||
'song:play': function (song) {
|
||||
this.lyrics = 'Loading…';
|
||||
|
||||
songStore.getLyrics(song, () => this.lyrics = song.lyrics);
|
||||
|
||||
return true;
|
||||
methods: {
|
||||
resetState() {
|
||||
this.lyrics = '';
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
events: {
|
||||
'song:info-loaded': function (song) {
|
||||
this.lyrics = song.lyrics;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="sass">
|
||||
@import "resources/assets/sass/partials/_vars.scss";
|
||||
@import "resources/assets/sass/partials/_mixins.scss";
|
||||
|
||||
#lyrics {
|
||||
color: $color2ndText;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -83,43 +83,9 @@
|
|||
|
||||
.buttons {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
|
||||
button {
|
||||
$buttonHeight: 28px;
|
||||
background-color: $colorHighlight;
|
||||
font-size: 12px;
|
||||
height: $buttonHeight;
|
||||
padding: 0 16px;
|
||||
line-height: $buttonHeight;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
|
||||
border-radius: $buttonHeight/2 0px 0px $buttonHeight/2;
|
||||
|
||||
&:last-of-type {
|
||||
border-top-right-radius: $buttonHeight/2;
|
||||
border-bottom-right-radius: $buttonHeight/2;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: darken($colorHighlight, 10%);
|
||||
}
|
||||
|
||||
@include inset-when-pressed();
|
||||
}
|
||||
@include button-group();
|
||||
}
|
||||
|
||||
input[type="search"] {
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
* Play all songs in the current album.
|
||||
*/
|
||||
play() {
|
||||
playback.queueAndPlay(this.album.songs);
|
||||
playback.playAllInAlbum(this.album);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
* Play all songs by the current artist.
|
||||
*/
|
||||
play() {
|
||||
playback.queueAndPlay(artistStore.getSongsByArtist(this.artist));
|
||||
playback.playAllByArtist(this.artist);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
@click.prevent="like"></i>
|
||||
|
||||
<span class="control"
|
||||
@click.prevent="toggleLyrics"
|
||||
:class="{ active: prefs.showExtraPanel }">Lyrics</span>
|
||||
@click.prevent="toggleExtraPanel"
|
||||
:class="{ active: prefs.showExtraPanel }">Info</span>
|
||||
|
||||
<i class="queue control fa fa-list-ol control"
|
||||
:class="{ 'active': viewingQueue }"
|
||||
|
@ -216,9 +216,9 @@
|
|||
/**
|
||||
* <That's it. That's it!>
|
||||
*
|
||||
* Toggle hide or show the lyrics panel.
|
||||
* Toggle hide or show the extra panel.
|
||||
*/
|
||||
toggleLyrics() {
|
||||
toggleExtraPanel() {
|
||||
preferenceStore.set('showExtraPanel', !this.prefs.showExtraPanel);
|
||||
},
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import $ from 'jquery';
|
|||
|
||||
import queueStore from '../stores/queue';
|
||||
import songStore from '../stores/song';
|
||||
import artistStore from '../stores/artist';
|
||||
import albumStore from '../stores/album';
|
||||
import preferenceStore from '../stores/preference';
|
||||
import config from '../config';
|
||||
|
||||
|
@ -294,4 +296,24 @@ export default {
|
|||
|
||||
this.play(queueStore.first());
|
||||
},
|
||||
|
||||
/**
|
||||
* Play all songs by an artist.
|
||||
*
|
||||
* @param {Object} artist The artist object
|
||||
* @param {Boolean} shuffle Whether to shuffle the songs
|
||||
*/
|
||||
playAllByArtist(artist, shuffle = true) {
|
||||
this.queueAndPlay(artistStore.getSongsByArtist(artist), true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Play all songs in an album.
|
||||
*
|
||||
* @param {Object} album The album object
|
||||
* @param {Boolean} shuffle Whether to shuffle the songs
|
||||
*/
|
||||
playAllInAlbum(album, shuffle = true) {
|
||||
this.queueAndPlay(album.songs, true);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -107,13 +107,14 @@ export default {
|
|||
},
|
||||
|
||||
/**
|
||||
* Get a song's lyrics.
|
||||
* A HTTP request will be made if the song has no lyrics attribute yet.
|
||||
* Get extra song information (lyrics, artist info, album info).
|
||||
*
|
||||
* @param object song
|
||||
* @param function cb
|
||||
* @param {Object} song
|
||||
* @param {Function} cb
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
getLyrics(song, cb = null) {
|
||||
getInfo(song, cb = null) {
|
||||
if (!_.isUndefined(song.lyrics)) {
|
||||
if (cb) {
|
||||
cb();
|
||||
|
@ -122,12 +123,31 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
http.get(`${song.id}/lyrics`, lyrics => {
|
||||
song.lyrics = lyrics;
|
||||
http.get(`${song.id}/info`, data => {
|
||||
song.lyrics = data.lyrics;
|
||||
|
||||
// If the artist image is not in a nice form, don't use it.
|
||||
if (data.artist_info && typeof data.artist_info.image !== 'string') {
|
||||
data.artist_info.image = null;
|
||||
}
|
||||
|
||||
song.album.artist.info = data.artist_info;
|
||||
|
||||
// Convert the duration into i:s
|
||||
if (data.album_info && data.album_info.tracks) {
|
||||
_.each(data.album_info.tracks, track => track.fmtLength = utils.secondsToHis(track.length));
|
||||
}
|
||||
|
||||
// If the album cover is not in a nice form, don't use it.
|
||||
if (data.album_info && typeof data.album_info.image !== 'string') {
|
||||
data.album_info.image = null;
|
||||
}
|
||||
|
||||
song.album.info = data.album_info;
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -104,3 +104,42 @@
|
|||
box-shadow: inset 0px 10px 10px -10px rgba(0,0,0,1);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin button-group() {
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
$buttonHeight: 28px;
|
||||
background-color: $colorHighlight;
|
||||
font-size: 12px;
|
||||
height: $buttonHeight;
|
||||
padding: 0 16px;
|
||||
line-height: $buttonHeight;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
|
||||
border-radius: $buttonHeight/2 0px 0px $buttonHeight/2;
|
||||
|
||||
&:last-of-type {
|
||||
border-top-right-radius: $buttonHeight/2;
|
||||
border-bottom-right-radius: $buttonHeight/2;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: darken($colorHighlight, 10%);
|
||||
}
|
||||
|
||||
@include inset-when-pressed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,9 +43,10 @@ a, a:link, a:visited {
|
|||
}
|
||||
|
||||
.clear, .clearfix {
|
||||
&:after {
|
||||
&::after {
|
||||
content: " ";
|
||||
clear: both;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ class ArtistTest extends TestCase
|
|||
|
||||
public function testUtf16Names()
|
||||
{
|
||||
$name = file_get_contents(dirname(__FILE__).'/stubs/utf16');
|
||||
$name = file_get_contents(dirname(__FILE__).'/blobs/utf16');
|
||||
|
||||
$artist = Artist::get($name);
|
||||
$artist = Artist::get($name); // to make sure there's no constraint exception
|
||||
|
|
88
tests/LastfmTest.php
Normal file
88
tests/LastfmTest.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Services\Lastfm;
|
||||
|
||||
class LastfmTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions, WithoutMiddleware;
|
||||
|
||||
public function testGetArtistInfo()
|
||||
{
|
||||
$client = \Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(dirname(__FILE__).'/blobs/lastfm/artist.xml')),
|
||||
]);
|
||||
|
||||
$api = new Lastfm(null, null, $client);
|
||||
|
||||
$this->assertEquals([
|
||||
'url' => 'http://www.last.fm/music/Kamelot',
|
||||
'image' => 'http://foo.bar/extralarge.jpg',
|
||||
'bio' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $api->getArtistInfo('foo'));
|
||||
|
||||
// Is it cached?
|
||||
$this->assertNotNull(Cache::get(md5('lastfm_artist_foo')));
|
||||
}
|
||||
|
||||
public function testGetArtistInfoFailed()
|
||||
{
|
||||
$client = \Mockery::mock(Client::class, [
|
||||
'get' => new Response(400, [], file_get_contents(dirname(__FILE__).'/blobs/lastfm/artist-notfound.xml')),
|
||||
]);
|
||||
|
||||
$api = new Lastfm(null, null, $client);
|
||||
|
||||
$this->assertFalse($api->getArtistInfo('foo'));
|
||||
}
|
||||
|
||||
public function testGetAlbumInfo()
|
||||
{
|
||||
$client = \Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], file_get_contents(dirname(__FILE__).'/blobs/lastfm/album.xml')),
|
||||
]);
|
||||
|
||||
$api = new Lastfm(null, null, $client);
|
||||
|
||||
$this->assertEquals([
|
||||
'url' => 'http://www.last.fm/music/Kamelot/Epica',
|
||||
'image' => 'http://foo.bar/extralarge.jpg',
|
||||
'tracks' => [
|
||||
[
|
||||
'title' => 'Track 1',
|
||||
'url' => 'http://foo/track1',
|
||||
'length' => 100,
|
||||
],
|
||||
[
|
||||
'title' => 'Track 2',
|
||||
'url' => 'http://foo/track2',
|
||||
'length' => 150,
|
||||
],
|
||||
],
|
||||
'wiki' => [
|
||||
'summary' => 'Quisque ut nisi.',
|
||||
'full' => 'Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.',
|
||||
],
|
||||
], $api->getAlbumInfo('foo', 'bar'));
|
||||
|
||||
// Is it cached?
|
||||
$this->assertNotNull(Cache::get(md5('lastfm_album_foo_bar')));
|
||||
}
|
||||
|
||||
public function testGetAlbumInfoFailed()
|
||||
{
|
||||
$client = \Mockery::mock(Client::class, [
|
||||
'get' => new Response(400, [], file_get_contents(dirname(__FILE__).'/blobs/lastfm/album-notfound.xml')),
|
||||
]);
|
||||
|
||||
$api = new Lastfm(null, null, $client);
|
||||
|
||||
$this->assertFalse($api->getAlbumInfo('foo', 'bar'));
|
||||
}
|
||||
}
|
37
tests/RESTfulAPIServiceTest.php
Normal file
37
tests/RESTfulAPIServiceTest.php
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Foundation\Testing\WithoutMiddleware;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use App\Services\RESTfulService;
|
||||
|
||||
class RESTfulAPIServiceTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions, WithoutMiddleware;
|
||||
|
||||
public function testUrlConstruction()
|
||||
{
|
||||
$api = new RESTfulService('bar', null, 'http://foo.com', \Mockery::mock(Client::class));
|
||||
$this->assertEquals('http://foo.com/get/param?key=bar', $api->buildUrl('get/param'));
|
||||
$this->assertEquals('http://foo.com/get/param?baz=moo&key=bar', $api->buildUrl('/get/param?baz=moo'));
|
||||
$this->assertEquals('http://baz.com/?key=bar', $api->buildUrl('http://baz.com/'));
|
||||
}
|
||||
|
||||
public function testRequest()
|
||||
{
|
||||
$client = \Mockery::mock(Client::class, [
|
||||
'get' => new Response(200, [], '{"foo":"bar"}'),
|
||||
'post' => new Response(200, [], '{"foo":"bar"}'),
|
||||
'delete' => new Response(200, [], '{"foo":"bar"}'),
|
||||
'put' => new Response(200, [], '{"foo":"bar"}'),
|
||||
]);
|
||||
|
||||
$api = new RESTfulService('foo', null, 'http://foo.com', $client);
|
||||
|
||||
$this->assertObjectHasAttribute('foo', $api->get('/'));
|
||||
$this->assertObjectHasAttribute('foo', $api->post('/'));
|
||||
$this->assertObjectHasAttribute('foo', $api->put('/'));
|
||||
$this->assertObjectHasAttribute('foo', $api->delete('/'));
|
||||
}
|
||||
}
|
3
tests/blobs/lastfm/album-notfound.xml
Normal file
3
tests/blobs/lastfm/album-notfound.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<lfm status="failed"><error code="6">Album not found</error>
|
||||
</lfm>
|
54
tests/blobs/lastfm/album.xml
Normal file
54
tests/blobs/lastfm/album.xml
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<lfm status="ok"><album><name>Epica</name>
|
||||
<artist>Kamelot</artist>
|
||||
<mbid>4c5b22d5-e901-3e0c-89b1-ded24953449a</mbid>
|
||||
<url>http://www.last.fm/music/Kamelot/Epica</url>
|
||||
<image size="small">http://foo.bar/small.jpg</image>
|
||||
<image size="medium">http://foo.bar/medium.jpg</image>
|
||||
<image size="large">http://foo.bar/large.jpg</image>
|
||||
<image size="extralarge">http://foo.bar/extralarge.jpg</image>
|
||||
<image size="mega">http://foo.bar/mega.jpg</image>
|
||||
<image size="">http://foo.bar/fuckisthis.jpg</image>
|
||||
<listeners>184505</listeners>
|
||||
<playcount>4433646</playcount>
|
||||
<tracks><track rank="1"><name>Track 1</name>
|
||||
<url>http://foo/track1</url>
|
||||
<duration>100</duration>
|
||||
<streamable fulltrack="0">0</streamable>
|
||||
<artist><name>Kamelot</name>
|
||||
<mbid>46f4cd77-8870-4ad8-a205-1bee18901d0f</mbid>
|
||||
<url>http://www.last.fm/music/Kamelot</url>
|
||||
</artist>
|
||||
</track>
|
||||
<track rank="2"><name>Track 2</name>
|
||||
<url>http://foo/track2</url>
|
||||
<duration>150</duration>
|
||||
<streamable fulltrack="0">0</streamable>
|
||||
<artist><name>Kamelot</name>
|
||||
<mbid>46f4cd77-8870-4ad8-a205-1bee18901d0f</mbid>
|
||||
<url>http://www.last.fm/music/Kamelot</url>
|
||||
</artist>
|
||||
</track>
|
||||
</tracks>
|
||||
<tags><tag><name>Power metal</name>
|
||||
<url>http://www.last.fm/tag/Power+metal</url>
|
||||
</tag>
|
||||
<tag><name>symphonic metal</name>
|
||||
<url>http://www.last.fm/tag/symphonic+metal</url>
|
||||
</tag>
|
||||
<tag><name>Progressive metal</name>
|
||||
<url>http://www.last.fm/tag/Progressive+metal</url>
|
||||
</tag>
|
||||
<tag><name>Melodic Power Metal</name>
|
||||
<url>http://www.last.fm/tag/Melodic+Power+Metal</url>
|
||||
</tag>
|
||||
<tag><name>albums I own</name>
|
||||
<url>http://www.last.fm/tag/albums+I+own</url>
|
||||
</tag>
|
||||
</tags>
|
||||
<wiki><published>03 Sep 2008, 01:16</published>
|
||||
<summary>Quisque ut nisi.</summary>
|
||||
<content>Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.</content>
|
||||
</wiki>
|
||||
</album>
|
||||
</lfm>
|
3
tests/blobs/lastfm/artist-notfound.xml
Normal file
3
tests/blobs/lastfm/artist-notfound.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<lfm status="failed"><error code="6">The artist you supplied could not be found</error>
|
||||
</lfm>
|
58
tests/blobs/lastfm/artist.xml
Normal file
58
tests/blobs/lastfm/artist.xml
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<lfm status="ok"><artist><name>Kamelot</name>
|
||||
<mbid>46f4cd77-8870-4ad8-a205-1bee18901d0f</mbid>
|
||||
<url>http://www.last.fm/music/Kamelot</url>
|
||||
<image size="small">http://foo.bar/small.jpg</image>
|
||||
<image size="medium">http://foo.bar/medium.jpg</image>
|
||||
<image size="large">http://foo.bar/large.jpg</image>
|
||||
<image size="extralarge">http://foo.bar/extralarge.jpg</image>
|
||||
<image size="mega">http://foo.bar/mega.jpg</image>
|
||||
<image size="">http://foo.bar/fuckisthis.jpg</image>
|
||||
<streamable>0</streamable>
|
||||
<ontour>0</ontour>
|
||||
<stats><listeners>446805</listeners>
|
||||
<playcount>30274291</playcount>
|
||||
</stats>
|
||||
<similar><artist><name>Serenity</name>
|
||||
<url>http://www.last.fm/music/Serenity</url>
|
||||
<image size="small">http://img2-ak.lst.fm/i/u/34s/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
<image size="medium">http://img2-ak.lst.fm/i/u/64s/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
<image size="large">http://img2-ak.lst.fm/i/u/174s/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
<image size="extralarge">http://img2-ak.lst.fm/i/u/300x300/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
<image size="mega">http://img2-ak.lst.fm/i/u/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
<image size="">http://img2-ak.lst.fm/i/u/arQ/98efb50f63fd430d815fed3bb151b8e4.png</image>
|
||||
</artist>
|
||||
<artist><name>Seventh Wonder</name>
|
||||
<url>http://www.last.fm/music/Seventh+Wonder</url>
|
||||
<image size="small">http://img2-ak.lst.fm/i/u/34s/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
<image size="medium">http://img2-ak.lst.fm/i/u/64s/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
<image size="large">http://img2-ak.lst.fm/i/u/174s/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
<image size="extralarge">http://img2-ak.lst.fm/i/u/300x300/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
<image size="mega">http://img2-ak.lst.fm/i/u/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
<image size="">http://img2-ak.lst.fm/i/u/arQ/9bb1ed430424492dc9a39bc5d0ec00dc.png</image>
|
||||
</artist>
|
||||
</similar>
|
||||
<tags><tag><name>Power metal</name>
|
||||
<url>http://www.last.fm/tag/Power+metal</url>
|
||||
</tag>
|
||||
<tag><name>symphonic metal</name>
|
||||
<url>http://www.last.fm/tag/symphonic+metal</url>
|
||||
</tag>
|
||||
<tag><name>Progressive metal</name>
|
||||
<url>http://www.last.fm/tag/Progressive+metal</url>
|
||||
</tag>
|
||||
<tag><name>metal</name>
|
||||
<url>http://www.last.fm/tag/metal</url>
|
||||
</tag>
|
||||
<tag><name>melodic metal</name>
|
||||
<url>http://www.last.fm/tag/melodic+metal</url>
|
||||
</tag>
|
||||
</tags>
|
||||
<bio><links><link rel="original" href="http://last.fm/music/Kamelot/+wiki"></link>
|
||||
</links>
|
||||
<published>10 Feb 2006, 13:43</published>
|
||||
<summary>Quisque ut nisi.</summary>
|
||||
<content>Quisque ut nisi. Vestibulum ullamcorper mauris at ligula.</content>
|
||||
</bio>
|
||||
</artist>
|
||||
</lfm>
|
Loading…
Reference in a new issue