diff --git a/Dockerfile b/Dockerfile
index 01ab228e..5b64fb84 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,9 +20,9 @@ RUN docker-php-ext-configure gd --with-jpeg-dir=/usr/include/ \
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
RUN echo "file_uploads = On\n" \
- "memory_limit = 100M\n" \
- "upload_max_filesize = 100M\n" \
- "post_max_size = 120M\n" \
+ "memory_limit = 512M\n" \
+ "upload_max_filesize = 500M\n" \
+ "post_max_size = 512M\n" \
"max_execution_time = 300\n" \
> /usr/local/etc/php/conf.d/uploads.ini
diff --git a/app/Config/Analytics.php b/app/Config/Analytics.php
new file mode 100644
index 00000000..4391703d
--- /dev/null
+++ b/app/Config/Analytics.php
@@ -0,0 +1,35 @@
+ 'permission:podcasts-view,podcast-view',
+ 'analytics-data' => 'permission:podcasts-view,podcast-view',
+ 'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
+ ];
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ // set the analytics gateway behind the admin gateway.
+ // Only logged in users should be able to view analytics
+ $this->gateway = config('App')->adminGateway . '/analytics';
+ }
+
+ public function getEnclosureUrl($enclosureUri)
+ {
+ helper('media');
+
+ return media_base_url($enclosureUri);
+ }
+}
diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php
index de861bdb..adb58349 100644
--- a/app/Config/Autoload.php
+++ b/app/Config/Autoload.php
@@ -43,6 +43,7 @@ class Autoload extends AutoloadConfig
APP_NAMESPACE => APPPATH, // For custom app namespace
'Config' => APPPATH . 'Config',
'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
+ 'Analytics' => APPPATH . 'Libraries/Analytics',
];
/**
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 3e3a16fc..32009467 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -70,18 +70,6 @@ $routes->group(config('App')->installGateway, function ($routes) {
]);
});
-// Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
-$routes->head('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [
- 'as' => 'analytics_hit',
-]);
-$routes->get('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [
- 'as' => 'analytics_hit',
-]);
-
-// Show the Unknown UserAgents
-$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
-$routes->get('.well-known/unknown-useragents/(:num)', 'UnknownUserAgents/$1');
-
$routes->get('.well-known/platforms', 'Platform');
// Admin area
@@ -237,31 +225,6 @@ $routes->group(
);
});
- $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',
- [
- 'as' => 'analytics-data',
- 'filter' => 'permission:podcasts-view,podcast-view',
- ],
- );
- $routes->get(
- 'analytics-data/(:segment)/(:segment)/(:num)',
- 'AnalyticsData::getData/$1/$2/$3/$4',
- [
- 'as' => 'analytics-filtered-data',
- 'filter' => 'permission:podcasts-view,podcast-view',
- ],
- );
-
// Podcast episodes
$routes->group('episodes', function ($routes) {
$routes->get('/', 'Episode::list/$1', [
diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php
deleted file mode 100644
index ba5e1673..00000000
--- a/app/Controllers/Admin/AnalyticsData.php
+++ /dev/null
@@ -1,70 +0,0 @@
- 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' . (empty($params[2]) ? '' : $params[2]);
- if (count($params) > 3) {
- if (
- !($this->episode = (new EpisodeModel())
- ->where([
- 'podcast_id' => $this->podcast->id,
- 'id' => $params[3],
- ])
- ->first())
- ) {
- throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
- 'Episode not found: ' . $params[3]
- );
- }
- }
- }
-
- return $this->$method();
- }
- public function getData()
- {
- $analytics_model = new $this->className();
- $methodName = $this->methodName;
- if ($this->episode) {
- return $this->response->setJSON(
- $analytics_model->$methodName(
- $this->podcast->id,
- $this->episode->id
- )
- );
- } else {
- return $this->response->setJSON(
- $analytics_model->$methodName($this->podcast->id)
- );
- }
- }
-}
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 31eee58f..b791a6b0 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -44,8 +44,6 @@ class Episode extends BaseController
public function index()
{
- $episodeModel = new EpisodeModel();
-
self::triggerWebpageHit($this->podcast->id);
$locale = service('request')->getLocale();
@@ -65,7 +63,7 @@ class Episode extends BaseController
'persons' => $podcastPersons,
];
- $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+ $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
@@ -112,7 +110,6 @@ class Episode extends BaseController
$cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
if (!($cachedView = cache($cacheName))) {
- $episodeModel = new EpisodeModel();
$theme = EpisodeModel::$themes[$theme];
$data = [
@@ -121,7 +118,7 @@ class Episode extends BaseController
'theme' => $theme,
];
- $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
+ $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
diff --git a/app/Controllers/Install.php b/app/Controllers/Install.php
index d612b919..c3e60160 100644
--- a/app/Controllers/Install.php
+++ b/app/Controllers/Install.php
@@ -111,7 +111,7 @@ class Install extends Controller
// show database config view to fix value
session()->setFlashdata(
'error',
- lang('Install.messages.databaseConnectError')
+ lang('Install.messages.databaseConnectError'),
);
return view('install/database_config');
@@ -159,7 +159,7 @@ class Install extends Controller
return redirect()
->to(
(empty(host_url()) ? config('App')->baseURL : host_url()) .
- config('App')->installGateway
+ config('App')->installGateway,
)
->withInput()
->with('errors', $this->validator->getErrors());
@@ -181,8 +181,8 @@ class Install extends Controller
// redirect to full install url with new baseUrl input
return redirect(0)->to(
reduce_double_slashes(
- $baseUrl . '/' . config('App')->installGateway
- )
+ $baseUrl . '/' . config('App')->installGateway,
+ ),
);
}
@@ -209,14 +209,14 @@ class Install extends Controller
self::writeEnv([
'database.default.hostname' => $this->request->getPost(
- 'db_hostname'
+ 'db_hostname',
),
'database.default.database' => $this->request->getPost('db_name'),
'database.default.username' => $this->request->getPost(
- 'db_username'
+ 'db_username',
),
'database.default.password' => $this->request->getPost(
- 'db_password'
+ 'db_password',
),
'database.default.DBPrefix' => $this->request->getPost('db_prefix'),
]);
@@ -258,6 +258,7 @@ class Install extends Controller
!$migrations->setNamespace('Myth\Auth')->latest();
!$migrations->setNamespace('ActivityPub')->latest();
+ !$migrations->setNamespace('Analytics')->latest();
!$migrations->setNamespace(APP_NAMESPACE)->latest();
}
@@ -296,7 +297,7 @@ class Install extends Controller
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
- ]
+ ],
);
if (!$this->validate($rules)) {
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 1529dd09..ef11e954 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -37,6 +37,8 @@ class Podcast extends BaseController
public function activity()
{
+ self::triggerWebpageHit($this->podcast->id);
+
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
index 4af1128b..7adb67fa 100644
--- a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
+++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
@@ -10,6 +10,7 @@
*/
namespace App\Database\Seeds;
+
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
@@ -23,16 +24,16 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$jsonUserAgents = json_decode(
file_get_contents(
- 'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json'
+ 'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json',
),
- true
+ true,
);
$jsonRSSUserAgents = json_decode(
file_get_contents(
- 'https://raw.githubusercontent.com/opawg/podcast-rss-useragents/master/src/rss-ua.json'
+ 'https://raw.githubusercontent.com/opawg/podcast-rss-useragents/master/src/rss-ua.json',
),
- true
+ true,
);
if ($podcast) {
@@ -60,7 +61,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
->findAll();
foreach ($episodes as $episode) {
$age = floor(
- ($date - strtotime($episode->published_at)) / 86400
+ ($date - strtotime($episode->published_at)) / 86400,
);
$proba1 = floor(exp(3 - $age / 40)) + 1;
@@ -97,7 +98,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
$cityReader = new \GeoIp2\Database\Reader(
WRITEPATH .
- 'uploads/GeoLite2-City/GeoLite2-City.mmdb'
+ 'uploads/GeoLite2-City/GeoLite2-City.mmdb',
);
$countryCode = 'N/A';
@@ -196,7 +197,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
->insertBatch($analytics_podcasts_by_region);
}
} else {
- echo "Create one podcast and some episodes first.\n";
+ echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
}
}
}
diff --git a/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
index daa37da9..af6661ad 100644
--- a/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
+++ b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
@@ -10,6 +10,7 @@
*/
namespace App\Database\Seeds;
+
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
@@ -193,7 +194,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
->findAll();
foreach ($episodes as $episode) {
$age = floor(
- ($date - strtotime($episode->published_at)) / 86400
+ ($date - strtotime($episode->published_at)) / 86400,
);
$proba1 = floor(exp(3 - $age / 40)) + 1;
@@ -254,7 +255,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
->insertBatch($website_by_referer);
}
} else {
- echo "Create one podcast and some episodes first.\n";
+ echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
}
}
}
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index ced727eb..e7fdac99 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -336,37 +336,14 @@ class Episode extends Entity
{
helper('analytics');
- return base_url(
- route_to(
- 'analytics_hit',
- base64_url_encode(
- pack(
- 'I*',
- $this->attributes['podcast_id'],
- $this->attributes['id'],
- // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
- // - if file is shorter than 60sec, then it's enclosure_filesize
- // - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds
- $this->attributes['enclosure_duration'] <= 60
- ? $this->attributes['enclosure_filesize']
- : $this->attributes['enclosure_headersize'] +
- floor(
- (($this->attributes['enclosure_filesize'] -
- $this->attributes[
- 'enclosure_headersize'
- ]) /
- $this->attributes[
- 'enclosure_duration'
- ]) *
- 60,
- ),
- $this->attributes['enclosure_filesize'],
- $this->attributes['enclosure_duration'],
- strtotime($this->attributes['published_at']),
- ),
- ),
- $this->attributes['enclosure_uri'],
- ),
+ return generate_episode_analytics_url(
+ $this->podcast_id,
+ $this->id,
+ $this->enclosure_uri,
+ $this->enclosure_duration,
+ $this->enclosure_filesize,
+ $this->enclosure_headersize,
+ $this->published_at,
);
}
@@ -520,9 +497,9 @@ class Episode extends Entity
empty($this->getPodcast()->partner_image_url)
? ''
: "
") .
$this->attributes['description_html'] .
(empty($this->getPodcast()->episode_description_footer_html)
diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php
deleted file mode 100644
index e7b393db..00000000
--- a/app/Helpers/analytics_helper.php
+++ /dev/null
@@ -1,357 +0,0 @@
-start();
-
- if (!$session->has('denyListIp')) {
- $session->set(
- 'denyListIp',
- \Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null,
- );
- }
-}
-
-/**
- * Set user country in session variable, for analytics purpose
- */
-function set_user_session_location()
-{
- $session = \Config\Services::session();
- $session->start();
-
- $location = [
- 'countryCode' => 'N/A',
- 'regionCode' => 'N/A',
- 'latitude' => null,
- 'longitude' => null,
- ];
-
- // Finds location:
- if (!$session->has('location')) {
- try {
- $cityReader = new \GeoIp2\Database\Reader(
- WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb',
- );
- $city = $cityReader->city($_SERVER['REMOTE_ADDR']);
-
- $location = [
- 'countryCode' => empty($city->country->isoCode)
- ? 'N/A'
- : $city->country->isoCode,
- 'regionCode' => empty($city->subdivisions[0]->isoCode)
- ? 'N/A'
- : $city->subdivisions[0]->isoCode,
- 'latitude' => round($city->location->latitude, 3),
- 'longitude' => round($city->location->longitude, 3),
- ];
- } catch (\Exception $e) {
- // If things go wrong the show must go on and the user must be able to download the file
- }
- $session->set('location', $location);
- }
-}
-
-/**
- * Set user player in session variable, for analytics purpose
- */
-function set_user_session_player()
-{
- $session = \Config\Services::session();
- $session->start();
-
- if (!$session->has('player')) {
- $playerFound = null;
- $userAgent = $_SERVER['HTTP_USER_AGENT'];
-
- try {
- $playerFound = \Opawg\UserAgentsPhp\UserAgents::find($userAgent);
- } catch (\Exception $e) {
- // If things go wrong the show must go on and the user must be able to download the file
- }
- if ($playerFound) {
- $session->set('player', $playerFound);
- } else {
- $session->set('player', [
- 'app' => '- unknown -',
- 'device' => '',
- 'os' => '',
- 'bot' => 0,
- ]);
- // Add to unknown list
- try {
- $db = \Config\Database::connect();
- $procedureNameAnalyticsUnknownUseragents = $db->prefixTable(
- 'analytics_unknown_useragents',
- );
- $db->query("CALL $procedureNameAnalyticsUnknownUseragents(?)", [
- $userAgent,
- ]);
- } catch (\Exception $e) {
- // If things go wrong the show must go on and the user must be able to download the file
- }
- }
- }
-}
-
-/**
- * Set user browser in session variable, for analytics purpose
- */
-function set_user_session_browser()
-{
- $session = \Config\Services::session();
- $session->start();
-
- if (!$session->has('browser')) {
- $browserName = '- Other -';
- try {
- $whichbrowser = new \WhichBrowser\Parser(getallheaders());
- $browserName = $whichbrowser->browser->name;
- } catch (\Exception $e) {
- $browserName = '- Could not get browser name -';
- }
- if ($browserName == null) {
- $browserName = '- Could not get browser name -';
- }
- $session->set('browser', $browserName);
- }
-}
-
-/**
- * Set user referer in session variable, for analytics purpose
- */
-function set_user_session_referer()
-{
- $session = \Config\Services::session();
- $session->start();
-
- $newreferer = isset($_SERVER['HTTP_REFERER'])
- ? $_SERVER['HTTP_REFERER']
- : '- Direct -';
- $newreferer =
- parse_url($newreferer, PHP_URL_HOST) ==
- parse_url(current_url(false), PHP_URL_HOST)
- ? '- Direct -'
- : $newreferer;
- if (!$session->has('referer') or $newreferer != '- Direct -') {
- $session->set('referer', $newreferer);
- }
-}
-
-/**
- * Set user entry page in session variable, for analytics purpose
- */
-function set_user_session_entry_page()
-{
- $session = \Config\Services::session();
- $session->start();
-
- $entryPage = $_SERVER['REQUEST_URI'];
- if (!$session->has('entryPage')) {
- $session->set('entryPage', $entryPage);
- }
-}
-
-function webpage_hit($podcast_id)
-{
- $session = \Config\Services::session();
- $session->start();
-
- if (!$session->get('denyListIp')) {
- $db = \Config\Database::connect();
-
- $referer = $session->get('referer');
- $domain = empty(parse_url($referer, PHP_URL_HOST))
- ? '- Direct -'
- : parse_url($referer, PHP_URL_HOST);
- parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
- $keywords = empty($queries['q']) ? null : $queries['q'];
-
- $procedureName = $db->prefixTable('analytics_website');
- $db->query("call $procedureName(?,?,?,?,?,?)", [
- $podcast_id,
- $session->get('browser'),
- $session->get('entryPage'),
- $referer,
- $domain,
- $keywords,
- ]);
- }
-}
-
-/**
- * Counting podcast episode downloads for analytics purposes
- * ✅ No IP address is ever stored on the server.
- * ✅ Only aggregate data is stored in the database.
- * We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
- * https://iabtechlab.com/standards/podcast-measurement-guidelines/
- * https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf
- * ✅ Rolling 24-hour window
- * ✅ Castopod does not do pre-load
- * ✅ IP deny list https://github.com/client9/ipcat
- * ✅ User-agent Filtering https://github.com/opawg/user-agents
- * ✅ RSS User-agent https://github.com/opawg/podcast-rss-useragents
- * ✅ Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app)
- * ✅ In case of partial content, adds up all requests to check >1mn was downloaded
- * ✅ Identifying Uniques is done with a combination of IP Address and User Agent
- * @param int $podcastId The podcast ID
- * @param int $episodeId The Episode ID
- * @param int $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn)
- * @param int $fileSize The podcast complete file size
- * @param string $serviceName The name of the service that had fetched the RSS feed
- *
- * @return void
- */
-function podcast_hit(
- $podcastId,
- $episodeId,
- $bytesThreshold,
- $fileSize,
- $duration,
- $publicationDate,
- $serviceName
-) {
- $session = \Config\Services::session();
- $session->start();
-
- // We try to count (but if things went wrong the show should go on and the user should be able to download the file):
- try {
- // If the user IP is denied it's probably a bot:
- if ($session->get('denyListIp')) {
- $session->get('player')['bot'] = true;
- }
- //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'] .
- '_' .
- $_SERVER['HTTP_USER_AGENT'] .
- '_' .
- $episodeId,
- );
- // Was this episode downloaded in the past 24h:
- $downloadedBytes = cache($episodeHashId);
- // Rolling window is 24 hours (86400 seconds):
- $rollingTTL = 86400;
- if ($downloadedBytes) {
- // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
- $rollingTTL =
- cache()->getMetadata($episodeHashId)['expire'] - time();
- } else {
- // If it was never downloaded that means that zero byte were downloaded:
- $downloadedBytes = 0;
- }
- // If the number of downloaded bytes was previously below the 1mn threshold we go on:
- // (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 (!$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.
- // We don't count these requests:
- if ($httpRange != 'bytes=0-1') {
- // We calculate how many bytes are being downloaded based on HTTP_RANGE values:
- $ranges = explode(',', substr($httpRange, 6));
- foreach ($ranges as $range) {
- $parts = explode('-', $range);
- $downloadedBytes += empty($parts[1])
- ? $fileSize
- : $parts[1] - (empty($parts[0]) ? 0 : $parts[0]);
- }
- }
- }
- // We save the number of downloaded bytes for this user and this episode:
- cache()->save($episodeHashId, $downloadedBytes, $rollingTTL);
-
- // 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');
-
- $age = intdiv(time() - $publicationDate, 86400);
-
- // 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);
-
- $db->query(
- "CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
- [
- $podcastId,
- $episodeId,
- $session->get('location')['countryCode'],
- $session->get('location')['regionCode'],
- $session->get('location')['latitude'],
- $session->get('location')['longitude'],
- $serviceName,
- $session->get('player')['app'],
- $session->get('player')['device'],
- $session->get('player')['os'],
- $session->get('player')['bot'],
- $fileSize,
- $duration,
- $age,
- $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/Helpers/url_helper.php b/app/Helpers/url_helper.php
index 43e287f9..9e80311d 100644
--- a/app/Helpers/url_helper.php
+++ b/app/Helpers/url_helper.php
@@ -88,7 +88,7 @@ if (!function_exists('extract_params_from_episode_uri')) {
preg_match(
'/@(?P[a-zA-Z0-9\_]{1,32})\/episodes\/(?P[a-zA-Z0-9\-]{1,191})/',
$episodeUri->getPath(),
- $matches
+ $matches,
);
if (
diff --git a/app/Libraries/Analytics/Config/Analytics.php b/app/Libraries/Analytics/Config/Analytics.php
new file mode 100644
index 00000000..fa5ff6a4
--- /dev/null
+++ b/app/Libraries/Analytics/Config/Analytics.php
@@ -0,0 +1,38 @@
+ '',
+ 'analytics-data' => '',
+ 'analytics-filtered-data' => '',
+ ];
+
+ /**
+ * get the full enclosure url
+ *
+ * @param string $filename
+ * @return string
+ */
+ public function getEnclosureUrl(string $enclosureUri)
+ {
+ return base_url($enclosureUri);
+ }
+}
diff --git a/app/Libraries/Analytics/Config/Routes.php b/app/Libraries/Analytics/Config/Routes.php
new file mode 100644
index 00000000..62de878c
--- /dev/null
+++ b/app/Libraries/Analytics/Config/Routes.php
@@ -0,0 +1,74 @@
+addPlaceholder(
+ 'class',
+ '\bPodcastByCountry|\bPodcastByEpisode|\bPodcastByHour|\bPodcastByPlayer|\bPodcastByRegion|\bPodcastByService|\bPodcast|\bWebsiteByBrowser|\bWebsiteByEntryPage|\bWebsiteByReferer',
+);
+$routes->addPlaceholder(
+ 'filter',
+ '\bWeekly|\bYearly|\bByDay|\bByWeekday|\bByMonth|\bByAppWeekly|\bByAppYearly|\bByOsWeekly|\bByDeviceWeekly|\bBots|\bByServiceWeekly|\bBandwidthByDay|\bUniqueListenersByDay|\bUniqueListenersByMonth|\bTotalListeningTimeByDay|\bTotalListeningTimeByMonth|\bByDomainWeekly|\bByDomainYearly',
+);
+
+$routes->group('', ['namespace' => 'Analytics\Controllers'], function (
+ $routes
+) {
+ $routes->group(config('Analytics')->gateway . '/(:num)/(:class)', function (
+ $routes
+ ) {
+ $routes->get('/', 'AnalyticsController::getData/$1/$2', [
+ 'as' => 'analytics-full-data',
+ 'filter' => config('Analytics')->routeFilters[
+ 'analytics-full-data'
+ ],
+ ]);
+
+ $routes->get('(:filter)', 'AnalyticsController::getData/$1/$2/$3', [
+ 'as' => 'analytics-data',
+ 'filter' => config('Analytics')->routeFilters['analytics-data'],
+ ]);
+
+ $routes->get(
+ '(:filter)/(:num)',
+ 'AnalyticsController::getData/$1/$2/$3/$4',
+ [
+ 'as' => 'analytics-filtered-data',
+ 'filter' => config('Analytics')->routeFilters[
+ 'analytics-filtered-data'
+ ],
+ ],
+ );
+ });
+
+ // Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
+ $routes->head(
+ 'audio/(:base64)/(:any)',
+ 'EpisodeAnalyticsController::hit/$1/$2',
+ [
+ 'as' => 'episode-analytics-hit',
+ ],
+ );
+ $routes->get(
+ 'audio/(:base64)/(:any)',
+ 'EpisodeAnalyticsController::hit/$1/$2',
+ [
+ 'as' => 'episode-analytics-hit',
+ ],
+ );
+});
+
+// Show the Unknown UserAgents
+$routes->get('.well-known/unknown-useragents', 'UnknownUserAgentsController');
+$routes->get(
+ '.well-known/unknown-useragents/(:num)',
+ 'UnknownUserAgentsController/$1',
+);
diff --git a/app/Libraries/Analytics/Controllers/AnalyticsController.php b/app/Libraries/Analytics/Controllers/AnalyticsController.php
new file mode 100644
index 00000000..d80f03d5
--- /dev/null
+++ b/app/Libraries/Analytics/Controllers/AnalyticsController.php
@@ -0,0 +1,54 @@
+className = model('Analytics' . $params[1] . 'Model');
+ $this->methodName = 'getData' . (empty($params[2]) ? '' : $params[2]);
+
+ return $this->$method(
+ $params[0],
+ isset($params[3]) ? $params[3] : null,
+ );
+ }
+
+ public function getData($podcastId, $episodeId)
+ {
+ $analytics_model = new $this->className();
+ $methodName = $this->methodName;
+ if ($episodeId) {
+ return $this->response->setJSON(
+ $analytics_model->$methodName($podcastId, $episodeId),
+ );
+ } else {
+ return $this->response->setJSON(
+ $analytics_model->$methodName($podcastId),
+ );
+ }
+ }
+}
diff --git a/app/Controllers/Analytics.php b/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
similarity index 83%
rename from app/Controllers/Analytics.php
rename to app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
index c687cb65..7e372d9a 100644
--- a/app/Controllers/Analytics.php
+++ b/app/Libraries/Analytics/Controllers/EpisodeAnalyticsController.php
@@ -1,18 +1,16 @@
config = config('Analytics');
}
// Add one hit to this episode:
- public function hit($base64EpisodeData, ...$filename)
+ public function hit($base64EpisodeData, ...$enclosureUri)
{
- helper('media', 'analytics');
$session = \Config\Services::session();
$session->start();
$serviceName = '';
@@ -62,7 +65,7 @@ class Analytics extends Controller
$episodeData = unpack(
'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
- base64_url_decode($base64EpisodeData)
+ base64_url_decode($base64EpisodeData),
);
podcast_hit(
@@ -72,8 +75,9 @@ class Analytics extends Controller
$episodeData['fileSize'],
$episodeData['duration'],
$episodeData['publicationDate'],
- $serviceName
+ $serviceName,
);
- return redirect()->to(media_base_url($filename));
+
+ return redirect()->to($this->config->getEnclosureUrl($enclosureUri));
}
}
diff --git a/app/Controllers/UnknownUserAgents.php b/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
similarity index 70%
rename from app/Controllers/UnknownUserAgents.php
rename to app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
index 94718725..c2664818 100644
--- a/app/Controllers/UnknownUserAgents.php
+++ b/app/Libraries/Analytics/Controllers/UnknownUserAgentsController.php
@@ -6,15 +6,15 @@
* @link https://castopod.org/
*/
-namespace App\Controllers;
+namespace Analytics\Controllers;
use CodeIgniter\Controller;
-class UnknownUserAgents extends Controller
+class UnknownUserAgentsController extends Controller
{
public function index($lastKnownId = 0)
{
- $model = new \App\Models\UnknownUserAgentsModel();
+ $model = model('UnknownUserAgentsModel');
return $this->response->setJSON($model->getUserAgents($lastKnownId));
}
diff --git a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-120000_add_analytics_podcasts.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-120000_add_analytics_podcasts.php
index 35821439..e89b1d19 100644
--- a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-120000_add_analytics_podcasts.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_episode.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_episode.php
index 39dba610..72d9b861 100644
--- a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_episode.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_hour.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_hour.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_hour.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_hour.php
index ae2d85e3..4e898f41 100644
--- a/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_hour.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-130000_add_analytics_podcasts_by_hour.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-140000_add_analytics_podcasts_by_player.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-140000_add_analytics_podcasts_by_player.php
index a1ab3174..424ede82 100644
--- a/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-140000_add_analytics_podcasts_by_player.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-150000_add_analytics_podcasts_by_country.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-150000_add_analytics_podcasts_by_country.php
index ce728e96..d2a14f22 100644
--- a/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-150000_add_analytics_podcasts_by_country.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_analytics_podcasts_by_region.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_analytics_podcasts_by_region.php
index 009894fd..ee55120d 100644
--- a/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_analytics_podcasts_by_region.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_podcasts_platforms.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_podcasts_platforms.php
index 68df0a81..dbfd70e6 100644
--- a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-160000_add_podcasts_platforms.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-170000_add_analytics_website_by_browser.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-170000_add_analytics_website_by_browser.php
index 891a76e7..5a542c49 100644
--- a/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-170000_add_analytics_website_by_browser.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-180000_add_analytics_website_by_referer.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-180000_add_analytics_website_by_referer.php
index 0fa2fa70..45711429 100644
--- a/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-180000_add_analytics_website_by_referer.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-190000_add_analytics_website_by_entry_page.php
similarity index 97%
rename from app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-190000_add_analytics_website_by_entry_page.php
index 366b75af..8e331a6e 100644
--- a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-190000_add_analytics_website_by_entry_page.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_unknown_useragents.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-200000_add_analytics_unknown_useragents.php
similarity index 93%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_unknown_useragents.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-200000_add_analytics_unknown_useragents.php
index fcce6f49..2ff7fcea 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_unknown_useragents.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-200000_add_analytics_unknown_useragents.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
@@ -38,10 +38,10 @@ class AddAnalyticsUnknownUseragents extends Migration
$this->forge->addPrimaryKey('id');
// `created_at` and `updated_at` are created with SQL because Model class won’t be used for insertion (Procedure will be used instead)
$this->forge->addField(
- '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
+ '`created_at` timestamp NOT NULL DEFAULT current_timestamp()',
);
$this->forge->addField(
- '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+ '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()',
);
$this->forge->createTable('analytics_unknown_useragents');
}
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_podcasts_procedure.php
similarity index 98%
rename from app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_podcasts_procedure.php
index f1792a6c..ca579b3a 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_podcasts_procedure.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_unknown_useragents_procedure.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_unknown_useragents_procedure.php
similarity index 96%
rename from app/Database/Migrations/2020-06-11-210000_add_analytics_unknown_useragents_procedure.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_unknown_useragents_procedure.php
index 39e8d3aa..5f6a65a9 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_unknown_useragents_procedure.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_unknown_useragents_procedure.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_procedure.php b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_website_procedure.php
similarity index 97%
rename from app/Database/Migrations/2020-06-11-210000_add_analytics_website_procedure.php
rename to app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_website_procedure.php
index 01e89391..c4e5ac8c 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_procedure.php
+++ b/app/Libraries/Analytics/Database/Migrations/2017-12-01-210000_add_analytics_website_procedure.php
@@ -9,7 +9,7 @@
* @link https://castopod.org/
*/
-namespace App\Database\Migrations;
+namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
diff --git a/app/Entities/AnalyticsPodcasts.php b/app/Libraries/Analytics/Entities/AnalyticsPodcasts.php
similarity index 94%
rename from app/Entities/AnalyticsPodcasts.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcasts.php
index b8e74b8f..1c05c275 100644
--- a/app/Entities/AnalyticsPodcasts.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcasts.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByCountry.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
similarity index 95%
rename from app/Entities/AnalyticsPodcastsByCountry.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
index d8b80de4..0f03599a 100644
--- a/app/Entities/AnalyticsPodcastsByCountry.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByCountry.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByEpisode.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByEpisode.php
similarity index 93%
rename from app/Entities/AnalyticsPodcastsByEpisode.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByEpisode.php
index 783bf2d5..d2e570da 100644
--- a/app/Entities/AnalyticsPodcastsByEpisode.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByEpisode.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByHour.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByHour.php
similarity index 93%
rename from app/Entities/AnalyticsPodcastsByHour.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByHour.php
index 32dde6f3..6dc44330 100644
--- a/app/Entities/AnalyticsPodcastsByHour.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByHour.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByPlayer.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByPlayer.php
similarity index 94%
rename from app/Entities/AnalyticsPodcastsByPlayer.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByPlayer.php
index e19f8360..1bd636ee 100644
--- a/app/Entities/AnalyticsPodcastsByPlayer.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByPlayer.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByRegion.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
similarity index 95%
rename from app/Entities/AnalyticsPodcastsByRegion.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
index de0a9b76..7f62f570 100644
--- a/app/Entities/AnalyticsPodcastsByRegion.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByRegion.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsPodcastsByService.php b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
similarity index 91%
rename from app/Entities/AnalyticsPodcastsByService.php
rename to app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
index d34d57ac..d73eaeff 100644
--- a/app/Entities/AnalyticsPodcastsByService.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsPodcastsByService.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
@@ -32,7 +32,7 @@ class AnalyticsPodcastsByService extends Entity
public function getLabels()
{
return \Opawg\UserAgentsPhp\UserAgentsRSS::getName(
- $this->attributes['labels']
+ $this->attributes['labels'],
) ?? $this->attributes['labels'];
}
}
diff --git a/app/Entities/AnalyticsUnknownUseragents.php b/app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php
similarity index 93%
rename from app/Entities/AnalyticsUnknownUseragents.php
rename to app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php
index cff8088e..b5a9dd3e 100644
--- a/app/Entities/AnalyticsUnknownUseragents.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsUnknownUseragents.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsWebsiteByBrowser.php b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByBrowser.php
similarity index 93%
rename from app/Entities/AnalyticsWebsiteByBrowser.php
rename to app/Libraries/Analytics/Entities/AnalyticsWebsiteByBrowser.php
index 7b170f2e..d753e9e7 100644
--- a/app/Entities/AnalyticsWebsiteByBrowser.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByBrowser.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsWebsiteByEntryPage.php b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByEntryPage.php
similarity index 94%
rename from app/Entities/AnalyticsWebsiteByEntryPage.php
rename to app/Libraries/Analytics/Entities/AnalyticsWebsiteByEntryPage.php
index d12c9bdb..1e1c6d98 100644
--- a/app/Entities/AnalyticsWebsiteByEntryPage.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByEntryPage.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Entities/AnalyticsWebsiteByReferer.php b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByReferer.php
similarity index 93%
rename from app/Entities/AnalyticsWebsiteByReferer.php
rename to app/Libraries/Analytics/Entities/AnalyticsWebsiteByReferer.php
index 30b0b2bf..4fdf50b1 100644
--- a/app/Entities/AnalyticsWebsiteByReferer.php
+++ b/app/Libraries/Analytics/Entities/AnalyticsWebsiteByReferer.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Entities;
+namespace Analytics\Entities;
use CodeIgniter\Entity;
diff --git a/app/Libraries/Analytics/Helpers/analytics_helper.php b/app/Libraries/Analytics/Helpers/analytics_helper.php
new file mode 100644
index 00000000..227a7ef6
--- /dev/null
+++ b/app/Libraries/Analytics/Helpers/analytics_helper.php
@@ -0,0 +1,451 @@
+start();
+
+ if (!$session->has('denyListIp')) {
+ $session->set(
+ 'denyListIp',
+ \Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null,
+ );
+ }
+ }
+}
+
+if (!function_exists('set_user_session_location')) {
+ /**
+ * Set user country in session variable, for analytic purposes
+ */
+ function set_user_session_location()
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ $location = [
+ 'countryCode' => 'N/A',
+ 'regionCode' => 'N/A',
+ 'latitude' => null,
+ 'longitude' => null,
+ ];
+
+ // Finds location:
+ if (!$session->has('location')) {
+ try {
+ $cityReader = new \GeoIp2\Database\Reader(
+ WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb',
+ );
+ $city = $cityReader->city($_SERVER['REMOTE_ADDR']);
+
+ $location = [
+ 'countryCode' => empty($city->country->isoCode)
+ ? 'N/A'
+ : $city->country->isoCode,
+ 'regionCode' => empty($city->subdivisions[0]->isoCode)
+ ? 'N/A'
+ : $city->subdivisions[0]->isoCode,
+ 'latitude' => round($city->location->latitude, 3),
+ 'longitude' => round($city->location->longitude, 3),
+ ];
+ } catch (\Exception $e) {
+ // If things go wrong the show must go on and the user must be able to download the file
+ }
+ $session->set('location', $location);
+ }
+ }
+}
+
+if (!function_exists('set_user_session_player')) {
+ /**
+ * Set user player in session variable, for analytic purposes
+ */
+ function set_user_session_player()
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ if (!$session->has('player')) {
+ $playerFound = null;
+ $userAgent = $_SERVER['HTTP_USER_AGENT'];
+
+ try {
+ $playerFound = \Opawg\UserAgentsPhp\UserAgents::find(
+ $userAgent,
+ );
+ } catch (\Exception $e) {
+ // If things go wrong the show must go on and the user must be able to download the file
+ }
+ if ($playerFound) {
+ $session->set('player', $playerFound);
+ } else {
+ $session->set('player', [
+ 'app' => '- unknown -',
+ 'device' => '',
+ 'os' => '',
+ 'bot' => 0,
+ ]);
+ // Add to unknown list
+ try {
+ $db = \Config\Database::connect();
+ $procedureNameAnalyticsUnknownUseragents = $db->prefixTable(
+ 'analytics_unknown_useragents',
+ );
+ $db->query(
+ "CALL $procedureNameAnalyticsUnknownUseragents(?)",
+ [$userAgent],
+ );
+ } catch (\Exception $e) {
+ // If things go wrong the show must go on and the user must be able to download the file
+ }
+ }
+ }
+ }
+}
+
+if (!function_exists('set_user_session_browser')) {
+ /**
+ * Set user browser in session variable, for analytic purposes
+ *
+ * @return void
+ */
+ function set_user_session_browser()
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ if (!$session->has('browser')) {
+ $browserName = '- Other -';
+ try {
+ $whichbrowser = new \WhichBrowser\Parser(getallheaders());
+ $browserName = $whichbrowser->browser->name;
+ } catch (\Exception $e) {
+ $browserName = '- Could not get browser name -';
+ }
+ if ($browserName == null) {
+ $browserName = '- Could not get browser name -';
+ }
+ $session->set('browser', $browserName);
+ }
+ }
+}
+
+if (!function_exists('set_user_session_referer')) {
+ /**
+ * Set user referer in session variable, for analytic purposes
+ *
+ * @return void
+ */
+ function set_user_session_referer()
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ $newreferer = isset($_SERVER['HTTP_REFERER'])
+ ? $_SERVER['HTTP_REFERER']
+ : '- Direct -';
+ $newreferer =
+ parse_url($newreferer, PHP_URL_HOST) ==
+ parse_url(current_url(false), PHP_URL_HOST)
+ ? '- Direct -'
+ : $newreferer;
+ if (!$session->has('referer') or $newreferer != '- Direct -') {
+ $session->set('referer', $newreferer);
+ }
+ }
+}
+
+if (!function_exists('set_user_session_entry_page')) {
+ /**
+ * Set user entry page in session variable, for analytic purposes
+ *
+ * @return void
+ */
+ function set_user_session_entry_page()
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ $entryPage = $_SERVER['REQUEST_URI'];
+ if (!$session->has('entryPage')) {
+ $session->set('entryPage', $entryPage);
+ }
+ }
+}
+
+if (!function_exists('webpage_hit')) {
+ /**
+ *
+ * @param integer $podcastId
+ * @return void
+ */
+ function webpage_hit($podcastId)
+ {
+ $session = \Config\Services::session();
+ $session->start();
+
+ if (!$session->get('denyListIp')) {
+ $db = \Config\Database::connect();
+
+ $referer = $session->get('referer');
+ $domain = empty(parse_url($referer, PHP_URL_HOST))
+ ? '- Direct -'
+ : parse_url($referer, PHP_URL_HOST);
+ parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
+ $keywords = empty($queries['q']) ? null : $queries['q'];
+
+ $procedureName = $db->prefixTable('analytics_website');
+ $db->query("call $procedureName(?,?,?,?,?,?)", [
+ $podcastId,
+ $session->get('browser'),
+ $session->get('entryPage'),
+ $referer,
+ $domain,
+ $keywords,
+ ]);
+ }
+ }
+}
+
+if (!function_exists('podcast_hit')) {
+ /**
+ * Counting podcast episode downloads for analytic purposes
+ * ✅ No IP address is ever stored on the server.
+ * ✅ Only aggregate data is stored in the database.
+ * We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
+ * https://iabtechlab.com/standards/podcast-measurement-guidelines/
+ * https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf
+ * ✅ Rolling 24-hour window
+ * ✅ Castopod does not do pre-load
+ * ✅ IP deny list https://github.com/client9/ipcat
+ * ✅ User-agent Filtering https://github.com/opawg/user-agents
+ * ✅ RSS User-agent https://github.com/opawg/podcast-rss-useragents
+ * ✅ Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app)
+ * ✅ In case of partial content, adds up all requests to check >1mn was downloaded
+ * ✅ Identifying Uniques is done with a combination of IP Address and User Agent
+ * @param integer $podcastId The podcast ID
+ * @param integer $episodeId The Episode ID
+ * @param integer $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn)
+ * @param integer $fileSize The podcast complete file size
+ * @param string $serviceName The name of the service that had fetched the RSS feed
+ *
+ * @return void
+ */
+ function podcast_hit(
+ $podcastId,
+ $episodeId,
+ $bytesThreshold,
+ $fileSize,
+ $duration,
+ $publicationDate,
+ $serviceName
+ ) {
+ $session = \Config\Services::session();
+ $session->start();
+
+ // We try to count (but if things went wrong the show should go on and the user should be able to download the file):
+ try {
+ // If the user IP is denied it's probably a bot:
+ if ($session->get('denyListIp')) {
+ $session->get('player')['bot'] = true;
+ }
+ //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'] .
+ '_' .
+ $_SERVER['HTTP_USER_AGENT'] .
+ '_' .
+ $episodeId,
+ );
+ // Was this episode downloaded in the past 24h:
+ $downloadedBytes = cache($episodeHashId);
+ // Rolling window is 24 hours (86400 seconds):
+ $rollingTTL = 86400;
+ if ($downloadedBytes) {
+ // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
+ $rollingTTL =
+ cache()->getMetadata($episodeHashId)['expire'] - time();
+ } else {
+ // If it was never downloaded that means that zero byte were downloaded:
+ $downloadedBytes = 0;
+ }
+ // If the number of downloaded bytes was previously below the 1mn threshold we go on:
+ // (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 (!$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.
+ // We don't count these requests:
+ if ($httpRange != 'bytes=0-1') {
+ // We calculate how many bytes are being downloaded based on HTTP_RANGE values:
+ $ranges = explode(',', substr($httpRange, 6));
+ foreach ($ranges as $range) {
+ $parts = explode('-', $range);
+ $downloadedBytes += empty($parts[1])
+ ? $fileSize
+ : $parts[1] -
+ (empty($parts[0]) ? 0 : $parts[0]);
+ }
+ }
+ }
+ // We save the number of downloaded bytes for this user and this episode:
+ cache()->save($episodeHashId, $downloadedBytes, $rollingTTL);
+
+ // 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');
+
+ $age = intdiv(time() - $publicationDate, 86400);
+
+ // 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,
+ );
+
+ $db->query(
+ "CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
+ [
+ $podcastId,
+ $episodeId,
+ $session->get('location')['countryCode'],
+ $session->get('location')['regionCode'],
+ $session->get('location')['latitude'],
+ $session->get('location')['longitude'],
+ $serviceName,
+ $session->get('player')['app'],
+ $session->get('player')['device'],
+ $session->get('player')['os'],
+ $session->get('player')['bot'],
+ $fileSize,
+ $duration,
+ $age,
+ $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/Models/AnalyticsPodcastByCountryModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByCountryModel.php
similarity index 95%
rename from app/Models/AnalyticsPodcastByCountryModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastByCountryModel.php
index af76704c..13b9fdf8 100644
--- a/app/Models/AnalyticsPodcastByCountryModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByCountryModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastByCountryModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcastsByCountry::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcastsByCountry::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Libraries/Analytics/Models/AnalyticsPodcastByEpisodeModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByEpisodeModel.php
new file mode 100644
index 00000000..ed53ce22
--- /dev/null
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByEpisodeModel.php
@@ -0,0 +1,90 @@
+select('date as labels')
+ ->selectSum('hits', 'values')
+ ->where([
+ 'episode_id' => $episodeId,
+ 'podcast_id' => $podcastId,
+ 'age <' => 60,
+ ])
+ ->groupBy('labels')
+ ->orderBy('labels', 'ASC')
+ ->findAll();
+
+ cache()->save(
+ "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
+ $found,
+ 600,
+ );
+ }
+ return $found;
+ }
+
+ /**
+ * @param int $podcastId
+ * @param int $episodeId
+ *
+ * @return array
+ */
+ public function getDataByMonth(int $podcastId, int $episodeId = null): array
+ {
+ if (
+ !($found = cache(
+ "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
+ ))
+ ) {
+ $found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
+ ->selectSum('hits', 'values')
+ ->where([
+ 'episode_id' => $episodeId,
+ 'podcast_id' => $podcastId,
+ ])
+ ->groupBy('labels')
+ ->orderBy('labels', 'ASC')
+ ->findAll();
+
+ cache()->save(
+ "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
+ $found,
+ 600,
+ );
+ }
+ return $found;
+ }
+}
diff --git a/app/Models/AnalyticsPodcastByHourModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByHourModel.php
similarity index 92%
rename from app/Models/AnalyticsPodcastByHourModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastByHourModel.php
index df43d038..4b5df270 100644
--- a/app/Models/AnalyticsPodcastByHourModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByHourModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastByHourModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcastsByHour::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcastsByHour::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsPodcastByPlayerModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByPlayerModel.php
similarity index 98%
rename from app/Models/AnalyticsPodcastByPlayerModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastByPlayerModel.php
index 668ca123..23784344 100644
--- a/app/Models/AnalyticsPodcastByPlayerModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByPlayerModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastByPlayerModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcastsByPlayer::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcastsByPlayer::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByRegionModel.php
similarity index 93%
rename from app/Models/AnalyticsPodcastByRegionModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastByRegionModel.php
index be9a81c0..76a2f2dc 100644
--- a/app/Models/AnalyticsPodcastByRegionModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByRegionModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastByRegionModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcastsByRegion::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsPodcastByServiceModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastByServiceModel.php
similarity index 93%
rename from app/Models/AnalyticsPodcastByServiceModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastByServiceModel.php
index 9170f861..a3039ae9 100644
--- a/app/Models/AnalyticsPodcastByServiceModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastByServiceModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastByServiceModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcastsByService::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcastsByService::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Libraries/Analytics/Models/AnalyticsPodcastModel.php
similarity index 98%
rename from app/Models/AnalyticsPodcastModel.php
rename to app/Libraries/Analytics/Models/AnalyticsPodcastModel.php
index b8e84fb1..a621bae2 100644
--- a/app/Models/AnalyticsPodcastModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsPodcastModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsPodcastModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsPodcasts::class;
+ protected $returnType = \Analytics\Entities\AnalyticsPodcasts::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsUnknownUseragentsModel.php b/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
similarity index 82%
rename from app/Models/AnalyticsUnknownUseragentsModel.php
rename to app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
index 8dae7486..c002668b 100644
--- a/app/Models/AnalyticsUnknownUseragentsModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsUnknownUseragentsModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -19,7 +19,7 @@ class AnalyticsUnknownUseragentsModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsUnknownUseragents::class;
+ protected $returnType = \Analytics\Entities\AnalyticsUnknownUseragents::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Libraries/Analytics/Models/AnalyticsWebsiteByBrowserModel.php
similarity index 92%
rename from app/Models/AnalyticsWebsiteByBrowserModel.php
rename to app/Libraries/Analytics/Models/AnalyticsWebsiteByBrowserModel.php
index b7b7c132..efdfc95f 100644
--- a/app/Models/AnalyticsWebsiteByBrowserModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsWebsiteByBrowserModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsWebsiteByBrowserModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsWebsiteByBrowser::class;
+ protected $returnType = \Analytics\Entities\AnalyticsWebsiteByBrowser::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Libraries/Analytics/Models/AnalyticsWebsiteByEntryPageModel.php
similarity index 92%
rename from app/Models/AnalyticsWebsiteByEntryPageModel.php
rename to app/Libraries/Analytics/Models/AnalyticsWebsiteByEntryPageModel.php
index 220719d4..95931b42 100644
--- a/app/Models/AnalyticsWebsiteByEntryPageModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsWebsiteByEntryPageModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsWebsiteByEntryPageModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsWebsiteByEntryPage::class;
+ protected $returnType = \Analytics\Entities\AnalyticsWebsiteByEntryPage::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Libraries/Analytics/Models/AnalyticsWebsiteByRefererModel.php
similarity index 96%
rename from app/Models/AnalyticsWebsiteByRefererModel.php
rename to app/Libraries/Analytics/Models/AnalyticsWebsiteByRefererModel.php
index aed2f46b..65ee8dd2 100644
--- a/app/Models/AnalyticsWebsiteByRefererModel.php
+++ b/app/Libraries/Analytics/Models/AnalyticsWebsiteByRefererModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
@@ -18,7 +18,7 @@ class AnalyticsWebsiteByRefererModel extends Model
protected $allowedFields = [];
- protected $returnType = \App\Entities\AnalyticsWebsiteByReferer::class;
+ protected $returnType = \Analytics\Entities\AnalyticsWebsiteByReferer::class;
protected $useSoftDeletes = false;
protected $useTimestamps = false;
diff --git a/app/Models/UnknownUserAgentsModel.php b/app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
similarity index 95%
rename from app/Models/UnknownUserAgentsModel.php
rename to app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
index 5afabf5d..1f531f45 100644
--- a/app/Models/UnknownUserAgentsModel.php
+++ b/app/Libraries/Analytics/Models/UnknownUserAgentsModel.php
@@ -8,7 +8,7 @@
* @link https://castopod.org/
*/
-namespace App\Models;
+namespace Analytics\Models;
use CodeIgniter\Model;
diff --git a/app/Models/AnalyticsPodcastByEpisodeModel.php b/app/Models/AnalyticsPodcastByEpisodeModel.php
deleted file mode 100644
index 15725032..00000000
--- a/app/Models/AnalyticsPodcastByEpisodeModel.php
+++ /dev/null
@@ -1,145 +0,0 @@
-select('id, season_number, number, title')
- ->orderBy('id', 'DESC')
- ->where(['podcast_id' => $podcastId])
- ->findAll(5);
-
- $found = $this->select('age AS X');
-
- $letter = 97;
- foreach ($lastEpisodes as $episode) {
- $found = $found
- ->selectSum(
- '(CASE WHEN episode_id=' .
- $episode->id .
- ' THEN hits END)',
- '' . chr($letter) . 'Y',
- )
- ->select(
- '"' .
- (empty($episode->season_number)
- ? ''
- : $episode->season_number) .
- (empty($episode->number)
- ? ''
- : '-' . $episode->number . '/ ') .
- $episode->title .
- '" AS ' .
- chr($letter) .
- 'Value',
- );
- $letter++;
- }
-
- $found = $found
- ->where([
- 'podcast_id' => $podcastId,
- 'age <' => 60,
- ])
- ->groupBy('X')
- ->orderBy('X', 'ASC')
- ->findAll();
-
- cache()->save(
- "{$podcastId}_analytics_podcast_by_episode_by_day",
- $found,
- 600,
- );
- }
- return $found;
- } else {
- if (
- !($found = cache(
- "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
- ))
- ) {
- $found = $this->select('date as labels')
- ->selectSum('hits', 'values')
- ->where([
- 'episode_id' => $episodeId,
- 'podcast_id' => $podcastId,
- 'age <' => 60,
- ])
- ->groupBy('labels')
- ->orderBy('labels', 'ASC')
- ->findAll();
-
- cache()->save(
- "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day",
- $found,
- 600,
- );
- }
- return $found;
- }
- }
-
- /**
- * @param int $podcastId, $episodeId
- *
- * @return array
- */
- public function getDataByMonth(int $podcastId, int $episodeId = null): array
- {
- if (
- !($found = cache(
- "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
- ))
- ) {
- $found = $this->select('DATE_FORMAT(date,"%Y-%m-01") as labels')
- ->selectSum('hits', 'values')
- ->where([
- 'episode_id' => $episodeId,
- 'podcast_id' => $podcastId,
- ])
- ->groupBy('labels')
- ->orderBy('labels', 'ASC')
- ->findAll();
-
- cache()->save(
- "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month",
- $found,
- 600,
- );
- }
- return $found;
- }
-}
diff --git a/app/Views/admin/podcast/analytics/index.php b/app/Views/admin/podcast/analytics/index.php
index 54b95443..69b32b2e 100644
--- a/app/Views/admin/podcast/analytics/index.php
+++ b/app/Views/admin/podcast/analytics/index.php
@@ -15,7 +15,7 @@
'analytics-data',
$podcast->id,
'Podcast',
- 'ByDay'
+ 'ByDay',
) ?>">
@@ -25,7 +25,7 @@
'analytics-data',
$podcast->id,
'Podcast',
- 'ByMonth'
+ 'ByMonth',
) ?>">
@@ -35,17 +35,7 @@
'analytics-data',
$podcast->id,
'Podcast',
- 'BandwidthByDay'
-) ?>">
-
-
-
-
= lang('Charts.episodes_by_day') ?>
-
diff --git a/docs/setup-development.md b/docs/setup-development.md
index f39ba6b8..6423818d 100644
--- a/docs/setup-development.md
+++ b/docs/setup-development.md
@@ -95,9 +95,9 @@ Go to project's root folder and run:
docker-compose up -d
# See all running processes (you should see 3 processes running)
-docker ps
+docker-compose ps
-# Alternatively, you can check all processes (you should see composer with an Exited status)
+# Alternatively, you can check all docker processes (you should see composer and npm with an Exited status)
docker ps -a
```
@@ -146,17 +146,20 @@ docker-compose run --rm app php spark db:seed LanguageSeeder
docker-compose run --rm app php spark db:seed PlatformSeeder
# Populates all Authentication data (roles definition…)
docker-compose run --rm app php spark db:seed AuthSeeder
-# Populates test data (login: admin / password: AGUehL3P)
-docker-compose run --rm app php spark db:seed TestSeeder
```
3. (optionnal) Populate the database with test data:
```bash
+# Populates test data (login: admin / password: AGUehL3P)
docker-compose run --rm app php spark db:seed TestSeeder
+# Populates with fake podcast analytics
+docker-compose run --rm app php spark db:seed FakePodcastsAnalyticsSeeder
+# Populates with fake website analytics
+docker-compose run --rm app php spark db:seed FakeWebsiteAnalyticsSeeder
```
-This will add an active superadmin user with the following credentials:
+TestSeeder will add an active superadmin user with the following credentials:
- username: **admin**
- password: **AGUehL3P**
@@ -205,8 +208,8 @@ To see your changes, go to:
- [localhost:8080](http://localhost:8080/) for the castopod app
- [localhost:8888](http://localhost:8888/) for the phpmyadmin interface:
- - **Username**: podlibre
- - **Password**: castopod
+ - username: **podlibre**
+ - password: **castopod**
---
@@ -216,19 +219,22 @@ To see your changes, go to:
```bash
# monitor the app container
-docker logs --tail 50 --follow --timestamps castopod_app
+docker-compose logs --tail 50 --follow --timestamps app
# monitor the mariadb container
-docker logs --tail 50 --follow --timestamps castopod_mariadb
+docker-compose logs --tail 50 --follow --timestamps mariadb
# monitor the phpmyadmin container
-docker logs --tail 50 --follow --timestamps castopod_phpmyadmin
+docker-compose logs --tail 50 --follow --timestamps phpmyadmin
# restart docker containers
docker-compose restart
# Destroy all containers, opposite of `up` command
docker-compose down
+
+# Rebuild app container
+docker-compose build app
```
Check [docker](https://docs.docker.com/engine/reference/commandline/docker/) and