diff --git a/config/routes/api_station.php b/config/routes/api_station.php
index 791b07231..ab931c35d 100644
--- a/config/routes/api_station.php
+++ b/config/routes/api_station.php
@@ -517,6 +517,11 @@ return static function (RouteCollectorProxy $group) {
Controller\Api\Stations\Reports\Overview\ByCountry::class
)->setName('api:stations:reports:by-country');
+ $group->get(
+ '/overview/by-stream',
+ Controller\Api\Stations\Reports\Overview\ByStream::class
+ )->setName('api:stations:reports:by-stream');
+
$group->get(
'/soundexchange',
Controller\Api\Stations\Reports\SoundExchangeAction::class
diff --git a/frontend/vue/components/Stations/Reports/Overview.vue b/frontend/vue/components/Stations/Reports/Overview.vue
index 1a631fdac..52bacceb8 100644
--- a/frontend/vue/components/Stations/Reports/Overview.vue
+++ b/frontend/vue/components/Stations/Reports/Overview.vue
@@ -30,6 +30,15 @@
+
+
+ Streams
+
+
+
+
+
+
Browsers
@@ -58,9 +67,11 @@ import ListenersByTimePeriodTab from "./Overview/ListenersByTimePeriodTab";
import BestAndWorstTab from "./Overview/BestAndWorstTab";
import BrowsersTab from "./Overview/BrowsersTab";
import CountriesTab from "~/components/Stations/Reports/Overview/CountriesTab";
+import StreamsTab from "~/components/Stations/Reports/Overview/StreamsTab";
export default {
components: {
+ StreamsTab,
CountriesTab,
BrowsersTab,
BestAndWorstTab,
@@ -72,6 +83,7 @@ export default {
showFullAnalytics: Boolean,
listenersByTimePeriodUrl: String,
bestAndWorstUrl: String,
+ byStreamUrl: String,
byBrowserUrl: String,
byCountryUrl: String,
},
diff --git a/frontend/vue/components/Stations/Reports/Overview/StreamsTab.vue b/frontend/vue/components/Stations/Reports/Overview/StreamsTab.vue
new file mode 100644
index 000000000..bbd7a935b
--- /dev/null
+++ b/frontend/vue/components/Stations/Reports/Overview/StreamsTab.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatTime(row.item.connected_seconds) }}
+
+
+
+
+
+
+
diff --git a/src/Controller/Api/Stations/Reports/Overview/ByStream.php b/src/Controller/Api/Stations/Reports/Overview/ByStream.php
new file mode 100644
index 000000000..cc318f0ff
--- /dev/null
+++ b/src/Controller/Api/Stations/Reports/Overview/ByStream.php
@@ -0,0 +1,99 @@
+isAnalyticsEnabled()) {
+ return $response->withStatus(400)
+ ->withJson(new Entity\Api\Status(false, 'Reporting is restricted due to system analytics level.'));
+ }
+
+ $station = $request->getStation();
+ $stationTz = $station->getTimezoneObject();
+
+ $dateRange = $this->getDateRange($request, $stationTz);
+
+ $statsRaw = $this->em->getConnection()->fetchAllAssociative(
+ <<<'SQL'
+ SELECT l.stream_id,
+ COUNT(l.listener_hash) AS listeners,
+ SUM(l.connected_seconds) AS connected_seconds
+ FROM (
+ SELECT IF (
+ mount_id IS NOT NULL,
+ CONCAT('local_', mount_id),
+ CONCAT('remote_', remote_id)
+ ) AS stream_id,
+ SUM(timestamp_end - timestamp_start) AS connected_seconds,
+ listener_hash
+ FROM listener
+ WHERE station_id = :station_id
+ AND timestamp_end >= :start
+ AND timestamp_start <= :end
+ GROUP BY listener_hash
+ ) AS l
+ GROUP BY l.stream_id
+ SQL,
+ [
+ 'station_id' => $station->getIdRequired(),
+ 'start' => $dateRange->getStartTimestamp(),
+ 'end' => $dateRange->getEndTimestamp(),
+ ]
+ );
+
+ $streamLookup = [];
+ foreach ($this->mountRepo->getDisplayNames($station) as $id => $displayName) {
+ $streamLookup['local_' . $id] = $displayName;
+ }
+ foreach ($this->remoteRepo->getDisplayNames($station) as $id => $displayName) {
+ $streamLookup['remote_' . $id] = $displayName;
+ }
+
+ $listenersByStream = [];
+ $connectedTimeByStream = [];
+ $stats = [];
+
+ foreach ($statsRaw as $row) {
+ if (!isset($streamLookup[$row['stream_id']])) {
+ continue;
+ }
+
+ $row['stream'] = $streamLookup[$row['stream_id']];
+ $stats[] = $row;
+
+ $listenersByStream[$row['stream']] = $row['listeners'];
+ $connectedTimeByStream[$row['stream']] = $row['connected_seconds'];
+ }
+
+ return $response->withJson([
+ 'all' => $stats,
+ 'top_listeners' => $this->buildChart($listenersByStream, __('Listeners')),
+ 'top_connected_time' => $this->buildChart($connectedTimeByStream, __('Connected Seconds')),
+ ]);
+ }
+}
diff --git a/src/Controller/Stations/Reports/OverviewAction.php b/src/Controller/Stations/Reports/OverviewAction.php
index c3e75c19a..772d9cd5c 100644
--- a/src/Controller/Stations/Reports/OverviewAction.php
+++ b/src/Controller/Stations/Reports/OverviewAction.php
@@ -42,6 +42,7 @@ final class OverviewAction
'showFullAnalytics' => Entity\Enums\AnalyticsLevel::All === $analyticsLevel,
'listenersByTimePeriodUrl' => (string)$router->fromHere('api:stations:reports:overview-charts'),
'bestAndWorstUrl' => (string)$router->fromHere('api:stations:reports:best-and-worst'),
+ 'byStreamUrl' => (string)$router->fromHere('api:stations:reports:by-stream'),
'byBrowserUrl' => (string)$router->fromHere('api:stations:reports:by-browser'),
'byCountryUrl' => (string)$router->fromHere('api:stations:reports:by-country'),
]
diff --git a/tests/Functional/Api_Stations_ReportsCest.php b/tests/Functional/Api_Stations_ReportsCest.php
index 0bf2fa6ab..2c6f0628c 100644
--- a/tests/Functional/Api_Stations_ReportsCest.php
+++ b/tests/Functional/Api_Stations_ReportsCest.php
@@ -26,6 +26,9 @@ class Api_Stations_ReportsCest extends CestAbstract
$I->sendGet($uriBase . '/reports/overview/best-and-worst');
$I->seeResponseCodeIs(200);
+ $I->sendGet($uriBase . '/reports/overview/by-stream');
+ $I->seeResponseCodeIs(200);
+
$I->sendGet($uriBase . '/reports/overview/by-browser');
$I->seeResponseCodeIs(200);