aboutsummaryrefslogtreecommitdiffhomepage
path: root/client
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-05-06 14:23:02 +0200
committerChocobozzz <me@florianbigard.com>2022-05-06 14:23:02 +0200
commitf40712abbbb74e51f06037ef02757c42736bccf8 (patch)
tree4b130c6387f9687d52d570907eb1bbac6bc04b61 /client
parent49f0468d44468528c2fb2c8b0efd19cdaeeec43d (diff)
downloadPeerTube-f40712abbbb74e51f06037ef02757c42736bccf8.tar.gz
PeerTube-f40712abbbb74e51f06037ef02757c42736bccf8.tar.zst
PeerTube-f40712abbbb74e51f06037ef02757c42736bccf8.zip
Add ability to filter overall video stats by date
Diffstat (limited to 'client')
-rw-r--r--client/src/app/+stats/stats-routing.module.ts2
-rw-r--r--client/src/app/+stats/stats.module.ts4
-rw-r--r--client/src/app/+stats/video/video-stats.component.html78
-rw-r--r--client/src/app/+stats/video/video-stats.component.scss34
-rw-r--r--client/src/app/+stats/video/video-stats.component.ts212
-rw-r--r--client/src/app/+stats/video/video-stats.service.ts14
-rw-r--r--client/src/app/+video-studio/video-studio-routing.module.ts2
-rw-r--r--client/src/app/shared/shared-video-live/live-video.service.ts7
8 files changed, 272 insertions, 81 deletions
diff --git a/client/src/app/+stats/stats-routing.module.ts b/client/src/app/+stats/stats-routing.module.ts
index 59519a703..b6225cafd 100644
--- a/client/src/app/+stats/stats-routing.module.ts
+++ b/client/src/app/+stats/stats-routing.module.ts
@@ -1,11 +1,13 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core'
3import { VideoResolver } from '@app/shared/shared-main' 4import { VideoResolver } from '@app/shared/shared-main'
4import { VideoStatsComponent } from './video' 5import { VideoStatsComponent } from './video'
5 6
6const statsRoutes: Routes = [ 7const statsRoutes: Routes = [
7 { 8 {
8 path: 'videos/:videoId', 9 path: 'videos/:videoId',
10 canActivate: [ LoginGuard ],
9 component: VideoStatsComponent, 11 component: VideoStatsComponent,
10 data: { 12 data: {
11 meta: { 13 meta: {
diff --git a/client/src/app/+stats/stats.module.ts b/client/src/app/+stats/stats.module.ts
index 0497576e7..e81378220 100644
--- a/client/src/app/+stats/stats.module.ts
+++ b/client/src/app/+stats/stats.module.ts
@@ -1,7 +1,9 @@
1import { ChartModule } from 'primeng/chart' 1import { ChartModule } from 'primeng/chart'
2import { NgModule } from '@angular/core' 2import { NgModule } from '@angular/core'
3import { SharedFormModule } from '@app/shared/shared-forms'
3import { SharedGlobalIconModule } from '@app/shared/shared-icons' 4import { SharedGlobalIconModule } from '@app/shared/shared-icons'
4import { SharedMainModule } from '@app/shared/shared-main' 5import { SharedMainModule } from '@app/shared/shared-main'
6import { SharedVideoLiveModule } from '@app/shared/shared-video-live'
5import { StatsRoutingModule } from './stats-routing.module' 7import { StatsRoutingModule } from './stats-routing.module'
6import { VideoStatsComponent, VideoStatsService } from './video' 8import { VideoStatsComponent, VideoStatsService } from './video'
7 9
@@ -10,7 +12,9 @@ import { VideoStatsComponent, VideoStatsService } from './video'
10 StatsRoutingModule, 12 StatsRoutingModule,
11 13
12 SharedMainModule, 14 SharedMainModule,
15 SharedFormModule,
13 SharedGlobalIconModule, 16 SharedGlobalIconModule,
17 SharedVideoLiveModule,
14 18
15 ChartModule 19 ChartModule
16 ], 20 ],
diff --git a/client/src/app/+stats/video/video-stats.component.html b/client/src/app/+stats/video/video-stats.component.html
index 400c049eb..e5412f1b8 100644
--- a/client/src/app/+stats/video/video-stats.component.html
+++ b/client/src/app/+stats/video/video-stats.component.html
@@ -1,9 +1,9 @@
1<div class="margin-content"> 1<div class="margin-content">
2 <h1 class="title-page title-page-single" i18n>Stats for {{ video.name }}</h1> 2 <h1 class="title-page title-page-single" i18n>{{ video.name }}</h1>
3 3
4 <div class="overall-stats-embed"> 4 <div class="stats-embed">
5 <div class="overall-stats"> 5 <div class="global-stats">
6 <div *ngFor="let card of overallStatCards" class="card overall-stats-card"> 6 <div *ngFor="let card of globalStatsCards" class="card stats-card">
7 <div class="label">{{ card.label }}</div> 7 <div class="label">{{ card.label }}</div>
8 <div class="value">{{ card.value }}</div> 8 <div class="value">{{ card.value }}</div>
9 <div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div> 9 <div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
@@ -13,33 +13,51 @@
13 <my-embed [video]="video"></my-embed> 13 <my-embed [video]="video"></my-embed>
14 </div> 14 </div>
15 15
16 <div class="timeserie"> 16 <div class="stats-with-date">
17 <div ngbNav #nav="ngbNav" [activeId]="activeGraphId" (activeIdChange)="onChartChange($event)" class="nav-tabs"> 17 <div class="overall-stats">
18 18 <div class="date-filter-wrapper">
19 <ng-container *ngFor="let availableChart of availableCharts" [ngbNavItem]="availableChart.id"> 19 <h2>{{ getViewersStatsTitle() }}</h2>
20 <a ngbNavLink i18n> 20
21 <span>{{ availableChart.label }}</span> 21 <my-select-options [(ngModel)]="currentDateFilter" (ngModelChange)="onDateFilterChange()" [items]="dateFilters"></my-select-options>
22 </a> 22 </div>
23 23
24 <ng-template ngbNavContent> 24 <div class="cards">
25 <div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }"> 25 <div *ngFor="let card of overallStatCards" class="card stats-card">
26 <p-chart 26 <div class="label">{{ card.label }}</div>
27 *ngIf="chartOptions[availableChart.id]" 27 <div class="value">{{ card.value }}</div>
28 [height]="chartHeight" [width]="chartWidth" 28 <div *ngIf="card.moreInfo" class="more-info">{{ card.moreInfo }}</div>
29 [type]="chartOptions[availableChart.id].type" [options]="chartOptions[availableChart.id].options" [data]="chartOptions[availableChart.id].data" 29 </div>
30 [plugins]="chartPlugins" 30 </div>
31 ></p-chart>
32 </div>
33
34 <div class="zoom-container">
35 <span *ngIf="!hasZoom() && availableChart.zoomEnabled" i18n class="description">You can select a part of the graph to zoom in</span>
36
37 <my-button i18n *ngIf="hasZoom()" (click)="resetZoom()">Reset zoom</my-button>
38 </div>
39 </ng-template>
40 </ng-container>
41 </div> 31 </div>
42 32
43 <div [ngbNavOutlet]="nav"></div> 33 <div class="timeserie">
34 <div ngbNav #nav="ngbNav" [activeId]="activeGraphId" (activeIdChange)="onChartChange($event)" class="nav-tabs">
35
36 <ng-container *ngFor="let availableChart of availableCharts" [ngbNavItem]="availableChart.id">
37 <a ngbNavLink i18n>
38 <span>{{ availableChart.label }}</span>
39 </a>
40
41 <ng-template ngbNavContent>
42 <div class="chart-container" [ngStyle]="{ 'min-height': chartHeight }">
43 <p-chart
44 *ngIf="chartOptions[availableChart.id]"
45 [height]="chartHeight" [width]="chartWidth"
46 [type]="chartOptions[availableChart.id].type" [options]="chartOptions[availableChart.id].options" [data]="chartOptions[availableChart.id].data"
47 [plugins]="chartPlugins"
48 ></p-chart>
49 </div>
50
51 <div class="zoom-container">
52 <span *ngIf="!hasZoom() && availableChart.zoomEnabled" i18n class="description">You can select a part of the graph to zoom in</span>
53
54 <my-button i18n *ngIf="hasZoom()" (click)="resetZoom()">Reset zoom</my-button>
55 </div>
56 </ng-template>
57 </ng-container>
58 </div>
59
60 <div [ngbNavOutlet]="nav"></div>
61 </div>
44 </div> 62 </div>
45</div> 63</div>
diff --git a/client/src/app/+stats/video/video-stats.component.scss b/client/src/app/+stats/video/video-stats.component.scss
index e2a74152f..e4c2d899f 100644
--- a/client/src/app/+stats/video/video-stats.component.scss
+++ b/client/src/app/+stats/video/video-stats.component.scss
@@ -2,17 +2,31 @@
2@use '_mixins' as *; 2@use '_mixins' as *;
3@use '_nav' as *; 3@use '_nav' as *;
4 4
5.overall-stats-embed { 5.stats-embed {
6 display: flex; 6 display: flex;
7 justify-content: space-between; 7 justify-content: space-between;
8} 8}
9 9
10.overall-stats { 10.overall-stats,
11.global-stats {
11 display: flex; 12 display: flex;
12 flex-wrap: wrap; 13 flex-wrap: wrap;
14
15 h2 {
16 font-size: 16px;
17 width: 100%;
18 }
19}
20
21.overall-stats {
22 justify-content: space-between;
23
24 .cards {
25 display: flex;
26 }
13} 27}
14 28
15.overall-stats-card { 29.stats-card {
16 display: flex; 30 display: flex;
17 justify-content: center; 31 justify-content: center;
18 align-items: center; 32 align-items: center;
@@ -28,12 +42,6 @@
28 font-size: 14px; 42 font-size: 14px;
29 } 43 }
30 44
31 .label {
32 color: pvar(--greyForegroundColor);
33 font-weight: $font-semibold;
34 opacity: 0.8;
35 }
36
37 .value { 45 .value {
38 font-size: 24px; 46 font-size: 24px;
39 font-weight: $font-semibold; 47 font-weight: $font-semibold;
@@ -52,6 +60,12 @@ my-embed {
52 width: 100%; 60 width: 100%;
53} 61}
54 62
63.stats-with-date {
64 margin-top: 30px;
65 padding-top: 30px;
66 border-top: 1px solid $separator-border-color;
67}
68
55@include on-small-main-col { 69@include on-small-main-col {
56 my-embed { 70 my-embed {
57 display: none; 71 display: none;
@@ -59,7 +73,7 @@ my-embed {
59} 73}
60 74
61.tab-content { 75.tab-content {
62 margin-top: 15px; 76 margin-top: 5px;
63} 77}
64 78
65.nav-tabs { 79.nav-tabs {
diff --git a/client/src/app/+stats/video/video-stats.component.ts b/client/src/app/+stats/video/video-stats.component.ts
index a5435fe23..f433259ef 100644
--- a/client/src/app/+stats/video/video-stats.component.ts
+++ b/client/src/app/+stats/video/video-stats.component.ts
@@ -1,12 +1,21 @@
1import { ChartConfiguration, ChartData, PluginOptionsByType, Scale, TooltipItem } from 'chart.js' 1import { ChartConfiguration, ChartData, PluginOptionsByType, Scale, TooltipItem } from 'chart.js'
2import zoomPlugin from 'chartjs-plugin-zoom' 2import zoomPlugin from 'chartjs-plugin-zoom'
3import { Observable, of } from 'rxjs' 3import { Observable, of } from 'rxjs'
4import { SelectOptionsItem } from 'src/types'
4import { Component, OnInit } from '@angular/core' 5import { Component, OnInit } from '@angular/core'
5import { ActivatedRoute } from '@angular/router' 6import { ActivatedRoute } from '@angular/router'
6import { Notifier, PeerTubeRouterService } from '@app/core' 7import { Notifier, PeerTubeRouterService } from '@app/core'
7import { NumberFormatterPipe, VideoDetails } from '@app/shared/shared-main' 8import { NumberFormatterPipe, VideoDetails } from '@app/shared/shared-main'
9import { LiveVideoService } from '@app/shared/shared-video-live'
8import { secondsToTime } from '@shared/core-utils' 10import { secondsToTime } from '@shared/core-utils'
9import { VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models/videos' 11import { HttpStatusCode } from '@shared/models/http'
12import {
13 LiveVideoSession,
14 VideoStatsOverall,
15 VideoStatsRetention,
16 VideoStatsTimeserie,
17 VideoStatsTimeserieMetric
18} from '@shared/models/videos'
10import { VideoStatsService } from './video-stats.service' 19import { VideoStatsService } from './video-stats.service'
11 20
12type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries' 21type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries'
@@ -21,41 +30,24 @@ type ChartBuilderResult = {
21 displayLegend: boolean 30 displayLegend: boolean
22} 31}
23 32
33type Card = { label: string, value: string | number, moreInfo?: string }
34
24@Component({ 35@Component({
25 templateUrl: './video-stats.component.html', 36 templateUrl: './video-stats.component.html',
26 styleUrls: [ './video-stats.component.scss' ], 37 styleUrls: [ './video-stats.component.scss' ],
27 providers: [ NumberFormatterPipe ] 38 providers: [ NumberFormatterPipe ]
28}) 39})
29export class VideoStatsComponent implements OnInit { 40export class VideoStatsComponent implements OnInit {
30 overallStatCards: { label: string, value: string | number, moreInfo?: string }[] = [] 41 // Cannot handle date filters
42 globalStatsCards: Card[] = []
43 // Can handle date filters
44 overallStatCards: Card[] = []
31 45
32 chartOptions: { [ id in ActiveGraphId ]?: ChartConfiguration<'line' | 'bar'> } = {} 46 chartOptions: { [ id in ActiveGraphId ]?: ChartConfiguration<'line' | 'bar'> } = {}
33 chartHeight = '300px' 47 chartHeight = '300px'
34 chartWidth: string = null 48 chartWidth: string = null
35 49
36 availableCharts = [ 50 availableCharts: { id: string, label: string, zoomEnabled: boolean }[] = []
37 {
38 id: 'viewers',
39 label: $localize`Viewers`,
40 zoomEnabled: true
41 },
42 {
43 id: 'aggregateWatchTime',
44 label: $localize`Watch time`,
45 zoomEnabled: true
46 },
47 {
48 id: 'retention',
49 label: $localize`Retention`,
50 zoomEnabled: false
51 },
52 {
53 id: 'countries',
54 label: $localize`Countries`,
55 zoomEnabled: false
56 }
57 ]
58
59 activeGraphId: ActiveGraphId = 'viewers' 51 activeGraphId: ActiveGraphId = 'viewers'
60 52
61 video: VideoDetails 53 video: VideoDetails
@@ -64,8 +56,16 @@ export class VideoStatsComponent implements OnInit {
64 56
65 chartPlugins = [ zoomPlugin ] 57 chartPlugins = [ zoomPlugin ]
66 58
67 private timeseriesStartDate: Date 59 currentDateFilter = 'all'
68 private timeseriesEndDate: Date 60 dateFilters: SelectOptionsItem[] = [
61 {
62 id: 'all',
63 label: $localize`Since the video publication`
64 }
65 ]
66
67 private statsStartDate: Date
68 private statsEndDate: Date
69 69
70 private chartIngestData: { [ id in ActiveGraphId ]?: ChartIngestData } = {} 70 private chartIngestData: { [ id in ActiveGraphId ]?: ChartIngestData } = {}
71 71
@@ -74,25 +74,58 @@ export class VideoStatsComponent implements OnInit {
74 private notifier: Notifier, 74 private notifier: Notifier,
75 private statsService: VideoStatsService, 75 private statsService: VideoStatsService,
76 private peertubeRouter: PeerTubeRouterService, 76 private peertubeRouter: PeerTubeRouterService,
77 private numberFormatter: NumberFormatterPipe 77 private numberFormatter: NumberFormatterPipe,
78 private liveService: LiveVideoService
78 ) {} 79 ) {}
79 80
80 ngOnInit () { 81 ngOnInit () {
81 this.video = this.route.snapshot.data.video 82 this.video = this.route.snapshot.data.video
82 83
84 this.availableCharts = [
85 {
86 id: 'viewers',
87 label: $localize`Viewers`,
88 zoomEnabled: true
89 },
90 {
91 id: 'aggregateWatchTime',
92 label: $localize`Watch time`,
93 zoomEnabled: true
94 },
95 {
96 id: 'countries',
97 label: $localize`Countries`,
98 zoomEnabled: false
99 }
100 ]
101
102 if (!this.video.isLive) {
103 this.availableCharts.push({
104 id: 'retention',
105 label: $localize`Retention`,
106 zoomEnabled: false
107 })
108 }
109
110 const snapshotQuery = this.route.snapshot.queryParams
111 if (snapshotQuery.startDate || snapshotQuery.endDate) {
112 this.addAndSelectCustomDateFilter()
113 }
114
83 this.route.queryParams.subscribe(params => { 115 this.route.queryParams.subscribe(params => {
84 this.timeseriesStartDate = params.startDate 116 this.statsStartDate = params.startDate
85 ? new Date(params.startDate) 117 ? new Date(params.startDate)
86 : undefined 118 : undefined
87 119
88 this.timeseriesEndDate = params.endDate 120 this.statsEndDate = params.endDate
89 ? new Date(params.endDate) 121 ? new Date(params.endDate)
90 : undefined 122 : undefined
91 123
92 this.loadChart() 124 this.loadChart()
125 this.loadOverallStats()
93 }) 126 })
94 127
95 this.loadOverallStats() 128 this.loadDateFilters()
96 } 129 }
97 130
98 hasCountries () { 131 hasCountries () {
@@ -107,10 +140,30 @@ export class VideoStatsComponent implements OnInit {
107 140
108 resetZoom () { 141 resetZoom () {
109 this.peertubeRouter.silentNavigate([], {}) 142 this.peertubeRouter.silentNavigate([], {})
143 this.removeAndResetCustomDateFilter()
110 } 144 }
111 145
112 hasZoom () { 146 hasZoom () {
113 return !!this.timeseriesStartDate && this.isTimeserieGraph(this.activeGraphId) 147 return !!this.statsStartDate && this.isTimeserieGraph(this.activeGraphId)
148 }
149
150 getViewersStatsTitle () {
151 if (this.statsStartDate && this.statsEndDate) {
152 return $localize`Viewers stats between ${this.statsStartDate.toLocaleString()} and ${this.statsEndDate.toLocaleString()}`
153 }
154
155 return $localize`Viewers stats`
156 }
157
158 onDateFilterChange () {
159 if (this.currentDateFilter === 'all') {
160 return this.resetZoom()
161 }
162
163 const idParts = this.currentDateFilter.split('|')
164 if (idParts.length === 2) {
165 return this.peertubeRouter.silentNavigate([], { startDate: idParts[0], endDate: idParts[1] })
166 }
114 } 167 }
115 168
116 private isTimeserieGraph (graphId: ActiveGraphId) { 169 private isTimeserieGraph (graphId: ActiveGraphId) {
@@ -118,7 +171,7 @@ export class VideoStatsComponent implements OnInit {
118 } 171 }
119 172
120 private loadOverallStats () { 173 private loadOverallStats () {
121 this.statsService.getOverallStats(this.video.uuid) 174 this.statsService.getOverallStats({ videoId: this.video.uuid, startDate: this.statsStartDate, endDate: this.statsEndDate })
122 .subscribe({ 175 .subscribe({
123 next: res => { 176 next: res => {
124 this.countries = res.countries.slice(0, 10).map(c => ({ 177 this.countries = res.countries.slice(0, 10).map(c => ({
@@ -133,8 +186,70 @@ export class VideoStatsComponent implements OnInit {
133 }) 186 })
134 } 187 }
135 188
189 private loadDateFilters () {
190 if (this.video.isLive) return this.loadLiveDateFilters()
191
192 return this.loadVODDateFilters()
193 }
194
195 private loadLiveDateFilters () {
196 this.liveService.listSessions(this.video.id)
197 .subscribe({
198 next: ({ data }) => {
199 const newFilters = data.map(session => this.buildLiveFilter(session))
200
201 this.dateFilters = this.dateFilters.concat(newFilters)
202 },
203
204 error: err => this.notifier.error(err.message)
205 })
206 }
207
208 private loadVODDateFilters () {
209 this.liveService.findLiveSessionFromVOD(this.video.id)
210 .subscribe({
211 next: session => {
212 this.dateFilters = this.dateFilters.concat([ this.buildLiveFilter(session) ])
213 },
214
215 error: err => {
216 if (err.status === HttpStatusCode.NOT_FOUND_404) return
217
218 this.notifier.error(err.message)
219 }
220 })
221 }
222
223 private buildLiveFilter (session: LiveVideoSession) {
224 return {
225 id: session.startDate + '|' + session.endDate,
226 label: $localize`Of live of ${new Date(session.startDate).toLocaleString()}`
227 }
228 }
229
230 private addAndSelectCustomDateFilter () {
231 const exists = this.dateFilters.some(d => d.id === 'custom')
232
233 if (!exists) {
234 this.dateFilters = this.dateFilters.concat([
235 {
236 id: 'custom',
237 label: $localize`Custom dates`
238 }
239 ])
240 }
241
242 this.currentDateFilter = 'custom'
243 }
244
245 private removeAndResetCustomDateFilter () {
246 this.dateFilters = this.dateFilters.filter(d => d.id !== 'custom')
247
248 this.currentDateFilter = 'all'
249 }
250
136 private buildOverallStatCard (overallStats: VideoStatsOverall) { 251 private buildOverallStatCard (overallStats: VideoStatsOverall) {
137 this.overallStatCards = [ 252 this.globalStatsCards = [
138 { 253 {
139 label: $localize`Views`, 254 label: $localize`Views`,
140 value: this.numberFormatter.transform(this.video.views) 255 value: this.numberFormatter.transform(this.video.views)
@@ -142,12 +257,19 @@ export class VideoStatsComponent implements OnInit {
142 { 257 {
143 label: $localize`Likes`, 258 label: $localize`Likes`,
144 value: this.numberFormatter.transform(this.video.likes) 259 value: this.numberFormatter.transform(this.video.likes)
145 }, 260 }
261 ]
262
263 this.overallStatCards = [
146 { 264 {
147 label: $localize`Average watch time`, 265 label: $localize`Average watch time`,
148 value: secondsToTime(overallStats.averageWatchTime) 266 value: secondsToTime(overallStats.averageWatchTime)
149 }, 267 },
150 { 268 {
269 label: $localize`Total watch time`,
270 value: secondsToTime(overallStats.totalWatchTime)
271 },
272 {
151 label: $localize`Peak viewers`, 273 label: $localize`Peak viewers`,
152 value: this.numberFormatter.transform(overallStats.viewersPeak), 274 value: this.numberFormatter.transform(overallStats.viewersPeak),
153 moreInfo: overallStats.viewersPeak !== 0 275 moreInfo: overallStats.viewersPeak !== 0
@@ -155,6 +277,13 @@ export class VideoStatsComponent implements OnInit {
155 : undefined 277 : undefined
156 } 278 }
157 ] 279 ]
280
281 if (overallStats.countries.length !== 0) {
282 this.overallStatCards.push({
283 label: $localize`Countries`,
284 value: this.numberFormatter.transform(overallStats.countries.length)
285 })
286 }
158 } 287 }
159 288
160 private loadChart () { 289 private loadChart () {
@@ -163,14 +292,14 @@ export class VideoStatsComponent implements OnInit {
163 292
164 aggregateWatchTime: this.statsService.getTimeserieStats({ 293 aggregateWatchTime: this.statsService.getTimeserieStats({
165 videoId: this.video.uuid, 294 videoId: this.video.uuid,
166 startDate: this.timeseriesStartDate, 295 startDate: this.statsStartDate,
167 endDate: this.timeseriesEndDate, 296 endDate: this.statsEndDate,
168 metric: 'aggregateWatchTime' 297 metric: 'aggregateWatchTime'
169 }), 298 }),
170 viewers: this.statsService.getTimeserieStats({ 299 viewers: this.statsService.getTimeserieStats({
171 videoId: this.video.uuid, 300 videoId: this.video.uuid,
172 startDate: this.timeseriesStartDate, 301 startDate: this.statsStartDate,
173 endDate: this.timeseriesEndDate, 302 endDate: this.statsEndDate,
174 metric: 'viewers' 303 metric: 'viewers'
175 }), 304 }),
176 305
@@ -317,6 +446,7 @@ export class VideoStatsComponent implements OnInit {
317 const endDate = this.buildZoomEndDate(rawData.groupInterval, rawData.data[max].date) 446 const endDate = this.buildZoomEndDate(rawData.groupInterval, rawData.data[max].date)
318 447
319 this.peertubeRouter.silentNavigate([], { startDate, endDate }) 448 this.peertubeRouter.silentNavigate([], { startDate, endDate })
449 this.addAndSelectCustomDateFilter()
320 } 450 }
321 } 451 }
322 } 452 }
@@ -386,6 +516,10 @@ export class VideoStatsComponent implements OnInit {
386 516
387 const date = new Date(label) 517 const date = new Date(label)
388 518
519 if (data.groupInterval.match(/ month?$/)) {
520 return date.toLocaleDateString([], { month: 'numeric' })
521 }
522
389 if (data.groupInterval.match(/ days?$/)) { 523 if (data.groupInterval.match(/ days?$/)) {
390 return date.toLocaleDateString([], { month: 'numeric', day: 'numeric' }) 524 return date.toLocaleDateString([], { month: 'numeric', day: 'numeric' })
391 } 525 }
diff --git a/client/src/app/+stats/video/video-stats.service.ts b/client/src/app/+stats/video/video-stats.service.ts
index 712d03971..e019c87f7 100644
--- a/client/src/app/+stats/video/video-stats.service.ts
+++ b/client/src/app/+stats/video/video-stats.service.ts
@@ -17,8 +17,18 @@ export class VideoStatsService {
17 private restExtractor: RestExtractor 17 private restExtractor: RestExtractor
18 ) { } 18 ) { }
19 19
20 getOverallStats (videoId: string) { 20 getOverallStats (options: {
21 return this.authHttp.get<VideoStatsOverall>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/stats/overall') 21 videoId: string
22 startDate?: Date
23 endDate?: Date
24 }) {
25 const { videoId, startDate, endDate } = options
26
27 let params = new HttpParams()
28 if (startDate) params = params.append('startDate', startDate.toISOString())
29 if (endDate) params = params.append('endDate', endDate.toISOString())
30
31 return this.authHttp.get<VideoStatsOverall>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/stats/overall', { params })
22 .pipe(catchError(err => this.restExtractor.handleError(err))) 32 .pipe(catchError(err => this.restExtractor.handleError(err)))
23 } 33 }
24 34
diff --git a/client/src/app/+video-studio/video-studio-routing.module.ts b/client/src/app/+video-studio/video-studio-routing.module.ts
index 4c08631a1..9d276be7c 100644
--- a/client/src/app/+video-studio/video-studio-routing.module.ts
+++ b/client/src/app/+video-studio/video-studio-routing.module.ts
@@ -1,11 +1,13 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core'
3import { VideoResolver } from '@app/shared/shared-main' 4import { VideoResolver } from '@app/shared/shared-main'
4import { VideoStudioEditComponent } from './edit' 5import { VideoStudioEditComponent } from './edit'
5 6
6const videoStudioRoutes: Routes = [ 7const videoStudioRoutes: Routes = [
7 { 8 {
8 path: '', 9 path: '',
10 canActivateChild: [ LoginGuard ],
9 children: [ 11 children: [
10 { 12 {
11 path: 'edit/:videoId', 13 path: 'edit/:videoId',
diff --git a/client/src/app/shared/shared-video-live/live-video.service.ts b/client/src/app/shared/shared-video-live/live-video.service.ts
index 11b9dd739..89bfd84a0 100644
--- a/client/src/app/shared/shared-video-live/live-video.service.ts
+++ b/client/src/app/shared/shared-video-live/live-video.service.ts
@@ -4,6 +4,7 @@ import { Injectable } from '@angular/core'
4import { RestExtractor } from '@app/core' 4import { RestExtractor } from '@app/core'
5import { LiveVideo, LiveVideoCreate, LiveVideoSession, LiveVideoUpdate, ResultList, VideoCreateResult } from '@shared/models' 5import { LiveVideo, LiveVideoCreate, LiveVideoSession, LiveVideoUpdate, ResultList, VideoCreateResult } from '@shared/models'
6import { environment } from '../../../environments/environment' 6import { environment } from '../../../environments/environment'
7import { VideoService } from '../shared-main'
7 8
8@Injectable() 9@Injectable()
9export class LiveVideoService { 10export class LiveVideoService {
@@ -32,6 +33,12 @@ export class LiveVideoService {
32 .pipe(catchError(err => this.restExtractor.handleError(err))) 33 .pipe(catchError(err => this.restExtractor.handleError(err)))
33 } 34 }
34 35
36 findLiveSessionFromVOD (videoId: number | string) {
37 return this.authHttp
38 .get<LiveVideoSession>(VideoService.BASE_VIDEO_URL + '/' + videoId + '/live-session')
39 .pipe(catchError(err => this.restExtractor.handleError(err)))
40 }
41
35 updateLive (videoId: number | string, liveUpdate: LiveVideoUpdate) { 42 updateLive (videoId: number | string, liveUpdate: LiveVideoUpdate) {
36 return this.authHttp 43 return this.authHttp
37 .put(LiveVideoService.BASE_VIDEO_LIVE_URL + videoId, liveUpdate) 44 .put(LiveVideoService.BASE_VIDEO_LIVE_URL + videoId, liveUpdate)