diff --git a/app/Http/Controllers/Api/Web/StatsController.php b/app/Http/Controllers/Api/Web/StatsController.php new file mode 100644 index 00000000..fc37a38c --- /dev/null +++ b/app/Http/Controllers/Api/Web/StatsController.php @@ -0,0 +1,139 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; + +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; +use Poniverse\Ponyfm\Models\ResourceLogItem; +use Poniverse\Ponyfm\Models\Track; +use Auth; +use Cache; +use DB; +use Response; +use Carbon\Carbon; + +class StatsController extends ApiControllerBase +{ + private function getStatsData($id, $hourly = false) { + $playRange = "1 MONTH"; + + if ($hourly) { + $playRange = "2 DAY"; + } + + $statQuery = DB::table('resource_log_items') + ->selectRaw('created_at, COUNT(1) AS `plays`') + ->where('track_id', '=', $id) + ->where('log_type', '=', ResourceLogItem::PLAY) + ->whereRaw('`created_at` > now() - INTERVAL ' . $playRange) + ->groupBy('created_at') + ->orderBy('created_at') + ->get(); + + return $statQuery; + } + + private function sortTrackStatsArray($query, $hourly = false) { + $now = Carbon::now(); + $playsArray = []; + $output = []; + + if ($hourly) { + $playsArray = array_fill(0, 24, 0); + } else { + $playsArray = array_fill(0, 30, 0); + } + + foreach($query as $item) { + $playDate = new Carbon($item->created_at); + + $key = 0; + if ($hourly) { + $key = $playDate->diffInHours($now); + } else { + $key = $playDate->diffInDays($now); + } + + if (array_key_exists($key, $playsArray)) { + $playsArray[$key] += $item->plays; + } else { + $playsArray[$key] = $item->plays; + } + } + + krsort($playsArray); + + // Covert playsArray into output we can understand + foreach($playsArray as $timeOffet => $plays) { + if ($hourly) { + $set = [ + 'hours' => $timeOffet . ' ' . str_plural('hour', $timeOffet), + 'plays' => $plays + ]; + } else { + $set = [ + 'days' => $timeOffet . ' ' . str_plural('day', $timeOffet), + 'plays' => $plays + ]; + } + array_push($output, $set); + } + + if ($hourly) { + return Response::json(['playStats' => $output, 'type' => 'Hourly'], 200); + } else { + return Response::json(['playStats' => $output, 'type' => 'Daily'], 200); + } + } + + public function getTrackStats($id) { + $cachedOutput = Cache::remember('track_stats'.$id, 5, function() use ($id) { + try { + $track = Track::published()->findOrFail($id); + } catch (ModelNotFoundException $e) { + return $this->notFound('Track not found!'); + } + + // Do we have permission to view this track? + if (!$track->canView(Auth::user())) { + return $this->notFound('Track not found!'); + } + + // Run one of the functions depending on + // how old the track is + $now = Carbon::now(); + $trackDate = $track->published_at; + + $hourly = true; + + if ($trackDate->diffInDays($now) >= 1) { + $hourly = false; + } + + $statsData = $this->getStatsData($id, $hourly); + + $output = $this->sortTrackStatsArray($statsData, $hourly); + return $output; + }); + + return $cachedOutput; + } +} diff --git a/app/Http/Controllers/StatsController.php b/app/Http/Controllers/StatsController.php new file mode 100644 index 00000000..53863064 --- /dev/null +++ b/app/Http/Controllers/StatsController.php @@ -0,0 +1,31 @@ +. + */ + +namespace Poniverse\Ponyfm\Http\Controllers; + +use View; + +class StatsController extends Controller +{ + public function getIndex() + { + return View::make('tracks.stats'); + } +} diff --git a/app/Http/routes.php b/app/Http/routes.php index fb907ea2..52cd0cf2 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -36,6 +36,7 @@ Route::get('/tracks/random', 'TracksController@getIndex'); Route::get('tracks/{id}-{slug}', 'TracksController@getTrack'); Route::get('tracks/{id}-{slug}/edit', 'TracksController@getEdit'); +Route::get('tracks/{id}-{slug}/stats', 'StatsController@getIndex'); Route::get('t{id}', 'TracksController@getShortlink' )->where('id', '\d+'); Route::get('t{id}/embed', 'TracksController@getEmbed' ); Route::get('t{id}/stream.{extension}', 'TracksController@getStream' ); @@ -84,6 +85,7 @@ 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+', 'format' => '.+']); + Route::get('/tracks/{id}/stats', 'Api\Web\StatsController@getTrackStats')->where('id', '\d+'); Route::get('/albums', 'Api\Web\AlbumsController@getIndex'); Route::get('/albums/{id}', 'Api\Web\AlbumsController@getShow')->where('id', '\d+'); diff --git a/package.json b/package.json index 32c2ef9d..b9c0df2e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,9 @@ "dependencies": {}, "devDependencies": { "angular": "^1.5.0", + "angular-chart.js": "^1.0.0-alpha6", "angular-ui-router": "^0.2.18", + "chart.js": "^2.1.0", "coffee-loader": "^0.7.2", "coffee-script": "^1.10.0", "gulp": "^3.9.0", diff --git a/public/templates/partials/credits-dialog.html b/public/templates/partials/credits-dialog.html index ec31d0a3..abf7a30d 100644 --- a/public/templates/partials/credits-dialog.html +++ b/public/templates/partials/credits-dialog.html @@ -47,6 +47,9 @@
  • Colorbox - for allowing us to have pretty lightboxes
  • Marked - for the Markdown parser
  • angular-marked - for giving us an Angular way to use Marked
  • +
  • angular-marked - for giving us an Angular way to use Marked
  • +
  • chart.js - for giving us beautiful, programmable charts
  • +
  • angular-chart.js - for making using chart.js with Angular easy
  • diff --git a/public/templates/tracks/stats.html b/public/templates/tracks/stats.html new file mode 100644 index 00000000..595b9bd6 --- /dev/null +++ b/public/templates/tracks/stats.html @@ -0,0 +1,10 @@ +
    + Return to track +
    +

    Geeky Stats For Geeky People (BETA)

    +

    Plays over time

    +
    + + +
    diff --git a/resources/assets/scripts/app/app.coffee b/resources/assets/scripts/app/app.coffee index f223b598..607aaf8d 100644 --- a/resources/assets/scripts/app/app.coffee +++ b/resources/assets/scripts/app/app.coffee @@ -43,13 +43,15 @@ require 'script!../base/moment' require '../base/soundmanager2-nodebug' require 'script!../base/tumblr' require '../base/ui-bootstrap-tpls-0.4.0' +require 'chart.js'; +require 'angular-chart.js'; require '../shared/pfm-angular-marked' require '../shared/pfm-angular-sanitize' require '../shared/init.coffee' -ponyfm = angular.module 'ponyfm', ['ui.bootstrap', 'ui.router', 'ui.date', 'ui.sortable', 'angularytics', 'ngSanitize', 'hc.marked'] +ponyfm = angular.module 'ponyfm', ['ui.bootstrap', 'ui.router', 'ui.date', 'ui.sortable', 'angularytics', 'ngSanitize', 'hc.marked', 'chart.js'] window.pfm.preloaders = {} # Inspired by: https://stackoverflow.com/a/30652110/3225811 @@ -174,6 +176,11 @@ ponyfm.config [ templateUrl: '/templates/tracks/edit.html' controller: 'track-edit' + state.state 'content.track.stats', + url: '/stats' + templateUrl: '/templates/tracks/stats.html' + controller: 'track-stats' + # Albums diff --git a/resources/assets/scripts/app/controllers/track-stats.coffee b/resources/assets/scripts/app/controllers/track-stats.coffee new file mode 100644 index 00000000..68563244 --- /dev/null +++ b/resources/assets/scripts/app/controllers/track-stats.coffee @@ -0,0 +1,57 @@ +# Pony.fm - A community for pony fan music. +# Copyright (C) 2016 Josef Citrine +# +# 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 . + + +module.exports = angular.module('ponyfm').controller 'track-stats', [ + '$scope', '$state', 'track-stats' + ($scope, $state, statsService) -> + $scope.trackId = parseInt($state.params.id) + + labelArray = [] + dailyArray = [] + cumulativeArray = [] + + statsLoaded = (stats) -> + for key, value of stats.playStats + labelArray.push value.hour || value.days + dailyArray.push value.plays + + i = 0 + while i < dailyArray.length + if i == 0 + cumulativeArray[i] = dailyArray[0] + else + cumulativeArray[i] = cumulativeArray[i - 1] + dailyArray[i] + i++ + + $scope.playsLabels = labelArray + $scope.playsData = cumulativeArray + $scope.colours = ['#B885BD'] + $scope.series = ['Plays'] + $scope.totalSelected = true + + $scope.dailyText = stats.type + + $scope.totalClick = () -> + $scope.playsData = cumulativeArray + $scope.totalSelected = true + + $scope.dailyClick = () -> + $scope.playsData = dailyArray + $scope.totalSelected = false + + statsService.loadStats($scope.trackId).done statsLoaded +] diff --git a/resources/assets/scripts/app/services/track-stats.coffee b/resources/assets/scripts/app/services/track-stats.coffee new file mode 100644 index 00000000..97099856 --- /dev/null +++ b/resources/assets/scripts/app/services/track-stats.coffee @@ -0,0 +1,33 @@ +# Pony.fm - A community for pony fan music. +# Copyright (C) 2016 Josef Citrine +# +# 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 . + +module.exports = angular.module('ponyfm').factory('track-stats', [ + '$rootScope', '$http' + ($rootScope, $http) -> + stats = [] + + self = + loadStats: (id) -> + return def if def + def = new $.Deferred() + url = "/api/web/tracks/#{ id }/stats" + $http.get(url).success (stats) -> + def.resolve stats + + def.promise() + + self +]) diff --git a/resources/assets/styles/base/angular-chart.css b/resources/assets/styles/base/angular-chart.css new file mode 100644 index 00000000..9fc8593e --- /dev/null +++ b/resources/assets/styles/base/angular-chart.css @@ -0,0 +1,49 @@ +.chart-legend, +.bar-legend, +.line-legend, +.pie-legend, +.radar-legend, +.polararea-legend, +.doughnut-legend { + list-style-type: none; + margin-top: 5px; + text-align: center; + /* NOTE: Browsers automatically add 40px of padding-left to all lists, so we should offset that, otherwise the legend is off-center */ + -webkit-padding-start: 0; + /* Webkit */ + -moz-padding-start: 0; + /* Mozilla */ + padding-left: 0; + /* IE (handles all cases, really, but we should also include the vendor-specific properties just to be safe) */ +} +.chart-legend li, +.bar-legend li, +.line-legend li, +.pie-legend li, +.radar-legend li, +.polararea-legend li, +.doughnut-legend li { + display: inline-block; + white-space: nowrap; + position: relative; + margin-bottom: 4px; + border-radius: 5px; + padding: 2px 8px 2px 28px; + font-size: smaller; + cursor: default; +} +.chart-legend-icon, +.bar-legend-icon, +.line-legend-icon, +.pie-legend-icon, +.radar-legend-icon, +.polararea-legend-icon, +.doughnut-legend-icon { + display: block; + position: absolute; + left: 0; + top: 0; + width: 20px; + height: 20px; + border-radius: 5px; +} diff --git a/resources/assets/styles/components/components.less b/resources/assets/styles/components/components.less index 4976e753..749651a7 100644 --- a/resources/assets/styles/components/components.less +++ b/resources/assets/styles/components/components.less @@ -212,6 +212,10 @@ html body { color: #eee; } + .btn.selected { + background: #7A4F7D; + } + .ui-datepicker { .border-radius(0px); diff --git a/resources/assets/styles/layout.less b/resources/assets/styles/layout.less index 39dc35cf..5886807c 100644 --- a/resources/assets/styles/layout.less +++ b/resources/assets/styles/layout.less @@ -243,3 +243,11 @@ header { .file-over-notice { display: none; } + +.chart-container { + width: 50%; +} + +.chart-btn-container { + margin-bottom: 10px; +} diff --git a/resources/views/tracks/stats.blade.php b/resources/views/tracks/stats.blade.php new file mode 100644 index 00000000..9e9d4853 --- /dev/null +++ b/resources/views/tracks/stats.blade.php @@ -0,0 +1,24 @@ +{{-- + Pony.fm - A community for pony fan music. + Copyright (C) 2016 Josef Citrine + + 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 . +--}} + +@extends('shared._app_layout') + +@section('app_content') +

    Track Stats!

    +

    This page should be what search engines see

    +@endsection