Merge pull request #54 from Poniverse/feature/search

#1: The search feature
This commit is contained in:
Peter Deltchev 2016-01-17 08:04:42 -08:00
commit bfc17a2edb
40 changed files with 1773 additions and 233 deletions

View file

@ -54,6 +54,7 @@ class AddTrackToPlaylistCommand extends CommandBase
{
$songIndex = $this->_playlist->tracks()->count() + 1;
$this->_playlist->tracks()->attach($this->_track, ['position' => $songIndex]);
$this->_playlist->touch();
Playlist::whereId($this->_playlist->id)->update([
'track_count' => DB::raw('(SELECT COUNT(id) FROM playlist_track WHERE playlist_id = ' . $this->_playlist->id . ')')

View file

@ -0,0 +1,121 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\User;
use Symfony\Component\Console\Helper\ProgressBar;
class RebuildSearchIndex extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'rebuild:search';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Rebuilds the Elasticsearch index.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$totalTracks = Track::withTrashed()->count();
$totalAlbums = Album::withTrashed()->count();
$totalPlaylists = Playlist::withTrashed()->count();
$totalUsers = User::count();
$trackProgress = $this->output->createProgressBar($totalTracks);
$this->info("Processing tracks...");
Track::withTrashed()->chunk(200, function(Collection $tracks) use ($trackProgress) {
foreach($tracks as $track) {
/** @var Track $track */
$trackProgress->advance();
$track->updateElasticsearchEntry();
}
});
$trackProgress->finish();
$this->line('');
$albumProgress = $this->output->createProgressBar($totalAlbums);
$this->info("Processing albums...");
Album::withTrashed()->chunk(200, function(Collection $albums) use ($albumProgress) {
foreach($albums as $album) {
/** @var Album $album */
$albumProgress->advance();
$album->updateElasticsearchEntry();
}
});
$albumProgress->finish();
$this->line('');
$playlistProgress = $this->output->createProgressBar($totalPlaylists);
$this->info("Processing playlists...");
Playlist::withTrashed()->chunk(200, function(Collection $playlists) use ($playlistProgress) {
foreach($playlists as $playlist) {
/** @var Playlist $playlist */
$playlistProgress->advance();
$playlist->updateElasticsearchEntry();
}
});
$playlistProgress->finish();
$this->line('');
$userProgress = $this->output->createProgressBar($totalUsers);
$this->info("Processing users...");
User::chunk(200, function(Collection $users) use ($userProgress) {
foreach($users as $user) {
/** @var User $user */
$userProgress->advance();
$user->updateElasticsearchEntry();
}
});
$userProgress->finish();
$this->line('');
$this->info('Everything has been queued for re-indexing!');
}
}

View file

@ -42,6 +42,7 @@ class Kernel extends ConsoleKernel
\Poniverse\Ponyfm\Console\Commands\RebuildTrackCache::class,
\Poniverse\Ponyfm\Console\Commands\RebuildTrack::class,
\Poniverse\Ponyfm\Console\Commands\RebuildFilesizes::class,
\Poniverse\Ponyfm\Console\Commands\RebuildSearchIndex::class,
\Poniverse\Ponyfm\Console\Commands\MergeDuplicateAccounts::class,
];

View file

@ -0,0 +1,39 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Contracts;
interface Searchable {
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array;
/**
* @return bool whether this particular object should be indexed or not
*/
public function shouldBeIndexed():bool;
public function updateElasticsearchEntry();
public function updateElasticsearchEntrySynchronously();
}

View file

