Merge branch 'master' into feature/track-removals

This commit is contained in:
Mihail-K 2016-03-20 15:10:17 -04:00
commit e63c18f6be
311 changed files with 6611 additions and 23058 deletions

View file

@ -33,13 +33,14 @@ these are smaller in scope and easier to tackle if you're unfamiliar with the co
Starting a dev environment 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 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 npm install
Finally, build all of the scripts by executing: Finally, to compile and serve the assets in real time, run the following (and leave it running while you develop):
gulp build
During development, you should make a point to run "gulp watch". You can do this simply by executing:
gulp watch gulp watch
This will watch and compile the `.less` and `.coffee` files in real time.
Configuring the servers 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. 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. 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`

14
Vagrantfile vendored
View file

@ -3,11 +3,12 @@ Vagrant.configure("2") do |config|
config.hostmanager.enabled = true config.hostmanager.enabled = true
config.hostmanager.manage_host = true config.hostmanager.manage_host = true
config.vm.box = 'laravel/homestead-7' config.vm.box = 'laravel/homestead'
config.vm.box_version = '0.2.1' config.vm.box_version = '0.4.2'
config.vm.provider "virtualbox" do |v| config.vm.provider "virtualbox" do |v|
v.cpus = 4 v.cpus = 4
v.memory = 2048 v.memory = 1024
end end
config.vm.define 'default' do |node| config.vm.define 'default' do |node|
@ -17,13 +18,8 @@ Vagrant.configure("2") do |config|
end end
config.vm.synced_folder ".", "/vagrant", type: "nfs" config.vm.synced_folder ".", "/vagrant", type: "nfs"
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.bindfs.bind_folder "/vagrant", "/vagrant"
config.vm.provision "shell", path: "vagrant/install.sh"
config.vm.provision "shell", path: "vagrant/copy-and-restart-configs.sh", run: "always" config.vm.provision "shell", path: "vagrant/copy-and-restart-configs.sh", run: "always"
end end

View file

@ -20,11 +20,19 @@
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm;
use Poniverse\Ponyfm\Models\Album;
use ZipStream; use ZipStream;
class AlbumDownloader class AlbumDownloader
{ {
/**
* @var Album
*/
private $_album; private $_album;
/**
* @var string
*/
private $_format; private $_format;
function __construct($album, $format) function __construct($album, $format)

View file

@ -20,14 +20,18 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Illuminate\Support\Facades\Auth; use Auth;
use Illuminate\Support\Facades\DB; use DB;
use Validator;
class AddTrackToPlaylistCommand extends CommandBase class AddTrackToPlaylistCommand extends CommandBase
{ {
/** @var Track */
private $_track; private $_track;
/** @var Playlist */
private $_playlist; private $_playlist;
function __construct($playlistId, $trackId) function __construct($playlistId, $trackId)
@ -52,10 +56,22 @@ class AddTrackToPlaylistCommand extends CommandBase
*/ */
public function execute() 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; $songIndex = $this->_playlist->tracks()->count() + 1;
$this->_playlist->tracks()->attach($this->_track, ['position' => $songIndex]); $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 . ')') 'track_count' => DB::raw('(SELECT COUNT(id) FROM playlist_track WHERE playlist_id = ' . $this->_playlist->id . ')')
]); ]);

View file

@ -20,8 +20,8 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;

View file

@ -20,11 +20,11 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Comment; use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;

View file

@ -0,0 +1,75 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\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!']);
}
}

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;

View file

@ -20,18 +20,21 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Illuminate\Support\Facades\Auth; use Auth;
class DeleteAlbumCommand extends CommandBase class DeleteAlbumCommand extends CommandBase
{ {
/** @var int */
private $_albumId; private $_albumId;
/** @var Album */
private $_album; private $_album;
function __construct($albumId) function __construct($albumId)
{ {
$this->_albumId = $albumId; $this->_albumId = $albumId;
$this->_album = ALbum::find($albumId); $this->_album = Album::find($albumId);
} }
/** /**

View file

@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Commands;
use Gate; use Gate;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Poniverse\Ponyfm\Genre; use Poniverse\Ponyfm\Models\Genre;
use Poniverse\Ponyfm\Jobs\DeleteGenre; use Poniverse\Ponyfm\Jobs\DeleteGenre;
use Validator; use Validator;

View file

@ -20,12 +20,15 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Illuminate\Support\Facades\Auth; use Auth;
class DeletePlaylistCommand extends CommandBase class DeletePlaylistCommand extends CommandBase
{ {
/** @var int */
private $_playlistId; private $_playlistId;
/** @var Playlist */
private $_playlist; private $_playlist;
function __construct($playlistId) function __construct($playlistId)

View file

@ -20,11 +20,15 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Track; use Gate;
use Poniverse\Ponyfm\Models\Track;
class DeleteTrackCommand extends CommandBase class DeleteTrackCommand extends CommandBase
{ {
/** @var int */
private $_trackId; private $_trackId;
/** @var Track */
private $_track; private $_track;
function __construct($trackId) function __construct($trackId)
@ -38,9 +42,7 @@ class DeleteTrackCommand extends CommandBase
*/ */
public function authorize() public function authorize()
{ {
$user = \Auth::user(); return Gate::allows('delete', $this->_track);
return $this->_track && $user != null && $this->_track->user_id == $user->id;
} }
/** /**

View file

@ -20,16 +20,18 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Illuminate\Support\Facades\Auth; use Auth;
use Illuminate\Support\Facades\DB; use DB;
use Illuminate\Support\Facades\Validator; use Validator;
class EditAlbumCommand extends CommandBase class EditAlbumCommand extends CommandBase
{ {
private $_input; private $_input;
/** @var int */
private $_albumId; private $_albumId;
/** @var Album */
private $_album; private $_album;
function __construct($trackId, $input) function __construct($trackId, $input)
@ -88,10 +90,6 @@ class EditAlbumCommand extends CommandBase
$this->_album->syncTrackIds($trackIds); $this->_album->syncTrackIds($trackIds);
$this->_album->save(); $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)]); return CommandResponse::succeed(['real_cover_url' => $this->_album->getCoverUrl(Image::NORMAL)]);
} }
} }

View file

@ -20,8 +20,8 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\PinnedPlaylist; use Poniverse\Ponyfm\Models\PinnedPlaylist;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;

View file

@ -20,11 +20,12 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Album; use Gate;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\TrackType; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\TrackType;
use Poniverse\Ponyfm\Models\User;
use Auth; use Auth;
use DB; use DB;
@ -46,9 +47,7 @@ class EditTrackCommand extends CommandBase
*/ */
public function authorize() public function authorize()
{ {
$user = \Auth::user(); return $this->_track && Gate::allows('edit', $this->_track);
return $this->_track && $user != null && $this->_track->user_id == $user->id;
} }
/** /**
@ -61,8 +60,11 @@ class EditTrackCommand extends CommandBase
$rules = [ $rules = [
'title' => 'required|min:3|max:80', 'title' => 'required|min:3|max:80',
'released_at' => 'before:' . (date('Y-m-d', 'released_at' => 'before:' .
time() + (86400 * 2))) . (isset($this->_input['released_at']) && $this->_input['released_at'] != "" ? '|date' : ''), (date('Y-m-d', time() + (86400 * 2))) . (
isset($this->_input['released_at']) && $this->_input['released_at'] != ""
? '|date'
: ''),
'license_id' => 'required|exists:licenses,id', 'license_id' => 'required|exists:licenses,id',
'genre_id' => 'required|exists:genres,id', 'genre_id' => 'required|exists:genres,id',
'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350', 'cover' => 'image|mimes:png,jpeg|min_width:350|min_height:350',
@ -140,7 +142,7 @@ class EditTrackCommand extends CommandBase
} else { } else {
if (isset($this->_input['cover'])) { if (isset($this->_input['cover'])) {
$cover = $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 { } else {
if ($this->_input['remove_cover'] == 'true') { if ($this->_input['remove_cover'] == 'true') {
$track->cover_id = null; $track->cover_id = null;

View file

@ -0,0 +1,187 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\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();
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\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();
}
}

View file

@ -0,0 +1,441 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\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;
}
}
}
}

View file

@ -21,12 +21,16 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Gate; use Gate;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Poniverse\Ponyfm\Genre; use Poniverse\Ponyfm\Jobs\UpdateTagsForRenamedGenre;
use Poniverse\Ponyfm\Models\Genre;
use Validator; use Validator;
class RenameGenreCommand extends CommandBase class RenameGenreCommand extends CommandBase
{ {
use DispatchesJobs;
/** @var Genre */ /** @var Genre */
private $_genre; private $_genre;
private $_newName; private $_newName;
@ -72,6 +76,8 @@ class RenameGenreCommand extends CommandBase
$this->_genre->slug = $slug; $this->_genre->slug = $slug;
$this->_genre->save(); $this->_genre->save();
$this->dispatch(new UpdateTagsForRenamedGenre($this->_genre));
return CommandResponse::succeed(['message' => 'Genre renamed!']); return CommandResponse::succeed(['message' => 'Genre renamed!']);
} }
} }

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;

View file

@ -20,8 +20,8 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Favourite; use Poniverse\Ponyfm\Models\Favourite;
use Poniverse\Ponyfm\ResourceUser; use Poniverse\Ponyfm\Models\ResourceUser;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;

View file

@ -20,8 +20,8 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Poniverse\Ponyfm\Follower; use Poniverse\Ponyfm\Models\Follower;
use Poniverse\Ponyfm\ResourceUser; use Poniverse\Ponyfm\Models\ResourceUser;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
class ToggleFollowingCommand extends CommandBase class ToggleFollowingCommand extends CommandBase

View file

@ -22,46 +22,30 @@ namespace Poniverse\Ponyfm\Commands;
use Carbon\Carbon; use Carbon\Carbon;
use Config; use Config;
use getID3;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Input; use Input;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Track;
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 AudioCache; use AudioCache;
use File; use Validator;
use Illuminate\Support\Str;
use Poniverse\Ponyfm\TrackType;
use Poniverse\Ponyfm\User;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class UploadTrackCommand extends CommandBase class UploadTrackCommand extends CommandBase
{ {
use DispatchesJobs; use DispatchesJobs;
private $_allowLossy; private $_allowLossy;
private $_allowShortTrack; private $_allowShortTrack;
private $_customTrackSource; private $_customTrackSource;
private $_autoPublishByDefault; private $_autoPublishByDefault;
private $_losslessFormats = [ /**
'flac', * UploadTrackCommand constructor.
'pcm_s16le ([1][0][0][0] / 0x0001)', *
'pcm_s16be', * @param bool $allowLossy
'adpcm_ms ([2][0][0][0] / 0x0002)', * @param bool $allowShortTrack allow tracks shorter than 30 seconds
'pcm_s24le ([1][0][0][0] / 0x0001)', * @param string|null $customTrackSource value to set in the track's "source" field; if left blank, "direct_upload" is used
'pcm_s24be', * @param bool $autoPublishByDefault
'pcm_f32le ([3][0][0][0] / 0x0003)', */
'pcm_f32be (fl32 / 0x32336C66)' public function __construct(bool $allowLossy = false, bool $allowShortTrack = false, string $customTrackSource = null, bool $autoPublishByDefault = false)
];
public function __construct($allowLossy = false, $allowShortTrack = false, $customTrackSource = null, $autoPublishByDefault = false)
{ {
$this->_allowLossy = $allowLossy; $this->_allowLossy = $allowLossy;
$this->_allowShortTrack = $allowShortTrack; $this->_allowShortTrack = $allowShortTrack;
@ -84,22 +68,21 @@ class UploadTrackCommand extends CommandBase
public function execute() public function execute()
{ {
$user = \Auth::user(); $user = \Auth::user();
$trackFile = \Input::file('track', null); $trackFile = Input::file('track', null);
$coverFile = Input::file('cover', null);
if (null === $trackFile) { if (null === $trackFile) {
return CommandResponse::fail(['track' => ['You must upload an audio file!']]); return CommandResponse::fail(['track' => ['You must upload an audio file!']]);
} }
$audio = \AudioCache::get($trackFile->getPathname()); $audio = \AudioCache::get($trackFile->getPathname());
list($parsedTags, $rawTags) = $this->parseOriginalTags($trackFile, $user, $audio->getAudioCodec());
$track = new Track(); $track = new Track();
$track->user_id = $user->id; $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->duration = $audio->getDuration();
$track->save(); $track->save();
$track->ensureDirectoryExists(); $track->ensureDirectoryExists();
@ -110,11 +93,14 @@ class UploadTrackCommand extends CommandBase
$input = Input::all(); $input = Input::all();
$input['track'] = $trackFile; $input['track'] = $trackFile;
$input['cover'] = $coverFile;
$validator = \Validator::make($input, [ $validator = \Validator::make($input, [
'track' => 'track' =>
'required|' '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|') . ($this->_allowShortTrack ? '' : 'min_duration:30|')
. 'audio_channels:1,2', . 'audio_channels:1,2',
@ -139,467 +125,22 @@ class UploadTrackCommand extends CommandBase
$track->delete(); $track->delete();
return CommandResponse::fail($validator); return CommandResponse::fail($validator);
} }
// Process optional track fields
$autoPublish = (bool) ($input['auto_publish'] ?? $this->_autoPublishByDefault); $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 // If json_decode() isn't called here, Laravel will surround the JSON
// string with quotes when storing it in the database, which breaks things. // string with quotes when storing it in the database, which breaks things.
$track->metadata = json_decode(Input::get('metadata', null)); $track->metadata = json_decode(Input::get('metadata', null));
$track->original_tags = ['parsed_tags' => $parsedTags, 'raw_tags' => $rawTags];
$track->save(); $track->save();
// Parse any tags in the uploaded files.
try { $parseTagsCommand = new ParseTrackTagsCommand($track, $trackFile, $input);
$source = $trackFile->getPathname(); $result = $parseTagsCommand->execute();
if ($result->didFail()) {
// Lossy uploads need to be identified and set as the master file return $result;
// 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(); $generateTrackFiles = new GenerateTrackFilesCommand($track, $trackFile, $autoPublish);
$trackFile->is_master = true; return $generateTrackFiles->execute();
$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;
}
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;
}
}
} }
} }

View file

@ -20,9 +20,9 @@
namespace Poniverse\Ponyfm\Console\Commands; namespace Poniverse\Ponyfm\Console\Commands;
use Poniverse\Ponyfm\ShowSong; use Poniverse\Ponyfm\Models\ShowSong;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\TrackType; use Poniverse\Ponyfm\Models\TrackType;
use DB; use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Str; use Illuminate\Support\Str;

View file

@ -24,7 +24,7 @@ use Carbon\Carbon;
use File; use File;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
class ClearTrackCache extends Command class ClearTrackCache extends Command
{ {
@ -96,9 +96,6 @@ class ClearTrackCache extends Command
$this->info('Deleted ' . $trackFile->getFile()); $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.'); $this->info($count . ' files deleted. Deletion complete. Exiting.');
} else { } else {

View file

@ -0,0 +1,154 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use 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!');
}
}
}
}

View file

@ -1,87 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use 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!');
}
}

View file

@ -1,519 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use 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
];
}
}

View file

@ -0,0 +1,86 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use 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();
}
}

View file

@ -20,8 +20,8 @@
namespace Poniverse\Ponyfm\Console\Commands; namespace Poniverse\Ponyfm\Console\Commands;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use DB; use DB;
use Exception; use Exception;
use Illuminate\Console\Command; use Illuminate\Console\Command;

View file

@ -1,72 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use 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.");
}
}

View file

@ -21,7 +21,7 @@
namespace Poniverse\Ponyfm\Console\Commands; namespace Poniverse\Ponyfm\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
class RebuildArtists extends Command class RebuildArtists extends Command
{ {
@ -56,12 +56,19 @@ class RebuildArtists extends Command
*/ */
public function handle() public function handle()
{ {
$numberOfUsers = User::count();
$bar = $this->output->createProgressBar($numberOfUsers);
foreach(User::with(['tracks' => function($query) { foreach(User::with(['tracks' => function($query) {
$query->published()->listed(); $query->published()->listed();
}])->get() as $user) { }])->get() as $user) {
$bar->advance();
$user->track_count = $user->tracks->count(); $user->track_count = $user->tracks->count();
$user->save(); $user->save();
$this->info('Updated user #'.$user->id.'!'); }
}
$bar->finish();
$this->line('');
} }
} }

View file

@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Console\Commands;
use File; use File;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
class RebuildFilesizes extends Command class RebuildFilesizes extends Command
{ {

View file

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

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Console\Commands; namespace Poniverse\Ponyfm\Console\Commands;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RebuildTags extends Command class RebuildTags extends Command
@ -61,16 +61,18 @@ class RebuildTags extends Command
$tracks = [$track]; $tracks = [$track];
} else { } 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) { foreach($tracks as $track) {
$this->comment('Rewriting tags for track #'.$track->id.'...'); /** @var $track Track */
$track->updateTags(); $track->updateTags();
$bar->advance(); $bar->advance();
$this->line('');
} }
$bar->finish(); $bar->finish();

View file

@ -0,0 +1,105 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\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}");
}
}

View file

@ -24,8 +24,8 @@ use File;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
class RebuildTrackCache extends Command class RebuildTrackCache extends Command
{ {

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Console\Commands; namespace Poniverse\Ponyfm\Console\Commands;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use DB; use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;

View file

@ -33,17 +33,18 @@ class Kernel extends ConsoleKernel
protected $commands = [ protected $commands = [
\Poniverse\Ponyfm\Console\Commands\MigrateOldData::class, \Poniverse\Ponyfm\Console\Commands\MigrateOldData::class,
\Poniverse\Ponyfm\Console\Commands\RefreshCache::class, \Poniverse\Ponyfm\Console\Commands\RefreshCache::class,
\Poniverse\Ponyfm\Console\Commands\ImportMLPMA::class,
\Poniverse\Ponyfm\Console\Commands\ClassifyMLPMA::class, \Poniverse\Ponyfm\Console\Commands\ClassifyMLPMA::class,
\Poniverse\Ponyfm\Console\Commands\PublishUnclassifiedMlpmaTracks::class,
\Poniverse\Ponyfm\Console\Commands\RebuildTags::class, \Poniverse\Ponyfm\Console\Commands\RebuildTags::class,
\Poniverse\Ponyfm\Console\Commands\RebuildArtists::class, \Poniverse\Ponyfm\Console\Commands\RebuildArtists::class,
\Poniverse\Ponyfm\Console\Commands\FixYearZeroLogs::class,
\Poniverse\Ponyfm\Console\Commands\BootstrapLocalEnvironment::class, \Poniverse\Ponyfm\Console\Commands\BootstrapLocalEnvironment::class,
\Poniverse\Ponyfm\Console\Commands\PoniverseApiSetup::class, \Poniverse\Ponyfm\Console\Commands\PoniverseApiSetup::class,
\Poniverse\Ponyfm\Console\Commands\ClearTrackCache::class, \Poniverse\Ponyfm\Console\Commands\ClearTrackCache::class,
\Poniverse\Ponyfm\Console\Commands\RebuildTrackCache::class, \Poniverse\Ponyfm\Console\Commands\RebuildTrackCache::class,
\Poniverse\Ponyfm\Console\Commands\RebuildTrack::class,
\Poniverse\Ponyfm\Console\Commands\RebuildFilesizes::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,
]; ];
/** /**

View file

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

View file

@ -22,9 +22,9 @@ namespace Poniverse\Ponyfm\Http\Controllers;
use Poniverse\Ponyfm\AlbumDownloader; use Poniverse\Ponyfm\AlbumDownloader;
use App; use App;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;
use View; use View;

View file

@ -21,7 +21,7 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Mobile; namespace Poniverse\Ponyfm\Http\Controllers\Api\Mobile;
use Poniverse\Ponyfm\Http\Controllers\Controller; use Poniverse\Ponyfm\Http\Controllers\Controller;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Response; use Response;
class TracksController extends Controller class TracksController extends Controller

View file

@ -22,8 +22,8 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\V1;
use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Commands\UploadTrackCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Response; use Response;
class TracksController extends ApiControllerBase class TracksController extends ApiControllerBase

View file

@ -34,6 +34,7 @@ class AccountController extends ApiControllerBase
$user = Auth::user(); $user = Auth::user();
return Response::json([ return Response::json([
'id' => $user->id,
'bio' => $user->bio, 'bio' => $user->bio,
'can_see_explicit_content' => $user->can_see_explicit_content == 1, 'can_see_explicit_content' => $user->can_see_explicit_content == 1,
'display_name' => $user->display_name, 'display_name' => $user->display_name,

View file

@ -21,19 +21,18 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\File; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Album;
use Poniverse\Ponyfm\Commands\CreateAlbumCommand; use Poniverse\Ponyfm\Commands\CreateAlbumCommand;
use Poniverse\Ponyfm\Commands\DeleteAlbumCommand; use Poniverse\Ponyfm\Commands\DeleteAlbumCommand;
use Poniverse\Ponyfm\Commands\EditAlbumCommand; use Poniverse\Ponyfm\Commands\EditAlbumCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\ResourceLogItem; use Auth;
use Illuminate\Support\Facades\Auth; use Input;
use Illuminate\Support\Facades\Input; use Poniverse\Ponyfm\Models\User;
use Illuminate\Support\Facades\Response; use Response;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
class AlbumsController extends ApiControllerBase class AlbumsController extends ApiControllerBase
{ {
@ -142,10 +141,13 @@ class AlbumsController extends ApiControllerBase
200); 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 = []; $albums = [];
foreach ($query as $album) { foreach ($query as $album) {
$albums[] = [ $albums[] = [
'id' => $album->id, 'id' => $album->id,

View file

@ -20,14 +20,14 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Poniverse\Ponyfm\Album; use Gate;
use Poniverse\Ponyfm\Comment; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Favourite; use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Models\Favourite;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
use Cover;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
@ -36,12 +36,13 @@ class ArtistsController extends ApiControllerBase
{ {
public function getFavourites($slug) public function getFavourites($slug)
{ {
$user = User::whereSlug($slug)->first(); $user = User::where('slug', $slug)->whereNull('disabled_at')->first();
if (!$user) { if (!$user) {
App::abort(404); App::abort(404);
} }
$favs = Favourite::whereUserId($user->id)->with([ $favs = Favourite::where('user_id', $user->id)
->with([
'track.genre', 'track.genre',
'track.cover', 'track.cover',
'track.user', 'track.user',
@ -59,10 +60,10 @@ class ArtistsController extends ApiControllerBase
$albums = []; $albums = [];
foreach ($favs as $fav) { foreach ($favs as $fav) {
if ($fav->type == 'Poniverse\Ponyfm\Track') { if ($fav->type == 'Poniverse\Ponyfm\Models\Track') {
$tracks[] = Track::mapPublicTrackSummary($fav->track); $tracks[] = Track::mapPublicTrackSummary($fav->track);
} else { } else {
if ($fav->type == 'Poniverse\Ponyfm\Album') { if ($fav->type == 'Poniverse\Ponyfm\Models\Album') {
$albums[] = Album::mapPublicAlbumSummary($fav->album); $albums[] = Album::mapPublicAlbumSummary($fav->album);
} }
} }
@ -76,7 +77,7 @@ class ArtistsController extends ApiControllerBase
public function getContent($slug) public function getContent($slug)
{ {
$user = User::whereSlug($slug)->first(); $user = User::where('slug', $slug)->whereNull('disabled_at')->first();
if (!$user) { if (!$user) {
App::abort(404); App::abort(404);
} }
@ -111,7 +112,8 @@ class ArtistsController extends ApiControllerBase
public function getShow($slug) public function getShow($slug)
{ {
$user = User::whereSlug($slug) $user = User::where('slug', $slug)
->whereNull('disabled_at')
->userDetails() ->userDetails()
->with([ ->with([
'comments' => function ($query) { 'comments' => function ($query) {
@ -157,7 +159,7 @@ class ArtistsController extends ApiControllerBase
return Response::json([ return Response::json([
'artist' => [ 'artist' => [
'id' => (int)$user->id, 'id' => $user->id,
'name' => $user->display_name, 'name' => $user->display_name,
'slug' => $user->slug, 'slug' => $user->slug,
'is_archived' => (bool)$user->is_archived, 'is_archived' => (bool)$user->is_archived,
@ -173,7 +175,10 @@ class ArtistsController extends ApiControllerBase
'bio' => $user->bio, 'bio' => $user->bio,
'mlpforums_username' => $user->username, 'mlpforums_username' => $user->username,
'message_url' => $user->message_url, 'message_url' => $user->message_url,
'user_data' => $userData 'user_data' => $userData,
'permissions' => [
'edit' => Gate::allows('edit', $user)
]
] ]
], 200); ], 200);
} }
@ -195,18 +200,7 @@ class ArtistsController extends ApiControllerBase
$users = []; $users = [];
foreach ($query->get() as $user) { foreach ($query->get() as $user) {
$users[] = [ $users[] = User::mapPublicUserSummary($user);
'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
];
} }
return Response::json(["artists" => $users, "current_page" => $page, "total_pages" => ceil($count / $perPage)], return Response::json(["artists" => $users, "current_page" => $page, "total_pages" => ceil($count / $perPage)],

View file

@ -22,7 +22,7 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use App; use App;
use Poniverse\Ponyfm\Commands\CreateCommentCommand; use Poniverse\Ponyfm\Commands\CreateCommentCommand;
use Poniverse\Ponyfm\Comment; use Poniverse\Ponyfm\Models\Comment;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;

View file

@ -21,7 +21,7 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;

View file

@ -20,12 +20,12 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Poniverse\Ponyfm\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Commands\ToggleFavouriteCommand; use Poniverse\Ponyfm\Commands\ToggleFavouriteCommand;
use Poniverse\Ponyfm\Favourite; use Poniverse\Ponyfm\Models\Favourite;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input; use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;

View file

@ -21,9 +21,10 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Input; use Input;
use Poniverse\Ponyfm\Commands\CreateGenreCommand;
use Poniverse\Ponyfm\Commands\DeleteGenreCommand; use Poniverse\Ponyfm\Commands\DeleteGenreCommand;
use Poniverse\Ponyfm\Commands\RenameGenreCommand; use Poniverse\Ponyfm\Commands\RenameGenreCommand;
use Poniverse\Ponyfm\Genre; use Poniverse\Ponyfm\Models\Genre;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Response; use Response;
@ -45,6 +46,11 @@ class GenresController extends ApiControllerBase
], 200); ], 200);
} }
public function postCreate()
{
$command = new CreateGenreCommand(Input::get('name'));
return $this->execute($command);
}
public function putRename($genreId) public function putRename($genreId)
{ {

View file

@ -20,17 +20,21 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Auth;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Cover; use Poniverse\Ponyfm\Models\User;
use Illuminate\Support\Facades\Response; use Response;
class ImagesController extends ApiControllerBase 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 = []; $images = [];
foreach ($query->get() as $image) { foreach ($query->get() as $image) {
$images[] = [ $images[] = [
'id' => $image->id, 'id' => $image->id,

View file

@ -27,13 +27,13 @@ use Poniverse\Ponyfm\Commands\DeletePlaylistCommand;
use Poniverse\Ponyfm\Commands\EditPlaylistCommand; use Poniverse\Ponyfm\Commands\EditPlaylistCommand;
use Poniverse\Ponyfm\Commands\RemoveTrackFromPlaylistCommand; use Poniverse\Ponyfm\Commands\RemoveTrackFromPlaylistCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Illuminate\Support\Facades\Auth; use Auth;
use Illuminate\Support\Facades\Input; use Input;
use Illuminate\Support\Facades\Response; use Response;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
class PlaylistsController extends ApiControllerBase class PlaylistsController extends ApiControllerBase
{ {
@ -175,8 +175,12 @@ class PlaylistsController extends ApiControllerBase
public function getOwned() public function getOwned()
{ {
$query = Playlist::summary()->with('pins', 'tracks', 'tracks.cover')->where('user_id', $query = Playlist::summary()
\Auth::user()->id)->orderBy('title', 'asc')->get(); ->with('pins', 'tracks', 'tracks.cover')
->where('user_id', Auth::user()->id)
->orderBy('title', 'asc')
->get();
$playlists = []; $playlists = [];
foreach ($query as $playlist) { foreach ($query as $playlist) {
$playlists[] = [ $playlists[] = [
@ -191,7 +195,8 @@ class PlaylistsController extends ApiControllerBase
'normal' => $playlist->getCoverUrl(Image::NORMAL) 'normal' => $playlist->getCoverUrl(Image::NORMAL)
], ],
'is_pinned' => $playlist->hasPinFor(Auth::user()->id), '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')
]; ];
} }

View file

@ -2,7 +2,7 @@
/** /**
* Pony.fm - A community for pony fan music. * 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 * 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 * 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; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Poniverse\Ponyfm\Http\Controllers\Controller; use Elasticsearch;
use Poniverse\Ponyfm\ProfileRequest; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Cache; use Input;
use Config; use Poniverse\Ponyfm\Library\Search;
use Response; use Response;
class ProfilerController extends Controller class SearchController extends ApiControllerBase
{ {
public function getRequest($id) public function getSearch(Search $search)
{ {
if (!Config::get('app.debug')) { $results = $search->searchAllContent(Input::query('query'));
return;
}
$key = 'profiler-request-' . $id; return Response::json([
$request = Cache::get($key); 'results' => $results,
if (!$request) { ], 200);
exit();
}
Cache::forget($key);
return Response::json(['request' => ProfileRequest::load($request)->toArray()], 200);
} }
} }

View file

@ -20,11 +20,11 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; 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\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\License; use Poniverse\Ponyfm\Models\License;
use Poniverse\Ponyfm\ShowSong; use Poniverse\Ponyfm\Models\ShowSong;
use Poniverse\Ponyfm\TrackType; use Poniverse\Ponyfm\Models\TrackType;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
class TaxonomiesController extends ApiControllerBase class TaxonomiesController extends ApiControllerBase
@ -33,8 +33,10 @@ class TaxonomiesController extends ApiControllerBase
{ {
return \Response::json([ return \Response::json([
'licenses' => License::all()->toArray(), 'licenses' => License::all()->toArray(),
'genres' => Genre::select('genres.*', 'genres' => Genre::with('trackCountRelation')
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(), ->orderBy('name')
->get()
->toArray(),
'track_types' => TrackType::select('track_types.*', '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')) 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) ->where('id', '!=', TrackType::UNCLASSIFIED_TRACK)

View file

@ -22,15 +22,14 @@ namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException;
use File; use File;
use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException;
use Poniverse\Ponyfm\Commands\DeleteTrackCommand; use Poniverse\Ponyfm\Commands\DeleteTrackCommand;
use Poniverse\Ponyfm\Commands\EditTrackCommand; use Poniverse\Ponyfm\Commands\EditTrackCommand;
use Poniverse\Ponyfm\Commands\UploadTrackCommand; use Poniverse\Ponyfm\Commands\UploadTrackCommand;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile; use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Auth; use Auth;
use Input; use Input;
use Response; use Response;
@ -41,7 +40,7 @@ class TracksController extends ApiControllerBase
{ {
session_write_close(); session_write_close();
return $this->execute(new UploadTrackCommand()); return $this->execute(new UploadTrackCommand(true));
} }
public function getUploadStatus($trackId) public function getUploadStatus($trackId)
@ -184,9 +183,7 @@ class TracksController extends ApiControllerBase
return $this->notFound('Track ' . $id . ' not found!'); return $this->notFound('Track ' . $id . ' not found!');
} }
if ($track->user_id != Auth::user()->id) { $this->authorize('edit', $track);
return $this->notAuthorized();
}
return Response::json(Track::mapPrivateTrackShow($track), 200); return Response::json(Track::mapPrivateTrackShow($track), 200);
} }
@ -225,6 +222,9 @@ class TracksController extends ApiControllerBase
} }
if (Input::has('songs')) { 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) { $query->join('show_song_track', function ($join) {
$join->on('tracks.id', '=', 'show_song_track.track_id'); $join->on('tracks.id', '=', 'show_song_track.track_id');
}); });

View file

@ -21,7 +21,7 @@
namespace Poniverse\Ponyfm\Http\Controllers; namespace Poniverse\Ponyfm\Http\Controllers;
use App; use App;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
use View; use View;
use Redirect; use Redirect;
@ -32,9 +32,17 @@ class ArtistsController extends Controller
return View::make('artists.index'); 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) public function getProfile($slug)
{ {
$user = User::whereSlug($slug)->first(); $user = User::whereSlug($slug)->whereNull('disabled_at')->first();
if (!$user) { if (!$user) {
App::abort('404'); App::abort('404');
} }
@ -45,10 +53,10 @@ class ArtistsController extends Controller
public function getShortlink($id) public function getShortlink($id)
{ {
$user = User::find($id); $user = User::find($id);
if (!$user) { if (!$user || $user->disabled_at !== NULL) {
App::abort('404'); App::abort('404');
} }
return Redirect::action('ArtistsController@getProfile', [$id]); return Redirect::action('ArtistsController@getProfile', [$user->slug]);
} }
} }

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Http\Controllers; namespace Poniverse\Ponyfm\Http\Controllers;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
use Auth; use Auth;
use Config; use Config;
use DB; use DB;

View file

@ -20,7 +20,7 @@
namespace Poniverse\Ponyfm\Http\Controllers; namespace Poniverse\Ponyfm\Http\Controllers;
use Poniverse\Ponyfm\Image; use Poniverse\Ponyfm\Models\Image;
use Config; use Config;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;

View file

@ -21,9 +21,9 @@
namespace Poniverse\Ponyfm\Http\Controllers; namespace Poniverse\Ponyfm\Http\Controllers;
use App; use App;
use Poniverse\Ponyfm\Playlist; use Poniverse\Ponyfm\Models\Playlist;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\PlaylistDownloader; use Poniverse\Ponyfm\PlaylistDownloader;
use Auth; use Auth;
use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Redirect;

View file

@ -20,9 +20,9 @@
namespace Poniverse\Ponyfm\Http\Controllers; namespace Poniverse\Ponyfm\Http\Controllers;
use Poniverse\Ponyfm\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
use Auth; use Auth;
use Config; use Config;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
@ -91,6 +91,11 @@ class TracksController extends Controller
return View::make('tracks.show'); return View::make('tracks.show');
} }
public function getEdit($id, $slug)
{
return $this->getTrack($id, $slug);
}
public function getShortlink($id) public function getShortlink($id)
{ {
$track = Track::find($id); $track = Track::find($id);

View file

@ -36,7 +36,7 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Poniverse\Ponyfm\Http\Middleware\VerifyCsrfToken::class, \Poniverse\Ponyfm\Http\Middleware\VerifyCsrfToken::class,
\Poniverse\Ponyfm\Http\Middleware\Profiler::class, \Poniverse\Ponyfm\Http\Middleware\DisabledAccountCheck::class,
]; ];
/** /**

View file

@ -20,11 +20,13 @@
namespace Poniverse\Ponyfm\Http\Middleware; namespace Poniverse\Ponyfm\Http\Middleware;
use Auth;
use Closure; use Closure;
use GuzzleHttp; use GuzzleHttp;
use Illuminate\Auth\Guard;
use Illuminate\Http\Request;
use Illuminate\Session\Store;
use Poniverse; use Poniverse;
use Poniverse\Ponyfm\User; use Poniverse\Ponyfm\Models\User;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class AuthenticateOAuth class AuthenticateOAuth
@ -34,8 +36,20 @@ class AuthenticateOAuth
*/ */
private $poniverse; 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->poniverse = $poniverse;
$this->auth = $auth;
$this->session = $session;
} }
/** /**
@ -47,10 +61,10 @@ class AuthenticateOAuth
* @return mixed * @return mixed
* @throws \OAuth2\Exception * @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. // 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 // check that access token is valid at Poniverse.net
$accessTokenInfo = $this->poniverse->getAccessTokenInfo($accessToken); $accessTokenInfo = $this->poniverse->getAccessTokenInfo($accessToken);
@ -65,13 +79,29 @@ class AuthenticateOAuth
// Log in as the given user, creating the account if necessary. // Log in as the given user, creating the account if necessary.
$this->poniverse->setAccessToken($accessToken); $this->poniverse->setAccessToken($accessToken);
session()->put('api_client_id', $accessTokenInfo->getClientId()); $this->session->put('api_client_id', $accessTokenInfo->getClientId());
$poniverseUser = $this->poniverse->getUser(); $poniverseUser = $this->poniverse->getUser();
$user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']); $user = User::findOrCreate($poniverseUser['username'], $poniverseUser['display_name'], $poniverseUser['email']);
Auth::login($user); $this->auth->onceUsingId($user);
return $next($request); 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');
}
} }

View file

@ -0,0 +1,68 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Http\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);
}
}

View file

@ -1,80 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Http\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());
}
}

View file

@ -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('/dashboard', 'TracksController@getIndex');
Route::get('/tracks', ['as' => 'tracks.discover', 'uses' => 'TracksController@getIndex']); Route::get('/tracks', ['as' => 'tracks.discover', 'uses' => 'TracksController@getIndex']);
Route::get('/tracks/popular', 'TracksController@getIndex'); Route::get('/tracks/popular', 'TracksController@getIndex');
Route::get('/tracks/random', 'TracksController@getIndex'); Route::get('/tracks/random', 'TracksController@getIndex');
Route::get('tracks/{id}-{slug}', 'TracksController@getTrack'); 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}', 'TracksController@getShortlink' )->where('id', '\d+');
Route::get('t{id}/embed', 'TracksController@getEmbed' ); Route::get('t{id}/embed', 'TracksController@getEmbed' );
Route::get('t{id}/stream.{extension}', 'TracksController@getStream' ); Route::get('t{id}/stream.{extension}', 'TracksController@getStream' );
@ -54,6 +51,7 @@ Route::get('playlists', 'PlaylistsController@getIndex');
Route::get('/register', 'AccountController@getRegister'); Route::get('/register', 'AccountController@getRegister');
Route::get('/login', 'AuthController@getLogin'); Route::get('/login', 'AuthController@getLogin');
Route::post('/auth/logout', 'AuthController@postLogout');
Route::get('/auth/oauth', 'AuthController@getOAuth'); Route::get('/auth/oauth', 'AuthController@getOAuth');
Route::get('/about', function() { return View::make('pages.about'); }); 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::group(['prefix' => 'api/web'], function() {
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll'); Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');
Route::get('/search', 'Api\Web\SearchController@getSearch');
Route::get('/playlists/show/{id}', 'Api\Web\PlaylistsController@getShow');
Route::get('/tracks', 'Api\Web\TracksController@getIndex'); Route::get('/tracks', 'Api\Web\TracksController@getIndex');
Route::get('/tracks/{id}', 'Api\Web\TracksController@getShow')->where('id', '\d+'); 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('/albums/cached/{id}/{format}', 'Api\Web\AlbumsController@getCachedAlbum')->where(['id' => '\d+', 'format' => '.+']);
Route::get('/playlists', 'Api\Web\PlaylistsController@getIndex'); 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/{id}', 'Api\Web\PlaylistsController@getShow')->where('id', '\d+');
Route::get('/playlists/cached/{id}/{format}', 'Api\Web\PlaylistsController@getCachedPlaylist')->where(['id' => '\d+', 'format' => '.+']); 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::group(['middleware' => 'auth'], function() {
Route::get('/account/settings', 'Api\Web\AccountController@getSettings'); 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/owned', 'Api\Web\TracksController@getOwned');
Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit'); 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('/albums/edit/{id}', 'Api\Web\AlbumsController@getEdit');
Route::get('/playlists/owned', 'Api\Web\PlaylistsController@getOwned'); 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::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() {
Route::get('/genres', 'Api\Web\GenresController@getIndex'); 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::put('/genres/{id}', 'Api\Web\GenresController@putRename')->where('id', '\d+');
Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+'); Route::delete('/genres/{id}', 'Api\Web\GenresController@deleteGenre')->where('id', '\d+');
}); });
@ -160,11 +159,23 @@ Route::group(['prefix' => 'api/web'], function() {
Route::post('/auth/logout', 'Api\Web\AuthController@postLogout'); 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::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() {
Route::get('/genres', 'AdminController@getGenres');
Route::get('/', 'AdminController@getIndex');
});
Route::get('u{id}', 'ArtistsController@getShortlink')->where('id', '\d+');
Route::get('users/{id}-{slug}', 'ArtistsController@getShortlink')->where('id', '\d+');
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', 'ContentController@getTracks');
Route::get('/tracks/edit/{id}', 'ContentController@getTracks'); Route::get('/tracks/edit/{id}', 'ContentController@getTracks');
Route::get('/albums', 'ContentController@getAlbums'); Route::get('/albums', 'ContentController@getAlbums');
@ -176,18 +187,8 @@ Route::group(['prefix' => 'account', 'middleware' => 'auth'], function() {
Route::get('/', 'AccountController@getIndex'); Route::get('/', 'AccountController@getIndex');
}); });
Route::group(['prefix' => 'admin', 'middleware' => ['auth', 'can:access-admin-area']], function() {
Route::get('/genres', 'AdminController@getGenres');
Route::get('/', 'AdminController@getIndex');
}); });
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::get('/', 'HomeController@getIndex'); Route::get('/', 'HomeController@getIndex');
Route::group(['domain' => 'api.pony.fm'], function() { Route::group(['domain' => 'api.pony.fm'], function() {

View file

@ -21,11 +21,12 @@
namespace Poniverse\Ponyfm\Jobs; namespace Poniverse\Ponyfm\Jobs;
use Auth; use Auth;
use Poniverse\Ponyfm\Genre; use DB;
use Poniverse\Ponyfm\Models\Genre;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use SerializesModels; use SerializesModels;
class DeleteGenre extends Job implements SelfHandling, ShouldQueue class DeleteGenre extends Job implements SelfHandling, ShouldQueue
@ -60,6 +61,8 @@ class DeleteGenre extends Job implements SelfHandling, ShouldQueue
*/ */
public function handle() public function handle()
{ {
$this->beforeHandle();
// The user who kicked off this job is used when generating revision log entries. // The user who kicked off this job is used when generating revision log entries.
Auth::login($this->executingUser); Auth::login($this->executingUser);

View file

@ -24,17 +24,15 @@ namespace Poniverse\Ponyfm\Jobs;
use Carbon\Carbon; use Carbon\Carbon;
use DB; use DB;
use File; use File;
use Illuminate\Support\Facades\Config; use Config;
use Illuminate\Support\Facades\Log; use Log;
use OAuth2\Exception;
use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException; use Poniverse\Ponyfm\Exceptions\InvalidEncodeOptionsException;
use Poniverse\Ponyfm\Jobs\Job;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Bus\SelfHandling; use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Poniverse\Ponyfm\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\TrackFile; use Poniverse\Ponyfm\Models\TrackFile;
use Symfony\Component\Process\Exception\ProcessFailedException; use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
@ -44,19 +42,19 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
/** /**
* @var TrackFile * @var TrackFile
*/ */
private $trackFile; protected $trackFile;
/** /**
* @var * @var
*/ */
private $isExpirable; protected $isExpirable;
/** /**
* @var bool * @var bool
*/ */
private $isForUpload; protected $isForUpload;
/** /**
* @var bool * @var bool
*/ */
private $autoPublishWhenComplete; protected $autoPublishWhenComplete;
/** /**
* Create a new job instance. * Create a new job instance.
@ -87,9 +85,21 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
*/ */
public function handle() 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 // Start the job
$this->trackFile->status = TrackFile::STATUS_PROCESSING; $this->trackFile->status = TrackFile::STATUS_PROCESSING;
$this->trackFile->update(); $this->trackFile->save();
// Use the track's master file as the source // Use the track's master file as the source
if ($this->isForUpload) { if ($this->isForUpload) {
@ -120,19 +130,18 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
$process->mustRun(); $process->mustRun();
} catch (ProcessFailedException $e) { } catch (ProcessFailedException $e) {
Log::error('An exception occured in the encoding process for track file ' . $this->trackFile->id . ' - ' . $e->getMessage()); Log::error('An exception occured in the encoding process for track file ' . $this->trackFile->id . ' - ' . $e->getMessage());
Log::info($process->getOutput());
// Ensure queue fails // Ensure queue fails
throw $e; throw $e;
} finally {
Log::info($process->getOutput());
} }
// Update the tags of the track // Update the tags of the track
$this->trackFile->track->updateTags($this->trackFile->format); $this->trackFile->track->updateTags($this->trackFile->format);
// Insert the expiration time for cached tracks // 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->expires_at = Carbon::now()->addMinutes(Config::get('ponyfm.track_file_cache_duration'));
$this->trackFile->update(); $this->trackFile->save();
} }
// Update file size // Update file size
@ -140,7 +149,7 @@ class EncodeTrackFile extends Job implements SelfHandling, ShouldQueue
// Complete the job // Complete the job
$this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED; $this->trackFile->status = TrackFile::STATUS_NOT_BEING_PROCESSED;
$this->trackFile->update(); $this->trackFile->save();
if ($this->isForUpload) { if ($this->isForUpload) {
if (!$this->trackFile->is_master && $this->trackFile->is_cacheable) { 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->status = TrackFile::STATUS_PROCESSING_ERROR;
$this->trackFile->expires_at = null; $this->trackFile->expires_at = null;
$this->trackFile->update(); $this->trackFile->save();
} }
} }

View file

@ -20,6 +20,8 @@
namespace Poniverse\Ponyfm\Jobs; namespace Poniverse\Ponyfm\Jobs;
use App;
use DB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
abstract class Job abstract class Job
@ -36,4 +38,15 @@ abstract class Job
*/ */
use Queueable; 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();
}
}
} }

View file

@ -0,0 +1,58 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Jobs;
use 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();
}
}

View file

@ -0,0 +1,100 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Jobs;
use 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);
}
}

View file

@ -20,27 +20,38 @@
class Assets class Assets
{ {
public static function scriptIncludes($area = 'app') public static function scriptIncludes(string $area) {
{ $scriptTags = '';
if (!Config::get("app.debug")) {
return '<script src="/build/scripts/' . $area . '.js?' . filemtime("./build/scripts/" . $area . ".js") . '"></script>'; 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)); foreach ($scripts as $filename) {
$retVal = ""; if (Config::get('app.debug') && $filename !== 'templates.js') {
$scriptTags .= "<script src='http://localhost:61999/build/scripts/{$filename}'></script>";
foreach ($scripts as $script) { } else {
$filename = self::replaceExtensionWith($script, ".coffee", ".js"); $scriptTags .= "<script src='/build/scripts/{$filename}?" . filemtime(public_path("build/scripts/{$filename}")) . "'></script>";
$retVal .= "<script src='/build/$filename?" . filemtime('./build/' . $filename) . "'></script>"; }
} }
return $retVal; if (Config::get('app.debug')) {
$scriptTags .= '<script src="http://localhost:61999/webpack-dev-server.js"></script>';
}
return $scriptTags;
} }
public static function styleIncludes($area = 'app') public static function styleIncludes($area = 'app')
{ {
if (!Config::get("app.debug")) { if (!Config::get("app.debug")) {
return '<script>document.write(\'<link rel="stylesheet" href="build/styles/' . $area . '.css?' . filemtime("build/styles/" . $area . ".css") . '" />\');</script>'; return '<script>document.write(\'<link rel="stylesheet" href="build/styles/' . $area . '.css?' .
filemtime(public_path("/build/styles/${area}.css"))
. '" />\');</script>';
} }
$styles = self::mergeGlobs(self::getStylesForArea($area)); $styles = self::mergeGlobs(self::getStylesForArea($area));
@ -48,7 +59,7 @@ class Assets
foreach ($styles as $style) { foreach ($styles as $style) {
$filename = self::replaceExtensionWith($style, ".less", ".css"); $filename = self::replaceExtensionWith($style, ".less", ".css");
$retVal .= "<link rel='stylesheet' href='/build/$filename?" . filemtime('./build/' . $filename) . "' />"; $retVal .= "<link rel='stylesheet' href='/build/$filename?" .filemtime(public_path("/build/${filename}")). "' />";
} }
return $retVal; return $retVal;
@ -82,41 +93,6 @@ class Assets
return $files; 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) private static function getStylesForArea($area)
{ {
if ($area == 'app') { if ($area == 'app') {
@ -124,7 +100,6 @@ class Assets
"styles/base/jquery-ui.css", "styles/base/jquery-ui.css",
"styles/base/colorbox.css", "styles/base/colorbox.css",
"styles/app.less", "styles/app.less",
"styles/profiler.less"
]; ];
} else { } else {
if ($area == 'embed') { if ($area == 'embed') {

View file

@ -22,11 +22,7 @@ class AudioCache
{ {
private static $_movieCache = array(); private static $_movieCache = array();
/** public static function get(string $filename):FFmpegMovie
* @param $filename
* @return FFmpegMovie
*/
public static function get($filename)
{ {
if (isset(self::$_movieCache[$filename])) { if (isset(self::$_movieCache[$filename])) {
return self::$_movieCache[$filename]; return self::$_movieCache[$filename];

View file

@ -1,55 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use 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;
}
}

View file

@ -18,17 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
use Illuminate\Support\Facades\Log; use Symfony\Component\Process\Process;
class External class External
{ {
public static function execute($command) public static function execute($command)
{ {
$output = []; $process = new Process($command);
$error = exec($command, $output); $process->run();
if ($error != null) { if (!$process->isSuccessful()) {
Log::error('"' . $command . '" failed with "' . $error . '"'); Log::error('"' . $command . '" failed with "' . $process->getErrorOutput() . '"');
} }
} }
} }

View file

@ -18,8 +18,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
use Illuminate\Support\Facades\URL;
class Gravatar class Gravatar
{ {
public static function getUrl($email, $size = 80, $default = null, $rating = 'g') public static function getUrl($email, $size = 80, $default = null, $rating = 'g')
@ -40,7 +38,10 @@ class Gravatar
} }
} }
$url .= "&d=" . urlencode(URL::to('/images/icons/profile_' . $size . '.png')); // Pony.fm's production URL is hardcoded here so Gravatar can
// serve functioning default avatars in the dev environment,
// which it won't be able to access.
$url .= "&d=" . urlencode(URL::to('https://pony.fm/images/icons/profile_' . $size . '.png'));
} }
return $url; return $url;

View file

@ -1,53 +0,0 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2015 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
use Illuminate\Contracts\Hashing\Hasher;
class IpsHasher implements Hasher
{
public function make($value, array $options = array())
{
return md5(md5($options['salt']) . md5(static::ips_sanitize($value)));
}
public function check($value, $hashedValue, array $options = array())
{
return static::make($value, ['salt' => $options['salt']]) === $hashedValue;
}
public function needsRehash($hashedValue, array $options = array())
{
return false;
}
static public function ips_sanitize($value)
{
$value = str_replace('&', '&amp;', $value);
$value = str_replace('\\', '&#092;', $value);
$value = str_replace('!', '&#33;', $value);
$value = str_replace('$', '&#036;', $value);
$value = str_replace('"', '&quot;', $value);
$value = str_replace('<', '&lt;', $value);
$value = str_replace('>', '&gt;', $value);
$value = str_replace('\'', '&#39;', $value);
return $value;
}
}

View file

@ -1,4 +1,5 @@
<?php <?php
use Illuminate\Support\Str;
/** /**
* Pony.fm - A community for pony fan music. * Pony.fm - A community for pony fan music.
@ -45,6 +46,27 @@ class PfmValidator extends Illuminate\Validation\Validator
// value is the file array itself // value is the file array itself
// parameters is a list of formats the file can be, verified via ffmpeg // parameters is a list of formats the file can be, verified via ffmpeg
$file = AudioCache::get($value->getPathname()); $file = AudioCache::get($value->getPathname());
$codecString = $file->getAudioCodec();
// PCM, ADPCM, and AAC come in several variations as far as FFmpeg
// is concerned. They're all acceptable for Pony.fm, so we check what
// the codec string returned by FFmpeg starts with instead of looking
// for an exact match for these.
if (in_array('adpcm', $parameters) && Str::startsWith($codecString, 'adpcm')) {
return true;
}
if (in_array('pcm', $parameters) && Str::startsWith($codecString, 'pcm')) {
return true;
}
if (in_array('aac', $parameters) && Str::startsWith($codecString, 'aac')) {
return true;
}
if (in_array('alac', $parameters) && Str::startsWith($codecString, 'alac')) {
return true;
}
return in_array($file->getAudioCodec(), $parameters); return in_array($file->getAudioCodec(), $parameters);
} }

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

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

View file

@ -18,6 +18,24 @@
Version History Version History
=============== ===============
1.9.11: [2015-12-24] James Heinrich
* bugfix (G:64): update constructor syntax for PHP 7
* bugfix (G:62): infinite loop in large PNG files
* bugfix (G:61): ID3v2 remove BOM from frame descriptions
* bugfix (G:60): missing "break" in module.audio-video.quicktime.php
* bugfix (G:59): .gitignore comments
* bugfix (G:58): inconsistency in relation to module.tag.id3v2.php
* bugfix (G:57): comparing instead of assign
* bugfix (G:56): unsupported MIME type "audio/x-wave"
* bugfix (G:55): readme.md variable reference
* bugfix (G:54): QuickTime false 1000fps
* bugfix (G:53): Quicktime / ID3v2 multiple genres
* bugfix (G:52): sys_get_temp_dir in GetDataImageSize
* bugfix (#1903): Quicktime meta atom not parsed
* demo.joinmp3.php enhancements
* m4b (audiobook) chapters not parsed correctly
* sqlite3 caching not working
1.9.10: [2015-09-14] James Heinrich 1.9.10: [2015-09-14] James Heinrich
* bugfix (G:49): Declaration of getID3_cached_sqlite3 * bugfix (G:49): Declaration of getID3_cached_sqlite3
* bugfix (#1892): extension.cache.mysql * bugfix (#1892): extension.cache.mysql

View file

@ -109,7 +109,7 @@ class getID3
protected $startup_error = ''; protected $startup_error = '';
protected $startup_warning = ''; protected $startup_warning = '';
const VERSION = '1.9.10-201511241457'; const VERSION = '1.9.11-201601190922';
const FREAD_BUFFER_SIZE = 32768; const FREAD_BUFFER_SIZE = 32768;
const ATTACHMENTS_NONE = false; const ATTACHMENTS_NONE = false;

View file

@ -499,6 +499,18 @@ class getid3_quicktime extends getid3_handler
$atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 8)); $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 8));
break; break;
case 'covr':
$atom_structure['data'] = substr($boxdata, 8);
// not a foolproof check, but better than nothing
if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/jpeg';
} elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/png';
} elseif (preg_match('#^GIF#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/gif';
}
break;
case 'atID': case 'atID':
case 'cnID': case 'cnID':
case 'geID': case 'geID':
@ -516,9 +528,9 @@ class getid3_quicktime extends getid3_handler
$atom_structure['data'] = substr($boxdata, 8); $atom_structure['data'] = substr($boxdata, 8);
if ($atomname == 'covr') { if ($atomname == 'covr') {
// not a foolproof check, but better than nothing // not a foolproof check, but better than nothing
if (preg_match('#^\xFF\xD8\xFF#', $atom_structure['data'])) { if (preg_match('#^\\xFF\\xD8\\xFF#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/jpeg'; $atom_structure['image_mime'] = 'image/jpeg';
} elseif (preg_match('#^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A#', $atom_structure['data'])) { } elseif (preg_match('#^\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/png'; $atom_structure['image_mime'] = 'image/png';
} elseif (preg_match('#^GIF#', $atom_structure['data'])) { } elseif (preg_match('#^GIF#', $atom_structure['data'])) {
$atom_structure['image_mime'] = 'image/gif'; $atom_structure['image_mime'] = 'image/gif';
@ -1579,6 +1591,10 @@ if (!empty($atom_structure['sample_description_table'][$i]['width']) && !empty($
// Furthermore, for historical reasons the list of atoms is optionally // Furthermore, for historical reasons the list of atoms is optionally
// terminated by a 32-bit integer set to 0. If you are writing a program // terminated by a 32-bit integer set to 0. If you are writing a program
// to read user data atoms, you should allow for the terminating 0. // to read user data atoms, you should allow for the terminating 0.
if (strlen($atom_data) > 12) {
$subatomoffset += 4;
continue;
}
return $atom_structure; return $atom_structure;
} }

View file

@ -1170,9 +1170,16 @@ class getid3_riff extends getid3_handler {
} }
break; break;
case 'WEBP':
// https://developers.google.com/speed/webp/docs/riff_container
$info['fileformat'] = 'webp';
$info['mime_type'] = 'image/webp';
$info['error'][] = 'WebP image parsing not supported in this version of getID3()';
break;
default: default:
$info['error'][] = 'Unknown RIFF type: expecting one of (WAVE|RMP3|AVI |CDDA|AIFF|AIFC|8SVX|CDXA), found "'.$RIFFsubtype.'" instead'; $info['error'][] = 'Unknown RIFF type: expecting one of (WAVE|RMP3|AVI |CDDA|AIFF|AIFC|8SVX|CDXA|WEBP), found "'.$RIFFsubtype.'" instead';
//unset($info['fileformat']); //unset($info['fileformat']);
} }

View file

@ -18,23 +18,53 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Exception; use Exception;
use Helpers; use Helpers;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Auth; use Auth;
use Cache; use Cache;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException; use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\TrackCollection; use Poniverse\Ponyfm\Traits\TrackCollection;
use Poniverse\Ponyfm\Traits\SlugTrait; use Poniverse\Ponyfm\Traits\SlugTrait;
use Venturecraft\Revisionable\RevisionableTrait; use Venturecraft\Revisionable\RevisionableTrait;
class Album extends Model /**
* Poniverse\Ponyfm\Models\Album
*
* @property integer $id
* @property integer $user_id
* @property string $title
* @property string $slug
* @property string $description
* @property integer $cover_id
* @property integer $track_count
* @property integer $view_count
* @property integer $download_count
* @property integer $favourite_count
* @property integer $comment_count
* @property \Carbon\Carbon $created_at
* @property string $updated_at
* @property \Carbon\Carbon $deleted_at
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Favourite[] $favourites
* @property-read \Poniverse\Ponyfm\Models\Image $cover
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments
* @property-read mixed $url
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Album userDetails()
*/
class Album extends Model implements Searchable
{ {
use SoftDeletes, SlugTrait, DispatchesJobs, TrackCollection, RevisionableTrait; use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'album';
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $fillable = ['user_id', 'title', 'slug']; protected $fillable = ['user_id', 'title', 'slug'];
@ -62,27 +92,27 @@ class Album extends Model
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function users() public function users()
{ {
return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser');
} }
public function favourites() public function favourites()
{ {
return $this->hasMany('Poniverse\Ponyfm\Favourite'); return $this->hasMany('Poniverse\Ponyfm\Models\Favourite');
} }
public function cover() public function cover()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Image'); return $this->belongsTo('Poniverse\Ponyfm\Models\Image');
} }
public function tracks() public function tracks()
{ {
return $this->hasMany('Poniverse\Ponyfm\Track')->orderBy('track_number', 'asc'); return $this->hasMany('Poniverse\Ponyfm\Models\Track')->orderBy('track_number', 'asc');
} }
public function trackFiles() { public function trackFiles() {
@ -91,7 +121,7 @@ class Album extends Model
public function comments() public function comments()
{ {
return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
} }
public static function mapPublicAlbumShow(Album $album) public static function mapPublicAlbumShow(Album $album)
@ -182,6 +212,7 @@ class Album extends Model
'user' => [ 'user' => [
'id' => (int) $album->user->id, 'id' => (int) $album->user->id,
'name' => $album->user->display_name, 'name' => $album->user->display_name,
'slug' => $album->user->slug,
'url' => $album->user->url, 'url' => $album->user->url,
], ],
'user_data' => $userData, 'user_data' => $userData,
@ -361,4 +392,40 @@ class Album extends Model
{ {
return 'album-'.$this->id.'-'.$key; return 'album-'.$this->id.'-'.$key;
} }
/**
* The number of tracks in an album will always be in sync.
*
* @param array $options
* @return bool
*/
public function save(array $options = []) {
$this->recountTracks();
return parent::save($options);
}
protected function recountTracks() {
$this->track_count = $this->tracks->count();
}
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'title' => $this->title,
'artist' => $this->user->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->track_count > 0 && !$this->trashed();
}
} }

View file

@ -18,11 +18,32 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Poniverse\Ponyfm\Models\Comment
*
* @property integer $id
* @property integer $user_id
* @property string $ip_address
* @property string $content
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Carbon\Carbon $deleted_at
* @property integer $profile_id
* @property integer $track_id
* @property integer $album_id
* @property integer $playlist_id
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read \Poniverse\Ponyfm\Models\Track $track
* @property-read \Poniverse\Ponyfm\Models\Album $album
* @property-read \Poniverse\Ponyfm\Models\Playlist $playlist
* @property-read \Poniverse\Ponyfm\Models\User $profile
* @property-read mixed $resource
*/
class Comment extends Model class Comment extends Model
{ {
@ -34,27 +55,27 @@ class Comment extends Model
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function track() public function track()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Track'); return $this->belongsTo('Poniverse\Ponyfm\Models\Track');
} }
public function album() public function album()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Album'); return $this->belongsTo('Poniverse\Ponyfm\Models\Album');
} }
public function playlist() public function playlist()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Playlist'); return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist');
} }
public function profile() public function profile()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User', 'profile_id'); return $this->belongsTo('Poniverse\Ponyfm\Models\User', 'profile_id');
} }
public static function mapPublic($comment) public static function mapPublic($comment)

View file

@ -18,10 +18,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\Favourite
*
* @property integer $id
* @property integer $user_id
* @property integer $track_id
* @property integer $album_id
* @property integer $playlist_id
* @property string $created_at
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read \Poniverse\Ponyfm\Models\Track $track
* @property-read \Poniverse\Ponyfm\Models\Album $album
* @property-read \Poniverse\Ponyfm\Models\Playlist $playlist
* @property-read mixed $resource
* @property-read mixed $type
*/
class Favourite extends Model class Favourite extends Model
{ {
protected $table = 'favourites'; protected $table = 'favourites';
@ -35,22 +51,22 @@ class Favourite extends Model
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function track() public function track()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Track'); return $this->belongsTo('Poniverse\Ponyfm\Models\Track');
} }
public function album() public function album()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Album'); return $this->belongsTo('Poniverse\Ponyfm\Models\Album');
} }
public function playlist() public function playlist()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Playlist'); return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist');
} }
/** /**

View file

@ -18,10 +18,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\Follower
*
* @property integer $id
* @property integer $user_id
* @property integer $artist_id
* @property integer $playlist_id
* @property string $created_at
*/
class Follower extends Model class Follower extends Model
{ {
protected $table = 'followers'; protected $table = 'followers';

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use DB; use DB;
use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Eloquent\Relations\Relation;
@ -27,6 +27,22 @@ use Poniverse\Ponyfm\Traits\SlugTrait;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Venturecraft\Revisionable\RevisionableTrait; use Venturecraft\Revisionable\RevisionableTrait;
/**
* Poniverse\Ponyfm\Models\Genre
*
* @property integer $id
* @property string $name
* @property string $slug
* @property string $deleted_at
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks
* @property-read \Poniverse\Ponyfm\Models\Track $trackCountRelation
* @property-read mixed $track_count
* @property-read mixed $url
* @property-write mixed $title
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Genre extends Model class Genre extends Model
{ {
protected $table = 'genres'; protected $table = 'genres';
@ -35,8 +51,6 @@ class Genre extends Model
protected $appends = ['track_count', 'url']; protected $appends = ['track_count', 'url'];
protected $hidden = ['trackCountRelation']; protected $hidden = ['trackCountRelation'];
public $timestamps = false;
use SlugTrait, SoftDeletes, RevisionableTrait; use SlugTrait, SoftDeletes, RevisionableTrait;
public function tracks(){ public function tracks(){

View file

@ -18,13 +18,27 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use External; use External;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Config; use Config;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\File\UploadedFile;
/**
* Poniverse\Ponyfm\Models\Image
*
* @property integer $id
* @property string $filename
* @property string $mime
* @property string $extension
* @property integer $size
* @property string $hash
* @property integer $uploaded_by
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
*/
class Image extends Model class Image extends Model
{ {
const NORMAL = 1; const NORMAL = 1;
@ -52,7 +66,14 @@ class Image extends Model
return null; return null;
} }
public static function upload(UploadedFile $file, $user) /**
* @param UploadedFile $file
* @param int|User $user
* @param bool $forceReupload forces the image to be re-processed even if a matching hash is found
* @return Image
* @throws \Exception
*/
public static function upload(UploadedFile $file, $user, bool $forceReupload = false)
{ {
$userId = $user; $userId = $user;
if ($user instanceof User) { if ($user instanceof User) {
@ -63,10 +84,25 @@ class Image extends Model
$image = Image::whereHash($hash)->whereUploadedBy($userId)->first(); $image = Image::whereHash($hash)->whereUploadedBy($userId)->first();
if ($image) { if ($image) {
if ($forceReupload) {
// delete existing versions of the image
$filenames = scandir($image->getDirectory());
$imagePrefix = $image->id.'_';
$filenames = array_filter($filenames, function(string $filename) use ($imagePrefix) {
return Str::startsWith($filename, $imagePrefix);
});
foreach($filenames as $filename) {
unlink($image->getDirectory().'/'.$filename);
}
} else {
return $image; return $image;
} }
} else {
$image = new Image(); $image = new Image();
}
try { try {
$image->uploaded_by = $userId; $image->uploaded_by = $userId;
$image->size = $file->getSize(); $image->size = $file->getSize();
@ -79,7 +115,7 @@ class Image extends Model
$image->ensureDirectoryExists(); $image->ensureDirectoryExists();
foreach (self::$ImageTypes as $coverType) { foreach (self::$ImageTypes as $coverType) {
if ($coverType['id'] === self::ORIGINAL && $image->mime === 'image/jpeg') { if ($coverType['id'] === self::ORIGINAL && $image->mime === 'image/jpeg') {
$command = 'cp '.$file->getPathname().' '.$image->getFile($coverType['id']); $command = 'cp "'.$file->getPathname().'" '.$image->getFile($coverType['id']);
} else { } else {
// ImageMagick options reference: http://www.imagemagick.org/script/command-line-options.php // ImageMagick options reference: http://www.imagemagick.org/script/command-line-options.php
@ -100,6 +136,7 @@ class Image extends Model
} }
External::execute($command); External::execute($command);
chmod($image->getFile($coverType['id']), 0644);
} }
return $image; return $image;

View file

@ -18,10 +18,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\License
*
* @property integer $id
* @property string $title
* @property string $description
* @property boolean $affiliate_distribution
* @property boolean $open_distribution
* @property boolean $remix
*/
class License extends Model class License extends Model
{ {
protected $table = 'licenses'; protected $table = 'licenses';

View file

@ -18,21 +18,32 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\PinnedPlaylist
*
* @property integer $id
* @property integer $user_id
* @property integer $playlist_id
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read \Poniverse\Ponyfm\Models\Playlist $playlist
*/
class PinnedPlaylist extends Model class PinnedPlaylist extends Model
{ {
protected $table = 'pinned_playlists'; protected $table = 'pinned_playlists';
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function playlist() public function playlist()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Playlist'); return $this->belongsTo('Poniverse\Ponyfm\Models\Playlist');
} }
} }

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Helpers; use Helpers;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -26,18 +26,61 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Auth; use Auth;
use Cache; use Cache;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException; use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\TrackCollection; use Poniverse\Ponyfm\Traits\TrackCollection;
use Poniverse\Ponyfm\Traits\SlugTrait; use Poniverse\Ponyfm\Traits\SlugTrait;
use Venturecraft\Revisionable\RevisionableTrait; use Venturecraft\Revisionable\RevisionableTrait;
class Playlist extends Model /**
* Poniverse\Ponyfm\Models\Playlist
*
* @property integer $id
* @property integer $user_id
* @property string $title
* @property string $slug
* @property string $description
* @property boolean $is_public
* @property integer $track_count
* @property integer $view_count
* @property integer $download_count
* @property integer $favourite_count
* @property integer $follow_count
* @property integer $comment_count
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property \Carbon\Carbon $deleted_at
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\PinnedPlaylist[] $pins
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read mixed $url
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Playlist userDetails()
*/
class Playlist extends Model implements Searchable
{ {
use SoftDeletes, SlugTrait, DispatchesJobs, TrackCollection, RevisionableTrait; use SoftDeletes, SlugTrait, TrackCollection, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'playlist';
protected $table = 'playlists'; protected $table = 'playlists';
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
protected $casts = [
'id' => 'integer',
'user_id' => 'integer',
'title' => 'string',
'description' => 'string',
'is_public' => 'boolean',
'track_count' => 'integer',
'view_count' => 'integer',
'download_count' => 'integer',
'favourte_count' => 'integer',
'follow_count' => 'integer',
'comment_count' => 'integer',
];
public static function summary() public static function summary()
{ {
@ -152,7 +195,7 @@ class Playlist extends Model
public function tracks() public function tracks()
{ {
return $this return $this
->belongsToMany('Poniverse\Ponyfm\Track') ->belongsToMany('Poniverse\Ponyfm\Models\Track')
->withPivot('position') ->withPivot('position')
->withTimestamps() ->withTimestamps()
->orderBy('position', 'asc'); ->orderBy('position', 'asc');
@ -166,22 +209,22 @@ class Playlist extends Model
public function users() public function users()
{ {
return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser');
} }
public function comments() public function comments()
{ {
return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
} }
public function pins() public function pins()
{ {
return $this->hasMany('Poniverse\Ponyfm\PinnedPlaylist'); return $this->hasMany('Poniverse\Ponyfm\Models\PinnedPlaylist');
} }
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function hasPinFor($userId) public function hasPinFor($userId)
@ -258,4 +301,27 @@ class Playlist extends Model
{ {
return 'playlist-' . $this->id . '-' . $key; return 'playlist-' . $this->id . '-' . $key;
} }
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'title' => $this->title,
'curator' => $this->user->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->is_public &&
$this->track_count > 0 &&
!$this->trashed();
}
} }

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon; use Carbon\Carbon;
@ -26,6 +26,19 @@ use Auth;
use DB; use DB;
use Request; use Request;
/**
* Poniverse\Ponyfm\Models\ResourceLogItem
*
* @property integer $id
* @property integer $user_id
* @property integer $log_type
* @property string $ip_address
* @property integer $track_format_id
* @property integer $track_id
* @property integer $album_id
* @property integer $playlist_id
* @property \Carbon\Carbon $created_at
*/
class ResourceLogItem extends Model class ResourceLogItem extends Model
{ {
protected $table = 'resource_log_items'; protected $table = 'resource_log_items';

View file

@ -18,10 +18,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\ResourceUser
*
* @property integer $id
* @property integer $user_id
* @property integer $track_id
* @property integer $album_id
* @property integer $playlist_id
* @property integer $artist_id
* @property boolean $is_followed
* @property boolean $is_favourited
* @property boolean $is_pinned
* @property integer $view_count
* @property integer $play_count
* @property integer $download_count
*/
class ResourceUser extends Model class ResourceUser extends Model
{ {
protected $table = 'resource_users'; protected $table = 'resource_users';

View file

@ -18,10 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\Role
*
* @property integer $id
* @property string $name
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\User[] $users
*/
class Role extends Model class Role extends Model
{ {
protected $table = 'roles'; protected $table = 'roles';

View file

@ -18,10 +18,18 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\ShowSong
*
* @property integer $id
* @property string $title
* @property string $lyrics
* @property string $slug
*/
class ShowSong extends Model class ShowSong extends Model
{ {
protected $table = 'show_songs'; protected $table = 'show_songs';

View file

@ -18,13 +18,16 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Auth; use Auth;
use Cache; use Cache;
use Config; use Config;
use DB; use DB;
use Gate;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException; use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Poniverse\Ponyfm\Traits\SlugTrait; use Poniverse\Ponyfm\Traits\SlugTrait;
use Exception; use Exception;
use External; use External;
@ -36,9 +39,68 @@ use Illuminate\Support\Str;
use Log; use Log;
use Venturecraft\Revisionable\RevisionableTrait; use Venturecraft\Revisionable\RevisionableTrait;
class Track extends Model /**
* Poniverse\Ponyfm\Models\Track
*
* @property integer $id
* @property integer $user_id
* @property integer $license_id
* @property integer $genre_id
* @property integer $track_type_id
* @property string $title
* @property string $slug
* @property string $description
* @property string $lyrics
* @property boolean $is_vocal
* @property boolean $is_explicit
* @property integer $cover_id
* @property boolean $is_downloadable
* @property float $duration
* @property integer $play_count
* @property integer $view_count
* @property integer $download_count
* @property integer $favourite_count
* @property integer $comment_count
* @property \Carbon\Carbon $created_at
* @property string $updated_at
* @property \Carbon\Carbon $deleted_at
* @property \Carbon\Carbon $published_at
* @property \Carbon\Carbon $released_at
* @property integer $album_id
* @property integer $track_number
* @property boolean $is_latest
* @property string $hash
* @property boolean $is_listed
* @property string $source
* @property string $original_tags
* @property string $metadata
* @property-read \Poniverse\Ponyfm\Models\Genre $genre
* @property-read \Poniverse\Ponyfm\Models\TrackType $trackType
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Favourite[] $favourites
* @property-read \Poniverse\Ponyfm\Models\Image $cover
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ShowSong[] $showSongs
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users
* @property-read \Poniverse\Ponyfm\Models\User $user
* @property-read \Poniverse\Ponyfm\Models\Album $album
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\TrackFile[] $trackFiles
* @property-read mixed $year
* @property-read mixed $url
* @property-read mixed $download_directory
* @property-read mixed $status
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track userDetails()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track published()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track listed()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track explicitFilter()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track withComments()
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\Track mlpma()
*/
class Track extends Model implements Searchable
{ {
use SoftDeletes; use SoftDeletes, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'track';
protected $dates = ['deleted_at', 'published_at', 'released_at']; protected $dates = ['deleted_at', 'published_at', 'released_at'];
protected $hidden = ['original_tags', 'metadata']; protected $hidden = ['original_tags', 'metadata'];
@ -192,6 +254,9 @@ class Track extends Model
$query->join('mlpma_tracks', 'tracks.id', '=', 'mlpma_tracks.track_id'); $query->join('mlpma_tracks', 'tracks.id', '=', 'mlpma_tracks.track_id');
} }
/**
* @param integer $count
*/
public static function popular($count, $allowExplicit = false) public static function popular($count, $allowExplicit = false)
{ {
$trackIds = Cache::remember('popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'), 5, $trackIds = Cache::remember('popular_tracks'.$count.'-'.($allowExplicit ? 'explicit' : 'safe'), 5,
@ -238,6 +303,10 @@ class Track extends Model
$processed[] = Track::mapPublicTrackSummary($track); $processed[] = Track::mapPublicTrackSummary($track);
} }
// Songs that get played more should drop down
// in the list so they don't hog the top spots.
array_reverse($processed);
return $processed; return $processed;
} }
@ -328,11 +397,11 @@ class Track extends Model
], ],
'url' => $track->url, 'url' => $track->url,
'slug' => $track->slug, 'slug' => $track->slug,
'is_vocal' => (bool)$track->is_vocal, 'is_vocal' => $track->is_vocal,
'is_explicit' => (bool)$track->is_explicit, 'is_explicit' => $track->is_explicit,
'is_downloadable' => (bool)$track->is_downloadable, 'is_downloadable' => $track->is_downloadable,
'is_published' => (bool)$track->isPublished(), 'is_published' => $track->isPublished(),
'published_at' => $track->published_at->format('c'), 'published_at' => $track->isPublished() ? $track->published_at->format('c') : null,
'duration' => $track->duration, 'duration' => $track->duration,
'genre' => $track->genre != null 'genre' => $track->genre != null
? ?
@ -355,8 +424,8 @@ class Track extends Model
], ],
'user_data' => $userData, 'user_data' => $userData,
'permissions' => [ 'permissions' => [
'delete' => Auth::check() && Auth::user()->id == $track->user_id, 'delete' => Gate::allows('delete', $track),
'edit' => Auth::check() && Auth::user()->id == $track->user_id 'edit' => Gate::allows('edit', $track)
] ]
]; ];
} }
@ -371,9 +440,10 @@ class Track extends Model
$returnValue = self::mapPrivateTrackSummary($track); $returnValue = self::mapPrivateTrackSummary($track);
$returnValue['album_id'] = $track->album_id; $returnValue['album_id'] = $track->album_id;
$returnValue['show_songs'] = $showSongs; $returnValue['show_songs'] = $showSongs;
$returnValue['cover_id'] = $track->cover_id;
$returnValue['real_cover_url'] = $track->getCoverUrl(Image::NORMAL); $returnValue['real_cover_url'] = $track->getCoverUrl(Image::NORMAL);
$returnValue['cover_url'] = $track->hasCover() ? $track->getCoverUrl(Image::NORMAL) : null; $returnValue['cover_url'] = $track->hasCover() ? $track->getCoverUrl(Image::NORMAL) : null;
$returnValue['released_at'] = $track->released_at; $returnValue['released_at'] = $track->released_at ? $track->released_at->toDateString() : null;
$returnValue['lyrics'] = $track->lyrics; $returnValue['lyrics'] = $track->lyrics;
$returnValue['description'] = $track->description; $returnValue['description'] = $track->description;
$returnValue['is_downloadable'] = !$track->isPublished() ? true : (bool) $track->is_downloadable; $returnValue['is_downloadable'] = !$track->isPublished() ? true : (bool) $track->is_downloadable;
@ -407,52 +477,52 @@ class Track extends Model
public function genre() public function genre()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Genre'); return $this->belongsTo('Poniverse\Ponyfm\Models\Genre');
} }
public function trackType() public function trackType()
{ {
return $this->belongsTo('Poniverse\Ponyfm\TrackType', 'track_type_id'); return $this->belongsTo('Poniverse\Ponyfm\Models\TrackType', 'track_type_id');
} }
public function comments() public function comments()
{ {
return $this->hasMany('Poniverse\Ponyfm\Comment')->orderBy('created_at', 'desc'); return $this->hasMany('Poniverse\Ponyfm\Models\Comment')->orderBy('created_at', 'desc');
} }
public function favourites() public function favourites()
{ {
return $this->hasMany('Poniverse\Ponyfm\Favourite'); return $this->hasMany('Poniverse\Ponyfm\Models\Favourite');
} }
public function cover() public function cover()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Image'); return $this->belongsTo('Poniverse\Ponyfm\Models\Image');
} }
public function showSongs() public function showSongs()
{ {
return $this->belongsToMany('Poniverse\Ponyfm\ShowSong'); return $this->belongsToMany('Poniverse\Ponyfm\Models\ShowSong');
} }
public function users() public function users()
{ {
return $this->hasMany('Poniverse\Ponyfm\ResourceUser'); return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser');
} }
public function user() public function user()
{ {
return $this->belongsTo('Poniverse\Ponyfm\User'); return $this->belongsTo('Poniverse\Ponyfm\Models\User');
} }
public function album() public function album()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Album'); return $this->belongsTo('Poniverse\Ponyfm\Models\Album');
} }
public function trackFiles() public function trackFiles()
{ {
return $this->hasMany('Poniverse\Ponyfm\TrackFile'); return $this->hasMany('Poniverse\Ponyfm\Models\TrackFile');
} }
public function getYearAttribute() public function getYearAttribute()
@ -592,6 +662,9 @@ class Track extends Model
return "{$this->title}.{$format['extension']}"; return "{$this->title}.{$format['extension']}";
} }
/**
* @return string
*/
public function getFileFor($format) public function getFileFor($format)
{ {
if (!isset(self::$Formats[$format])) { if (!isset(self::$Formats[$format])) {
@ -610,7 +683,7 @@ class Track extends Model
* *
* @return string * @return string
*/ */
public function getTemporarySourceFile() { public function getTemporarySourceFile():string {
return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id; return Config::get('ponyfm.files_directory').'/queued-tracks/'.$this->id;
} }
@ -637,11 +710,11 @@ class Track extends Model
} elseif ( } elseif (
$carry !== static::STATUS_ERROR && $carry !== static::STATUS_ERROR &&
(int) $trackFile->status === TrackFile::STATUS_PROCESSING) { in_array($trackFile->status, [TrackFile::STATUS_PROCESSING, TrackFile::STATUS_PROCESSING_PENDING])) {
return static::STATUS_PROCESSING; return static::STATUS_PROCESSING;
} elseif ( } elseif (
!in_array($carry, [static::STATUS_ERROR, static::STATUS_PROCESSING]) && !in_array($carry, [static::STATUS_ERROR, static::STATUS_PROCESSING, TrackFile::STATUS_PROCESSING_PENDING]) &&
(int) $trackFile->status === TrackFile::STATUS_NOT_BEING_PROCESSED (int) $trackFile->status === TrackFile::STATUS_NOT_BEING_PROCESSED
) { ) {
return static::STATUS_COMPLETE; return static::STATUS_COMPLETE;
@ -670,7 +743,7 @@ class Track extends Model
} }
} }
private function updateTagsForTrackFile($trackFile) { private function updateTagsForTrackFile(TrackFile $trackFile) {
$trackFile->touch(); $trackFile->touch();
if (\File::exists($trackFile->getFile())) { if (\File::exists($trackFile->getFile())) {
@ -699,7 +772,7 @@ class Track extends Model
} }
if ($this->cover !== null) { if ($this->cover !== null) {
$command .= '--artwork ' . $this->cover->getFile() . ' '; $command .= '--artwork '.$this->cover->getFile(Image::ORIGINAL).' ';
} }
$command .= '--overWrite'; $command .= '--overWrite';
@ -740,7 +813,7 @@ class Track extends Model
if ($format == 'MP3' && $this->cover_id != null && is_file($this->cover->getFile())) { if ($format == 'MP3' && $this->cover_id != null && is_file($this->cover->getFile())) {
$tagWriter->tag_data['attached_picture'][0] = [ $tagWriter->tag_data['attached_picture'][0] = [
'data' => file_get_contents($this->cover->getFile()), 'data' => file_get_contents($this->cover->getFile(Image::ORIGINAL)),
'picturetypeid' => 2, 'picturetypeid' => 2,
'description' => 'cover', 'description' => 'cover',
'mime' => $this->cover->mime 'mime' => $this->cover->mime
@ -765,4 +838,28 @@ class Track extends Model
{ {
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')
];
}
} }

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Config; use Config;
use Helpers; use Helpers;
@ -26,17 +26,47 @@ use Illuminate\Database\Eloquent\Model;
use App; use App;
use File; use File;
/**
* Poniverse\Ponyfm\Models\TrackFile
*
* @property integer $id
* @property integer $track_id
* @property boolean $is_master
* @property string $format
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property boolean $is_cacheable
* @property boolean $status
* @property \Carbon\Carbon $expires_at
* @property integer $filesize
* @property-read \Poniverse\Ponyfm\Models\Track $track
* @property-read mixed $extension
* @property-read mixed $url
* @property-read mixed $size
* @property-read mixed $is_expired
*/
class TrackFile extends Model class TrackFile extends Model
{ {
// used for the "status" property // used for the "status" property
const STATUS_NOT_BEING_PROCESSED = 0; const STATUS_NOT_BEING_PROCESSED = 0;
const STATUS_PROCESSING = 1; const STATUS_PROCESSING = 1;
const STATUS_PROCESSING_ERROR = 2; const STATUS_PROCESSING_ERROR = 2;
const STATUS_PROCESSING_PENDING = 3;
protected $appends = ['is_expired'];
protected $dates = ['expires_at'];
protected $casts = [
'id' => 'integer',
'track_id' => 'integer',
'is_master' => 'boolean',
'format' => 'string',
'is_cacheable' => 'boolean',
'status' => 'integer',
'filesize' => 'integer',
];
public function track() public function track() {
{ return $this->belongsTo(Track::class)->withTrashed();
return $this->belongsTo('Poniverse\Ponyfm\Track')->withTrashed();
} }
/** /**
@ -75,6 +105,11 @@ class TrackFile extends Model
} }
} }
public function getIsExpiredAttribute() {
return $this->attributes['expires_at'] === null ||
$this->expires_at->isPast();
}
public function getFormatAttribute($value) public function getFormatAttribute($value)
{ {
return $value; return $value;

View file

@ -18,10 +18,17 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* Poniverse\Ponyfm\Models\TrackType
*
* @property integer $id
* @property string $title
* @property string $editor_title
*/
class TrackType extends Model class TrackType extends Model
{ {
protected $table = 'track_types'; protected $table = 'track_types';

View file

@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm\Models;
use Gravatar; use Gravatar;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
@ -29,11 +29,46 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\Access\Authorizable; use Illuminate\Foundation\Auth\Access\Authorizable;
use Auth; use Auth;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
use Venturecraft\Revisionable\RevisionableTrait; use Venturecraft\Revisionable\RevisionableTrait;
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable /**
* Poniverse\Ponyfm\Models\User
*
* @property integer $id
* @property string $display_name
* @property string $username
* @property boolean $sync_names
* @property string $email
* @property string $gravatar
* @property string $slug
* @property boolean $uses_gravatar
* @property boolean $can_see_explicit_content
* @property string $bio
* @property integer $track_count
* @property integer $comment_count
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property integer $avatar_id
* @property string $remember_token
* @property boolean $is_archived
* @property \Carbon\Carbon $disabled_at
* @property-read \Poniverse\Ponyfm\Models\Image $avatar
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\ResourceUser[] $users
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Role[] $roles
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Comment[] $comments
* @property-read \Illuminate\Database\Eloquent\Collection|\Poniverse\Ponyfm\Models\Track[] $tracks
* @property-read mixed $url
* @property-read mixed $message_url
* @property-read \Illuminate\Database\Eloquent\Collection|\Venturecraft\Revisionable\Revision[] $revisionHistory
* @method static \Illuminate\Database\Query\Builder|\Poniverse\Ponyfm\Models\User userDetails()
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, \Illuminate\Contracts\Auth\Access\Authorizable, Searchable
{ {
use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait; use Authenticatable, CanResetPassword, Authorizable, RevisionableTrait, IndexedInElasticsearchTrait;
protected $elasticsearchType = 'user';
protected $table = 'users'; protected $table = 'users';
protected $casts = [ protected $casts = [
@ -46,6 +81,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
'avatar_id' => 'integer', 'avatar_id' => 'integer',
'is_archived' => 'boolean', 'is_archived' => 'boolean',
]; ];
protected $dates = ['created_at', 'updated_at', 'disabled_at'];
protected $hidden = ['disabled_at'];
public function scopeUserDetails($query) public function scopeUserDetails($query)
{ {
@ -89,12 +126,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function avatar() public function avatar()
{ {
return $this->belongsTo('Poniverse\Ponyfm\Image'); return $this->belongsTo('Poniverse\Ponyfm\Models\Image');
} }
public function users() public function users()
{ {
return $this->hasMany('Poniverse\Ponyfm\ResourceUser', 'artist_id'); return $this->hasMany('Poniverse\Ponyfm\Models\ResourceUser', 'artist_id');
} }
public function roles() public function roles()
@ -104,12 +141,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function comments() public function comments()
{ {
return $this->hasMany('Poniverse\Ponyfm\Comment', 'profile_id')->orderBy('created_at', 'desc'); return $this->hasMany('Poniverse\Ponyfm\Models\Comment', 'profile_id')->orderBy('created_at', 'desc');
} }
public function tracks() public function tracks()
{ {
return $this->hasMany('Poniverse\Ponyfm\Track', 'user_id'); return $this->hasMany('Poniverse\Ponyfm\Models\Track', 'user_id');
} }
public function getIsArchivedAttribute() public function getIsArchivedAttribute()
@ -201,10 +238,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/** /**
* Returns true if this user has the given role. * Returns true if this user has the given role.
* *
* @param $roleName * @param string $roleName
* @return bool * @return bool
*/ */
public function hasRole($roleName) public function hasRole($roleName):bool
{ {
foreach ($this->roles as $role) { foreach ($this->roles as $role) {
if ($role->name === $roleName) { if ($role->name === $roleName) {
@ -214,4 +251,40 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return false; return false;
} }
public static function mapPublicUserSummary(User $user) {
return [
'id' => $user->id,
'name' => $user->display_name,
'slug' => $user->slug,
'url' => $user->url,
'is_archived' => $user->is_archived,
'avatars' => [
'small' => $user->getAvatarUrl(Image::SMALL),
'normal' => $user->getAvatarUrl(Image::NORMAL)
],
'created_at' => $user->created_at
];
}
/**
* Returns this model in Elasticsearch-friendly form. The array returned by
* this method should match the current mapping for this model's ES type.
*
* @return array
*/
public function toElasticsearch():array {
return [
'username' => $this->username,
'display_name' => $this->display_name,
'tracks' => $this->tracks->pluck('title'),
];
}
/**
* @inheritdoc
*/
public function shouldBeIndexed():bool {
return $this->disabled_at === null;
}
} }

View file

@ -20,11 +20,19 @@
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm;
use Poniverse\Ponyfm\Models\Playlist;
use ZipStream; use ZipStream;
class PlaylistDownloader class PlaylistDownloader
{ {
/**
* @var Playlist
*/
private $_playlist; private $_playlist;
/**
* @var string
*/
private $_format; private $_format;
function __construct($playlist, $format) function __construct($playlist, $format)

Some files were not shown because too many files have changed in this diff Show more