feat: add unique listeners analytics

- add unique listener
- add some charts
- correct minor bugs
This commit is contained in:
Benjamin Bellamy 2020-10-08 14:45:46 +00:00 committed by Yassine Doghri
parent 9660aa97c8
commit 3a4925816f
24 changed files with 501 additions and 189 deletions

View File

@ -121,6 +121,14 @@ $routes->group(
'as' => 'podcast-analytics', 'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view', '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( $routes->get(
'analytics-data/(:segment)/(:segment)', 'analytics-data/(:segment)/(:segment)',
'AnalyticsData::getData/$1/$2/$3', 'AnalyticsData::getData/$1/$2/$3',

View File

@ -23,14 +23,15 @@ class AnalyticsData extends BaseController
public function _remap($method, ...$params) public function _remap($method, ...$params)
{ {
if (count($params) > 2) { if (count($params) > 1) {
if (!($this->podcast = (new PodcastModel())->find($params[0]))) { if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound( throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
'Podcast not found: ' . $params[0] 'Podcast not found: ' . $params[0]
); );
} }
$this->className = '\App\Models\Analytics' . $params[1] . 'Model'; $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 (count($params) > 3) {
if ( if (
!($this->episode = (new EpisodeModel()) !($this->episode = (new EpisodeModel())

View File

@ -1,8 +1,8 @@
<?php <?php
/** /**
* Class AddAnalyticsPodcastsByCountry * Class AddAnalyticsPodcasts
* Creates analytics_podcasts_by_country table in database * Creates analytics_podcasts table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
@ -30,6 +30,11 @@ class AddAnalyticsPodcasts extends Migration
'constraint' => 10, 'constraint' => 10,
'default' => 1, 'default' => 1,
], ],
'unique_listeners' => [
'type' => 'INT',
'constraint' => 10,
'default' => 1,
],
]); ]);
$this->forge->addPrimaryKey(['podcast_id', 'date']); $this->forge->addPrimaryKey(['podcast_id', 'date']);
$this->forge->addField( $this->forge->addField(

View File

@ -1,8 +1,8 @@
<?php <?php
/** /**
* Class AddAnalyticsEpisodesByCountry * Class AddAnalyticsPodcastsByEpisode
* Creates analytics_episodes_by_country table in database * Creates analytics_episodes_by_episode table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/

View File

@ -1,8 +1,8 @@
<?php <?php
/** /**
* Class AddAnalyticsWebsiteByReferer * Class AddAnalyticsWebsiteByEntryPage
* Creates analytics_website_by_referer table in database * Creates analytics_website_by_entry_page table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/

View File

@ -17,7 +17,7 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration
public function up() public function up()
{ {
// Creates Stored Procedure for data insertion // 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(); $prefix = $this->db->getPrefix();
$createQuery = <<<EOD $createQuery = <<<EOD
@ -31,7 +31,8 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` (
IN `p_app` VARCHAR(128) CHARSET utf8mb4, IN `p_app` VARCHAR(128) CHARSET utf8mb4,
IN `p_device` VARCHAR(32) CHARSET utf8mb4, IN `p_device` VARCHAR(32) CHARSET utf8mb4,
IN `p_os` 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 ) MODIFIES SQL DATA
DETERMINISTIC DETERMINISTIC
SQL SECURITY INVOKER SQL SECURITY INVOKER
@ -40,7 +41,7 @@ BEGIN
IF NOT `p_bot` THEN IF NOT `p_bot` THEN
INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`) INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`)
VALUES (p_podcast_id, DATE(NOW())) 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`) 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 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; ON DUPLICATE KEY UPDATE `hits`=`hits`+1;

View File

@ -114,6 +114,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date), 'date' => date('Y-m-d', $date),
'hits' => $hits, 'hits' => $hits,
'unique_listeners' => $hits,
]; ];
$analytics_podcasts_by_country[] = [ $analytics_podcasts_by_country[] = [
'podcast_id' => $podcast->id, 'podcast_id' => $podcast->id,

View File

@ -18,5 +18,6 @@ class AnalyticsPodcasts extends Entity
'podcast_id' => 'integer', 'podcast_id' => 'integer',
'date' => 'datetime', 'date' => 'datetime',
'hits' => 'integer', 'hits' => 'integer',
'unique_listeners' => 'integer',
]; ];
} }

View File

@ -199,7 +199,7 @@ function webpage_hit($podcast_id)
$referer = $session->get('referer'); $referer = $session->get('referer');
$domain = empty(parse_url($referer, PHP_URL_HOST)) $domain = empty(parse_url($referer, PHP_URL_HOST))
? null ? '- Direct -'
: parse_url($referer, PHP_URL_HOST); : parse_url($referer, PHP_URL_HOST);
parse_str(parse_url($referer, PHP_URL_QUERY), $queries); parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
$keywords = empty($queries['q']) ? null : $queries['q']; $keywords = empty($queries['q']) ? null : $queries['q'];
@ -248,9 +248,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
if ($session->get('denyListIp')) { if ($session->get('denyListIp')) {
$session->get('player')['bot'] = true; $session->get('player')['bot'] = true;
} }
$httpRange = $_SERVER['HTTP_RANGE']; //We get the HTTP header field `Range`:
// We create a sha1 hash for this IP_Address+User_Agent+Episode_ID: $httpRange = isset($_SERVER['HTTP_RANGE'])
$hashID = ? $_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_' . '_IpUaEp_' .
sha1( sha1(
$_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_ADDR'] .
@ -260,12 +264,13 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
$episodeId $episodeId
); );
// Was this episode downloaded in the past 24h: // Was this episode downloaded in the past 24h:
$downloadedBytes = cache($hashID); $downloadedBytes = cache($episodeHashId);
// Rolling window is 24 hours (86400 seconds): // Rolling window is 24 hours (86400 seconds):
$ttl = 86400; $rollingTTL = 86400;
if ($downloadedBytes) { if ($downloadedBytes) {
// In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download): // 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 { } else {
// If it was never downloaded that means that zero byte were downloaded: // If it was never downloaded that means that zero byte were downloaded:
$downloadedBytes = 0; $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) // (Otherwise it means that this was already counted, therefore we don't do anything)
if ($downloadedBytes < $bytesThreshold) { if ($downloadedBytes < $bytesThreshold) {
// If HTTP_RANGE is null we are downloading the complete file: // If HTTP_RANGE is null we are downloading the complete file:
if (!isset($httpRange)) { if (!$httpRange) {
$downloadedBytes = $fileSize; $downloadedBytes = $fileSize;
} else { } else {
// [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working. // [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: // 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) { if ($downloadedBytes >= $bytesThreshold) {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$procedureName = $db->prefixTable('analytics_podcasts'); $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']; $app = $session->get('player')['app'];
$device = $session->get('player')['device']; $device = $session->get('player')['device'];
$os = $session->get('player')['os']; $os = $session->get('player')['os'];
$bot = $session->get('player')['bot']; $bot = $session->get('player')['bot'];
$db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [ $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?);", [
$podcastId, $podcastId,
$episodeId, $episodeId,
$session->get('location')['countryCode'], $session->get('location')['countryCode'],
@ -314,10 +344,12 @@ function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
$device == null ? '' : $device, $device == null ? '' : $device,
$os == null ? '' : $os, $os == null ? '' : $os,
$bot == null ? 0 : $bot, $bot == null ? 0 : $bot,
$newListener,
]); ]);
} }
} }
} catch (\Exception $e) { } catch (\Exception $e) {
// If things go wrong the show must go on and the user must be able to download the file // If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $e);
} }
} }

View File

@ -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)',
];

View File

@ -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;
}

View File

@ -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;
}

View File

@ -1,8 +1,8 @@
<?php <?php
/** /**
* Class AnalyticsPodcastsModel * Class AnalyticsPodcastByCountryModel
* Model for analytics_podcasts table in database * Model for analytics_podcasts_by_country table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
@ -12,44 +12,43 @@ namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
class AnalyticsPodcastsModel extends Model class AnalyticsPodcastByCountryModel extends Model
{ {
protected $table = 'analytics_podcasts'; protected $table = 'analytics_podcasts_by_country';
protected $allowedFields = []; protected $allowedFields = [];
protected $returnType = \App\Entities\AnalyticsPodcasts::class; protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class;
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $useTimestamps = false; protected $useTimestamps = false;
/** /**
* Gets all data for a podcast * Gets country data for a podcast
* *
* @param int $podcastId * @param int $podcastId
* *
* @return array * @return array
*/ */
public function getDataByDay(int $podcastId): array public function getData(int $podcastId): array
{ {
if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) { if (!($found = cache("{$podcastId}_analytics_podcast_by_country"))) {
$found = $this->select('`date` as `labels`') $found = $this->select('`country_code` as `labels`')
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->groupBy('`country_code`')
->where([ ->where([
'`podcast_id`' => $podcastId, '`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(); ->findAll();
cache()->save( cache()->save(
"{$podcastId}_analytics_podcast_by_day", "{$podcastId}_analytics_podcast_by_country",
$found, $found,
14400 600
); );
} }
return $found; return $found;
} }
} }

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Class AnalyticsPodcastsByEpisodeModel * Class AnalyticsPodcastByEpisodeModel
* Model for analytics_podcasts_by_episodes table in database * Model for analytics_podcasts_by_episodes table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -12,7 +12,7 @@ namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
class AnalyticsPodcastsByEpisodeModel extends Model class AnalyticsPodcastByEpisodeModel extends Model
{ {
protected $table = 'analytics_podcasts_by_episode'; protected $table = 'analytics_podcasts_by_episode';
@ -81,7 +81,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model
cache()->save( cache()->save(
"{$podcastId}_analytics_podcast_by_episode_by_day", "{$podcastId}_analytics_podcast_by_episode_by_day",
$found, $found,
14400 600
); );
} }
return $found; return $found;
@ -104,7 +104,7 @@ class AnalyticsPodcastsByEpisodeModel extends Model
cache()->save( cache()->save(
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day", "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
$found, $found,
14400 600
); );
} }
return $found; return $found;

View File

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Class AnalyticsPodcastsByPlayerModel * Class AnalyticsPodcastByPlayerModel
* Model for analytics_podcasts_by_player table in database * Model for analytics_podcasts_by_player table in database
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -12,7 +12,7 @@ namespace App\Models;
use CodeIgniter\Model; use CodeIgniter\Model;
class AnalyticsPodcastsByPlayerModel extends Model class AnalyticsPodcastByPlayerModel extends Model
{ {
protected $table = 'analytics_podcasts_by_player'; protected $table = 'analytics_podcasts_by_player';
@ -24,7 +24,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
protected $useTimestamps = false; protected $useTimestamps = false;
/** /**
* Gets all data for a podcast * Gets player data for a podcast
* *
* @param int $podcastId * @param int $podcastId
* *
@ -41,18 +41,18 @@ class AnalyticsPodcastsByPlayerModel extends Model
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->where([ ->where([
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`app` !=' => null, '`app` !=' => '',
'`bot`' => 0, '`bot`' => 0,
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->groupBy('`labels`') ->groupBy('`labels`')
->orderBy('`values``', 'DESC') ->orderBy('`values`', 'DESC')
->findAll(10); ->findAll(10);
cache()->save( cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_app", "{$podcastId}_analytics_podcasts_by_player_by_app",
$found, $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 * @param int $podcastId
* *
@ -84,7 +84,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->groupBy('`ids`') ->groupBy('`ids`')
->orderBy('`values``', 'DESC') ->orderBy('`values`', 'DESC')
->findAll(); ->findAll();
$foundOs = $this->select( $foundOs = $this->select(
@ -98,7 +98,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->groupBy('`ids`') ->groupBy('`ids`')
->orderBy('`values``', 'DESC') ->orderBy('`values`', 'DESC')
->findAll(); ->findAll();
$foundDevice = $this->select( $foundDevice = $this->select(
@ -112,7 +112,7 @@ class AnalyticsPodcastsByPlayerModel extends Model
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->groupBy('`ids`') ->groupBy('`ids`')
->orderBy('`values``', 'DESC') ->orderBy('`values`', 'DESC')
->findAll(); ->findAll();
$foundBot = $this->select( $foundBot = $this->select(
@ -125,14 +125,14 @@ class AnalyticsPodcastsByPlayerModel extends Model
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->groupBy('`ids`') ->groupBy('`ids`')
->orderBy('`values``', 'DESC') ->orderBy('`values`', 'DESC')
->findAll(); ->findAll();
$found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot); $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot);
cache()->save( cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_device", "{$podcastId}_analytics_podcasts_by_player_by_device",
$found, $found,
14400 600
); );
} }

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -22,4 +22,33 @@ class AnalyticsWebsiteByBrowserModel extends Model
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $useTimestamps = 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;
}
} }

View File

@ -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;
}

View File

@ -22,4 +22,33 @@ class AnalyticsWebsiteByEntryPageModel extends Model
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $useTimestamps = 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;
}
} }

View File

@ -22,4 +22,62 @@ class AnalyticsWebsiteByRefererModel extends Model
protected $useSoftDeletes = false; protected $useSoftDeletes = false;
protected $useTimestamps = 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;
}
} }

View File

@ -9,24 +9,76 @@
<?= $this->endSection() ?> <?= $this->endSection() ?>
<?= $this->section('content') ?> <?= $this->section('content') ?>
<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
'analytics-data', <h2><?= lang('Charts.podcast_by_day') ?></h2>
$podcast->id,
'PodcastsByPlayer',
'ByApp'
) ?>"></div>
<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( <div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
'analytics-data', 'analytics-data',
$podcast->id, $podcast->id,
'Podcasts', 'Podcast',
'ByDay' 'ByDay'
) ?>"></div> ) ?>"></div>
<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,
'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( <div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to(
'analytics-data', 'analytics-data',
$podcast->id, $podcast->id,
'PodcastsByEpisode', 'PodcastByEpisode',
'ByDay' 'ByDay'
) ?>"></div> ) ?>"></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> <script src="/assets/charts.js" type="module"></script>
<?= $this->endSection() ?> <?= $this->endSection() ?>