Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Show changes
Commits on Source (3)
Showing
with 265 additions and 38 deletions
# [1.0.0-alpha.14](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.13...v1.0.0-alpha.14) (2020-11-02)
### Features
* **analytics:** add weekday and hour bar charts ([8ab3132](https://code.podlibre.org/podlibre/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
# [1.0.0-alpha.13](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2020-10-29)
......
# Castopod
# ![Castopod Logo](https://podlibre.org/static/images/Castopod-Title.svg)
Castopod is an open-source podcast hosting solution for everyone. Whether you
are a beginner, an amateur or a professional, you will get everything you need:
create, upload, publish, manage server subscriptions (WebSub embedded server),
connect to the usual directories (Apple, Google, Spotify…), connect to the
Fediverse (ActivityPub, Mastodon, Pleroma…) and measure your audience (IAB 2.0
compliant) so that you can monetize your content. Take back control: interact
with your audience on your plateform (like, share, comment), the social network
IS the podcast. Of course you may also export to proprietary social
networks(Twitter, Instagram, Youtube, Facebook). Castopod can be hosted on any
PHP/MySQL server: Unzip it and you and other podcasters are ready to broadcast
professionally.
Castopod is an open-source podcast hosting solution for everyone.\
Whether you are a beginner, an amateur or a professional, you will get everything
you need:\
Create, upload, publish, and get comprehensive audience measurement that respects your
listeners privacy.
## Free
Castopod is a free and open-source solution (AGPL v3).\
Whether you choose to install it on your own server or have it hosted by a
professional, all your data and analytics belong to you and you only.
Castopod is a free and open-source solution (AGPL v3). Whether you choose to
install it on your own server or have it hosted by a professional, all your data
and analytics belong to you and you only.
![Castopod Logo](https://podlibre.org/static/images/Castopod-Mascot-Server.svg)
## Social Media
## Installation
Castopod is a part of Fediverse (Mastodon, Pleroma, PixelFed, PeerTube…).
Podcasters and their audience can post, subscribe, like, comment and share
natively. Millions of users already on Fediverse will be able to interact
seamlessly.
Castopod can be hosted on any PHP/MySQL server:\
Unzip it and you are ready to broadcast.
## Flexible
Castopod is compatible with all Podcasts players and platforms (it can
automatically generate an RSS feed). Moreover Podcasters can choose to publish
on Castopod while keeping their existing hosting solution (it can automatically
generate posts from an existing RSS feed).
![Castopod Users](https://podlibre.org/static/images/Business-31.svg)
---
To install Castopod on your server:
- Download [Castopod latest Package (zip or tar.gz)](https://code.podlibre.org/podlibre/castopod/-/releases),
- Follow the procedure “[How to install Castopod](./INSTALL.md)”.
## Documentation
......
......@@ -7,7 +7,7 @@
//
// NOTE: this constant is updated upon release with Continuous Integration.
//
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.13');
defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.14');
//--------------------------------------------------------------------
// App Namespace
......
......@@ -158,6 +158,14 @@ $routes->group(
'filter' => 'permission:podcasts-view,podcast-view',
]
);
$routes->get(
'time-periods',
'Podcast::viewAnalyticsTimePeriods/$1',
[
'as' => 'podcast-analytics-time-periods',
'filter' => 'permission:podcasts-view,podcast-view',
]
);
$routes->get(
'players',
'Podcast::viewAnalyticsPlayers/$1',
......
......@@ -98,6 +98,14 @@ class Podcast extends BaseController
return view('admin/podcast/analytics/listening_time', $data);
}
public function viewAnalyticsTimePeriods()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/time_periods', $data);
}
public function viewAnalyticsPlayers()
{
$data = ['podcast' => $this->podcast];
......
......@@ -28,4 +28,5 @@ return [
'unique-listeners' => 'Unique listeners',
'players' => 'Players',
'listening-time' => 'Listening time',
'time-periods' => 'Time periods',
];
......@@ -30,4 +30,7 @@ return [
'podcast_bots' => 'Bots (crawlers)',
'daily_listening_time' => 'Daily cumulative listening time',
'monthly_listening_time' => 'Monthly cumulative listening time',
'by_weekday' => 'By week day (for the past 60 days)',
'by_hour' => 'By time of day (for the past 60 days)',
'podcast_by_bandwidth' => 'Daily used bandwidth (in MB)',
];
......@@ -26,4 +26,5 @@ return [
'podcast-analytics-unique-listeners' => 'Unique listeners',
'podcast-analytics-players' => 'Players',
'podcast-analytics-listening-time' => 'Listening time',
'podcast-analytics-time-periods' => 'Time periods',
];
......@@ -28,4 +28,5 @@ return [
'unique-listeners' => 'Auditeurs uniques',
'players' => 'Lecteurs',
'listening-time' => 'Durée d’écoute',
'time-periods' => 'Périodes',
];
......@@ -43,4 +43,7 @@ return [
'podcast_bots' => 'Robots (bots)',
'daily_listening_time' => 'Durée quotidienne d’écoute cumulée',
'monthly_listening_time' => 'Durée mensuelle d’écoute cumulée',
'by_weekday' => 'Par jour de la semaine (sur les 60 derniers jours)',
'by_hour' => 'Par heure de la journée (sur les 60 derniers jours)',
'podcast_by_bandwidth' => 'Bande passante quotidienne consommée (en Mo)',
];
......@@ -26,4 +26,5 @@ return [
'podcast-analytics-unique-listeners' => 'Auditeurs uniques',
'podcast-analytics-players' => 'Lecteurs',
'podcast-analytics-listening-time' => 'Durée d’écoute',
'podcast-analytics-time-periods' => 'Périodes',
];
......@@ -37,15 +37,33 @@ class AnalyticsPodcastByCountryModel extends Model
"{$podcastId}_analytics_podcast_by_country_weekly"
))
) {
$oneWeekAgo=date('Y-m-d', strtotime('-1 week'));
$found = $this->select('`country_code` as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
'`date` >' => $oneWeekAgo,
])
->groupBy('`labels`')
->orderBy('`values`', 'DESC')
->findAll(10);
->findAll(9);
$found[] = $this->db
->query(
"SELECT
\"Other\" AS `labels`,
SUM(`others`.`values`) AS `values`
FROM
(SELECT SUM(`hits`) AS `values`
FROM {$this->db->prefixTable($this->table)}
WHERE `date` > $oneWeekAgo
GROUP BY `country_code`
ORDER BY `values` DESC
LIMIT 18446744073709551610 OFFSET 9
) AS `others`
GROUP BY `labels`"
)
->getRow(0);
cache()->save(
"{$podcastId}_analytics_podcast_by_country_weekly",
......@@ -70,16 +88,34 @@ class AnalyticsPodcastByCountryModel extends Model
"{$podcastId}_analytics_podcast_by_country_yearly"
))
) {
$oneYearAgo = date('Y-m-d', strtotime('-1 year'));
$found = $this->select('`country_code` as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 year')),
'`date` >' => $oneYearAgo,
])
->groupBy('`labels`')
->orderBy('`values`', 'DESC')
->findAll(10);
$found[] = $this->db
->query(
"SELECT
\"Other\" AS `labels`,
SUM(`others`.`values`) AS `values`
FROM
(SELECT SUM(`hits`) AS `values`
FROM {$this->db->prefixTable($this->table)}
WHERE `date` > $oneyearago
GROUP BY `country_code`
ORDER BY `values` DESC
LIMIT 18446744073709551610 OFFSET 9
) AS `others`
GROUP BY `labels`"
)
->getRow(0);
cache()->save(
"{$podcastId}_analytics_podcast_by_country_yearly",
$found,
......
......@@ -22,4 +22,38 @@ class AnalyticsPodcastByHourModel extends Model
protected $useSoftDeletes = false;
protected $useTimestamps = false;
/**
* Gets hits data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getData(int $podcastId): array
{
if (!($found = cache("{$podcastId}_analytics_podcasts_by_hour"))) {
$found = $this->select('`hour` as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-60 days')),
])
->groupBy('`labels`')
->orderBy('`labels`', 'ASC')
->findAll();
cache()->save(
"{$podcastId}_analytics_podcasts_by_hour",
$found,
600
);
}
return $found;
}
}
......@@ -46,6 +46,66 @@ class AnalyticsPodcastModel extends Model
return $found;
}
/**
* Gets hits data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getDataByWeekday(int $podcastId): array
{
if (!($found = cache("{$podcastId}_analytics_podcasts_by_weekday"))) {
$found = $this->select(
'LEFT(DAYNAME(`date`),3) as `labels`, WEEKDAY(`date`) as `sort_labels`'
)
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-60 days')),
])
->groupBy('`labels`, `sort_labels`')
->orderBy('`sort_labels`', 'ASC')
->findAll();
cache()->save(
"{$podcastId}_analytics_podcasts_by_weekday",
$found,
600
);
}
return $found;
}
/**
* Gets bandwidth data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getDataBandwidthByDay(int $podcastId): array
{
if (!($found = cache("{$podcastId}_analytics_podcast_by_bandwidth"))) {
$found = $this->select(
'`date` as `labels`, round(`bandwidth` / 1048576, 1) as `values`'
)
->where([
'`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-60 days')),
])
->orderBy('`labels`', 'ASC')
->findAll();
cache()->save(
"{$podcastId}_analytics_podcast_by_bandwidth",
$found,
600
);
}
return $found;
}
/**
* Gets hits data for a podcast
*
......
......@@ -52,7 +52,7 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
const series = chart.series.push(new am4charts.LineSeries());
series.dataFields.valueY = "values";
series.dataFields.dateX = "labels";
series.tooltipText = "{valueY} hits";
series.tooltipText = "{valueY}";
series.strokeWidth = 2;
// Make bullets grow on hover
const bullet = series.bullets.push(new am4charts.CircleBullet());
......@@ -68,6 +68,35 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
chart.scrollbarX = new am4core.Scrollbar();
};
const drawBarChart = (chartDivId: string, dataUrl: string | null): void => {
// Create chart instance
const chart = am4core.create(chartDivId, am4charts.XYChart);
am4core.percent(100);
chart.exporting.menu = new am4core.ExportMenu();
chart.exporting.menu.align = "right";
chart.exporting.menu.verticalAlign = "bottom";
// Set theme
am4core.useTheme(am4themes_material);
chart.dataSource.url = dataUrl || "";
chart.dataSource.parser.options.emptyAs = 0;
const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
categoryAxis.dataFields.category = "labels";
categoryAxis.renderer.grid.template.location = 0;
categoryAxis.renderer.minGridDistance = 30;
chart.yAxes.push(new am4charts.ValueAxis());
// Create series
const series = chart.series.push(new am4charts.ColumnSeries());
series.dataFields.valueY = "values";
series.dataFields.categoryX = "labels";
series.name = "Hits";
series.columns.template.tooltipText = "{valueY} hits";
series.columns.template.fillOpacity = .8;
const columnTemplate = series.columns.template;
columnTemplate.strokeWidth = 2;
columnTemplate.strokeOpacity = 1;
};
const drawXYDurationChart = (
chartDivId: string,
dataUrl: string | null
......@@ -205,6 +234,9 @@ const DrawCharts = (): void => {
case "xy-chart":
drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
break;
case "bar-chart":
drawBarChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
break;
case "xy-duration-chart":
drawXYDurationChart(
chartDiv.id,
......
......@@ -16,6 +16,7 @@ $podcastNavigation = [
'podcast-analytics-listening-time',
'podcast-analytics-players',
'podcast-analytics-locations',
'podcast-analytics-time-periods',
'podcast-analytics-webpages',
],
],
......
......@@ -29,6 +29,16 @@
) ?>"></div>
</div>
<div class="mb-12 text-center">
<h2><?= lang('Charts.podcast_by_bandwidth') ?></h2>
<div class="chart-xy" id="by-bandwidth-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'BandwidthByDay'
) ?>"></div>
</div>
<div class="mb-12 text-center">
<h2><?= lang('Charts.episodes_by_day') ?></h2>
<div class="chart-xy" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to(
......
<?= $this->extend('admin/_layout') ?>
<?= $this->section('title') ?>
<?= $podcast->title ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= $podcast->title ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<div class="lg:divide-x lg:grid lg:grid-cols-2">
<div class="mb-12 mr-6 text-center">
<h2><?= lang('Charts.by_weekday') ?></h2>
<div class="chart-xy" id="by-weekday-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to(
'analytics-data',
$podcast->id,
'Podcast',
'ByWeekday'
) ?>"></div>
</div>
<div class="mb-12 mr-6 text-center">
<h2><?= lang('Charts.by_hour') ?></h2>
<div class="chart-xy" id="by-hour-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to(
'analytics-full-data',
$podcast->id,
'PodcastByHour'
) ?>"></div>
</div>
</div>
<script src="/assets/charts.js" type="module"></script>
<?= $this->endSection() ?>
{
"name": "podlibre/castopod",
"version": "1.0.0-alpha13",
"version": "1.0.0-alpha14",
"type": "project",
"description": "Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
"homepage": "https://castopod.org",
......
{
"name": "castopod",
"version": "1.0.0-alpha.13",
"version": "1.0.0-alpha.14",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......