Table,
UpdatedAt
} from 'sequelize-typescript'
+import { setAsUpdated } from '@server/helpers/database-utils'
import { buildNSFWFilter } from '@server/helpers/express-utils'
import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
import { LiveManager } from '@server/lib/live-manager'
-import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
import { getServerActor } from '@server/models/application/application'
import { ModelCache } from '@server/models/model-cache'
import { VideoFile } from '@shared/models/videos/video-file.model'
import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
import { VideoObject } from '../../../shared/models/activitypub/objects'
-import { Video, VideoDetails } from '../../../shared/models/videos'
+import { Video, VideoDetails, VideoRateType } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
API_VERSION,
CONSTRAINTS_FIELDS,
LAZY_STATIC_PATHS,
- REMOTE_SCHEME,
- STATIC_DOWNLOAD_PATHS,
STATIC_PATHS,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
MStreamingPlaylistFilesVideo,
MUserAccountId,
MUserId,
+ MVideo,
MVideoAccountLight,
MVideoAccountLightBlacklistAllFiles,
MVideoAP,
import { VideoAbuseModel } from '../abuse/video-abuse'
import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate'
+import { ActorImageModel } from '../account/actor-image'
import { UserModel } from '../account/user'
import { UserVideoHistoryModel } from '../account/user-video-history'
import { ActorModel } from '../activitypub/actor'
-import { AvatarModel } from '../avatar/avatar'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server'
+import { TrackerModel } from '../server/tracker'
+import { VideoTrackerModel } from '../server/video-tracker'
import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { TagModel } from './tag'
FOR_API = 'FOR_API',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
+ WITH_TRACKERS = 'WITH_TRACKERS',
WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
required: false
},
{
- model: AvatarModel.unscoped(),
+ model: ActorImageModel.unscoped(),
+ as: 'Avatar',
required: false
}
]
required: false
},
{
- model: AvatarModel.unscoped(),
+ model: ActorImageModel.unscoped(),
+ as: 'Avatar',
required: false
}
]
[ScopeNames.WITH_TAGS]: {
include: [ TagModel ]
},
+ [ScopeNames.WITH_TRACKERS]: {
+ include: [
+ {
+ attributes: [ 'id', 'url' ],
+ model: TrackerModel
+ }
+ ]
+ },
[ScopeNames.WITH_BLACKLISTED]: {
include: [
{
include: [
{
model: VideoFileModel,
- separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
+ separate: true,
required: false,
include: subInclude
}
include: [
{
model: VideoStreamingPlaylistModel.unscoped(),
- separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
required: false,
+ separate: true,
include: subInclude
}
]
]
},
{ fields: [ 'duration' ] },
- { fields: [ 'views' ] },
+ {
+ fields: [
+ { name: 'views', order: 'DESC' },
+ { name: 'id', order: 'ASC' }
+ ]
+ },
{ fields: [ 'channelId' ] },
{
fields: [ 'originallyPublishedAt' ],
})
Tags: TagModel[]
+ @BelongsToMany(() => TrackerModel, {
+ foreignKey: 'videoId',
+ through: () => VideoTrackerModel,
+ onDelete: 'CASCADE'
+ })
+ Trackers: TrackerModel[]
+
@HasMany(() => ThumbnailModel, {
foreignKey: {
name: 'videoId',
@BeforeDestroy
static async sendDelete (instance: MVideoAccountLight, options) {
- if (instance.isOwned()) {
- if (!instance.VideoChannel) {
- instance.VideoChannel = await instance.$get('VideoChannel', {
- include: [
- ActorModel,
- AccountModel
- ],
- transaction: options.transaction
- }) as MChannelAccountDefault
- }
+ if (!instance.isOwned()) return undefined
- return sendDeleteVideo(instance, options.transaction)
+ // Lazy load channels
+ if (!instance.VideoChannel) {
+ instance.VideoChannel = await instance.$get('VideoChannel', {
+ include: [
+ ActorModel,
+ AccountModel
+ ],
+ transaction: options.transaction
+ }) as MChannelAccountDefault
}
- return undefined
+ return sendDeleteVideo(instance, options.transaction)
}
@BeforeDestroy
// Remove physical files and torrents
instance.VideoFiles.forEach(file => {
tasks.push(instance.removeFile(file))
- tasks.push(instance.removeTorrent(file))
+ tasks.push(file.removeTorrent())
})
// Remove playlists file
logger.info('Saving video abuses details of video %s.', instance.url)
+ if (!instance.Trackers) instance.Trackers = await instance.$get('Trackers', { transaction: options.transaction })
const details = instance.toFormattedDetailsJSON()
for (const abuse of instance.VideoAbuses) {
return undefined
}
- static listLocal (): Promise<MVideoWithAllFiles[]> {
+ static listLocal (): Promise<MVideo[]> {
const query = {
where: {
remote: false
}
}
- return VideoModel.scope([
- ScopeNames.WITH_WEBTORRENT_FILES,
- ScopeNames.WITH_STREAMING_PLAYLISTS,
- ScopeNames.WITH_THUMBNAILS
- ]).findAll(query)
+ return VideoModel.findAll(query)
}
static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
},
include: [
{
- attributes: [ 'language', 'fileUrl' ],
+ attributes: [ 'filename', 'language', 'fileUrl' ],
model: VideoCaptionModel.unscoped(),
required: false
},
start: number
count: number
sort: string
+ isLive?: boolean
search?: string
}) {
- const { accountId, start, count, sort, search } = options
+ const { accountId, start, count, sort, search, isLive } = options
function buildBaseQuery (): FindOptions {
- let baseQuery = {
+ const where: WhereOptions = {}
+
+ if (search) {
+ where.name = {
+ [Op.iLike]: '%' + search + '%'
+ }
+ }
+
+ if (isLive) {
+ where.isLive = isLive
+ }
+
+ const baseQuery = {
offset: start,
limit: count,
+ where,
order: getVideoSort(sort),
include: [
{
]
}
- if (search) {
- baseQuery = Object.assign(baseQuery, {
- where: {
- name: {
- [Op.iLike]: '%' + search + '%'
- }
- }
- })
- }
-
return baseQuery
}
start: number
count: number
sort: string
+
nsfw: boolean
+ filter?: VideoFilter
+ isLive?: boolean
+
includeLocalVideos: boolean
withFiles: boolean
+
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
- filter?: VideoFilter
+
accountId?: number
videoChannelId?: number
+
followerActorId?: number
+
videoPlaylistId?: number
+
trendingDays?: number
+
user?: MUserAccountId
historyOfUser?: MUserId
+
countVideos?: boolean
+
search?: string
}) {
if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
const trendingDays = options.sort.endsWith('trending')
? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
: undefined
- const hot = options.sort.endsWith('hot')
+ let trendingAlgorithm
+ if (options.sort.endsWith('hot')) trendingAlgorithm = 'hot'
+ if (options.sort.endsWith('best')) trendingAlgorithm = 'best'
const serverActor = await getServerActor()
followerActorId,
serverAccountId: serverActor.Account.id,
nsfw: options.nsfw,
+ isLive: options.isLive,
categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf,
languageOneOf: options.languageOneOf,
user: options.user,
historyOfUser: options.historyOfUser,
trendingDays,
- hot,
+ trendingAlgorithm,
search: options.search
}
originallyPublishedStartDate?: string
originallyPublishedEndDate?: string
nsfw?: boolean
+ isLive?: boolean
categoryOneOf?: number[]
licenceOneOf?: number[]
languageOneOf?: string[]
filter?: VideoFilter
}) {
const serverActor = await getServerActor()
+
const queryOptions = {
followerActorId: serverActor.id,
serverAccountId: serverActor.Account.id,
+
includeLocalVideos: options.includeLocalVideos,
nsfw: options.nsfw,
+ isLive: options.isLive,
+
categoryOneOf: options.categoryOneOf,
licenceOneOf: options.licenceOneOf,
languageOneOf: options.languageOneOf,
+
tagsOneOf: options.tagsOneOf,
tagsAllOf: options.tagsAllOf,
+
user: options.user,
filter: options.filter,
+
start: options.start,
count: options.count,
sort: options.sort,
+
startDate: options.startDate,
endDate: options.endDate,
+
originallyPublishedStartDate: options.originallyPublishedStartDate,
originallyPublishedEndDate: options.originallyPublishedEndDate,
return VideoModel.scope([
ScopeNames.WITH_BLACKLISTED,
- ScopeNames.WITH_USER_ID,
- ScopeNames.WITH_THUMBNAILS
+ ScopeNames.WITH_USER_ID
]).findOne(options)
}
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS,
ScopeNames.WITH_LIVE,
+ ScopeNames.WITH_TRACKERS,
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
]
})
}
+ static updateRatesOf (videoId: number, type: VideoRateType, t: Transaction) {
+ const field = type === 'like'
+ ? 'likes'
+ : 'dislikes'
+
+ const rawQuery = `UPDATE "video" SET "${field}" = ` +
+ '(' +
+ 'SELECT COUNT(id) FROM "accountVideoRate" WHERE "accountVideoRate"."videoId" = "video"."id" AND type = :rateType' +
+ ') ' +
+ 'WHERE "video"."id" = :videoId'
+
+ return AccountVideoRateModel.sequelize.query(rawQuery, {
+ transaction: t,
+ replacements: { videoId, rateType: type },
+ type: QueryTypes.UPDATE
+ })
+ }
+
static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
// Instances only share videos
const query = 'SELECT 1 FROM "videoShare" ' +
'resolution',
'size',
'extname',
+ 'filename',
+ 'fileUrl',
+ 'torrentFilename',
+ 'torrentUrl',
'infoHash',
'fps',
'videoId',
'createdAt',
'updatedAt'
]
+ const buildOpts = { raw: true }
function buildActor (rowActor: any) {
const avatarModel = rowActor.Avatar.id !== null
- ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
+ ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
: null
const serverModel = rowActor.Server.id !== null
- ? new ServerModel(pick(rowActor.Server, serverKeys))
+ ? new ServerModel(pick(rowActor.Server, serverKeys), buildOpts)
: null
- const actorModel = new ActorModel(pick(rowActor, actorKeys))
+ const actorModel = new ActorModel(pick(rowActor, actorKeys), buildOpts)
actorModel.Avatar = avatarModel
actorModel.Server = serverModel
if (!videosMemo[row.id]) {
// Build Channel
const channel = row.VideoChannel
- const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
+ const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]), buildOpts)
channelModel.Actor = buildActor(channel.Actor)
const account = row.VideoChannel.Account
- const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
+ const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]), buildOpts)
accountModel.Actor = buildActor(account.Actor)
channelModel.Account = accountModel
- const videoModel = new VideoModel(pick(row, videoKeys))
+ const videoModel = new VideoModel(pick(row, videoKeys), buildOpts)
videoModel.VideoChannel = channelModel
videoModel.UserVideoHistories = []
const videoModel = videosMemo[row.id]
if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
- const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
+ const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]), buildOpts)
videoModel.UserVideoHistories.push(historyModel)
historyDone.add(row.userVideoHistory.id)
}
if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
- const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
+ const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]), buildOpts)
videoModel.Thumbnails.push(thumbnailModel)
thumbnailsDone.add(row.Thumbnails.id)
}
if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
- const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
+ const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys), buildOpts)
videoModel.VideoFiles.push(videoFileModel)
videoFilesDone.add(row.VideoFiles.id)
}
if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
- const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys))
+ const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys), buildOpts)
streamingPlaylist.VideoFiles = []
videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
- const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys))
+ const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys), buildOpts)
streamingPlaylist.VideoFiles.push(videoFileModel)
videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
this.Thumbnails.push(savedThumbnail)
}
- generateThumbnailName () {
- return this.uuid + '.jpg'
- }
-
getMiniature () {
if (Array.isArray(this.Thumbnails) === false) return undefined
return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
}
- generatePreviewName () {
- return this.uuid + '.jpg'
- }
-
hasPreview () {
return !!this.getPreview()
}
return videoModelToFormattedDetailsJSON(this)
}
- getFormattedVideoFilesJSON (): VideoFile[] {
- const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
+ getFormattedVideoFilesJSON (includeMagnet = true): VideoFile[] {
let files: VideoFile[] = []
if (Array.isArray(this.VideoFiles)) {
- const result = videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, this.VideoFiles)
+ const result = videoFilesModelToFormattedJSON(this, this.VideoFiles, includeMagnet)
files = files.concat(result)
}
for (const p of (this.VideoStreamingPlaylists || [])) {
- p.Video = this
-
- const result = videoFilesModelToFormattedJSON(p, baseUrlHttp, baseUrlWs, p.VideoFiles)
+ const result = videoFilesModelToFormattedJSON(this, p.VideoFiles, includeMagnet)
files = files.concat(result)
}
.catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
}
- removeTorrent (videoFile: MVideoFile) {
- const torrentPath = getTorrentFilePath(this, videoFile)
- return remove(torrentPath)
- .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
- }
-
async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
const directoryPath = getHLSDirectory(this, isRedundancy)
// Remove physical files and torrents
await Promise.all(
- streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
+ streamingPlaylistWithFiles.VideoFiles.map(file => file.removeTorrent())
)
}
}
}
setAsRefreshed () {
- this.changed('updatedAt', true)
-
- return this.save()
+ return setAsUpdated('video', this.id)
}
requiresAuth () {
return false
}
- getBaseUrls () {
- if (this.isOwned()) {
- return {
- baseUrlHttp: WEBSERVER.URL,
- baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
- }
- }
-
- return {
- baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
- baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
- }
- }
-
- getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
- return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
- }
-
- getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
- }
-
- getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
- }
-
- getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
- }
-
- getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- const path = '/api/v1/videos/'
-
- return this.isOwned()
- ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
- : videoFile.metadataUrl
- }
-
- getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
+ getBandwidthBits (videoFile: MVideoFile) {
+ return Math.ceil((videoFile.size * 8) / this.duration)
}
- getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
- return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
- }
+ getTrackerUrls () {
+ if (this.isOwned()) {
+ return [
+ WEBSERVER.URL + '/tracker/announce',
+ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket'
+ ]
+ }
- getBandwidthBits (videoFile: MVideoFile) {
- return Math.ceil((videoFile.size * 8) / this.duration)
+ return this.Trackers.map(t => t.url)
}
}