diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 5d2c45f8051cdec4245b05133294d46209919b99..159afc4764cdd67b87fae28ec034ced162b1e3fd 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -121,6 +121,14 @@ $routes->group( 'as' => 'podcast-analytics', 'filter' => 'permission:podcasts-view,podcast-view', ]); + $routes->get( + 'analytics-data/(:segment)', + 'AnalyticsData::getData/$1/$2', + [ + 'as' => 'analytics-full-data', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); $routes->get( 'analytics-data/(:segment)/(:segment)', 'AnalyticsData::getData/$1/$2/$3', diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php index a57960dfd1561982dbdf33aab4d32bd2ba8657f3..ba5e1673a39021db6f2430d792813daf8cec20e0 100644 --- a/app/Controllers/Admin/AnalyticsData.php +++ b/app/Controllers/Admin/AnalyticsData.php @@ -23,14 +23,15 @@ class AnalyticsData extends BaseController public function _remap($method, ...$params) { - if (count($params) > 2) { + if (count($params) > 1) { if (!($this->podcast = (new PodcastModel())->find($params[0]))) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound( 'Podcast not found: ' . $params[0] ); } $this->className = '\App\Models\Analytics' . $params[1] . 'Model'; - $this->methodName = 'getData' . $params[2]; + $this->methodName = + 'getData' . (empty($params[2]) ? '' : $params[2]); if (count($params) > 3) { if ( !($this->episode = (new EpisodeModel()) diff --git a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php index e31d932d5f758bdd897334ffba807fea667b10d2..041a195a98109a3230ce4d04b77b8d910fb0eb7e 100644 --- a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php +++ b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php @@ -1,8 +1,8 @@ <?php /** - * Class AddAnalyticsPodcastsByCountry - * Creates analytics_podcasts_by_country table in database + * Class AddAnalyticsPodcasts + * Creates analytics_podcasts table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -30,6 +30,11 @@ class AddAnalyticsPodcasts extends Migration 'constraint' => 10, 'default' => 1, ], + 'unique_listeners' => [ + 'type' => 'INT', + 'constraint' => 10, + 'default' => 1, + ], ]); $this->forge->addPrimaryKey(['podcast_id', 'date']); $this->forge->addField( diff --git a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php index f26977ac01f1ab9ab8f4dad13986069b79257b87..c9c50221ea13651da5909077450a581bb23e4b86 100644 --- a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php +++ b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php @@ -1,8 +1,8 @@ <?php /** - * Class AddAnalyticsEpisodesByCountry - * Creates analytics_episodes_by_country table in database + * Class AddAnalyticsPodcastsByEpisode + * Creates analytics_episodes_by_episode table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ diff --git a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php index 426783c290b752e696638867fe9679cb32822c18..78355e86798bfaa72d2feda2d20685b3f2243934 100644 --- a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php +++ b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php @@ -1,8 +1,8 @@ <?php /** - * Class AddAnalyticsWebsiteByReferer - * Creates analytics_website_by_referer table in database + * Class AddAnalyticsWebsiteByEntryPage + * Creates analytics_website_by_entry_page table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php index caf355009a0420b2581c9cab9439c9737bb29642..7089e51e627a18df346d6bd317848261ec9096cd 100644 --- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php +++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php @@ -17,7 +17,7 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration public function up() { // Creates Stored Procedure for data insertion - // Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer'); + // Example: CALL analytics_podcasts(1, 2, 'FR', 'IDF', 48.853, 2.349, PodcastAddict, 'phone', 'android', 0, 1); $prefix = $this->db->getPrefix(); $createQuery = <<<EOD @@ -31,7 +31,8 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` ( IN `p_app` VARCHAR(128) CHARSET utf8mb4, IN `p_device` VARCHAR(32) CHARSET utf8mb4, IN `p_os` VARCHAR(32) CHARSET utf8mb4, - IN `p_bot` TINYINT(1) UNSIGNED + IN `p_bot` TINYINT(1) UNSIGNED, + IN `p_new_listener` TINYINT(1) UNSIGNED ) MODIFIES SQL DATA DETERMINISTIC SQL SECURITY INVOKER @@ -40,7 +41,7 @@ BEGIN IF NOT `p_bot` THEN INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`) VALUES (p_podcast_id, DATE(NOW())) - ON DUPLICATE KEY UPDATE `hits`=`hits`+1; + ON DUPLICATE KEY UPDATE `hits`=`hits`+1, `unique_listeners`=`unique_listeners`+`p_new_listener`; INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) SELECT p_podcast_id, p_episode_id, DATE(NOW()), datediff(now(),`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id ON DUPLICATE KEY UPDATE `hits`=`hits`+1; diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php index e312a3a0160e6c8838afcc2e529d00d99c5984a4..e17e177ddc6f93cb48f811e3511838887293b219 100644 --- a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php +++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php @@ -114,6 +114,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder 'podcast_id' => $podcast->id, 'date' => date('Y-m-d', $date), 'hits' => $hits, + 'unique_listeners' => $hits, ]; $analytics_podcasts_by_country[] = [ 'podcast_id' => $podcast->id, diff --git a/app/Entities/AnalyticsPodcasts.php b/app/Entities/AnalyticsPodcasts.php index 7f0f169e17cc6a33da15a39890de8b373ccf764e..b15e94e367cf2455b92a2159eb89ef83780e1efd 100644 --- a/app/Entities/AnalyticsPodcasts.php +++ b/app/Entities/AnalyticsPodcasts.php @@ -18,5 +18,6 @@ class AnalyticsPodcasts extends Entity 'podcast_id' => 'integer', 'date' => 'datetime', 'hits' => 'integer', + 'unique_listeners' => 'integer', ]; } diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index cef4e0ac2de225229e7cb472d0009053e613596c..5c242d27c1fa6ae1c2cbf2193a8a9751bd8f182c 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -199,7 +199,7 @@ function webpage_hit($podcast_id) $referer = $session->get('referer'); $domain = empty(parse_url($referer, PHP_URL_HOST)) - ? null + ? '- Direct -' : parse_url($referer, PHP_URL_HOST); parse_str(parse_url($referer, PHP_URL_QUERY), $queries); $keywords = empty($queries['q']) ? null : $queries['q']; @@ -248,9 +248,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) if ($session->get('denyListIp')) { $session->get('player')['bot'] = true; } - $httpRange = $_SERVER['HTTP_RANGE']; - // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID: - $hashID = + //We get the HTTP header field `Range`: + $httpRange = isset($_SERVER['HTTP_RANGE']) + ? $_SERVER['HTTP_RANGE'] + : null; + + // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID (used to count only once multiple episode downloads): + $episodeHashId = '_IpUaEp_' . sha1( $_SERVER['REMOTE_ADDR'] . @@ -260,12 +264,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) $episodeId ); // Was this episode downloaded in the past 24h: - $downloadedBytes = cache($hashID); + $downloadedBytes = cache($episodeHashId); // Rolling window is 24 hours (86400 seconds): - $ttl = 86400; + $rollingTTL = 86400; if ($downloadedBytes) { // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download): - $ttl = cache()->getMetadata($hashID)['expire'] - time(); + $rollingTTL = + cache()->getMetadata($episodeHashId)['expire'] - time(); } else { // If it was never downloaded that means that zero byte were downloaded: $downloadedBytes = 0; @@ -274,7 +279,7 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) // (Otherwise it means that this was already counted, therefore we don't do anything) if ($downloadedBytes < $bytesThreshold) { // If HTTP_RANGE is null we are downloading the complete file: - if (!isset($httpRange)) { + if (!$httpRange) { $downloadedBytes = $fileSize; } else { // [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working. @@ -291,19 +296,44 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) } } // We save the number of downloaded bytes for this user and this episode: - cache()->save($hashID, $downloadedBytes, $ttl); + cache()->save($episodeHashId, $downloadedBytes, $rollingTTL); - // If more that 1mn was downloaded, we send that to the database: + // If more that 1mn was downloaded, that's a hit, we send that to the database: if ($downloadedBytes >= $bytesThreshold) { $db = \Config\Database::connect(); $procedureName = $db->prefixTable('analytics_podcasts'); + // We create a sha1 hash for this IP_Address+User_Agent+Podcast_ID (used to count unique listeners): + $listenerHashId = + '_IpUaPo_' . + sha1( + $_SERVER['REMOTE_ADDR'] . + '_' . + $_SERVER['HTTP_USER_AGENT'] . + '_' . + $podcastId + ); + $newListener = 1; + // Has this listener already downloaded an episode today: + $downloadsByUser = cache($listenerHashId); + // We add one download + if ($downloadsByUser) { + $newListener = 0; + $downloadsByUser++; + } else { + $downloadsByUser = 1; + } + // Listener count is calculated from 00h00 to 23h59: + $midnightTTL = strtotime('tomorrow') - time(); + // We save the download count for this user until midnight: + cache()->save($listenerHashId, $downloadsByUser, $midnightTTL); + $app = $session->get('player')['app']; $device = $session->get('player')['device']; $os = $session->get('player')['os']; $bot = $session->get('player')['bot']; - $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [ + $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?);", [ $podcastId, $episodeId, $session->get('location')['countryCode'], @@ -314,10 +344,12 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) $device == null ? '' : $device, $os == null ? '' : $os, $bot == null ? 0 : $bot, + $newListener, ]); } } } catch (\Exception $e) { // If things go wrong the show must go on and the user must be able to download the file + log_message('critical', $e); } } diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php new file mode 100644 index 0000000000000000000000000000000000000000..c654e32ad921415928c2cf201d50d6925a5f3af0 --- /dev/null +++ b/app/Language/en/Charts.php @@ -0,0 +1,20 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +return [ + 'by_player' => 'Podcast downloads by player (for the past week)', + 'unique_daily_listeners' => 'Daily unique listeners', + 'unique_monthly_listeners' => 'Monthly unique listeners', + 'by_browser' => 'Website usage by browser (for the past week)', + 'podcast_by_day' => 'Podcast daily downloads', + 'podcast_by_month' => 'Podcast monthly downloads', + 'episodes_by_day' => + '5 latest episodes downloads (during their first 60 days)', + 'by_country' => 'Podcast downloads by country (for the past week)', + 'by_domain' => 'Website visits by origin (for the past week)', +]; diff --git a/app/Models/AnalyticsEpisodesByCountryModel.php b/app/Models/AnalyticsEpisodesByCountryModel.php deleted file mode 100644 index 25644114abd29c7f0d58f6c89b41b7248f0e2aac..0000000000000000000000000000000000000000 --- a/app/Models/AnalyticsEpisodesByCountryModel.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -/** - * Class AnalyticsEpisodesByCountry - * Model for analytics_episodes_by_country table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class AnalyticsEpisodesByCountryModel extends Model -{ - protected $table = 'analytics_episodes_by_country'; - protected $primaryKey = 'id'; - - protected $allowedFields = []; - - protected $returnType = \App\Entities\AnalyticsEpisodesByCountry::class; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Models/AnalyticsEpisodesByPlayerModel.php b/app/Models/AnalyticsEpisodesByPlayerModel.php deleted file mode 100644 index efeefb784422adee83cbf3167f7878bd352ce3ae..0000000000000000000000000000000000000000 --- a/app/Models/AnalyticsEpisodesByPlayerModel.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -/** - * Class AnalyticsEpisodesByPlayerModel - * Model for analytics_episodes_by_player table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class AnalyticsEpisodesByPlayerModel extends Model -{ - protected $table = 'analytics_episodes_by_player'; - protected $primaryKey = 'id'; - - protected $allowedFields = []; - - protected $returnType = \App\Entities\AnalyticsEpisodesByPlayer::class; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Models/AnalyticsPodcastsModel.php b/app/Models/AnalyticsPodcastByCountryModel.php similarity index 51% rename from app/Models/AnalyticsPodcastsModel.php rename to app/Models/AnalyticsPodcastByCountryModel.php index 8ab115a0ea6a38733344ed1748cbbc99eb101cc7..d26b5868451136825a836247de75da32ebd298a7 100644 --- a/app/Models/AnalyticsPodcastsModel.php +++ b/app/Models/AnalyticsPodcastByCountryModel.php @@ -1,8 +1,8 @@ <?php /** - * Class AnalyticsPodcastsModel - * Model for analytics_podcasts table in database + * Class AnalyticsPodcastByCountryModel + * Model for analytics_podcasts_by_country table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -12,44 +12,43 @@ namespace App\Models; use CodeIgniter\Model; -class AnalyticsPodcastsModel extends Model +class AnalyticsPodcastByCountryModel extends Model { - protected $table = 'analytics_podcasts'; + protected $table = 'analytics_podcasts_by_country'; protected $allowedFields = []; - protected $returnType = \App\Entities\AnalyticsPodcasts::class; + protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class; protected $useSoftDeletes = false; protected $useTimestamps = false; /** - * Gets all data for a podcast + * Gets country data for a podcast * * @param int $podcastId * * @return array */ - public function getDataByDay(int $podcastId): array + public function getData(int $podcastId): array { - if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) { - $found = $this->select('`date` as `labels`') + if (!($found = cache("{$podcastId}_analytics_podcast_by_country"))) { + $found = $this->select('`country_code` as `labels`') ->selectSum('`hits`', '`values`') + ->groupBy('`country_code`') ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 year')), + '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) - ->groupBy('`labels`') - ->orderBy('`labels``', 'ASC') + ->orderBy('`labels`', 'ASC') ->findAll(); cache()->save( - "{$podcastId}_analytics_podcast_by_day", + "{$podcastId}_analytics_podcast_by_country", $found, - 14400 + 600 ); } - return $found; } } diff --git a/app/Models/AnalyticsPodcastsByEpisodeModel.php b/app/Models/AnalyticsPodcastByEpisodeModel.php similarity index 96% rename from app/Models/AnalyticsPodcastsByEpisodeModel.php rename to app/Models/AnalyticsPodcastByEpisodeModel.php index 59c82360ddcaa6e0b1d284da93103bbc1d758e48..2dc3e0ee1905457b815c1bfbd0607a6e891df5da 100644 --- a/app/Models/AnalyticsPodcastsByEpisodeModel.php +++ b/app/Models/AnalyticsPodcastByEpisodeModel.php @@ -1,7 +1,7 @@ <?php /** - * Class AnalyticsPodcastsByEpisodeModel + * Class AnalyticsPodcastByEpisodeModel * Model for analytics_podcasts_by_episodes table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 @@ -12,7 +12,7 @@ namespace App\Models; use CodeIgniter\Model; -class AnalyticsPodcastsByEpisodeModel extends Model +class AnalyticsPodcastByEpisodeModel extends Model { protected $table = 'analytics_podcasts_by_episode'; @@ -81,7 +81,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model cache()->save( "{$podcastId}_analytics_podcast_by_episode_by_day", $found, - 14400 + 600 ); } return $found; @@ -104,7 +104,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model cache()->save( "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day", $found, - 14400 + 600 ); } return $found; diff --git a/app/Models/AnalyticsPodcastsByPlayerModel.php b/app/Models/AnalyticsPodcastByPlayerModel.php similarity index 88% rename from app/Models/AnalyticsPodcastsByPlayerModel.php rename to app/Models/AnalyticsPodcastByPlayerModel.php index 900b44fa3929003df4737288e676450853d7ef74..dfe3e4938c791aaa554fe863e0541b6167382116 100644 --- a/app/Models/AnalyticsPodcastsByPlayerModel.php +++ b/app/Models/AnalyticsPodcastByPlayerModel.php @@ -1,7 +1,7 @@ <?php /** - * Class AnalyticsPodcastsByPlayerModel + * Class AnalyticsPodcastByPlayerModel * Model for analytics_podcasts_by_player table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 @@ -12,7 +12,7 @@ namespace App\Models; use CodeIgniter\Model; -class AnalyticsPodcastsByPlayerModel extends Model +class AnalyticsPodcastByPlayerModel extends Model { protected $table = 'analytics_podcasts_by_player'; @@ -24,7 +24,7 @@ class AnalyticsPodcastsByPlayerModel extends Model protected $useTimestamps = false; /** - * Gets all data for a podcast + * Gets player data for a podcast * * @param int $podcastId * @@ -41,18 +41,18 @@ class AnalyticsPodcastsByPlayerModel extends Model ->selectSum('`hits`', '`values`') ->where([ '`podcast_id`' => $podcastId, - '`app` !=' => null, + '`app` !=' => '', '`bot`' => 0, '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) ->groupBy('`labels`') - ->orderBy('`values``', 'DESC') + ->orderBy('`values`', 'DESC') ->findAll(10); cache()->save( "{$podcastId}_analytics_podcasts_by_player_by_app", $found, - 14400 + 600 ); } @@ -60,7 +60,7 @@ class AnalyticsPodcastsByPlayerModel extends Model } /** - * Gets all data for a podcast + * Gets device data for a podcast * * @param int $podcastId * @@ -84,7 +84,7 @@ class AnalyticsPodcastsByPlayerModel extends Model '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) ->groupBy('`ids`') - ->orderBy('`values``', 'DESC') + ->orderBy('`values`', 'DESC') ->findAll(); $foundOs = $this->select( @@ -98,7 +98,7 @@ class AnalyticsPodcastsByPlayerModel extends Model '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) ->groupBy('`ids`') - ->orderBy('`values``', 'DESC') + ->orderBy('`values`', 'DESC') ->findAll(); $foundDevice = $this->select( @@ -112,7 +112,7 @@ class AnalyticsPodcastsByPlayerModel extends Model '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) ->groupBy('`ids`') - ->orderBy('`values``', 'DESC') + ->orderBy('`values`', 'DESC') ->findAll(); $foundBot = $this->select( @@ -125,14 +125,14 @@ class AnalyticsPodcastsByPlayerModel extends Model '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) ->groupBy('`ids`') - ->orderBy('`values``', 'DESC') + ->orderBy('`values`', 'DESC') ->findAll(); $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot); cache()->save( "{$podcastId}_analytics_podcasts_by_player_by_device", $found, - 14400 + 600 ); } diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Models/AnalyticsPodcastByRegionModel.php new file mode 100644 index 0000000000000000000000000000000000000000..9953fbe2e05bca3f82f289bd8bc503241bd7a958 --- /dev/null +++ b/app/Models/AnalyticsPodcastByRegionModel.php @@ -0,0 +1,58 @@ +<?php + +/** + * Class AnalyticsPodcastByRegionModel + * Model for analytics_podcasts_by_region table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsPodcastByRegionModel extends Model +{ + protected $table = 'analytics_podcasts_by_region'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; + + /** + * Gets region data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getData(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcast_by_region"))) { + $found = $this->select( + '`country_code`, `region_code`, `latitude`, `longitude`' + ) + ->selectSum('`hits`', '`values`') + ->groupBy( + '`country_code`, `region_code`, `latitude`, `longitude`' + ) + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->orderBy('`country_code`, `region_code`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_by_region", + $found, + 600 + ); + } + return $found; + } +} diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Models/AnalyticsPodcastModel.php new file mode 100644 index 0000000000000000000000000000000000000000..82e5711330bd95c69e7d7480db760cec0500837e --- /dev/null +++ b/app/Models/AnalyticsPodcastModel.php @@ -0,0 +1,146 @@ +<?php + +/** + * Class AnalyticsPodcastModel + * Model for analytics_podcasts table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsPodcastModel extends Model +{ + protected $table = 'analytics_podcasts'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsPodcasts::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; + + /** + * Gets hits data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDay(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) { + $found = $this->select('`date` as `labels`, `hits` as `values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save("{$podcastId}_analytics_podcast_by_day", $found, 600); + } + return $found; + } + + /** + * Gets hits data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByMonth(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcast_by_month"))) { + $found = $this->select( + 'concat(year(`date`),"-",month(`date`),"-01") as `labels`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->orderBy('`date`', 'ASC') + ->groupBy('concat(month(`date`),"-",year(`date`))') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_by_month", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets unique listeners data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataUniqueListenersByDay(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_unique_listeners_by_day" + )) + ) { + $found = $this->select( + '`date` as `labels`, `unique_listeners` as `values`' + ) + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_unique_listeners_by_day", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets unique listeners data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataUniqueListenersByMonth(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_unique_listeners_by_month" + )) + ) { + $found = $this->select( + 'concat(year(`date`),"-",month(`date`),"-01") as `labels`' + ) + ->selectSum('`unique_listeners`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + ]) + ->groupBy('concat(month(`date`),"-",year(`date`))') + ->orderBy('`date`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_unique_listeners_by_month", + $found, + 600 + ); + } + return $found; + } +} diff --git a/app/Models/AnalyticsPodcastsByCountryModel.php b/app/Models/AnalyticsPodcastsByCountryModel.php deleted file mode 100644 index 4f2094532ce8d7d4235a4e202d867899be08f7d9..0000000000000000000000000000000000000000 --- a/app/Models/AnalyticsPodcastsByCountryModel.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * Class AnalyticsPodcastsByCountryModel - * Model for analytics_podcasts_by_country table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class AnalyticsPodcastsByCountryModel extends Model -{ - protected $table = 'analytics_podcasts_by_country'; - - protected $allowedFields = []; - - protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Models/AnalyticsPodcastsByRegionModel.php b/app/Models/AnalyticsPodcastsByRegionModel.php deleted file mode 100644 index 81ab8537f89a322fda5b0b5973c23b2aea736eed..0000000000000000000000000000000000000000 --- a/app/Models/AnalyticsPodcastsByRegionModel.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * Class AnalyticsPodcastsByRegionModel - * Model for analytics_podcasts_by_region table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class AnalyticsPodcastsByRegionModel extends Model -{ - protected $table = 'analytics_podcasts_by_region'; - - protected $allowedFields = []; - - protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Models/AnalyticsWebsiteByBrowserModel.php index 85b4fc92fe65d8fde7e19f11d1b4790b5099f84e..4d5c0e280166fa9a1c96b2a89237724a1eb5ccf0 100644 --- a/app/Models/AnalyticsWebsiteByBrowserModel.php +++ b/app/Models/AnalyticsWebsiteByBrowserModel.php @@ -22,4 +22,33 @@ class AnalyticsWebsiteByBrowserModel extends Model protected $useSoftDeletes = false; protected $useTimestamps = false; + + /** + * Gets browser data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getData(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_website_by_browser"))) { + $found = $this->select('`browser` as `labels`') + ->selectSum('`hits`', '`values`') + ->groupBy('`browser`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->orderBy('`browser`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_website_by_browser", + $found, + 600 + ); + } + return $found; + } } diff --git a/app/Models/AnalyticsWebsiteByCountryModel.php b/app/Models/AnalyticsWebsiteByCountryModel.php deleted file mode 100644 index 0163b65bfe8bade82ec7e9a762dbe0ed0e9888bc..0000000000000000000000000000000000000000 --- a/app/Models/AnalyticsWebsiteByCountryModel.php +++ /dev/null @@ -1,26 +0,0 @@ -<?php - -/** - * Class AnalyticsWebsiteByCountryModel - * Model for analytics_website_by_country table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Models; - -use CodeIgniter\Model; - -class AnalyticsWebsiteByCountryModel extends Model -{ - protected $table = 'analytics_website_by_country'; - protected $primaryKey = 'id'; - - protected $allowedFields = []; - - protected $returnType = \App\Entities\AnalyticsWebsiteByCountry::class; - protected $useSoftDeletes = false; - - protected $useTimestamps = false; -} diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Models/AnalyticsWebsiteByEntryPageModel.php index 6e7cfa0c9a0e2f31189f79ed3eb744b8ff123e1c..91c4053a1f13375c5082119481d6819c217538fb 100644 --- a/app/Models/AnalyticsWebsiteByEntryPageModel.php +++ b/app/Models/AnalyticsWebsiteByEntryPageModel.php @@ -22,4 +22,33 @@ class AnalyticsWebsiteByEntryPageModel extends Model protected $useSoftDeletes = false; protected $useTimestamps = false; + + /** + * Gets entry pages data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getData(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_website_by_entry_page"))) { + $found = $this->select('`entry_page` as `labels`') + ->selectSum('`hits`', '`values`') + ->groupBy('`entry_page`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->orderBy('`entry_page`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_website_by_entry_page", + $found, + 600 + ); + } + return $found; + } } diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Models/AnalyticsWebsiteByRefererModel.php index 5d60298ce1cee9f526a1c0c3cf3bfd0b09d74ab8..e45ea20ba2e7ef89c970a1f89fe57645ccf750c1 100644 --- a/app/Models/AnalyticsWebsiteByRefererModel.php +++ b/app/Models/AnalyticsWebsiteByRefererModel.php @@ -22,4 +22,62 @@ class AnalyticsWebsiteByRefererModel extends Model protected $useSoftDeletes = false; protected $useTimestamps = false; + + /** + * Gets referer data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getData(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_website_by_referer"))) { + $found = $this->select('`referer` as `labels`') + ->selectSum('`hits`', '`values`') + ->groupBy('`referer`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->orderBy('`referer`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_website_by_referer", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets domain data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDomain(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_website_by_domain"))) { + $found = $this->select('`domain` as `labels`') + ->selectSum('`hits`', '`values`') + ->groupBy('`domain`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->orderBy('`domain`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_website_by_domain", + $found, + 600 + ); + } + return $found; + } } diff --git a/app/Views/admin/podcast/analytics.php b/app/Views/admin/podcast/analytics.php index c57559b648bbdeaedc52ed29d8622d87841048b4..c09619f23448c7139dea16c2650137f38163709a 100644 --- a/app/Views/admin/podcast/analytics.php +++ b/app/Views/admin/podcast/analytics.php @@ -9,24 +9,76 @@ <?= $this->endSection() ?> <?= $this->section('content') ?> -<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + +<h2><?= lang('Charts.podcast_by_day') ?></h2> +<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( 'analytics-data', $podcast->id, - 'PodcastsByPlayer', - 'ByApp' + 'Podcast', + 'ByDay' ) ?>"></div> -<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + +<h2><?= lang('Charts.podcast_by_month') ?></h2> +<div class="h-64" id="by-month-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( 'analytics-data', $podcast->id, - 'Podcasts', - 'ByDay' + 'Podcast', + 'ByMonth' ) ?>"></div> + +<h2><?= lang('Charts.unique_daily_listeners') ?></h2> +<div class="h-64" id="by-day-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'UniqueListenersByDay' +) ?>"></div> + +<h2><?= lang('Charts.unique_monthly_listeners') ?></h2> +<div class="h-64" id="by-month-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'UniqueListenersByMonth' +) ?>"></div> + +<h2><?= lang('Charts.episodes_by_day') ?></h2> <div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to( 'analytics-data', $podcast->id, - 'PodcastsByEpisode', + 'PodcastByEpisode', 'ByDay' ) ?>"></div> +<h2><?= lang('Charts.by_player') ?></h2> +<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'ByApp' +) ?>"></div> + +<h2><?= lang('Charts.by_browser') ?></h2> +<div class="h-64" id="by-browser-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'WebsiteByBrowser' +) ?>"></div> + +<h2><?= lang('Charts.by_country') ?></h2> +<div class="h-64" id="by-country-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'PodcastByCountry' +) ?>"></div> + +<h2><?= lang('Charts.by_domain') ?></h2> +<div class="h-64" id="by-domain-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'WebsiteByReferer', + 'ByDomain' +) ?>"></div> + <script src="/assets/charts.js" type="module"></script> <?= $this->endSection() ?>