@ -195,18 +195,7 @@ class ArtistsController extends ApiControllerBase
$users = [];
foreach ($query->get() as $user) {
$users[] = [
'id' => $user->id,
'name' => $user->display_name,
'slug' => $user->slug,
'url' => $user->url,
'is_archived' => $user->is_archived,
'avatars' => [
'small' => $user->getAvatarUrl(Image::SMALL),
'normal' => $user->getAvatarUrl(Image::NORMAL)
],
'created_at' => $user->created_at
];
$users[] = User::mapPublicUserSummary($user);
}
return Response::json(["artists" => $users, "current_page" => $page, "total_pages" => ceil($count / $perPage)],

View file

@ -0,0 +1,39 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Elasticsearch;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Input;
use Poniverse\Ponyfm\Library\Search;
use Response;
class SearchController extends ApiControllerBase
{
public function getSearch(Search $search)
{
$results = $search->searchAllContent(Input::query('query'));
return Response::json([
'results' => $results,
], 200);
}
}

View file

@ -82,8 +82,7 @@ Route::group(['prefix' => 'api/v1', 'middleware' => 'json-exceptions'], function
Route::group(['prefix' => 'api/web'], function() {
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');
Route::get('/playlists/show/{id}', 'Api\Web\PlaylistsController@getShow');
Route::get('/search', ['middleware' => ['auth', 'can:access-search'], 'uses' => 'Api\Web\SearchController@getSearch']);
Route::get('/tracks', 'Api\Web\TracksController@getIndex');
Route::get('/tracks/{id}', 'Api\Web\TracksController@getShow')->where('id', '\d+');
@ -94,6 +93,7 @@ Route::group(['prefix' => 'api/web'], function() {
Route::get('/albums/cached/{id}/{format}', 'Api\Web\AlbumsController@getCachedAlbum')->where(['id' => '\d+', 'format' => '.+']);
Route::get('/playlists', 'Api\Web\PlaylistsController@getIndex');
Route::get('/playlists/show/{id}', 'Api\Web\PlaylistsController@getShow');
Route::get('/playlists/{id}', 'Api\Web\PlaylistsController@getShow')->where('id', '\d+');
Route::get('/playlists/cached/{id}/{format}', 'Api\Web\PlaylistsController@getCachedPlaylist')->where(['id' => '\d+', 'format' => '.+']);

View file

@ -0,0 +1,56 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Queue\InteractsWithQueue;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Jobs\Job;
use Illuminate\Contracts\Bus\SelfHandling;
use SerializesModels;
class UpdateSearchIndexForEntity extends Job implements SelfHandling, ShouldQueue
{
use InteractsWithQueue, SerializesModels;
protected $entity;
/**
* Create a new job instance.
*
* @param Model $entity
*/
public function __construct(Searchable $entity)
{
$this->entity = $entity;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->entity->updateElasticsearchEntrySynchronously();
}
}

206
app/Library/Search.php Normal file
View file

@ -0,0 +1,206 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Library;
use DB;
use Elasticsearch\Client;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\User;
class Search {
protected $elasticsearch;
protected $index;
public function __construct(Client $connection, string $indexName) {
$this->elasticsearch = $connection;
$this->index = $indexName;
}
/**
* @param string $query
* @param int $resultsPerContentType
* @return array
*/
public function searchAllContent(string $query) {
$results = $this->elasticsearch->msearch([
'index' => $this->index,
'body' => [
//===== Tracks=====//
['type' => 'track'],
[
'query' => [
'multi_match' => [
'query' => $query,
'fields' => [
'title^3',
'artist^2',
'genre',
'track_type',
'show_songs^2',
],
'tie_breaker' => 0.3,
],
],
'size' => 11
],
//===== Albums =====//
['type' => 'album'],
[
'query' => [
'multi_match' => [
'query' => $query,
'fields' => [
'title^2',
'artist',
'tracks',
],
'tie_breaker' => 0.3,
],
],
'size' => 3
],
//===== Playlists =====//
['type' => 'playlist'],
[
'query' => [
'multi_match' => [
'query' => $query,
'fields' => [
'title^3',
'curator',
'tracks^2',
],
'tie_breaker' => 0.3,
],
],
'size' => 3
],
//===== Users =====//
['type' => 'user'],
[
'query' => [
'multi_match' => [
'query' => $query,
'fields' => [
'display_name',
'tracks',
],
'tie_breaker' => 0.3,
],
],
'size' => 3
],
]
]);
$tracks = $this->transformTracks($results['responses'][0]['hits']['hits']);
$albums = $this->transformAlbums($results['responses'][1]['hits']['hits']);
$playlists = $this->transformPlaylists($results['responses'][2]['hits']['hits']);
$users = $this->transformUsers($results['responses'][3]['hits']['hits']);
return [
'tracks' => $tracks,
'albums' => $albums,
'playlists' => $playlists,
'users' => $users
];
}
protected function transformTracks(array $searchHits) {
$tracks = $this->transformToEloquent(Track::class, $searchHits);
$tracks = $tracks->map(function (Track $track) {
return Track::mapPublicTrackSummary($track);
});
return $tracks;
}
protected function transformAlbums(array $searchHits) {
$albums = $this->transformToEloquent(Album::class, $searchHits);
$albums = $albums->map(function (Album $album) {
return Album::mapPublicAlbumSummary($album);
});
return $albums;
}
protected function transformPlaylists(array $searchHits) {
$playlists = $this->transformToEloquent(Playlist::class, $searchHits);
$playlists = $playlists->map(function (Playlist $playlist) {
return Playlist::mapPublicPlaylistSummary($playlist);
});
return $playlists;
}
protected function transformUsers(array $searchHits) {
$users = $this->transformToEloquent(User::class, $searchHits);
$users = $users->map(function (User $user) {
return User::mapPublicUserSummary($user);
});
return $users;
}
/**
* Transforms the given Elasticsearch results into a collection of corresponding
* Eloquent models.
*
* This method assumes that the given class uses soft deletes.
*
* @param string $modelClass The Eloquent model class to instantiate these results as
* @param array $searchHits
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function transformToEloquent(string $modelClass, array $searchHits) {
if (empty($searchHits)) {
return new Collection();
}
$ids = [];
$caseStatement = 'CASE id ';
$i = 0;
foreach ($searchHits as $result) {
$ids[$result['_id']] = $result['_score'];
$caseStatement .= "WHEN ${result['_id']} THEN $i ";
$i++;
}
$caseStatement .= 'END';
/** @var Builder $modelInstances */
$modelInstances = $modelClass::query();
if (method_exists($modelClass, 'withTrashed')) {
$modelInstances = $modelInstances->withTrashed();
}
$modelInstances = $modelInstances
->whereIn('id', array_keys($ids))
->orderBy(DB::raw($caseStatement))
->get();
return $modelInstances;
}
}

View file

@ -27,7 +27,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Auth;
use Cache;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\TrackCollection;
use Poniverse\Ponyfm\Traits\SlugTrait;
use Venturecraft\Revisionable\RevisionableTrait;
@ -59,9 +61,11 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Album userDetails()
*/
class Album extends Model
class Album extends Model implements Searchable
{
use SoftDeletes, SlugTrait, DispatchesJobs, TrackCollection, RevisionableTrait;
use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'album';
protected $dates = ['deleted_at'];
protected $fillable = ['user_id', 'title', 'slug'];
@ -403,4 +407,25 @@ class Album extends Model
protected function recountTracks() {
$this->track_count = $this->tracks->count();
}
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'title' => $this->title,
'artist' => $this->user->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->track_count > 0 && !$this->trashed();
}
}

View file

@ -26,7 +26,9 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Auth;
use Cache;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\TrackCollection;
use Poniverse\Ponyfm\Traits\SlugTrait;
use Venturecraft\Revisionable\RevisionableTrait;
@ -58,13 +60,27 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Playlist userDetails()
*/
class Playlist extends Model
class Playlist extends Model implements Searchable
{
use SoftDeletes, SlugTrait, DispatchesJobs, TrackCollection, RevisionableTrait;
use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'playlist';
protected $table = 'playlists';
protected $dates = ['deleted_at'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'title' => 'string',
'description' => 'string',
'is_public' => 'boolean',
'track_count' => 'integer',
'view_count' => 'integer',
'download_count' => 'integer',
'favourte_count' => 'integer',
'follow_count' => 'integer',
'comment_count' => 'integer',
];
public static function summary()
{
@ -285,4 +301,27 @@ class Playlist extends Model
{
return 'playlist-' . $this->id . '-' . $key;
}
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'title' => $this->title,
'curator' => $this->user->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->is_public &&
$this->track_count > 0 &&
!$this->trashed();
}
}

View file

@ -24,7 +24,10 @@ use Auth;
use Cache;
use Config;
use DB;
use Elasticsearch;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\SlugTrait;
use Exception;
use External;
@ -93,9 +96,11 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track withComments()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track mlpma()
*/
class Track extends Model
class Track extends Model implements Searchable
{
use SoftDeletes;
use SoftDeletes, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'track';
protected $dates = ['deleted_at', 'published_at', 'released_at'];
protected $hidden = ['original_tags', 'metadata'];
@ -826,4 +831,28 @@ class Track extends Model
{
return 'track-' . $this->id . '-' . $key;
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->is_listed &&
$this->published_at !== null &&
!$this->trashed();
}
/**
* @inheritdoc
*/
public function toElasticsearch():array {
return [
'title' => $this->title,
'artist' => $this->user->display_name,
'published_at' => $this->published_at ? $this->published_at->toIso8601String() : null,
'genre' => $this->genre->name,
'track_type' => $this->trackType->title,
'show_songs' => $this->showSongs->pluck('title')
];
}
}

View file

@ -29,6 +29,8 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Auth;
use Illuminate\Support\Str;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Venturecraft\Revisionable\RevisionableTrait;
/**
@ -62,9 +64,11 @@ use Venturecraft\Revisionable\RevisionableTrait;
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User userDetails()
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable
{
use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait;
use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'user';
protected $table = 'users';
protected $casts = [
@ -247,4 +251,40 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return false;
}
public static function mapPublicUserSummary(User $user) {
return [
'id' => $user->id,
'name' => $user->display_name,
'slug' => $user->slug,
'url' => $user->url,
'is_archived' => $user->is_archived,
'avatars' => [
'small' => $user->getAvatarUrl(Image::SMALL),
'normal' => $user->getAvatarUrl(Image::NORMAL)
],
'created_at' => $user->created_at
];
}
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'username' => $this->username,
'display_name' => $this->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->disabled_at === null;
}
}

View file

@ -20,8 +20,6 @@
namespace Poniverse\Ponyfm\Providers;
use DB;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
use PfmValidator;
@ -53,5 +51,12 @@ class AppServiceProvider extends ServiceProvider
$this->app->bind(Poniverse::class, function(Application $app) {
return new Poniverse($app['config']->get('poniverse.client_id'), $app['config']->get('poniverse.secret'));
});
$this->app->bind(Poniverse\Ponyfm\Library\Search::class, function(Application $app) {
return new Poniverse\Ponyfm\Library\Search(
\Elasticsearch::connection(),
$app['config']->get('ponyfm.elasticsearch_index')
);
});
}
}

View file

@ -48,6 +48,10 @@ class AuthServiceProvider extends ServiceProvider
*/
public function boot(GateContract $gate)
{
$gate->define('access-search', function(User $user) {
return $user->hasRole('admin') || $user->hasRole('moderator');
});
$gate->define('access-admin-area', function(User $user) {
return $user->hasRole('admin');
});

View file

@ -0,0 +1,111 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Traits;
use Config;
use Elasticsearch;
use Elasticsearch\Common\Exceptions\Missing404Exception;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Jobs\UpdateSearchIndexForEntity;
/**
* Class IndexedInElasticsearch
*
* Classes using this trait must declare the `$elasticsearchType` property and
* implement the `Searchable` interface.
*
* @package Poniverse\Ponyfm\Traits
*/
trait IndexedInElasticsearchTrait
{
use DispatchesJobs;
// These two functions are from the Searchable interface. They're included
// here, without being implemented, to assist IDE's when editing this trait.
public abstract function toElasticsearch():array;
public abstract function shouldBeIndexed():bool;
// Laravel automatically runs this method based on the trait's name. #magic
public static function bootIndexedInElasticsearchTrait() {
static::saved(function (Searchable $entity) {
$entity->updateElasticsearchEntry();
});
static::deleted(function (Searchable $entity) {
$entity->updateElasticsearchEntry();
});
}
/**
* @param bool $includeBody set to false when deleting documents
* @return array
*/
private function getElasticsearchParameters(bool $includeBody = true) {
$parameters = [
'index' => Config::get('ponyfm.elasticsearch_index'),
'type' => $this->elasticsearchType,
'id' => $this->id,
];
if ($includeBody) {
$parameters['body'] = $this->toElasticsearch();
}
return $parameters;
}
private function createOrUpdateElasticsearchEntry() {
Elasticsearch::connection()->index($this->getElasticsearchParameters());
}
private function deleteElasticsearchEntry() {
try {
Elasticsearch::connection()->delete($this->getElasticsearchParameters(false));
} catch (Missing404Exception $e) {
// If the entity we're trying to delete isn't indexed in Elasticsearch,
// that's fine.
}
}
/**
* Asynchronously updates the Elasticsearch entry.
* When in doubt, this is the method to use.
*/
public function updateElasticsearchEntry() {
$job = (new UpdateSearchIndexForEntity($this))->onQueue(Config::get('ponyfm.indexing_queue'));
$this->dispatch($job);
}
/**
* Synchronously updates the Elasticsearch entry. This should only be
* called from the UpdateSearchIndexForEntity job.
*/
public function updateElasticsearchEntrySynchronously() {
if ($this->shouldBeIndexed()) {
$this->createOrUpdateElasticsearchEntry();
} else {
$this->deleteElasticsearchEntry();
}
}
}

View file

@ -13,7 +13,8 @@
"guzzlehttp/guzzle": "~6.0",
"doctrine/dbal": "^2.5",
"venturecraft/revisionable": "^1.23",
"pda/pheanstalk": "~3.0"
"pda/pheanstalk": "~3.0",
"cviebrock/laravel-elasticsearch": "^1.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",

492
composer.lock generated
View file

@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "d99fa4165b8a1c9e929248bbb16183ad",
"content-hash": "31347c82515003f78f1f9adef010f1f5",
"hash": "0deb7713636ee82aadee47da3a9217cc",
"content-hash": "9dea148233d815e53eb636413f2bcaed",
"packages": [
{
"name": "barryvdh/laravel-ide-helper",
@ -168,6 +168,55 @@
],
"time": "2013-05-05 09:10:04"
},
{
"name": "cviebrock/laravel-elasticsearch",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/cviebrock/laravel-elasticsearch.git",
"reference": "52aa1f8228006cb0bb60954e26c068af523bf47b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cviebrock/laravel-elasticsearch/zipball/52aa1f8228006cb0bb60954e26c068af523bf47b",
"reference": "52aa1f8228006cb0bb60954e26c068af523bf47b",
"shasum": ""
},
"require": {
"elasticsearch/elasticsearch": "^2.0",
"illuminate/support": "~4|~5",
"monolog/monolog": "~1",
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Cviebrock\\LaravelElasticsearch\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Colin Viebrock",
"email": "colin@viebrock.ca"
},
{
"name": "Brandon Martel",
"email": "brandonmartel@gmail.com"
}
],
"description": "An easy way to use the official PHP ElasticSearch client in your Laravel applications",
"keywords": [
"client",
"elasticsearch",
"laravel",
"search"
],
"time": "2016-01-06 15:58:07"
},
{
"name": "danielstjules/stringy",
"version": "1.10.0",
@ -327,33 +376,33 @@
},
{
"name": "doctrine/cache",
"version": "v1.5.4",
"version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136"
"reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/47cdc76ceb95cc591d9c79a36dc3794975b5d136",
"reference": "47cdc76ceb95cc591d9c79a36dc3794975b5d136",
"url": "https://api.github.com/repos/doctrine/cache/zipball/f8af318d14bdb0eff0336795b428b547bd39ccb6",
"reference": "f8af318d14bdb0eff0336795b428b547bd39ccb6",
"shasum": ""
},
"require": {
"php": ">=5.3.2"
"php": "~5.5|~7.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"phpunit/phpunit": ">=3.7",
"phpunit/phpunit": "~4.8|~5.0",
"predis/predis": "~1.0",
"satooshi/php-coveralls": "~0.6"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.5.x-dev"
"dev-master": "1.6.x-dev"
}
},
"autoload": {
@ -393,7 +442,7 @@
"cache",
"caching"
],
"time": "2015-12-19 05:03:47"
"time": "2015-12-31 16:37:02"
},
{
"name": "doctrine/collections",
@ -463,16 +512,16 @@
},
{
"name": "doctrine/common",
"version": "v2.5.2",
"version": "v2.6.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
"reference": "311001fd9865a4d0d59efff4eac6d7dcb3f5270c"
"reference": "a579557bc689580c19fee4e27487a67fe60defc0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/common/zipball/311001fd9865a4d0d59efff4eac6d7dcb3f5270c",
"reference": "311001fd9865a4d0d59efff4eac6d7dcb3f5270c",
"url": "https://api.github.com/repos/doctrine/common/zipball/a579557bc689580c19fee4e27487a67fe60defc0",
"reference": "a579557bc689580c19fee4e27487a67fe60defc0",
"shasum": ""
},
"require": {
@ -481,20 +530,20 @@
"doctrine/collections": "1.*",
"doctrine/inflector": "1.*",
"doctrine/lexer": "1.*",
"php": ">=5.3.2"
"php": "~5.5|~7.0"
},
"require-dev": {
"phpunit/phpunit": "~3.7"
"phpunit/phpunit": "~4.8|~5.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.5.x-dev"
"dev-master": "2.7.x-dev"
}
},
"autoload": {
"psr-0": {
"Doctrine\\Common\\": "lib/"
"psr-4": {
"Doctrine\\Common\\": "lib/Doctrine/Common"
}
},
"notification-url": "https://packagist.org/downloads/",
@ -532,24 +581,24 @@
"persistence",
"spl"
],
"time": "2015-12-04 12:49:42"
"time": "2015-12-25 13:18:31"
},
{
"name": "doctrine/dbal",
"version": "v2.5.2",
"version": "v2.5.4",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "01dbcbc5cd0a913d751418e635434a18a2f2a75c"
"reference": "abbdfd1cff43a7b99d027af3be709bc8fc7d4769"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/01dbcbc5cd0a913d751418e635434a18a2f2a75c",
"reference": "01dbcbc5cd0a913d751418e635434a18a2f2a75c",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/abbdfd1cff43a7b99d027af3be709bc8fc7d4769",
"reference": "abbdfd1cff43a7b99d027af3be709bc8fc7d4769",
"shasum": ""
},
"require": {
"doctrine/common": ">=2.4,<2.6-dev",
"doctrine/common": ">=2.4,<2.7-dev",
"php": ">=5.3.2"
},
"require-dev": {
@ -603,7 +652,7 @@
"persistence",
"queryobject"
],
"time": "2015-09-16 16:29:33"
"time": "2016-01-05 22:11:12"
},
{
"name": "doctrine/inflector",
@ -726,6 +775,60 @@
],
"time": "2014-09-09 13:34:57"
},
{
"name": "elasticsearch/elasticsearch",
"version": "v2.0.3",
"source": {
"type": "git",
"url": "https://github.com/elastic/elasticsearch-php.git",
"reference": "9ce5bd7606f6c185d434de4f80863f998f74e179"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/elastic/elasticsearch-php/zipball/9ce5bd7606f6c185d434de4f80863f998f74e179",
"reference": "9ce5bd7606f6c185d434de4f80863f998f74e179",
"shasum": ""
},
"require": {
"guzzlehttp/ringphp": "~1.0",
"php": ">=5.4",
"psr/log": "~1.0"
},
"require-dev": {
"athletic/athletic": "~0.1",
"cpliakas/git-wrapper": "~1.0",
"mockery/mockery": "dev-master@dev",
"phpunit/phpunit": "3.7.*",
"symfony/yaml": "2.4.3 as 2.4.2",
"twig/twig": "1.*"
},
"suggest": {
"ext-curl": "*",
"monolog/monolog": "Allows for client-level logging and tracing"
},
"type": "library",
"autoload": {
"psr-4": {
"Elasticsearch\\": "src/Elasticsearch/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache 2"
],
"authors": [
{
"name": "Zachary Tong"
}
],
"description": "PHP Client for Elasticsearch",
"keywords": [
"client",
"elasticsearch",
"search"
],
"time": "2015-11-05 15:29:21"
},
{
"name": "guzzlehttp/guzzle",
"version": "6.1.1",
@ -897,6 +1000,107 @@
],
"time": "2015-11-03 01:34:55"
},
{
"name": "guzzlehttp/ringphp",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/RingPHP.git",
"reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/RingPHP/zipball/dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
"reference": "dbbb91d7f6c191e5e405e900e3102ac7f261bc0b",
"shasum": ""
},
"require": {
"guzzlehttp/streams": "~3.0",
"php": ">=5.4.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Ring\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"time": "2015-05-20 03:37:09"
},
{
"name": "guzzlehttp/streams",
"version": "3.0.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/streams.git",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/streams/zipball/47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"reference": "47aaa48e27dae43d39fc1cea0ccf0d84ac1a2ba5",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Stream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Provides a simple abstraction over streams of data",
"homepage": "http://guzzlephp.org/",
"keywords": [
"Guzzle",
"stream"
],
"time": "2014-10-12 19:18:40"
},
{
"name": "intouch/laravel-newrelic",
"version": "2.0.0",
@ -1129,16 +1333,16 @@
},
{
"name": "laravel/framework",
"version": "v5.1.27",
"version": "v5.1.28",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "b16f80878fd3603022d3c84593397cedd9af0bcf"
"reference": "3f0fd27939dfdafb1e50058423cd24e640894ba2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/b16f80878fd3603022d3c84593397cedd9af0bcf",
"reference": "b16f80878fd3603022d3c84593397cedd9af0bcf",
"url": "https://api.github.com/repos/laravel/framework/zipball/3f0fd27939dfdafb1e50058423cd24e640894ba2",
"reference": "3f0fd27939dfdafb1e50058423cd24e640894ba2",
"shasum": ""
},
"require": {
@ -1253,7 +1457,7 @@
"framework",
"laravel"
],
"time": "2015-12-17 20:35:38"
"time": "2015-12-31 17:41:30"
},
{
"name": "league/flysystem",
@ -1560,16 +1764,16 @@
},
{
"name": "paragonie/random_compat",
"version": "1.1.4",
"version": "1.1.5",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "d762ee5b099a29044603cd4649851e81aa66cb47"
"reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/d762ee5b099a29044603cd4649851e81aa66cb47",
"reference": "d762ee5b099a29044603cd4649851e81aa66cb47",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/dd8998b7c846f6909f4e7a5f67fabebfc412a4f7",
"reference": "dd8998b7c846f6909f4e7a5f67fabebfc412a4f7",
"shasum": ""
},
"require": {
@ -1604,7 +1808,7 @@
"pseudorandom",
"random"
],
"time": "2015-12-10 14:48:13"
"time": "2016-01-06 13:31:20"
},
{
"name": "pda/pheanstalk",
@ -1864,6 +2068,50 @@
],
"time": "2015-11-12 16:18:56"
},
{
"name": "react/promise",
"version": "v2.2.1",
"source": {
"type": "git",
"url": "https://github.com/reactphp/promise.git",
"reference": "3b6fca09c7d56321057fa8867c8dbe1abf648627"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reactphp/promise/zipball/3b6fca09c7d56321057fa8867c8dbe1abf648627",
"reference": "3b6fca09c7d56321057fa8867c8dbe1abf648627",
"shasum": ""
},
"require": {
"php": ">=5.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"autoload": {
"psr-4": {
"React\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jan Sorgalla",
"email": "jsorgalla@gmail.com"
}
],
"description": "A lightweight implementation of CommonJS Promises/A for PHP",
"time": "2015-07-03 13:48:55"
},
{
"name": "swiftmailer/swiftmailer",
"version": "v5.4.1",
@ -1919,16 +2167,16 @@
},
{
"name": "symfony/class-loader",
"version": "v2.8.0",
"version": "v2.8.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160"
"reference": "98e9089a428ed0e39423b67352c57ef5910a3269"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/class-loader/zipball/51f83451bf0ddfc696e47e4642d6cd10fcfce160",
"reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160",
"url": "https://api.github.com/repos/symfony/class-loader/zipball/98e9089a428ed0e39423b67352c57ef5910a3269",
"reference": "98e9089a428ed0e39423b67352c57ef5910a3269",
"shasum": ""
},
"require": {
@ -1967,20 +2215,20 @@
],
"description": "Symfony ClassLoader Component",
"homepage": "https://symfony.com",
"time": "2015-11-26 07:00:59"
"time": "2016-01-03 15:33:41"
},
{
"name": "symfony/console",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "16bb1cb86df43c90931df65f529e7ebd79636750"
"reference": "d3fc138b6ed8f8074591821d3416d8f9c04d6ca6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/16bb1cb86df43c90931df65f529e7ebd79636750",
"reference": "16bb1cb86df43c90931df65f529e7ebd79636750",
"url": "https://api.github.com/repos/symfony/console/zipball/d3fc138b6ed8f8074591821d3416d8f9c04d6ca6",
"reference": "d3fc138b6ed8f8074591821d3416d8f9c04d6ca6",
"shasum": ""
},
"require": {
@ -2026,20 +2274,20 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2015-11-18 09:54:26"
"time": "2016-01-14 08:26:43"
},
{
"name": "symfony/css-selector",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
"reference": "abb47717fb88aebd9437da2fc8bb01a50a36679f"
"reference": "1a869e59cc3b2802961fc2124139659e12b72fe5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/abb47717fb88aebd9437da2fc8bb01a50a36679f",
"reference": "abb47717fb88aebd9437da2fc8bb01a50a36679f",
"url": "https://api.github.com/repos/symfony/css-selector/zipball/1a869e59cc3b2802961fc2124139659e12b72fe5",
"reference": "1a869e59cc3b2802961fc2124139659e12b72fe5",
"shasum": ""
},
"require": {
@ -2079,20 +2327,20 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"time": "2015-10-30 20:10:21"
"time": "2016-01-03 15:32:00"
},
{
"name": "symfony/debug",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "0dbc119596f4afc82d9b2eb2a7e6a4af1ee763fa"
"reference": "5aca4aa9600b943287b4a1799a4d1d78b5388175"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/0dbc119596f4afc82d9b2eb2a7e6a4af1ee763fa",
"reference": "0dbc119596f4afc82d9b2eb2a7e6a4af1ee763fa",
"url": "https://api.github.com/repos/symfony/debug/zipball/5aca4aa9600b943287b4a1799a4d1d78b5388175",
"reference": "5aca4aa9600b943287b4a1799a4d1d78b5388175",
"shasum": ""
},
"require": {
@ -2136,20 +2384,20 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2015-10-30 20:10:21"
"time": "2016-01-13 07:57:33"
},
{
"name": "symfony/dom-crawler",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "b33593cbfe1d81b50d48353f338aca76a08658d8"
"reference": "55cc79a177193eb3bd74ac54b353691fbb211d3a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b33593cbfe1d81b50d48353f338aca76a08658d8",
"reference": "b33593cbfe1d81b50d48353f338aca76a08658d8",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/55cc79a177193eb3bd74ac54b353691fbb211d3a",
"reference": "55cc79a177193eb3bd74ac54b353691fbb211d3a",
"shasum": ""
},
"require": {
@ -2191,20 +2439,20 @@
],
"description": "Symfony DomCrawler Component",
"homepage": "https://symfony.com",
"time": "2015-11-02 20:20:53"
"time": "2016-01-03 15:32:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v2.8.0",
"version": "v2.8.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc"
"reference": "ee278f7c851533e58ca307f66305ccb9188aceda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5eb815363c0388e83247e7e9853e5dbc14999cc",
"reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ee278f7c851533e58ca307f66305ccb9188aceda",
"reference": "ee278f7c851533e58ca307f66305ccb9188aceda",
"shasum": ""
},
"require": {
@ -2251,20 +2499,20 @@
],
"description": "Symfony EventDispatcher Component",
"homepage": "https://symfony.com",
"time": "2015-10-30 20:15:42"
"time": "2016-01-13 10:28:07"
},
{
"name": "symfony/finder",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "a06a0c0ff7db3736a50d530c908cca547bf13da9"
"reference": "d20ac81c81a67ab898b0c0afa435f3e9a7d460cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/a06a0c0ff7db3736a50d530c908cca547bf13da9",
"reference": "a06a0c0ff7db3736a50d530c908cca547bf13da9",
"url": "https://api.github.com/repos/symfony/finder/zipball/d20ac81c81a67ab898b0c0afa435f3e9a7d460cf",
"reference": "d20ac81c81a67ab898b0c0afa435f3e9a7d460cf",
"shasum": ""
},
"require": {
@ -2300,20 +2548,20 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"time": "2015-10-30 20:10:21"
"time": "2016-01-14 08:26:43"
},
{
"name": "symfony/http-foundation",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "e83a3d105ddaf5a113e803c904fdec552d1f1c35"
"reference": "2f9d240056f026af5f7ba7f7052b0c6709bf288c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/e83a3d105ddaf5a113e803c904fdec552d1f1c35",
"reference": "e83a3d105ddaf5a113e803c904fdec552d1f1c35",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/2f9d240056f026af5f7ba7f7052b0c6709bf288c",
"reference": "2f9d240056f026af5f7ba7f7052b0c6709bf288c",
"shasum": ""
},
"require": {
@ -2355,20 +2603,20 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2015-11-20 17:41:18"
"time": "2016-01-13 10:26:43"
},
{
"name": "symfony/http-kernel",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "5570de31e8fbc03777a8c61eb24f9b626e5e5941"
"reference": "aa2f1e544d6cb862452504b5479a5095b7bfc53f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/5570de31e8fbc03777a8c61eb24f9b626e5e5941",
"reference": "5570de31e8fbc03777a8c61eb24f9b626e5e5941",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/aa2f1e544d6cb862452504b5479a5095b7bfc53f",
"reference": "aa2f1e544d6cb862452504b5479a5095b7bfc53f",
"shasum": ""
},
"require": {
@ -2437,20 +2685,20 @@
],
"description": "Symfony HttpKernel Component",
"homepage": "https://symfony.com",
"time": "2015-11-23 11:57:49"
"time": "2016-01-14 10:41:45"
},
{
"name": "symfony/polyfill-php56",
"version": "v1.0.0",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php56.git",
"reference": "a6bd4770a6967517e6610529e14afaa3111094a3"
"reference": "e2e77609a9e2328eb370fbb0e0d8b2000ebb488f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/a6bd4770a6967517e6610529e14afaa3111094a3",
"reference": "a6bd4770a6967517e6610529e14afaa3111094a3",
"url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/e2e77609a9e2328eb370fbb0e0d8b2000ebb488f",
"reference": "e2e77609a9e2328eb370fbb0e0d8b2000ebb488f",
"shasum": ""
},
"require": {
@ -2493,11 +2741,11 @@
"portable",
"shim"
],
"time": "2015-11-04 20:28:58"
"time": "2015-12-18 15:10:25"
},
{
"name": "symfony/polyfill-util",
"version": "v1.0.0",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-util.git",
@ -2549,16 +2797,16 @@
},
{
"name": "symfony/process",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "f6290983c8725d0afa29bdc3e5295879de3e58f5"
"reference": "0570b9ca51135ee7da0f19239eaf7b07ffb87034"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/f6290983c8725d0afa29bdc3e5295879de3e58f5",
"reference": "f6290983c8725d0afa29bdc3e5295879de3e58f5",
"url": "https://api.github.com/repos/symfony/process/zipball/0570b9ca51135ee7da0f19239eaf7b07ffb87034",
"reference": "0570b9ca51135ee7da0f19239eaf7b07ffb87034",
"shasum": ""
},
"require": {
@ -2594,20 +2842,20 @@
],
"description": "Symfony Process Component",
"homepage": "https://symfony.com",
"time": "2015-11-19 16:11:24"
"time": "2016-01-06 09:57:37"
},
{
"name": "symfony/routing",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "7450f6196711b124fb8b04a12286d01a0401ddfe"
"reference": "6fec77993acfe19aecf60544b9c7d32f3d5b2506"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/7450f6196711b124fb8b04a12286d01a0401ddfe",
"reference": "7450f6196711b124fb8b04a12286d01a0401ddfe",
"url": "https://api.github.com/repos/symfony/routing/zipball/6fec77993acfe19aecf60544b9c7d32f3d5b2506",
"reference": "6fec77993acfe19aecf60544b9c7d32f3d5b2506",
"shasum": ""
},
"require": {
@ -2667,20 +2915,20 @@
"uri",
"url"
],
"time": "2015-11-18 13:41:01"
"time": "2016-01-03 15:32:00"
},
{
"name": "symfony/translation",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
"reference": "e4ecb9c3ba1304eaf24de15c2d7a428101c1982f"
"reference": "8cbab8445ad4269427077ba02fff8718cb397e22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e4ecb9c3ba1304eaf24de15c2d7a428101c1982f",
"reference": "e4ecb9c3ba1304eaf24de15c2d7a428101c1982f",
"url": "https://api.github.com/repos/symfony/translation/zipball/8cbab8445ad4269427077ba02fff8718cb397e22",
"reference": "8cbab8445ad4269427077ba02fff8718cb397e22",
"shasum": ""
},
"require": {
@ -2730,20 +2978,20 @@
],
"description": "Symfony Translation Component",
"homepage": "https://symfony.com",
"time": "2015-11-18 13:41:01"
"time": "2016-01-03 15:32:00"
},
{
"name": "symfony/var-dumper",
"version": "v2.7.7",
"version": "v2.7.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "72bcb27411780eaee9469729aace73c0d46fb2b8"
"reference": "ad39199e91f2f845a0181b14d459fda13a622138"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/72bcb27411780eaee9469729aace73c0d46fb2b8",
"reference": "72bcb27411780eaee9469729aace73c0d46fb2b8",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/ad39199e91f2f845a0181b14d459fda13a622138",
"reference": "ad39199e91f2f845a0181b14d459fda13a622138",
"shasum": ""
},
"require": {
@ -2789,20 +3037,20 @@
"debug",
"dump"
],
"time": "2015-11-18 13:41:01"
"time": "2016-01-07 11:12:32"
},
{
"name": "venturecraft/revisionable",
"version": "1.24.0",
"version": "1.26.0",
"source": {
"type": "git",
"url": "https://github.com/VentureCraft/revisionable.git",
"reference": "99c27d94f80ae9240cec89c4276f61e748e989a5"
"reference": "7a3d5304de6c10d43cfb0d9ebe0bbdbb6e5b82ee"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/VentureCraft/revisionable/zipball/99c27d94f80ae9240cec89c4276f61e748e989a5",
"reference": "99c27d94f80ae9240cec89c4276f61e748e989a5",
"url": "https://api.github.com/repos/VentureCraft/revisionable/zipball/7a3d5304de6c10d43cfb0d9ebe0bbdbb6e5b82ee",
"reference": "7a3d5304de6c10d43cfb0d9ebe0bbdbb6e5b82ee",
"shasum": ""
},
"require": {
@ -2837,7 +3085,7 @@
"model",
"revision"
],
"time": "2015-12-09 21:48:10"
"time": "2016-01-13 12:14:05"
},
{
"name": "vlucas/phpdotenv",
@ -3139,16 +3387,16 @@
},
{
"name": "phpspec/phpspec",
"version": "2.4.0",
"version": "2.4.1",
"source": {
"type": "git",
"url": "https://github.com/phpspec/phpspec.git",
"reference": "1d3938e6d9ffb1bd4805ea8ddac62ea48767f358"
"reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/phpspec/zipball/1d3938e6d9ffb1bd4805ea8ddac62ea48767f358",
"reference": "1d3938e6d9ffb1bd4805ea8ddac62ea48767f358",
"url": "https://api.github.com/repos/phpspec/phpspec/zipball/5528ce1e93a1efa090c9404aba3395c329b4e6ed",
"reference": "5528ce1e93a1efa090c9404aba3395c329b4e6ed",
"shasum": ""
},
"require": {
@ -3213,7 +3461,7 @@
"testing",
"tests"
],
"time": "2015-11-29 02:03:49"
"time": "2016-01-01 10:17:54"
},
{
"name": "phpspec/prophecy",
@ -4016,16 +4264,16 @@
},
{
"name": "symfony/yaml",
"version": "v3.0.0",
"version": "v3.0.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002"
"reference": "3df409958a646dad2bc5046c3fb671ee24a1a691"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002",
"reference": "177a015cb0e19ff4a49e0e2e2c5fc1c1bee07002",
"url": "https://api.github.com/repos/symfony/yaml/zipball/3df409958a646dad2bc5046c3fb671ee24a1a691",
"reference": "3df409958a646dad2bc5046c3fb671ee24a1a691",
"shasum": ""
},
"require": {
@ -4061,7 +4309,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2015-11-30 12:36:17"
"time": "2015-12-26 13:39:53"
}
],
"aliases": [],

