feat(episodes): replace all audio file URL parameters with base64 encoded data

This commit is contained in:
Benjamin Bellamy 2020-10-29 17:27:16 +01:00
parent 391c349daa
commit e1f65cd3b5
5 changed files with 71 additions and 41 deletions

View File

@ -31,6 +31,7 @@ $routes->setAutoRoute(false);
$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
/**
* --------------------------------------------------------------------
@ -59,14 +60,10 @@ $routes->group(config('App')->installGateway, function ($routes) {
]);
});
// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/duration/podcast_folder/filename.mp3)
$routes->add(
'audio/(:num)/(:num)/(:num)/(:num)/(:num)/(:any)',
'Analytics::hit/$1/$2/$3/$4/$5/$6',
[
'as' => 'analytics_hit',
]
);
// Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3)
$routes->add('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [
'as' => 'analytics_hit',
]);
// Show the Unknown UserAgents
$routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');

View File

@ -46,24 +46,24 @@ class Analytics extends Controller
}
// Add one hit to this episode:
public function hit(
$podcastId,
$episodeId,
$bytesThreshold,
$fileSize,
$duration,
...$filename
) {
helper('media');
public function hit($base64EpisodeData, ...$filename)
{
helper('media', 'analytics');
$serviceName = isset($_GET['_from']) ? $_GET['_from'] : '';
$episodeData = unpack(
'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
base64_url_decode($base64EpisodeData)
);
podcast_hit(
$podcastId,
$episodeId,
$bytesThreshold,
$fileSize,
$duration,
$episodeData['podcastId'],
$episodeData['episodeId'],
$episodeData['bytesThreshold'],
$episodeData['fileSize'],
$episodeData['duration'],
$episodeData['publicationDate'],
$serviceName
);
return redirect()->to(media_base_url($filename));

View File

@ -35,6 +35,7 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` (
IN `p_bot` TINYINT(1) UNSIGNED,
IN `p_filesize` INT UNSIGNED,
IN `p_duration` INT UNSIGNED,
IN `p_age` INT UNSIGNED,
IN `p_new_listener` TINYINT(1) UNSIGNED
) MODIFIES SQL DATA
DETERMINISTIC
@ -57,8 +58,8 @@ IF NOT `p_bot` THEN
INSERT INTO `{$prefix}analytics_podcasts_by_hour`(`podcast_id`, `date`, `hour`)
VALUES (p_podcast_id, @current_date, @current_hour)
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`)
SELECT p_podcast_id, p_episode_id, @current_date, datediff(@current_datetime,`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id
INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`)
VALUES (p_podcast_id, p_episode_id, @current_date, p_age)
ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`)
VALUES (p_podcast_id, p_country_code, @current_date)

View File

@ -179,25 +179,37 @@ class Episode extends Entity
public function getEnclosureUrl()
{
helper('analytics');
return base_url(
route_to(
'analytics_hit',
$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'],
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']
)
);

View File

@ -30,6 +30,22 @@ if (!function_exists('getallheaders')) {
}
}
/**
* Encode Base64 for URLs
*/
function base64_url_encode($input)
{
return strtr(base64_encode($input), '+/=', '._-');
}
/**
* Decode Base64 from URL
*/
function base64_url_decode($input)
{
return base64_decode(strtr($input, '._-', '+/='));
}
/**
* Set user country in session variable, for analytics purpose
*/
@ -245,6 +261,7 @@ function podcast_hit(
$bytesThreshold,
$fileSize,
$duration,
$publicationDate,
$serviceName
) {
$session = \Config\Services::session();
@ -311,6 +328,8 @@ function podcast_hit(
$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_' .
@ -337,7 +356,7 @@ function podcast_hit(
cache()->save($listenerHashId, $downloadsByUser, $midnightTTL);
$db->query(
"CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
"CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
[
$podcastId,
$episodeId,
@ -352,6 +371,7 @@ function podcast_hit(
$session->get('player')['bot'],
$fileSize,
$duration,
$age,
$newListener,
]
);