#4: Implement cached playlist downloads

This commit is contained in:
Kelvin Zhang 2015-11-08 17:46:35 +00:00
parent 80ad614b5f
commit 6c5155f583
8 changed files with 149 additions and 22 deletions

View file

@ -282,7 +282,7 @@ class Album extends Model
return $trackCount; return $trackCount;
} }
public function countCacheableTrackFiles($format) public function countCachedTrackFiles($format)
{ {
$cachedCount = 0; $cachedCount = 0;

View file

@ -102,7 +102,7 @@ class AlbumsController extends ApiControllerBase
$trackCount = $album->countDownloadableTracks(); $trackCount = $album->countDownloadableTracks();
try { try {
$cachedCount = $album->countCacheableTrackFiles($format); $cachedCount = $album->countCachedTrackFiles($format);
} catch (ModelNotFoundException $e) { } catch (ModelNotFoundException $e) {
return $this->notFound('Track file in album not found!'); return $this->notFound('Track file in album not found!');
} }

View file

@ -20,6 +20,7 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Poniverse\Ponyfm\Commands\AddTrackToPlaylistCommand; use Poniverse\Ponyfm\Commands\AddTrackToPlaylistCommand;
use Poniverse\Ponyfm\Commands\CreatePlaylistCommand; use Poniverse\Ponyfm\Commands\CreatePlaylistCommand;
use Poniverse\Ponyfm\Commands\DeletePlaylistCommand; use Poniverse\Ponyfm\Commands\DeletePlaylistCommand;
@ -31,6 +32,7 @@ use Poniverse\Ponyfm\ResourceLogItem;
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;
use Poniverse\Ponyfm\Track;
class PlaylistsController extends ApiControllerBase class PlaylistsController extends ApiControllerBase
{ {
@ -110,6 +112,40 @@ class PlaylistsController extends ApiControllerBase
return Response::json(Playlist::mapPublicPlaylistShow($playlist), 200); return Response::json(Playlist::mapPublicPlaylistShow($playlist), 200);
} }
public function getCachedPlaylist($id, $format)
{
// Validation
try {
$playlist = Playlist::with('tracks.trackFiles')->findOrFail($id);
} catch (ModelNotFoundException $e) {
return $this->notFound('Playlist not found!');
}
if ((!$playlist->is_public && !Auth::check()) || (!$playlist->is_public && ($playlist->user_id !== Auth::user()->id))) {
return $this->notFound('Playlist not found!');
}
if (!in_array($format, Track::$CacheableFormats)) {
return $this->notFound('Format not found!');
}
$trackCount = $playlist->countDownloadableTracks();
try {
$cachedCount = $playlist->countCachedTrackFiles($format);
} catch (ModelNotFoundException $e) {
return $this->notFound('Track file in playlist not found!');
}
if ($trackCount === $cachedCount) {
$url = $playlist->getDownloadUrl($format);
} else {
$playlist->encodeCacheableTrackFiles($format);
$url = null;
}
return Response::json(['url' => $url], 200);
}
public function getPinned() public function getPinned()
{ {
$query = Playlist $query = Playlist

View file

@ -79,14 +79,15 @@ Route::group(['prefix' => 'api/web'], function() {
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+');
Route::get('/tracks/cached/{id}/{format}', 'Api\Web\TracksController@getCachedTrack')->where(['id' => '\d+', 'name' => '.+']); Route::get('/tracks/cached/{id}/{format}', 'Api\Web\TracksController@getCachedTrack')->where(['id' => '\d+', 'format' => '.+']);
Route::get('/albums', 'Api\Web\AlbumsController@getIndex'); Route::get('/albums', 'Api\Web\AlbumsController@getIndex');
Route::get('/albums/{id}', 'Api\Web\AlbumsController@getShow')->where('id', '\d+'); Route::get('/albums/{id}', 'Api\Web\AlbumsController@getShow')->where('id', '\d+');
Route::get('/albums/cached/{id}/{format}', 'Api\Web\AlbumsController@getCachedAlbum')->where(['id' => '\d+', 'name' => '.+']); 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/{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('/comments/{type}/{id}', 'Api\Web\CommentsController@getIndex')->where('id', '\d+'); Route::get('/comments/{type}/{id}', 'Api\Web\CommentsController@getIndex')->where('id', '\d+');

View file

@ -20,19 +20,23 @@
namespace Poniverse\Ponyfm; namespace Poniverse\Ponyfm;
use File;
use Helpers; use Helpers;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
use Poniverse\Ponyfm\Traits\SlugTrait; use Poniverse\Ponyfm\Traits\SlugTrait;
class Playlist extends Model class Playlist extends Model
{ {
protected $table = 'playlists'; use SoftDeletes, SlugTrait, DispatchesJobs;
use SoftDeletes, SlugTrait; protected $table = 'playlists';
protected $dates = ['deleted_at']; protected $dates = ['deleted_at'];
@ -68,7 +72,8 @@ class Playlist extends Model
'name' => $name, 'name' => $name,
'extension' => $format['extension'], 'extension' => $format['extension'],
'url' => $playlist->getDownloadUrl($name), 'url' => $playlist->getDownloadUrl($name),
'size' => Helpers::formatBytes($playlist->getFilesize($name)) 'size' => Helpers::formatBytes($playlist->getFilesize($name)),
'isCacheable' => (in_array($name, Track::$CacheableFormats) ? true : false)
]; ];
} }
@ -104,25 +109,25 @@ class Playlist extends Model
$userRow = $playlist->users[0]; $userRow = $playlist->users[0];
$userData = [ $userData = [
'stats' => [ 'stats' => [
'views' => (int) $userRow->view_count, 'views' => (int)$userRow->view_count,
'downloads' => (int) $userRow->download_count, 'downloads' => (int)$userRow->download_count,
], ],
'is_favourited' => (bool) $userRow->is_favourited 'is_favourited' => (bool)$userRow->is_favourited
]; ];
} }
return [ return [
'id' => (int) $playlist->id, 'id' => (int)$playlist->id,
'track_count' => $playlist->track_count, 'track_count' => $playlist->track_count,
'title' => $playlist->title, 'title' => $playlist->title,
'slug' => $playlist->slug, 'slug' => $playlist->slug,
'created_at' => $playlist->created_at, 'created_at' => $playlist->created_at,
'is_public' => (bool) $playlist->is_public, 'is_public' => (bool)$playlist->is_public,
'stats' => [ 'stats' => [
'views' => (int) $playlist->view_count, 'views' => (int)$playlist->view_count,
'downloads' => (int) $playlist->download_count, 'downloads' => (int)$playlist->download_count,
'comments' => (int) $playlist->comment_count, 'comments' => (int)$playlist->comment_count,
'favourites' => (int) $playlist->favourite_count 'favourites' => (int)$playlist->favourite_count
], ],
'covers' => [ 'covers' => [
'small' => $playlist->getCoverUrl(Image::SMALL), 'small' => $playlist->getCoverUrl(Image::SMALL),
@ -130,7 +135,7 @@ class Playlist extends Model
], ],
'url' => $playlist->url, 'url' => $playlist->url,
'user' => [ 'user' => [
'id' => (int) $playlist->user->id, 'id' => (int)$playlist->user->id,
'name' => $playlist->user->display_name, 'name' => $playlist->user->display_name,
'url' => $playlist->user->url, 'url' => $playlist->user->url,
], ],
@ -197,6 +202,63 @@ class Playlist extends Model
return URL::to('p' . $this->id . '/dl.' . Track::$Formats[$format]['extension']); return URL::to('p' . $this->id . '/dl.' . Track::$Formats[$format]['extension']);
} }
public function countDownloadableTracks()
{
$trackCount = 0;
foreach ($this->tracks as $track) {
if ($track->is_downloadable == true) {
$trackCount++;
} else {
continue;
}
}
return $trackCount;
}
public function countCachedTrackFiles($format)
{
$cachedCount = 0;
foreach ($this->tracks as $track) {
if ($track->is_downloadable == false) {
continue;
}
try {
$trackFile = $track->trackFiles()->where('format', $format)->firstOrFail();
} catch (ModelNotFoundException $e) {
throw $e;
}
if ($trackFile->expires_at != null && File::exists($trackFile->getFile())) {
$cachedCount++;
}
}
return $cachedCount;
}
public function encodeCacheableTrackFiles($format)
{
foreach ($this->tracks as $track) {
if ($track->is_downloadable == false) {
continue;
}
try {
$trackFile = $track->trackFiles()->where('format', $format)->firstOrFail();
} catch (ModelNotFoundException $e) {
throw $e;
}
if (!File::exists($trackFile->getFile()) && $trackFile->is_in_progress != true) {
$this->dispatch(new EncodeTrackFile($trackFile, true));
}
}
}
public function getFilesize($format) public function getFilesize($format)
{ {
$tracks = $this->tracks; $tracks = $this->tracks;
@ -207,7 +269,10 @@ class Playlist extends Model
return Cache::remember($this->getCacheKey('filesize-' . $format), 1440, function () use ($tracks, $format) { return Cache::remember($this->getCacheKey('filesize-' . $format), 1440, function () use ($tracks, $format) {
$size = 0; $size = 0;
foreach ($tracks as $track) { foreach ($tracks as $track) {
$size += $track->getFilesize($format); // Ensure that only downloadable tracks are added onto the file size
if ($track->is_downloadable == 1) {
$size += $track->getFilesize($format);
}
} }
return $size; return $size;

View file

@ -16,7 +16,7 @@
</a> </a>
</li> </li>
<li ng-show="isInProgress" class="cache-loading"><img src="/images/loading.gif" /></li> <li ng-show="isInProgress" class="cache-loading"><img src="/images/loading.gif" /></li>
<li ng-show="isInProgress" class="cache-loading"><small>We&#39;re getting your download ready! This may take up to a few minutes .</small></li> <li ng-show="isInProgress" class="cache-loading"><small>We&#39;re getting your download ready! This may take up to a few minutes.</small></li>
</ul> </ul>
</li> </li>
<li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li> <li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li>

View file

@ -5,7 +5,18 @@
Downloads Downloads
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li bindonce ng-repeat="format in playlist.formats"><a target="_blank" bo-href="format.url"><span bo-text="format.name"></span> <small bo-text="'(' + format.size + ')'"></small></a></li> <li bindonce ng-repeat="format in playlist.formats" ng-hide="isInProgress">
<a target="_blank" ng-if="!format.isCacheable" bo-href="format.url">
<span bo-text="format.name"></span>
<small bo-text="'(' + format.size + ')'"></small>
</a>
<a ng-if="format.isCacheable" ng-click="getCachedPlaylist(playlist.id, format.name);" href="">
<span bo-text="format.name"></span>
<small bo-text="'(' + format.size + ')'"></small>
</a>
</li>
<li ng-show="isInProgress" class="cache-loading"><img src="/images/loading.gif" /></li>
<li ng-show="isInProgress" class="cache-loading"><small>We&#39;re getting your download ready! This may take up to a few minutes.</small></li>
</ul> </ul>
</li> </li>
<li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li> <li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li>

View file

@ -21,8 +21,8 @@ window.pfm.preloaders['playlist'] = [
] ]
angular.module('ponyfm').controller 'playlist', [ angular.module('ponyfm').controller 'playlist', [
'$scope', '$state', 'playlists', '$dialog' '$scope', '$state', 'playlists', '$dialog', 'download-cached', '$window', '$timeout'
($scope, $state, playlists, $dialog) -> ($scope, $state, playlists, $dialog, cachedPlaylist, $window, $timeout) ->
playlist = null playlist = null
playlists.fetch($state.params.id).done (playlistResponse) -> playlists.fetch($state.params.id).done (playlistResponse) ->
@ -34,4 +34,18 @@ angular.module('ponyfm').controller 'playlist', [
templateUrl: '/templates/partials/playlist-share-dialog.html', templateUrl: '/templates/partials/playlist-share-dialog.html',
controller: ['$scope', ($scope) -> $scope.playlist = playlist; $scope.close = () -> dialog.close()] controller: ['$scope', ($scope) -> $scope.playlist = playlist; $scope.close = () -> dialog.close()]
dialog.open() dialog.open()
$scope.getCachedPlaylist = (id, format) ->
$scope.isInProgress = true
cachedPlaylist.download('playlists', id, format).then (response) ->
$scope.playlistUrl = response
if $scope.playlistUrl == 'error'
$scope.isInProgress = false
else if $scope.playlistUrl == 'pending'
$timeout $scope.getCachedPlaylist(id, format), 5000
else
$scope.isInProgress = false
$window.open $scope.playlistUrl
] ]