View file

@ -147,6 +147,7 @@ return [
Intouch\LaravelNewrelic\NewrelicServiceProvider::class,
Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,
Cviebrock\LaravelElasticsearch\ServiceProvider::class,
],
@ -197,6 +198,7 @@ return [
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'Elasticsearch' => Cviebrock\LaravelElasticsearch\Facade::class,
'Newrelic' => Intouch\LaravelNewrelic\Facades\Newrelic::class,
],

168
config/elasticsearch.php Normal file
View file

@ -0,0 +1,168 @@
<?php
return [
/**
* You can specify one of several different connections when building an
* Elasticsearch client.
*
* Here you may specify which of the connections below you wish to use
* as your default connection when building an client. Of course you may
* use create several clients at once, each with different configurations.
*/
'defaultConnection' => 'default',
/**
* These are the connection parameters used when building a client.
*/
'connections' => [
'default' => [
/**
* Hosts
*
* This is an array of hosts that the client will connect to. It can be a
* single host name, or an array if you are running a cluster of Elasticsearch
* instances.
*
* This is the only configuration value that is mandatory.
*
* If set in an environment variable, this should be a comma-separated
* list of hostnames. Port numbers are optional; 9200 is the default.
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_host_configuration
*/
'hosts' => explode(',', env('ELASTICSEARCH_HOSTS', 'localhost:9200')),
/**
* SSL
*
* If your Elasticsearch instance uses an out-dated or self-signed SSL
* certificate, you will need to pass in the certificate bundle. This can
* either be the path to the certificate file (for self-signed certs), or a
* package like https://github.com/Kdyby/CurlCaBundle. See the documentation
* below for all the details.
*
* If you are using SSL instances, and the certificates are up-to-date and
* signed by a public certificate authority, then you can leave this null and
* just use "https" in the host path(s) above and you should be fine.
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_security.html#_ssl_encryption_2
*/
'sslVerification' => null,
/**
* Logging
*
* Logging is handled by passing in an instance of Monolog\Logger (which
* coincidentally is what Laravel's default logger is).
*
* If logging is enabled, you either need to set the path and log level
* (some defaults are given for you below), or you can use a custom logger by
* setting 'logObject' to an instance of Psr\Log\LoggerInterface. In fact,
* if you just want to use the default Laravel logger, then set 'logObject'
* to \Log::getMonolog().
*
* Note: 'logObject' takes precedent over 'logPath'/'logLevel', so set
* 'logObject' null if you just want file-based logging to a custom path.
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#enabling_logger
*/
'logging' => false,
// If you have an existing instance of Monolog you can use it here.
//'logObject' => \Log::getMonolog(),
'logPath' => storage_path('logs/elasticsearch.log'),
'logLevel' => Monolog\Logger::INFO,
/**
* Retries
*
* By default, the client will retry n times, where n = number of nodes in
* your cluster. If you would like to disable retries, or change the number,
* you can do so here.
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_set_retries
*/
'retries' => null,
/**
* The remainder of the configuration options can almost always be left
* as-is unless you have specific reasons to change them. Refer to the
* appropriate sections in the Elasticsearch documentation for what each option
* does and what values it expects.
*/
/**
* Sniff On Start
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html
*/
'sniffOnStart' => false,
/**
* HTTP Handler
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_configure_the_http_handler
* @see http://ringphp.readthedocs.org/en/latest/client_handlers.html
*/
'httpHandler' => null,
/**
* Connection Pool
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_setting_the_connection_pool
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_connection_pool.html
*/
'connectionPool' => null,
/**
* Connection Selector
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_setting_the_connection_selector
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_selectors.html
*/
'connectionSelector' => null,
/**
* Serializer
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_setting_the_serializer
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_serializers.html
*/
'serializer' => null,
/**
* Connection Factory
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_setting_a_custom_connectionfactory
*/
'connectionFactory' => null,
/**
* Endpoint
*
* @see https://www.elastic.co/guide/en/elasticsearch/client/php-api/2.0/_configuration.html#_set_the_endpoint_closure
*/
'endpoint' => null,
]
]
];

