diff --git a/app/Commands/CreateShowSongCommand.php b/app/Commands/CreateShowSongCommand.php new file mode 100644 index 00000000..0e4bcb17 --- /dev/null +++ b/app/Commands/CreateShowSongCommand.php @@ -0,0 +1,76 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Illuminate\Support\Str; +use Poniverse\Ponyfm\Models\ShowSong; +use Validator; + +class CreateShowSongCommand extends CommandBase +{ + /** @var ShowSong */ + private $_songName; + + public function __construct($songName) + { + $this->_songName = $songName; + } + + /** + * @return bool + */ + public function authorize() + { + return Gate::allows('create-show-song'); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $slug = Str::slug($this->_songName); + + $rules = [ + 'title' => 'required|unique:show_songs,title,NULL,id,deleted_at,NULL|max:250', + 'slug' => 'required|unique:show_songs,slug,NULL,id,deleted_at,NULL' + ]; + + $validator = Validator::make([ + 'title' => $this->_songName, + 'slug' => $slug + ], $rules); + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + ShowSong::create([ + 'title' => $this->_songName, + 'slug' => $slug, + 'lyrics' => '' + ]); + + return CommandResponse::succeed(['message' => 'Song created!']); + } +} diff --git a/app/Commands/DeleteShowSongCommand.php b/app/Commands/DeleteShowSongCommand.php new file mode 100644 index 00000000..7cab2ef5 --- /dev/null +++ b/app/Commands/DeleteShowSongCommand.php @@ -0,0 +1,76 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Poniverse\Ponyfm\Models\ShowSong; +use Poniverse\Ponyfm\Jobs\DeleteShowSong; +use Validator; + +class DeleteShowSongCommand extends CommandBase +{ + use DispatchesJobs; + + + /** @var ShowSong */ + private $_songToDelete; + private $_destinationSong; + + public function __construct($songId, $destinationSongId) { + $this->_songToDelete = ShowSong::find($songId); + $this->_destinationSong = ShowSong::find($destinationSongId); + } + + /** + * @return bool + */ + public function authorize() { + return Gate::allows('delete', $this->_destinationSong); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() { + $rules = [ + 'song_to_delete' => 'required', + 'destination_song' => 'required', + ]; + + // The validation will fail if the genres don't exist + // because they'll be null. + $validator = Validator::make([ + 'song_to_delete' => $this->_songToDelete, + 'destination_song' => $this->_destinationSong, + ], $rules); + + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + $this->dispatch(new DeleteShowSong($this->_songToDelete, $this->_destinationSong)); + + return CommandResponse::succeed(['message' => 'Song deleted!']); + } +} diff --git a/app/Commands/RenameShowSongCommand.php b/app/Commands/RenameShowSongCommand.php new file mode 100644 index 00000000..987b5329 --- /dev/null +++ b/app/Commands/RenameShowSongCommand.php @@ -0,0 +1,83 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Illuminate\Support\Str; +use Poniverse\Ponyfm\Jobs\UpdateTagsForRenamedShowSong; +use Poniverse\Ponyfm\Models\ShowSong; +use Validator; + +class RenameShowSongCommand extends CommandBase +{ + use DispatchesJobs; + + /** @var Song */ + private $_song; + private $_newName; + + public function __construct($genreId, $newName) + { + $this->_song = ShowSong::find($genreId); + $this->_newName = $newName; + } + + /** + * @return bool + */ + public function authorize() + { + return Gate::allows('rename', $this->_song); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $slug = Str::slug($this->_newName); + + $rules = [ + 'title' => 'required|unique:show_songs,title,'.$this->_song->id.',id|max:250', + 'slug' => 'required|unique:show_songs,slug,'.$this->_song->id.',id' + ]; + + $validator = Validator::make([ + 'title' => $this->_newName, + 'slug' => $slug + ], $rules); + + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + $this->_song->title = $this->_newName; + $this->_song->slug = $slug; + $this->_song->save(); + + $this->dispatch(new UpdateTagsForRenamedShowSong($this->_song)); + + return CommandResponse::succeed(['message' => 'Show song renamed!']); + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 6e12222d..ba061245 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -27,7 +27,7 @@ class AdminController extends Controller { public function getIndex() { - return Redirect::to('AdminController@getGenres'); + return View::make('shared.null'); } public function getGenres() @@ -39,4 +39,9 @@ class AdminController extends Controller { return View::make('shared.null'); } + + public function getShowSongs() + { + return View::make('shared.null'); + } } diff --git a/app/Http/Controllers/Api/Web/ShowSongsController.php b/app/Http/Controllers/Api/Web/ShowSongsController.php new file mode 100644 index 00000000..258b97fd --- /dev/null +++ b/app/Http/Controllers/Api/Web/ShowSongsController.php @@ -0,0 +1,68 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; + +use Input; +use Poniverse\Ponyfm\Commands\CreateShowSongCommand; +use Poniverse\Ponyfm\Commands\DeleteShowSongCommand; +use Poniverse\Ponyfm\Commands\RenameShowSongCommand; +use Poniverse\Ponyfm\Models\ShowSong; +use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; +use Response; + + +class ShowSongsController extends ApiControllerBase +{ + public function getIndex() + { + $this->authorize('access-admin-area'); + + $songs = ShowSong::with(['trackCountRelation' => function($query) { + $query->withTrashed(); + }]) + ->orderBy('title', 'asc') + ->select('id', 'title', 'slug') + ->get(); + + return Response::json([ + 'showsongs' => $songs->toArray() + ], 200); + } + + public function postCreate() + { + $command = new CreateShowSongCommand(Input::get('title')); + return $this->execute($command); + } + + public function putRename($songId) + { + $command = new RenameShowSongCommand($songId, Input::get('title')); + return $this->execute($command); + } + + + public function deleteSong($songId) + { + $command = new DeleteShowSongCommand($songId, Input::get('destination_song_id')); + return $this->execute($command); + } +} diff --git a/app/Http/Controllers/Api/Web/TaxonomiesController.php b/app/Http/Controllers/Api/Web/TaxonomiesController.php index 7c6de020..4c09ad02 100644 --- a/app/Http/Controllers/Api/Web/TaxonomiesController.php +++ b/app/Http/Controllers/Api/Web/TaxonomiesController.php @@ -42,7 +42,7 @@ class TaxonomiesController extends ApiControllerBase ->where('id', '!=', TrackType::UNCLASSIFIED_TRACK) ->get()->toArray(), 'show_songs' => ShowSong::select('title', 'id', 'slug', - DB::raw('(SELECT COUNT(tracks.id) FROM show_song_track INNER JOIN tracks ON tracks.id = show_song_track.track_id WHERE show_song_track.show_song_id = show_songs.id AND tracks.published_at IS NOT NULL) AS track_count'))->get()->toArray() + DB::raw('(SELECT COUNT(tracks.id) FROM show_song_track INNER JOIN tracks ON tracks.id = show_song_track.track_id WHERE show_song_track.show_song_id = show_songs.id AND tracks.published_at IS NOT NULL) AS track_count'))->orderBy('title')->get()->toArray() ], 200); } } diff --git a/app/Http/routes.php b/app/Http/routes.php index 3463c36c..560647f0 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -161,6 +161,11 @@ Route::group(['prefix' => 'api/web'], function() { Route::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+'); Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+'); + Route::get('/showsongs', 'Api\Web\ShowSongsController@getIndex'); + Route::post('/showsongs', 'Api\Web\ShowSongsController@postCreate'); + Route::put('/showsongs/{id}', 'Api\Web\ShowSongsController@putRename')->where('id', '\d+'); + Route::delete('/showsongs/{id}', 'Api\Web\ShowSongsController@deleteSong')->where('id', '\d+'); + Route::get('/tracks', 'Api\Web\TracksController@getAllTracks'); }); @@ -171,6 +176,7 @@ Route::group(['prefix' => 'api/web'], function() { Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() { Route::get('/genres', 'AdminController@getGenres'); Route::get('/tracks', 'AdminController@getTracks'); + Route::get('/show-songs', 'AdminController@getShowSongs'); Route::get('/', 'AdminController@getIndex'); }); diff --git a/app/Jobs/DeleteShowSong.php b/app/Jobs/DeleteShowSong.php new file mode 100644 index 00000000..64635c65 --- /dev/null +++ b/app/Jobs/DeleteShowSong.php @@ -0,0 +1,89 @@ +. + */ + +namespace Poniverse\Ponyfm\Jobs; + +use Auth; +use DB; +use Poniverse\Ponyfm\Models\ShowSong; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Contracts\Bus\SelfHandling; +use Illuminate\Contracts\Queue\ShouldQueue; +use Poniverse\Ponyfm\Models\Track; +use SerializesModels; + +class DeleteShowSong extends Job implements SelfHandling, ShouldQueue +{ + use InteractsWithQueue, SerializesModels; + + protected $executingUser; + protected $songToDelete; + protected $destinationSong; + + /** + * Create a new job instance. + * + * @param ShowSong $songToDelete + * @param ShowSong $destinationSong + */ + public function __construct(ShowSong $songToDelete, ShowSong $destinationSong) + { + $this->executingUser = Auth::user(); + $this->songToDelete = $songToDelete; + $this->destinationSong = $destinationSong; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->beforeHandle(); + + // The user who kicked off this job is used when generating revision log entries. + Auth::login($this->executingUser); + + // This is done instead of a single UPDATE query in order to + // generate revision logs for the change. + $this->songToDelete->tracks()->chunk(200, function ($tracks) { + foreach ($tracks as $track) { + /** @var Track $track */ + $oldSongs = $track->showSongs; + $newSongs = []; + + foreach ($oldSongs as $key => $showSong) { + if ($showSong->id == $this->songToDelete->id) { + $newSongs[$key] = $this->destinationSong->id; + } else { + $newSongs[$key] = $showSong->id; + } + } + + $track->showSongs()->sync($newSongs); + $track->save(); + $track->updateTags(); + } + }); + + $this->songToDelete->delete(); + } +} diff --git a/app/Jobs/UpdateTagsForRenamedShowSong.php b/app/Jobs/UpdateTagsForRenamedShowSong.php new file mode 100644 index 00000000..a39a09da --- /dev/null +++ b/app/Jobs/UpdateTagsForRenamedShowSong.php @@ -0,0 +1,99 @@ +. + */ + +namespace Poniverse\Ponyfm\Jobs; + +use Auth; +use Cache; +use Log; +use Poniverse\Ponyfm\Models\ShowSong; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Contracts\Bus\SelfHandling; +use Illuminate\Contracts\Queue\ShouldQueue; +use Poniverse\Ponyfm\Models\Track; +use SerializesModels; + +/** + * Class RenameGenre + * + * NOTE: It is assumed that the genre passed into this job has already been renamed! + * All this job does is update the tags in that genre's tracks. + * + * @package Poniverse\Ponyfm\Jobs + */ +class UpdateTagsForRenamedShowSong extends Job implements SelfHandling, ShouldQueue +{ + use InteractsWithQueue, SerializesModels; + + protected $executingUser; + protected $songThatWasRenamed; + protected $lockKey; + + /** + * Create a new job instance. + * + * @param ShowSong $songThatWasRenamed + */ + public function __construct(ShowSong $songThatWasRenamed) + { + $this->executingUser = Auth::user(); + $this->songThatWasRenamed = $songThatWasRenamed; + + $this->lockKey = "show-song-{$this->songThatWasRenamed->id}-tag-update-lock"; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->beforeHandle(); + + // The user who kicked off this job is used when generating revision log entries. + Auth::login($this->executingUser); + + // "Lock" this genre to prevent race conditions + if (Cache::has($this->lockKey)) { + Log::info("Tag updates for the \"{$this->songThatWasRenamed->title}\" song are currently in progress! Will try again in 30 seconds."); + $this->release(30); + return; + + } else { + Cache::forever($this->lockKey, true); + } + + + $this->songThatWasRenamed->tracks()->chunk(200, function ($tracks) { + foreach ($tracks as $track) { + /** @var Track $track */ + $track->updateTags(); + } + }); + + Cache::forget($this->lockKey); + } + + public function failed() + { + Cache::forget($this->lockKey); + } +} diff --git a/app/Models/ShowSong.php b/app/Models/ShowSong.php index 8b14f466..34667ea1 100644 --- a/app/Models/ShowSong.php +++ b/app/Models/ShowSong.php @@ -20,6 +20,7 @@ namespace Poniverse\Ponyfm\Models; +use DB; use Illuminate\Database\Eloquent\Model; /** @@ -33,4 +34,15 @@ use Illuminate\Database\Eloquent\Model; class ShowSong extends Model { protected $table = 'show_songs'; + protected $fillable = ['title', 'slug', 'lyrics']; + + public function trackCountRelation() { + return $this->belongsToMany(Track::class) + ->select(['show_song_id', DB::raw('count(*) as track_count')]) + ->groupBy('show_song_id'); + } + + public function tracks(){ + return $this->belongsToMany(Track::class); + } } diff --git a/app/Policies/ShowSongPolicy.php b/app/Policies/ShowSongPolicy.php new file mode 100644 index 00000000..0e865e38 --- /dev/null +++ b/app/Policies/ShowSongPolicy.php @@ -0,0 +1,17 @@ +hasRole('admin'); + } + + public function delete(User $user, ShowSong $song) { + return $user->hasRole('admin'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 7656ba92..9a6abbe2 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -26,9 +26,11 @@ use Poniverse\Ponyfm\Models\Album; use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Policies\AlbumPolicy; use Poniverse\Ponyfm\Policies\GenrePolicy; +use Poniverse\Ponyfm\Policies\ShowSongPolicy; use Poniverse\Ponyfm\Policies\TrackPolicy; use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\User; +use Poniverse\Ponyfm\Models\ShowSong; use Poniverse\Ponyfm\Policies\UserPolicy; class AuthServiceProvider extends ServiceProvider @@ -43,6 +45,7 @@ class AuthServiceProvider extends ServiceProvider Track::class => TrackPolicy::class, Album::class => AlbumPolicy::class, User::class => UserPolicy::class, + ShowSong::class => ShowSongPolicy::class ]; /** @@ -61,6 +64,10 @@ class AuthServiceProvider extends ServiceProvider return $user->hasRole('admin'); }); + $gate->define('create-show-song', function(User $user) { + return $user->hasRole('admin'); + }); + $this->registerPolicies($gate); } } diff --git a/database/migrations/2016_06_05_221208_add_timestamps_to_show_songs.php b/database/migrations/2016_06_05_221208_add_timestamps_to_show_songs.php new file mode 100644 index 00000000..17a58bba --- /dev/null +++ b/database/migrations/2016_06_05_221208_add_timestamps_to_show_songs.php @@ -0,0 +1,33 @@ +timestamp('created_at')->default(DB::raw('CURRENT_TIMESTAMP')); + $table->timestamp('updated_at')->default(DB::raw('CURRENT_TIMESTAMP')); + $table->timestamp('deleted_at')->default(DB::raw('CURRENT_TIMESTAMP')); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('show_songs', function (Blueprint $table) { + $table->dropColumn(['created_at', 'updated_at', 'deleted_at']); + }); + } +} diff --git a/public/templates/admin/_layout.html b/public/templates/admin/_layout.html index bf04aabd..6e485deb 100644 --- a/public/templates/admin/_layout.html +++ b/public/templates/admin/_layout.html @@ -1,6 +1,7 @@
Enter a show song name and press enter to create it!
+ + +Song | ++ | # of tracks (including deleted) | +Actions | + +
---|---|---|---|
+
+
+ {{ song.errorMessage }}
+
+ |
+ + | {{ song.track_count_relation[0].track_count }} | ++ + + + | +