diff --git a/README.md b/README.md index 0fa5a0fe..e4df4578 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,14 @@ these are smaller in scope and easier to tackle if you're unfamiliar with the co Starting a dev environment ========================== -To begin development, you must do three things: +To begin development, do the following: -1. Install the `vagrant-hostmanager` plugin: `vagrant plugin install vagrant-hostmanager` +1. Install [Vagrant](https://www.vagrantup.com/downloads.html) and + [VirtualBox](https://www.virtualbox.org/wiki/Downloads) if you don't have them already. -2. Install the `vagrant-bindfs` plugin: `vagrant plugin install vagrant-bindfs` +2. Install the `vagrant-hostmanager` plugin: `vagrant plugin install vagrant-hostmanager` -3. Create the directory `pony.fm.files` in the repository's parent directory +3. Install the `vagrant-bindfs` plugin: `vagrant plugin install vagrant-bindfs` 4. Run `vagrant up` from the folder in which you cloned the repository @@ -62,21 +63,13 @@ And then install all of the required local packages by invoking: npm install -Finally, build all of the scripts by executing: - - gulp build - -During development, you should make a point to run "gulp watch". You can do this simply by executing: +Finally, to compile and serve the assets in real time, run the following (and leave it running while you develop): gulp watch -This will watch and compile the `.less` and `.coffee` files in real time. - Configuring the servers ----------------------- Pony.fm uses nginx, php-fpm, redis, and MySQL. You can modify the configuration of these services by locating the appropriate config file in the `vagrant` folder. Once modified, you must reload the configuration by running the appropriate shell script (`reload-config.sh`) or bat files (`reload-config.bat` and `reload-config.vmware.bat`). These scripts simply tell Vagrant to run `copy-and-restart-config.sh` on the VM. If you need to change any other configuration file on the VM - copy the entire file over into the vagrant folder, make your changes, and update the `copy-and-restart-config.sh` script to copy the modified config back into the proper folder. All potential configuration requirements should be represented in the `vagrant` folder **and never only on the VM itself** as changes will not be preserved. - -**NOTE:** currently, Redis's configuration is not reloaded by the `copy-and-restart-config.sh` diff --git a/Vagrantfile b/Vagrantfile index d930d8fc..606d2018 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -3,11 +3,12 @@ Vagrant.configure("2") do |config| config.hostmanager.enabled = true config.hostmanager.manage_host = true - config.vm.box = 'laravel/homestead-7' - config.vm.box_version = '0.2.1' + config.vm.box = 'laravel/homestead' + config.vm.box_version = '0.4.2' + config.vm.provider "virtualbox" do |v| v.cpus = 4 - v.memory = 2048 + v.memory = 1024 end config.vm.define 'default' do |node| @@ -17,13 +18,8 @@ Vagrant.configure("2") do |config| end config.vm.synced_folder ".", "/vagrant", type: "nfs" + config.bindfs.bind_folder "/vagrant", "/vagrant" config.vm.provision "shell", path: "vagrant/install.sh" - - config.vm.network "forwarded_port", guest: 3306, host: 33060 - - config.vm.synced_folder "../pony.fm.files", "/vagrant-files", type: "nfs" - config.bindfs.bind_folder "/vagrant", "/vagrant" - config.vm.provision "shell", path: "vagrant/copy-and-restart-configs.sh", run: "always" end diff --git a/app/AlbumDownloader.php b/app/AlbumDownloader.php index 8feaff98..1569a807 100644 --- a/app/AlbumDownloader.php +++ b/app/AlbumDownloader.php @@ -20,11 +20,19 @@ namespace Poniverse\Ponyfm; +use Poniverse\Ponyfm\Models\Album; use ZipStream; class AlbumDownloader { + /** + * @var Album + */ private $_album; + + /** + * @var string + */ private $_format; function __construct($album, $format) @@ -35,25 +43,25 @@ class AlbumDownloader function download() { - $zip = new ZipStream($this->_album->user->display_name . ' - ' . $this->_album->title . '.zip'); + $zip = new ZipStream($this->_album->user->display_name.' - '.$this->_album->title.'.zip'); $zip->setComment( - 'Album: ' . $this->_album->title . "\r\n" . - 'Artist: ' . $this->_album->user->display_name . "\r\n" . - 'URL: ' . $this->_album->url . "\r\n" . "\r\n" . - 'Downloaded on ' . date('l, F jS, Y, \a\t h:i:s A') . '.' + 'Album: '.$this->_album->title."\r\n". + 'Artist: '.$this->_album->user->display_name."\r\n". + 'URL: '.$this->_album->url."\r\n"."\r\n". + 'Downloaded on '.date('l, F jS, Y, \a\t h:i:s A').'.' ); - $directory = $this->_album->user->display_name . '/' . $this->_album->title . '/'; + $directory = $this->_album->user->display_name.'/'.$this->_album->title.'/'; $notes = - 'Album: ' . $this->_album->title . "\r\n" . - 'Artist: ' . $this->_album->user->display_name . "\r\n" . - 'URL: ' . $this->_album->url . "\r\n" . - "\r\n" . - $this->_album->description . "\r\n" . - "\r\n" . - "\r\n" . - 'Tracks' . "\r\n" . + 'Album: '.$this->_album->title."\r\n". + 'Artist: '.$this->_album->user->display_name."\r\n". + 'URL: '.$this->_album->url."\r\n". + "\r\n". + $this->_album->description."\r\n". + "\r\n". + "\r\n". + 'Tracks'."\r\n". "\r\n"; foreach ($this->_album->tracks as $track) { @@ -62,14 +70,14 @@ class AlbumDownloader } $zip->addLargeFile($track->getFileFor($this->_format), - $directory . $track->getDownloadFilenameFor($this->_format)); + $directory.$track->getDownloadFilenameFor($this->_format)); $notes .= - $track->track_number . '. ' . $track->title . "\r\n" . - $track->description . "\r\n" . + $track->track_number.'. '.$track->title."\r\n". + $track->description."\r\n". "\r\n"; } - $zip->addFile($notes, $directory . 'Album Notes.txt'); + $zip->addFile($notes, $directory.'Album Notes.txt'); $zip->finalize(); } } diff --git a/app/Commands/AddTrackToPlaylistCommand.php b/app/Commands/AddTrackToPlaylistCommand.php index d894d0c1..b6d80e7c 100644 --- a/app/Commands/AddTrackToPlaylistCommand.php +++ b/app/Commands/AddTrackToPlaylistCommand.php @@ -20,14 +20,18 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Playlist; -use Poniverse\Ponyfm\Track; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\Track; +use Auth; +use DB; +use Validator; class AddTrackToPlaylistCommand extends CommandBase { + /** @var Track */ private $_track; + + /** @var Playlist */ private $_playlist; function __construct($playlistId, $trackId) @@ -52,10 +56,22 @@ class AddTrackToPlaylistCommand extends CommandBase */ public function execute() { + // check if this track is already in the playlist + $validator = Validator::make( + ['track_id' => $this->_track->id], + ['track_id' => "unique:playlist_track,track_id,null,id,playlist_id,{$this->_playlist->id}",] + ); + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + $songIndex = $this->_playlist->tracks()->count() + 1; $this->_playlist->tracks()->attach($this->_track, ['position' => $songIndex]); + $this->_playlist->touch(); - Playlist::whereId($this->_playlist->id)->update([ + Playlist::where('id', $this->_playlist->id)->update([ 'track_count' => DB::raw('(SELECT COUNT(id) FROM playlist_track WHERE playlist_id = ' . $this->_playlist->id . ')') ]); diff --git a/app/Commands/CreateAlbumCommand.php b/app/Commands/CreateAlbumCommand.php index bc772f10..c16b6762 100644 --- a/app/Commands/CreateAlbumCommand.php +++ b/app/Commands/CreateAlbumCommand.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Image; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Image; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; diff --git a/app/Commands/CreateCommentCommand.php b/app/Commands/CreateCommentCommand.php index ceb01af4..ade69196 100644 --- a/app/Commands/CreateCommentCommand.php +++ b/app/Commands/CreateCommentCommand.php @@ -20,11 +20,11 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Comment; -use Poniverse\Ponyfm\Playlist; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Comment; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; diff --git a/app/Commands/CreateGenreCommand.php b/app/Commands/CreateGenreCommand.php new file mode 100644 index 00000000..23e7d2df --- /dev/null +++ b/app/Commands/CreateGenreCommand.php @@ -0,0 +1,75 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Gate; +use Illuminate\Support\Str; +use Poniverse\Ponyfm\Models\Genre; +use Validator; + +class CreateGenreCommand extends CommandBase +{ + /** @var Genre */ + private $_genreName; + + public function __construct($genreName) + { + $this->_genreName = $genreName; + } + + /** + * @return bool + */ + public function authorize() + { + return Gate::allows('create-genre'); + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $slug = Str::slug($this->_genreName); + + $rules = [ + 'name' => 'required|unique:genres,name,NULL,id,deleted_at,NULL|max:50', + 'slug' => 'required|unique:genres,slug,NULL,id,deleted_at,NULL' + ]; + + $validator = Validator::make([ + 'name' => $this->_genreName, + 'slug' => $slug + ], $rules); + + if ($validator->fails()) { + return CommandResponse::fail($validator); + } + + Genre::create([ + 'name' => $this->_genreName, + 'slug' => $slug + ]); + + return CommandResponse::succeed(['message' => 'Genre created!']); + } +} diff --git a/app/Commands/CreatePlaylistCommand.php b/app/Commands/CreatePlaylistCommand.php index 69a731a9..51c9c8f7 100644 --- a/app/Commands/CreatePlaylistCommand.php +++ b/app/Commands/CreatePlaylistCommand.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Playlist; +use Poniverse\Ponyfm\Models\Playlist; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; diff --git a/app/Commands/DeleteAlbumCommand.php b/app/Commands/DeleteAlbumCommand.php index 1e2306f8..da1bacb8 100644 --- a/app/Commands/DeleteAlbumCommand.php +++ b/app/Commands/DeleteAlbumCommand.php @@ -20,18 +20,21 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Album; -use Illuminate\Support\Facades\Auth; +use Poniverse\Ponyfm\Models\Album; +use Auth; class DeleteAlbumCommand extends CommandBase { + /** @var int */ private $_albumId; + + /** @var Album */ private $_album; function __construct($albumId) { $this->_albumId = $albumId; - $this->_album = ALbum::find($albumId); + $this->_album = Album::find($albumId); } /** diff --git a/app/Commands/DeleteGenreCommand.php b/app/Commands/DeleteGenreCommand.php index ae9cee0e..0ce27885 100644 --- a/app/Commands/DeleteGenreCommand.php +++ b/app/Commands/DeleteGenreCommand.php @@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Commands; use Gate; use Illuminate\Foundation\Bus\DispatchesJobs; -use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Jobs\DeleteGenre; use Validator; diff --git a/app/Commands/DeletePlaylistCommand.php b/app/Commands/DeletePlaylistCommand.php index eb5c2cb1..9467d554 100644 --- a/app/Commands/DeletePlaylistCommand.php +++ b/app/Commands/DeletePlaylistCommand.php @@ -20,12 +20,15 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Playlist; -use Illuminate\Support\Facades\Auth; +use Poniverse\Ponyfm\Models\Playlist; +use Auth; class DeletePlaylistCommand extends CommandBase { + /** @var int */ private $_playlistId; + + /** @var Playlist */ private $_playlist; function __construct($playlistId) diff --git a/app/Commands/DeleteTrackCommand.php b/app/Commands/DeleteTrackCommand.php index fd18a7d9..fde024ca 100644 --- a/app/Commands/DeleteTrackCommand.php +++ b/app/Commands/DeleteTrackCommand.php @@ -20,11 +20,15 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Track; +use Gate; +use Poniverse\Ponyfm\Models\Track; class DeleteTrackCommand extends CommandBase { + /** @var int */ private $_trackId; + + /** @var Track */ private $_track; function __construct($trackId) @@ -38,9 +42,7 @@ class DeleteTrackCommand extends CommandBase */ public function authorize() { - $user = \Auth::user(); - - return $this->_track && $user != null && $this->_track->user_id == $user->id; + return Gate::allows('delete', $this->_track); } /** diff --git a/app/Commands/EditAlbumCommand.php b/app/Commands/EditAlbumCommand.php index 2bfadeea..63cc6267 100644 --- a/app/Commands/EditAlbumCommand.php +++ b/app/Commands/EditAlbumCommand.php @@ -20,16 +20,18 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Image; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Validator; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Image; +use Auth; +use DB; +use Validator; class EditAlbumCommand extends CommandBase { private $_input; + /** @var int */ private $_albumId; + /** @var Album */ private $_album; function __construct($trackId, $input) @@ -88,10 +90,6 @@ class EditAlbumCommand extends CommandBase $this->_album->syncTrackIds($trackIds); $this->_album->save(); - Album::where('id', $this->_album->id)->update([ - 'track_count' => DB::raw('(SELECT COUNT(id) FROM tracks WHERE album_id = ' . $this->_album->id . ')') - ]); - return CommandResponse::succeed(['real_cover_url' => $this->_album->getCoverUrl(Image::NORMAL)]); } } diff --git a/app/Commands/EditPlaylistCommand.php b/app/Commands/EditPlaylistCommand.php index 796b4511..c4136364 100644 --- a/app/Commands/EditPlaylistCommand.php +++ b/app/Commands/EditPlaylistCommand.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\PinnedPlaylist; -use Poniverse\Ponyfm\Playlist; +use Poniverse\Ponyfm\Models\PinnedPlaylist; +use Poniverse\Ponyfm\Models\Playlist; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; diff --git a/app/Commands/EditTrackCommand.php b/app/Commands/EditTrackCommand.php index dae10b6d..0e2ee814 100644 --- a/app/Commands/EditTrackCommand.php +++ b/app/Commands/EditTrackCommand.php @@ -20,11 +20,12 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackType; -use Poniverse\Ponyfm\User; +use Gate; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackType; +use Poniverse\Ponyfm\Models\User; use Auth; use DB; @@ -46,9 +47,7 @@ class EditTrackCommand extends CommandBase */ public function authorize() { - $user = \Auth::user(); - - return $this->_track && $user != null && $this->_track->user_id == $user->id; + return $this->_track && Gate::allows('edit', $this->_track); } /** @@ -61,8 +60,11 @@ class EditTrackCommand extends CommandBase $rules = [ 'title' => 'required|min:3|max:80', - 'released_at' => 'before:' . (date('Y-m-d', - time() + (86400 * 2))) . (isset($this->_input['released_at']) && $this->_input['released_at'] != "" ? '|date' : ''), + 'released_at' => 'before:' . + (date('Y-m-d', time() + (86400 * 2))) . ( + isset($this->_input['released_at']) && $this->_input['released_at'] != "" + ? '|date' + : ''), 'license_id' => 'required|exists:licenses,id', 'genre_id' => 'required|exists:genres,id', 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', @@ -140,7 +142,7 @@ class EditTrackCommand extends CommandBase } else { if (isset($this->_input['cover'])) { $cover = $this->_input['cover']; - $track->cover_id = Image::upload($cover, Auth::user())->id; + $track->cover_id = Image::upload($cover, $track->user_id)->id; } else { if ($this->_input['remove_cover'] == 'true') { $track->cover_id = null; diff --git a/app/Commands/GenerateTrackFilesCommand.php b/app/Commands/GenerateTrackFilesCommand.php new file mode 100644 index 00000000..8663cfe2 --- /dev/null +++ b/app/Commands/GenerateTrackFilesCommand.php @@ -0,0 +1,187 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use FFmpegMovie; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; +use Poniverse\Ponyfm\Jobs\EncodeTrackFile; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackFile; +use AudioCache; +use File; +use Illuminate\Support\Str; +use SplFileInfo; +use Validator; + +/** + * This command is the "second phase" of the upload process - once metadata has + * been parsed and the track object is created, this generates the track's + * corresponding TrackFile objects and ensures that all of them have been encoded. + * + * @package Poniverse\Ponyfm\Commands + */ +class GenerateTrackFilesCommand extends CommandBase +{ + use DispatchesJobs; + + private $track; + private $autoPublish; + private $sourceFile; + + protected static $_losslessFormats = [ + 'flac', + 'pcm', + 'adpcm', + 'alac' + ]; + + public function __construct(Track $track, SplFileInfo $sourceFile, bool $autoPublish = false) + { + $this->track = $track; + $this->autoPublish = $autoPublish; + $this->sourceFile = $sourceFile; + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + try { + $source = $this->sourceFile->getPathname(); + + // Lossy uploads need to be identified and set as the master file + // without being re-encoded. + $audioObject = AudioCache::get($source); + $isLossyUpload = !$this->isLosslessFile($audioObject); + $codecString = $audioObject->getAudioCodec(); + + if ($isLossyUpload) { + if ($codecString === 'mp3') { + $masterFormat = 'MP3'; + + } else if (Str::startsWith($codecString, 'aac')) { + $masterFormat = 'AAC'; + + } else if ($codecString === 'vorbis') { + $masterFormat = 'OGG Vorbis'; + + } else { + $this->track->delete(); + return CommandResponse::fail(['track' => "The track does not contain audio in a known lossy format. The format read from the file is: {$codecString}"]); + } + + // Sanity check: skip creating this TrackFile if it already exists. + $trackFile = $this->trackFileExists($masterFormat); + + if (!$trackFile) { + $trackFile = new TrackFile(); + $trackFile->is_master = true; + $trackFile->format = $masterFormat; + $trackFile->track_id = $this->track->id; + $trackFile->save(); + } + + // Lossy masters are copied into the datastore - no re-encoding involved. + File::copy($source, $trackFile->getFile()); + } + + + $trackFiles = []; + + foreach (Track::$Formats as $name => $format) { + // Don't bother with lossless transcodes of lossy uploads, and + // don't re-encode the lossy master. + if ($isLossyUpload && ($format['is_lossless'] || $name === $masterFormat)) { + continue; + } + + // Sanity check: skip creating this TrackFile if it already exists. + // But, we'll still encode it! + if ($trackFile = $this->trackFileExists($name)) { + $trackFiles[] = $trackFile; + continue; + } + + $trackFile = new TrackFile(); + $trackFile->is_master = $name === 'FLAC' ? true : false; + $trackFile->format = $name; + $trackFile->status = TrackFile::STATUS_PROCESSING_PENDING; + + if (in_array($name, Track::$CacheableFormats) && !$trackFile->is_master) { + $trackFile->is_cacheable = true; + } else { + $trackFile->is_cacheable = false; + } + $this->track->trackFiles()->save($trackFile); + + // All TrackFile records we need are synchronously created + // before kicking off the encode jobs in order to avoid a race + // condition with the "temporary" source file getting deleted. + $trackFiles[] = $trackFile; + } + + try { + foreach ($trackFiles as $trackFile) { + $this->dispatch(new EncodeTrackFile($trackFile, false, true, $this->autoPublish)); + } + + } catch (InvalidEncodeOptionsException $e) { + $this->track->delete(); + return CommandResponse::fail(['track' => [$e->getMessage()]]); + } + + } catch (\Exception $e) { + $this->track->delete(); + throw $e; + } + + return CommandResponse::succeed([ + 'id' => $this->track->id, + 'name' => $this->track->name, + 'title' => $this->track->title, + 'slug' => $this->track->slug, + 'autoPublish' => $this->autoPublish, + ]); + } + + /** + * @param FFmpegMovie|string $file object or full path of the file we're checking + * @return bool whether the given file is lossless + */ + private function isLosslessFile($file) { + if (is_string($file)) { + $file = AudioCache::get($file); + } + + return Str::startsWith($file->getAudioCodec(), static::$_losslessFormats); + } + + /** + * @param string $format + * @return TrackFile|null + */ + private function trackFileExists(string $format) { + return $this->track->trackFiles()->where('format', $format)->first(); + } +} diff --git a/app/Commands/MergeAccountsCommand.php b/app/Commands/MergeAccountsCommand.php new file mode 100644 index 00000000..c41eb087 --- /dev/null +++ b/app/Commands/MergeAccountsCommand.php @@ -0,0 +1,120 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Carbon\Carbon; +use DB; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Comment; +use Poniverse\Ponyfm\Models\Favourite; +use Poniverse\Ponyfm\Models\Follower; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\PinnedPlaylist; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\ResourceUser; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; + +class MergeAccountsCommand extends CommandBase +{ + private $sourceAccount; + private $destinationAccount; + + function __construct(User $sourceAccount, User $destinationAccount) + { + $this->sourceAccount = $sourceAccount; + $this->destinationAccount = $destinationAccount; + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + DB::transaction(function() { + $accountIds = [$this->sourceAccount->id]; + + foreach (Album::whereIn('user_id', $accountIds)->get() as $album) { + $album->user_id = $this->destinationAccount->id; + $album->save(); + } + + foreach (Comment::whereIn('user_id', $accountIds)->get() as $comment) { + $comment->user_id = $this->destinationAccount->id; + $comment->save(); + } + + foreach (Favourite::whereIn('user_id', $accountIds)->get() as $favourite) { + $favourite->user_id = $this->destinationAccount->id; + $favourite->save(); + } + + foreach (Follower::whereIn('artist_id', $accountIds)->get() as $follow) { + $follow->artist_id = $this->destinationAccount->id; + $follow->save(); + } + + foreach (Image::whereIn('uploaded_by', $accountIds)->get() as $image) { + $image->uploaded_by = $this->destinationAccount->id; + $image->save(); + } + + foreach (Image::whereIn('uploaded_by', $accountIds)->get() as $image) { + $image->uploaded_by = $this->destinationAccount->id; + $image->save(); + } + + DB::table('oauth2_tokens')->whereIn('user_id', $accountIds)->update(['user_id' => $this->destinationAccount->id]); + + foreach (PinnedPlaylist::whereIn('user_id', $accountIds)->get() as $playlist) { + $playlist->user_id = $this->destinationAccount->id; + $playlist->save(); + } + + foreach (Playlist::whereIn('user_id', $accountIds)->get() as $playlist) { + $playlist->user_id = $this->destinationAccount->id; + $playlist->save(); + } + + foreach (ResourceLogItem::whereIn('user_id', $accountIds)->get() as $item) { + $item->user_id = $this->destinationAccount->id; + $item->save(); + } + + foreach (ResourceUser::whereIn('user_id', $accountIds)->get() as $item) { + $item->user_id = $this->destinationAccount->id; + $item->save(); + } + + foreach (Track::whereIn('user_id', $accountIds)->get() as $track) { + $track->user_id = $this->destinationAccount->id; + $track->save(); + } + + $this->sourceAccount->disabled_at = Carbon::now(); + $this->sourceAccount->save(); + }); + + return CommandResponse::succeed(); + } +} diff --git a/app/Commands/ParseTrackTagsCommand.php b/app/Commands/ParseTrackTagsCommand.php new file mode 100644 index 00000000..6fbfb1d2 --- /dev/null +++ b/app/Commands/ParseTrackTagsCommand.php @@ -0,0 +1,441 @@ +. + */ + +namespace Poniverse\Ponyfm\Commands; + +use Carbon\Carbon; +use Config; +use getID3; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Genre; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\Track; +use AudioCache; +use File; +use Illuminate\Support\Str; +use Poniverse\Ponyfm\Models\TrackType; +use Poniverse\Ponyfm\Models\User; +use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; + + +class ParseTrackTagsCommand extends CommandBase +{ + private $track; + private $fileToParse; + private $input; + + public function __construct(Track $track, \Symfony\Component\HttpFoundation\File\File $fileToParse, $inputTags = []) + { + $this->track = $track; + $this->fileToParse = $fileToParse; + $this->input = $inputTags; + } + + /** + * @throws \Exception + * @return CommandResponse + */ + public function execute() + { + $audio = AudioCache::get($this->fileToParse->getPathname()); + list($parsedTags, $rawTags) = $this->parseOriginalTags($this->fileToParse, $this->track->user, $audio->getAudioCodec()); + $this->track->original_tags = ['parsed_tags' => $parsedTags, 'raw_tags' => $rawTags]; + + + if ($this->input['cover'] !== null) { + $this->track->cover_id = Image::upload($this->input['cover'], $this->track->user_id)->id; + } else { + $this->track->cover_id = $parsedTags['cover_id']; + } + + $this->track->title = $this->input['title'] ?? $parsedTags['title'] ?? $this->track->title; + $this->track->track_type_id = $this->input['track_type_id'] ?? TrackType::UNCLASSIFIED_TRACK; + + $this->track->genre_id = isset($this->input['genre']) + ? $this->getGenreId($this->input['genre']) + : $parsedTags['genre_id']; + + $this->track->album_id = isset($this->input['album']) + ? $this->getAlbumId($this->track->user_id, $this->input['album']) + : $parsedTags['album_id']; + + if ($this->track->album_id === null) { + $this->track->track_number = null; + } else { + $this->track->track_number = $this->input['track_number'] ?? $parsedTags['track_number']; + } + + $this->track->released_at = isset($this->input['released_at']) + ? Carbon::createFromFormat(Carbon::ISO8601, $this->input['released_at']) + : $parsedTags['release_date']; + + $this->track->description = $this->input['description'] ?? $parsedTags['comments']; + $this->track->lyrics = $this->input['lyrics'] ?? $parsedTags['lyrics']; + + $this->track->is_vocal = $this->input['is_vocal'] ?? $parsedTags['is_vocal']; + $this->track->is_explicit = $this->input['is_explicit'] ?? false; + $this->track->is_downloadable = $this->input['is_downloadable'] ?? true; + $this->track->is_listed = $this->input['is_listed'] ?? true; + + $this->track->save(); + return CommandResponse::succeed(); + } + + /** + * Returns the ID of the given genre, creating it if necessary. + * + * @param string $genreName + * @return int + */ + protected function getGenreId(string $genreName) { + return Genre::firstOrCreate([ + 'name' => $genreName, + 'slug' => Str::slug($genreName) + ])->id; + } + + /** + * Returns the ID of the given album, creating it if necessary. + * The cover ID is only used if a new album is created - it will not be + * written to an existing album. + * + * @param int $artistId + * @param string|null $albumName + * @param null $coverId + * @return int|null + */ + protected function getAlbumId(int $artistId, $albumName, $coverId = null) { + if (null !== $albumName) { + $album = Album::firstOrNew([ + 'user_id' => $artistId, + 'title' => $albumName + ]); + + if (null === $album->id) { + $album->description = ''; + $album->track_count = 0; + $album->cover_id = $coverId; + $album->save(); + } + + return $album->id; + } else { + return null; + } + } + + /** + * Extracts a file's tags. + * + * @param \Symfony\Component\HttpFoundation\File\File $file + * @param User $artist + * @param string $audioCodec + * @return array the "processed" and raw tags extracted from the file + * @throws \Exception + */ + protected function parseOriginalTags(\Symfony\Component\HttpFoundation\File\File $file, User $artist, string $audioCodec) { + //========================================================================================================== + // Extract the original tags. + //========================================================================================================== + $getId3 = new getID3; + + // all tags read by getID3, including the cover art + $allTags = $getId3->analyze($file->getPathname()); + + // $rawTags => tags specific to a file format (ID3 or Atom), pre-normalization but with cover art removed + // $parsedTags => normalized tags used by Pony.fm + + if ($audioCodec === 'mp3') { + list($parsedTags, $rawTags) = $this->getId3Tags($allTags); + + } elseif (Str::startsWith($audioCodec, ['aac', 'alac'])) { + list($parsedTags, $rawTags) = $this->getAtomTags($allTags); + + } elseif (in_array($audioCodec, ['vorbis', 'flac'])) { + list($parsedTags, $rawTags) = $this->getVorbisTags($allTags); + + } elseif (Str::startsWith($audioCodec, ['pcm', 'adpcm'])) { + list($parsedTags, $rawTags) = $this->getAtomTags($allTags); + + } else { + // Assume the file is untagged if it's in an unknown format. + $parsedTags = [ + 'title' => null, + 'artist' => null, + 'band' => null, + 'genre' => null, + 'track_number' => null, + 'album' => null, + 'year' => null, + 'release_date' => null, + 'comments' => null, + 'lyrics' => null, + ]; + $rawTags = []; + } + + //========================================================================================================== + // Determine the release date. + //========================================================================================================== + if ($parsedTags['release_date'] === null && $parsedTags['year'] !== null) { + $parsedTags['release_date'] = Carbon::create($parsedTags['year'], 1, 1); + } + + //========================================================================================================== + // Does this track have vocals? + //========================================================================================================== + $parsedTags['is_vocal'] = $parsedTags['lyrics'] !== null; + + + //========================================================================================================== + // Determine the genre. + //========================================================================================================== + $genreName = $parsedTags['genre']; + + if ($genreName !== null) { + $parsedTags['genre_id'] = $this->getGenreId($genreName); + + } else { + $parsedTags['genre_id'] = null; + } + + //========================================================================================================== + // Extract the cover art, if any exists. + //========================================================================================================== + + $coverId = null; + if (array_key_exists('comments', $allTags) && array_key_exists('picture', $allTags['comments'])) { + $image = $allTags['comments']['picture'][0]; + + if ($image['image_mime'] === 'image/png') { + $extension = 'png'; + + } elseif ($image['image_mime'] === 'image/jpeg') { + $extension = 'jpg'; + + } else { + throw new BadRequestHttpException('Unknown cover format embedded in the track file!'); + } + + // write temporary image file + $tmpPath = Config::get('ponyfm.files_directory') . '/tmp'; + + $filename = $file->getFilename() . ".cover.${extension}"; + $imageFilePath = "${tmpPath}/${filename}"; + + File::put($imageFilePath, $image['data']); + $imageFile = new UploadedFile($imageFilePath, $filename, $image['image_mime']); + + $cover = Image::upload($imageFile, $artist); + $coverId = $cover->id; + + } else { + // no cover art was found - carry on + } + + $parsedTags['cover_id'] = $coverId; + + + //========================================================================================================== + // Is this part of an album? + //========================================================================================================== + $albumId = null; + $albumName = $parsedTags['album']; + + if ($albumName !== null) { + $albumId = $this->getAlbumId($artist->id, $albumName, $coverId); + } + + $parsedTags['album_id'] = $albumId; + + + return [$parsedTags, $rawTags]; + } + + + /** + * @param array $rawTags + * @return array + */ + protected function getId3Tags($rawTags) { + if (array_key_exists('tags', $rawTags) && array_key_exists('id3v2', $rawTags['tags'])) { + $tags = $rawTags['tags']['id3v2']; + } elseif (array_key_exists('tags', $rawTags) && array_key_exists('id3v1', $rawTags['tags'])) { + $tags = $rawTags['tags']['id3v1']; + } else { + $tags = []; + } + + + $comment = null; + + if (isset($tags['comment'])) { + // The "comment" tag comes in with a badly encoded string index + // so its array key has to be used implicitly. + $key = array_keys($tags['comment'])[0]; + + // The comment may have a null byte at the end. trim() removes it. + $comment = trim($tags['comment'][$key]); + + // Replace the malformed comment with the "fixed" one. + unset($tags['comment'][$key]); + $tags['comment'][0] = $comment; + } + + return [ + [ + 'title' => isset($tags['title']) ? $tags['title'][0] : null, + 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, + 'band' => isset($tags['band']) ? $tags['band'][0] : null, + 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, + 'track_number' => isset($tags['track_number']) ? $tags['track_number'][0] : null, + 'album' => isset($tags['album']) ? $tags['album'][0] : null, + 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, + 'release_date' => isset($tags['release_date']) ? $this->parseDateString($tags['release_date'][0]) : null, + 'comments' => $comment, + 'lyrics' => isset($tags['unsynchronised_lyric']) ? $tags['unsynchronised_lyric'][0] : null, + ], + $tags + ]; + } + + /** + * @param array $rawTags + * @return array + */ + protected function getAtomTags($rawTags) { + if (array_key_exists('tags', $rawTags) && array_key_exists('quicktime', $rawTags['tags'])) { + $tags = $rawTags['tags']['quicktime']; + } else { + $tags = []; + } + + $trackNumber = null; + if (isset($tags['track_number'])) { + $trackNumberComponents = explode('/', $tags['track_number'][0]); + $trackNumber = $trackNumberComponents[0]; + } + + if (isset($tags['release_date'])) { + $releaseDate = $this->parseDateString($tags['release_date'][0]); + + } elseif (isset($tags['creation_date'])) { + $releaseDate = $this->parseDateString($tags['creation_date'][0]); + } else { + $releaseDate = 0; + } + + return [ + [ + 'title' => isset($tags['title']) ? $tags['title'][0] : null, + 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, + 'band' => isset($tags['band']) ? $tags['band'][0] : null, + 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, + 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, + 'track_number' => $trackNumber, + 'album' => isset($tags['album']) ? $tags['album'][0] : null, + 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, + 'release_date' => $releaseDate, + 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, + 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, + ], + $tags + ]; + } + + /** + * @param array $rawTags + * @return array + */ + protected function getVorbisTags($rawTags) { + if (array_key_exists('tags', $rawTags) && array_key_exists('vorbiscomment', $rawTags['tags'])) { + $tags = $rawTags['tags']['vorbiscomment']; + } else { + $tags = []; + } + + $trackNumber = null; + if (isset($tags['track_number'])) { + $trackNumberComponents = explode('/', $tags['track_number'][0]); + $trackNumber = $trackNumberComponents[0]; + } + + return [ + [ + 'title' => isset($tags['title']) ? $tags['title'][0] : null, + 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, + 'band' => isset($tags['band']) ? $tags['band'][0] : null, + 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, + 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, + 'track_number' => $trackNumber, + 'album' => isset($tags['album']) ? $tags['album'][0] : null, + 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, + 'release_date' => isset($tags['date']) ? $this->parseDateString($tags['date'][0]) : null, + 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, + 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, + ], + $tags + ]; + } + + /** + * Parses a potentially-partial date string into a proper date object. + * + * The tagging formats we deal with base their date format on ISO 8601, but + * the timestamp may be incomplete. + * + * @link https://code.google.com/p/mp4v2/wiki/iTunesMetadata + * @link https://wiki.xiph.org/VorbisComment#Date_and_time + * @link http://id3.org/id3v2.4.0-frames + * + * @param string $dateString + * @return null|Carbon + */ + protected function parseDateString(string $dateString) { + switch (Str::length($dateString)) { + // YYYY + case 4: + return Carbon::createFromFormat('Y', $dateString) + ->month(1) + ->day(1); + + // YYYY-MM + case 7: + return Carbon::createFromFormat('Y-m', $dateString) + ->day(1); + + // YYYY-MM-DD + case 10: + return Carbon::createFromFormat('Y-m-d', $dateString); + break; + + default: + // We might have an ISO-8601 string in our hooves. + // If not, give up. + try { + return Carbon::createFromFormat(Carbon::ISO8601, $dateString); + + } catch (\InvalidArgumentException $e) { + return null; + } + } + } +} diff --git a/app/Commands/RenameGenreCommand.php b/app/Commands/RenameGenreCommand.php index afbe5d9d..bb71aa9b 100644 --- a/app/Commands/RenameGenreCommand.php +++ b/app/Commands/RenameGenreCommand.php @@ -21,12 +21,16 @@ namespace Poniverse\Ponyfm\Commands; use Gate; +use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Support\Str; -use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Jobs\UpdateTagsForRenamedGenre; +use Poniverse\Ponyfm\Models\Genre; use Validator; class RenameGenreCommand extends CommandBase { + use DispatchesJobs; + /** @var Genre */ private $_genre; private $_newName; @@ -72,6 +76,8 @@ class RenameGenreCommand extends CommandBase $this->_genre->slug = $slug; $this->_genre->save(); + $this->dispatch(new UpdateTagsForRenamedGenre($this->_genre)); + return CommandResponse::succeed(['message' => 'Genre renamed!']); } } diff --git a/app/Commands/SaveAccountSettingsCommand.php b/app/Commands/SaveAccountSettingsCommand.php index 9a285c25..2c535125 100644 --- a/app/Commands/SaveAccountSettingsCommand.php +++ b/app/Commands/SaveAccountSettingsCommand.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Image; +use Poniverse\Ponyfm\Models\Image; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Validator; diff --git a/app/Commands/ToggleFavouriteCommand.php b/app/Commands/ToggleFavouriteCommand.php index 4e97ac3f..c7bff8e3 100644 --- a/app/Commands/ToggleFavouriteCommand.php +++ b/app/Commands/ToggleFavouriteCommand.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Favourite; -use Poniverse\Ponyfm\ResourceUser; +use Poniverse\Ponyfm\Models\Favourite; +use Poniverse\Ponyfm\Models\ResourceUser; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; diff --git a/app/Commands/ToggleFollowingCommand.php b/app/Commands/ToggleFollowingCommand.php index ff2c5d66..fda5aa13 100644 --- a/app/Commands/ToggleFollowingCommand.php +++ b/app/Commands/ToggleFollowingCommand.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Commands; -use Poniverse\Ponyfm\Follower; -use Poniverse\Ponyfm\ResourceUser; +use Poniverse\Ponyfm\Models\Follower; +use Poniverse\Ponyfm\Models\ResourceUser; use Illuminate\Support\Facades\Auth; class ToggleFollowingCommand extends CommandBase diff --git a/app/Commands/UploadTrackCommand.php b/app/Commands/UploadTrackCommand.php index 4c09f97c..a18d792a 100644 --- a/app/Commands/UploadTrackCommand.php +++ b/app/Commands/UploadTrackCommand.php @@ -22,46 +22,30 @@ namespace Poniverse\Ponyfm\Commands; use Carbon\Carbon; use Config; -use getID3; use Illuminate\Foundation\Bus\DispatchesJobs; use Input; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; -use Poniverse\Ponyfm\Genre; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Jobs\EncodeTrackFile; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\Track; use AudioCache; -use File; -use Illuminate\Support\Str; -use Poniverse\Ponyfm\TrackType; -use Poniverse\Ponyfm\User; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Validator; class UploadTrackCommand extends CommandBase { use DispatchesJobs; - private $_allowLossy; private $_allowShortTrack; private $_customTrackSource; private $_autoPublishByDefault; - private $_losslessFormats = [ - 'flac', - 'pcm_s16le ([1][0][0][0] / 0x0001)', - 'pcm_s16be', - 'adpcm_ms ([2][0][0][0] / 0x0002)', - 'pcm_s24le ([1][0][0][0] / 0x0001)', - 'pcm_s24be', - 'pcm_f32le ([3][0][0][0] / 0x0003)', - 'pcm_f32be (fl32 / 0x32336C66)' - ]; - - public function __construct($allowLossy = false, $allowShortTrack = false, $customTrackSource = null, $autoPublishByDefault = false) + /** + * UploadTrackCommand constructor. + * + * @param bool $allowLossy + * @param bool $allowShortTrack allow tracks shorter than 30 seconds + * @param string|null $customTrackSource value to set in the track's "source" field; if left blank, "direct_upload" is used + * @param bool $autoPublishByDefault + */ + public function __construct(bool $allowLossy = false, bool $allowShortTrack = false, string $customTrackSource = null, bool $autoPublishByDefault = false) { $this->_allowLossy = $allowLossy; $this->_allowShortTrack = $allowShortTrack; @@ -84,22 +68,21 @@ class UploadTrackCommand extends CommandBase public function execute() { $user = \Auth::user(); - $trackFile = \Input::file('track', null); + $trackFile = Input::file('track', null); + $coverFile = Input::file('cover', null); if (null === $trackFile) { return CommandResponse::fail(['track' => ['You must upload an audio file!']]); } $audio = \AudioCache::get($trackFile->getPathname()); - list($parsedTags, $rawTags) = $this->parseOriginalTags($trackFile, $user, $audio->getAudioCodec()); - $track = new Track(); $track->user_id = $user->id; - $track->title = Input::get('title', $parsedTags['title']); + // The title set here is a placeholder; it'll be replaced by ParseTrackTagsCommand + // if the file contains a title tag. + $track->title = Input::get('title', pathinfo($trackFile->getClientOriginalName(), PATHINFO_FILENAME)); $track->duration = $audio->getDuration(); - - $track->save(); $track->ensureDirectoryExists(); @@ -110,11 +93,14 @@ class UploadTrackCommand extends CommandBase $input = Input::all(); $input['track'] = $trackFile; + $input['cover'] = $coverFile; $validator = \Validator::make($input, [ 'track' => 'required|' - . ($this->_allowLossy ? '' : 'audio_format:'. implode(',', $this->_losslessFormats).'|') + . ($this->_allowLossy + ? 'audio_format:flac,alac,pcm,adpcm,aac,mp3,vorbis|' + : 'audio_format:flac,alac,pcm,adpcm|') . ($this->_allowShortTrack ? '' : 'min_duration:30|') . 'audio_channels:1,2', @@ -139,467 +125,22 @@ class UploadTrackCommand extends CommandBase $track->delete(); return CommandResponse::fail($validator); } - - - // Process optional track fields $autoPublish = (bool) ($input['auto_publish'] ?? $this->_autoPublishByDefault); - - if (Input::hasFile('cover')) { - $track->cover_id = Image::upload(Input::file('cover'), $track->user_id)->id; - } else { - $track->cover_id = $parsedTags['cover_id']; - } - - $track->title = $input['title'] ?? $parsedTags['title'] ?? $track->title; - $track->track_type_id = $input['track_type_id'] ?? TrackType::UNCLASSIFIED_TRACK; - - $track->genre_id = isset($input['genre']) - ? $this->getGenreId($input['genre']) - : $parsedTags['genre_id']; - - $track->album_id = isset($input['album']) - ? $this->getAlbumId($user->id, $input['album']) - : $parsedTags['album_id']; - - if ($track->album_id === null) { - $track->track_number = null; - } else { - $track->track_number = $input['track_number'] ?? $parsedTags['track_number']; - } - - $track->released_at = isset($input['released_at']) - ? Carbon::createFromFormat(Carbon::ISO8601, $input['released_at']) - : $parsedTags['release_date']; - - $track->description = $input['description'] ?? $parsedTags['comments']; - $track->lyrics = $input['lyrics'] ?? $parsedTags['lyrics']; - - $track->is_vocal = $input['is_vocal'] ?? $parsedTags['is_vocal']; - $track->is_explicit = $input['is_explicit'] ?? false; - $track->is_downloadable = $input['is_downloadable'] ?? true; - $track->is_listed = $input['is_listed'] ?? true; - $track->source = $this->_customTrackSource ?? 'direct_upload'; + $track->source = $this->_customTrackSource ?? 'direct_upload'; // If json_decode() isn't called here, Laravel will surround the JSON // string with quotes when storing it in the database, which breaks things. $track->metadata = json_decode(Input::get('metadata', null)); - $track->original_tags = ['parsed_tags' => $parsedTags, 'raw_tags' => $rawTags]; - $track->save(); - - try { - $source = $trackFile->getPathname(); - - // Lossy uploads need to be identified and set as the master file - // without being re-encoded. - $audioObject = AudioCache::get($source); - $isLossyUpload = !in_array($audioObject->getAudioCodec(), $this->_losslessFormats); - - if ($isLossyUpload) { - if ($audioObject->getAudioCodec() === 'mp3') { - $masterFormat = 'MP3'; - - } else if (Str::startsWith($audioObject->getAudioCodec(), 'aac')) { - $masterFormat = 'AAC'; - - } else if ($audioObject->getAudioCodec() === 'vorbis') { - $masterFormat = 'OGG Vorbis'; - - } else { - $validator->messages()->add('track', 'The track does not contain audio in a known lossy format.'); - $track->delete(); - return CommandResponse::fail($validator); - } - - $trackFile = new TrackFile(); - $trackFile->is_master = true; - $trackFile->format = $masterFormat; - $trackFile->track_id = $track->id; - $trackFile->save(); - - // Lossy masters are copied into the datastore - no re-encoding involved. - File::copy($source, $trackFile->getFile()); - } - - - $trackFiles = []; - - foreach (Track::$Formats as $name => $format) { - // Don't bother with lossless transcodes of lossy uploads, and - // don't re-encode the lossy master. - if ($isLossyUpload && ($format['is_lossless'] || $name === $masterFormat)) { - continue; - } - - $trackFile = new TrackFile(); - $trackFile->is_master = $name === 'FLAC' ? true : false; - $trackFile->format = $name; - $trackFile->status = TrackFile::STATUS_PROCESSING; - - if (in_array($name, Track::$CacheableFormats) && $trackFile->is_master == false) { - $trackFile->is_cacheable = true; - } else { - $trackFile->is_cacheable = false; - } - $track->trackFiles()->save($trackFile); - - // All TrackFile records we need are synchronously created - // before kicking off the encode jobs in order to avoid a race - // condition with the "temporary" source file getting deleted. - $trackFiles[] = $trackFile; - } - - try { - foreach($trackFiles as $trackFile) { - $this->dispatch(new EncodeTrackFile($trackFile, false, true, $autoPublish)); - } - - } catch (InvalidEncodeOptionsException $e) { - $track->delete(); - return CommandResponse::fail(['track' => [$e->getMessage()]]); - } - - } catch (\Exception $e) { - $track->delete(); - throw $e; + // Parse any tags in the uploaded files. + $parseTagsCommand = new ParseTrackTagsCommand($track, $trackFile, $input); + $result = $parseTagsCommand->execute(); + if ($result->didFail()) { + return $result; } - return CommandResponse::succeed([ - 'id' => $track->id, - 'name' => $track->name, - 'title' => $track->title, - 'slug' => $track->slug, - 'autoPublish' => $autoPublish, - ]); - } - - /** - * Returns the ID of the given genre, creating it if necessary. - * - * @param string $genreName - * @return int - */ - protected function getGenreId(string $genreName) { - return Genre::firstOrCreate([ - 'name' => $genreName, - 'slug' => Str::slug($genreName) - ])->id; - } - - /** - * Returns the ID of the given album, creating it if necessary. - * The cover ID is only used if a new album is created - it will not be - * written to an existing album. - * - * @param int $artistId - * @param string|null $albumName - * @param null $coverId - * @return int|null - */ - protected function getAlbumId(int $artistId, $albumName, $coverId = null) { - if (null !== $albumName) { - $album = Album::firstOrNew([ - 'user_id' => $artistId, - 'title' => $albumName - ]); - - if (null === $album->id) { - $album->description = ''; - $album->track_count = 0; - $album->cover_id = $coverId; - $album->save(); - } - - return $album->id; - } else { - return null; - } - } - - /** - * Extracts a file's tags. - * - * @param UploadedFile $file - * @param User $artist - * @param string $audioCodec - * @return array the "processed" and raw tags extracted from the file - * @throws \Exception - */ - protected function parseOriginalTags(UploadedFile $file, User $artist, string $audioCodec) { - //========================================================================================================== - // Extract the original tags. - //========================================================================================================== - $getId3 = new getID3; - - // all tags read by getID3, including the cover art - $allTags = $getId3->analyze($file->getPathname()); - - // tags specific to a file format (ID3 or Atom), pre-normalization but with cover art removed - $rawTags = []; - - // normalized tags used by Pony.fm - $parsedTags = []; - - if ($audioCodec === 'mp3') { - list($parsedTags, $rawTags) = $this->getId3Tags($allTags); - - } elseif (Str::startsWith($audioCodec, 'aac')) { - list($parsedTags, $rawTags) = $this->getAtomTags($allTags); - - } elseif ($audioCodec === 'vorbis') { - list($parsedTags, $rawTags) = $this->getVorbisTags($allTags); - - } elseif ($audioCodec === 'flac') { - list($parsedTags, $rawTags) = $this->getVorbisTags($allTags); - - } elseif (Str::startsWith($audioCodec, ['pcm', 'adpcm'])) { - list($parsedTags, $rawTags) = $this->getAtomTags($allTags); - - } - - - //========================================================================================================== - // Fill in the title tag if it's missing - //========================================================================================================== - $parsedTags['title'] = $parsedTags['title'] ?? pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME); - - - //========================================================================================================== - // Determine the release date. - //========================================================================================================== - if ($parsedTags['release_date'] === null && $parsedTags['year'] !== null) { - $parsedTags['release_date'] = Carbon::create($parsedTags['year'], 1, 1); - } - - //========================================================================================================== - // Does this track have vocals? - //========================================================================================================== - $parsedTags['is_vocal'] = $parsedTags['lyrics'] !== null; - - - //========================================================================================================== - // Determine the genre. - //========================================================================================================== - $genreName = $parsedTags['genre']; - - if ($genreName) { - $parsedTags['genre_id'] = $this->getGenreId($genreName); - - } else { - $parsedTags['genre_id'] = $this->getGenreId('Unknown'); - } - - //========================================================================================================== - // Extract the cover art, if any exists. - //========================================================================================================== - - $coverId = null; - if (array_key_exists('comments', $allTags) && array_key_exists('picture', $allTags['comments'])) { - $image = $allTags['comments']['picture'][0]; - - if ($image['image_mime'] === 'image/png') { - $extension = 'png'; - - } elseif ($image['image_mime'] === 'image/jpeg') { - $extension = 'jpg'; - - } else { - throw new BadRequestHttpException('Unknown cover format embedded in the track file!'); - } - - // write temporary image file - $tmpPath = Config::get('ponyfm.files_directory') . '/tmp'; - - $filename = $file->getFilename() . ".cover.${extension}"; - $imageFilePath = "${tmpPath}/${filename}"; - - File::put($imageFilePath, $image['data']); - $imageFile = new UploadedFile($imageFilePath, $filename, $image['image_mime']); - - $cover = Image::upload($imageFile, $artist); - $coverId = $cover->id; - - } else { - // no cover art was found - carry on - } - - $parsedTags['cover_id'] = $coverId; - - - //========================================================================================================== - // Is this part of an album? - //========================================================================================================== - $albumId = null; - $albumName = $parsedTags['album']; - - if ($albumName !== null) { - $albumId = $this->getAlbumId($artist->id, $albumName, $coverId); - } - - $parsedTags['album_id'] = $albumId; - - - return [$parsedTags, $rawTags]; - } - - - /** - * @param array $rawTags - * @return array - */ - protected function getId3Tags($rawTags) { - if (array_key_exists('tags', $rawTags) && array_key_exists('id3v2', $rawTags['tags'])) { - $tags = $rawTags['tags']['id3v2']; - } elseif (array_key_exists('tags', $rawTags) && array_key_exists('id3v1', $rawTags['tags'])) { - $tags = $rawTags['tags']['id3v1']; - } else { - $tags = []; - } - - - $comment = null; - - if (isset($tags['comment'])) { - // The "comment" tag comes in with a badly encoded string index - // so its array key has to be used implicitly. - $key = array_keys($tags['comment'])[0]; - - // The comment may have a null byte at the end. trim() removes it. - $comment = trim($tags['comment'][$key]); - - // Replace the malformed comment with the "fixed" one. - unset($tags['comment'][$key]); - $tags['comment'][0] = $comment; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => isset($tags['track_number']) ? $tags['track_number'][0] : null, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, - 'release_date' => isset($tags['release_date']) ? $this->parseDateString($tags['release_date'][0]) : null, - 'comments' => $comment, - 'lyrics' => isset($tags['unsynchronised_lyric']) ? $tags['unsynchronised_lyric'][0] : null, - ], - $tags - ]; - } - - /** - * @param array $rawTags - * @return array - */ - protected function getAtomTags($rawTags) { - if (array_key_exists('tags', $rawTags) && array_key_exists('quicktime', $rawTags['tags'])) { - $tags = $rawTags['tags']['quicktime']; - } else { - $tags = []; - } - - $trackNumber = null; - if (isset($tags['track_number'])) { - $trackNumberComponents = explode('/', $tags['track_number'][0]); - $trackNumber = $trackNumberComponents[0]; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => $trackNumber, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, - 'release_date' => isset($tags['release_date']) ? $this->parseDateString($tags['release_date'][0]) : null, - 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, - 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, - ], - $tags - ]; - } - - /** - * @param array $rawTags - * @return array - */ - protected function getVorbisTags($rawTags) { - if (array_key_exists('tags', $rawTags) && array_key_exists('vorbiscomment', $rawTags['tags'])) { - $tags = $rawTags['tags']['vorbiscomment']; - } else { - $tags = []; - } - - $trackNumber = null; - if (isset($tags['track_number'])) { - $trackNumberComponents = explode('/', $tags['track_number'][0]); - $trackNumber = $trackNumberComponents[0]; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => $trackNumber, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int) $tags['year'][0] : null, - 'release_date' => isset($tags['date']) ? $this->parseDateString($tags['date'][0]) : null, - 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, - 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, - ], - $tags - ]; - } - - /** - * Parses a potentially-partial date string into a proper date object. - * - * The tagging formats we deal with base their date format on ISO 8601, but - * the timestamp may be incomplete. - * - * @link https://code.google.com/p/mp4v2/wiki/iTunesMetadata - * @link https://wiki.xiph.org/VorbisComment#Date_and_time - * @link http://id3.org/id3v2.4.0-frames - * - * @param string $dateString - * @return null|Carbon - */ - protected function parseDateString(string $dateString) { - switch (Str::length($dateString)) { - // YYYY - case 4: - return Carbon::createFromFormat('Y', $dateString) - ->month(1) - ->day(1); - - // YYYY-MM - case 7: - return Carbon::createFromFormat('Y-m', $dateString) - ->day(1); - - // YYYY-MM-DD - case 10: - return Carbon::createFromFormat('Y-m-d', $dateString); - break; - - default: - // We might have an ISO-8601 string in our hooves. - // If not, give up. - try { - return Carbon::createFromFormat(Carbon::ISO8601, $dateString); - - } catch (\InvalidArgumentException $e) { - return null; - } - } + $generateTrackFiles = new GenerateTrackFilesCommand($track, $trackFile, $autoPublish); + return $generateTrackFiles->execute(); } } diff --git a/app/Console/Commands/ClassifyMLPMA.php b/app/Console/Commands/ClassifyMLPMA.php index 88f9ec98..926c2988 100644 --- a/app/Console/Commands/ClassifyMLPMA.php +++ b/app/Console/Commands/ClassifyMLPMA.php @@ -20,9 +20,9 @@ namespace Poniverse\Ponyfm\Console\Commands; -use Poniverse\Ponyfm\ShowSong; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackType; +use Poniverse\Ponyfm\Models\ShowSong; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackType; use DB; use Illuminate\Console\Command; use Illuminate\Support\Str; diff --git a/app/Console/Commands/ClearTrackCache.php b/app/Console/Commands/ClearTrackCache.php index 5c5feea1..88e5b279 100644 --- a/app/Console/Commands/ClearTrackCache.php +++ b/app/Console/Commands/ClearTrackCache.php @@ -24,7 +24,7 @@ use Carbon\Carbon; use File; use Illuminate\Console\Command; use Illuminate\Support\Facades\Cache; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\TrackFile; class ClearTrackCache extends Command { @@ -96,9 +96,6 @@ class ClearTrackCache extends Command $this->info('Deleted ' . $trackFile->getFile()); } - // Remove the cached file size for the album - Cache::forget($trackFile->track->album->getCacheKey('filesize-' . $trackFile->format)); - } $this->info($count . ' files deleted. Deletion complete. Exiting.'); } else { @@ -108,4 +105,4 @@ class ClearTrackCache extends Command } } -} \ No newline at end of file +} diff --git a/app/Console/Commands/FixMLPMAImages.php b/app/Console/Commands/FixMLPMAImages.php new file mode 100644 index 00000000..ef48157a --- /dev/null +++ b/app/Console/Commands/FixMLPMAImages.php @@ -0,0 +1,154 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use Config; +use DB; +use File; +use getID3; +use Illuminate\Console\Command; +use Poniverse\Ponyfm\Models\Image; +use Symfony\Component\HttpFoundation\File\UploadedFile; + +class FixMLPMAImages extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'mlpma:fix-images + {--startAt=1 : Track to start importing from. Useful for resuming an interrupted import.}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Re-imports MLPMA cover art'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + protected $currentFile; + + + /** + * File extensions to ignore when importing the archive. + * + * @var array + */ + protected $ignoredExtensions = [ + 'db', + 'jpg', + 'png', + 'txt', + 'rtf', + 'wma', + 'wmv' + ]; + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $mlpmaPath = Config::get('ponyfm.files_directory') . '/mlpma'; + $tmpPath = Config::get('ponyfm.files_directory') . '/tmp'; + + $this->comment('Enumerating MLP Music Archive source files...'); + $files = File::allFiles($mlpmaPath); + $this->info(sizeof($files) . ' files found!'); + + $this->comment('Importing tracks...'); + $totalFiles = sizeof($files); + $fileToStartAt = (int) $this->option('startAt') - 1; + + $this->comment("Skipping $fileToStartAt files..." . PHP_EOL); + $files = array_slice($files, $fileToStartAt); + $this->currentFile = $fileToStartAt; + + + foreach ($files as $file) { + $this->currentFile ++; + + $this->info('[' . $this->currentFile . '/' . $totalFiles . '] Importing track [' . $file->getFilename() . ']...'); + if (in_array($file->getExtension(), $this->ignoredExtensions)) { + $this->comment('This is not an audio file! Skipping...' . PHP_EOL); + continue; + } + // Get this track's MLPMA record + $importedTrack = DB::table('mlpma_tracks') + ->where('filename', '=', $file->getFilename()) + ->join('tracks', 'mlpma_tracks.track_id', '=', 'tracks.id') + ->first(); + $artistId = $importedTrack->user_id; + + + //========================================================================================================== + // Extract the original tags. + //========================================================================================================== + + $getId3 = new getID3; + // all tags read by getID3, including the cover art + $allTags = $getId3->analyze($file->getPathname()); + + + //========================================================================================================== + // Extract the cover art, if any exists. + //========================================================================================================== + $coverId = null; + + if (array_key_exists('comments', $allTags) && array_key_exists('picture', $allTags['comments'])) { + $this->comment('Extracting cover art!'); + $image = $allTags['comments']['picture'][0]; + if ($image['image_mime'] === 'image/png') { + $extension = 'png'; + } elseif ($image['image_mime'] === 'image/jpeg') { + $extension = 'jpg'; + } elseif ($image['image_mime'] === 'image/gif') { + $extension = 'gif'; + } else { + $this->error('Unknown cover art format!'); + } + // write temporary image file + $imageFilename = $file->getFilename() . ".cover.$extension"; + $imageFilePath = "$tmpPath/" . $imageFilename; + File::put($imageFilePath, $image['data']); + $imageFile = new UploadedFile($imageFilePath, $imageFilename, $image['image_mime']); + $cover = Image::upload($imageFile, $artistId, true); + $coverId = $cover->id; + + } else { + $this->comment('No cover art found!'); + } + } + } +} diff --git a/app/Console/Commands/FixYearZeroLogs.php b/app/Console/Commands/FixYearZeroLogs.php deleted file mode 100644 index 73912193..00000000 --- a/app/Console/Commands/FixYearZeroLogs.php +++ /dev/null @@ -1,87 +0,0 @@ -. - */ - -namespace Poniverse\Ponyfm\Console\Commands; - -use Carbon\Carbon; -use Poniverse\Ponyfm\ResourceLogItem; -use Illuminate\Console\Command; - -class FixYearZeroLogs extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'poni:year-zero'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Fills in missing timestamps in the resource_log_items table.'; - - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $items = ResourceLogItem::where('created_at', '0000-00-00 00:00:00')->orderBy('id', 'asc')->get(); - $totalItems = $items->count(); - - // calculate the start and end of the logging gap - $lastGoodId = (int) $items[0]->id - 1; - $lastGoodItem = ResourceLogItem::find($lastGoodId); - - $lastGoodDate = $lastGoodItem->created_at; - $now = Carbon::now(); - - $secondsDifference = $now->diffInSeconds($lastGoodDate); - $oneInterval = $secondsDifference / $totalItems; - - $this->info('Correcting records...'); - $bar = $this->output->createProgressBar($totalItems); - - foreach ($items as $i => $item) { - $bar->advance(); - $dateOffset = (int) ($oneInterval * $i); - $item->created_at = $lastGoodDate->copy()->addSeconds($dateOffset); - $item->save(); - } - - $bar->finish(); - $this->line(''); - $this->info('All done!'); - } -} diff --git a/app/Console/Commands/ImportMLPMA.php b/app/Console/Commands/ImportMLPMA.php deleted file mode 100644 index d6907962..00000000 --- a/app/Console/Commands/ImportMLPMA.php +++ /dev/null @@ -1,519 +0,0 @@ -. - */ - -namespace Poniverse\Ponyfm\Console\Commands; - -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Commands\UploadTrackCommand; -use Poniverse\Ponyfm\Genre; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\User; -use Auth; -use Carbon\Carbon; -use Config; -use DB; -use File; -use Illuminate\Console\Command; -use Illuminate\Support\Str; -use Input; -use Symfony\Component\HttpFoundation\File\UploadedFile; -use getID3; - - -class ImportMLPMA extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'mlpma:import - {--startAt=1 : Track to start importing from. Useful for resuming an interrupted import.}'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Imports the MLP Music Archive'; - - /** - * File extensions to ignore when importing the archive. - * - * @var array - */ - protected $ignoredExtensions = ['db', 'jpg', 'png', 'txt', 'rtf', 'wma', 'wmv']; - - /** - * Used to stop the import process when a SIGINT is received. - * - * @var bool - */ - protected $isInterrupted = false; - - /** - * A counter for the number of processed tracks. - * - * @var int - */ - protected $currentFile; - - /** - * Create a new command instance. - * - */ - public function __construct() - { - parent::__construct(); - } - - public function handleInterrupt($signo) - { - $this->error('Import aborted!'); - $this->error('Resume it from here using: --startAt=' . $this->currentFile); - $this->isInterrupted = true; - } - - /** - * Execute the console command. - * - * @return void - */ - public function handle() - { - pcntl_signal(SIGINT, [$this, 'handleInterrupt']); - - $mlpmaPath = Config::get('ponyfm.files_directory') . '/mlpma'; - $tmpPath = Config::get('ponyfm.files_directory') . '/tmp'; - - if (!File::exists($tmpPath)) { - File::makeDirectory($tmpPath); - } - - $UNKNOWN_GENRE = Genre::firstOrCreate([ - 'name' => 'Unknown', - 'slug' => 'unknown' - ]); - - $this->comment('Enumerating MLP Music Archive source files...'); - $files = File::allFiles($mlpmaPath); - $this->info(sizeof($files) . ' files found!'); - - $this->comment('Enumerating artists...'); - $artists = File::directories($mlpmaPath); - $this->info(sizeof($artists) . ' artists found!'); - - $this->comment('Importing tracks...'); - - $totalFiles = sizeof($files); - - $fileToStartAt = (int)$this->option('startAt') - 1; - $this->comment("Skipping $fileToStartAt files..." . PHP_EOL); - - $files = array_slice($files, $fileToStartAt); - $this->currentFile = $fileToStartAt; - - foreach ($files as $file) { - $this->currentFile++; - - pcntl_signal_dispatch(); - if ($this->isInterrupted) { - break; - } - - $this->comment('[' . $this->currentFile . '/' . $totalFiles . '] Importing track [' . $file->getFilename() . ']...'); - - if (in_array($file->getExtension(), $this->ignoredExtensions)) { - $this->comment('This is not an audio file! Skipping...' . PHP_EOL); - continue; - } - - - // Has this track already been imported? - $importedTrack = DB::table('mlpma_tracks') - ->where('filename', '=', $file->getFilename()) - ->first(); - - if ($importedTrack) { - $this->comment('This track has already been imported! Skipping...' . PHP_EOL); - continue; - } - - - //========================================================================================================== - // Extract the original tags. - //========================================================================================================== - $getId3 = new getID3; - - // all tags read by getID3, including the cover art - $allTags = $getId3->analyze($file->getPathname()); - - // tags specific to a file format (ID3 or Atom), pre-normalization but with cover art removed - $rawTags = []; - - // normalized tags used by Pony.fm - $parsedTags = []; - - if (Str::lower($file->getExtension()) === 'mp3') { - list($parsedTags, $rawTags) = $this->getId3Tags($allTags); - - } elseif (Str::lower($file->getExtension()) === 'm4a') { - list($parsedTags, $rawTags) = $this->getAtomTags($allTags); - - } elseif (Str::lower($file->getExtension()) === 'ogg') { - list($parsedTags, $rawTags) = $this->getVorbisTags($allTags); - - } elseif (Str::lower($file->getExtension()) === 'flac') { - list($parsedTags, $rawTags) = $this->getVorbisTags($allTags); - - } elseif (Str::lower($file->getExtension()) === 'wav') { - list($parsedTags, $rawTags) = $this->getAtomTags($allTags); - - } - - - //========================================================================================================== - // Determine the release date. - //========================================================================================================== - $modifiedDate = Carbon::createFromTimeStampUTC(File::lastModified($file->getPathname())); - $taggedYear = $parsedTags['year']; - - $this->info('Modification year: ' . $modifiedDate->year); - $this->info('Tagged year: ' . $taggedYear); - - if ($taggedYear !== null && $modifiedDate->year === $taggedYear) { - $releasedAt = $modifiedDate; - } elseif ($taggedYear !== null && Str::length((string)$taggedYear) !== 4) { - $this->error('This track\'s tagged year makes no sense! Using the track\'s last modified date...'); - $releasedAt = $modifiedDate; - } elseif ($taggedYear !== null && $modifiedDate->year !== $taggedYear) { - $this->error('Release years don\'t match! Using the tagged year...'); - $releasedAt = Carbon::create($taggedYear); - - } else { - // $taggedYear is null - $this->error('This track isn\'t tagged with its release year! Using the track\'s last modified date...'); - $releasedAt = $modifiedDate; - } - - // This is later used by the classification/publishing script to determine the publication date. - $parsedTags['released_at'] = $releasedAt->toDateTimeString(); - - //========================================================================================================== - // Does this track have vocals? - //========================================================================================================== - $isVocal = $parsedTags['lyrics'] !== null; - - - //========================================================================================================== - // Fill in the title tag if it's missing. - //========================================================================================================== - if (!$parsedTags['title']) { - $parsedTags['title'] = $file->getBasename('.' . $file->getExtension()); - } - - - //========================================================================================================== - // Determine the genre. - //========================================================================================================== - $genreName = $parsedTags['genre']; - $genreSlug = Str::slug($genreName); - $this->info('Genre: ' . $genreName); - - if ($genreName && $genreSlug !== '') { - $genre = Genre::where('name', '=', $genreName)->first(); - if ($genre) { - $genreId = $genre->id; - - } else { - $genre = new Genre(); - $genre->name = $genreName; - $genre->slug = $genreSlug; - $genre->save(); - $genreId = $genre->id; - $this->comment('Created a new genre!'); - } - - } else { - $genreId = $UNKNOWN_GENRE->id; // "Unknown" genre ID - } - - - //========================================================================================================== - // Determine which artist account this file belongs to using the containing directory. - //========================================================================================================== - $this->info('Path to file: ' . $file->getRelativePath()); - $path_components = explode(DIRECTORY_SEPARATOR, $file->getRelativePath()); - $artist_name = $path_components[0]; - $album_name = array_key_exists(1, $path_components) ? $path_components[1] : null; - - $this->info('Artist: ' . $artist_name); - $this->info('Album: ' . $album_name); - - $artist = User::where('display_name', '=', $artist_name)->first(); - - if (!$artist) { - $artist = new User; - $artist->display_name = $artist_name; - $artist->email = null; - $artist->is_archived = true; - - $artist->slug = Str::slug($artist_name); - - $slugExists = User::where('slug', '=', $artist->slug)->first(); - if ($slugExists) { - $this->error('Horsefeathers! The slug ' . $artist->slug . ' is already taken!'); - $artist->slug = $artist->slug . '-' . Str::random(4); - } - - $artist->save(); - } - - //========================================================================================================== - // Extract the cover art, if any exists. - //========================================================================================================== - - $this->comment('Extracting cover art!'); - $coverId = null; - if (array_key_exists('comments', $allTags) && array_key_exists('picture', $allTags['comments'])) { - $image = $allTags['comments']['picture'][0]; - - if ($image['image_mime'] === 'image/png') { - $extension = 'png'; - - } elseif ($image['image_mime'] === 'image/jpeg') { - $extension = 'jpg'; - - } elseif ($image['image_mime'] === 'image/gif') { - $extension = 'gif'; - - } else { - $this->error('Unknown cover art format!'); - } - - // write temporary image file - $imageFilename = $file->getFilename() . ".cover.$extension"; - $imageFilePath = "$tmpPath/" . $imageFilename; - File::put($imageFilePath, $image['data']); - - - $imageFile = new UploadedFile($imageFilePath, $imageFilename, $image['image_mime']); - - $cover = Image::upload($imageFile, $artist); - $coverId = $cover->id; - - } else { - $this->comment('No cover art found!'); - } - - - //========================================================================================================== - // Is this part of an album? - //========================================================================================================== - - $albumId = null; - $albumName = $parsedTags['album']; - - if ($albumName !== null) { - $album = Album::where('user_id', '=', $artist->id) - ->where('title', '=', $albumName) - ->first(); - - if (!$album) { - $album = new Album; - - $album->title = $albumName; - $album->user_id = $artist->id; - $album->cover_id = $coverId; - - $album->save(); - } - - $albumId = $album->id; - } - - - //========================================================================================================== - // Save this track. - //========================================================================================================== - // "Upload" the track to Pony.fm - $this->comment('Transcoding the track!'); - Auth::loginUsingId($artist->id); - - $trackFile = new UploadedFile($file->getPathname(), $file->getFilename(), $allTags['mime_type']); - Input::instance()->files->add(['track' => $trackFile]); - - $upload = new UploadTrackCommand(true, true); - $result = $upload->execute(); - - if ($result->didFail()) { - $this->error(json_encode($result->getMessages(), JSON_PRETTY_PRINT)); - - } else { - // Save metadata. - $track = Track::find($result->getResponse()['id']); - - $track->title = $parsedTags['title']; - $track->cover_id = $coverId; - $track->album_id = $albumId; - $track->genre_id = $genreId; - $track->track_number = $parsedTags['track_number']; - $track->released_at = $releasedAt; - $track->description = $parsedTags['comments']; - $track->is_downloadable = true; - $track->lyrics = $parsedTags['lyrics']; - $track->is_vocal = $isVocal; - $track->license_id = 2; - $track->save(); - - // If we made it to here, the track is intact! Log the import. - DB::table('mlpma_tracks') - ->insert([ - 'track_id' => $result->getResponse()['id'], - 'path' => $file->getRelativePath(), - 'filename' => $file->getFilename(), - 'extension' => $file->getExtension(), - 'imported_at' => Carbon::now(), - 'parsed_tags' => json_encode($parsedTags), - 'raw_tags' => json_encode($rawTags), - ]); - } - - echo PHP_EOL . PHP_EOL; - } - } - - /** - * @param array $rawTags - * @return array - */ - protected function getId3Tags($rawTags) - { - if (array_key_exists('tags', $rawTags) && array_key_exists('id3v2', $rawTags['tags'])) { - $tags = $rawTags['tags']['id3v2']; - } elseif (array_key_exists('tags', $rawTags) && array_key_exists('id3v1', $rawTags['tags'])) { - $tags = $rawTags['tags']['id3v1']; - } else { - $tags = []; - } - - - $comment = null; - - if (isset($tags['comment'])) { - // The "comment" tag comes in with a badly encoded string index - // so its array key has to be used implicitly. - $key = array_keys($tags['comment'])[0]; - - // The comment may have a null byte at the end. trim() removes it. - $comment = trim($tags['comment'][$key]); - - // Replace the malformed comment with the "fixed" one. - unset($tags['comment'][$key]); - $tags['comment'][0] = $comment; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => isset($tags['track_number']) ? $tags['track_number'][0] : null, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int)$tags['year'][0] : null, - 'comments' => $comment, - 'lyrics' => isset($tags['unsynchronised_lyric']) ? $tags['unsynchronised_lyric'][0] : null, - ], - $tags - ]; - } - - /** - * @param array $rawTags - * @return array - */ - protected function getAtomTags($rawTags) - { - if (array_key_exists('tags', $rawTags) && array_key_exists('quicktime', $rawTags['tags'])) { - $tags = $rawTags['tags']['quicktime']; - } else { - $tags = []; - } - - $trackNumber = null; - if (isset($tags['track_number'])) { - $trackNumberComponents = explode('/', $tags['track_number'][0]); - $trackNumber = $trackNumberComponents[0]; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => $trackNumber, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int)$tags['year'][0] : null, - 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, - 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, - ], - $tags - ]; - } - - /** - * @param array $rawTags - * @return array - */ - protected function getVorbisTags($rawTags) - { - if (array_key_exists('tags', $rawTags) && array_key_exists('vorbiscomment', $rawTags['tags'])) { - $tags = $rawTags['tags']['vorbiscomment']; - } else { - $tags = []; - } - - $trackNumber = null; - if (isset($tags['track_number'])) { - $trackNumberComponents = explode('/', $tags['track_number'][0]); - $trackNumber = $trackNumberComponents[0]; - } - - return [ - [ - 'title' => isset($tags['title']) ? $tags['title'][0] : null, - 'artist' => isset($tags['artist']) ? $tags['artist'][0] : null, - 'band' => isset($tags['band']) ? $tags['band'][0] : null, - 'album_artist' => isset($tags['album_artist']) ? $tags['album_artist'][0] : null, - 'genre' => isset($tags['genre']) ? $tags['genre'][0] : null, - 'track_number' => $trackNumber, - 'album' => isset($tags['album']) ? $tags['album'][0] : null, - 'year' => isset($tags['year']) ? (int)$tags['year'][0] : null, - 'comments' => isset($tags['comments']) ? $tags['comments'][0] : null, - 'lyrics' => isset($tags['lyrics']) ? $tags['lyrics'][0] : null, - ], - $tags - ]; - } -} diff --git a/app/Console/Commands/MergeAccounts.php b/app/Console/Commands/MergeAccounts.php new file mode 100644 index 00000000..1e3a12c6 --- /dev/null +++ b/app/Console/Commands/MergeAccounts.php @@ -0,0 +1,86 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use Carbon\Carbon; +use DB; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Poniverse\Ponyfm\Commands\MergeAccountsCommand; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Comment; +use Poniverse\Ponyfm\Models\Favourite; +use Poniverse\Ponyfm\Models\Follower; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\PinnedPlaylist; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\ResourceUser; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; + +class MergeAccounts extends Command +{ + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'accounts:merge + {sourceAccountId : ID of the source account (the one being disabled and having content transferred out of it)} + {destinationAccountId : ID of the destination account (the one gaining content)}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Merges two accounts'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $sourceAccountId = $this->argument('sourceAccountId'); + $destinationAccountId = $this->argument('destinationAccountId'); + + $sourceAccount = User::find($sourceAccountId); + $destinationAccount = User::find($destinationAccountId); + + $this->info("Merging {$sourceAccount->display_name} ({$sourceAccountId}) into {$destinationAccount->display_name} ({$destinationAccountId})..."); + + $command = new MergeAccountsCommand($sourceAccount, $destinationAccount); + $command->execute(); + } +} diff --git a/app/Console/Commands/MigrateOldData.php b/app/Console/Commands/MigrateOldData.php index a34d2ef8..166be24c 100644 --- a/app/Console/Commands/MigrateOldData.php +++ b/app/Console/Commands/MigrateOldData.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Console\Commands; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\ResourceLogItem; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\ResourceLogItem; use DB; use Exception; use Illuminate\Console\Command; diff --git a/app/Console/Commands/PublishUnclassifiedMlpmaTracks.php b/app/Console/Commands/PublishUnclassifiedMlpmaTracks.php deleted file mode 100644 index 215a354d..00000000 --- a/app/Console/Commands/PublishUnclassifiedMlpmaTracks.php +++ /dev/null @@ -1,72 +0,0 @@ -. - */ - -namespace Poniverse\Ponyfm\Console\Commands; - -use Carbon\Carbon; -use DB; -use Illuminate\Console\Command; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackType; - -class PublishUnclassifiedMlpmaTracks extends Command -{ - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'mlpma:declassify'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'This publishes all unpublished MLPMA tracks as the "unclassified" track type.'; - - /** - * Create a new command instance. - * - * @return void - */ - public function __construct() - { - parent::__construct(); - } - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() - { - $affectedTracks = Track::mlpma()-> - whereNull('published_at') - ->update([ - 'track_type_id' => TrackType::UNCLASSIFIED_TRACK, - 'published_at' => DB::raw('released_at'), - 'updated_at' => Carbon::now(), - ]); - - $this->info("Updated ${affectedTracks} tracks."); - } -} diff --git a/app/Console/Commands/RebuildArtists.php b/app/Console/Commands/RebuildArtists.php index 3c540711..3f9bf502 100644 --- a/app/Console/Commands/RebuildArtists.php +++ b/app/Console/Commands/RebuildArtists.php @@ -21,7 +21,7 @@ namespace Poniverse\Ponyfm\Console\Commands; use Illuminate\Console\Command; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\User; class RebuildArtists extends Command { @@ -56,12 +56,19 @@ class RebuildArtists extends Command */ public function handle() { + $numberOfUsers = User::count(); + + $bar = $this->output->createProgressBar($numberOfUsers); + foreach(User::with(['tracks' => function($query) { $query->published()->listed(); }])->get() as $user) { + $bar->advance(); $user->track_count = $user->tracks->count(); $user->save(); - $this->info('Updated user #'.$user->id.'!'); } + + $bar->finish(); + $this->line(''); } } diff --git a/app/Console/Commands/RebuildFilesizes.php b/app/Console/Commands/RebuildFilesizes.php index 8b0e99ae..e3236b9a 100644 --- a/app/Console/Commands/RebuildFilesizes.php +++ b/app/Console/Commands/RebuildFilesizes.php @@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Console\Commands; use File; use Illuminate\Console\Command; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\TrackFile; class RebuildFilesizes extends Command { diff --git a/app/Console/Commands/RebuildSearchIndex.php b/app/Console/Commands/RebuildSearchIndex.php new file mode 100644 index 00000000..41431e51 --- /dev/null +++ b/app/Console/Commands/RebuildSearchIndex.php @@ -0,0 +1,121 @@ +. + */ + +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!'); + } +} diff --git a/app/Console/Commands/RebuildTags.php b/app/Console/Commands/RebuildTags.php index efdd9588..366e12a5 100644 --- a/app/Console/Commands/RebuildTags.php +++ b/app/Console/Commands/RebuildTags.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Console\Commands; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Console\Command; class RebuildTags extends Command @@ -61,16 +61,18 @@ class RebuildTags extends Command $tracks = [$track]; } else { - $tracks = Track::whereNotNull('published_at')->orderBy('id', 'asc')->get(); + $tracks = Track::whereNotNull('published_at')->withTrashed()->orderBy('id', 'asc')->get(); } - $bar = $this->output->createProgressBar(sizeof($tracks)); + $numberOfTracks = sizeof($tracks); + + $this->info("Updating tags for ${numberOfTracks} tracks..."); + $bar = $this->output->createProgressBar($numberOfTracks); foreach($tracks as $track) { - $this->comment('Rewriting tags for track #'.$track->id.'...'); + /** @var $track Track */ $track->updateTags(); $bar->advance(); - $this->line(''); } $bar->finish(); diff --git a/app/Console/Commands/RebuildTrack.php b/app/Console/Commands/RebuildTrack.php new file mode 100644 index 00000000..f0725d02 --- /dev/null +++ b/app/Console/Commands/RebuildTrack.php @@ -0,0 +1,105 @@ +. + */ + +namespace Poniverse\Ponyfm\Console\Commands; + +use Illuminate\Console\Command; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Poniverse\Ponyfm\Commands\GenerateTrackFilesCommand; +use Poniverse\Ponyfm\Jobs\EncodeTrackFile; +use Poniverse\Ponyfm\Models\Track; + +class RebuildTrack extends Command +{ + use DispatchesJobs; + + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'rebuild:track + {trackId : ID of the track to rebuild} + {--upload : Include this option to use the uploaded file as the encode source instead of the master file}'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Re-encodes a track\'s files'; + + /** + * Create a new command instance. + * + * @return void + */ + public function __construct() + { + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + /** @var Track $track */ + $track = Track::with('trackFiles')->withTrashed()->find((int) $this->argument('trackId')); + $this->printTrackInfo($track); + + if($this->option('upload')) { + // The track would've been deleted if its original upload failed. + // It should be restored so the user can publish the track! + $track->restore(); + $this->info("Attempting to finish this track's upload..."); + + $sourceFile = new \SplFileInfo($track->getTemporarySourceFile()); + $generateTrackFiles = new GenerateTrackFilesCommand($track, $sourceFile, false); + $result = $generateTrackFiles->execute(); + // The GenerateTrackFiles command will re-encode all TrackFiles. + + if ($result->didFail()) { + $this->error("Something went wrong!"); + print_r($result->getMessages()); + } + + } else { + $this->info("Re-encoding this track's files - there should be a line of output for each format!"); + + foreach ($track->trackFiles as $trackFile) { + if (!$trackFile->is_master) { + $this->info("Re-encoding this track's {$trackFile->format} file..."); + $this->dispatch(new EncodeTrackFile($trackFile, true)); + } + } + } + } + + private function printTrackInfo(Track $track) { + $this->comment("Track info:"); + $this->comment(" Title: {$track->title}"); + $this->comment(" Uploaded at: {$track->created_at}"); + $this->comment(" Artist: {$track->user->display_name} [User ID: {$track->user_id}]"); + $this->comment(" Artist email: {$track->user->email}"); + } +} diff --git a/app/Console/Commands/RebuildTrackCache.php b/app/Console/Commands/RebuildTrackCache.php index 5b4386d1..67e4727d 100644 --- a/app/Console/Commands/RebuildTrackCache.php +++ b/app/Console/Commands/RebuildTrackCache.php @@ -24,8 +24,8 @@ use File; use Illuminate\Console\Command; use Illuminate\Foundation\Bus\DispatchesJobs; use Poniverse\Ponyfm\Jobs\EncodeTrackFile; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackFile; class RebuildTrackCache extends Command { diff --git a/app/Console/Commands/RefreshCache.php b/app/Console/Commands/RefreshCache.php index 54e0360f..19b985f6 100644 --- a/app/Console/Commands/RefreshCache.php +++ b/app/Console/Commands/RefreshCache.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Console\Commands; -use Poniverse\Ponyfm\ResourceLogItem; +use Poniverse\Ponyfm\Models\ResourceLogItem; use DB; use Illuminate\Console\Command; diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index ca89fb41..ade0d41f 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -33,17 +33,18 @@ class Kernel extends ConsoleKernel protected $commands = [ \Poniverse\Ponyfm\Console\Commands\MigrateOldData::class, \Poniverse\Ponyfm\Console\Commands\RefreshCache::class, - \Poniverse\Ponyfm\Console\Commands\ImportMLPMA::class, \Poniverse\Ponyfm\Console\Commands\ClassifyMLPMA::class, - \Poniverse\Ponyfm\Console\Commands\PublishUnclassifiedMlpmaTracks::class, \Poniverse\Ponyfm\Console\Commands\RebuildTags::class, \Poniverse\Ponyfm\Console\Commands\RebuildArtists::class, - \Poniverse\Ponyfm\Console\Commands\FixYearZeroLogs::class, \Poniverse\Ponyfm\Console\Commands\BootstrapLocalEnvironment::class, \Poniverse\Ponyfm\Console\Commands\PoniverseApiSetup::class, \Poniverse\Ponyfm\Console\Commands\ClearTrackCache::class, \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\MergeAccounts::class, + \Poniverse\Ponyfm\Console\Commands\FixMLPMAImages::class, ]; /** diff --git a/app/Contracts/Searchable.php b/app/Contracts/Searchable.php new file mode 100644 index 00000000..8b81b440 --- /dev/null +++ b/app/Contracts/Searchable.php @@ -0,0 +1,39 @@ +. + */ + +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(); +} diff --git a/app/Http/Controllers/AlbumsController.php b/app/Http/Controllers/AlbumsController.php index df372260..e2fbba09 100644 --- a/app/Http/Controllers/AlbumsController.php +++ b/app/Http/Controllers/AlbumsController.php @@ -22,9 +22,9 @@ namespace Poniverse\Ponyfm\Http\Controllers; use Poniverse\Ponyfm\AlbumDownloader; use App; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\ResourceLogItem; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Support\Facades\Redirect; use View; diff --git a/app/Http/Controllers/Api/Mobile/TracksController.php b/app/Http/Controllers/Api/Mobile/TracksController.php index 96a70fd0..4f1a96f6 100644 --- a/app/Http/Controllers/Api/Mobile/TracksController.php +++ b/app/Http/Controllers/Api/Mobile/TracksController.php @@ -21,7 +21,7 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Mobile; use Poniverse\Ponyfm\Http\Controllers\Controller; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Response; class TracksController extends Controller diff --git a/app/Http/Controllers/Api/V1/TracksController.php b/app/Http/Controllers/Api/V1/TracksController.php index 9c1bd4cb..aeba018b 100644 --- a/app/Http/Controllers/Api/V1/TracksController.php +++ b/app/Http/Controllers/Api/V1/TracksController.php @@ -22,8 +22,8 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\V1; use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\Track; use Response; class TracksController extends ApiControllerBase diff --git a/app/Http/Controllers/Api/Web/AccountController.php b/app/Http/Controllers/Api/Web/AccountController.php index 53739d9a..70173f57 100644 --- a/app/Http/Controllers/Api/Web/AccountController.php +++ b/app/Http/Controllers/Api/Web/AccountController.php @@ -34,6 +34,7 @@ class AccountController extends ApiControllerBase $user = Auth::user(); return Response::json([ + 'id' => $user->id, 'bio' => $user->bio, 'can_see_explicit_content' => $user->can_see_explicit_content == 1, 'display_name' => $user->display_name, diff --git a/app/Http/Controllers/Api/Web/AlbumsController.php b/app/Http/Controllers/Api/Web/AlbumsController.php index 530a1992..f5056802 100644 --- a/app/Http/Controllers/Api/Web/AlbumsController.php +++ b/app/Http/Controllers/Api/Web/AlbumsController.php @@ -21,19 +21,18 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Illuminate\Database\Eloquent\ModelNotFoundException; -use Illuminate\Support\Facades\File; -use Poniverse\Ponyfm\Album; +use Poniverse\Ponyfm\Models\Album; use Poniverse\Ponyfm\Commands\CreateAlbumCommand; use Poniverse\Ponyfm\Commands\DeleteAlbumCommand; use Poniverse\Ponyfm\Commands\EditAlbumCommand; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Jobs\EncodeTrackFile; -use Poniverse\Ponyfm\ResourceLogItem; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Input; -use Illuminate\Support\Facades\Response; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Auth; +use Input; +use Poniverse\Ponyfm\Models\User; +use Response; +use Poniverse\Ponyfm\Models\Track; class AlbumsController extends ApiControllerBase { @@ -142,10 +141,13 @@ class AlbumsController extends ApiControllerBase 200); } - public function getOwned() + public function getOwned(User $user) { - $query = Album::summary()->where('user_id', \Auth::user()->id)->orderBy('created_at', 'desc')->get(); + $this->authorize('get-albums', $user); + + $query = Album::summary()->where('user_id', $user->id)->orderBy('created_at', 'desc')->get(); $albums = []; + foreach ($query as $album) { $albums[] = [ 'id' => $album->id, diff --git a/app/Http/Controllers/Api/Web/ArtistsController.php b/app/Http/Controllers/Api/Web/ArtistsController.php index 3b27357d..652ff0eb 100644 --- a/app/Http/Controllers/Api/Web/ArtistsController.php +++ b/app/Http/Controllers/Api/Web/ArtistsController.php @@ -20,14 +20,14 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; -use Poniverse\Ponyfm\Album; -use Poniverse\Ponyfm\Comment; -use Poniverse\Ponyfm\Favourite; +use Gate; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Comment; +use Poniverse\Ponyfm\Models\Favourite; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\User; -use Cover; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Response; @@ -36,12 +36,13 @@ class ArtistsController extends ApiControllerBase { public function getFavourites($slug) { - $user = User::whereSlug($slug)->first(); + $user = User::where('slug', $slug)->whereNull('disabled_at')->first(); if (!$user) { App::abort(404); } - $favs = Favourite::whereUserId($user->id)->with([ + $favs = Favourite::where('user_id', $user->id) + ->with([ 'track.genre', 'track.cover', 'track.user', @@ -59,10 +60,10 @@ class ArtistsController extends ApiControllerBase $albums = []; foreach ($favs as $fav) { - if ($fav->type == 'Poniverse\Ponyfm\Track') { + if ($fav->type == 'Poniverse\Ponyfm\Models\Track') { $tracks[] = Track::mapPublicTrackSummary($fav->track); } else { - if ($fav->type == 'Poniverse\Ponyfm\Album') { + if ($fav->type == 'Poniverse\Ponyfm\Models\Album') { $albums[] = Album::mapPublicAlbumSummary($fav->album); } } @@ -76,7 +77,7 @@ class ArtistsController extends ApiControllerBase public function getContent($slug) { - $user = User::whereSlug($slug)->first(); + $user = User::where('slug', $slug)->whereNull('disabled_at')->first(); if (!$user) { App::abort(404); } @@ -111,7 +112,8 @@ class ArtistsController extends ApiControllerBase public function getShow($slug) { - $user = User::whereSlug($slug) + $user = User::where('slug', $slug) + ->whereNull('disabled_at') ->userDetails() ->with([ 'comments' => function ($query) { @@ -157,7 +159,7 @@ class ArtistsController extends ApiControllerBase return Response::json([ 'artist' => [ - 'id' => (int)$user->id, + 'id' => $user->id, 'name' => $user->display_name, 'slug' => $user->slug, 'is_archived' => (bool)$user->is_archived, @@ -173,7 +175,10 @@ class ArtistsController extends ApiControllerBase 'bio' => $user->bio, 'mlpforums_username' => $user->username, 'message_url' => $user->message_url, - 'user_data' => $userData + 'user_data' => $userData, + 'permissions' => [ + 'edit' => Gate::allows('edit', $user) + ] ] ], 200); } @@ -195,18 +200,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)], diff --git a/app/Http/Controllers/Api/Web/CommentsController.php b/app/Http/Controllers/Api/Web/CommentsController.php index 9bbe7a0b..efbe16ac 100644 --- a/app/Http/Controllers/Api/Web/CommentsController.php +++ b/app/Http/Controllers/Api/Web/CommentsController.php @@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use App; use Poniverse\Ponyfm\Commands\CreateCommentCommand; -use Poniverse\Ponyfm\Comment; +use Poniverse\Ponyfm\Models\Comment; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Response; diff --git a/app/Http/Controllers/Api/Web/DashboardController.php b/app/Http/Controllers/Api/Web/DashboardController.php index d90c1a6c..2ae700ac 100644 --- a/app/Http/Controllers/Api/Web/DashboardController.php +++ b/app/Http/Controllers/Api/Web/DashboardController.php @@ -21,7 +21,7 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Response; diff --git a/app/Http/Controllers/Api/Web/FavouritesController.php b/app/Http/Controllers/Api/Web/FavouritesController.php index da412d76..e1eb59f0 100644 --- a/app/Http/Controllers/Api/Web/FavouritesController.php +++ b/app/Http/Controllers/Api/Web/FavouritesController.php @@ -20,12 +20,12 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; -use Poniverse\Ponyfm\Album; +use Poniverse\Ponyfm\Models\Album; use Poniverse\Ponyfm\Commands\ToggleFavouriteCommand; -use Poniverse\Ponyfm\Favourite; +use Poniverse\Ponyfm\Models\Favourite; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Playlist; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Response; diff --git a/app/Http/Controllers/Api/Web/GenresController.php b/app/Http/Controllers/Api/Web/GenresController.php index 78710baa..0de4fb38 100644 --- a/app/Http/Controllers/Api/Web/GenresController.php +++ b/app/Http/Controllers/Api/Web/GenresController.php @@ -21,9 +21,10 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Input; +use Poniverse\Ponyfm\Commands\CreateGenreCommand; use Poniverse\Ponyfm\Commands\DeleteGenreCommand; use Poniverse\Ponyfm\Commands\RenameGenreCommand; -use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Response; @@ -45,6 +46,11 @@ class GenresController extends ApiControllerBase ], 200); } + public function postCreate() + { + $command = new CreateGenreCommand(Input::get('name')); + return $this->execute($command); + } public function putRename($genreId) { diff --git a/app/Http/Controllers/Api/Web/ImagesController.php b/app/Http/Controllers/Api/Web/ImagesController.php index 09687024..390da1a4 100644 --- a/app/Http/Controllers/Api/Web/ImagesController.php +++ b/app/Http/Controllers/Api/Web/ImagesController.php @@ -20,17 +20,21 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; +use Auth; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Image; -use Cover; -use Illuminate\Support\Facades\Response; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\User; +use Response; class ImagesController extends ApiControllerBase { - public function getOwned() + public function getOwned(User $user) { - $query = Image::where('uploaded_by', \Auth::user()->id); + $this->authorize('get-images', $user); + + $query = Image::where('uploaded_by', $user->id); $images = []; + foreach ($query->get() as $image) { $images[] = [ 'id' => $image->id, diff --git a/app/Http/Controllers/Api/Web/PlaylistsController.php b/app/Http/Controllers/Api/Web/PlaylistsController.php index baffd566..b86f288e 100644 --- a/app/Http/Controllers/Api/Web/PlaylistsController.php +++ b/app/Http/Controllers/Api/Web/PlaylistsController.php @@ -27,13 +27,13 @@ use Poniverse\Ponyfm\Commands\DeletePlaylistCommand; use Poniverse\Ponyfm\Commands\EditPlaylistCommand; use Poniverse\Ponyfm\Commands\RemoveTrackFromPlaylistCommand; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\Image; -use Poniverse\Ponyfm\Playlist; -use Poniverse\Ponyfm\ResourceLogItem; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Input; -use Illuminate\Support\Facades\Response; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Image; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Auth; +use Input; +use Response; +use Poniverse\Ponyfm\Models\Track; class PlaylistsController extends ApiControllerBase { @@ -175,8 +175,12 @@ class PlaylistsController extends ApiControllerBase public function getOwned() { - $query = Playlist::summary()->with('pins', 'tracks', 'tracks.cover')->where('user_id', - \Auth::user()->id)->orderBy('title', 'asc')->get(); + $query = Playlist::summary() + ->with('pins', 'tracks', 'tracks.cover') + ->where('user_id', Auth::user()->id) + ->orderBy('title', 'asc') + ->get(); + $playlists = []; foreach ($query as $playlist) { $playlists[] = [ @@ -191,7 +195,8 @@ class PlaylistsController extends ApiControllerBase 'normal' => $playlist->getCoverUrl(Image::NORMAL) ], 'is_pinned' => $playlist->hasPinFor(Auth::user()->id), - 'is_public' => $playlist->is_public == 1 + 'is_public' => $playlist->is_public == 1, + 'track_ids' => $playlist->tracks->pluck('id') ]; } diff --git a/app/Http/Controllers/Api/Web/ProfilerController.php b/app/Http/Controllers/Api/Web/SearchController.php similarity index 59% rename from app/Http/Controllers/Api/Web/ProfilerController.php rename to app/Http/Controllers/Api/Web/SearchController.php index 1e420549..f2304bf0 100644 --- a/app/Http/Controllers/Api/Web/ProfilerController.php +++ b/app/Http/Controllers/Api/Web/SearchController.php @@ -2,7 +2,7 @@ /** * Pony.fm - A community for pony fan music. - * Copyright (C) 2015 Peter Deltchev + * 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 @@ -20,28 +20,20 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; -use Poniverse\Ponyfm\Http\Controllers\Controller; -use Poniverse\Ponyfm\ProfileRequest; -use Cache; -use Config; +use Elasticsearch; +use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; +use Input; +use Poniverse\Ponyfm\Library\Search; use Response; -class ProfilerController extends Controller +class SearchController extends ApiControllerBase { - public function getRequest($id) + public function getSearch(Search $search) { - if (!Config::get('app.debug')) { - return; - } + $results = $search->searchAllContent(Input::query('query')); - $key = 'profiler-request-' . $id; - $request = Cache::get($key); - if (!$request) { - exit(); - } - - Cache::forget($key); - - return Response::json(['request' => ProfileRequest::load($request)->toArray()], 200); + return Response::json([ + 'results' => $results, + ], 200); } } diff --git a/app/Http/Controllers/Api/Web/TaxonomiesController.php b/app/Http/Controllers/Api/Web/TaxonomiesController.php index a2107bf7..7c6de020 100644 --- a/app/Http/Controllers/Api/Web/TaxonomiesController.php +++ b/app/Http/Controllers/Api/Web/TaxonomiesController.php @@ -20,11 +20,11 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; -use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; -use Poniverse\Ponyfm\License; -use Poniverse\Ponyfm\ShowSong; -use Poniverse\Ponyfm\TrackType; +use Poniverse\Ponyfm\Models\License; +use Poniverse\Ponyfm\Models\ShowSong; +use Poniverse\Ponyfm\Models\TrackType; use Illuminate\Support\Facades\DB; class TaxonomiesController extends ApiControllerBase @@ -33,8 +33,10 @@ class TaxonomiesController extends ApiControllerBase { return \Response::json([ 'licenses' => License::all()->toArray(), - 'genres' => Genre::select('genres.*', - DB::raw('(SELECT COUNT(id) FROM tracks WHERE tracks.genre_id = genres.id AND tracks.published_at IS NOT NULL) AS track_count'))->orderBy('name')->get()->toArray(), + 'genres' => Genre::with('trackCountRelation') + ->orderBy('name') + ->get() + ->toArray(), 'track_types' => TrackType::select('track_types.*', DB::raw('(SELECT COUNT(id) FROM tracks WHERE tracks.track_type_id = track_types.id AND tracks.published_at IS NOT NULL) AS track_count')) ->where('id', '!=', TrackType::UNCLASSIFIED_TRACK) diff --git a/app/Http/Controllers/Api/Web/TracksController.php b/app/Http/Controllers/Api/Web/TracksController.php index 78e27950..29ac29ff 100644 --- a/app/Http/Controllers/Api/Web/TracksController.php +++ b/app/Http/Controllers/Api/Web/TracksController.php @@ -22,15 +22,14 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; use Illuminate\Database\Eloquent\ModelNotFoundException; use File; -use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Commands\DeleteTrackCommand; use Poniverse\Ponyfm\Commands\EditTrackCommand; use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Jobs\EncodeTrackFile; -use Poniverse\Ponyfm\ResourceLogItem; -use Poniverse\Ponyfm\TrackFile; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\TrackFile; +use Poniverse\Ponyfm\Models\Track; use Auth; use Input; use Response; @@ -41,7 +40,7 @@ class TracksController extends ApiControllerBase { session_write_close(); - return $this->execute(new UploadTrackCommand()); + return $this->execute(new UploadTrackCommand(true)); } public function getUploadStatus($trackId) @@ -184,9 +183,7 @@ class TracksController extends ApiControllerBase return $this->notFound('Track ' . $id . ' not found!'); } - if ($track->user_id != Auth::user()->id) { - return $this->notAuthorized(); - } + $this->authorize('edit', $track); return Response::json(Track::mapPrivateTrackShow($track), 200); } @@ -225,6 +222,9 @@ class TracksController extends ApiControllerBase } if (Input::has('songs')) { + // DISTINCT is needed here to avoid duplicate results + // when a track is associated with multiple show songs. + $query->distinct(); $query->join('show_song_track', function ($join) { $join->on('tracks.id', '=', 'show_song_track.track_id'); }); diff --git a/app/Http/Controllers/ArtistsController.php b/app/Http/Controllers/ArtistsController.php index 9e7a4c7a..17a2754e 100644 --- a/app/Http/Controllers/ArtistsController.php +++ b/app/Http/Controllers/ArtistsController.php @@ -21,7 +21,7 @@ namespace Poniverse\Ponyfm\Http\Controllers; use App; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\User; use View; use Redirect; @@ -32,9 +32,17 @@ class ArtistsController extends Controller return View::make('artists.index'); } + public function getFavourites($slug) { + return $this->getProfile($slug); + } + + public function getContent($slug) { + return $this->getProfile($slug); + } + public function getProfile($slug) { - $user = User::whereSlug($slug)->first(); + $user = User::whereSlug($slug)->whereNull('disabled_at')->first(); if (!$user) { App::abort('404'); } @@ -45,10 +53,10 @@ class ArtistsController extends Controller public function getShortlink($id) { $user = User::find($id); - if (!$user) { + if (!$user || $user->disabled_at !== NULL) { App::abort('404'); } - return Redirect::action('ArtistsController@getProfile', [$id]); + return Redirect::action('ArtistsController@getProfile', [$user->slug]); } } diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 13c1f8ee..a1c4dd2f 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Http\Controllers; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\User; use Auth; use Config; use DB; @@ -80,7 +80,7 @@ class AuthController extends Controller $setData = [ 'access_token' => $code['result']['access_token'], - 'expires' => date('Y-m-d H:i:s', strtotime("+" . $code['result']['expires_in'] . " Seconds", time())), + 'expires' => date('Y-m-d H:i:s', strtotime("+".$code['result']['expires_in']." Seconds", time())), 'type' => $code['result']['token_type'], ]; diff --git a/app/Http/Controllers/ImagesController.php b/app/Http/Controllers/ImagesController.php index 15f8fc3f..7801c878 100644 --- a/app/Http/Controllers/ImagesController.php +++ b/app/Http/Controllers/ImagesController.php @@ -20,7 +20,7 @@ namespace Poniverse\Ponyfm\Http\Controllers; -use Poniverse\Ponyfm\Image; +use Poniverse\Ponyfm\Models\Image; use Config; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Redirect; diff --git a/app/Http/Controllers/PlaylistsController.php b/app/Http/Controllers/PlaylistsController.php index 92655ae2..f0ea6a82 100644 --- a/app/Http/Controllers/PlaylistsController.php +++ b/app/Http/Controllers/PlaylistsController.php @@ -21,9 +21,9 @@ namespace Poniverse\Ponyfm\Http\Controllers; use App; -use Poniverse\Ponyfm\Playlist; -use Poniverse\Ponyfm\ResourceLogItem; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Playlist; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\PlaylistDownloader; use Auth; use Illuminate\Support\Facades\Redirect; diff --git a/app/Http/Controllers/TracksController.php b/app/Http/Controllers/TracksController.php index f56c5450..866478a7 100644 --- a/app/Http/Controllers/TracksController.php +++ b/app/Http/Controllers/TracksController.php @@ -20,9 +20,9 @@ namespace Poniverse\Ponyfm\Http\Controllers; -use Poniverse\Ponyfm\ResourceLogItem; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackFile; use Auth; use Config; use Illuminate\Support\Facades\App; @@ -91,6 +91,11 @@ class TracksController extends Controller return View::make('tracks.show'); } + public function getEdit($id, $slug) + { + return $this->getTrack($id, $slug); + } + public function getShortlink($id) { $track = Track::find($id); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 9c51a069..fcb5d192 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,7 +36,7 @@ class Kernel extends HttpKernel \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Poniverse\Ponyfm\Http\Middleware\VerifyCsrfToken::class, - \Poniverse\Ponyfm\Http\Middleware\Profiler::class, + \Poniverse\Ponyfm\Http\Middleware\DisabledAccountCheck::class, ]; /** diff --git a/app/Http/Middleware/AuthenticateOAuth.php b/app/Http/Middleware/AuthenticateOAuth.php index bd3c75a4..aecde626 100644 --- a/app/Http/Middleware/AuthenticateOAuth.php +++ b/app/Http/Middleware/AuthenticateOAuth.php @@ -20,11 +20,13 @@ namespace Poniverse\Ponyfm\Http\Middleware; -use Auth; use Closure; use GuzzleHttp; +use Illuminate\Auth\Guard; +use Illuminate\Http\Request; +use Illuminate\Session\Store; use Poniverse; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\User; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AuthenticateOAuth @@ -34,8 +36,20 @@ class AuthenticateOAuth */ private $poniverse; - public function __construct(Poniverse $poniverse) { + /** + * @var Guard + */ + private $auth; + + /** + * @var Store + */ + private $session; + + public function __construct(Poniverse $poniverse, Guard $auth, Store $session) { $this->poniverse = $poniverse; + $this->auth = $auth; + $this->session = $session; } /** @@ -47,10 +61,10 @@ class AuthenticateOAuth * @return mixed * @throws \OAuth2\Exception */ - public function handle($request, Closure $next, $requiredScope) + public function handle(Request $request, Closure $next, $requiredScope) { // Ensure this is a valid OAuth client. - $accessToken = $request->get('access_token'); + $accessToken = $this->determineAccessToken($request, false); // check that access token is valid at Poniverse.net $accessTokenInfo = $this->poniverse->getAccessTokenInfo($accessToken); @@ -65,13 +79,29 @@ class AuthenticateOAuth // Log in as the given user, creating the account if necessary. $this->poniverse->setAccessToken($accessToken); - session()->put('api_client_id', $accessTokenInfo->getClientId()); + $this->session->put('api_client_id', $accessTokenInfo->getClientId()); $poniverseUser = $this->poniverse->getUser(); $user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']); - Auth::login($user); + $this->auth->onceUsingId($user); return $next($request); } + + + private function determineAccessToken(Request $request, $headerOnly = true) + { + $header = $request->header('Authorization'); + + if ($header !== null && substr($header, 0, 7) === 'Bearer ') { + return trim(substr($header, 7)); + } + + if ($headerOnly) { + return null; + } + + return $request->get('access_token'); + } } diff --git a/app/Http/Middleware/DisabledAccountCheck.php b/app/Http/Middleware/DisabledAccountCheck.php new file mode 100644 index 00000000..4e834134 --- /dev/null +++ b/app/Http/Middleware/DisabledAccountCheck.php @@ -0,0 +1,68 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Middleware; + +use Closure; +use Illuminate\Contracts\Auth\Guard; + +class DisabledAccountCheck +{ + /** + * The Guard implementation. + * + * @var Guard + */ + protected $auth; + + /** + * Create a new filter instance. + * + * @param Guard $auth + */ + public function __construct(Guard $auth) { + $this->auth = $auth; + } + + + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + // TODO: don't automatically log the user out some time after + // issue #29 is fixed or when disabled_at starts being used for + // something other than merged accounts. + if ($this->auth->check() + && $this->auth->user()->disabled_at !== null + && !($request->getMethod() === 'POST' && $request->getRequestUri() == '/auth/logout') + ) { + $this->auth->logout(); +// return Response::view('home.account-disabled', ['username' => $this->auth->user()->username], 403); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Profiler.php b/app/Http/Middleware/Profiler.php deleted file mode 100644 index e5c8f66d..00000000 --- a/app/Http/Middleware/Profiler.php +++ /dev/null @@ -1,80 +0,0 @@ -. - */ - -namespace Poniverse\Ponyfm\Http\Middleware; - -use App; -use Closure; -use Cache; -use Config; -use DB; -use Log; -use Poniverse\Ponyfm\ProfileRequest; -use Symfony\Component\HttpFoundation\Response; - -class Profiler -{ - /** - * Handle an incoming request. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - */ - public function handle($request, Closure $next) - { - // Profiling magic time! - if (Config::get('app.debug')) { - DB::enableQueryLog(); - $profiler = ProfileRequest::create(); - - try { - $response = $next($request); - } catch (\Exception $e) { - $response = \Response::make([ - 'message' => $e->getMessage(), - 'lineNumber' => $e->getLine(), - 'exception' => $e->getTrace() - ], method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 500); - $profiler->log('error', $e->__toString(), []); - } - - $response = $this->processResponse($profiler, $response); - - Log::listen(function ($level, $message, $context) use ($profiler, $request) { - $profiler->log($level, $message, $context); - }); - - } else { - // Process the request the usual, boring way. - $response = $next($request); - } - - return $response; - } - - - protected function processResponse(ProfileRequest $profiler, Response $response) { - $profiler->recordQueries(); - - Cache::put('profiler-request-' . $profiler->getId(), $profiler->toString(), 2); - return $response->header('X-Request-Id', $profiler->getId()); - } -} diff --git a/app/Http/routes.php b/app/Http/routes.php index 0ab6299d..fb907ea2 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -29,16 +29,13 @@ | */ -if (Config::get('app.debug')) { - Route::get('/api/web/profiler/{id}', 'Api\Web\ProfilerController@getRequest'); -} - Route::get('/dashboard', 'TracksController@getIndex'); Route::get('/tracks', ['as' => 'tracks.discover', 'uses' => 'TracksController@getIndex']); Route::get('/tracks/popular', 'TracksController@getIndex'); Route::get('/tracks/random', 'TracksController@getIndex'); Route::get('tracks/{id}-{slug}', 'TracksController@getTrack'); +Route::get('tracks/{id}-{slug}/edit', 'TracksController@getEdit'); Route::get('t{id}', 'TracksController@getShortlink' )->where('id', '\d+'); Route::get('t{id}/embed', 'TracksController@getEmbed' ); Route::get('t{id}/stream.{extension}', 'TracksController@getStream' ); @@ -54,6 +51,7 @@ Route::get('playlists', 'PlaylistsController@getIndex'); Route::get('/register', 'AccountController@getRegister'); Route::get('/login', 'AuthController@getLogin'); +Route::post('/auth/logout', 'AuthController@postLogout'); Route::get('/auth/oauth', 'AuthController@getOAuth'); Route::get('/about', function() { return View::make('pages.about'); }); @@ -81,8 +79,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', 'Api\Web\SearchController@getSearch'); Route::get('/tracks', 'Api\Web\TracksController@getIndex'); Route::get('/tracks/{id}', 'Api\Web\TracksController@getShow')->where('id', '\d+'); @@ -93,6 +90,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' => '.+']); @@ -135,12 +133,12 @@ Route::group(['prefix' => 'api/web'], function() { Route::group(['middleware' => 'auth'], function() { Route::get('/account/settings', 'Api\Web\AccountController@getSettings'); - Route::get('/images/owned', 'Api\Web\ImagesController@getOwned'); - Route::get('/tracks/owned', 'Api\Web\TracksController@getOwned'); Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit'); - Route::get('/albums/owned', 'Api\Web\AlbumsController@getOwned'); + Route::get('/users/{userId}/albums', 'Api\Web\AlbumsController@getOwned')->where('id', '\d+'); + Route::get('/users/{userId}/images', 'Api\Web\ImagesController@getOwned')->where('id', '\d+'); + Route::get('/albums/edit/{id}', 'Api\Web\AlbumsController@getEdit'); Route::get('/playlists/owned', 'Api\Web\PlaylistsController@getOwned'); @@ -153,6 +151,7 @@ Route::group(['prefix' => 'api/web'], function() { Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() { Route::get('/genres', 'Api\Web\GenresController@getIndex'); + Route::post('/genres', 'Api\Web\GenresController@postCreate'); Route::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+'); Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+'); }); @@ -160,22 +159,6 @@ Route::group(['prefix' => 'api/web'], function() { Route::post('/auth/logout', 'Api\Web\AuthController@postLogout'); }); -Route::group(['prefix' => 'account', 'middleware' => 'auth'], function() { - Route::get('/favourites/tracks', 'FavouritesController@getTracks'); - Route::get('/favourites/albums', 'FavouritesController@getAlbums'); - Route::get('/favourites/playlists', 'FavouritesController@getPlaylists'); - - Route::get('/tracks', 'ContentController@getTracks'); - Route::get('/tracks/edit/{id}', 'ContentController@getTracks'); - Route::get('/albums', 'ContentController@getAlbums'); - Route::get('/albums/edit/{id}', 'ContentController@getAlbums'); - Route::get('/albums/create', 'ContentController@getAlbums'); - Route::get('/playlists', 'ContentController@getPlaylists'); - - Route::get('/uploader', 'UploaderController@getIndex'); - - Route::get('/', 'AccountController@getIndex'); -}); Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() { Route::get('/genres', 'AdminController@getGenres'); @@ -184,9 +167,27 @@ Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-ar Route::get('u{id}', 'ArtistsController@getShortlink')->where('id', '\d+'); Route::get('users/{id}-{slug}', 'ArtistsController@getShortlink')->where('id', '\d+'); -Route::get('{slug}', 'ArtistsController@getProfile'); -Route::get('{slug}/content', 'ArtistsController@getProfile'); -Route::get('{slug}/favourites', 'ArtistsController@getProfile'); + + +Route::group(['prefix' => '{slug}'], function() { + Route::get('/', 'ArtistsController@getProfile'); + Route::get('/content', 'ArtistsController@getContent'); + Route::get('/favourites', 'ArtistsController@getFavourites'); + + + Route::group(['prefix' => 'account', 'middleware' => 'auth'], function() { + Route::get('/tracks', 'ContentController@getTracks'); + Route::get('/tracks/edit/{id}', 'ContentController@getTracks'); + Route::get('/albums', 'ContentController@getAlbums'); + Route::get('/albums/edit/{id}', 'ContentController@getAlbums'); + Route::get('/albums/create', 'ContentController@getAlbums'); + Route::get('/playlists', 'ContentController@getPlaylists'); + + Route::get('/uploader', 'UploaderController@getIndex'); + + Route::get('/', 'AccountController@getIndex'); + }); +}); Route::get('/', 'HomeController@getIndex'); diff --git a/app/Jobs/DeleteGenre.php b/app/Jobs/DeleteGenre.php index 5a518f1b..4c633ec2 100644 --- a/app/Jobs/DeleteGenre.php +++ b/app/Jobs/DeleteGenre.php @@ -21,11 +21,12 @@ namespace Poniverse\Ponyfm\Jobs; use Auth; -use Poniverse\Ponyfm\Genre; +use DB; +use Poniverse\Ponyfm\Models\Genre; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Queue\ShouldQueue; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use SerializesModels; class DeleteGenre extends Job implements SelfHandling, ShouldQueue @@ -60,6 +61,8 @@ class DeleteGenre extends Job implements SelfHandling, ShouldQueue */ public function handle() { + $this->beforeHandle(); + // The user who kicked off this job is used when generating revision log entries. Auth::login($this->executingUser); diff --git a/app/Jobs/EncodeTrackFile.php b/app/Jobs/EncodeTrackFile.php index a54b68e7..9bdfb564 100644 --- a/app/Jobs/EncodeTrackFile.php +++ b/app/Jobs/EncodeTrackFile.php @@ -24,17 +24,15 @@ namespace Poniverse\Ponyfm\Jobs; use Carbon\Carbon; use DB; use File; -use Illuminate\Support\Facades\Config; -use Illuminate\Support\Facades\Log; -use OAuth2\Exception; +use Config; +use Log; use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; -use Poniverse\Ponyfm\Jobs\Job; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Queue\ShouldQueue; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\TrackFile; use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Process; @@ -44,19 +42,19 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue /** * @var TrackFile */ - private $trackFile; + protected $trackFile; /** * @var */ - private $isExpirable; + protected $isExpirable; /** * @var bool */ - private $isForUpload; + protected $isForUpload; /** * @var bool */ - private $autoPublishWhenComplete; + protected $autoPublishWhenComplete; /** * Create a new job instance. @@ -87,9 +85,21 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue */ public function handle() { + $this->beforeHandle(); + + // Sanity check: was this file just generated, or is it already being processed? + if ($this->trackFile->status === TrackFile::STATUS_PROCESSING) { + Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is already being processed!'); + return; + + } elseif (!$this->trackFile->is_expired) { + Log::warning('Track file #'.$this->trackFile->id.' (track #'.$this->trackFile->track_id.') is still valid! No need to re-encode it.'); + return; + } + // Start the job $this->trackFile->status = TrackFile::STATUS_PROCESSING; - $this->trackFile->update(); + $this->trackFile->save(); // Use the track's master file as the source if ($this->isForUpload) { @@ -120,19 +130,18 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue $process->mustRun(); } catch (ProcessFailedException $e) { Log::error('An exception occured in the encoding process for track file ' . $this->trackFile->id . ' - ' . $e->getMessage()); + Log::info($process->getOutput()); // Ensure queue fails throw $e; - } finally { - Log::info($process->getOutput()); } // Update the tags of the track $this->trackFile->track->updateTags($this->trackFile->format); // Insert the expiration time for cached tracks - if ($this->isExpirable) { + if ($this->isExpirable && $this->trackFile->is_cacheable) { $this->trackFile->expires_at = Carbon::now()->addMinutes(Config::get('ponyfm.track_file_cache_duration')); - $this->trackFile->update(); + $this->trackFile->save(); } // Update file size @@ -140,7 +149,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue // Complete the job $this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED; - $this->trackFile->update(); + $this->trackFile->save(); if ($this->isForUpload) { if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) { @@ -171,6 +180,6 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue { $this->trackFile->status = TrackFile::STATUS_PROCESSING_ERROR; $this->trackFile->expires_at = null; - $this->trackFile->update(); + $this->trackFile->save(); } } diff --git a/app/Jobs/Job.php b/app/Jobs/Job.php index b9a37582..4b215d15 100644 --- a/app/Jobs/Job.php +++ b/app/Jobs/Job.php @@ -20,6 +20,8 @@ namespace Poniverse\Ponyfm\Jobs; +use App; +use DB; use Illuminate\Bus\Queueable; abstract class Job @@ -36,4 +38,15 @@ abstract class Job */ use Queueable; + + /** + * This method should be called at the beginning of every job's handle() + * method. It ensures that we don't lose the in-memory database during + * testing by disconnecting from it - which causes tests to fail. + */ + protected function beforeHandle() { + if (App::environment() !== 'testing') { + DB::reconnect(); + } + } } diff --git a/app/Jobs/UpdateSearchIndexForEntity.php b/app/Jobs/UpdateSearchIndexForEntity.php new file mode 100644 index 00000000..1e2ca678 --- /dev/null +++ b/app/Jobs/UpdateSearchIndexForEntity.php @@ -0,0 +1,58 @@ +. + */ + +namespace Poniverse\Ponyfm\Jobs; + +use DB; +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->beforeHandle(); + $this->entity->updateElasticsearchEntrySynchronously(); + } +} diff --git a/app/Jobs/UpdateTagsForRenamedGenre.php b/app/Jobs/UpdateTagsForRenamedGenre.php new file mode 100644 index 00000000..ab948e1f --- /dev/null +++ b/app/Jobs/UpdateTagsForRenamedGenre.php @@ -0,0 +1,100 @@ +. + */ + +namespace Poniverse\Ponyfm\Jobs; + +use Auth; +use Cache; +use DB; +use Log; +use Poniverse\Ponyfm\Models\Genre; +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 UpdateTagsForRenamedGenre extends Job implements SelfHandling, ShouldQueue +{ + use InteractsWithQueue, SerializesModels; + + protected $executingUser; + protected $genreThatWasRenamed; + protected $lockKey; + + /** + * Create a new job instance. + * + * @param Genre $genreThatWasRenamed + */ + public function __construct(Genre $genreThatWasRenamed) + { + $this->executingUser = Auth::user(); + $this->genreThatWasRenamed = $genreThatWasRenamed; + + $this->lockKey = "genre-{$this->genreThatWasRenamed->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->genreThatWasRenamed->name}\" genre are currently in progress! Will try again in 30 seconds."); + $this->release(30); + return; + + } else { + Cache::forever($this->lockKey, true); + } + + + $this->genreThatWasRenamed->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/Library/Assets.php b/app/Library/Assets.php index 973a3a09..5c3c1404 100644 --- a/app/Library/Assets.php +++ b/app/Library/Assets.php @@ -20,27 +20,38 @@ class Assets { - public static function scriptIncludes($area = 'app') - { - if (!Config::get("app.debug")) { - return ''; + public static function scriptIncludes(string $area) { + $scriptTags = ''; + + if ('app' === $area) { + $scripts = ['app.js', 'templates.js']; + } elseif ('embed' === $area) { + $scripts = ['embed.js']; + } else { + throw new InvalidArgumentException('A valid app area must be specified!'); } - $scripts = self::mergeGlobs(self::getScriptsForArea($area)); - $retVal = ""; - - foreach ($scripts as $script) { - $filename = self::replaceExtensionWith($script, ".coffee", ".js"); - $retVal .= ""; + foreach ($scripts as $filename) { + if (Config::get('app.debug') && $filename !== 'templates.js') { + $scriptTags .= ""; + } else { + $scriptTags .= ""; + } } - return $retVal; + if (Config::get('app.debug')) { + $scriptTags .= ''; + } + + return $scriptTags; } public static function styleIncludes($area = 'app') { if (!Config::get("app.debug")) { - return ''; + return ''; } $styles = self::mergeGlobs(self::getStylesForArea($area)); @@ -48,7 +59,7 @@ class Assets foreach ($styles as $style) { $filename = self::replaceExtensionWith($style, ".less", ".css"); - $retVal .= ""; + $retVal .= ""; } return $retVal; @@ -82,41 +93,6 @@ class Assets return $files; } - private static function getScriptsForArea($area) - { - if ($area == 'app') { - return [ - "scripts/base/jquery-2.0.2.js", - "scripts/base/angular.js", - "scripts/base/marked.js", - "scripts/base/*.{coffee,js}", - "scripts/shared/*.{coffee,js}", - "scripts/app/*.{coffee,js}", - "scripts/app/services/*.{coffee,js}", - "scripts/app/filters/*.{coffee,js}", - "scripts/app/directives/*.{coffee,js}", - "scripts/app/controllers/*.{coffee,js}", - "scripts/**/*.{coffee,js}" - ]; - } else { - if ($area == 'embed') { - return [ - "scripts/base/jquery-2.0.2.js", - "scripts/base/jquery.cookie.js", - "scripts/base/jquery.viewport.js", - "scripts/base/underscore.js", - "scripts/base/moment.js", - "scripts/base/jquery.timeago.js", - "scripts/base/soundmanager2-nodebug.js", - "scripts/shared/jquery-extensions.js", - "scripts/embed/*.coffee" - ]; - } - } - - throw new Exception(); - } - private static function getStylesForArea($area) { if ($area == 'app') { @@ -124,7 +100,6 @@ class Assets "styles/base/jquery-ui.css", "styles/base/colorbox.css", "styles/app.less", - "styles/profiler.less" ]; } else { if ($area == 'embed') { diff --git a/app/Library/AudioCache.php b/app/Library/AudioCache.php index 23474848..aa1440d3 100644 --- a/app/Library/AudioCache.php +++ b/app/Library/AudioCache.php @@ -22,11 +22,7 @@ class AudioCache { private static $_movieCache = array(); - /** - * @param $filename - * @return FFmpegMovie - */ - public static function get($filename) + public static function get(string $filename):FFmpegMovie { if (isset(self::$_movieCache[$filename])) { return self::$_movieCache[$filename]; diff --git a/app/Library/CacheBusterAsset.php b/app/Library/CacheBusterAsset.php deleted file mode 100644 index 707088cd..00000000 --- a/app/Library/CacheBusterAsset.php +++ /dev/null @@ -1,55 +0,0 @@ -. - */ - -use Assetic\Asset\BaseAsset; -use Assetic\Filter\FilterInterface; - -/** - * Class CacheBusterAsset - * OH GOD IT BUUUUUUURNS - * - * Well, I may as well tell you why this awful class exists. So... Assetic doesn't quite support less's import - * directive. I mean; it supports it insofar as Less itself supports it - but it doesn't take into account the - * last modified time for imported assets. Since we only have one less file that imports everything else... well - * you can see where this is going. This asset will let us override the last modified time for an entire collection - * which allows me to write a custom mechanism for cache busting. - */ -class CacheBusterAsset extends BaseAsset -{ - private $_lastModified; - - /** - * @param int $lastModified - */ - public function __construct($lastModified) - { - $this->_lastModified = $lastModified; - parent::__construct([], '', '', []); - } - - public function load(FilterInterface $additionalFilter = null) - { - } - - public function getLastModified() - { - return $this->_lastModified; - } -} diff --git a/app/Library/External.php b/app/Library/External.php index a866f31c..25aaeea6 100644 --- a/app/Library/External.php +++ b/app/Library/External.php @@ -18,17 +18,17 @@ * along with this program. If not, see . */ -use Illuminate\Support\Facades\Log; +use Symfony\Component\Process\Process; class External { public static function execute($command) { - $output = []; - $error = exec($command, $output); + $process = new Process($command); + $process->run(); - if ($error != null) { - Log::error('"' . $command . '" failed with "' . $error . '"'); + if (!$process->isSuccessful()) { + Log::error('"' . $command . '" failed with "' . $process->getErrorOutput() . '"'); } } } diff --git a/app/Library/Gravatar.php b/app/Library/Gravatar.php index f25a2d0d..1b74532e 100644 --- a/app/Library/Gravatar.php +++ b/app/Library/Gravatar.php @@ -18,8 +18,6 @@ * along with this program. If not, see . */ -use Illuminate\Support\Facades\URL; - class Gravatar { public static function getUrl($email, $size = 80, $default = null, $rating = 'g') @@ -40,7 +38,10 @@ class Gravatar } } - $url .= "&d=" . urlencode(URL::to('/images/icons/profile_' . $size . '.png')); + // Pony.fm's production URL is hardcoded here so Gravatar can + // serve functioning default avatars in the dev environment, + // which it won't be able to access. + $url .= "&d=" . urlencode(URL::to('https://pony.fm/images/icons/profile_' . $size . '.png')); } return $url; diff --git a/app/Library/IpsHasher.php b/app/Library/IpsHasher.php deleted file mode 100644 index 5fb28d2f..00000000 --- a/app/Library/IpsHasher.php +++ /dev/null @@ -1,53 +0,0 @@ -. - */ - -use Illuminate\Contracts\Hashing\Hasher; - -class IpsHasher implements Hasher -{ - public function make($value, array $options = array()) - { - return md5(md5($options['salt']) . md5(static::ips_sanitize($value))); - } - - public function check($value, $hashedValue, array $options = array()) - { - return static::make($value, ['salt' => $options['salt']]) === $hashedValue; - } - - public function needsRehash($hashedValue, array $options = array()) - { - return false; - } - - static public function ips_sanitize($value) - { - $value = str_replace('&', '&', $value); - $value = str_replace('\\', '\', $value); - $value = str_replace('!', '!', $value); - $value = str_replace('$', '$', $value); - $value = str_replace('"', '"', $value); - $value = str_replace('<', '<', $value); - $value = str_replace('>', '>', $value); - $value = str_replace('\'', ''', $value); - - return $value; - } -} diff --git a/app/Library/PfmValidator.php b/app/Library/PfmValidator.php index 132bcd74..9b4ce6db 100644 --- a/app/Library/PfmValidator.php +++ b/app/Library/PfmValidator.php @@ -1,4 +1,5 @@ getPathname()); + $codecString = $file->getAudioCodec(); + + // PCM, ADPCM, and AAC come in several variations as far as FFmpeg + // is concerned. They're all acceptable for Pony.fm, so we check what + // the codec string returned by FFmpeg starts with instead of looking + // for an exact match for these. + if (in_array('adpcm', $parameters) && Str::startsWith($codecString, 'adpcm')) { + return true; + } + + if (in_array('pcm', $parameters) && Str::startsWith($codecString, 'pcm')) { + return true; + } + + if (in_array('aac', $parameters) && Str::startsWith($codecString, 'aac')) { + return true; + } + + if (in_array('alac', $parameters) && Str::startsWith($codecString, 'alac')) { + return true; + } return in_array($file->getAudioCodec(), $parameters); } diff --git a/app/Library/Search.php b/app/Library/Search.php new file mode 100644 index 00000000..13959f09 --- /dev/null +++ b/app/Library/Search.php @@ -0,0 +1,206 @@ +. + */ + +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' => 13 + ], + + //===== 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; + } +} diff --git a/app/Library/getid3/changelog.txt b/app/Library/getid3/changelog.txt index 2c0cca62..11c1cf34 100644 --- a/app/Library/getid3/changelog.txt +++ b/app/Library/getid3/changelog.txt @@ -18,6 +18,24 @@ Version History =============== +1.9.11: [2015-12-24] James Heinrich + * bugfix (G:64): update constructor syntax for PHP 7 + * bugfix (G:62): infinite loop in large PNG files + * bugfix (G:61): ID3v2 remove BOM from frame descriptions + * bugfix (G:60): missing "break" in module.audio-video.quicktime.php + * bugfix (G:59): .gitignore comments + * bugfix (G:58): inconsistency in relation to module.tag.id3v2.php + * bugfix (G:57): comparing instead of assign + * bugfix (G:56): unsupported MIME type "audio/x-wave" + * bugfix (G:55): readme.md variable reference + * bugfix (G:54): QuickTime false 1000fps + * bugfix (G:53): Quicktime / ID3v2 multiple genres + * bugfix (G:52): sys_get_temp_dir in GetDataImageSize + * bugfix (#1903): Quicktime meta atom not parsed + * demo.joinmp3.php enhancements + * m4b (audiobook) chapters not parsed correctly + * sqlite3 caching not working + 1.9.10: [2015-09-14] James Heinrich * bugfix (G:49): Declaration of getID3_cached_sqlite3 * bugfix (#1892): extension.cache.mysql diff --git a/app/Library/getid3/getid3/getid3.php b/app/Library/getid3/getid3/getid3.php index b7b54d35..c085e8c4 100644 --- a/app/Library/getid3/getid3/getid3.php +++ b/app/Library/getid3/getid3/getid3.php @@ -109,7 +109,7 @@ class getID3 protected $startup_error = ''; protected $startup_warning = ''; - const VERSION = '1.9.10-201511241457'; + const VERSION = '1.9.11-201601190922'; const FREAD_BUFFER_SIZE = 32768; const ATTACHMENTS_NONE = false; diff --git a/app/Library/getid3/getid3/module.audio-video.quicktime.php b/app/Library/getid3/getid3/module.audio-video.quicktime.php index 73853d09..d46cc6ea 100644 --- a/app/Library/getid3/getid3/module.audio-video.quicktime.php +++ b/app/Library/getid3/getid3/module.audio-video.quicktime.php @@ -499,6 +499,18 @@ class getid3_quicktime extends getid3_handler $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 8)); break; + case 'covr': + $atom_structure['data'] = substr($boxdata, 8); + // not a foolproof check, but better than nothing + if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/jpeg'; + } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/png'; + } elseif (preg_match('#^GIF#', $atom_structure['data'])) { + $atom_structure['image_mime'] = 'image/gif'; + } + break; + case 'atID': case 'cnID': case 'geID': @@ -516,9 +528,9 @@ class getid3_quicktime extends getid3_handler $atom_structure['data'] = substr($boxdata, 8); if ($atomname == 'covr') { // not a foolproof check, but better than nothing - if (preg_match('#^\xFF\xD8\xFF#', $atom_structure['data'])) { + if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) { $atom_structure['image_mime'] = 'image/jpeg'; - } elseif (preg_match('#^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A#', $atom_structure['data'])) { + } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) { $atom_structure['image_mime'] = 'image/png'; } elseif (preg_match('#^GIF#', $atom_structure['data'])) { $atom_structure['image_mime'] = 'image/gif'; @@ -1579,6 +1591,10 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($ // Furthermore, for historical reasons the list of atoms is optionally // terminated by a 32-bit integer set to 0. If you are writing a program // to read user data atoms, you should allow for the terminating 0. + if (strlen($atom_data) > 12) { + $subatomoffset += 4; + continue; + } return $atom_structure; } diff --git a/app/Library/getid3/getid3/module.audio-video.riff.php b/app/Library/getid3/getid3/module.audio-video.riff.php index e8ba9445..f0be500a 100644 --- a/app/Library/getid3/getid3/module.audio-video.riff.php +++ b/app/Library/getid3/getid3/module.audio-video.riff.php @@ -1170,9 +1170,16 @@ class getid3_riff extends getid3_handler { } break; + case 'WEBP': + // https://developers.google.com/speed/webp/docs/riff_container + $info['fileformat'] = 'webp'; + $info['mime_type'] = 'image/webp'; + +$info['error'][] = 'WebP image parsing not supported in this version of getID3()'; + break; default: - $info['error'][] = 'Unknown RIFF type: expecting one of (WAVE|RMP3|AVI |CDDA|AIFF|AIFC|8SVX|CDXA), found "'.$RIFFsubtype.'" instead'; + $info['error'][] = 'Unknown RIFF type: expecting one of (WAVE|RMP3|AVI |CDDA|AIFF|AIFC|8SVX|CDXA|WEBP), found "'.$RIFFsubtype.'" instead'; //unset($info['fileformat']); } diff --git a/app/Album.php b/app/Models/Album.php similarity index 68% rename from app/Album.php rename to app/Models/Album.php index 5530ce61..e3000e17 100644 --- a/app/Album.php +++ b/app/Models/Album.php @@ -18,23 +18,53 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Exception; use Helpers; use Illuminate\Database\Eloquent\Model; 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; -class Album extends Model +/** + * Poniverse\Ponyfm\Models\Album + * + * @property integer $id + * @property integer $user_id + * @property string $title + * @property string $slug + * @property string $description + * @property integer $cover_id + * @property integer $track_count + * @property integer $view_count + * @property integer $download_count + * @property integer $favourite_count + * @property integer $comment_count + * @property \Carbon\Carbon $created_at + * @property string $updated_at + * @property \Carbon\Carbon $deleted_at + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Favourite[] $favourites + * @property-read \Poniverse\Ponyfm\Models\Image $cover + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments + * @property-read mixed $url + * @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 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']; @@ -49,7 +79,7 @@ class Album extends Model { if (Auth::check()) { $query->with([ - 'users' => function ($query) { + 'users' => function($query) { $query->whereUserId(Auth::user()->id); } ]); @@ -62,27 +92,27 @@ class Album extends Model public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function users() { - return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); + return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser'); } public function favourites() { - return $this->hasMany('Poniverse\Ponyfm\Favourite'); + return $this->hasMany('Poniverse\Ponyfm\Models\Favourite'); } public function cover() { - return $this->belongsTo('Poniverse\Ponyfm\Image'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Image'); } public function tracks() { - return $this->hasMany('Poniverse\Ponyfm\Track')->orderBy('track_number', 'asc'); + return $this->hasMany('Poniverse\Ponyfm\Models\Track')->orderBy('track_number', 'asc'); } public function trackFiles() { @@ -91,7 +121,7 @@ class Album extends Model public function comments() { - return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); + return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc'); } public static function mapPublicAlbumShow(Album $album) @@ -133,8 +163,8 @@ class Album extends Model $data['is_downloadable'] = $is_downloadable; $data['share'] = [ 'url' => action('AlbumsController@getShortlink', ['id' => $album->id]), - 'tumblrUrl' => 'http://www.tumblr.com/share/link?url=' . urlencode($album->url) . '&name=' . urlencode($album->title) . '&description=' . urlencode($album->description), - 'twitterUrl' => 'https://platform.twitter.com/widgets/tweet_button.html?text=' . $album->title . ' by ' . $album->user->display_name . ' on Pony.fm' + 'tumblrUrl' => 'http://www.tumblr.com/share/link?url='.urlencode($album->url).'&name='.urlencode($album->title).'&description='.urlencode($album->description), + 'twitterUrl' => 'https://platform.twitter.com/widgets/tweet_button.html?text='.$album->title.' by '.$album->user->display_name.' on Pony.fm' ]; return $data; @@ -154,24 +184,24 @@ class Album extends Model $userRow = $album->users[0]; $userData = [ 'stats' => [ - 'views' => (int)$userRow->view_count, - 'downloads' => (int)$userRow->download_count, + 'views' => (int) $userRow->view_count, + 'downloads' => (int) $userRow->download_count, ], - 'is_favourited' => (bool)$userRow->is_favourited + 'is_favourited' => (bool) $userRow->is_favourited ]; } return [ - 'id' => (int)$album->id, - 'track_count' => (int)$album->track_count, + 'id' => (int) $album->id, + 'track_count' => (int) $album->track_count, 'title' => $album->title, 'slug' => $album->slug, 'created_at' => $album->created_at->format('c'), 'stats' => [ - 'views' => (int)$album->view_count, - 'downloads' => (int)$album->download_count, - 'comments' => (int)$album->comment_count, - 'favourites' => (int)$album->favourite_count + 'views' => (int) $album->view_count, + 'downloads' => (int) $album->download_count, + 'comments' => (int) $album->comment_count, + 'favourites' => (int) $album->favourite_count ], 'covers' => [ 'small' => $album->getCoverUrl(Image::SMALL), @@ -180,8 +210,9 @@ class Album extends Model ], 'url' => $album->url, 'user' => [ - 'id' => (int)$album->user->id, + 'id' => (int) $album->user->id, 'name' => $album->user->display_name, + 'slug' => $album->user->slug, 'url' => $album->user->url, ], 'user_data' => $userData, @@ -214,7 +245,7 @@ class Album extends Model return 0; } - return Cache::remember($this->getCacheKey('filesize-' . $format), 1440, function () use ($tracks, $format) { + return Cache::remember($this->getCacheKey('filesize-'.$format), 1440, function() use ($tracks, $format) { $size = 0; foreach ($tracks as $track) { @@ -246,9 +277,9 @@ class Album extends Model public function getDirectory() { - $dir = (string)(floor($this->id / 100) * 100); + $dir = (string) (floor($this->id / 100) * 100); - return \Config::get('ponyfm.files_directory') . '/tracks/' . $dir; + return \Config::get('ponyfm.files_directory').'/tracks/'.$dir; } public function getDates() @@ -353,12 +384,48 @@ class Album extends Model } foreach (Track::$Formats as $name => $format) { - Cache::forget($this->getCacheKey('filesize' . $name)); + Cache::forget($this->getCacheKey('filesize'.$name)); } } public function getCacheKey($key) { - return 'album-' . $this->id . '-' . $key; + return 'album-'.$this->id.'-'.$key; + } + + /** + * The number of tracks in an album will always be in sync. + * + * @param array $options + * @return bool + */ + public function save(array $options = []) { + $this->recountTracks(); + return parent::save($options); + } + + 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(); } } diff --git a/app/Comment.php b/app/Models/Comment.php similarity index 69% rename from app/Comment.php rename to app/Models/Comment.php index b5f6699f..2b71b549 100644 --- a/app/Comment.php +++ b/app/Models/Comment.php @@ -18,11 +18,32 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +/** + * Poniverse\Ponyfm\Models\Comment + * + * @property integer $id + * @property integer $user_id + * @property string $ip_address + * @property string $content + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property \Carbon\Carbon $deleted_at + * @property integer $profile_id + * @property integer $track_id + * @property integer $album_id + * @property integer $playlist_id + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read \Poniverse\Ponyfm\Models\Track $track + * @property-read \Poniverse\Ponyfm\Models\Album $album + * @property-read \Poniverse\Ponyfm\Models\Playlist $playlist + * @property-read \Poniverse\Ponyfm\Models\User $profile + * @property-read mixed $resource + */ class Comment extends Model { @@ -34,27 +55,27 @@ class Comment extends Model public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function track() { - return $this->belongsTo('Poniverse\Ponyfm\Track'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Track'); } public function album() { - return $this->belongsTo('Poniverse\Ponyfm\Album'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Album'); } public function playlist() { - return $this->belongsTo('Poniverse\Ponyfm\Playlist'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist'); } public function profile() { - return $this->belongsTo('Poniverse\Ponyfm\User', 'profile_id'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User', 'profile_id'); } public static function mapPublic($comment) diff --git a/app/Favourite.php b/app/Models/Favourite.php similarity index 70% rename from app/Favourite.php rename to app/Models/Favourite.php index e3f52f5e..912cf633 100644 --- a/app/Favourite.php +++ b/app/Models/Favourite.php @@ -18,10 +18,26 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\Favourite + * + * @property integer $id + * @property integer $user_id + * @property integer $track_id + * @property integer $album_id + * @property integer $playlist_id + * @property string $created_at + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read \Poniverse\Ponyfm\Models\Track $track + * @property-read \Poniverse\Ponyfm\Models\Album $album + * @property-read \Poniverse\Ponyfm\Models\Playlist $playlist + * @property-read mixed $resource + * @property-read mixed $type + */ class Favourite extends Model { protected $table = 'favourites'; @@ -35,22 +51,22 @@ class Favourite extends Model public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function track() { - return $this->belongsTo('Poniverse\Ponyfm\Track'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Track'); } public function album() { - return $this->belongsTo('Poniverse\Ponyfm\Album'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Album'); } public function playlist() { - return $this->belongsTo('Poniverse\Ponyfm\Playlist'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist'); } /** diff --git a/app/Follower.php b/app/Models/Follower.php similarity index 79% rename from app/Follower.php rename to app/Models/Follower.php index 1c3b6d31..3357e76d 100644 --- a/app/Follower.php +++ b/app/Models/Follower.php @@ -18,10 +18,19 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\Follower + * + * @property integer $id + * @property integer $user_id + * @property integer $artist_id + * @property integer $playlist_id + * @property string $created_at + */ class Follower extends Model { protected $table = 'followers'; diff --git a/app/Genre.php b/app/Models/Genre.php similarity index 78% rename from app/Genre.php rename to app/Models/Genre.php index 529352b3..0569e539 100644 --- a/app/Genre.php +++ b/app/Models/Genre.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use DB; use Illuminate\Database\Eloquent\Relations\Relation; @@ -27,6 +27,22 @@ use Poniverse\Ponyfm\Traits\SlugTrait; use Illuminate\Database\Eloquent\Model; use Venturecraft\Revisionable\RevisionableTrait; +/** + * Poniverse\Ponyfm\Models\Genre + * + * @property integer $id + * @property string $name + * @property string $slug + * @property string $deleted_at + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks + * @property-read \Poniverse\Ponyfm\Models\Track $trackCountRelation + * @property-read mixed $track_count + * @property-read mixed $url + * @property-write mixed $title + * @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + */ class Genre extends Model { protected $table = 'genres'; @@ -35,8 +51,6 @@ class Genre extends Model protected $appends = ['track_count', 'url']; protected $hidden = ['trackCountRelation']; - public $timestamps = false; - use SlugTrait, SoftDeletes, RevisionableTrait; public function tracks(){ diff --git a/app/Image.php b/app/Models/Image.php similarity index 75% rename from app/Image.php rename to app/Models/Image.php index cc0d4d1e..882e8d0c 100644 --- a/app/Image.php +++ b/app/Models/Image.php @@ -18,13 +18,27 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use External; use Illuminate\Database\Eloquent\Model; use Config; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\File\UploadedFile; +/** + * Poniverse\Ponyfm\Models\Image + * + * @property integer $id + * @property string $filename + * @property string $mime + * @property string $extension + * @property integer $size + * @property string $hash + * @property integer $uploaded_by + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + */ class Image extends Model { const NORMAL = 1; @@ -52,7 +66,14 @@ class Image extends Model return null; } - public static function upload(UploadedFile $file, $user) + /** + * @param UploadedFile $file + * @param int|User $user + * @param bool $forceReupload forces the image to be re-processed even if a matching hash is found + * @return Image + * @throws \Exception + */ + public static function upload(UploadedFile $file, $user, bool $forceReupload = false) { $userId = $user; if ($user instanceof User) { @@ -63,10 +84,25 @@ class Image extends Model $image = Image::whereHash($hash)->whereUploadedBy($userId)->first(); if ($image) { - return $image; + if ($forceReupload) { + // delete existing versions of the image + $filenames = scandir($image->getDirectory()); + $imagePrefix = $image->id.'_'; + + $filenames = array_filter($filenames, function(string $filename) use ($imagePrefix) { + return Str::startsWith($filename, $imagePrefix); + }); + + foreach($filenames as $filename) { + unlink($image->getDirectory().'/'.$filename); + } + } else { + return $image; + } + } else { + $image = new Image(); } - $image = new Image(); try { $image->uploaded_by = $userId; $image->size = $file->getSize(); @@ -79,7 +115,7 @@ class Image extends Model $image->ensureDirectoryExists(); foreach (self::$ImageTypes as $coverType) { if ($coverType['id'] === self::ORIGINAL && $image->mime === 'image/jpeg') { - $command = 'cp '.$file->getPathname().' '.$image->getFile($coverType['id']); + $command = 'cp "'.$file->getPathname().'" '.$image->getFile($coverType['id']); } else { // ImageMagick options reference: http://www.imagemagick.org/script/command-line-options.php @@ -100,6 +136,7 @@ class Image extends Model } External::execute($command); + chmod($image->getFile($coverType['id']), 0644); } return $image; diff --git a/app/License.php b/app/Models/License.php similarity index 75% rename from app/License.php rename to app/Models/License.php index f3c48c27..394d40aa 100644 --- a/app/License.php +++ b/app/Models/License.php @@ -18,10 +18,20 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\License + * + * @property integer $id + * @property string $title + * @property string $description + * @property boolean $affiliate_distribution + * @property boolean $open_distribution + * @property boolean $remix + */ class License extends Model { protected $table = 'licenses'; diff --git a/app/PinnedPlaylist.php b/app/Models/PinnedPlaylist.php similarity index 65% rename from app/PinnedPlaylist.php rename to app/Models/PinnedPlaylist.php index 84d7d0f3..6af92f4f 100644 --- a/app/PinnedPlaylist.php +++ b/app/Models/PinnedPlaylist.php @@ -18,21 +18,32 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\PinnedPlaylist + * + * @property integer $id + * @property integer $user_id + * @property integer $playlist_id + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read \Poniverse\Ponyfm\Models\Playlist $playlist + */ class PinnedPlaylist extends Model { protected $table = 'pinned_playlists'; public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function playlist() { - return $this->belongsTo('Poniverse\Ponyfm\Playlist'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist'); } } diff --git a/app/Playlist.php b/app/Models/Playlist.php similarity index 71% rename from app/Playlist.php rename to app/Models/Playlist.php index a859acc4..c9f4d7ad 100644 --- a/app/Playlist.php +++ b/app/Models/Playlist.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Helpers; use Illuminate\Database\Eloquent\Model; @@ -26,18 +26,61 @@ 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; -class Playlist extends Model +/** + * Poniverse\Ponyfm\Models\Playlist + * + * @property integer $id + * @property integer $user_id + * @property string $title + * @property string $slug + * @property string $description + * @property boolean $is_public + * @property integer $track_count + * @property integer $view_count + * @property integer $download_count + * @property integer $favourite_count + * @property integer $follow_count + * @property integer $comment_count + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property \Carbon\Carbon $deleted_at + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\PinnedPlaylist[] $pins + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read mixed $url + * @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 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() { @@ -152,7 +195,7 @@ class Playlist extends Model public function tracks() { return $this - ->belongsToMany('Poniverse\Ponyfm\Track') + ->belongsToMany('Poniverse\Ponyfm\Models\Track') ->withPivot('position') ->withTimestamps() ->orderBy('position', 'asc'); @@ -166,22 +209,22 @@ class Playlist extends Model public function users() { - return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); + return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser'); } public function comments() { - return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); + return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc'); } public function pins() { - return $this->hasMany('Poniverse\Ponyfm\PinnedPlaylist'); + return $this->hasMany('Poniverse\Ponyfm\Models\PinnedPlaylist'); } public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function hasPinFor($userId) @@ -258,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(); + } } diff --git a/app/ResourceLogItem.php b/app/Models/ResourceLogItem.php similarity index 90% rename from app/ResourceLogItem.php rename to app/Models/ResourceLogItem.php index 39dc985c..643be428 100644 --- a/app/ResourceLogItem.php +++ b/app/Models/ResourceLogItem.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; use Carbon\Carbon; @@ -26,6 +26,19 @@ use Auth; use DB; use Request; +/** + * Poniverse\Ponyfm\Models\ResourceLogItem + * + * @property integer $id + * @property integer $user_id + * @property integer $log_type + * @property string $ip_address + * @property integer $track_format_id + * @property integer $track_id + * @property integer $album_id + * @property integer $playlist_id + * @property \Carbon\Carbon $created_at + */ class ResourceLogItem extends Model { protected $table = 'resource_log_items'; diff --git a/app/ResourceUser.php b/app/Models/ResourceUser.php similarity index 77% rename from app/ResourceUser.php rename to app/Models/ResourceUser.php index e4b7b833..5b1413af 100644 --- a/app/ResourceUser.php +++ b/app/Models/ResourceUser.php @@ -18,10 +18,26 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\ResourceUser + * + * @property integer $id + * @property integer $user_id + * @property integer $track_id + * @property integer $album_id + * @property integer $playlist_id + * @property integer $artist_id + * @property boolean $is_followed + * @property boolean $is_favourited + * @property boolean $is_pinned + * @property integer $view_count + * @property integer $play_count + * @property integer $download_count + */ class ResourceUser extends Model { protected $table = 'resource_users'; diff --git a/app/Role.php b/app/Models/Role.php similarity index 81% rename from app/Role.php rename to app/Models/Role.php index 35215138..19ca1b48 100644 --- a/app/Role.php +++ b/app/Models/Role.php @@ -18,10 +18,17 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\Role + * + * @property integer $id + * @property string $name + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\User[] $users + */ class Role extends Model { protected $table = 'roles'; diff --git a/app/ShowSong.php b/app/Models/ShowSong.php similarity index 82% rename from app/ShowSong.php rename to app/Models/ShowSong.php index 4d6b71c4..8b14f466 100644 --- a/app/ShowSong.php +++ b/app/Models/ShowSong.php @@ -18,10 +18,18 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\ShowSong + * + * @property integer $id + * @property string $title + * @property string $lyrics + * @property string $slug + */ class ShowSong extends Model { protected $table = 'show_songs'; diff --git a/app/Track.php b/app/Models/Track.php similarity index 70% rename from app/Track.php rename to app/Models/Track.php index acc49bb3..cbfd510d 100644 --- a/app/Track.php +++ b/app/Models/Track.php @@ -18,13 +18,16 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Auth; use Cache; use Config; use DB; +use Gate; +use Poniverse\Ponyfm\Contracts\Searchable; use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException; +use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait; use Poniverse\Ponyfm\Traits\SlugTrait; use Exception; use External; @@ -36,9 +39,68 @@ use Illuminate\Support\Str; use Log; use Venturecraft\Revisionable\RevisionableTrait; -class Track extends Model +/** + * Poniverse\Ponyfm\Models\Track + * + * @property integer $id + * @property integer $user_id + * @property integer $license_id + * @property integer $genre_id + * @property integer $track_type_id + * @property string $title + * @property string $slug + * @property string $description + * @property string $lyrics + * @property boolean $is_vocal + * @property boolean $is_explicit + * @property integer $cover_id + * @property boolean $is_downloadable + * @property float $duration + * @property integer $play_count + * @property integer $view_count + * @property integer $download_count + * @property integer $favourite_count + * @property integer $comment_count + * @property \Carbon\Carbon $created_at + * @property string $updated_at + * @property \Carbon\Carbon $deleted_at + * @property \Carbon\Carbon $published_at + * @property \Carbon\Carbon $released_at + * @property integer $album_id + * @property integer $track_number + * @property boolean $is_latest + * @property string $hash + * @property boolean $is_listed + * @property string $source + * @property string $original_tags + * @property string $metadata + * @property-read \Poniverse\Ponyfm\Models\Genre $genre + * @property-read \Poniverse\Ponyfm\Models\TrackType $trackType + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Favourite[] $favourites + * @property-read \Poniverse\Ponyfm\Models\Image $cover + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ShowSong[] $showSongs + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users + * @property-read \Poniverse\Ponyfm\Models\User $user + * @property-read \Poniverse\Ponyfm\Models\Album $album + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\TrackFile[] $trackFiles + * @property-read mixed $year + * @property-read mixed $url + * @property-read mixed $download_directory + * @property-read mixed $status + * @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory + * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track userDetails() + * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track published() + * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track listed() + * @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track explicitFilter() + * @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 implements Searchable { - use SoftDeletes; + use SoftDeletes, IndexedInElasticsearchTrait; + + protected $elasticsearchType = 'track'; protected $dates = ['deleted_at', 'published_at', 'released_at']; protected $hidden = ['original_tags', 'metadata']; @@ -149,7 +211,7 @@ class Track extends Model { if (Auth::check()) { $query->with([ - 'users' => function ($query) { + 'users' => function($query) { $query->whereUserId(Auth::user()->id); } ]); @@ -176,7 +238,7 @@ class Track extends Model public function scopeWithComments($query) { $query->with([ - 'comments' => function ($query) { + 'comments' => function($query) { $query->with('user'); } ]); @@ -192,10 +254,13 @@ class Track extends Model $query->join('mlpma_tracks', 'tracks.id', '=', 'mlpma_tracks.track_id'); } + /** + * @param integer $count + */ public static function popular($count, $allowExplicit = false) { - $trackIds = Cache::remember('popular_tracks' . $count . '-' . ($allowExplicit ? 'explicit' : 'safe'), 5, - function () use ($allowExplicit, $count) { + $trackIds = Cache::remember('popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'), 5, + function() use ($allowExplicit, $count) { $query = static ::published() ->listed() @@ -238,6 +303,10 @@ class Track extends Model $processed[] = Track::mapPublicTrackSummary($track); } + // Songs that get played more should drop down + // in the list so they don't hog the top spots. + array_reverse($processed); + return $processed; } @@ -276,12 +345,12 @@ class Track extends Model $returnValue['share'] = [ 'url' => action('TracksController@getShortlink', ['id' => $track->id]), - 'html' => '', - 'bbcode' => '[url=' . $track->url . '][img]' . $track->getCoverUrl() . '[/img][/url]', - 'twitterUrl' => 'https://platform.twitter.com/widgets/tweet_button.html?text=' . $track->title . ' by ' . $track->user->display_name . ' on Pony.fm' + 'html' => '', + 'bbcode' => '[url='.$track->url.'][img]'.$track->getCoverUrl().'[/img][/url]', + 'twitterUrl' => 'https://platform.twitter.com/widgets/tweet_button.html?text='.$track->title.' by '.$track->user->display_name.' on Pony.fm' ]; - $returnValue['share']['tumblrUrl'] = 'http://www.tumblr.com/share/video?embed=' . urlencode($returnValue['share']['html']) . '&caption=' . urlencode($track->title); + $returnValue['share']['tumblrUrl'] = 'http://www.tumblr.com/share/video?embed='.urlencode($returnValue['share']['html']).'&caption='.urlencode($track->title); $returnValue['formats'] = $formats; @@ -303,41 +372,41 @@ class Track extends Model $userRow = $track->users[0]; $userData = [ 'stats' => [ - 'views' => (int)$userRow->view_count, - 'plays' => (int)$userRow->play_count, + 'views' => (int) $userRow->view_count, + 'plays' => (int) $userRow->play_count, 'downloads' => $userRow->download_count, ], - 'is_favourited' => (bool)$userRow->is_favourited + 'is_favourited' => (bool) $userRow->is_favourited ]; } return [ - 'id' => (int)$track->id, + 'id' => (int) $track->id, 'title' => $track->title, 'user' => [ - 'id' => (int)$track->user->id, + 'id' => (int) $track->user->id, 'name' => $track->user->display_name, 'url' => $track->user->url ], 'stats' => [ - 'views' => (int)$track->view_count, - 'plays' => (int)$track->play_count, - 'downloads' => (int)$track->download_count, - 'comments' => (int)$track->comment_count, - 'favourites' => (int)$track->favourite_count + 'views' => (int) $track->view_count, + 'plays' => (int) $track->play_count, + 'downloads' => (int) $track->download_count, + 'comments' => (int) $track->comment_count, + 'favourites' => (int) $track->favourite_count ], 'url' => $track->url, 'slug' => $track->slug, - 'is_vocal' => (bool)$track->is_vocal, - 'is_explicit' => (bool)$track->is_explicit, - 'is_downloadable' => (bool)$track->is_downloadable, - 'is_published' => (bool)$track->isPublished(), - 'published_at' => $track->published_at->format('c'), + 'is_vocal' => $track->is_vocal, + 'is_explicit' => $track->is_explicit, + 'is_downloadable' => $track->is_downloadable, + 'is_published' => $track->isPublished(), + 'published_at' => $track->isPublished() ? $track->published_at->format('c') : null, 'duration' => $track->duration, 'genre' => $track->genre != null ? [ - 'id' => (int)$track->genre->id, + 'id' => (int) $track->genre->id, 'slug' => $track->genre->slug, 'name' => $track->genre->name ] : null, @@ -355,8 +424,8 @@ class Track extends Model ], 'user_data' => $userData, 'permissions' => [ - 'delete' => Auth::check() && Auth::user()->id == $track->user_id, - 'edit' => Auth::check() && Auth::user()->id == $track->user_id + 'delete' => Gate::allows('delete', $track), + 'edit' => Gate::allows('edit', $track) ] ]; } @@ -371,12 +440,13 @@ class Track extends Model $returnValue = self::mapPrivateTrackSummary($track); $returnValue['album_id'] = $track->album_id; $returnValue['show_songs'] = $showSongs; + $returnValue['cover_id'] = $track->cover_id; $returnValue['real_cover_url'] = $track->getCoverUrl(Image::NORMAL); $returnValue['cover_url'] = $track->hasCover() ? $track->getCoverUrl(Image::NORMAL) : null; - $returnValue['released_at'] = $track->released_at; + $returnValue['released_at'] = $track->released_at ? $track->released_at->toDateString() : null; $returnValue['lyrics'] = $track->lyrics; $returnValue['description'] = $track->description; - $returnValue['is_downloadable'] = !$track->isPublished() ? true : (bool)$track->is_downloadable; + $returnValue['is_downloadable'] = !$track->isPublished() ? true : (bool) $track->is_downloadable; $returnValue['license_id'] = $track->license_id != null ? $track->license_id : 3; return $returnValue; @@ -407,52 +477,52 @@ class Track extends Model public function genre() { - return $this->belongsTo('Poniverse\Ponyfm\Genre'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Genre'); } public function trackType() { - return $this->belongsTo('Poniverse\Ponyfm\TrackType', 'track_type_id'); + return $this->belongsTo('Poniverse\Ponyfm\Models\TrackType', 'track_type_id'); } public function comments() { - return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); + return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc'); } public function favourites() { - return $this->hasMany('Poniverse\Ponyfm\Favourite'); + return $this->hasMany('Poniverse\Ponyfm\Models\Favourite'); } public function cover() { - return $this->belongsTo('Poniverse\Ponyfm\Image'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Image'); } public function showSongs() { - return $this->belongsToMany('Poniverse\Ponyfm\ShowSong'); + return $this->belongsToMany('Poniverse\Ponyfm\Models\ShowSong'); } public function users() { - return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); + return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser'); } public function user() { - return $this->belongsTo('Poniverse\Ponyfm\User'); + return $this->belongsTo('Poniverse\Ponyfm\Models\User'); } public function album() { - return $this->belongsTo('Poniverse\Ponyfm\Album'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Album'); } public function trackFiles() { - return $this->hasMany('Poniverse\Ponyfm\TrackFile'); + return $this->hasMany('Poniverse\Ponyfm\Models\TrackFile'); } public function getYearAttribute() @@ -462,7 +532,7 @@ class Track extends Model public function setTitleAttribute($value) { - $this->setTitleAttributeSlug($value);; + $this->setTitleAttributeSlug($value); ; $this->updateHash(); } @@ -501,7 +571,7 @@ class Track extends Model public function getDownloadDirectoryAttribute() { if ($this->album) { - return $this->user->display_name . '/' . $this->album->title; + return $this->user->display_name.'/'.$this->album->title; } return $this->user->display_name; @@ -560,9 +630,9 @@ class Track extends Model public function getDirectory() { - $dir = (string)(floor($this->id / 100) * 100); + $dir = (string) (floor($this->id / 100) * 100); - return \Config::get('ponyfm.files_directory') . '/tracks/' . $dir; + return \Config::get('ponyfm.files_directory').'/tracks/'.$dir; } public function getDates() @@ -592,6 +662,9 @@ class Track extends Model return "{$this->title}.{$format['extension']}"; } + /** + * @return string + */ public function getFileFor($format) { if (!isset(self::$Formats[$format])) { @@ -610,8 +683,8 @@ class Track extends Model * * @return string */ - public function getTemporarySourceFile() { - return Config::get('ponyfm.files_directory') . '/queued-tracks/' . $this->id; + public function getTemporarySourceFile():string { + return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id; } @@ -630,18 +703,18 @@ class Track extends Model /** * @return string one of the Track::STATUS_* values, indicating whether this track is currently being processed */ - public function getStatusAttribute(){ - return $this->trackFiles->reduce(function($carry, $trackFile){ - if((int) $trackFile->status === TrackFile::STATUS_PROCESSING_ERROR) { + public function getStatusAttribute() { + return $this->trackFiles->reduce(function($carry, $trackFile) { + if ((int) $trackFile->status === TrackFile::STATUS_PROCESSING_ERROR) { return static::STATUS_ERROR; } elseif ( $carry !== static::STATUS_ERROR && - (int) $trackFile->status === TrackFile::STATUS_PROCESSING) { + in_array($trackFile->status, [TrackFile::STATUS_PROCESSING, TrackFile::STATUS_PROCESSING_PENDING])) { return static::STATUS_PROCESSING; } elseif ( - !in_array($carry, [static::STATUS_ERROR, static::STATUS_PROCESSING]) && + !in_array($carry, [static::STATUS_ERROR, static::STATUS_PROCESSING, TrackFile::STATUS_PROCESSING_PENDING]) && (int) $trackFile->status === TrackFile::STATUS_NOT_BEING_PROCESSED ) { return static::STATUS_COMPLETE; @@ -655,7 +728,7 @@ class Track extends Model public function updateHash() { - $this->hash = md5(Helpers::sanitizeInputForHashing($this->user->display_name) . ' - ' . Helpers::sanitizeInputForHashing($this->title)); + $this->hash = md5(Helpers::sanitizeInputForHashing($this->user->display_name).' - '.Helpers::sanitizeInputForHashing($this->title)); } public function updateTags($trackFileFormat = 'all') @@ -670,7 +743,7 @@ class Track extends Model } } - private function updateTagsForTrackFile($trackFile) { + private function updateTagsForTrackFile(TrackFile $trackFile) { $trackFile->touch(); if (\File::exists($trackFile->getFile())) { @@ -684,22 +757,22 @@ class Track extends Model /** @noinspection PhpUnusedPrivateMethodInspection */ private function updateTagsWithAtomicParsley($format) { - $command = 'AtomicParsley "' . $this->getFileFor($format) . '" '; - $command .= '--title ' . escapeshellarg($this->title) . ' '; - $command .= '--artist ' . escapeshellarg($this->user->display_name) . ' '; - $command .= '--year "' . $this->year . '" '; - $command .= '--genre ' . escapeshellarg($this->genre != null ? $this->genre->name : '') . ' '; - $command .= '--copyright ' . escapeshellarg('© ' . $this->year . ' ' . $this->user->display_name) . ' '; - $command .= '--comment "' . 'Downloaded from: https://pony.fm/' . '" '; - $command .= '--encodingTool "' . 'Pony.fm - https://pony.fm/' . '" '; + $command = 'AtomicParsley "'.$this->getFileFor($format).'" '; + $command .= '--title '.escapeshellarg($this->title).' '; + $command .= '--artist '.escapeshellarg($this->user->display_name).' '; + $command .= '--year "'.$this->year.'" '; + $command .= '--genre '.escapeshellarg($this->genre != null ? $this->genre->name : '').' '; + $command .= '--copyright '.escapeshellarg('© '.$this->year.' '.$this->user->display_name).' '; + $command .= '--comment "'.'Downloaded from: https://pony.fm/'.'" '; + $command .= '--encodingTool "'.'Pony.fm - https://pony.fm/'.'" '; if ($this->album_id !== null) { - $command .= '--album ' . escapeshellarg($this->album->title) . ' '; - $command .= '--tracknum ' . $this->track_number . ' '; + $command .= '--album '.escapeshellarg($this->album->title).' '; + $command .= '--tracknum '.$this->track_number.' '; } if ($this->cover !== null) { - $command .= '--artwork ' . $this->cover->getFile() . ' '; + $command .= '--artwork '.$this->cover->getFile(Image::ORIGINAL).' '; } $command .= '--overWrite'; @@ -710,8 +783,8 @@ class Track extends Model /** @noinspection PhpUnusedPrivateMethodInspection */ private function updateTagsWithGetId3($format) { - require_once(app_path() . '/Library/getid3/getid3/getid3.php'); - require_once(app_path() . '/Library/getid3/getid3/write.php'); + require_once(app_path().'/Library/getid3/getid3/getid3.php'); + require_once(app_path().'/Library/getid3/getid3/write.php'); $tagWriter = new getid3_writetags; $tagWriter->overwrite_tags = true; @@ -721,10 +794,10 @@ class Track extends Model $tagWriter->tag_data = [ 'title' => [$this->title], 'artist' => [$this->user->display_name], - 'year' => ['' . $this->year], + 'year' => [''.$this->year], 'genre' => [$this->genre != null ? $this->genre->name : ''], 'comment' => ['Downloaded from: https://pony.fm/'], - 'copyright' => ['© ' . $this->year . ' ' . $this->user->display_name], + 'copyright' => ['© '.$this->year.' '.$this->user->display_name], 'publisher' => ['Pony.fm - https://pony.fm/'], 'encoded_by' => ['https://pony.fm/'], // 'url_artist' => [$this->user->url], @@ -740,7 +813,7 @@ class Track extends Model if ($format == 'MP3' && $this->cover_id != null && is_file($this->cover->getFile())) { $tagWriter->tag_data['attached_picture'][0] = [ - 'data' => file_get_contents($this->cover->getFile()), + 'data' => file_get_contents($this->cover->getFile(Image::ORIGINAL)), 'picturetypeid' => 2, 'description' => 'cover', 'mime' => $this->cover->mime @@ -752,17 +825,41 @@ class Track extends Model if ($tagWriter->WriteTags()) { if (!empty($tagWriter->warnings)) { - Log::warning('Track #' . $this->id . ': There were some warnings:
' . implode('

', + Log::warning('Track #'.$this->id.': There were some warnings:
'.implode('

', $tagWriter->warnings)); } } else { - Log::error('Track #' . $this->id . ': Failed to write tags!
' . implode('

', + Log::error('Track #'.$this->id.': Failed to write tags!
'.implode('

', $tagWriter->errors)); } } private function getCacheKey($key) { - return 'track-' . $this->id . '-' . $key; + 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') + ]; } } diff --git a/app/TrackFile.php b/app/Models/TrackFile.php similarity index 74% rename from app/TrackFile.php rename to app/Models/TrackFile.php index a705c998..77521cd0 100644 --- a/app/TrackFile.php +++ b/app/Models/TrackFile.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Config; use Helpers; @@ -26,17 +26,47 @@ use Illuminate\Database\Eloquent\Model; use App; use File; +/** + * Poniverse\Ponyfm\Models\TrackFile + * + * @property integer $id + * @property integer $track_id + * @property boolean $is_master + * @property string $format + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property boolean $is_cacheable + * @property boolean $status + * @property \Carbon\Carbon $expires_at + * @property integer $filesize + * @property-read \Poniverse\Ponyfm\Models\Track $track + * @property-read mixed $extension + * @property-read mixed $url + * @property-read mixed $size + * @property-read mixed $is_expired + */ class TrackFile extends Model { // used for the "status" property const STATUS_NOT_BEING_PROCESSED = 0; const STATUS_PROCESSING = 1; const STATUS_PROCESSING_ERROR = 2; + const STATUS_PROCESSING_PENDING = 3; + protected $appends = ['is_expired']; + protected $dates = ['expires_at']; + protected $casts = [ + 'id' => 'integer', + 'track_id' => 'integer', + 'is_master' => 'boolean', + 'format' => 'string', + 'is_cacheable' => 'boolean', + 'status' => 'integer', + 'filesize' => 'integer', + ]; - public function track() - { - return $this->belongsTo('Poniverse\Ponyfm\Track')->withTrashed(); + public function track() { + return $this->belongsTo(Track::class)->withTrashed(); } /** @@ -75,6 +105,11 @@ class TrackFile extends Model } } + public function getIsExpiredAttribute() { + return $this->attributes['expires_at'] === null || + $this->expires_at->isPast(); + } + public function getFormatAttribute($value) { return $value; diff --git a/app/TrackType.php b/app/Models/TrackType.php similarity index 86% rename from app/TrackType.php rename to app/Models/TrackType.php index 23589e63..464f7a38 100644 --- a/app/TrackType.php +++ b/app/Models/TrackType.php @@ -18,10 +18,17 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Illuminate\Database\Eloquent\Model; +/** + * Poniverse\Ponyfm\Models\TrackType + * + * @property integer $id + * @property string $title + * @property string $editor_title + */ class TrackType extends Model { protected $table = 'track_types'; diff --git a/app/User.php b/app/Models/User.php similarity index 59% rename from app/User.php rename to app/Models/User.php index 562d7727..89854764 100644 --- a/app/User.php +++ b/app/Models/User.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm; +namespace Poniverse\Ponyfm\Models; use Gravatar; use Illuminate\Auth\Authenticatable; @@ -29,11 +29,46 @@ 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; -class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable +/** + * Poniverse\Ponyfm\Models\User + * + * @property integer $id + * @property string $display_name + * @property string $username + * @property boolean $sync_names + * @property string $email + * @property string $gravatar + * @property string $slug + * @property boolean $uses_gravatar + * @property boolean $can_see_explicit_content + * @property string $bio + * @property integer $track_count + * @property integer $comment_count + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * @property integer $avatar_id + * @property string $remember_token + * @property boolean $is_archived + * @property \Carbon\Carbon $disabled_at + * @property-read \Poniverse\Ponyfm\Models\Image $avatar + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Role[] $roles + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments + * @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks + * @property-read mixed $url + * @property-read mixed $message_url + * @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, Searchable { - use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait; + use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait, IndexedInElasticsearchTrait; + + protected $elasticsearchType = 'user'; protected $table = 'users'; protected $casts = [ @@ -46,12 +81,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon 'avatar_id' => 'integer', 'is_archived' => 'boolean', ]; + protected $dates = ['created_at', 'updated_at', 'disabled_at']; + protected $hidden = ['disabled_at']; public function scopeUserDetails($query) { if (Auth::check()) { $query->with([ - 'users' => function ($query) { + 'users' => function($query) { $query->whereUserId(Auth::user()->id); } ]); @@ -89,12 +126,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function avatar() { - return $this->belongsTo('Poniverse\Ponyfm\Image'); + return $this->belongsTo('Poniverse\Ponyfm\Models\Image'); } public function users() { - return $this->hasMany('Poniverse\Ponyfm\ResourceUser', 'artist_id'); + return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser', 'artist_id'); } public function roles() @@ -104,17 +141,17 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function comments() { - return $this->hasMany('Poniverse\Ponyfm\Comment', 'profile_id')->orderBy('created_at', 'desc'); + return $this->hasMany('Poniverse\Ponyfm\Models\Comment', 'profile_id')->orderBy('created_at', 'desc'); } public function tracks() { - return $this->hasMany('Poniverse\Ponyfm\Track', 'user_id'); + return $this->hasMany('Poniverse\Ponyfm\Models\Track', 'user_id'); } public function getIsArchivedAttribute() { - return (bool)$this->attributes['is_archived']; + return (bool) $this->attributes['is_archived']; } public function getUrlAttribute() @@ -124,7 +161,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMessageUrlAttribute() { - return 'http://mlpforums.com/index.php?app=members&module=messaging§ion=send&do=form&fromMemberID=' . $this->id; + return 'http://mlpforums.com/index.php?app=members&module=messaging§ion=send&do=form&fromMemberID='.$this->id; } public function getAuthIdentifier() @@ -155,7 +192,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon } if ($this->email == "redacted@example.net") { - return Gravatar::getUrl($this->id . "", Image::$ImageTypes[$type]['width'], "identicon"); + return Gravatar::getUrl($this->id."", Image::$ImageTypes[$type]['width'], "identicon"); } $email = $this->gravatar; @@ -201,10 +238,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon /** * Returns true if this user has the given role. * - * @param $roleName + * @param string $roleName * @return bool */ - public function hasRole($roleName) + public function hasRole($roleName):bool { foreach ($this->roles as $role) { if ($role->name === $roleName) { @@ -214,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; + } } diff --git a/app/PlaylistDownloader.php b/app/PlaylistDownloader.php index a145c906..e9764dee 100644 --- a/app/PlaylistDownloader.php +++ b/app/PlaylistDownloader.php @@ -20,11 +20,19 @@ namespace Poniverse\Ponyfm; +use Poniverse\Ponyfm\Models\Playlist; use ZipStream; class PlaylistDownloader { + /** + * @var Playlist + */ private $_playlist; + + /** + * @var string + */ private $_format; function __construct($playlist, $format) @@ -35,23 +43,23 @@ class PlaylistDownloader function download() { - $zip = new ZipStream($this->_playlist->user->display_name . ' - ' . $this->_playlist->title . '.zip'); + $zip = new ZipStream($this->_playlist->user->display_name.' - '.$this->_playlist->title.'.zip'); $zip->setComment( - 'Playlist: ' . $this->_playlist->title . "\r\n" . - 'Curator: ' . $this->_playlist->user->display_name . "\r\n" . - 'URL: ' . $this->_playlist->url . "\r\n" . "\r\n" . - 'Downloaded on ' . date('l, F jS, Y, \a\t h:i:s A') . '.' + 'Playlist: '.$this->_playlist->title."\r\n". + 'Curator: '.$this->_playlist->user->display_name."\r\n". + 'URL: '.$this->_playlist->url."\r\n"."\r\n". + 'Downloaded on '.date('l, F jS, Y, \a\t h:i:s A').'.' ); $notes = - 'Playlist: ' . $this->_playlist->title . "\r\n" . - 'Curator: ' . $this->_playlist->user->display_name . "\r\n" . - 'URL: ' . $this->_playlist->url . "\r\n" . - "\r\n" . - $this->_playlist->description . "\r\n" . - "\r\n" . - "\r\n" . - 'Tracks' . "\r\n" . + 'Playlist: '.$this->_playlist->title."\r\n". + 'Curator: '.$this->_playlist->user->display_name."\r\n". + 'URL: '.$this->_playlist->url."\r\n". + "\r\n". + $this->_playlist->description."\r\n". + "\r\n". + "\r\n". + 'Tracks'."\r\n". "\r\n"; $m3u = ''; @@ -61,22 +69,22 @@ class PlaylistDownloader continue; } - $trackTarget = $track->downloadDirectory . '/' . $track->getDownloadFilenameFor($this->_format); + $trackTarget = $track->downloadDirectory.'/'.$track->getDownloadFilenameFor($this->_format); $zip->addLargeFile($track->getFileFor($this->_format), $trackTarget); $notes .= - $index . '. ' . $track->title . "\r\n" . - $track->description . "\r\n" . + $index.'. '.$track->title."\r\n". + $track->description."\r\n". "\r\n"; - $m3u .= '#EXTINF:' . $track->duration . ',' . $track->title . "\r\n"; - $m3u .= '../' . $trackTarget . "\r\n"; + $m3u .= '#EXTINF:'.$track->duration.','.$track->title."\r\n"; + $m3u .= '../'.$trackTarget."\r\n"; $index++; } $playlistDir = 'Pony.fm Playlists/'; - $zip->addFile($notes, $playlistDir . $this->_playlist->title . '.txt'); - $zip->addFile($m3u, $playlistDir . $this->_playlist->title . '.m3u'); + $zip->addFile($notes, $playlistDir.$this->_playlist->title.'.txt'); + $zip->addFile($m3u, $playlistDir.$this->_playlist->title.'.m3u'); $zip->finalize(); } } diff --git a/app/Http/Controllers/FavouritesController.php b/app/Policies/AlbumPolicy.php similarity index 64% rename from app/Http/Controllers/FavouritesController.php rename to app/Policies/AlbumPolicy.php index 54ffd501..a5836e65 100644 --- a/app/Http/Controllers/FavouritesController.php +++ b/app/Policies/AlbumPolicy.php @@ -2,7 +2,7 @@ /** * Pony.fm - A community for pony fan music. - * Copyright (C) 2015 Peter Deltchev + * 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 @@ -18,24 +18,18 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm\Http\Controllers; +namespace Poniverse\Ponyfm\Policies; -use View; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\User; -class FavouritesController extends Controller +class AlbumPolicy { - public function getTracks() - { - return View::make('shared.null'); + public function edit(User $user, Album $album) { + return $user->id === $album->user_id || $user->hasRole('admin'); } - public function getAlbums() - { - return View::make('shared.null'); - } - - public function getPlaylists() - { - return View::make('shared.null'); + public function delete(User $user, Album $album) { + return $user->id === $album->user_id || $user->hasRole('admin'); } } diff --git a/app/Policies/GenrePolicy.php b/app/Policies/GenrePolicy.php index 54e07780..711149a6 100644 --- a/app/Policies/GenrePolicy.php +++ b/app/Policies/GenrePolicy.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Policies; -use Poniverse\Ponyfm\Genre; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\Genre; +use Poniverse\Ponyfm\Models\User; class GenrePolicy { diff --git a/app/Policies/TrackPolicy.php b/app/Policies/TrackPolicy.php index c137954a..7b788564 100644 --- a/app/Policies/TrackPolicy.php +++ b/app/Policies/TrackPolicy.php @@ -20,8 +20,8 @@ namespace Poniverse\Ponyfm\Policies; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; class TrackPolicy { diff --git a/app/Library/PFMAuth.php b/app/Policies/UserPolicy.php similarity index 57% rename from app/Library/PFMAuth.php rename to app/Policies/UserPolicy.php index d5e98417..7fc5fd53 100644 --- a/app/Library/PFMAuth.php +++ b/app/Policies/UserPolicy.php @@ -2,7 +2,7 @@ /** * Pony.fm - A community for pony fan music. - * Copyright (C) 2015 Peter Deltchev + * 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 @@ -18,28 +18,21 @@ * along with this program. If not, see . */ -use Illuminate\Auth\EloquentUserProvider; -use Illuminate\Contracts\Hashing\Hasher; +namespace Poniverse\Ponyfm\Policies; -class NullHasher implements Hasher +use Poniverse\Ponyfm\Models\User; + +class UserPolicy { - public function make($value, array $options = array()) - { + public function getAlbums(User $userToAuthorize, User $user) { + return $userToAuthorize->id === $user->id || $userToAuthorize->hasRole('admin'); } - public function check($value, $hashedValue, array $options = array()) - { + public function getImages(User $userToAuthorize, User $user) { + return $userToAuthorize->id === $user->id || $userToAuthorize->hasRole('admin'); } - public function needsRehash($hashedValue, array $options = array()) - { - } -} - -class PFMAuth extends EloquentUserProvider -{ - function __construct() - { - parent::__construct(new NullHasher(), 'Poniverse\Ponyfm\User'); + public function edit(User $userToAuthorize, User $user) { + return $userToAuthorize->id === $user->id; } } diff --git a/app/ProfileRequest.php b/app/ProfileRequest.php deleted file mode 100644 index 8c10e98e..00000000 --- a/app/ProfileRequest.php +++ /dev/null @@ -1,97 +0,0 @@ -. - */ - -namespace Poniverse\Ponyfm; - -use DB; - -class ProfileRequest -{ - private $_id = null; - private $_data = null; - - public static function load($data) - { - $req = new ProfileRequest(); - $req->_data = json_decode($data); - - return $req; - } - - /** - * @return ProfileRequest - */ - public static function create() - { - $req = new ProfileRequest(); - $req->_id = uniqid(); - - return $req; - } - - private function __construct() - { - $this->_data = ['log' => []]; - } - - public function toArray() - { - return $this->_data; - } - - public function toString() - { - return json_encode($this->_data); - } - - public function getId() - { - return $this->_id; - } - - public function recordQueries() - { - $this->_data['queries'] = []; - - foreach (DB::getQueryLog() as $query) { - if (starts_with($query['query'], 'select * from `cache` where')) { - continue; - } - - if (starts_with($query['query'], 'delete from `cache` where')) { - continue; - } - - if (starts_with($query['query'], 'insert into `cache`')) { - continue; - } - - $this->_data['queries'][] = $query; - } - } - - public function log($level, $message, $context) - { - $this->_data['log'][] = [ - 'level' => $level, - 'message' => $message - ]; - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index df875e1a..58cc94cf 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -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') + ); + }); } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 5367a7ad..7656ba92 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -22,11 +22,14 @@ namespace Poniverse\Ponyfm\Providers; use Illuminate\Contracts\Auth\Access\Gate as GateContract; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; -use Poniverse\Ponyfm\Genre; +use Poniverse\Ponyfm\Models\Album; +use Poniverse\Ponyfm\Models\Genre; +use Poniverse\Ponyfm\Policies\AlbumPolicy; use Poniverse\Ponyfm\Policies\GenrePolicy; use Poniverse\Ponyfm\Policies\TrackPolicy; -use Poniverse\Ponyfm\Track; -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\Track; +use Poniverse\Ponyfm\Models\User; +use Poniverse\Ponyfm\Policies\UserPolicy; class AuthServiceProvider extends ServiceProvider { @@ -38,6 +41,8 @@ class AuthServiceProvider extends ServiceProvider protected $policies = [ Genre::class => GenrePolicy::class, Track::class => TrackPolicy::class, + Album::class => AlbumPolicy::class, + User::class => UserPolicy::class, ]; /** @@ -52,6 +57,10 @@ class AuthServiceProvider extends ServiceProvider return $user->hasRole('admin'); }); + $gate->define('create-genre', function(User $user) { + return $user->hasRole('admin'); + }); + $this->registerPolicies($gate); } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 6c785098..39111b4f 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -22,6 +22,7 @@ namespace Poniverse\Ponyfm\Providers; use Illuminate\Routing\Router; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; +use Poniverse\Ponyfm\Models\User; class RouteServiceProvider extends ServiceProvider { @@ -42,9 +43,9 @@ class RouteServiceProvider extends ServiceProvider */ public function boot(Router $router) { - // - parent::boot($router); + + $router->model('userId', User::class); } /** diff --git a/app/Traits/IndexedInElasticsearchTrait.php b/app/Traits/IndexedInElasticsearchTrait.php new file mode 100644 index 00000000..8254521a --- /dev/null +++ b/app/Traits/IndexedInElasticsearchTrait.php @@ -0,0 +1,111 @@ +. + */ + +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(); + } + } +} diff --git a/app/Traits/TrackCollection.php b/app/Traits/TrackCollection.php index ec8f15c9..9b4bd938 100644 --- a/app/Traits/TrackCollection.php +++ b/app/Traits/TrackCollection.php @@ -26,7 +26,7 @@ use File; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\Relation; use Poniverse\Ponyfm\Jobs\EncodeTrackFile; -use Poniverse\Ponyfm\TrackFile; +use Poniverse\Ponyfm\Models\TrackFile; /** @@ -102,7 +102,6 @@ trait TrackCollection foreach ($trackFiles as $trackFile) { /** @var TrackFile $trackFile */ - if (!File::exists($trackFile->getFile()) && $trackFile->status == TrackFile::STATUS_NOT_BEING_PROCESSED) { $this->dispatch(new EncodeTrackFile($trackFile, true)); } diff --git a/composer.json b/composer.json index e1aa7a46..3ac3ffbd 100644 --- a/composer.json +++ b/composer.json @@ -8,13 +8,15 @@ "php": ">=7.0.1", "laravel/framework": "5.1.*", "codescale/ffmpeg-php": "2.7.0", - "kriswallsmith/assetic": "1.2.*@dev", "intouch/laravel-newrelic": "*", "barryvdh/laravel-ide-helper": "^2.1", "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", + "barryvdh/laravel-debugbar": "^2.1", + "predis/predis": "^1.0" }, "require-dev": { "fzaninotto/faker": "~1.4", diff --git a/composer.lock b/composer.lock index a215e567..e4f495c5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3bd61c88ad775531ffc26d83323d4598", - "content-hash": "be4e60cde6dc921af9be5a5fb94ac4e3", + "hash": "a12fe5f5687e21aac441868ccc882fa2", + "content-hash": "95039400d11d1851dabe46e4c0456965", "packages": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "974fd16e328ca851a081449100d9509af59cf0ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/974fd16e328ca851a081449100d9509af59cf0ff", + "reference": "974fd16e328ca851a081449100d9509af59cf0ff", + "shasum": "" + }, + "require": { + "illuminate/support": "~5.0.17|5.1.*|5.2.*", + "maximebf/debugbar": "~1.11.0", + "php": ">=5.4.0", + "symfony/finder": "~2.6|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2015-12-22 06:22:38" + }, { "name": "barryvdh/laravel-ide-helper", "version": "v2.1.2", @@ -168,6 +222,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 +430,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 +496,7 @@ "cache", "caching" ], - "time": "2015-12-19 05:03:47" + "time": "2015-12-31 16:37:02" }, { "name": "doctrine/collections", @@ -463,16 +566,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 +584,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 +635,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 +706,7 @@ "persistence", "queryobject" ], - "time": "2015-09-16 16:29:33" + "time": "2016-01-05 22:11:12" }, { "name": "doctrine/inflector", @@ -726,6 +829,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 +1054,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", @@ -1127,95 +1385,18 @@ ], "time": "2015-12-05 17:17:57" }, - { - "name": "kriswallsmith/assetic", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/kriswallsmith/assetic.git", - "reference": "5bea57990f7ddbeb3d57e7ef96eae9ddd56c4297" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kriswallsmith/assetic/zipball/5bea57990f7ddbeb3d57e7ef96eae9ddd56c4297", - "reference": "5bea57990f7ddbeb3d57e7ef96eae9ddd56c4297", - "shasum": "" - }, - "require": { - "php": ">=5.3.1", - "symfony/process": "~2.1|~3.0" - }, - "conflict": { - "twig/twig": "<1.23" - }, - "require-dev": { - "cssmin/cssmin": "*", - "joliclic/javascript-packer": "*", - "kamicane/packager": "*", - "leafo/lessphp": "*", - "leafo/scssphp": "*", - "leafo/scssphp-compass": "*", - "mrclay/minify": "*", - "patchwork/jsqueeze": "*", - "phpunit/phpunit": "~3.7", - "psr/log": "~1.0", - "ptachoire/cssembed": "*", - "twig/twig": "~1.6" - }, - "suggest": { - "leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler", - "leafo/scssphp": "Assetic provides the integration with the scssphp SCSS compiler", - "leafo/scssphp-compass": "Assetic provides the integration with the SCSS compass plugin", - "patchwork/jsqueeze": "Assetic provides the integration with the JSqueeze JavaScript compressor", - "ptachoire/cssembed": "Assetic provides the integration with phpcssembed to embed data uris", - "twig/twig": "Assetic provides the integration with the Twig templating engine" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - }, - "autoload": { - "psr-0": { - "Assetic": "src/" - }, - "files": [ - "src/functions.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kris Wallsmith", - "email": "kris.wallsmith@gmail.com", - "homepage": "http://kriswallsmith.net/" - } - ], - "description": "Asset Management for PHP", - "homepage": "https://github.com/kriswallsmith/assetic", - "keywords": [ - "assets", - "compression", - "minification" - ], - "time": "2014-07-08 11:30:40" - }, { "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": { @@ -1330,7 +1511,7 @@ "framework", "laravel" ], - "time": "2015-12-17 20:35:38" + "time": "2015-12-31 17:41:30" }, { "name": "league/flysystem", @@ -1416,6 +1597,67 @@ ], "time": "2015-12-19 20:16:43" }, + { + "name": "maximebf/debugbar", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "07741d84d39d10f00551c94284cdefcc69703e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/07741d84d39d10f00551c94284cdefcc69703e77", + "reference": "07741d84d39d10f00551c94284cdefcc69703e77", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "^1.0", + "symfony/var-dumper": "^2.6|^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0|^5.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "time": "2015-12-10 09:50:24" + }, { "name": "monolog/monolog", "version": "1.17.2", @@ -1637,16 +1879,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": { @@ -1681,7 +1923,7 @@ "pseudorandom", "random" ], - "time": "2015-12-10 14:48:13" + "time": "2016-01-06 13:31:20" }, { "name": "pda/pheanstalk", @@ -1782,6 +2024,56 @@ ], "time": "2015-02-03 12:10:50" }, + { + "name": "predis/predis", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/nrk/predis.git", + "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nrk/predis/zipball/84060b9034d756b4d79641667d7f9efe1aeb8e04", + "reference": "84060b9034d756b4d79641667d7f9efe1aeb8e04", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "ext-curl": "Allows access to Webdis when paired with phpiredis", + "ext-phpiredis": "Allows faster serialization and deserialization of the Redis protocol" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniele Alessandri", + "email": "suppakilla@gmail.com", + "homepage": "http://clorophilla.net" + } + ], + "description": "Flexible and feature-complete PHP client library for Redis", + "homepage": "http://github.com/nrk/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "time": "2015-07-30 18:34:15" + }, { "name": "psr/http-message", "version": "1.0", @@ -1941,6 +2233,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", @@ -1996,16 +2332,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": { @@ -2044,20 +2380,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": { @@ -2103,20 +2439,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": { @@ -2156,20 +2492,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": { @@ -2213,20 +2549,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": { @@ -2268,20 +2604,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": { @@ -2328,20 +2664,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": { @@ -2377,20 +2713,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": { @@ -2432,20 +2768,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": { @@ -2514,20 +2850,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": { @@ -2570,11 +2906,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", @@ -2626,16 +2962,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": { @@ -2671,20 +3007,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": { @@ -2744,20 +3080,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": { @@ -2807,20 +3143,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": { @@ -2866,20 +3202,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": { @@ -2914,7 +3250,7 @@ "model", "revision" ], - "time": "2015-12-09 21:48:10" + "time": "2016-01-13 12:14:05" }, { "name": "vlucas/phpdotenv", @@ -3216,16 +3552,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": { @@ -3290,7 +3626,7 @@ "testing", "tests" ], - "time": "2015-11-29 02:03:49" + "time": "2016-01-01 10:17:54" }, { "name": "phpspec/prophecy", @@ -4093,16 +4429,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": { @@ -4138,18 +4474,16 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2015-11-30 12:36:17" + "time": "2015-12-26 13:39:53" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "kriswallsmith/assetic": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.5.9" + "php": ">=7.0.1" }, "platform-dev": [] } diff --git a/config/app.php b/config/app.php index 8fcbfdd7..65f0e0f4 100644 --- a/config/app.php +++ b/config/app.php @@ -147,6 +147,8 @@ return [ Intouch\LaravelNewrelic\NewrelicServiceProvider::class, Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class, + Cviebrock\LaravelElasticsearch\ServiceProvider::class, + Barryvdh\Debugbar\ServiceProvider::class, ], @@ -197,6 +199,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, ], diff --git a/config/auth.php b/config/auth.php index c9bf4000..4aa4ab65 100644 --- a/config/auth.php +++ b/config/auth.php @@ -28,7 +28,7 @@ return [ | */ - 'model' => Poniverse\Ponyfm\User::class, + 'model' => Poniverse\Ponyfm\Models\User::class, /* |-------------------------------------------------------------------------- diff --git a/config/debugbar.php b/config/debugbar.php new file mode 100644 index 00000000..4b942c51 --- /dev/null +++ b/config/debugbar.php @@ -0,0 +1,169 @@ + null, + + /* + |-------------------------------------------------------------------------- + | Storage settings + |-------------------------------------------------------------------------- + | + | DebugBar stores data for session/ajax requests. + | You can disable this, so the debugbar stores data in headers/session, + | but this can cause problems with large data collectors. + | By default, file storage (in the storage folder) is used. Redis and PDO + | can also be used. For PDO, run the package migrations first. + | + */ + 'storage' => array( + 'enabled' => true, + 'driver' => 'redis', // redis, file, pdo + 'path' => storage_path() . '/debugbar', // For file driver + 'connection' => null, // Leave null for default connection (Redis/PDO) + ), + + /* + |-------------------------------------------------------------------------- + | Vendors + |-------------------------------------------------------------------------- + | + | Vendor files are included by default, but can be set to false. + | This can also be set to 'js' or 'css', to only include javascript or css vendor files. + | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + | and for js: jquery and and highlight.js + | So if you want syntax highlighting, set it to true. + | jQuery is set to not conflict with existing jQuery scripts. + | + */ + + 'include_vendors' => true, + + /* + |-------------------------------------------------------------------------- + | Capture Ajax Requests + |-------------------------------------------------------------------------- + | + | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + | you can use this option to disable sending the data through the headers. + | + */ + + 'capture_ajax' => true, + + /* + |-------------------------------------------------------------------------- + | Clockwork integration + |-------------------------------------------------------------------------- + | + | The Debugbar can emulate the Clockwork headers, so you can use the Chrome + | Extension, without the server-side code. It uses Debugbar collectors instead. + | + */ + 'clockwork' => false, + + /* + |-------------------------------------------------------------------------- + | DataCollectors + |-------------------------------------------------------------------------- + | + | Enable/disable DataCollectors + | + */ + + 'collectors' => array( + 'phpinfo' => true, // Php version + 'messages' => true, // Messages + 'time' => true, // Time Datalogger + 'memory' => true, // Memory usage + 'exceptions' => true, // Exception displayer + 'log' => true, // Logs from Monolog (merged in messages if enabled) + 'db' => true, // Show database (PDO) queries and bindings + 'views' => true, // Views with their data + 'route' => true, // Current route information + 'laravel' => false, // Laravel version and environment + 'events' => false, // All events fired + 'default_request' => false, // Regular or special Symfony request logger + 'symfony_request' => true, // Only one can be enabled.. + 'mail' => true, // Catch mail messages + 'logs' => false, // Add the latest log messages + 'files' => false, // Show the included files + 'config' => false, // Display config settings + 'auth' => false, // Display Laravel authentication status + 'gate' => false, // Display Laravel Gate checks + 'session' => true, // Display session data + ), + + /* + |-------------------------------------------------------------------------- + | Extra options + |-------------------------------------------------------------------------- + | + | Configure some DataCollectors + | + */ + + 'options' => array( + 'auth' => array( + 'show_name' => false, // Also show the users name/email in the debugbar + ), + 'db' => array( + 'with_params' => true, // Render SQL with the parameters substituted + 'timeline' => false, // Add the queries to the timeline + 'backtrace' => false, // EXPERIMENTAL: Use a backtrace to find the origin of the query in your files. + 'explain' => array( // EXPERIMENTAL: Show EXPLAIN output on queries + 'enabled' => false, + 'types' => array('SELECT'), // array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); for MySQL 5.6.3+ + ), + 'hints' => true, // Show hints for common mistakes + ), + 'mail' => array( + 'full_log' => false + ), + 'views' => array( + 'data' => false, //Note: Can slow down the application, because the data can be quite large.. + ), + 'route' => array( + 'label' => true // show complete route on bar + ), + 'logs' => array( + 'file' => null + ), + ), + + /* + |-------------------------------------------------------------------------- + | Inject Debugbar in Response + |-------------------------------------------------------------------------- + | + | Usually, the debugbar is added just before , by listening to the + | Response after the App is done. If you disable this, you have to add them + | in your template yourself. See http://phpdebugbar.com/docs/rendering.html + | + */ + + 'inject' => true, + + /* + |-------------------------------------------------------------------------- + | DebugBar route prefix + |-------------------------------------------------------------------------- + | + | Sometimes you want to set route prefix to be used by DebugBar to load + | its resources from. Usually the need comes from misconfigured web server or + | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + | + */ + 'route_prefix' => '_debugbar', + +); diff --git a/config/elasticsearch.php b/config/elasticsearch.php new file mode 100644 index 00000000..182f2a52 --- /dev/null +++ b/config/elasticsearch.php @@ -0,0 +1,168 @@ + '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, + + ] + ] + +]; diff --git a/config/ide-helper.php b/config/ide-helper.php new file mode 100644 index 00000000..8991b930 --- /dev/null +++ b/config/ide-helper.php @@ -0,0 +1,119 @@ + '_ide_helper', + 'format' => 'php', + + /* + |-------------------------------------------------------------------------- + | Helper files to include + |-------------------------------------------------------------------------- + | + | Include helper files. By default not included, but can be toggled with the + | -- helpers (-H) option. Extra helper files can be included. + | + */ + + 'include_helpers' => false, + + 'helper_files' => array( + base_path().'/vendor/laravel/framework/src/Illuminate/Support/helpers.php', + ), + + /* + |-------------------------------------------------------------------------- + | Model locations to include + |-------------------------------------------------------------------------- + | + | Define in which directories the ide-helper:models command should look + | for models. + | + */ + + 'model_locations' => array( + 'app/Models', + ), + + + /* + |-------------------------------------------------------------------------- + | Extra classes + |-------------------------------------------------------------------------- + | + | These implementations are not really extended, but called with magic functions + | + */ + + 'extra' => array( + 'Eloquent' => array('Illuminate\Database\Eloquent\Builder', 'Illuminate\Database\Query\Builder'), + 'Session' => array('Illuminate\Session\Store'), + ), + + 'magic' => array( + 'Log' => array( + 'debug' => 'Monolog\Logger::addDebug', + 'info' => 'Monolog\Logger::addInfo', + 'notice' => 'Monolog\Logger::addNotice', + 'warning' => 'Monolog\Logger::addWarning', + 'error' => 'Monolog\Logger::addError', + 'critical' => 'Monolog\Logger::addCritical', + 'alert' => 'Monolog\Logger::addAlert', + 'emergency' => 'Monolog\Logger::addEmergency', + ) + ), + + /* + |-------------------------------------------------------------------------- + | Interface implementations + |-------------------------------------------------------------------------- + | + | These interfaces will be replaced with the implementing class. Some interfaces + | are detected by the helpers, others can be listed below. + | + */ + + 'interfaces' => array( + + ), + + /* + |-------------------------------------------------------------------------- + | Support for custom DB types + |-------------------------------------------------------------------------- + | + | This setting allow you to map any custom database type (that you may have + | created using CREATE TYPE statement or imported using database plugin + | / extension to a Doctrine type. + | + | Each key in this array is a name of the Doctrine2 DBAL Platform. Currently valid names are: + | 'postgresql', 'db2', 'drizzle', 'mysql', 'oracle', 'sqlanywhere', 'sqlite', 'mssql' + | + | This name is returned by getName() method of the specific Doctrine/DBAL/Platforms/AbstractPlatform descendant + | + | The value of the array is an array of type mappings. Key is the name of the custom type, + | (for example, "jsonb" from Postgres 9.4) and the value is the name of the corresponding Doctrine2 type (in + | our case it is 'json_array'. Doctrine types are listed here: + | http://doctrine-dbal.readthedocs.org/en/latest/reference/types.html + | + | So to support jsonb in your models when working with Postgres, just add the following entry to the array below: + | + | "postgresql" => array( + | "jsonb" => "json_array", + | ), + | + */ + 'custom_db_types' => array( + + ), + +); diff --git a/config/ponyfm.php b/config/ponyfm.php index 1470dcd4..c94ed5c1 100644 --- a/config/ponyfm.php +++ b/config/ponyfm.php @@ -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', + ]; diff --git a/config/queue.php b/config/queue.php index cf9b09da..2092ae44 100644 --- a/config/queue.php +++ b/config/queue.php @@ -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, - ], - ], /* diff --git a/config/services.php b/config/services.php index 4f128fbf..f8e89649 100644 --- a/config/services.php +++ b/config/services.php @@ -30,7 +30,7 @@ return [ ], 'stripe' => [ - 'model' => Poniverse\Ponyfm\User::class, + 'model' => Poniverse\Ponyfm\Models\User::class, 'key' => '', 'secret' => '', ], diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index b49765a0..6be92289 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -29,9 +29,9 @@ | */ -use Poniverse\Ponyfm\User; +use Poniverse\Ponyfm\Models\User; -$factory->define(Poniverse\Ponyfm\User::class, function (\Faker\Generator $faker) { +$factory->define(Poniverse\Ponyfm\Models\User::class, function (\Faker\Generator $faker) { return [ 'username' => $faker->userName, 'display_name' => $faker->userName, @@ -45,14 +45,14 @@ $factory->define(Poniverse\Ponyfm\User::class, function (\Faker\Generator $faker ]; }); -$factory->define(\Poniverse\Ponyfm\Track::class, function(\Faker\Generator $faker) { +$factory->define(\Poniverse\Ponyfm\Models\Track::class, function(\Faker\Generator $faker) { $user = factory(User::class)->create(); return [ 'user_id' => $user->id, 'hash' => $faker->md5, 'title' => $faker->sentence(5), - 'track_type_id' => \Poniverse\Ponyfm\TrackType::UNCLASSIFIED_TRACK, + 'track_type_id' => \Poniverse\Ponyfm\Models\TrackType::UNCLASSIFIED_TRACK, 'genre' => $faker->word, 'album' => $faker->sentence(5), 'track_number' => null, diff --git a/database/migrations/2013_09_23_031316_create_track_hashes.php b/database/migrations/2013_09_23_031316_create_track_hashes.php index d80a4ff2..13c87c28 100644 --- a/database/migrations/2013_09_23_031316_create_track_hashes.php +++ b/database/migrations/2013_09_23_031316_create_track_hashes.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Database\Migrations\Migration; class CreateTrackHashes extends Migration diff --git a/database/migrations/2014_05_28_071738_update_track_hash.php b/database/migrations/2014_05_28_071738_update_track_hash.php index 43c8fc0e..fd207ad1 100644 --- a/database/migrations/2014_05_28_071738_update_track_hash.php +++ b/database/migrations/2014_05_28_071738_update_track_hash.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Database\Migrations\Migration; class UpdateTrackHash extends Migration diff --git a/database/migrations/2015_05_25_011121_create_track_files_table.php b/database/migrations/2015_05_25_011121_create_track_files_table.php index 301d2e52..8f94cfc2 100644 --- a/database/migrations/2015_05_25_011121_create_track_files_table.php +++ b/database/migrations/2015_05_25_011121_create_track_files_table.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; use Illuminate\Database\Migrations\Migration; diff --git a/database/migrations/2015_10_28_162655_AddTrackFilesForDeletedTracks.php b/database/migrations/2015_10_28_162655_AddTrackFilesForDeletedTracks.php index 0763ef03..8c98055a 100644 --- a/database/migrations/2015_10_28_162655_AddTrackFilesForDeletedTracks.php +++ b/database/migrations/2015_10_28_162655_AddTrackFilesForDeletedTracks.php @@ -20,7 +20,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; -use Poniverse\Ponyfm\Track; +use Poniverse\Ponyfm\Models\Track; class AddTrackFilesForDeletedTracks extends Migration { diff --git a/app/Console/Commands/Inspire.php b/database/migrations/2015_12_29_152005_add_account_disabled_column.php similarity index 60% rename from app/Console/Commands/Inspire.php rename to database/migrations/2015_12_29_152005_add_account_disabled_column.php index a5253eea..347f9b79 100644 --- a/app/Console/Commands/Inspire.php +++ b/database/migrations/2015_12_29_152005_add_account_disabled_column.php @@ -18,34 +18,32 @@ * along with this program. If not, see . */ -namespace Poniverse\Ponyfm\Console\Commands; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; -use Illuminate\Console\Command; -use Illuminate\Foundation\Inspiring; - -class Inspire extends Command +class AddAccountDisabledColumn extends Migration { /** - * The name and signature of the console command. + * Run the migrations. * - * @var string + * @return void */ - protected $signature = 'inspire'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Display an inspiring quote'; - - /** - * Execute the console command. - * - * @return mixed - */ - public function handle() + public function up() { - $this->comment(PHP_EOL.Inspiring::quote().PHP_EOL); + Schema::table('users', function(Blueprint $table){ + $table->dateTime('disabled_at')->nullable()->index(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function(Blueprint $table){ + $table->dropColumn('disabled_at'); + }); } } diff --git a/database/migrations/2016_01_01_001340_update_model_namespaces_in_revisions.php b/database/migrations/2016_01_01_001340_update_model_namespaces_in_revisions.php new file mode 100644 index 00000000..eb47fa66 --- /dev/null +++ b/database/migrations/2016_01_01_001340_update_model_namespaces_in_revisions.php @@ -0,0 +1,47 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class UpdateModelNamespacesInRevisions extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + DB::table('revisions') + ->update(['revisionable_type' => DB::raw("replace(revisionable_type, 'Poniverse\\\\Ponyfm', 'Poniverse\\\\Ponyfm\\\\Models')")]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('revisions') + ->update(['revisionable_type' => DB::raw("replace(revisionable_type, 'Poniverse\\\\Ponyfm\\\\Models', 'Poniverse\\\\Ponyfm')")]); + } +} diff --git a/database/migrations/2016_01_06_123513_add_genre_timestamps.php b/database/migrations/2016_01_06_123513_add_genre_timestamps.php new file mode 100644 index 00000000..207e50b2 --- /dev/null +++ b/database/migrations/2016_01_06_123513_add_genre_timestamps.php @@ -0,0 +1,49 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class AddGenreTimestamps extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('genres', function(Blueprint $table) { + $table->nullableTimestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('genres', function(Blueprint $table) { + $table->dropTimestamps(); + }); + } +} diff --git a/database/migrations/2016_01_14_021607_setup_elasticsearch.php b/database/migrations/2016_01_14_021607_setup_elasticsearch.php new file mode 100644 index 00000000..8b0eac0a --- /dev/null +++ b/database/migrations/2016_01_14_021607_setup_elasticsearch.php @@ -0,0 +1,127 @@ +. + */ + +use Illuminate\Database\Migrations\Migration; + +class SetupElasticsearch extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + // As of 2016-02-21, Elasticsearch is not used in tests. + // Letting this migration run will blow away all data in it. + if (App::environment() === 'testing') { + return; + } + + $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() + { + // As of 2016-02-21, Elasticsearch is not used in tests. + // Letting this migration run will blow away all data in it. + if (App::environment() === 'testing') { + return; + } + + $elasticsearch = Elasticsearch::connection(); + + $elasticsearch->indices()->delete(['index' => 'ponyfm']); + } +} diff --git a/database/migrations/2016_01_23_062640_EnforceUniqueTracksInPlaylists.php b/database/migrations/2016_01_23_062640_EnforceUniqueTracksInPlaylists.php new file mode 100644 index 00000000..f33ae14f --- /dev/null +++ b/database/migrations/2016_01_23_062640_EnforceUniqueTracksInPlaylists.php @@ -0,0 +1,99 @@ +. + */ + +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; +use Poniverse\Ponyfm\Models\Playlist; + +class EnforceUniqueTracksInPlaylists extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + DB::transaction(function(){ + $playlistIds = DB::table('playlists')->lists('id'); + + foreach ($playlistIds as $playlistId) { + /** @var Playlist $playlist */ + + // Deletes all of a playlist's entries of a + // duplicate track except for the first one. + $ids = DB::select( + DB::raw( +<< 1 +) +ORDER BY position ASC +LIMIT 1,18446744073709551615 +EOF + ) + , [$playlistId, $playlistId]); + $ids = collect($ids)->pluck('id'); + + DB::table('playlist_track') + ->whereIn('id', $ids) + ->delete(); + + // Using this instead of $model->fresh(); because that + // doesn't deal with soft-deleted models. + $playlist = Playlist::with('tracks')->withTrashed()->find($playlistId); + + $position = 1; + foreach($playlist->tracks as $track) { + $track->pivot->position = $position; + $track->pivot->save(); + $position++; + } + } + }); + + Schema::table('playlist_track', function(Blueprint $table) { + $table->unique(['playlist_id', 'track_id']); + }); + + Artisan::call('refresh-cache'); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('playlist_track', function (Blueprint $table) { + $table->dropUnique('playlist_track_playlist_id_track_id_unique'); + }); + } +} diff --git a/gulpfile.js b/gulpfile.js index 5ee540b5..021c3417 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -17,9 +17,16 @@ */ var gulp = require("gulp"), + gutil = require("gulp-util"), plug = require("gulp-load-plugins")(), argv = require("yargs").argv, - header = require("gulp-header"); + header = require("gulp-header"), + webpack = require("webpack"), + WebpackDevServer = require("webpack-dev-server"), + webpackDevConfig = require("./webpack.dev.config.js"), + webpackProductionConfig = require("./webpack.production.config.js"), + webpackStream = require('webpack-stream'), + _ = require("underscore"); var plumberOptions = { errorHandler: plug.notify.onError("Error: <%= error.message %>") @@ -28,7 +35,7 @@ var plumberOptions = { var licenseHeader = [ "/**", "* Pony.fm - A community for pony fan music.", - "* Copyright (C) 2015 Peter Deltchev and others", + "* Copyright (C) 2016 Peter Deltchev and others", "*", "* 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", @@ -45,87 +52,33 @@ var licenseHeader = [ "*/", "", "" -].join('\n') +].join('\n'); -gulp.task("scripts-app", function () { - var paths = [ - "resources/assets/scripts/app/**/*.{coffee,js}", - "resources/assets/scripts/base/**/*.{coffee,js}", - "resources/assets/scripts/shared/**/*.{coffee,js}" - ]; - if (!argv.production) { - paths.push("resources/assets/scripts/debug/**/*.{coffee,js}"); - - // we also want to add the embed stuff, since we're in development mode - // we want to watch embed files and re-compile them. However, we want - // to leave this path out in production so that embed files are not bloating - // the js file - paths.push("resources/assets/scripts/embed/**/*.{coffee,js}"); - } - - return argv.production - // Production pipeline - ? gulp.src(paths, {base: "resources/assets/scripts"}) - .pipe(plug.plumber(plumberOptions)) - .pipe(plug.order([ - "resources/assets/scripts/base/jquery-2.0.2.js", - "resources/assets/scripts/base/angular.js", - "resources/assets/scripts/base/*.{coffee,js}", - "resources/assets/scripts/shared/*.{coffee,js}", - "resources/assets/scripts/app/*.{coffee,js}", - "resources/assets/scripts/app/services/*.{coffee,js}", - "resources/assets/scripts/app/filters/*.{coffee,js}", - "resources/assets/scripts/app/directives/*.{coffee,js}", - "resources/assets/scripts/app/controllers/*.{coffee,js}", - "resources/assets/scripts/**/*.{coffee,js}" - ], {base: "."})) - .pipe(plug.if(/\.coffee/, plug.coffee())) - .pipe(plug.concat("app.js")) - .pipe(plug.uglify()) +gulp.task("webpack-build", function() { + return gulp.src(_.values(webpackProductionConfig.entry)) + .pipe(webpackStream(webpackProductionConfig)) .pipe(header(licenseHeader)) - .pipe(gulp.dest("public/build/scripts")) - // Development/watch pipeline - : gulp.src(paths, {base: "resources/assets/scripts"}) - .pipe(plug.plumber(plumberOptions)) - .pipe(plug.cached('scripts')) - .pipe(plug.sourcemaps.init()) - .pipe(plug.if(/\.coffee/, plug.coffee())) - .pipe(plug.sourcemaps.write({ - includeContent: false, - sourceRoot: "/dev-scripts/" - })) - .pipe(header(licenseHeader)) - .pipe(gulp.dest("public/build/scripts")); + .pipe(gulp.dest('public')); }); -gulp.task("scripts-embed", function () { - // note that this task should really only ever be invoked for production - // since development-mode watches and builds include the embed scripts - // already - var includedScripts = [ - "resources/assets/scripts/base/jquery-2.0.2.js", - "resources/assets/scripts/base/jquery.cookie.js", - "resources/assets/scripts/base/jquery.viewport.js", - "resources/assets/scripts/base/underscore.js", - "resources/assets/scripts/base/moment.js", - "resources/assets/scripts/base/jquery.timeago.js", - "resources/assets/scripts/base/soundmanager2-nodebug.js", - "resources/assets/scripts/shared/jquery-extensions.js", - "resources/assets/scripts/embed/*.coffee" - ]; +gulp.task("webpack-dev-server", function () { + // Starts a webpack-dev-server + var compiler = webpack(webpackDevConfig); - return gulp.src(includedScripts, {base: "resources/assets/scripts"}) - .pipe(plug.plumber(plumberOptions)) - .pipe(plug.if(/\.coffee/, plug.coffee())) - .pipe(plug.order(includedScripts, {base: "."})) - .pipe(plug.concat("embed.js")) - .pipe(plug.uglify()) - .pipe(header(licenseHeader)) - .pipe(gulp.dest("public/build/scripts")); + new WebpackDevServer(compiler, { + // server and middleware options, currently blank + }).listen(61999, "localhost", function (err) { + if (err) + throw new gutil.PluginError("webpack-dev-server", err); + + // Server listening + gutil.log("[webpack-dev-server]", "http://localhost:61999/webpack-dev-server/index.html"); + }); }); + gulp.task("styles-app", function () { var includedStyles = [ "resources/assets/styles/base/jquery-ui.css", @@ -134,9 +87,6 @@ gulp.task("styles-app", function () { ]; if (!argv.production) { - includedStyles.push("resources/assets/styles/profiler.less"); - includedStyles.push("resources/assets/styles/prettify.css"); - // we also want to add the embed stuff, since we're in development mode // we want to watch embed files and re-compile them. However, we want // to leave this path out in production so that embed files are not bloating @@ -172,17 +122,15 @@ gulp.task("styles-app", function () { .pipe(plug.minifyCss()) .pipe(header(licenseHeader)) .pipe(gulp.dest("public/build/styles")) + // Development pipeline : gulp.src(includedStyles, {base: "resources/assets/styles"}) .pipe(plug.plumber(plumberOptions)) .pipe(plug.cached("styles")) .pipe(plug.sourcemaps.init()) .pipe(plug.if(/\.less/, plug.less())) - .pipe(plug.sourcemaps.write({ - includeContent: false, - sourceRoot: "/dev-styles/" - })) .pipe(header(licenseHeader)) + .pipe(plug.sourcemaps.write()) .pipe(gulp.dest("public/build/styles")) .pipe(plug.livereload()); }); @@ -217,18 +165,19 @@ gulp.task('copy:templates', function () { }); gulp.task('build', [ - 'scripts-app', + 'webpack-build', + 'copy:templates', 'styles-app', - 'scripts-embed', 'styles-embed' ]); -gulp.task("watch", function () { - plug.livereload.listen(); - gulp.watch("resources/assets/scripts/**/*.{coffee,js}", ["scripts-app"]); +gulp.task("watch-legacy", ["build"], function () { gulp.watch("resources/assets/styles/**/*.{css,less}", ["styles-app"]); }); +gulp.task("watch", ["webpack-dev-server", "watch-legacy"], function () {}); + + function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } diff --git a/package.json b/package.json index c8ff04dd..32c2ef9d 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,24 @@ { "name": "pony.fm", "version": "1.0.0", + "license": "AGPL-3.0", "repository": { "type": "git", "url": "ssh://git@phabricator.poniverse.net/diffusion/PF/pony-fm.git" }, "packages": {}, - "dependencies": { - "gulp-header": "^1.7.1" - }, + "dependencies": {}, "devDependencies": { + "angular": "^1.5.0", + "angular-ui-router": "^0.2.18", + "coffee-loader": "^0.7.2", + "coffee-script": "^1.10.0", "gulp": "^3.9.0", "gulp-angular-templatecache": "^1.6.0", "gulp-autoprefixer": "^2.2.0", "gulp-cached": "^1.0.4", - "gulp-coffee": "^2.3.1", "gulp-concat": "^2.5.2", + "gulp-header": "^1.7.1", "gulp-if": "^1.2.5", "gulp-less": "^3.0.3", "gulp-livereload": "^3.8.0", @@ -25,8 +28,15 @@ "gulp-order": "^1.1.1", "gulp-plumber": "^1.0.0", "gulp-sourcemaps": "^1.5.1", - "gulp-uglify": "^1.2.0", - "gulp-util": "^3.0.4", + "gulp-util": "^3.0.7", + "gulp-webpack": "^1.5.0", + "jquery": "^2.2.0", + "jquery-ui": "^1.10.5", + "script-loader": "^0.6.1", + "underscore": "^1.8.3", + "webpack": "^1.12.13", + "webpack-dev-server": "^1.14.1", + "webpack-stream": "^3.1.0", "yargs": "^3.7.2" } } diff --git a/phpunit.xml b/phpunit.xml index f6a7a5ca..f742d698 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,6 +21,7 @@ + diff --git a/public/.htaccess b/public/.htaccess deleted file mode 100644 index 5a35bec2..00000000 --- a/public/.htaccess +++ /dev/null @@ -1,23 +0,0 @@ - - - Options -MultiViews - - - RewriteEngine On - - # Redirect Trailing Slashes If Not A Folder... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteRule ^(.*)/$ /$1 [L,R=301] - - # Handle Front Controller... - RewriteCond %{REQUEST_FILENAME} !-d - RewriteCond %{REQUEST_FILENAME} !-f - RewriteRule ^ index.php [L] - - RewriteRule ^(.*\.(?:coffee))$ /asset.php?type=coffee&file=/$1 [L,QSA,NC] - RewriteRule ^(.*\.(?:less))$ /asset.php?type=less&file=/$1 [L,QSA,NC] - - - - XSendFile On - diff --git a/public/images/loading.gif b/public/images/loading.gif index 724eacb5..42ed62d8 100644 Binary files a/public/images/loading.gif and b/public/images/loading.gif differ diff --git a/resources/assets/styles/base/images/animated-overlay.gif b/public/styles/base/images/animated-overlay.gif similarity index 100% rename from resources/assets/styles/base/images/animated-overlay.gif rename to public/styles/base/images/animated-overlay.gif diff --git a/resources/assets/styles/base/images/border.png b/public/styles/base/images/border.png similarity index 100% rename from resources/assets/styles/base/images/border.png rename to public/styles/base/images/border.png diff --git a/resources/assets/styles/base/images/loading_background.png b/public/styles/base/images/loading_background.png similarity index 100% rename from resources/assets/styles/base/images/loading_background.png rename to public/styles/base/images/loading_background.png diff --git a/resources/assets/styles/base/images/overlay.png b/public/styles/base/images/overlay.png similarity index 100% rename from resources/assets/styles/base/images/overlay.png rename to public/styles/base/images/overlay.png diff --git a/resources/assets/styles/base/images/ui-bg_flat_0_aaaaaa_40x100.png b/public/styles/base/images/ui-bg_flat_0_aaaaaa_40x100.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_flat_0_aaaaaa_40x100.png rename to public/styles/base/images/ui-bg_flat_0_aaaaaa_40x100.png diff --git a/resources/assets/styles/base/images/ui-bg_flat_75_ffffff_40x100.png b/public/styles/base/images/ui-bg_flat_75_ffffff_40x100.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_flat_75_ffffff_40x100.png rename to public/styles/base/images/ui-bg_flat_75_ffffff_40x100.png diff --git a/resources/assets/styles/base/images/ui-bg_glass_55_fbf9ee_1x400.png b/public/styles/base/images/ui-bg_glass_55_fbf9ee_1x400.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_glass_55_fbf9ee_1x400.png rename to public/styles/base/images/ui-bg_glass_55_fbf9ee_1x400.png diff --git a/resources/assets/styles/base/images/ui-bg_glass_65_ffffff_1x400.png b/public/styles/base/images/ui-bg_glass_65_ffffff_1x400.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_glass_65_ffffff_1x400.png rename to public/styles/base/images/ui-bg_glass_65_ffffff_1x400.png diff --git a/resources/assets/styles/base/images/ui-bg_glass_75_dadada_1x400.png b/public/styles/base/images/ui-bg_glass_75_dadada_1x400.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_glass_75_dadada_1x400.png rename to public/styles/base/images/ui-bg_glass_75_dadada_1x400.png diff --git a/resources/assets/styles/base/images/ui-bg_glass_75_e6e6e6_1x400.png b/public/styles/base/images/ui-bg_glass_75_e6e6e6_1x400.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_glass_75_e6e6e6_1x400.png rename to public/styles/base/images/ui-bg_glass_75_e6e6e6_1x400.png diff --git a/resources/assets/styles/base/images/ui-bg_glass_95_fef1ec_1x400.png b/public/styles/base/images/ui-bg_glass_95_fef1ec_1x400.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_glass_95_fef1ec_1x400.png rename to public/styles/base/images/ui-bg_glass_95_fef1ec_1x400.png diff --git a/resources/assets/styles/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png b/public/styles/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png similarity index 100% rename from resources/assets/styles/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png rename to public/styles/base/images/ui-bg_highlight-soft_75_cccccc_1x100.png diff --git a/resources/assets/styles/base/images/ui-icons_222222_256x240.png b/public/styles/base/images/ui-icons_222222_256x240.png similarity index 100% rename from resources/assets/styles/base/images/ui-icons_222222_256x240.png rename to public/styles/base/images/ui-icons_222222_256x240.png diff --git a/resources/assets/styles/base/images/ui-icons_2e83ff_256x240.png b/public/styles/base/images/ui-icons_2e83ff_256x240.png similarity index 100% rename from resources/assets/styles/base/images/ui-icons_2e83ff_256x240.png rename to public/styles/base/images/ui-icons_2e83ff_256x240.png diff --git a/resources/assets/styles/base/images/ui-icons_454545_256x240.png b/public/styles/base/images/ui-icons_454545_256x240.png similarity index 100% rename from resources/assets/styles/base/images/ui-icons_454545_256x240.png rename to public/styles/base/images/ui-icons_454545_256x240.png diff --git a/resources/assets/styles/base/images/ui-icons_888888_256x240.png b/public/styles/base/images/ui-icons_888888_256x240.png similarity index 100% rename from resources/assets/styles/base/images/ui-icons_888888_256x240.png rename to public/styles/base/images/ui-icons_888888_256x240.png diff --git a/resources/assets/styles/base/images/ui-icons_cd0a0a_256x240.png b/public/styles/base/images/ui-icons_cd0a0a_256x240.png similarity index 100% rename from resources/assets/styles/base/images/ui-icons_cd0a0a_256x240.png rename to public/styles/base/images/ui-icons_cd0a0a_256x240.png diff --git a/public/templates/account/_layout.html b/public/templates/account/_layout.html index 60816d59..042b0ece 100644 --- a/public/templates/account/_layout.html +++ b/public/templates/account/_layout.html @@ -1,8 +1,19 @@ diff --git a/public/templates/account/album.html b/public/templates/account/album.html index 1b41c092..d2836506 100644 --- a/public/templates/account/album.html +++ b/public/templates/account/album.html @@ -22,25 +22,25 @@
- +
- Add Tracks + Add Tracks
    -
  • +
  • - {{track.title}} + {{::track.title}}
diff --git a/public/templates/account/albums.html b/public/templates/account/albums.html index 25518f84..8a2d90a0 100644 --- a/public/templates/account/albums.html +++ b/public/templates/account/albums.html @@ -1,7 +1,7 @@