diff options
Diffstat (limited to 'server/lib/activitypub')
-rw-r--r-- | server/lib/activitypub/activity.ts | 13 | ||||
-rw-r--r-- | server/lib/activitypub/context.ts | 23 | ||||
-rw-r--r-- | server/lib/activitypub/local-video-viewer.ts | 42 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-create.ts | 21 | ||||
-rw-r--r-- | server/lib/activitypub/process/process-view.ts | 4 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-create.ts | 17 | ||||
-rw-r--r-- | server/lib/activitypub/send/send-view.ts | 51 | ||||
-rw-r--r-- | server/lib/activitypub/url.ts | 6 | ||||
-rw-r--r-- | server/lib/activitypub/videos/shared/object-to-model-attributes.ts | 4 |
9 files changed, 156 insertions, 25 deletions
diff --git a/server/lib/activitypub/activity.ts b/server/lib/activitypub/activity.ts index cccb7b1c1..e6cec1ba7 100644 --- a/server/lib/activitypub/activity.ts +++ b/server/lib/activitypub/activity.ts | |||
@@ -4,6 +4,17 @@ function getAPId (object: string | { id: string }) { | |||
4 | return object.id | 4 | return object.id |
5 | } | 5 | } |
6 | 6 | ||
7 | function getActivityStreamDuration (duration: number) { | ||
8 | // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration | ||
9 | return 'PT' + duration + 'S' | ||
10 | } | ||
11 | |||
12 | function getDurationFromActivityStream (duration: string) { | ||
13 | return parseInt(duration.replace(/[^\d]+/, '')) | ||
14 | } | ||
15 | |||
7 | export { | 16 | export { |
8 | getAPId | 17 | getAPId, |
18 | getActivityStreamDuration, | ||
19 | getDurationFromActivityStream | ||
9 | } | 20 | } |
diff --git a/server/lib/activitypub/context.ts b/server/lib/activitypub/context.ts index 3bc40e2aa..b452cf9b3 100644 --- a/server/lib/activitypub/context.ts +++ b/server/lib/activitypub/context.ts | |||
@@ -15,7 +15,7 @@ export { | |||
15 | 15 | ||
16 | type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) } | 16 | type ContextValue = { [ id: string ]: (string | { '@type': string, '@id': string }) } |
17 | 17 | ||
18 | const contextStore = { | 18 | const contextStore: { [ id in ContextType ]: (string | { [ id: string ]: string })[] } = { |
19 | Video: buildContext({ | 19 | Video: buildContext({ |
20 | Hashtag: 'as:Hashtag', | 20 | Hashtag: 'as:Hashtag', |
21 | uuid: 'sc:identifier', | 21 | uuid: 'sc:identifier', |
@@ -109,7 +109,8 @@ const contextStore = { | |||
109 | stopTimestamp: { | 109 | stopTimestamp: { |
110 | '@type': 'sc:Number', | 110 | '@type': 'sc:Number', |
111 | '@id': 'pt:stopTimestamp' | 111 | '@id': 'pt:stopTimestamp' |
112 | } | 112 | }, |
113 | uuid: 'sc:identifier' | ||
113 | }), | 114 | }), |
114 | 115 | ||
115 | CacheFile: buildContext({ | 116 | CacheFile: buildContext({ |
@@ -128,6 +129,24 @@ const contextStore = { | |||
128 | } | 129 | } |
129 | }), | 130 | }), |
130 | 131 | ||
132 | WatchAction: buildContext({ | ||
133 | WatchAction: 'sc:WatchAction', | ||
134 | startTimestamp: { | ||
135 | '@type': 'sc:Number', | ||
136 | '@id': 'pt:startTimestamp' | ||
137 | }, | ||
138 | stopTimestamp: { | ||
139 | '@type': 'sc:Number', | ||
140 | '@id': 'pt:stopTimestamp' | ||
141 | }, | ||
142 | watchSection: { | ||
143 | '@type': 'sc:Number', | ||
144 | '@id': 'pt:stopTimestamp' | ||
145 | }, | ||
146 | uuid: 'sc:identifier' | ||
147 | }), | ||
148 | |||
149 | Collection: buildContext(), | ||
131 | Follow: buildContext(), | 150 | Follow: buildContext(), |
132 | Reject: buildContext(), | 151 | Reject: buildContext(), |
133 | Accept: buildContext(), | 152 | Accept: buildContext(), |
diff --git a/server/lib/activitypub/local-video-viewer.ts b/server/lib/activitypub/local-video-viewer.ts new file mode 100644 index 000000000..738083adc --- /dev/null +++ b/server/lib/activitypub/local-video-viewer.ts | |||
@@ -0,0 +1,42 @@ | |||
1 | import { Transaction } from 'sequelize' | ||
2 | import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer' | ||
3 | import { LocalVideoViewerWatchSectionModel } from '@server/models/view/local-video-viewer-watch-section' | ||
4 | import { MVideo } from '@server/types/models' | ||
5 | import { WatchActionObject } from '@shared/models' | ||
6 | import { getDurationFromActivityStream } from './activity' | ||
7 | |||
8 | async function createOrUpdateLocalVideoViewer (watchAction: WatchActionObject, video: MVideo, t: Transaction) { | ||
9 | const stats = await LocalVideoViewerModel.loadByUrl(watchAction.id) | ||
10 | if (stats) await stats.destroy({ transaction: t }) | ||
11 | |||
12 | const localVideoViewer = await LocalVideoViewerModel.create({ | ||
13 | url: watchAction.id, | ||
14 | uuid: watchAction.uuid, | ||
15 | |||
16 | watchTime: getDurationFromActivityStream(watchAction.duration), | ||
17 | |||
18 | startDate: new Date(watchAction.startTime), | ||
19 | endDate: new Date(watchAction.endTime), | ||
20 | |||
21 | country: watchAction.location | ||
22 | ? watchAction.location.addressCountry | ||
23 | : null, | ||
24 | |||
25 | videoId: video.id | ||
26 | }) | ||
27 | |||
28 | await LocalVideoViewerWatchSectionModel.bulkCreateSections({ | ||
29 | localVideoViewerId: localVideoViewer.id, | ||
30 | |||
31 | watchSections: watchAction.watchSections.map(s => ({ | ||
32 | start: s.startTimestamp, | ||
33 | end: s.endTimestamp | ||
34 | })) | ||
35 | }) | ||
36 | } | ||
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | export { | ||
41 | createOrUpdateLocalVideoViewer | ||
42 | } | ||
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts index b5b1a0feb..3e7931bb2 100644 --- a/server/lib/activitypub/process/process-create.ts +++ b/server/lib/activitypub/process/process-create.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' | 1 | import { isBlockedByServerOrAccount } from '@server/lib/blocklist' |
2 | import { isRedundancyAccepted } from '@server/lib/redundancy' | 2 | import { isRedundancyAccepted } from '@server/lib/redundancy' |
3 | import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject } from '@shared/models' | 3 | import { VideoModel } from '@server/models/video/video' |
4 | import { ActivityCreate, CacheFileObject, PlaylistObject, VideoCommentObject, VideoObject, WatchActionObject } from '@shared/models' | ||
4 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 5 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
5 | import { logger } from '../../../helpers/logger' | 6 | import { logger } from '../../../helpers/logger' |
6 | import { sequelizeTypescript } from '../../../initializers/database' | 7 | import { sequelizeTypescript } from '../../../initializers/database' |
@@ -8,6 +9,7 @@ import { APProcessorOptions } from '../../../types/activitypub-processor.model' | |||
8 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' | 9 | import { MActorSignature, MCommentOwnerVideo, MVideoAccountLightBlacklistAllFiles } from '../../../types/models' |
9 | import { Notifier } from '../../notifier' | 10 | import { Notifier } from '../../notifier' |
10 | import { createOrUpdateCacheFile } from '../cache-file' | 11 | import { createOrUpdateCacheFile } from '../cache-file' |
12 | import { createOrUpdateLocalVideoViewer } from '../local-video-viewer' | ||
11 | import { createOrUpdateVideoPlaylist } from '../playlists' | 13 | import { createOrUpdateVideoPlaylist } from '../playlists' |
12 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' | 14 | import { forwardVideoRelatedActivity } from '../send/shared/send-utils' |
13 | import { resolveThread } from '../video-comments' | 15 | import { resolveThread } from '../video-comments' |
@@ -32,6 +34,10 @@ async function processCreateActivity (options: APProcessorOptions<ActivityCreate | |||
32 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) | 34 | return retryTransactionWrapper(processCreateVideoComment, activity, byActor, notify) |
33 | } | 35 | } |
34 | 36 | ||
37 | if (activityType === 'WatchAction') { | ||
38 | return retryTransactionWrapper(processCreateWatchAction, activity) | ||
39 | } | ||
40 | |||
35 | if (activityType === 'CacheFile') { | 41 | if (activityType === 'CacheFile') { |
36 | return retryTransactionWrapper(processCreateCacheFile, activity, byActor) | 42 | return retryTransactionWrapper(processCreateCacheFile, activity, byActor) |
37 | } | 43 | } |
@@ -81,6 +87,19 @@ async function processCreateCacheFile (activity: ActivityCreate, byActor: MActor | |||
81 | } | 87 | } |
82 | } | 88 | } |
83 | 89 | ||
90 | async function processCreateWatchAction (activity: ActivityCreate) { | ||
91 | const watchAction = activity.object as WatchActionObject | ||
92 | |||
93 | if (watchAction.actionStatus !== 'CompletedActionStatus') return | ||
94 | |||
95 | const video = await VideoModel.loadByUrl(watchAction.object) | ||
96 | if (video.remote) return | ||
97 | |||
98 | await sequelizeTypescript.transaction(async t => { | ||
99 | return createOrUpdateLocalVideoViewer(watchAction, video, t) | ||
100 | }) | ||
101 | } | ||
102 | |||
84 | async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { | 103 | async function processCreateVideoComment (activity: ActivityCreate, byActor: MActorSignature, notify: boolean) { |
85 | const commentObject = activity.object as VideoCommentObject | 104 | const commentObject = activity.object as VideoCommentObject |
86 | const byAccount = byActor.Account | 105 | const byAccount = byActor.Account |
diff --git a/server/lib/activitypub/process/process-view.ts b/server/lib/activitypub/process/process-view.ts index c59940164..bad079843 100644 --- a/server/lib/activitypub/process/process-view.ts +++ b/server/lib/activitypub/process/process-view.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoViews } from '@server/lib/video-views' | 1 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' |
2 | import { ActivityView } from '../../../../shared/models/activitypub' | 2 | import { ActivityView } from '../../../../shared/models/activitypub' |
3 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' | 3 | import { APProcessorOptions } from '../../../types/activitypub-processor.model' |
4 | import { MActorSignature } from '../../../types/models' | 4 | import { MActorSignature } from '../../../types/models' |
@@ -32,7 +32,7 @@ async function processCreateView (activity: ActivityView, byActor: MActorSignatu | |||
32 | ? new Date(activity.expires) | 32 | ? new Date(activity.expires) |
33 | : undefined | 33 | : undefined |
34 | 34 | ||
35 | await VideoViews.Instance.processView({ video, ip: null, viewerExpires }) | 35 | await VideoViewsManager.Instance.processRemoteView({ video, viewerExpires }) |
36 | 36 | ||
37 | if (video.isOwned()) { | 37 | if (video.isOwned()) { |
38 | // Forward the view but don't resend the activity to the sender | 38 | // Forward the view but don't resend the activity to the sender |
diff --git a/server/lib/activitypub/send/send-create.ts b/server/lib/activitypub/send/send-create.ts index 5d8763495..7c3a6bdd0 100644 --- a/server/lib/activitypub/send/send-create.ts +++ b/server/lib/activitypub/send/send-create.ts | |||
@@ -6,6 +6,7 @@ import { VideoCommentModel } from '../../../models/video/video-comment' | |||
6 | import { | 6 | import { |
7 | MActorLight, | 7 | MActorLight, |
8 | MCommentOwnerVideo, | 8 | MCommentOwnerVideo, |
9 | MLocalVideoViewerWithWatchSections, | ||
9 | MVideoAccountLight, | 10 | MVideoAccountLight, |
10 | MVideoAP, | 11 | MVideoAP, |
11 | MVideoPlaylistFull, | 12 | MVideoPlaylistFull, |
@@ -19,6 +20,7 @@ import { | |||
19 | getActorsInvolvedInVideo, | 20 | getActorsInvolvedInVideo, |
20 | getAudienceFromFollowersOf, | 21 | getAudienceFromFollowersOf, |
21 | getVideoCommentAudience, | 22 | getVideoCommentAudience, |
23 | sendVideoActivityToOrigin, | ||
22 | sendVideoRelatedActivity, | 24 | sendVideoRelatedActivity, |
23 | unicastTo | 25 | unicastTo |
24 | } from './shared' | 26 | } from './shared' |
@@ -61,6 +63,18 @@ async function sendCreateCacheFile ( | |||
61 | }) | 63 | }) |
62 | } | 64 | } |
63 | 65 | ||
66 | async function sendCreateWatchAction (stats: MLocalVideoViewerWithWatchSections, transaction: Transaction) { | ||
67 | logger.info('Creating job to send create watch action %s.', stats.url, lTags(stats.uuid)) | ||
68 | |||
69 | const byActor = await getServerActor() | ||
70 | |||
71 | const activityBuilder = (audience: ActivityAudience) => { | ||
72 | return buildCreateActivity(stats.url, byActor, stats.toActivityPubObject(), audience) | ||
73 | } | ||
74 | |||
75 | return sendVideoActivityToOrigin(activityBuilder, { byActor, video: stats.Video, transaction, contextType: 'WatchAction' }) | ||
76 | } | ||
77 | |||
64 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) { | 78 | async function sendCreateVideoPlaylist (playlist: MVideoPlaylistFull, transaction: Transaction) { |
65 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined | 79 | if (playlist.privacy === VideoPlaylistPrivacy.PRIVATE) return undefined |
66 | 80 | ||
@@ -175,7 +189,8 @@ export { | |||
175 | buildCreateActivity, | 189 | buildCreateActivity, |
176 | sendCreateVideoComment, | 190 | sendCreateVideoComment, |
177 | sendCreateVideoPlaylist, | 191 | sendCreateVideoPlaylist, |
178 | sendCreateCacheFile | 192 | sendCreateCacheFile, |
193 | sendCreateWatchAction | ||
179 | } | 194 | } |
180 | 195 | ||
181 | // --------------------------------------------------------------------------- | 196 | // --------------------------------------------------------------------------- |
diff --git a/server/lib/activitypub/send/send-view.ts b/server/lib/activitypub/send/send-view.ts index 1f97307b9..1088bf258 100644 --- a/server/lib/activitypub/send/send-view.ts +++ b/server/lib/activitypub/send/send-view.ts | |||
@@ -1,27 +1,49 @@ | |||
1 | import { Transaction } from 'sequelize' | 1 | import { Transaction } from 'sequelize' |
2 | import { VideoViews } from '@server/lib/video-views' | 2 | import { VideoViewsManager } from '@server/lib/views/video-views-manager' |
3 | import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models' | 3 | import { MActorAudience, MActorLight, MVideoImmutable, MVideoUrl } from '@server/types/models' |
4 | import { ActivityAudience, ActivityView } from '@shared/models' | 4 | import { ActivityAudience, ActivityView } from '@shared/models' |
5 | import { logger } from '../../../helpers/logger' | 5 | import { logger } from '../../../helpers/logger' |
6 | import { ActorModel } from '../../../models/actor/actor' | ||
7 | import { audiencify, getAudience } from '../audience' | 6 | import { audiencify, getAudience } from '../audience' |
8 | import { getLocalVideoViewActivityPubUrl } from '../url' | 7 | import { getLocalVideoViewActivityPubUrl } from '../url' |
9 | import { sendVideoRelatedActivity } from './shared/send-utils' | 8 | import { sendVideoRelatedActivity } from './shared/send-utils' |
10 | 9 | ||
11 | async function sendView (byActor: ActorModel, video: MVideoImmutable, t: Transaction) { | 10 | type ViewType = 'view' | 'viewer' |
12 | logger.info('Creating job to send view of %s.', video.url) | 11 | |
12 | async function sendView (options: { | ||
13 | byActor: MActorLight | ||
14 | type: ViewType | ||
15 | video: MVideoImmutable | ||
16 | transaction?: Transaction | ||
17 | }) { | ||
18 | const { byActor, type, video, transaction } = options | ||
19 | |||
20 | logger.info('Creating job to send %s of %s.', type, video.url) | ||
13 | 21 | ||
14 | const activityBuilder = (audience: ActivityAudience) => { | 22 | const activityBuilder = (audience: ActivityAudience) => { |
15 | const url = getLocalVideoViewActivityPubUrl(byActor, video) | 23 | const url = getLocalVideoViewActivityPubUrl(byActor, video) |
16 | 24 | ||
17 | return buildViewActivity(url, byActor, video, audience) | 25 | return buildViewActivity({ url, byActor, video, audience, type }) |
18 | } | 26 | } |
19 | 27 | ||
20 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction: t, contextType: 'View' }) | 28 | return sendVideoRelatedActivity(activityBuilder, { byActor, video, transaction, contextType: 'View' }) |
21 | } | 29 | } |
22 | 30 | ||
23 | function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoUrl, audience?: ActivityAudience): ActivityView { | 31 | // --------------------------------------------------------------------------- |
24 | if (!audience) audience = getAudience(byActor) | 32 | |
33 | export { | ||
34 | sendView | ||
35 | } | ||
36 | |||
37 | // --------------------------------------------------------------------------- | ||
38 | |||
39 | function buildViewActivity (options: { | ||
40 | url: string | ||
41 | byActor: MActorAudience | ||
42 | video: MVideoUrl | ||
43 | type: ViewType | ||
44 | audience?: ActivityAudience | ||
45 | }): ActivityView { | ||
46 | const { url, byActor, type, video, audience = getAudience(byActor) } = options | ||
25 | 47 | ||
26 | return audiencify( | 48 | return audiencify( |
27 | { | 49 | { |
@@ -29,14 +51,11 @@ function buildViewActivity (url: string, byActor: MActorAudience, video: MVideoU | |||
29 | type: 'View' as 'View', | 51 | type: 'View' as 'View', |
30 | actor: byActor.url, | 52 | actor: byActor.url, |
31 | object: video.url, | 53 | object: video.url, |
32 | expires: new Date(VideoViews.Instance.buildViewerExpireTime()).toISOString() | 54 | |
55 | expires: type === 'viewer' | ||
56 | ? new Date(VideoViewsManager.Instance.buildViewerExpireTime()).toISOString() | ||
57 | : undefined | ||
33 | }, | 58 | }, |
34 | audience | 59 | audience |
35 | ) | 60 | ) |
36 | } | 61 | } |
37 | |||
38 | // --------------------------------------------------------------------------- | ||
39 | |||
40 | export { | ||
41 | sendView | ||
42 | } | ||
diff --git a/server/lib/activitypub/url.ts b/server/lib/activitypub/url.ts index 50be4fac9..8443fef4c 100644 --- a/server/lib/activitypub/url.ts +++ b/server/lib/activitypub/url.ts | |||
@@ -7,6 +7,7 @@ import { | |||
7 | MActorId, | 7 | MActorId, |
8 | MActorUrl, | 8 | MActorUrl, |
9 | MCommentId, | 9 | MCommentId, |
10 | MLocalVideoViewer, | ||
10 | MVideoId, | 11 | MVideoId, |
11 | MVideoPlaylistElement, | 12 | MVideoPlaylistElement, |
12 | MVideoUrl, | 13 | MVideoUrl, |
@@ -59,6 +60,10 @@ function getLocalVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) { | |||
59 | return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString() | 60 | return byActor.url + '/views/videos/' + video.id + '/' + new Date().toISOString() |
60 | } | 61 | } |
61 | 62 | ||
63 | function getLocalVideoViewerActivityPubUrl (stats: MLocalVideoViewer) { | ||
64 | return WEBSERVER.URL + '/videos/local-viewer/' + stats.uuid | ||
65 | } | ||
66 | |||
62 | function getVideoLikeActivityPubUrlByLocalActor (byActor: MActorUrl, video: MVideoId) { | 67 | function getVideoLikeActivityPubUrlByLocalActor (byActor: MActorUrl, video: MVideoId) { |
63 | return byActor.url + '/likes/' + video.id | 68 | return byActor.url + '/likes/' + video.id |
64 | } | 69 | } |
@@ -167,6 +172,7 @@ export { | |||
167 | getLocalVideoCommentsActivityPubUrl, | 172 | getLocalVideoCommentsActivityPubUrl, |
168 | getLocalVideoLikesActivityPubUrl, | 173 | getLocalVideoLikesActivityPubUrl, |
169 | getLocalVideoDislikesActivityPubUrl, | 174 | getLocalVideoDislikesActivityPubUrl, |
175 | getLocalVideoViewerActivityPubUrl, | ||
170 | 176 | ||
171 | getAbuseTargetUrl, | 177 | getAbuseTargetUrl, |
172 | checkUrlsSameHost, | 178 | checkUrlsSameHost, |
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts index c97217669..f02b9cba6 100644 --- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts +++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts | |||
@@ -24,6 +24,7 @@ import { | |||
24 | VideoPrivacy, | 24 | VideoPrivacy, |
25 | VideoStreamingPlaylistType | 25 | VideoStreamingPlaylistType |
26 | } from '@shared/models' | 26 | } from '@shared/models' |
27 | import { getDurationFromActivityStream } from '../../activity' | ||
27 | 28 | ||
28 | function getThumbnailFromIcons (videoObject: VideoObject) { | 29 | function getThumbnailFromIcons (videoObject: VideoObject) { |
29 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) | 30 | let validIcons = videoObject.icon.filter(i => i.width > THUMBNAILS_SIZE.minWidth) |
@@ -170,7 +171,6 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi | |||
170 | ? VideoPrivacy.PUBLIC | 171 | ? VideoPrivacy.PUBLIC |
171 | : VideoPrivacy.UNLISTED | 172 | : VideoPrivacy.UNLISTED |
172 | 173 | ||
173 | const duration = videoObject.duration.replace(/[^\d]+/, '') | ||
174 | const language = videoObject.language?.identifier | 174 | const language = videoObject.language?.identifier |
175 | 175 | ||
176 | const category = videoObject.category | 176 | const category = videoObject.category |
@@ -200,7 +200,7 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi | |||
200 | isLive: videoObject.isLiveBroadcast, | 200 | isLive: videoObject.isLiveBroadcast, |
201 | state: videoObject.state, | 201 | state: videoObject.state, |
202 | channelId: videoChannel.id, | 202 | channelId: videoChannel.id, |
203 | duration: parseInt(duration, 10), | 203 | duration: getDurationFromActivityStream(videoObject.duration), |
204 | createdAt: new Date(videoObject.published), | 204 | createdAt: new Date(videoObject.published), |
205 | publishedAt: new Date(videoObject.published), | 205 | publishedAt: new Date(videoObject.published), |
206 | 206 | ||