Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
Framasoft
PeerTube
PeerTube
Commits
e9fdb628
Verified
Commit
e9fdb628
authored
Apr 08, 2022
by
Chocobozzz
Browse files
Support interactive video stats graph
parent
bfaf6ba2
Changes
12
Hide whitespace changes
Inline
Side-by-side
client/package.json
View file @
e9fdb628
...
...
@@ -83,6 +83,7 @@
"buffer"
:
"^6.0.3"
,
"cache-chunk-store"
:
"^3.0.0"
,
"chart.js"
:
"^3.5.1"
,
"chartjs-plugin-zoom"
:
"^1.2.1"
,
"chromedriver"
:
"^99.0.0"
,
"core-js"
:
"^3.1.4"
,
"css-loader"
:
"^6.2.0"
,
...
...
client/src/app/+stats/video/video-stats.component.html
View file @
e9fdb628
...
...
@@ -22,13 +22,20 @@
</a>
<ng-template
ngbNavContent
>
<div
[ngStyle]=
"{ 'min-height': chartHeight }"
>
<div
class=
"chart-container"
[ngStyle]=
"{ 'min-height': chartHeight }"
>
<p-chart
*ngIf=
"chartOptions[availableChart.id]"
[height]=
"chartHeight"
[width]=
"chartWidth"
[type]=
"chartOptions[availableChart.id].type"
[options]=
"chartOptions[availableChart.id].options"
[data]=
"chartOptions[availableChart.id].data"
[plugins]=
"chartPlugins"
></p-chart>
</div>
<div
class=
"zoom-container"
>
<span
*ngIf=
"!hasZoom() && availableChart.zoomEnabled"
i18n
class=
"description"
>
You can select a part of the graph to zoom in
</span>
<my-button
i18n
*ngIf=
"hasZoom()"
(click)=
"resetZoom()"
>
Reset zoom
</my-button>
</div>
</ng-template>
</ng-container>
</div>
...
...
client/src/app/+stats/video/video-stats.component.scss
View file @
e9fdb628
...
...
@@ -21,6 +21,7 @@
min-width
:
200px
;
margin-right
:
15px
;
background-color
:
pvar
(
--
submenuBackgroundColor
);
margin-bottom
:
15px
;
.label
,
.more-info
{
...
...
@@ -37,6 +38,12 @@
font-size
:
24px
;
font-weight
:
$font-semibold
;
}
@media
screen
and
(
max-width
:
$mobile-view
)
{
min-height
:
fit-content
;
min-width
:
fit-content
;
padding
:
15px
;
}
}
my-embed
{
...
...
@@ -45,6 +52,12 @@ my-embed {
width
:
100%
;
}
@include
on-small-main-col
{
my-embed
{
display
:
none
;
}
}
.tab-content
{
margin-top
:
15px
;
}
...
...
@@ -52,3 +65,16 @@ my-embed {
.nav-tabs
{
@include
peertube-nav-tabs
(
$border-width
:
2px
);
}
.chart-container
{
margin-bottom
:
10px
;
}
.zoom-container
{
display
:
flex
;
justify-content
:
center
;
.description
{
font-style
:
italic
;
}
}
client/src/app/+stats/video/video-stats.component.ts
View file @
e9fdb628
import
{
ChartConfiguration
,
ChartData
}
from
'
chart.js
'
import
{
ChartConfiguration
,
ChartData
,
PluginOptionsByType
,
Scale
,
TooltipItem
}
from
'
chart.js
'
import
zoomPlugin
from
'
chartjs-plugin-zoom
'
import
{
Observable
,
of
}
from
'
rxjs
'
import
{
Component
,
OnInit
}
from
'
@angular/core
'
import
{
ActivatedRoute
}
from
'
@angular/router
'
import
{
Notifier
}
from
'
@app/core
'
import
{
Notifier
,
PeerTubeRouterService
}
from
'
@app/core
'
import
{
NumberFormatterPipe
,
VideoDetails
}
from
'
@app/shared/shared-main
'
import
{
secondsToTime
}
from
'
@shared/core-utils
'
import
{
VideoStatsOverall
,
VideoStatsRetention
,
VideoStatsTimeserie
,
VideoStatsTimeserieMetric
}
from
'
@shared/models/videos
'
...
...
@@ -15,6 +16,7 @@ type CountryData = { name: string, viewers: number }[]
type
ChartIngestData
=
VideoStatsTimeserie
|
VideoStatsRetention
|
CountryData
type
ChartBuilderResult
=
{
type
:
'
line
'
|
'
bar
'
plugins
:
Partial
<
PluginOptionsByType
<
'
line
'
|
'
bar
'
>>
data
:
ChartData
<
'
line
'
|
'
bar
'
>
displayLegend
:
boolean
}
...
...
@@ -34,19 +36,23 @@ export class VideoStatsComponent implements OnInit {
availableCharts
=
[
{
id
:
'
viewers
'
,
label
:
$localize
`Viewers`
label
:
$localize
`Viewers`
,
zoomEnabled
:
true
},
{
id
:
'
aggregateWatchTime
'
,
label
:
$localize
`Watch time`
label
:
$localize
`Watch time`
,
zoomEnabled
:
true
},
{
id
:
'
retention
'
,
label
:
$localize
`Retention`
label
:
$localize
`Retention`
,
zoomEnabled
:
false
},
{
id
:
'
countries
'
,
label
:
$localize
`Countries`
label
:
$localize
`Countries`
,
zoomEnabled
:
false
}
]
...
...
@@ -56,18 +62,37 @@ export class VideoStatsComponent implements OnInit {
countries
:
CountryData
=
[]
chartPlugins
=
[
zoomPlugin
]
private
timeseriesStartDate
:
Date
private
timeseriesEndDate
:
Date
private
chartIngestData
:
{
[
id
in
ActiveGraphId
]?:
ChartIngestData
}
=
{}
constructor
(
private
route
:
ActivatedRoute
,
private
notifier
:
Notifier
,
private
statsService
:
VideoStatsService
,
private
peertubeRouter
:
PeerTubeRouterService
,
private
numberFormatter
:
NumberFormatterPipe
)
{}
ngOnInit
()
{
this
.
video
=
this
.
route
.
snapshot
.
data
.
video
this
.
route
.
queryParams
.
subscribe
(
params
=>
{
this
.
timeseriesStartDate
=
params
.
startDate
?
new
Date
(
params
.
startDate
)
:
undefined
this
.
timeseriesEndDate
=
params
.
endDate
?
new
Date
(
params
.
endDate
)
:
undefined
this
.
loadChart
()
})
this
.
loadOverallStats
()
this
.
loadChart
()
}
hasCountries
()
{
...
...
@@ -80,6 +105,18 @@ export class VideoStatsComponent implements OnInit {
this
.
loadChart
()
}
resetZoom
()
{
this
.
peertubeRouter
.
silentNavigate
([],
{})
}
hasZoom
()
{
return
!!
this
.
timeseriesStartDate
&&
this
.
isTimeserieGraph
(
this
.
activeGraphId
)
}
private
isTimeserieGraph
(
graphId
:
ActiveGraphId
)
{
return
graphId
===
'
aggregateWatchTime
'
||
graphId
===
'
viewers
'
}
private
loadOverallStats
()
{
this
.
statsService
.
getOverallStats
(
this
.
video
.
uuid
)
.
subscribe
({
...
...
@@ -125,24 +162,35 @@ export class VideoStatsComponent implements OnInit {
private
loadChart
()
{
const
obsBuilders
:
{
[
id
in
ActiveGraphId
]:
Observable
<
ChartIngestData
>
}
=
{
retention
:
this
.
statsService
.
getRetentionStats
(
this
.
video
.
uuid
),
aggregateWatchTime
:
this
.
statsService
.
getTimeserieStats
(
this
.
video
.
uuid
,
'
aggregateWatchTime
'
),
viewers
:
this
.
statsService
.
getTimeserieStats
(
this
.
video
.
uuid
,
'
viewers
'
),
aggregateWatchTime
:
this
.
statsService
.
getTimeserieStats
({
videoId
:
this
.
video
.
uuid
,
startDate
:
this
.
timeseriesStartDate
,
endDate
:
this
.
timeseriesEndDate
,
metric
:
'
aggregateWatchTime
'
}),
viewers
:
this
.
statsService
.
getTimeserieStats
({
videoId
:
this
.
video
.
uuid
,
startDate
:
this
.
timeseriesStartDate
,
endDate
:
this
.
timeseriesEndDate
,
metric
:
'
viewers
'
}),
countries
:
of
(
this
.
countries
)
}
obsBuilders
[
this
.
activeGraphId
].
subscribe
({
next
:
res
=>
{
this
.
chartOptions
[
this
.
activeGraphId
]
=
this
.
buildChartOptions
(
this
.
activeGraphId
,
res
)
this
.
chartIngestData
[
this
.
activeGraphId
]
=
res
this
.
chartOptions
[
this
.
activeGraphId
]
=
this
.
buildChartOptions
(
this
.
activeGraphId
)
},
error
:
err
=>
this
.
notifier
.
error
(
err
.
message
)
})
}
private
buildChartOptions
(
graphId
:
ActiveGraphId
,
rawData
:
ChartIngestData
):
ChartConfiguration
<
'
line
'
|
'
bar
'
>
{
private
buildChartOptions
(
graphId
:
ActiveGraphId
):
ChartConfiguration
<
'
line
'
|
'
bar
'
>
{
const
dataBuilders
:
{
[
id
in
ActiveGraphId
]:
(
rawData
:
ChartIngestData
)
=>
ChartBuilderResult
}
=
{
...
...
@@ -152,7 +200,9 @@ export class VideoStatsComponent implements OnInit {
countries
:
(
rawData
:
CountryData
)
=>
this
.
buildCountryChartOptions
(
rawData
)
}
const
{
type
,
data
,
displayLegend
}
=
dataBuilders
[
graphId
](
rawData
)
const
{
type
,
data
,
displayLegend
,
plugins
}
=
dataBuilders
[
graphId
](
this
.
chartIngestData
[
graphId
])
const
self
=
this
return
{
type
,
...
...
@@ -162,6 +212,19 @@ export class VideoStatsComponent implements OnInit {
responsive
:
true
,
scales
:
{
x
:
{
ticks
:
{
callback
:
function
(
value
)
{
return
self
.
formatXTick
({
graphId
,
value
,
data
:
self
.
chartIngestData
[
graphId
]
as
VideoStatsTimeserie
,
scale
:
this
})
}
}
},
y
:
{
beginAtZero
:
true
,
...
...
@@ -170,7 +233,7 @@ export class VideoStatsComponent implements OnInit {
:
undefined
,
ticks
:
{
callback
:
value
=>
this
.
formatTick
(
graphId
,
value
)
callback
:
value
=>
this
.
format
Y
Tick
(
{
graphId
,
value
}
)
}
}
},
...
...
@@ -181,15 +244,18 @@ export class VideoStatsComponent implements OnInit {
},
tooltip
:
{
callbacks
:
{
label
:
value
=>
this
.
formatTick
(
graphId
,
value
.
raw
as
number
|
string
)
title
:
items
=>
this
.
formatTooltipTitle
({
graphId
,
items
}),
label
:
value
=>
this
.
formatYTick
({
graphId
,
value
:
value
.
raw
as
number
|
string
})
}
}
},
...
plugins
}
}
}
}
private
buildRetentionChartOptions
(
rawData
:
VideoStatsRetention
)
{
private
buildRetentionChartOptions
(
rawData
:
VideoStatsRetention
)
:
ChartBuilderResult
{
const
labels
:
string
[]
=
[]
const
data
:
number
[]
=
[]
...
...
@@ -203,6 +269,10 @@ export class VideoStatsComponent implements OnInit {
displayLegend
:
false
,
plugins
:
{
...
this
.
buildDisabledZoomPlugin
()
},
data
:
{
labels
,
datasets
:
[
...
...
@@ -215,12 +285,12 @@ export class VideoStatsComponent implements OnInit {
}
}
private
buildTimeserieChartOptions
(
rawData
:
VideoStatsTimeserie
)
{
private
buildTimeserieChartOptions
(
rawData
:
VideoStatsTimeserie
)
:
ChartBuilderResult
{
const
labels
:
string
[]
=
[]
const
data
:
number
[]
=
[]
for
(
const
d
of
rawData
.
data
)
{
labels
.
push
(
new
Date
(
d
.
date
).
toLocaleDateString
()
)
labels
.
push
(
d
.
date
)
data
.
push
(
d
.
value
)
}
...
...
@@ -229,6 +299,31 @@ export class VideoStatsComponent implements OnInit {
displayLegend
:
false
,
plugins
:
{
zoom
:
{
zoom
:
{
wheel
:
{
enabled
:
false
},
drag
:
{
enabled
:
true
},
pinch
:
{
enabled
:
true
},
mode
:
'
x
'
,
onZoomComplete
:
({
chart
})
=>
{
const
{
min
,
max
}
=
chart
.
scales
.
x
const
startDate
=
rawData
.
data
[
min
].
date
const
endDate
=
rawData
.
data
[
max
].
date
this
.
peertubeRouter
.
silentNavigate
([],
{
startDate
,
endDate
})
}
}
}
},
data
:
{
labels
,
datasets
:
[
...
...
@@ -241,7 +336,7 @@ export class VideoStatsComponent implements OnInit {
}
}
private
buildCountryChartOptions
(
rawData
:
CountryData
)
{
private
buildCountryChartOptions
(
rawData
:
CountryData
)
:
ChartBuilderResult
{
const
labels
:
string
[]
=
[]
const
data
:
number
[]
=
[]
...
...
@@ -255,8 +350,8 @@ export class VideoStatsComponent implements OnInit {
displayLegend
:
true
,
optio
ns
:
{
indexAxis
:
'
y
'
plugi
ns
:
{
...
this
.
buildDisabledZoomPlugin
()
},
data
:
{
...
...
@@ -277,13 +372,57 @@ export class VideoStatsComponent implements OnInit {
return
getComputedStyle
(
document
.
body
).
getPropertyValue
(
'
--mainColorLighter
'
)
}
private
formatTick
(
graphId
:
ActiveGraphId
,
value
:
number
|
string
)
{
private
formatXTick
(
options
:
{
graphId
:
ActiveGraphId
value
:
number
|
string
data
:
VideoStatsTimeserie
scale
:
Scale
})
{
const
{
graphId
,
value
,
data
,
scale
}
=
options
const
label
=
scale
.
getLabelForValue
(
value
as
number
)
if
(
!
this
.
isTimeserieGraph
(
graphId
))
{
return
label
}
const
date
=
new
Date
(
label
)
if
(
data
.
groupInterval
.
match
(
/ days
?
$/
))
{
return
date
.
toLocaleDateString
([],
{
month
:
'
numeric
'
,
day
:
'
numeric
'
})
}
if
(
data
.
groupInterval
.
match
(
/ hours
?
$/
))
{
return
date
.
toLocaleTimeString
([],
{
month
:
'
numeric
'
,
day
:
'
numeric
'
,
hour
:
'
numeric
'
,
minute
:
'
numeric
'
})
}
return
date
.
toLocaleTimeString
([],
{
hour
:
'
numeric
'
,
minute
:
'
numeric
'
})
}
private
formatYTick
(
options
:
{
graphId
:
ActiveGraphId
value
:
number
|
string
})
{
const
{
graphId
,
value
}
=
options
if
(
graphId
===
'
retention
'
)
return
value
+
'
%
'
if
(
graphId
===
'
aggregateWatchTime
'
)
return
secondsToTime
(
+
value
)
return
value
.
toLocaleString
()
}
private
formatTooltipTitle
(
options
:
{
graphId
:
ActiveGraphId
items
:
TooltipItem
<
any
>
[]
})
{
const
{
graphId
,
items
}
=
options
const
item
=
items
[
0
]
if
(
this
.
isTimeserieGraph
(
graphId
))
return
new
Date
(
item
.
label
).
toLocaleString
()
return
item
.
label
}
private
countryCodeToName
(
code
:
string
)
{
const
intl
:
any
=
Intl
if
(
!
intl
.
DisplayNames
)
return
code
...
...
@@ -292,4 +431,22 @@ export class VideoStatsComponent implements OnInit {
return
regionNames
.
of
(
code
)
}
private
buildDisabledZoomPlugin
()
{
return
{
zoom
:
{
zoom
:
{
wheel
:
{
enabled
:
false
},
drag
:
{
enabled
:
false
},
pinch
:
{
enabled
:
false
}
}
}
}
}
}
client/src/app/+stats/video/video-stats.service.ts
View file @
e9fdb628
import
{
catchError
}
from
'
rxjs
'
import
{
environment
}
from
'
src/environments/environment
'
import
{
HttpClient
}
from
'
@angular/common/http
'
import
{
HttpClient
,
HttpParams
}
from
'
@angular/common/http
'
import
{
Injectable
}
from
'
@angular/core
'
import
{
RestExtractor
}
from
'
@app/core
'
import
{
VideoService
}
from
'
@app/shared/shared-main
'
...
...
@@ -22,8 +22,19 @@ export class VideoStatsService {
.
pipe
(
catchError
(
err
=>
this
.
restExtractor
.
handleError
(
err
)))
}
getTimeserieStats
(
videoId
:
string
,
metric
:
VideoStatsTimeserieMetric
)
{
return
this
.
authHttp
.
get
<
VideoStatsTimeserie
>
(
VideoService
.
BASE_VIDEO_URL
+
'
/
'
+
videoId
+
'
/stats/timeseries/
'
+
metric
)
getTimeserieStats
(
options
:
{
videoId
:
string
metric
:
VideoStatsTimeserieMetric
startDate
?:
Date
endDate
?:
Date
})
{
const
{
videoId
,
metric
,
startDate
,
endDate
}
=
options
let
params
=
new
HttpParams
()
if
(
startDate
)
params
=
params
.
append
(
'
startDate
'
,
startDate
.
toISOString
())
if
(
endDate
)
params
=
params
.
append
(
'
endDate
'
,
endDate
.
toISOString
())
return
this
.
authHttp
.
get
<
VideoStatsTimeserie
>
(
VideoService
.
BASE_VIDEO_URL
+
'
/
'
+
videoId
+
'
/stats/timeseries/
'
+
metric
,
{
params
})
.
pipe
(
catchError
(
err
=>
this
.
restExtractor
.
handleError
(
err
)))
}
...
...
client/yarn.lock
View file @
e9fdb628
...
...
@@ -3792,6 +3792,13 @@ chart.js@^3.5.1:
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-3.7.1.tgz#0516f690c6a8680c6c707e31a4c1807a6f400ada"
integrity sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA==
chartjs-plugin-zoom@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/chartjs-plugin-zoom/-/chartjs-plugin-zoom-1.2.1.tgz#7e350ba20d907f397d0c055239dcc67d326df705"
integrity sha512-2zbWvw2pljrtMLMXkKw1uxYzAne5PtjJiOZftcut4Lo3Ee8qUt95RpMKDWrZ+pBZxZKQKOD/etdU4pN2jxZUmg==
dependencies:
hammerjs "^2.0.8"
chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.2:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
...
...
@@ -5961,6 +5968,11 @@ gzip-size@^6.0.0:
dependencies:
duplexer "^0.1.2"
hammerjs@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/hammerjs/-/hammerjs-2.0.8.tgz#04ef77862cff2bb79d30f7692095930222bf60f1"
integrity sha1-BO93hiz/K7edMPdpIJWTAiK/YPE=
handle-thing@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e"
...
...
server/lib/timeserie.ts
View file @
e9fdb628
import
{
logger
}
from
'
@server/helpers/logger
'
import
{
VideoStatsTimeserieGroupInterval
}
from
'
@shared/models
'
function
buildGroupByAndBoundaries
(
startDateString
:
string
,
endDateString
:
string
)
{
const
startDate
=
new
Date
(
startDateString
)
const
endDate
=
new
Date
(
endDateString
)
const
groupByMatrix
:
{
[
id
in
VideoStatsTimeserieGroupInterval
]:
string
}
=
{
one_day
:
'
1 day
'
,
one_hour
:
'
1 hour
'
,
ten_minutes
:
'
10 minutes
'
,
one_minute
:
'
1 minute
'
}
const
groupInterval
=
buildGroupInterval
(
startDate
,
endDate
)
logger
.
debug
(
'
Found "%s" group interval.
'
,
groupInterval
,
{
startDate
,
endDate
})
// Remove parts of the date we don't need
if
(
groupInterval
===
'
one_day
'
)
{
if
(
groupInterval
.
endsWith
(
'
day
'
)
||
groupInterval
.
endsWith
(
'
days
'
)
)
{
startDate
.
setHours
(
0
,
0
,
0
,
0
)
}
else
if
(
groupInterval
===
'
one_
hour
'
)
{
}
else
if
(
groupInterval
.
endsWith
(
'
hour
'
)
||
groupInterval
.
endsWith
(
'
hour
s
'
)
)
{
startDate
.
setMinutes
(
0
,
0
,
0
)
}
else
{
startDate
.
setSeconds
(
0
,
0
)
...
...
@@ -26,7 +19,6 @@ function buildGroupByAndBoundaries (startDateString: string, endDateString: stri
return
{
groupInterval
,
sqlInterval
:
groupByMatrix
[
groupInterval
],
startDate
,
endDate
}
...
...
@@ -40,16 +32,18 @@ export {
// ---------------------------------------------------------------------------
function
buildGroupInterval
(
startDate
:
Date
,
endDate
:
Date
):
VideoStatsTimeserieGroupInterval
{
function
buildGroupInterval
(
startDate
:
Date
,
endDate
:
Date
):
string
{
const
aDay
=
86400
const
anHour
=
3600
const
aMinute
=
60
const
diffSeconds
=
(
endDate
.
getTime
()
-
startDate
.
getTime
())
/
1000
if
(
diffSeconds
>=
6
*
aDay
)
return
'
one_day
'
if
(
diffSeconds
>=
6
*
anHour
)
return
'
one_hour
'
if
(
diffSeconds
>=
60
*
aMinute
)
return
'
ten_minutes
'
if
(
diffSeconds
>=
15
*
aDay
)
return
'
1 day
'
if
(
diffSeconds
>=
8
*
aDay
)
return
'
12 hours
'
if
(
diffSeconds
>=
4
*
aDay
)
return
'
6 hours
'
if
(
diffSeconds
>=
15
*
anHour
)
return
'
1 hour
'
if
(
diffSeconds
>=
180
*
aMinute
)
return
'
10 minutes
'
return
'
one_
minute
'
return
'
1
minute
'
}
server/models/view/local-video-viewer.ts
View file @
e9fdb628
...
...
@@ -221,7 +221,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
}):
Promise
<
VideoStatsTimeserie
>
{
const
{
video
,
metric
}
=
options
const
{
groupInterval
,
sqlInterval
,
startDate
,
endDate
}
=
buildGroupByAndBoundaries
(
options
.
startDate
,
options
.
endDate
)
const
{
groupInterval
,
startDate
,
endDate
}
=
buildGroupByAndBoundaries
(
options
.
startDate
,
options
.
endDate
)
const
selectMetrics
:
{
[
id
in
VideoStatsTimeserieMetric
]:
string
}
=
{
viewers
:
'
COUNT("localVideoViewer"."id")
'
,
...
...
@@ -230,9 +230,9 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
const
query
=
`WITH "intervals" AS (
SELECT
"time" AS "startDate", "time" + :
sql
Interval::interval as "endDate"
"time" AS "startDate", "time" + :
group
Interval::interval as "endDate"
FROM
generate_series(:startDate::timestamptz, :endDate::timestamptz, :
sql
Interval::interval) serie("time")
generate_series(:startDate::timestamptz, :endDate::timestamptz, :
group
Interval::interval) serie("time")
)
SELECT "intervals"."startDate" as "date", COALESCE(
${
selectMetrics
[
metric
]}
, 0) AS value
FROM
...
...
@@ -249,7 +249,7 @@ export class LocalVideoViewerModel extends Model<Partial<AttributesOnly<LocalVid
replacements
:
{
startDate
,
endDate
,
sql
Interval
,
group
Interval
,
videoId
:
video
.
id
}
}
...
...
server/tests/api/views/video-views-timeserie-stats.ts
View file @
e9fdb628
...
...
@@ -110,21 +110,21 @@ describe('Test views timeserie stats', function () {
it
(
'
Should use a custom start/end date
'
,
async
function
()
{
const
now
=
new
Date
()
const
tenDaysAgo
=
new
Date
()
tenDaysAgo
.
setDate
(
tenDaysAgo
.
getDate
()
-
9
)
const
t
w
en
ty
DaysAgo
=
new
Date
()
t
w
en
ty
DaysAgo
.
setDate
(
t
w
en
ty
DaysAgo
.
getDate
()
-
1
9
)
const
result
=
await
servers
[
0
].
videoStats
.
getTimeserieStats
({
videoId
:
vodVideoId
,
metric
:
'
aggregateWatchTime
'
,