import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
-import { createTorrentPromise, peertubeTruncate } from '../../helpers/core-utils'
+import { peertubeTruncate } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { isArray, isBooleanValid } from '../../helpers/custom-validators/misc'
import {
CONSTRAINTS_FIELDS,
HLS_REDUNDANCY_DIRECTORY,
HLS_STREAMING_PLAYLIST_DIRECTORY,
+ LAZY_STATIC_PATHS,
REMOTE_SCHEME,
STATIC_DOWNLOAD_PATHS,
STATIC_PATHS,
} from '../utils'
import { TagModel } from './tag'
import { VideoAbuseModel } from './video-abuse'
-import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
import { VideoCommentModel } from './video-comment'
import { VideoFileModel } from './video-file'
import { VideoShareModel } from './video-share'
videoModelToFormattedJSON
} from './video-format-utils'
import { UserVideoHistoryModel } from '../account/user-video-history'
-import { UserModel } from '../account/user'
import { VideoImportModel } from './video-import'
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
import { VideoPlaylistElementModel } from './video-playlist-element'
import { CONFIG } from '../../initializers/config'
import { ThumbnailModel } from './thumbnail'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
+import { createTorrentPromise } from '../../helpers/webtorrent'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import {
+ MChannel,
+ MChannelAccountDefault,
+ MChannelId,
+ MUserAccountId,
+ MUserId,
+ MVideoAccountLight,
+ MVideoAccountLightBlacklistAllFiles,
+ MVideoDetails,
+ MVideoForUser,
+ MVideoFullLight,
+ MVideoIdThumbnail,
+ MVideoThumbnail,
+ MVideoWithAllFiles, MVideoWithFile,
+ MVideoWithRights,
+ MVideoFormattable
+} from '../../typings/models'
+import { MVideoFile, MVideoFileRedundanciesOpt } from '../../typings/models/video/video-file'
+import { MThumbnail } from '../../typings/models/video/thumbnail'
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
const indexes: (ModelIndexesOptions & { where?: WhereOptions })[] = [
WITH_FILES = 'WITH_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
+ WITH_BLOCKLIST = 'WITH_BLOCKLIST',
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
WITH_USER_ID = 'WITH_USER_ID',
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
}
-type ForAPIOptions = {
- ids: number[]
+export type ForAPIOptions = {
+ ids?: number[]
videoPlaylistId?: number
withFiles?: boolean
+
+ withAccountBlockerIds?: number[]
}
-type AvailableForListIDsOptions = {
+export type AvailableForListIDsOptions = {
serverAccountId: number
followerActorId: number
includeLocalVideos: boolean
- withoutId?: boolean
+ attributesType?: 'none' | 'id' | 'all'
filter?: VideoFilter
categoryOneOf?: number[]
videoPlaylistId?: number
trendingDays?: number
- user?: UserModel,
- historyOfUser?: UserModel
+ user?: MUserAccountId
+ historyOfUser?: MUserId
baseWhere?: WhereOptions[]
}
@Scopes(() => ({
[ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
const query: FindOptions = {
- where: {
- id: {
- [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
- }
- },
include: [
{
- model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
+ model: VideoChannelModel.scope({
+ method: [
+ VideoChannelScopeNames.SUMMARY, {
+ withAccount: true,
+ withAccountBlockerIds: options.withAccountBlockerIds
+ } as SummaryOptions
+ ]
+ }),
required: true
},
{
]
}
+ if (options.ids) {
+ query.where = {
+ id: {
+ [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
+ }
+ }
+ }
+
if (options.withFiles === true) {
query.include.push({
model: VideoFileModel.unscoped(),
const query: FindOptions = {
raw: true,
- attributes: options.withoutId === true ? [] : [ 'id' ],
include: []
}
+ const attributesType = options.attributesType || 'id'
+
+ if (attributesType === 'id') query.attributes = [ 'id' ]
+ else if (attributesType === 'none') query.attributes = [ ]
+
whereAnd.push({
id: {
[ Op.notIn ]: Sequelize.literal(
}
})
- whereAnd.push({
- channelId: {
- [ Op.notIn ]: Sequelize.literal(
- '(' +
- 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
- buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
- ')' +
- ')'
- )
- }
- })
+ if (options.serverAccountId) {
+ whereAnd.push({
+ channelId: {
+ [ Op.notIn ]: Sequelize.literal(
+ '(' +
+ 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
+ buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
+ ')' +
+ ')'
+ )
+ }
+ })
+ }
// Only list public/published videos
if (!options.filter || options.filter !== 'all-local') {
}
return query
+ },
+ [ScopeNames.WITH_BLOCKLIST]: {
+
},
[ ScopeNames.WITH_THUMBNAILS ]: {
include: [
[ ScopeNames.WITH_BLACKLISTED ]: {
include: [
{
- attributes: [ 'id', 'reason' ],
+ attributes: [ 'id', 'reason', 'unfederated' ],
model: VideoBlacklistModel,
required: false
}
@HasMany(() => VideoPlaylistElementModel, {
foreignKey: {
name: 'videoId',
- allowNull: false
+ allowNull: true
},
- onDelete: 'cascade'
+ onDelete: 'set null'
})
VideoPlaylistElements: VideoPlaylistElementModel[]
VideoCaptions: VideoCaptionModel[]
@BeforeDestroy
- static async sendDelete (instance: VideoModel, options) {
+ static async sendDelete (instance: MVideoAccountLight, options) {
if (instance.isOwned()) {
if (!instance.VideoChannel) {
instance.VideoChannel = await instance.$get('VideoChannel', {
include: [
- {
- model: AccountModel,
- include: [ ActorModel ]
- }
+ ActorModel,
+ AccountModel
],
transaction: options.transaction
- }) as VideoChannelModel
+ }) as MChannelAccountDefault
}
return sendDeleteVideo(instance, options.transaction)
return undefined
}
- static listLocal () {
+ static listLocal (): Bluebird<MVideoWithAllFiles[]> {
const query = {
where: {
remote: false
})
}
- static listUserVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) {
+ static listUserVideosForApi (accountId: number, start: number, count: number, sort: string) {
function buildBaseQuery (): FindOptions {
return {
offset: start,
ScopeNames.WITH_THUMBNAILS
]
- if (withFiles === true) {
- findQuery.include.push({
- model: VideoFileModel.unscoped(),
- required: true
- })
- }
-
return Promise.all([
VideoModel.count(countQuery),
- VideoModel.scope(findScopes).findAll(findQuery)
+ VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
]).then(([ count, rows ]) => {
return {
data: rows,
followerActorId?: number
videoPlaylistId?: number,
trendingDays?: number,
- user?: UserModel,
- historyOfUser?: UserModel
+ user?: MUserAccountId,
+ historyOfUser?: MUserId
}, countVideos = true) {
if (options.filter && options.filter === 'all-local' && !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')
tagsAllOf?: string[]
durationMin?: number // seconds
durationMax?: number // seconds
- user?: UserModel,
+ user?: MUserAccountId,
filter?: VideoFilter
}) {
const whereAnd = []
return VideoModel.getAvailableForApi(query, queryOptions)
}
- static load (id: number | string, t?: Transaction) {
+ static load (id: number | string, t?: Transaction): Bluebird<MVideoThumbnail> {
const where = buildWhereIdOrUUID(id)
const options = {
where,
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
- static loadWithRights (id: number | string, t?: Transaction) {
+ static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
const where = buildWhereIdOrUUID(id)
const options = {
where,
]).findOne(options)
}
- static loadOnlyId (id: number | string, t?: Transaction) {
+ static loadOnlyId (id: number | string, t?: Transaction): Bluebird<MVideoIdThumbnail> {
const where = buildWhereIdOrUUID(id)
const options = {
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
- static loadWithFiles (id: number, t?: Transaction, logging?: boolean) {
+ static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Bluebird<MVideoWithAllFiles> {
+ const where = buildWhereIdOrUUID(id)
+
+ const query = {
+ where,
+ transaction: t,
+ logging
+ }
+
return VideoModel.scope([
ScopeNames.WITH_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
ScopeNames.WITH_THUMBNAILS
- ]).findByPk(id, { transaction: t, logging })
+ ]).findOne(query)
}
- static loadByUUIDWithFile (uuid: string) {
+ static loadByUUID (uuid: string): Bluebird<MVideoThumbnail> {
const options = {
where: {
uuid
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
}
- static loadByUrl (url: string, transaction?: Transaction) {
+ static loadByUrl (url: string, transaction?: Transaction): Bluebird<MVideoThumbnail> {
const query: FindOptions = {
where: {
url
return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
}
- static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction) {
+ static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Bluebird<MVideoAccountLightBlacklistAllFiles> {
const query: FindOptions = {
where: {
url
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_FILES,
ScopeNames.WITH_STREAMING_PLAYLISTS,
- ScopeNames.WITH_THUMBNAILS
+ ScopeNames.WITH_THUMBNAILS,
+ ScopeNames.WITH_BLACKLISTED
]).findOne(query)
}
- static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number) {
+ static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Bluebird<MVideoFullLight> {
const where = buildWhereIdOrUUID(id)
const options = {
.findOne(options)
}
- static loadForGetAPI (id: number | string, t?: Transaction, userId?: number) {
+ static loadForGetAPI (parameters: {
+ id: number | string,
+ t?: Transaction,
+ userId?: number
+ }): Bluebird<MVideoDetails> {
+ const { id, t, userId } = parameters
const where = buildWhereIdOrUUID(id)
const options = {
.then(results => results.length === 1)
}
- static bulkUpdateSupportField (videoChannel: VideoChannelModel, t: Transaction) {
+ static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
const options = {
where: {
channelId: videoChannel.id
return VideoModel.update({ support: videoChannel.support }, options)
}
- static getAllIdsFromChannel (videoChannel: VideoChannelModel) {
+ static getAllIdsFromChannel (videoChannel: MChannelId): Bluebird<number[]> {
const query = {
attributes: [ 'id' ],
where: {
serverAccountId: serverActor.Account.id,
followerActorId,
includeLocalVideos: true,
- withoutId: true // Don't break aggregation
+ attributesType: 'none' // Don't break aggregation
}
const query: FindOptions = {
return VIDEO_STATES[ id ] || 'Unknown'
}
- getOriginalFile () {
+ isBlacklisted () {
+ return !!this.VideoBlacklist
+ }
+
+ isBlocked () {
+ return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
+ this.VideoChannel.Account.isBlocked()
+ }
+
+ getOriginalFile <T extends MVideoWithFile> (this: T) {
if (Array.isArray(this.VideoFiles) === false) return undefined
// The original file is the file that have the higher resolution
return maxBy(this.VideoFiles, file => file.resolution)
}
- async addAndSaveThumbnail (thumbnail: ThumbnailModel, transaction: Transaction) {
+ getFile <T extends MVideoWithFile> (this: T, resolution: number) {
+ if (Array.isArray(this.VideoFiles) === false) return undefined
+
+ return this.VideoFiles.find(f => f.resolution === resolution)
+ }
+
+ async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
thumbnail.videoId = this.id
const savedThumbnail = await thumbnail.save({ transaction })
this.Thumbnails.push(savedThumbnail)
}
- getVideoFilename (videoFile: VideoFileModel) {
+ getVideoFilename (videoFile: MVideoFile) {
return this.uuid + '-' + videoFile.resolution + videoFile.extname
}
return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
}
- getTorrentFileName (videoFile: VideoFileModel) {
+ getTorrentFileName (videoFile: MVideoFile) {
const extension = '.torrent'
return this.uuid + '-' + videoFile.resolution + extension
}
return this.remote === false
}
- getTorrentFilePath (videoFile: VideoFileModel) {
+ getTorrentFilePath (videoFile: MVideoFile) {
return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
}
- getVideoFilePath (videoFile: VideoFileModel) {
+ getVideoFilePath (videoFile: MVideoFile) {
return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
}
- async createTorrentAndSetInfoHash (videoFile: VideoFileModel) {
+ async createTorrentAndSetInfoHash (videoFile: MVideoFile) {
const options = {
// Keep the extname, it's used by the client to stream the file inside a web browser
name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
if (!preview) return null
// We use a local cache, so specify our cache endpoint instead of potential remote URL
- return join(STATIC_PATHS.PREVIEWS, preview.filename)
+ return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
}
- toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
+ toFormattedJSON <T extends MVideoFormattable> (this: T, options?: VideoFormattingJSONOptions): Video {
return videoModelToFormattedJSON(this, options)
}
return `/api/${API_VERSION}/videos/${this.uuid}/description`
}
- removeFile (videoFile: VideoFileModel, isRedundancy = false) {
+ getHLSPlaylist () {
+ if (!this.VideoStreamingPlaylists) return undefined
+
+ return this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
+ }
+
+ removeFile (videoFile: MVideoFile, isRedundancy = false) {
const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
const filePath = join(baseDir, this.getVideoFilename(videoFile))
.catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
}
- removeTorrent (videoFile: VideoFileModel) {
+ removeTorrent (videoFile: MVideoFile) {
const torrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
return remove(torrentPath)
.catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
return { baseUrlHttp, baseUrlWs }
}
- generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
+ generateMagnetUri (videoFile: MVideoFileRedundanciesOpt, baseUrlHttp: string, baseUrlWs: string) {
const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
}
- getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+ getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
}
- getTorrentDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+ getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
}
- getVideoFileUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+ getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile)
}
- getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+ getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile)
}
- getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
+ getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
}
- getBandwidthBits (videoFile: VideoFileModel) {
+ getBandwidthBits (videoFile: MVideoFile) {
return Math.ceil((videoFile.size * 8) / this.duration)
}
}