View file

@ -56,7 +56,7 @@ return [
/*
|--------------------------------------------------------------------------
| Cache Duration
| Cache duration
|--------------------------------------------------------------------------
|
| Duration in minutes for track files to be stored in cache.
@ -65,4 +65,28 @@ return [
'track_file_cache_duration' => 1440,
/*
|--------------------------------------------------------------------------
| Elasticsearch index name
|--------------------------------------------------------------------------
|
| The name of the Elasticsearch index to store Pony.fm's search data in.
|
*/
'elasticsearch_index' => 'ponyfm',
/*
|--------------------------------------------------------------------------
| Indexing queue name
|--------------------------------------------------------------------------
|
| The name of the queue to process re-indexing jobs on. This is separated
| from the default queue to avoid having a site-wide re-index clog uploads
| and downloads.
|
*/
'indexing_queue' => 'indexing',
];

View file

@ -35,13 +35,6 @@ return [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'expire' => 60,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
@ -49,30 +42,6 @@ return [
'ttr' => 60,
],
'sqs' => [
'driver' => 'sqs',
'key' => 'your-public-key',
'secret' => 'your-secret-key',
'queue' => 'your-queue-url',
'region' => 'us-east-1',
],
'iron' => [
'driver' => 'iron',
'host' => 'mq-aws-us-east-1.iron.io',
'token' => 'your-token',
'project' => 'your-project-id',
'queue' => 'your-queue-name',
'encrypt' => true,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'expire' => 60,
],
],
/*

View file

@ -0,0 +1,115 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Illuminate\Database\Migrations\Migration;
class SetupElasticsearch extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$elasticsearch = Elasticsearch::connection();
$elasticsearch->indices()->create([
'index' => 'ponyfm',
'body' => [
'mappings' => [
'track' => [
'_source' => ['enabled' => true],
'dynamic' => 'strict',
'properties' => [
'title' => ['type' => 'string', 'analyzer' => 'english'],
'artist' => ['type' => 'string'],
'published_at' => ['type' => 'date'],
'genre' => ['type' => 'string', 'analyzer' => 'english'],
'track_type' => ['type' => 'string', 'index' => 'not_analyzed'],
// This field is intended to be used as an array.
// Note that all Elasticsearch fields can technically be used as arrays.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
'show_songs' => ['type' => 'string'],
]
],
'album' => [
'_source' => ['enabled' => true],
'dynamic' => 'strict',
'properties' => [
'title' => ['type' => 'string', 'analyzer' => 'english'],
'artist' => ['type' => 'string'],
// This field is intended to be used as an array.
// Note that all Elasticsearch fields can technically be used as arrays.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
'tracks' => ['type' => 'string', 'analyzer' => 'english']
]
],
'playlist' => [
'_source' => ['enabled' => true],
'dynamic' => 'strict',
'properties' => [
'title' => ['type' => 'string', 'analyzer' => 'english'],
'curator' => ['type' => 'string'],
// This field is intended to be used as an array.
// Note that all Elasticsearch fields can technically be used as arrays.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
'tracks' => ['type' => 'string', 'analyzer' => 'english']
]
],
'user' => [
'_source' => ['enabled' => true],
'dynamic' => 'strict',
'properties' => [
'username' => ['type' => 'string', 'index' => 'not_analyzed'],
'display_name' => ['type' => 'string'],
// This field is intended to be used as an array.
// Note that all Elasticsearch fields can technically be used as arrays.
// See: https://www.elastic.co/guide/en/elasticsearch/reference/current/array.html
'tracks' => ['type' => 'string', 'analyzer' => 'english']
]
],
]
]
]);
Artisan::call('rebuild:search');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$elasticsearch = Elasticsearch::connection();
$elasticsearch->indices()->delete(['index' => 'ponyfm']);
}
}

View file

@ -25,7 +25,7 @@
<pfm-image-upload set-image="setCover" image="album.cover" />
</div>
<div class="form-row track-selector">
<a pfm-popup="track-selector,right" href="#" class="btn btn-small pull-right btn-info">Add Tracks</a>
<a pfm-popup="track-selector,right" pfm-popup-close-on-click href="#" class="btn btn-small pull-right btn-info">Add Tracks</a>
<label class="strong">Album Tracks</label>
<div id="track-selector" class="pfm-popup">
<ul>

View file

@ -50,7 +50,7 @@
</div>
<div class="row-fluid">
<div class="form-row album span6" ng-class="{'has-error': errors.album_id != null}">
<a pfm-popup="album-selector" href="#" class="btn btn-small">
<a pfm-popup="album-selector" pfm-popup-close-on-click href="#" class="btn btn-small">
Album:
<strong ng-show="selectedAlbum">{{selectedAlbum.title}}</strong>
<strong ng-hide="selectedAlbum">None</strong>
@ -68,7 +68,7 @@
<div class="error">{{errors.album_id}}</div>
</div>
<div class="form-row show-songs span6" ng-show="edit.track_type_id == 2" ng-class="{'has-error': errors.show_song_ids != null}">
<a pfm-popup="song-selector" href="#" class="btn btn-small">Show Songs: <strong>{{selectedSongsTitle}}</strong></a>
<a pfm-popup="song-selector" pfm-popup-close-on-click href="#" class="btn btn-small">Show Songs: <strong>{{selectedSongsTitle}}</strong></a>
<div id="song-selector" class="pfm-popup">
<ul>
<li ng-repeat="song in taxonomies.showSongs" ng-class="{selected: selectedSongs[song.id]}">

View file

@ -1,21 +1,3 @@
<div class="stretch-to-bottom">
<ul class="artist-listing">
<li class="artist" ng-class="{'x-archived': artist.is_archived}" ng-repeat="artist in artists" bindonce>
<a href="{{artist.url}}">
<img class="image" pfm-src-loader="artist.avatars.small" pfm-src-size="small" />
<span class="info">
<span class="title" bo-text="artist.name"></span>
<span ng-hide="artist.is_archived" class="published">
joined <span bo-text="artist.created_at.date | momentFromNow"></span>
</span>
<span ng-show="artist.is_archived" class="published">
archived artist
</span>
</span>
</a>
</li>
<li ng-show="!artists.length" class="empty">
No artists found...
</li>
</ul>
<pfm-users-list users="artists"></pfm-users-list>
</div>

View file

@ -5,7 +5,7 @@
<input type="file" onchange="angular.element(this).scope().setImageFile(this)" />
</p>
<div class="btn-group">
<a href="#" pfm-popup="image-selector" class="btn btn-small"><i class="icon-picture"></i> Gallery</a>
<a href="#" pfm-popup="image-selector" pfm-popup-close-on-click class="btn btn-small"><i class="icon-picture"></i> Gallery</a>
<a href="#" pfm-eat-click ng-click="uploadImage()" class="btn btn-info btn-small"><i class="icon-upload"></i> Upload</a>
<a href="#" pfm-eat-click ng-click="clearImage()" class="btn btn-danger btn-small" ng-show="isImageLoaded"><i class="icon-remove"></i></a>
</div>

View file

@ -0,0 +1,34 @@
<div class="search" ng-blur="searchInProgress = false">
<input
class="search-input"
type="text"
placeholder="Search&hellip;"
ng-model="searchQuery"
pfm-popup="search-results"
/>
<section class="search-results pfm-popup" id="search-results">
<div ng-hide="searchInProgress">
<p class="empty-box">Type something to begin searching!</p>
</div>
<div ng-show="searchInProgress">
<div class="-column1">
<h3 class="-section-header">Matching tracks</h3>
<pfm-tracks-list tracks="tracks"></pfm-tracks-list>
</div>
<div class="-column2">
<h3 class="-section-header">Matching users</h3>
<pfm-users-list users="users" class="-condensed"></pfm-users-list>
<h3 class="-section-header">Matching albums</h3>
<pfm-albums-list albums="albums"></pfm-albums-list>
<h3 class="-section-header">Matching playlists</h3>
<pfm-playlists-list playlists="playlists"></pfm-playlists-list>
</div>
</div>
</section>
</div>

View file

@ -0,0 +1,19 @@
<ul class="users-listing">
<li class="artist" ng-class="{'x-archived': user.is_archived}" ng-repeat="user in users track by user.id" bindonce>
<a bo-href="user.url">
<img class="image" pfm-src-loader="user.avatars.small" pfm-src-size="small" />
<span class="info">
<span class="title" bo-text="user.name"></span>
<span ng-hide="user.is_archived" class="published">
joined <span bo-text="user.created_at.date | momentFromNow"></span>
</span>
<span ng-show="user.is_archived" class="published">
archived artist
</span>
</span>
</a>
</li>
<li ng-show="!users.length" class="empty">
No users found&hellip;
</li>
</ul>

View file

@ -27,20 +27,41 @@ angular.module('ponyfm').directive 'pfmPopup', () ->
$element = $ element
$positionParent = null
open = false
closeOnClick = attrs.pfmPopupCloseOnClick?
documentClickHandler = () ->
return if !open
close = () ->
$popup.removeClass 'open'
open = false
documentClickHandler = (event) ->
if !open
return
if (closeOnClick)
close()
return true
# Based on: https://stackoverflow.com/a/4660738/3225811
else if event.target.id == elementId or $(event.target).parents("##{elementId}").size()
return true
else
close()
return true
calculatePosition = ->
$popup.parents().each () ->
$this = $ this
$positionParent = $this if $positionParent == null && ($this.css('position') == 'relative' || $this.is 'body')
if $positionParent == null && ($this.css('position') == 'relative' || $this.is 'body')
$positionParent = $this
return false
position = $element.offset()
parentPosition = $positionParent.offset()
parentPosition = $positionParent.offset() + $positionParent.height()
windowWidth = $(window).width() - 15
left = position.left
@ -63,6 +84,7 @@ angular.module('ponyfm').directive 'pfmPopup', () ->
top: top - parentPosition.top,
height: height - 15}
windowResizeHandler = () ->
return if !open
$popup.css 'height', 'auto'
@ -70,7 +92,7 @@ angular.module('ponyfm').directive 'pfmPopup', () ->
$popup.css
left: position.left
top: position.top
height: position.height
maxHeight: position.height
$(document.body).bind 'click', documentClickHandler
$(window).bind 'resize', windowResizeHandler
@ -79,9 +101,8 @@ angular.module('ponyfm').directive 'pfmPopup', () ->
e.preventDefault()
e.stopPropagation()
if open
open = false
$popup.removeClass 'open'
if open and not $element.is(':focus')
close
return
$popup.addClass 'open'
@ -97,6 +118,11 @@ angular.module('ponyfm').directive 'pfmPopup', () ->
open = true
), 0
scope.$on '$stateChangeStart', () ->
close()
scope.$on '$destroy', () ->
$(document.body).unbind 'click', documentClickHandler
$(window).unbind 'click', windowResizeHandler

View file

@ -0,0 +1,66 @@
# Pony.fm - A community for pony fan music.
# Copyright (C) 2016 Peter Deltchev
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
angular.module('ponyfm').directive 'pfmSearch', () ->
restrict: 'E'
templateUrl: '/templates/directives/search.html'
scope:
resource: '=resource',
type: '@type'
controller: [
'$scope', 'search'
($scope, search) ->
$scope.searchQuery = ''
$scope.searchInProgress = false
$scope.tracks = []
$scope.albums = []
$scope.playlists = []
$scope.users = []
clearResults = ()->
$scope.tracks.length = 0
$scope.albums.length = 0
$scope.playlists.length = 0
$scope.users.length = 0
$scope.$watch 'searchQuery', _.debounce((searchQuery)->
$scope.$apply ()->
if searchQuery.length <3
clearResults()
$scope.searchInProgress = false
return
$scope.searchInProgress = true
search.searchAllContent(searchQuery)
.then (results)->
clearResults()
for track in results.tracks
$scope.tracks.push(track)
for album in results.albums
$scope.albums.push(album)
for playlist in results.playlists
$scope.playlists.push(playlist)
for user in results.users
$scope.users.push(user)
, 300)
]

View file

@ -0,0 +1,29 @@
# Pony.fm - A community for pony fan music.
# Copyright (C) 2016 Peter Deltchev
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
angular.module('ponyfm').directive 'pfmUsersList', () ->
restrict: 'E'
replace: true
templateUrl: '/templates/directives/users-list.html'
scope:
users: '=users',
class: '@class'
controller: [
'$scope', 'auth'
($scope, auth) ->
$scope.auth = auth.data
]

View file

@ -0,0 +1,35 @@
# Pony.fm - A community for pony fan music.
# Copyright (C) 2016 Peter Deltchev
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
angular.module('ponyfm').factory('search', [
'$http'
($http) ->
self =
searchAllContent: (query) ->
searchDef = new $.Deferred()
$http.get('/api/web/search?query=' + encodeURIComponent(query))
.success (results)->
searchDef.resolve(results.results)
.error (response)->
searchDef.reject(response)
searchDef.promise()
self
])

View file

@ -32,3 +32,4 @@
@import 'content';
@import 'dashboard';
@import 'uploader';
@import 'search';

View file

@ -22,7 +22,7 @@
@media (max-width: 1300px) and (min-width: 720px) {
html {
.albums-listing, .playlists-listing, .artist-listing {
.albums-listing, .playlists-listing, .users-listing {
&.two-columns li {
width: auto;
float: none;
@ -37,7 +37,7 @@
@media (max-width: 720px) {
html {
.albums-listing, .playlists-listing, .artist-listing {
.albums-listing, .playlists-listing, .users-listing {
&.two-columns li {
width: auto;
float: none;
@ -51,7 +51,7 @@
}
}
.albums-listing, .playlists-listing, .artist-listing {
.albums-listing, .playlists-listing, .users-listing {
margin: 0px;
padding: 0px;
list-style: none;
@ -345,20 +345,7 @@ html .single-player .play-button {
html {
li {
&.empty {
.border-radius(0px);
background: lighten(@pfm-purple, 30%);
border: 1px solid lighten(@pfm-purple, 10%);
color: lighten(@pfm-purple, 3%);
float: none !important;
width: auto !important;
display: block;
margin-top: 5px;
padding: 5px;
font-size: 9pt;
&:hover {
background-color: lighten(@pfm-purple, 30%);
}
.empty-box;
}
.cache-loading {
@ -374,6 +361,23 @@ html {
}
}
.empty-box {
.border-radius(0px);
background: lighten(@pfm-purple, 30%);
border: 1px solid lighten(@pfm-purple, 10%);
color: lighten(@pfm-purple, 3%);
float: none !important;
width: auto !important;
display: block;
margin-top: 5px;
padding: 5px;
font-size: 9pt;
&:hover {
background-color: lighten(@pfm-purple, 30%);
}
}
.tracks-listing {
margin: 0px;
padding: 0px;
@ -487,3 +491,20 @@ html {
}
}
}
.users-listing {
&.-condensed {
.image {
height: 30px;
width: 30px;
}
.info {
margin-left: 40px;
}
.published {
display: none;
}
}
}

74
resources/assets/styles/search.less vendored Normal file
View file

@ -0,0 +1,74 @@
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import 'base/bootstrap/bootstrap';
@import 'mixins';
@import 'variables';
.search {
position: relative;
}
input.search-input {
font-size: 13px;
padding: 15px 10px;
margin-bottom: 0;
}
.search-results {
width: 600px;
padding: 10px;
background: #fff;
.-column1, .-column2 {
width: 48%;
float: left;
}
.-column1 {
padding-right: 2%;
}
.-column2 {
padding-left: 2%;
}
ol li {
list-style: disc;
}
li a {
padding: 0;
overflow: hidden;
color: @pfm-light-grey;
}
.-section-header {
background: transparent;
color: @pfm-purple;
padding-left: 0;
}
.albums-listing, .playlists-listing, .users-listing {
li {
width: 100%;
}
}
}

View file

@ -8,6 +8,8 @@ DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
ELASTICSEARCH_HOSTS=localhost
SESSION_HTTPS_ONLY=false
QUEUE_DRIVER=sync

View file

@ -66,6 +66,9 @@
<div class="site-body">
<ul class="sidebar" ng-controller="sidebar">
@can('access-search')
<li><pfm-search></pfm-search></li>
@endcan
<li ng-class="{selected: stateIncludes('content.tracks') || stateIncludes('content.track')}"><a href="/tracks">Tracks</a></li>
<li ng-class="{selected: stateIncludes('content.albums') || stateIncludes('content.album')}"><a href="/albums">Albums</a></li>
<li ng-class="{selected: stateIncludes('content.playlists') || stateIncludes('content.playlist')}"><a href="/playlists">Playlists</a></li>

View file

@ -7,6 +7,7 @@ sudo cp /vagrant/vagrant/php-overrides.ini /etc/php/7.0/fpm/99-overrides.ini
sudo cp /vagrant/vagrant/pony.fm.redis.config /etc/redis/redis.conf
sudo service elasticsearch restart
sudo service nginx restart
sudo service php7.0-fpm restart

View file

@ -1,13 +1,28 @@
#!/usr/bin/env bash
if type java &>/dev/null; then
echo "Java is installed!"
else
sudo add-apt-repository -y ppa:webupd8team/java
echo /usr/bin/debconf shared/accepted-oracle-license-v1-1 select true | sudo debconf-set-selections
echo /usr/bin/debconf shared/accepted-oracle-license-v1-1 seen true | sudo debconf-set-selections
fi
if type /usr/share/elasticsearch/bin/elasticsearch &>/dev/null; then
echo "ElasticSearch is installed!"
else
wget -qO - https://packages.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
echo "deb http://packages.elastic.co/elasticsearch/2.x/debian stable main" | sudo tee -a /etc/apt/sources.list.d/elasticsearch-2.x.list
fi
echo "Running apt-get update..."
sudo apt-get -qq update
echo "Installing tagging tools..."
sudo apt-get -qq install -y AtomicParsley flac vorbis-tools imagemagick
echo "Installing ffmpeg dependencies.."
sudo apt-get -qq install -y pkg-config yasm libfaac-dev libmp3lame-dev libvorbis-dev libtheora-dev
echo "Installing tagging tools & other dependencies..."
sudo apt-get -qq install -y AtomicParsley flac vorbis-tools imagemagick oracle-java8-installer elasticsearch pkg-config yasm libfaac-dev libmp3lame-dev libvorbis-dev libtheora-dev
if type ffmpeg &>/dev/null; then