aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-10-29 10:54:27 +0200
committerChocobozzz <chocobozzz@cpy.re>2021-10-29 11:48:21 +0200
commit3c10840fa90fc88fc98e8169faf4745ff6c80893 (patch)
tree9a60c4de766700fbc33804b06ec46279b20c855e
parent2760b454a761f6af3138b2fb5f34340772ab0d1e (diff)
downloadPeerTube-3c10840fa90fc88fc98e8169faf4745ff6c80893.tar.gz
PeerTube-3c10840fa90fc88fc98e8169faf4745ff6c80893.tar.zst
PeerTube-3c10840fa90fc88fc98e8169faf4745ff6c80893.zip
Add video file size info in admin videos list
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.html38
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.scss5
-rw-r--r--client/src/app/+admin/overview/videos/video-list.component.ts20
-rw-r--r--client/src/app/shared/shared-main/video/video-details.model.ts4
-rw-r--r--client/src/app/shared/shared-main/video/video.model.ts10
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts6
-rw-r--r--server/controllers/api/accounts.ts1
-rw-r--r--server/controllers/api/overviews.ts1
-rw-r--r--server/controllers/api/users/my-subscriptions.ts1
-rw-r--r--server/controllers/api/video-channel.ts1
-rw-r--r--server/controllers/api/videos/index.ts1
-rw-r--r--server/controllers/bots.ts1
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/models/account/account.ts2
-rw-r--r--server/models/user/user-video-history.ts1
-rw-r--r--server/models/video/formatter/video-format-utils.ts32
-rw-r--r--server/models/video/sql/video-model-get-query-builder.ts6
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts4
-rw-r--r--server/models/video/sql/videos-model-list-query-builder.ts2
-rw-r--r--server/models/video/video.ts34
-rw-r--r--server/tests/api/videos/videos-common-filters.ts28
-rw-r--r--server/types/models/account/account.ts4
-rw-r--r--server/types/models/server/server.ts2
-rw-r--r--server/types/models/video/video.ts4
-rw-r--r--shared/models/videos/video-include.enum.ts3
-rw-r--r--shared/models/videos/video.model.ts5
26 files changed, 161 insertions, 65 deletions
diff --git a/client/src/app/+admin/overview/videos/video-list.component.html b/client/src/app/+admin/overview/videos/video-list.component.html
index 6250c00fb..eedf6f3dc 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.html
+++ b/client/src/app/+admin/overview/videos/video-list.component.html
@@ -37,6 +37,7 @@
37 <th style="width: 60px;"></th> 37 <th style="width: 60px;"></th>
38 <th i18n>Video</th> 38 <th i18n>Video</th>
39 <th i18n>Info</th> 39 <th i18n>Info</th>
40 <th i18n>Files</th>
40 <th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th> 41 <th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th>
41 </tr> 42 </tr>
42 </ng-template> 43 </ng-template>
@@ -63,8 +64,8 @@
63 <my-video-cell [video]="video"></my-video-cell> 64 <my-video-cell [video]="video"></my-video-cell>
64 </td> 65 </td>
65 66
66 <td class="badges"> 67 <td>
67 <span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge" i18n>{{ video.privacy.label }}</span> 68 <span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge">{{ video.privacy.label }}</span>
68 69
69 <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span> 70 <span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
70 71
@@ -77,6 +78,13 @@
77 </td> 78 </td>
78 79
79 <td> 80 <td>
81 <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span>
82 <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span>
83
84 <span *ngIf="!video.remote">{{ getFilesSize(video) | bytes: 1 }}</span>
85 </td>
86
87 <td>
80 {{ video.publishedAt | date: 'short' }} 88 {{ video.publishedAt | date: 'short' }}
81 </td> 89 </td>
82 90
@@ -85,8 +93,30 @@
85 93
86 <ng-template pTemplate="rowexpansion" let-video> 94 <ng-template pTemplate="rowexpansion" let-video>
87 <tr> 95 <tr>
88 <td colspan="50"> 96 <td class="video-info expand-cell" colspan="7">
89 <my-embed [video]="video"></my-embed> 97 <div>
98 <div *ngIf="isWebTorrent(video)">
99 WebTorrent:
100
101 <ul>
102 <li *ngFor="let file of video.files">
103 {{ file.resolution.label }}: {{ file.size | bytes: 1 }}
104 </li>
105 </ul>
106 </div>
107
108 <div *ngIf="isHLS(video)">
109 HLS:
110
111 <ul>
112 <li *ngFor="let file of video.streamingPlaylists[0].files">
113 {{ file.resolution.label }}: {{ file.size | bytes: 1 }}
114 </li>
115 </ul>
116 </div>
117
118 <my-embed class="ml-auto" [video]="video"></my-embed>
119 </div>
90 </td> 120 </td>
91 </tr> 121 </tr>
92 </ng-template> 122 </ng-template>
diff --git a/client/src/app/+admin/overview/videos/video-list.component.scss b/client/src/app/+admin/overview/videos/video-list.component.scss
index 250a917e4..158c161af 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.scss
+++ b/client/src/app/+admin/overview/videos/video-list.component.scss
@@ -3,6 +3,7 @@
3my-embed { 3my-embed {
4 display: block; 4 display: block;
5 max-width: 500px; 5 max-width: 500px;
6 width: 50%;
6} 7}
7 8
8.badge { 9.badge {
@@ -10,3 +11,7 @@ my-embed {
10 11
11 margin-right: 5px; 12 margin-right: 5px;
12} 13}
14
15.video-info > div {
16 display: flex;
17}
diff --git a/client/src/app/+admin/overview/videos/video-list.component.ts b/client/src/app/+admin/overview/videos/video-list.component.ts
index dd9225e6a..6885abfc7 100644
--- a/client/src/app/+admin/overview/videos/video-list.component.ts
+++ b/client/src/app/+admin/overview/videos/video-list.component.ts
@@ -3,7 +3,7 @@ import { Component, OnInit } from '@angular/core'
3import { ActivatedRoute, Router } from '@angular/router' 3import { ActivatedRoute, Router } from '@angular/router'
4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core' 4import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
5import { DropdownAction, Video, VideoService } from '@app/shared/shared-main' 5import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
6import { UserRight, VideoPrivacy, VideoState } from '@shared/models' 6import { UserRight, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
7import { AdvancedInputFilter } from '@app/shared/shared-forms' 7import { AdvancedInputFilter } from '@app/shared/shared-forms'
8import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature' 8import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
9 9
@@ -114,6 +114,24 @@ export class VideoListComponent extends RestTable implements OnInit {
114 return video.blacklisted 114 return video.blacklisted
115 } 115 }
116 116
117 isHLS (video: Video) {
118 return video.streamingPlaylists.some(p => p.type === VideoStreamingPlaylistType.HLS)
119 }
120
121 isWebTorrent (video: Video) {
122 return video.files.length !== 0
123 }
124
125 getFilesSize (video: Video) {
126 let files = video.files
127
128 if (this.isHLS(video)) {
129 files = files.concat(video.streamingPlaylists[0].files)
130 }
131
132 return files.reduce((p, f) => p += f.size, 0)
133 }
134
117 protected reloadData () { 135 protected reloadData () {
118 this.selectedVideos = [] 136 this.selectedVideos = []
119 137
diff --git a/client/src/app/shared/shared-main/video/video-details.model.ts b/client/src/app/shared/shared-main/video/video-details.model.ts
index f060d1dc9..45c053507 100644
--- a/client/src/app/shared/shared-main/video/video-details.model.ts
+++ b/client/src/app/shared/shared-main/video/video-details.model.ts
@@ -15,7 +15,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
15 support: string 15 support: string
16 channel: VideoChannel 16 channel: VideoChannel
17 tags: string[] 17 tags: string[]
18 files: VideoFile[]
19 account: Account 18 account: Account
20 commentsEnabled: boolean 19 commentsEnabled: boolean
21 downloadEnabled: boolean 20 downloadEnabled: boolean
@@ -28,13 +27,13 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
28 27
29 trackerUrls: string[] 28 trackerUrls: string[]
30 29
30 files: VideoFile[]
31 streamingPlaylists: VideoStreamingPlaylist[] 31 streamingPlaylists: VideoStreamingPlaylist[]
32 32
33 constructor (hash: VideoDetailsServerModel, translations = {}) { 33 constructor (hash: VideoDetailsServerModel, translations = {}) {
34 super(hash, translations) 34 super(hash, translations)
35 35
36 this.descriptionPath = hash.descriptionPath 36 this.descriptionPath = hash.descriptionPath
37 this.files = hash.files
38 this.channel = new VideoChannel(hash.channel) 37 this.channel = new VideoChannel(hash.channel)
39 this.account = new Account(hash.account) 38 this.account = new Account(hash.account)
40 this.tags = hash.tags 39 this.tags = hash.tags
@@ -43,7 +42,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
43 this.downloadEnabled = hash.downloadEnabled 42 this.downloadEnabled = hash.downloadEnabled
44 43
45 this.trackerUrls = hash.trackerUrls 44 this.trackerUrls = hash.trackerUrls
46 this.streamingPlaylists = hash.streamingPlaylists
47 45
48 this.buildLikeAndDislikePercents() 46 this.buildLikeAndDislikePercents()
49 } 47 }
diff --git a/client/src/app/shared/shared-main/video/video.model.ts b/client/src/app/shared/shared-main/video/video.model.ts
index 699eac7f1..b11316471 100644
--- a/client/src/app/shared/shared-main/video/video.model.ts
+++ b/client/src/app/shared/shared-main/video/video.model.ts
@@ -10,9 +10,11 @@ import {
10 UserRight, 10 UserRight,
11 Video as VideoServerModel, 11 Video as VideoServerModel,
12 VideoConstant, 12 VideoConstant,
13 VideoFile,
13 VideoPrivacy, 14 VideoPrivacy,
14 VideoScheduleUpdate, 15 VideoScheduleUpdate,
15 VideoState 16 VideoState,
17 VideoStreamingPlaylist
16} from '@shared/models' 18} from '@shared/models'
17 19
18export class Video implements VideoServerModel { 20export class Video implements VideoServerModel {
@@ -96,6 +98,9 @@ export class Video implements VideoServerModel {
96 98
97 pluginData?: any 99 pluginData?: any
98 100
101 streamingPlaylists?: VideoStreamingPlaylist[]
102 files?: VideoFile[]
103
99 static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) { 104 static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
100 return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid }) 105 return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
101 } 106 }
@@ -172,6 +177,9 @@ export class Video implements VideoServerModel {
172 this.blockedOwner = hash.blockedOwner 177 this.blockedOwner = hash.blockedOwner
173 this.blockedServer = hash.blockedServer 178 this.blockedServer = hash.blockedServer
174 179
180 this.streamingPlaylists = hash.streamingPlaylists
181 this.files = hash.files
182
175 this.userHistory = hash.userHistory 183 this.userHistory = hash.userHistory
176 184
177 this.originInstanceHost = this.account.host 185 this.originInstanceHost = this.account.host
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 0a3a51b0c..5db9a8704 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -208,7 +208,11 @@ export class VideoService {
208 ): Observable<ResultList<Video>> { 208 ): Observable<ResultList<Video>> {
209 const { pagination, search } = parameters 209 const { pagination, search } = parameters
210 210
211 const include = VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.HIDDEN_PRIVACY | VideoInclude.NOT_PUBLISHED_STATE 211 const include = VideoInclude.BLACKLISTED |
212 VideoInclude.BLOCKED_OWNER |
213 VideoInclude.HIDDEN_PRIVACY |
214 VideoInclude.NOT_PUBLISHED_STATE |
215 VideoInclude.FILES
212 216
213 let params = new HttpParams() 217 let params = new HttpParams()
214 params = this.buildCommonVideosParams({ params, include, ...parameters }) 218 params = this.buildCommonVideosParams({ params, include, ...parameters })
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts
index 44edffe38..46d89bafa 100644
--- a/server/controllers/api/accounts.ts
+++ b/server/controllers/api/accounts.ts
@@ -189,7 +189,6 @@ async function listAccountVideos (req: express.Request, res: express.Response) {
189 189
190 displayOnlyForFollower, 190 displayOnlyForFollower,
191 nsfw: buildNSFWFilter(res, query.nsfw), 191 nsfw: buildNSFWFilter(res, query.nsfw),
192 withFiles: false,
193 accountId: account.id, 192 accountId: account.id,
194 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 193 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
195 countVideos 194 countVideos
diff --git a/server/controllers/api/overviews.ts b/server/controllers/api/overviews.ts
index 68626a508..34585e557 100644
--- a/server/controllers/api/overviews.ts
+++ b/server/controllers/api/overviews.ts
@@ -122,7 +122,6 @@ async function getVideos (
122 }, 122 },
123 nsfw: buildNSFWFilter(res), 123 nsfw: buildNSFWFilter(res),
124 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 124 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
125 withFiles: false,
126 countVideos: false, 125 countVideos: false,
127 126
128 ...where 127 ...where
diff --git a/server/controllers/api/users/my-subscriptions.ts b/server/controllers/api/users/my-subscriptions.ts
index d96378180..6799ca8c5 100644
--- a/server/controllers/api/users/my-subscriptions.ts
+++ b/server/controllers/api/users/my-subscriptions.ts
@@ -181,7 +181,6 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res
181 orLocalVideos: false 181 orLocalVideos: false
182 }, 182 },
183 nsfw: buildNSFWFilter(res, query.nsfw), 183 nsfw: buildNSFWFilter(res, query.nsfw),
184 withFiles: false,
185 user, 184 user,
186 countVideos 185 countVideos
187 }) 186 })
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index f9c1a405d..d1a1e6473 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -347,7 +347,6 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
347 347
348 displayOnlyForFollower, 348 displayOnlyForFollower,
349 nsfw: buildNSFWFilter(res, query.nsfw), 349 nsfw: buildNSFWFilter(res, query.nsfw),
350 withFiles: false,
351 videoChannelId: videoChannelInstance.id, 350 videoChannelId: videoChannelInstance.id,
352 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 351 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
353 countVideos 352 countVideos
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index 821ed7ff3..821161c64 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -225,7 +225,6 @@ async function listVideos (req: express.Request, res: express.Response) {
225 orLocalVideos: true 225 orLocalVideos: true
226 }, 226 },
227 nsfw: buildNSFWFilter(res, query.nsfw), 227 nsfw: buildNSFWFilter(res, query.nsfw),
228 withFiles: false,
229 user: res.locals.oauth ? res.locals.oauth.token.User : undefined, 228 user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
230 countVideos 229 countVideos
231 }, 'filter:api.videos.list.params') 230 }, 'filter:api.videos.list.params')
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts
index 9f03de7e8..2a8d6863a 100644
--- a/server/controllers/bots.ts
+++ b/server/controllers/bots.ts
@@ -76,7 +76,6 @@ async function getSitemapLocalVideoUrls () {
76 }, 76 },
77 isLocal: true, 77 isLocal: true,
78 nsfw: buildNSFWFilter(), 78 nsfw: buildNSFWFilter(),
79 withFiles: false,
80 countVideos: false 79 countVideos: false
81 }) 80 })
82 81
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 1f6aebac3..29502a154 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -2,6 +2,7 @@ import express from 'express'
2import Feed from 'pfeed' 2import Feed from 'pfeed'
3import { getServerActor } from '@server/models/application/application' 3import { getServerActor } from '@server/models/application/application'
4import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 4import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
5import { VideoInclude } from '@shared/models'
5import { buildNSFWFilter } from '../helpers/express-utils' 6import { buildNSFWFilter } from '../helpers/express-utils'
6import { CONFIG } from '../initializers/config' 7import { CONFIG } from '../initializers/config'
7import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 8import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
@@ -171,8 +172,8 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
171 }, 172 },
172 nsfw, 173 nsfw,
173 isLocal: req.query.isLocal, 174 isLocal: req.query.isLocal,
174 include: req.query.include, 175 include: req.query.include | VideoInclude.FILES,
175 withFiles: true, 176 hasFiles: true,
176 countVideos: false, 177 countVideos: false,
177 ...options 178 ...options
178 }) 179 })
@@ -204,9 +205,10 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
204 nsfw, 205 nsfw,
205 206
206 isLocal: req.query.isLocal, 207 isLocal: req.query.isLocal,
207 include: req.query.include,
208 208
209 withFiles: true, 209 hasFiles: true,
210 include: req.query.include | VideoInclude.FILES,
211
210 countVideos: false, 212 countVideos: false,
211 213
212 displayOnlyForFollower: { 214 displayOnlyForFollower: {
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 056ec6857..71a9b8ccb 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -99,7 +99,7 @@ export type SummaryOptions = {
99 queryInclude.push({ 99 queryInclude.push({
100 attributes: [ 'id' ], 100 attributes: [ 'id' ],
101 model: AccountBlocklistModel.unscoped(), 101 model: AccountBlocklistModel.unscoped(),
102 as: 'BlockedAccounts', 102 as: 'BlockedBy',
103 required: false, 103 required: false,
104 where: { 104 where: {
105 accountId: { 105 accountId: {
diff --git a/server/models/user/user-video-history.ts b/server/models/user/user-video-history.ts
index d633cc9d5..1aefdf02b 100644
--- a/server/models/user/user-video-history.ts
+++ b/server/models/user/user-video-history.ts
@@ -70,7 +70,6 @@ export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVide
70 actorId: serverActor.id, 70 actorId: serverActor.id,
71 orLocalVideos: true 71 orLocalVideos: true
72 }, 72 },
73 withFiles: false,
74 user, 73 user,
75 historyOfUser: user 74 historyOfUser: user
76 }) 75 })
diff --git a/server/models/video/formatter/video-format-utils.ts b/server/models/video/formatter/video-format-utils.ts
index 5dc2c2f1b..ba49e41ae 100644
--- a/server/models/video/formatter/video-format-utils.ts
+++ b/server/models/video/formatter/video-format-utils.ts
@@ -42,6 +42,7 @@ export type VideoFormattingJSONOptions = {
42 waitTranscoding?: boolean 42 waitTranscoding?: boolean
43 scheduledUpdate?: boolean 43 scheduledUpdate?: boolean
44 blacklistInfo?: boolean 44 blacklistInfo?: boolean
45 files?: boolean
45 blockedOwner?: boolean 46 blockedOwner?: boolean
46 } 47 }
47} 48}
@@ -55,6 +56,7 @@ function guessAdditionalAttributesFromQuery (query: VideosCommonQueryAfterSaniti
55 waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), 56 waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
56 scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE), 57 scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
57 blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED), 58 blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
59 files: !!(query.include & VideoInclude.FILES),
58 blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER) 60 blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
59 } 61 }
60 } 62 }
@@ -150,22 +152,26 @@ function videoModelToFormattedJSON (video: MVideoFormattable, options: VideoForm
150 videoObject.blockedServer = !!(server?.isBlocked()) 152 videoObject.blockedServer = !!(server?.isBlocked())
151 } 153 }
152 154
155 if (add?.files === true) {
156 videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
157 videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
158 }
159
153 return videoObject 160 return videoObject
154} 161}
155 162
156function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails { 163function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails {
157 const formattedJson = video.toFormattedJSON({ 164 const videoJSON = video.toFormattedJSON({
158 additionalAttributes: { 165 additionalAttributes: {
159 scheduledUpdate: true, 166 scheduledUpdate: true,
160 blacklistInfo: true 167 blacklistInfo: true,
168 files: true
161 } 169 }
162 }) 170 }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>>
163 171
164 const tags = video.Tags ? video.Tags.map(t => t.name) : [] 172 const tags = video.Tags ? video.Tags.map(t => t.name) : []
165 173
166 const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists) 174 const detailsJSON = {
167
168 const detailsJson = {
169 support: video.support, 175 support: video.support,
170 descriptionPath: video.getDescriptionAPIPath(), 176 descriptionPath: video.getDescriptionAPIPath(),
171 channel: video.VideoChannel.toFormattedJSON(), 177 channel: video.VideoChannel.toFormattedJSON(),
@@ -179,20 +185,14 @@ function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): Vid
179 label: getStateLabel(video.state) 185 label: getStateLabel(video.state)
180 }, 186 },
181 187
182 trackerUrls: video.getTrackerUrls(), 188 trackerUrls: video.getTrackerUrls()
183
184 files: [],
185 streamingPlaylists
186 } 189 }
187 190
188 // Format and sort video files 191 return Object.assign(videoJSON, detailsJSON)
189 detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
190
191 return Object.assign(formattedJson, detailsJson)
192} 192}
193 193
194function streamingPlaylistsModelToFormattedJSON ( 194function streamingPlaylistsModelToFormattedJSON (
195 video: MVideoFormattableDetails, 195 video: MVideoFormattable,
196 playlists: MStreamingPlaylistRedundanciesOpt[] 196 playlists: MStreamingPlaylistRedundanciesOpt[]
197): VideoStreamingPlaylist[] { 197): VideoStreamingPlaylist[] {
198 if (isArray(playlists) === false) return [] 198 if (isArray(playlists) === false) return []
@@ -223,7 +223,7 @@ function sortByResolutionDesc (fileA: MVideoFile, fileB: MVideoFile) {
223} 223}
224 224
225function videoFilesModelToFormattedJSON ( 225function videoFilesModelToFormattedJSON (
226 video: MVideoFormattableDetails, 226 video: MVideoFormattable,
227 videoFiles: MVideoFileRedundanciesOpt[], 227 videoFiles: MVideoFileRedundanciesOpt[],
228 includeMagnet = true 228 includeMagnet = true
229): VideoFile[] { 229): VideoFile[] {
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts
index d18ddae67..2f34d5602 100644
--- a/server/models/video/sql/video-model-get-query-builder.ts
+++ b/server/models/video/sql/video-model-get-query-builder.ts
@@ -32,7 +32,7 @@ export type BuildVideoGetQueryOptions = {
32 logging?: boolean 32 logging?: boolean
33} 33}
34 34
35export class VideosModelGetQueryBuilder { 35export class VideoModelGetQueryBuilder {
36 videoQueryBuilder: VideosModelGetQuerySubBuilder 36 videoQueryBuilder: VideosModelGetQuerySubBuilder
37 webtorrentFilesQueryBuilder: VideoFileQueryBuilder 37 webtorrentFilesQueryBuilder: VideoFileQueryBuilder
38 streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder 38 streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder
@@ -53,11 +53,11 @@ export class VideosModelGetQueryBuilder {
53 const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([ 53 const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
54 this.videoQueryBuilder.queryVideos(options), 54 this.videoQueryBuilder.queryVideos(options),
55 55
56 VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) 56 VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
57 ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options) 57 ? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options)
58 : Promise.resolve(undefined), 58 : Promise.resolve(undefined),
59 59
60 VideosModelGetQueryBuilder.videoFilesInclude.has(options.type) 60 VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
61 ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options) 61 ? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
62 : Promise.resolve(undefined) 62 : Promise.resolve(undefined)
63 ]) 63 ])
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 3eb547e75..4d6e0ea4b 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -43,7 +43,7 @@ export type BuildVideosListQueryOptions = {
43 43
44 uuids?: string[] 44 uuids?: string[]
45 45
46 withFiles?: boolean 46 hasFiles?: boolean
47 47
48 accountId?: number 48 accountId?: number
49 videoChannelId?: number 49 videoChannelId?: number
@@ -165,7 +165,7 @@ export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
165 this.whereFollowerActorId(options.displayOnlyForFollower) 165 this.whereFollowerActorId(options.displayOnlyForFollower)
166 } 166 }
167 167
168 if (options.withFiles === true) { 168 if (options.hasFiles === true) {
169 this.whereFileExists() 169 this.whereFileExists()
170 } 170 }
171 171
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts
index ef92bd2b0..cd721f055 100644
--- a/server/models/video/sql/videos-model-list-query-builder.ts
+++ b/server/models/video/sql/videos-model-list-query-builder.ts
@@ -52,7 +52,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder
52 this.includeAccounts() 52 this.includeAccounts()
53 this.includeThumbnails() 53 this.includeThumbnails()
54 54
55 if (options.withFiles) { 55 if (options.include & VideoInclude.FILES) {
56 this.includeWebtorrentFiles() 56 this.includeWebtorrentFiles()
57 this.includeStreamingPlaylistFiles() 57 this.includeStreamingPlaylistFiles()
58 } 58 }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 26be34329..f9618c102 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -105,7 +105,7 @@ import {
105 videoModelToFormattedJSON 105 videoModelToFormattedJSON
106} from './formatter/video-format-utils' 106} from './formatter/video-format-utils'
107import { ScheduleVideoUpdateModel } from './schedule-video-update' 107import { ScheduleVideoUpdateModel } from './schedule-video-update'
108import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' 108import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder'
109import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' 109import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
110import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' 110import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
111import { TagModel } from './tag' 111import { TagModel } from './tag'
@@ -1029,7 +1029,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1029 isLocal?: boolean 1029 isLocal?: boolean
1030 include?: VideoInclude 1030 include?: VideoInclude
1031 1031
1032 withFiles: boolean 1032 hasFiles?: boolean // default false
1033 1033
1034 categoryOneOf?: number[] 1034 categoryOneOf?: number[]
1035 licenceOneOf?: number[] 1035 licenceOneOf?: number[]
@@ -1053,7 +1053,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1053 1053
1054 search?: string 1054 search?: string
1055 }) { 1055 }) {
1056 if (options.include && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1056 if (VideoModel.isPrivateInclude(options.include) && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1057 throw new Error('Try to filter all-local but no user has not the see all videos right') 1057 throw new Error('Try to filter all-local but no user has not the see all videos right')
1058 } 1058 }
1059 1059
@@ -1082,7 +1082,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1082 'isLocal', 1082 'isLocal',
1083 'include', 1083 'include',
1084 'displayOnlyForFollower', 1084 'displayOnlyForFollower',
1085 'withFiles', 1085 'hasFiles',
1086 'accountId', 1086 'accountId',
1087 'videoChannelId', 1087 'videoChannelId',
1088 'videoPlaylistId', 1088 'videoPlaylistId',
@@ -1229,13 +1229,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1229 } 1229 }
1230 1230
1231 static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> { 1231 static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> {
1232 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1232 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1233 1233
1234 return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' }) 1234 return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' })
1235 } 1235 }
1236 1236
1237 static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> { 1237 static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> {
1238 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1238 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1239 1239
1240 return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' }) 1240 return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' })
1241 } 1241 }
@@ -1279,31 +1279,31 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1279 } 1279 }
1280 1280
1281 static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> { 1281 static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> {
1282 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1282 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1283 1283
1284 return queryBuilder.queryVideo({ id, transaction, type: 'id' }) 1284 return queryBuilder.queryVideo({ id, transaction, type: 'id' })
1285 } 1285 }
1286 1286
1287 static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> { 1287 static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
1288 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1288 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1289 1289
1290 return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging }) 1290 return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging })
1291 } 1291 }
1292 1292
1293 static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> { 1293 static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
1294 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1294 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1295 1295
1296 return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' }) 1296 return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' })
1297 } 1297 }
1298 1298
1299 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> { 1299 static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
1300 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1300 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1301 1301
1302 return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' }) 1302 return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' })
1303 } 1303 }
1304 1304
1305 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> { 1305 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
1306 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1306 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1307 1307
1308 return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId }) 1308 return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId })
1309 } 1309 }
@@ -1314,7 +1314,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1314 userId?: number 1314 userId?: number
1315 }): Promise<MVideoDetails> { 1315 }): Promise<MVideoDetails> {
1316 const { id, transaction, userId } = parameters 1316 const { id, transaction, userId } = parameters
1317 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize) 1317 const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
1318 1318
1319 return queryBuilder.queryVideo({ id, transaction, type: 'api', userId }) 1319 return queryBuilder.queryVideo({ id, transaction, type: 'api', userId })
1320 } 1320 }
@@ -1345,8 +1345,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1345 displayOnlyForFollower: { 1345 displayOnlyForFollower: {
1346 actorId: serverActor.id, 1346 actorId: serverActor.id,
1347 orLocalVideos: true 1347 orLocalVideos: true
1348 }, 1348 }
1349 withFiles: false
1350 }) 1349 })
1351 1350
1352 return { 1351 return {
@@ -1490,6 +1489,13 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1490 } 1489 }
1491 } 1490 }
1492 1491
1492 private static isPrivateInclude (include: VideoInclude) {
1493 return include & VideoInclude.BLACKLISTED ||
1494 include & VideoInclude.BLOCKED_OWNER ||
1495 include & VideoInclude.HIDDEN_PRIVACY ||
1496 include & VideoInclude.NOT_PUBLISHED_STATE
1497 }
1498
1493 isBlacklisted () { 1499 isBlacklisted () {
1494 return !!this.VideoBlacklist 1500 return !!this.VideoBlacklist
1495 } 1501 }
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
index eb2d2ab50..03c5c3b3f 100644
--- a/server/tests/api/videos/videos-common-filters.ts
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -13,7 +13,7 @@ import {
13 setDefaultVideoChannel, 13 setDefaultVideoChannel,
14 waitJobs 14 waitJobs
15} from '@shared/extra-utils' 15} from '@shared/extra-utils'
16import { HttpStatusCode, UserRole, Video, VideoInclude, VideoPrivacy } from '@shared/models' 16import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models'
17 17
18describe('Test videos filter', function () { 18describe('Test videos filter', function () {
19 let servers: PeerTubeServer[] 19 let servers: PeerTubeServer[]
@@ -365,6 +365,32 @@ describe('Test videos filter', function () {
365 await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host }) 365 await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
366 }) 366 })
367 367
368 it('Should include video files', async function () {
369 for (const path of paths) {
370 {
371 const videos = await listVideos({ server: servers[0], path })
372
373 for (const video of videos) {
374 const videoWithFiles = video as VideoDetails
375
376 expect(videoWithFiles.files).to.not.exist
377 expect(videoWithFiles.streamingPlaylists).to.not.exist
378 }
379 }
380
381 {
382 const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES })
383
384 for (const video of videos) {
385 const videoWithFiles = video as VideoDetails
386
387 expect(videoWithFiles.files).to.exist
388 expect(videoWithFiles.files).to.have.length.at.least(1)
389 }
390 }
391 }
392 })
393
368 it('Should filter by tags and category', async function () { 394 it('Should filter by tags and category', async function () {
369 await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } }) 395 await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
370 await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } }) 396 await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })
diff --git a/server/types/models/account/account.ts b/server/types/models/account/account.ts
index abe0de27b..71f6c79aa 100644
--- a/server/types/models/account/account.ts
+++ b/server/types/models/account/account.ts
@@ -23,7 +23,7 @@ type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
23 23
24export type MAccount = 24export type MAccount =
25 Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' | 25 Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
26 'VideoComments' | 'BlockedAccounts'> 26 'VideoComments' | 'BlockedBy'>
27 27
28// ############################################################################ 28// ############################################################################
29 29
@@ -84,7 +84,7 @@ export type MAccountSummary =
84 84
85export type MAccountSummaryBlocks = 85export type MAccountSummaryBlocks =
86 MAccountSummary & 86 MAccountSummary &
87 Use<'BlockedByAccounts', MAccountBlocklistId[]> 87 Use<'BlockedBy', MAccountBlocklistId[]>
88 88
89export type MAccountAPI = 89export type MAccountAPI =
90 MAccount & 90 MAccount &
diff --git a/server/types/models/server/server.ts b/server/types/models/server/server.ts
index f8b053e3b..876186fc0 100644
--- a/server/types/models/server/server.ts
+++ b/server/types/models/server/server.ts
@@ -15,7 +15,7 @@ export type MServerRedundancyAllowed = Pick<MServer, 'redundancyAllowed'>
15 15
16export type MServerHostBlocks = 16export type MServerHostBlocks =
17 MServerHost & 17 MServerHost &
18 Use<'BlockedByAccounts', MAccountBlocklistId[]> 18 Use<'BlockedBy', MAccountBlocklistId[]>
19 19
20// ############################################################################ 20// ############################################################################
21 21
diff --git a/server/types/models/video/video.ts b/server/types/models/video/video.ts
index 16ddaf740..9a6b27888 100644
--- a/server/types/models/video/video.ts
+++ b/server/types/models/video/video.ts
@@ -210,7 +210,9 @@ export type MVideoFormattable =
210 PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> & 210 PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
211 Use<'VideoChannel', MChannelAccountSummaryFormattable> & 211 Use<'VideoChannel', MChannelAccountSummaryFormattable> &
212 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> & 212 PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
213 PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>> 213 PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>> &
214 PickWithOpt<VideoModel, 'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
215 PickWithOpt<VideoModel, 'VideoFiles', MVideoFile[]>
214 216
215export type MVideoFormattableDetails = 217export type MVideoFormattableDetails =
216 MVideoFormattable & 218 MVideoFormattable &
diff --git a/shared/models/videos/video-include.enum.ts b/shared/models/videos/video-include.enum.ts
index fa720b348..72fa8cd30 100644
--- a/shared/models/videos/video-include.enum.ts
+++ b/shared/models/videos/video-include.enum.ts
@@ -3,5 +3,6 @@ export const enum VideoInclude {
3 NOT_PUBLISHED_STATE = 1 << 0, 3 NOT_PUBLISHED_STATE = 1 << 0,
4 HIDDEN_PRIVACY = 1 << 1, 4 HIDDEN_PRIVACY = 1 << 1,
5 BLACKLISTED = 1 << 2, 5 BLACKLISTED = 1 << 2,
6 BLOCKED_OWNER = 1 << 3 6 BLOCKED_OWNER = 1 << 3,
7 FILES = 1 << 4
7} 8}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index dadde38af..26cb595e7 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -62,6 +62,9 @@ export interface Video {
62 62
63 blockedOwner?: boolean 63 blockedOwner?: boolean
64 blockedServer?: boolean 64 blockedServer?: boolean
65
66 files?: VideoFile[]
67 streamingPlaylists?: VideoStreamingPlaylist[]
65} 68}
66 69
67export interface VideoDetails extends Video { 70export interface VideoDetails extends Video {
@@ -70,7 +73,6 @@ export interface VideoDetails extends Video {
70 channel: VideoChannel 73 channel: VideoChannel
71 account: Account 74 account: Account
72 tags: string[] 75 tags: string[]
73 files: VideoFile[]
74 commentsEnabled: boolean 76 commentsEnabled: boolean
75 downloadEnabled: boolean 77 downloadEnabled: boolean
76 78
@@ -80,5 +82,6 @@ export interface VideoDetails extends Video {
80 82
81 trackerUrls: string[] 83 trackerUrls: string[]
82 84
85 files: VideoFile[]
83 streamingPlaylists: VideoStreamingPlaylist[] 86 streamingPlaylists: VideoStreamingPlaylist[]
84} 87}