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
' . 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
Genre | -- | # of tracks (including deleted) | -Actions | - -
---|---|---|---|
-
-
- {{ genre.errorMessage }}
-
- |
- - | {{ genre.track_count }} | -- - - - | -
Enter a genre name and press enter to create it!
+ + +Genre | ++ | # of tracks (including deleted) | +Actions | + +
---|---|---|---|
+
+
+ {{ genre.errorMessage }}
+
+ |
+ + | {{ genre.track_count }} | ++ + + + | +
This is an archived artist profile that's part of the MLP + Music Archive. If it's yours, email + feld0@pony.fm to claim it!
+
All Comments ({{resource.comments.length}})
---
-
+
-
+
--
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/templates/directives/track-editor.html b/public/templates/directives/track-editor.html
new file mode 100644
index 00000000..d8cf5a82
--- /dev/null
+++ b/public/templates/directives/track-editor.html
@@ -0,0 +1,127 @@
+
diff --git a/public/templates/directives/track-player.html b/public/templates/directives/track-player.html
index aadf3ecf..27dbf1b7 100644
--- a/public/templates/directives/track-player.html
+++ b/public/templates/directives/track-player.html
@@ -1,7 +1,7 @@
-
-
+
+
-
+
diff --git a/public/templates/directives/tracks-list.html b/public/templates/directives/tracks-list.html
index e45ae81b..dcb4416b 100644
--- a/public/templates/directives/tracks-list.html
+++ b/public/templates/directives/tracks-list.html
@@ -1,27 +1,29 @@
-
+-
+
+
- {{playlist.title}}
+ {{::playlist.title}}
- by {{playlist.user.name}}
+ by {{::playlist.user.name}}
-
-
-
+ {{::playlist.stats.favourites}}
+ {{::playlist.stats.comments}}
+ {{::playlist.stats.downloads}}
- -
- No playlists found...
+
-
+ No playlists found…
diff --git a/public/templates/directives/search.html b/public/templates/directives/search.html new file mode 100644 index 00000000..84a4e6d3 --- /dev/null +++ b/public/templates/directives/search.html @@ -0,0 +1,33 @@ +Type something to begin searching!
+ +Searching…
+ +Matching tracks
+Matching users
+Matching albums
+Matching playlists
+--
+
-
-
-
-
diff --git a/public/templates/favourites/albums.html b/public/templates/favourites/albums.html
deleted file mode 100644
index fb7e2ed1..00000000
--- a/public/templates/favourites/albums.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/public/templates/favourites/playlists.html b/public/templates/favourites/playlists.html
deleted file mode 100644
index 76615fb0..00000000
--- a/public/templates/favourites/playlists.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/public/templates/favourites/tracks.html b/public/templates/favourites/tracks.html
deleted file mode 100644
index 6f12f17a..00000000
--- a/public/templates/favourites/tracks.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/public/templates/pages/mlpforums-advertising-program.html b/public/templates/pages/mlpforums-advertising-program.html
index dd3b56dc..66ede1db 100644
--- a/public/templates/pages/mlpforums-advertising-program.html
+++ b/public/templates/pages/mlpforums-advertising-program.html
@@ -44,7 +44,7 @@
nothing but relevant results. This means Pony.fm's listeners spend less
time searching for your music and more time listening to it!
-
+-
-
-
+
+
-
-
+
+
+
+
-
- {{track.title}}
-
- {{track.stats.favourites}}f
- {{track.stats.comments}}c
- {{track.stats.plays}}p
+
+ {{::track.title}}
+
+ {{::track.stats.favourites}}f
+ {{::track.stats.comments}}c
+ {{::track.stats.plays}}p
- {{track.user.name}} / {{track.genre.name}}
+ {{::track.user.name}} / {{::track.genre.name}}
- -
- No tracks found...
+
-
+ No tracks found…
diff --git a/public/templates/directives/users-list.html b/public/templates/directives/users-list.html new file mode 100644 index 00000000..10335144 --- /dev/null +++ b/public/templates/directives/users-list.html @@ -0,0 +1,19 @@ ++-
+
+
+
+ {{::user.name}}
+
+ joined {{::user.created_at.date | momentFromNow}}
+
+
+ archived artist
+
+
+
+
+ -
+ No users found…
+
+
diff --git a/public/templates/favourites/_layout.html b/public/templates/favourites/_layout.html deleted file mode 100644 index 349aaefc..00000000 --- a/public/templates/favourites/_layout.html +++ /dev/null @@ -1,14 +0,0 @@ --- Tracks
- - Albums
- - Playlists
-
- -Pony.fm also proides you with features that sites like SoundCloud and +
Pony.fm also provides you with features that sites like SoundCloud and Bandcamp don't. With Pony.fm, you can offer streaming and unlimited downloads. You'll never run out of free downloads again!
diff --git a/public/templates/partials/auth/login.html b/public/templates/partials/auth/login.html index 171fe3a0..69f2731d 100644 --- a/public/templates/partials/auth/login.html +++ b/public/templates/partials/auth/login.html @@ -1,7 +1,7 @@