<th style="width: 60px;"></th>
<th i18n>Video</th>
<th i18n>Info</th>
+ <th i18n>Files</th>
<th style="width: 150px;" i18n pSortableColumn="publishedAt">Published <p-sortIcon field="publishedAt"></p-sortIcon></th>
</tr>
</ng-template>
<my-video-cell [video]="video"></my-video-cell>
</td>
- <td class="badges">
- <span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge" i18n>{{ video.privacy.label }}</span>
+ <td>
+ <span [ngClass]="getPrivacyBadgeClass(video.privacy.id)" class="badge">{{ video.privacy.label }}</span>
<span *ngIf="video.nsfw" class="badge badge-red" i18n>NSFW</span>
<span *ngIf="isVideoBlocked(video)" class="badge badge-red" i18n>Blocked</span>
</td>
+ <td>
+ <span *ngIf="isHLS(video)" class="badge badge-blue">HLS</span>
+ <span *ngIf="isWebTorrent(video)" class="badge badge-blue">WebTorrent</span>
+
+ <span *ngIf="!video.remote">{{ getFilesSize(video) | bytes: 1 }}</span>
+ </td>
+
<td>
{{ video.publishedAt | date: 'short' }}
</td>
<ng-template pTemplate="rowexpansion" let-video>
<tr>
- <td colspan="50">
- <my-embed [video]="video"></my-embed>
+ <td class="video-info expand-cell" colspan="7">
+ <div>
+ <div *ngIf="isWebTorrent(video)">
+ WebTorrent:
+
+ <ul>
+ <li *ngFor="let file of video.files">
+ {{ file.resolution.label }}: {{ file.size | bytes: 1 }}
+ </li>
+ </ul>
+ </div>
+
+ <div *ngIf="isHLS(video)">
+ HLS:
+
+ <ul>
+ <li *ngFor="let file of video.streamingPlaylists[0].files">
+ {{ file.resolution.label }}: {{ file.size | bytes: 1 }}
+ </li>
+ </ul>
+ </div>
+
+ <my-embed class="ml-auto" [video]="video"></my-embed>
+ </div>
</td>
</tr>
</ng-template>
my-embed {
display: block;
max-width: 500px;
+ width: 50%;
}
.badge {
margin-right: 5px;
}
+
+.video-info > div {
+ display: flex;
+}
import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, ConfirmService, Notifier, RestPagination, RestTable } from '@app/core'
import { DropdownAction, Video, VideoService } from '@app/shared/shared-main'
-import { UserRight, VideoPrivacy, VideoState } from '@shared/models'
+import { UserRight, VideoPrivacy, VideoState, VideoStreamingPlaylistType } from '@shared/models'
import { AdvancedInputFilter } from '@app/shared/shared-forms'
import { VideoActionsDisplayType } from '@app/shared/shared-video-miniature'
return video.blacklisted
}
+ isHLS (video: Video) {
+ return video.streamingPlaylists.some(p => p.type === VideoStreamingPlaylistType.HLS)
+ }
+
+ isWebTorrent (video: Video) {
+ return video.files.length !== 0
+ }
+
+ getFilesSize (video: Video) {
+ let files = video.files
+
+ if (this.isHLS(video)) {
+ files = files.concat(video.streamingPlaylists[0].files)
+ }
+
+ return files.reduce((p, f) => p += f.size, 0)
+ }
+
protected reloadData () {
this.selectedVideos = []
support: string
channel: VideoChannel
tags: string[]
- files: VideoFile[]
account: Account
commentsEnabled: boolean
downloadEnabled: boolean
trackerUrls: string[]
+ files: VideoFile[]
streamingPlaylists: VideoStreamingPlaylist[]
constructor (hash: VideoDetailsServerModel, translations = {}) {
super(hash, translations)
this.descriptionPath = hash.descriptionPath
- this.files = hash.files
this.channel = new VideoChannel(hash.channel)
this.account = new Account(hash.account)
this.tags = hash.tags
this.downloadEnabled = hash.downloadEnabled
this.trackerUrls = hash.trackerUrls
- this.streamingPlaylists = hash.streamingPlaylists
this.buildLikeAndDislikePercents()
}
UserRight,
Video as VideoServerModel,
VideoConstant,
+ VideoFile,
VideoPrivacy,
VideoScheduleUpdate,
- VideoState
+ VideoState,
+ VideoStreamingPlaylist
} from '@shared/models'
export class Video implements VideoServerModel {
pluginData?: any
+ streamingPlaylists?: VideoStreamingPlaylist[]
+ files?: VideoFile[]
+
static buildWatchUrl (video: Partial<Pick<Video, 'uuid' | 'shortUUID'>>) {
return buildVideoWatchPath({ shortUUID: video.shortUUID || video.uuid })
}
this.blockedOwner = hash.blockedOwner
this.blockedServer = hash.blockedServer
+ this.streamingPlaylists = hash.streamingPlaylists
+ this.files = hash.files
+
this.userHistory = hash.userHistory
this.originInstanceHost = this.account.host
): Observable<ResultList<Video>> {
const { pagination, search } = parameters
- const include = VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER | VideoInclude.HIDDEN_PRIVACY | VideoInclude.NOT_PUBLISHED_STATE
+ const include = VideoInclude.BLACKLISTED |
+ VideoInclude.BLOCKED_OWNER |
+ VideoInclude.HIDDEN_PRIVACY |
+ VideoInclude.NOT_PUBLISHED_STATE |
+ VideoInclude.FILES
let params = new HttpParams()
params = this.buildCommonVideosParams({ params, include, ...parameters })
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
- withFiles: false,
accountId: account.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
},
nsfw: buildNSFWFilter(res),
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
- withFiles: false,
countVideos: false,
...where
orLocalVideos: false
},
nsfw: buildNSFWFilter(res, query.nsfw),
- withFiles: false,
user,
countVideos
})
displayOnlyForFollower,
nsfw: buildNSFWFilter(res, query.nsfw),
- withFiles: false,
videoChannelId: videoChannelInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
orLocalVideos: true
},
nsfw: buildNSFWFilter(res, query.nsfw),
- withFiles: false,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined,
countVideos
}, 'filter:api.videos.list.params')
},
isLocal: true,
nsfw: buildNSFWFilter(),
- withFiles: false,
countVideos: false
})
import Feed from 'pfeed'
import { getServerActor } from '@server/models/application/application'
import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
+import { VideoInclude } from '@shared/models'
import { buildNSFWFilter } from '../helpers/express-utils'
import { CONFIG } from '../initializers/config'
import { FEEDS, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
},
nsfw,
isLocal: req.query.isLocal,
- include: req.query.include,
- withFiles: true,
+ include: req.query.include | VideoInclude.FILES,
+ hasFiles: true,
countVideos: false,
...options
})
nsfw,
isLocal: req.query.isLocal,
- include: req.query.include,
- withFiles: true,
+ hasFiles: true,
+ include: req.query.include | VideoInclude.FILES,
+
countVideos: false,
displayOnlyForFollower: {
queryInclude.push({
attributes: [ 'id' ],
model: AccountBlocklistModel.unscoped(),
- as: 'BlockedAccounts',
+ as: 'BlockedBy',
required: false,
where: {
accountId: {
actorId: serverActor.id,
orLocalVideos: true
},
- withFiles: false,
user,
historyOfUser: user
})
waitTranscoding?: boolean
scheduledUpdate?: boolean
blacklistInfo?: boolean
+ files?: boolean
blockedOwner?: boolean
}
}
waitTranscoding: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
scheduledUpdate: !!(query.include & VideoInclude.NOT_PUBLISHED_STATE),
blacklistInfo: !!(query.include & VideoInclude.BLACKLISTED),
+ files: !!(query.include & VideoInclude.FILES),
blockedOwner: !!(query.include & VideoInclude.BLOCKED_OWNER)
}
}
videoObject.blockedServer = !!(server?.isBlocked())
}
+ if (add?.files === true) {
+ videoObject.streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
+ videoObject.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
+ }
+
return videoObject
}
function videoModelToFormattedDetailsJSON (video: MVideoFormattableDetails): VideoDetails {
- const formattedJson = video.toFormattedJSON({
+ const videoJSON = video.toFormattedJSON({
additionalAttributes: {
scheduledUpdate: true,
- blacklistInfo: true
+ blacklistInfo: true,
+ files: true
}
- })
+ }) as Video & Required<Pick<Video, 'files' | 'streamingPlaylists'>>
const tags = video.Tags ? video.Tags.map(t => t.name) : []
- const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
-
- const detailsJson = {
+ const detailsJSON = {
support: video.support,
descriptionPath: video.getDescriptionAPIPath(),
channel: video.VideoChannel.toFormattedJSON(),
label: getStateLabel(video.state)
},
- trackerUrls: video.getTrackerUrls(),
-
- files: [],
- streamingPlaylists
+ trackerUrls: video.getTrackerUrls()
}
- // Format and sort video files
- detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
-
- return Object.assign(formattedJson, detailsJson)
+ return Object.assign(videoJSON, detailsJSON)
}
function streamingPlaylistsModelToFormattedJSON (
- video: MVideoFormattableDetails,
+ video: MVideoFormattable,
playlists: MStreamingPlaylistRedundanciesOpt[]
): VideoStreamingPlaylist[] {
if (isArray(playlists) === false) return []
}
function videoFilesModelToFormattedJSON (
- video: MVideoFormattableDetails,
+ video: MVideoFormattable,
videoFiles: MVideoFileRedundanciesOpt[],
includeMagnet = true
): VideoFile[] {
logging?: boolean
}
-export class VideosModelGetQueryBuilder {
+export class VideoModelGetQueryBuilder {
videoQueryBuilder: VideosModelGetQuerySubBuilder
webtorrentFilesQueryBuilder: VideoFileQueryBuilder
streamingPlaylistFilesQueryBuilder: VideoFileQueryBuilder
const [ videoRows, webtorrentFilesRows, streamingPlaylistFilesRows ] = await Promise.all([
this.videoQueryBuilder.queryVideos(options),
- VideosModelGetQueryBuilder.videoFilesInclude.has(options.type)
+ VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
? this.webtorrentFilesQueryBuilder.queryWebTorrentVideos(options)
: Promise.resolve(undefined),
- VideosModelGetQueryBuilder.videoFilesInclude.has(options.type)
+ VideoModelGetQueryBuilder.videoFilesInclude.has(options.type)
? this.streamingPlaylistFilesQueryBuilder.queryStreamingPlaylistVideos(options)
: Promise.resolve(undefined)
])
uuids?: string[]
- withFiles?: boolean
+ hasFiles?: boolean
accountId?: number
videoChannelId?: number
this.whereFollowerActorId(options.displayOnlyForFollower)
}
- if (options.withFiles === true) {
+ if (options.hasFiles === true) {
this.whereFileExists()
}
this.includeAccounts()
this.includeThumbnails()
- if (options.withFiles) {
+ if (options.include & VideoInclude.FILES) {
this.includeWebtorrentFiles()
this.includeStreamingPlaylistFiles()
}
videoModelToFormattedJSON
} from './formatter/video-format-utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
-import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
+import { VideoModelGetQueryBuilder } from './sql/video-model-get-query-builder'
import { BuildVideosListQueryOptions, DisplayOnlyForFollowerOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
import { TagModel } from './tag'
isLocal?: boolean
include?: VideoInclude
- withFiles: boolean
+ hasFiles?: boolean // default false
categoryOneOf?: number[]
licenceOneOf?: number[]
search?: string
}) {
- if (options.include && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
+ if (VideoModel.isPrivateInclude(options.include) && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
throw new Error('Try to filter all-local but no user has not the see all videos right')
}
'isLocal',
'include',
'displayOnlyForFollower',
- 'withFiles',
+ 'hasFiles',
'accountId',
'videoChannelId',
'videoPlaylistId',
}
static load (id: number | string, transaction?: Transaction): Promise<MVideoThumbnail> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails' })
}
static loadWithBlacklist (id: number | string, transaction?: Transaction): Promise<MVideoThumbnailBlacklist> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'thumbnails-blacklist' })
}
}
static loadOnlyId (id: number | string, transaction?: Transaction): Promise<MVideoId> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'id' })
}
static loadWithFiles (id: number | string, transaction?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'all-files', logging })
}
static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ url, transaction, type: 'thumbnails' })
}
static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ url, transaction, type: 'account-blacklist-files' })
}
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction: t, type: 'full-light', userId })
}
userId?: number
}): Promise<MVideoDetails> {
const { id, transaction, userId } = parameters
- const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
+ const queryBuilder = new VideoModelGetQueryBuilder(VideoModel.sequelize)
return queryBuilder.queryVideo({ id, transaction, type: 'api', userId })
}
displayOnlyForFollower: {
actorId: serverActor.id,
orLocalVideos: true
- },
- withFiles: false
+ }
})
return {
}
}
+ private static isPrivateInclude (include: VideoInclude) {
+ return include & VideoInclude.BLACKLISTED ||
+ include & VideoInclude.BLOCKED_OWNER ||
+ include & VideoInclude.HIDDEN_PRIVACY ||
+ include & VideoInclude.NOT_PUBLISHED_STATE
+ }
+
isBlacklisted () {
return !!this.VideoBlacklist
}
setDefaultVideoChannel,
waitJobs
} from '@shared/extra-utils'
-import { HttpStatusCode, UserRole, Video, VideoInclude, VideoPrivacy } from '@shared/models'
+import { HttpStatusCode, UserRole, Video, VideoDetails, VideoInclude, VideoPrivacy } from '@shared/models'
describe('Test videos filter', function () {
let servers: PeerTubeServer[]
await servers[0].blocklist.removeFromServerBlocklist({ server: servers[1].host })
})
+ it('Should include video files', async function () {
+ for (const path of paths) {
+ {
+ const videos = await listVideos({ server: servers[0], path })
+
+ for (const video of videos) {
+ const videoWithFiles = video as VideoDetails
+
+ expect(videoWithFiles.files).to.not.exist
+ expect(videoWithFiles.streamingPlaylists).to.not.exist
+ }
+ }
+
+ {
+ const videos = await listVideos({ server: servers[0], path, include: VideoInclude.FILES })
+
+ for (const video of videos) {
+ const videoWithFiles = video as VideoDetails
+
+ expect(videoWithFiles.files).to.exist
+ expect(videoWithFiles.files).to.have.length.at.least(1)
+ }
+ }
+ }
+ })
+
it('Should filter by tags and category', async function () {
await servers[0].videos.upload({ attributes: { name: 'tag filter', tags: [ 'tag1', 'tag2' ] } })
await servers[0].videos.upload({ attributes: { name: 'tag filter with category', tags: [ 'tag3' ], category: 4 } })
export type MAccount =
Omit<AccountModel, 'Actor' | 'User' | 'Application' | 'VideoChannels' | 'VideoPlaylists' |
- 'VideoComments' | 'BlockedAccounts'>
+ 'VideoComments' | 'BlockedBy'>
// ############################################################################
export type MAccountSummaryBlocks =
MAccountSummary &
- Use<'BlockedByAccounts', MAccountBlocklistId[]>
+ Use<'BlockedBy', MAccountBlocklistId[]>
export type MAccountAPI =
MAccount &
export type MServerHostBlocks =
MServerHost &
- Use<'BlockedByAccounts', MAccountBlocklistId[]>
+ Use<'BlockedBy', MAccountBlocklistId[]>
// ############################################################################
PickWithOpt<VideoModel, 'UserVideoHistories', MUserVideoHistoryTime[]> &
Use<'VideoChannel', MChannelAccountSummaryFormattable> &
PickWithOpt<VideoModel, 'ScheduleVideoUpdate', Pick<MScheduleVideoUpdate, 'updateAt' | 'privacy'>> &
- PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>>
+ PickWithOpt<VideoModel, 'VideoBlacklist', Pick<MVideoBlacklist, 'reason'>> &
+ PickWithOpt<VideoModel, 'VideoStreamingPlaylists', MStreamingPlaylistFiles[]> &
+ PickWithOpt<VideoModel, 'VideoFiles', MVideoFile[]>
export type MVideoFormattableDetails =
MVideoFormattable &
NOT_PUBLISHED_STATE = 1 << 0,
HIDDEN_PRIVACY = 1 << 1,
BLACKLISTED = 1 << 2,
- BLOCKED_OWNER = 1 << 3
+ BLOCKED_OWNER = 1 << 3,
+ FILES = 1 << 4
}
blockedOwner?: boolean
blockedServer?: boolean
+
+ files?: VideoFile[]
+ streamingPlaylists?: VideoStreamingPlaylist[]
}
export interface VideoDetails extends Video {
channel: VideoChannel
account: Account
tags: string[]
- files: VideoFile[]
commentsEnabled: boolean
downloadEnabled: boolean
trackerUrls: string[]
+ files: VideoFile[]
streamingPlaylists: VideoStreamingPlaylist[]
}