From 6c5155f5832d78a0b7050685f23cd81091c9bea8 Mon Sep 17 00:00:00 2001
From: Kelvin Zhang <me@iamkelv.in>
Date: Sun, 8 Nov 2015 17:46:35 +0000
Subject: [PATCH] #4: Implement cached playlist downloads

---
 app/Album.php                                 |  2 +-
 .../Controllers/Api/Web/AlbumsController.php  |  2 +-
 .../Api/Web/PlaylistsController.php           | 36 +++++++
 app/Http/routes.php                           |  5 +-
 app/Playlist.php                              | 93 ++++++++++++++++---
 public/templates/albums/show.html             |  2 +-
 public/templates/playlists/show.html          | 13 ++-
 .../scripts/app/controllers/playlist.coffee   | 18 +++-
 8 files changed, 149 insertions(+), 22 deletions(-)

diff --git a/app/Album.php b/app/Album.php
index a6b8c7be..9c5d2bc3 100644
--- a/app/Album.php
+++ b/app/Album.php
@@ -282,7 +282,7 @@ class Album extends Model
         return $trackCount;
     }
 
-    public function countCacheableTrackFiles($format)
+    public function countCachedTrackFiles($format)
     {
         $cachedCount = 0;
 
diff --git a/app/Http/Controllers/Api/Web/AlbumsController.php b/app/Http/Controllers/Api/Web/AlbumsController.php
index c2536145..67b0b98d 100644
--- a/app/Http/Controllers/Api/Web/AlbumsController.php
+++ b/app/Http/Controllers/Api/Web/AlbumsController.php
@@ -102,7 +102,7 @@ class AlbumsController extends ApiControllerBase
 
         $trackCount = $album->countDownloadableTracks();
         try {
-            $cachedCount = $album->countCacheableTrackFiles($format);
+            $cachedCount = $album->countCachedTrackFiles($format);
         } catch (ModelNotFoundException $e) {
             return $this->notFound('Track file in album not found!');
         }
diff --git a/app/Http/Controllers/Api/Web/PlaylistsController.php b/app/Http/Controllers/Api/Web/PlaylistsController.php
index 7ed44d91..246d2724 100644
--- a/app/Http/Controllers/Api/Web/PlaylistsController.php
+++ b/app/Http/Controllers/Api/Web/PlaylistsController.php
@@ -20,6 +20,7 @@
 
 namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
 
+use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Poniverse\Ponyfm\Commands\AddTrackToPlaylistCommand;
 use Poniverse\Ponyfm\Commands\CreatePlaylistCommand;
 use Poniverse\Ponyfm\Commands\DeletePlaylistCommand;
@@ -31,6 +32,7 @@ use Poniverse\Ponyfm\ResourceLogItem;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Input;
 use Illuminate\Support\Facades\Response;
+use Poniverse\Ponyfm\Track;
 
 class PlaylistsController extends ApiControllerBase
 {
@@ -110,6 +112,40 @@ class PlaylistsController extends ApiControllerBase
         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()
     {
         $query = Playlist
diff --git a/app/Http/routes.php b/app/Http/routes.php
index a2358339..a4e4cd07 100644
--- a/app/Http/routes.php
+++ b/app/Http/routes.php
@@ -79,14 +79,15 @@ Route::group(['prefix' => 'api/web'], function() {
 
     Route::get('/tracks', 'Api\Web\TracksController@getIndex');
     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/{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/{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+');
 
diff --git a/app/Playlist.php b/app/Playlist.php
index f3621122..216b0cb1 100644
--- a/app/Playlist.php
+++ b/app/Playlist.php
@@ -20,19 +20,23 @@
 
 namespace Poniverse\Ponyfm;
 
+use File;
 use Helpers;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\ModelNotFoundException;
 use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Foundation\Bus\DispatchesJobs;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\URL;
+use Poniverse\Ponyfm\Jobs\EncodeTrackFile;
 use Poniverse\Ponyfm\Traits\SlugTrait;
 
 class Playlist extends Model
 {
-    protected $table = 'playlists';
+    use SoftDeletes, SlugTrait, DispatchesJobs;
 
-    use SoftDeletes, SlugTrait;
+    protected $table = 'playlists';
 
     protected $dates = ['deleted_at'];
 
@@ -68,7 +72,8 @@ class Playlist extends Model
                 'name' => $name,
                 'extension' => $format['extension'],
                 '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];
             $userData = [
                 'stats' => [
-                    'views' => (int) $userRow->view_count,
-                    'downloads' => (int) $userRow->download_count,
+                    'views' => (int)$userRow->view_count,
+                    'downloads' => (int)$userRow->download_count,
                 ],
-                'is_favourited' => (bool) $userRow->is_favourited
+                'is_favourited' => (bool)$userRow->is_favourited
             ];
         }
 
         return [
-            'id' => (int) $playlist->id,
+            'id' => (int)$playlist->id,
             'track_count' => $playlist->track_count,
             'title' => $playlist->title,
             'slug' => $playlist->slug,
             'created_at' => $playlist->created_at,
-            'is_public' => (bool) $playlist->is_public,
+            'is_public' => (bool)$playlist->is_public,
             'stats' => [
-                'views' => (int) $playlist->view_count,
-                'downloads' => (int) $playlist->download_count,
-                'comments' => (int) $playlist->comment_count,
-                'favourites' => (int) $playlist->favourite_count
+                'views' => (int)$playlist->view_count,
+                'downloads' => (int)$playlist->download_count,
+                'comments' => (int)$playlist->comment_count,
+                'favourites' => (int)$playlist->favourite_count
             ],
             'covers' => [
                 'small' => $playlist->getCoverUrl(Image::SMALL),
@@ -130,7 +135,7 @@ class Playlist extends Model
             ],
             'url' => $playlist->url,
             'user' => [
-                'id' => (int) $playlist->user->id,
+                'id' => (int)$playlist->user->id,
                 'name' => $playlist->user->display_name,
                 'url' => $playlist->user->url,
             ],
@@ -197,6 +202,63 @@ class Playlist extends Model
         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)
     {
         $tracks = $this->tracks;
@@ -207,7 +269,10 @@ class Playlist extends Model
         return Cache::remember($this->getCacheKey('filesize-' . $format), 1440, function () use ($tracks, $format) {
             $size = 0;
             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;
diff --git a/public/templates/albums/show.html b/public/templates/albums/show.html
index 530c70ac..704aea0e 100644
--- a/public/templates/albums/show.html
+++ b/public/templates/albums/show.html
@@ -16,7 +16,7 @@
                     </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>
+                <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>
         </li>
         <li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li>
diff --git a/public/templates/playlists/show.html b/public/templates/playlists/show.html
index 4dcf9a69..57a2d2f8 100644
--- a/public/templates/playlists/show.html
+++ b/public/templates/playlists/show.html
@@ -5,7 +5,18 @@
                 Downloads
             </a>
             <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>
         </li>
         <li><a href="#" class="btn" pfm-eat-click ng-click="share()">Share</a></li>
diff --git a/resources/assets/scripts/app/controllers/playlist.coffee b/resources/assets/scripts/app/controllers/playlist.coffee
index 6180ab2c..2f39e744 100644
--- a/resources/assets/scripts/app/controllers/playlist.coffee
+++ b/resources/assets/scripts/app/controllers/playlist.coffee
@@ -21,8 +21,8 @@ window.pfm.preloaders['playlist'] = [
 ]
 
 angular.module('ponyfm').controller 'playlist', [
-    '$scope', '$state', 'playlists', '$dialog'
-    ($scope, $state, playlists, $dialog) ->
+    '$scope', '$state', 'playlists', '$dialog', 'download-cached', '$window', '$timeout'
+    ($scope, $state, playlists, $dialog, cachedPlaylist, $window, $timeout) ->
         playlist = null
 
         playlists.fetch($state.params.id).done (playlistResponse) ->
@@ -34,4 +34,18 @@ angular.module('ponyfm').controller 'playlist', [
                 templateUrl: '/templates/partials/playlist-share-dialog.html',
                 controller: ['$scope', ($scope) -> $scope.playlist = playlist; $scope.close = () -> dialog.close()]
             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
 ]
+