Add playlist rest tests

pull/1745/head
Chocobozzz 2019-03-05 10:58:44 +01:00 committed by Chocobozzz
parent 07b1a18aa6
commit df0b219d36
35 changed files with 1485 additions and 756 deletions

View File

@ -320,7 +320,10 @@ async function videoRedundancyController (req: express.Request, res: express.Res
async function videoPlaylistController (req: express.Request, res: express.Response) {
const playlist: VideoPlaylistModel = res.locals.videoPlaylist
const json = await playlist.toActivityPubObject()
// We need more attributes
playlist.OwnerAccount = await AccountModel.load(playlist.ownerAccountId)
const json = await playlist.toActivityPubObject(req.query.page, null)
const audience = getAudience(playlist.OwnerAccount.Actor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
const object = audiencify(json, audience)

View File

@ -18,6 +18,7 @@ import { JobQueue } from '../../lib/job-queue'
import { logger } from '../../helpers/logger'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
import { UserModel } from '../../models/account/user'
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
const accountsRouter = express.Router()
@ -57,6 +58,7 @@ accountsRouter.get('/:accountName/video-playlists',
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
asyncMiddleware(listAccountPlaylists)
)
@ -106,7 +108,8 @@ async function listAccountPlaylists (req: express.Request, res: express.Response
count: req.query.count,
sort: req.query.sort,
accountId: res.locals.account.id,
privateAndUnlisted
privateAndUnlisted,
type: req.query.playlistType
})
return res.json(getFormattedObjects(resultList.data, resultList.total))

View File

@ -6,7 +6,7 @@ import { getFormattedObjects } from '../../../helpers/utils'
import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
import { Emailer } from '../../../lib/emailer'
import { Redis } from '../../../lib/redis'
import { createUserAccountAndChannel } from '../../../lib/user'
import { createUserAccountAndChannelAndPlaylist } from '../../../lib/user'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
@ -174,7 +174,7 @@ async function createUser (req: express.Request, res: express.Response) {
videoQuotaDaily: body.videoQuotaDaily
})
const { user, account } = await createUserAccountAndChannel(userToCreate)
const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate)
auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
logger.info('User %s with its channel and account created.', body.username)
@ -205,7 +205,7 @@ async function registerUser (req: express.Request, res: express.Response) {
emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
})
const { user } = await createUserAccountAndChannel(userToCreate)
const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate)
auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
logger.info('User %s with its channel and account registered.', body.username)

View File

@ -33,6 +33,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils'
import { UserModel } from '../../models/account/user'
import { JobQueue } from '../../lib/job-queue'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
import { commonVideoPlaylistFiltersValidator } from '../../middlewares/validators/videos/video-playlists'
const auditLogger = auditLoggerFactory('channels')
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR })
@ -85,6 +86,7 @@ videoChannelRouter.get('/:nameWithHost/video-playlists',
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
asyncMiddleware(listVideoChannelPlaylists)
)
@ -197,6 +199,8 @@ async function removeVideoChannel (req: express.Request, res: express.Response)
const videoChannelInstance: VideoChannelModel = res.locals.videoChannel
await sequelizeTypescript.transaction(async t => {
await VideoPlaylistModel.resetPlaylistsOfChannel(videoChannelInstance.id, t)
await videoChannelInstance.destroy({ transaction: t })
auditLogger.delete(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannelInstance.toFormattedJSON()))
@ -225,7 +229,8 @@ async function listVideoChannelPlaylists (req: express.Request, res: express.Res
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
videoChannelId: res.locals.videoChannel.id
videoChannelId: res.locals.videoChannel.id,
type: req.query.playlistType
})
return res.json(getFormattedObjects(resultList.data, resultList.total))

View File

@ -17,6 +17,7 @@ import { logger } from '../../helpers/logger'
import { resetSequelizeInstance } from '../../helpers/database-utils'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
import {
commonVideoPlaylistFiltersValidator,
videoPlaylistsAddValidator,
videoPlaylistsAddVideoValidator,
videoPlaylistsDeleteValidator,
@ -45,6 +46,7 @@ import { VideoPlaylistElementModel } from '../../models/video/video-playlist-ele
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
import { copy, pathExists } from 'fs-extra'
import { AccountModel } from '../../models/account/account'
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT, { thumbnailfile: CONFIG.STORAGE.TMP_DIR })
@ -55,6 +57,7 @@ videoPlaylistRouter.get('/',
videoPlaylistsSortValidator,
setDefaultSort,
setDefaultPagination,
commonVideoPlaylistFiltersValidator,
asyncMiddleware(listVideoPlaylists)
)
@ -130,7 +133,8 @@ async function listVideoPlaylists (req: express.Request, res: express.Response)
followerActorId: serverActor.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort
sort: req.query.sort,
type: req.query.type
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
@ -171,7 +175,8 @@ async function addVideoPlaylist (req: express.Request, res: express.Response) {
const videoPlaylistCreated: VideoPlaylistModel = await sequelizeTypescript.transaction(async t => {
const videoPlaylistCreated = await videoPlaylist.save({ transaction: t })
videoPlaylistCreated.OwnerAccount = user.Account
// We need more attributes for the federation
videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
return videoPlaylistCreated
@ -216,6 +221,7 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
const videoChannel = res.locals.videoChannel as VideoChannelModel
videoPlaylistInstance.videoChannelId = videoChannel.id
videoPlaylistInstance.VideoChannel = videoChannel
}
}
@ -227,6 +233,8 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
}
const playlistUpdated = await videoPlaylistInstance.save(sequelizeOptions)
// We need more attributes for the federation
playlistUpdated.OwnerAccount = await AccountModel.load(playlistUpdated.OwnerAccount.id, t)
const isNewPlaylist = wasPrivatePlaylist && playlistUpdated.privacy !== VideoPlaylistPrivacy.PRIVATE
@ -290,11 +298,15 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
if (await pathExists(playlistThumbnailPath) === false) {
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
await copy(videoThumbnailPath, playlistThumbnailPath)
}
}
// We need more attributes for the federation
videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
await sendUpdateVideoPlaylist(videoPlaylist, t)
return playlistElement
@ -320,6 +332,8 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
const element = await videoPlaylistElement.save({ transaction: t })
// We need more attributes for the federation
videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
await sendUpdateVideoPlaylist(videoPlaylist, t)
return element
@ -341,6 +355,8 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
// Decrease position of the next elements
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t)
// We need more attributes for the federation
videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
await sendUpdateVideoPlaylist(videoPlaylist, t)
logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
@ -382,6 +398,8 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
// Decrease positions of elements after the old position of our ordered elements (decrease)
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t)
// We need more attributes for the federation
videoPlaylist.OwnerAccount = await AccountModel.load(videoPlaylist.OwnerAccount.id, t)
await sendUpdateVideoPlaylist(videoPlaylist, t)
})
@ -415,5 +433,6 @@ async function getVideoPlaylistVideos (req: express.Request, res: express.Respon
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
})
return res.json(getFormattedObjects(resultList.data, resultList.total))
const additionalAttributes = { playlistInfo: true }
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
}

View File

@ -1,4 +1,4 @@
import { exists } from '../misc'
import { exists, isDateValid } from '../misc'
import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
import * as validator from 'validator'
import { PlaylistElementObject } from '../../../../shared/models/activitypub/objects/playlist-element-object'
@ -7,7 +7,9 @@ import { isActivityPubUrlValid } from './misc'
function isPlaylistObjectValid (object: PlaylistObject) {
return exists(object) &&
object.type === 'Playlist' &&
validator.isInt(object.totalItems + '')
validator.isInt(object.totalItems + '') &&
isDateValid(object.published) &&
isDateValid(object.updated)
}
function isPlaylistElementObjectValid (object: PlaylistElementObject) {

View File

@ -1,9 +1,8 @@
import { exists } from './misc'
import * as validator from 'validator'
import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
import { CONSTRAINTS_FIELDS, VIDEO_PLAYLIST_PRIVACIES, VIDEO_PLAYLIST_TYPES } from '../../initializers'
import * as express from 'express'
import { VideoPlaylistModel } from '../../models/video/video-playlist'
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
const PLAYLISTS_CONSTRAINT_FIELDS = CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS
@ -19,8 +18,16 @@ function isVideoPlaylistPrivacyValid (value: number) {
return validator.isInt(value + '') && VIDEO_PLAYLIST_PRIVACIES[ value ] !== undefined
}
function isVideoPlaylistTimestampValid (value: any) {
return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
}
function isVideoPlaylistTypeValid (value: any) {
return exists(value) && VIDEO_PLAYLIST_TYPES[ value ] !== undefined
}
async function isVideoPlaylistExist (id: number | string, res: express.Response) {
const videoPlaylist = await VideoPlaylistModel.load(id, undefined)
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
if (!videoPlaylist) {
res.status(404)
@ -40,5 +47,7 @@ export {
isVideoPlaylistExist,
isVideoPlaylistNameValid,
isVideoPlaylistDescriptionValid,
isVideoPlaylistPrivacyValid
isVideoPlaylistPrivacyValid,
isVideoPlaylistTimestampValid,
isVideoPlaylistTypeValid
}

View File

@ -11,6 +11,7 @@ import { invert } from 'lodash'
import { CronRepeatOptions, EveryRepeatOptions } from 'bull'
import * as bytes from 'bytes'
import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
// Use a variable to reload the configuration if we need
let config: IConfig = require('config')
@ -522,6 +523,11 @@ const VIDEO_PLAYLIST_PRIVACIES = {
[VideoPlaylistPrivacy.PRIVATE]: 'Private'
}
const VIDEO_PLAYLIST_TYPES = {
[VideoPlaylistType.REGULAR]: 'Regular',
[VideoPlaylistType.WATCH_LATER]: 'Watch later'
}
const MIMETYPES = {
VIDEO: {
MIMETYPE_EXT: buildVideoMimetypeExt(),
@ -778,6 +784,7 @@ export {
STATIC_MAX_AGE,
STATIC_PATHS,
VIDEO_IMPORT_TIMEOUT,
VIDEO_PLAYLIST_TYPES,
ACTIVITY_PUB,
ACTIVITY_PUB_ACTOR_TYPES,
THUMBNAILS_SIZE,

View File

@ -1,7 +1,7 @@
import * as passwordGenerator from 'password-generator'
import { UserRole } from '../../shared'
import { logger } from '../helpers/logger'
import { createApplicationActor, createUserAccountAndChannel } from '../lib/user'
import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
import { UserModel } from '../models/account/user'
import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
@ -141,7 +141,7 @@ async function createOAuthAdminIfNotExist () {
}
const user = new UserModel(userData)
await createUserAccountAndChannel(user, validatePassword)
await createUserAccountAndChannelAndPlaylist(user, validatePassword)
logger.info('Username: ' + username)
logger.info('User password: ' + password)
}

View File

@ -28,7 +28,9 @@ function playlistObjectToDBAttributes (playlistObject: PlaylistObject, byAccount
url: playlistObject.id,
uuid: playlistObject.uuid,
ownerAccountId: byAccount.id,
videoChannelId: null
videoChannelId: null,
createdAt: new Date(playlistObject.published),
updatedAt: new Date(playlistObject.updated)
}
}

View File

@ -8,6 +8,7 @@ import { VideoModel } from '../../../models/video/video'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { forwardVideoRelatedActivity } from '../send/utils'
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
async function processDeleteActivity (activity: ActivityDelete, byActor: ActorModel) {
const objectUrl = typeof activity.object === 'string' ? activity.object : activity.object.id
@ -45,6 +46,15 @@ async function processDeleteActivity (activity: ActivityDelete, byActor: ActorMo
}
}
{
const videoPlaylist = await VideoPlaylistModel.loadByUrlAndPopulateAccount(objectUrl)
if (videoPlaylist) {
if (videoPlaylist.isOwned()) throw new Error(`Remote instance cannot delete owned playlist ${videoPlaylist.url}.`)
return retryTransactionWrapper(processDeleteVideoPlaylist, byActor, videoPlaylist)
}
}
return undefined
}
@ -70,6 +80,20 @@ async function processDeleteVideo (actor: ActorModel, videoToDelete: VideoModel)
logger.info('Remote video with uuid %s removed.', videoToDelete.uuid)
}
async function processDeleteVideoPlaylist (actor: ActorModel, playlistToDelete: VideoPlaylistModel) {
logger.debug('Removing remote video playlist "%s".', playlistToDelete.uuid)
await sequelizeTypescript.transaction(async t => {
if (playlistToDelete.OwnerAccount.Actor.id !== actor.id) {
throw new Error('Account ' + actor.url + ' does not own video playlist ' + playlistToDelete.url)
}
await playlistToDelete.destroy({ transaction: t })
})
logger.info('Remote video playlist with uuid %s removed.', playlistToDelete.uuid)
}
async function processDeleteAccount (accountToRemove: AccountModel) {
logger.debug('Removing remote account "%s".', accountToRemove.Actor.uuid)

View File

@ -45,7 +45,7 @@ async function sendCreateVideoPlaylist (playlist: VideoPlaylistModel, t: Transac
const byActor = playlist.OwnerAccount.Actor
const audience = getAudience(byActor, playlist.privacy === VideoPlaylistPrivacy.PUBLIC)
const object = await playlist.toActivityPubObject()
const object = await playlist.toActivityPubObject(null, t)
const createActivity = buildCreateActivity(playlist.url, byActor, object, audience)
const serverActor = await getServerActor()

View File

@ -31,7 +31,12 @@ async function sendDeleteActor (byActor: ActorModel, t: Transaction) {
const url = getDeleteActivityPubUrl(byActor.url)
const activity = buildDeleteActivity(url, byActor.url, byActor)
const actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t)
const actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
// In case the actor did not have any videos
const serverActor = await getServerActor()
actorsInvolved.push(serverActor)
actorsInvolved.push(byActor)
return broadcastToFollowers(activity, byActor, actorsInvolved, t)

View File

@ -52,7 +52,7 @@ async function sendUpdateActor (accountOrChannel: AccountModel | VideoChannelMod
let actorsInvolved: ActorModel[]
if (accountOrChannel instanceof AccountModel) {
// Actors that shared my videos are involved too
actorsInvolved = await VideoShareModel.loadActorsByVideoOwner(byActor.id, t)
actorsInvolved = await VideoShareModel.loadActorsWhoSharedVideosOf(byActor.id, t)
} else {
// Actors that shared videos of my channel are involved too
actorsInvolved = await VideoShareModel.loadActorsByVideoChannel(accountOrChannel.id, t)
@ -87,7 +87,7 @@ async function sendUpdateVideoPlaylist (videoPlaylist: VideoPlaylistModel, t: Tr
const url = getUpdateActivityPubUrl(videoPlaylist.url, videoPlaylist.updatedAt.toISOString())
const object = await videoPlaylist.toActivityPubObject()
const object = await videoPlaylist.toActivityPubObject(null, t)
const audience = getAudience(byActor, videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC)
const updateActivity = buildUpdateActivity(url, byActor, object, audience)

View File

@ -11,8 +11,9 @@ import { FilteredModelAttributes } from 'sequelize-typescript/lib/models/Model'
import { ActorModel } from '../models/activitypub/actor'
import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
import { createWatchLaterPlaylist } from './video-playlist'
async function createUserAccountAndChannel (userToCreate: UserModel, validateUser = true) {
async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, validateUser = true) {
const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
const userOptions = {
transaction: t,
@ -38,7 +39,9 @@ async function createUserAccountAndChannel (userToCreate: UserModel, validateUse
}
const videoChannel = await createVideoChannel(videoChannelInfo, accountCreated, t)
return { user: userCreated, account: accountCreated, videoChannel }
const videoPlaylist = await createWatchLaterPlaylist(accountCreated, t)
return { user: userCreated, account: accountCreated, videoChannel, videoPlaylist }
})
const [ accountKeys, channelKeys ] = await Promise.all([
@ -89,7 +92,7 @@ async function createApplicationActor (applicationId: number) {
export {
createApplicationActor,
createUserAccountAndChannel,
createUserAccountAndChannelAndPlaylist,
createLocalAccountWithoutKeys
}

View File

@ -0,0 +1,29 @@
import * as Sequelize from 'sequelize'
import { AccountModel } from '../models/account/account'
import { VideoPlaylistModel } from '../models/video/video-playlist'
import { VideoPlaylistPrivacy } from '../../shared/models/videos/playlist/video-playlist-privacy.model'
import { getVideoPlaylistActivityPubUrl } from './activitypub'
import { VideoPlaylistType } from '../../shared/models/videos/playlist/video-playlist-type.model'
async function createWatchLaterPlaylist (account: AccountModel, t: Sequelize.Transaction) {
const videoPlaylist = new VideoPlaylistModel({
name: 'Watch later',
privacy: VideoPlaylistPrivacy.PRIVATE,
type: VideoPlaylistType.WATCH_LATER,
ownerAccountId: account.id
})
videoPlaylist.url = getVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
await videoPlaylist.save({ transaction: t })
videoPlaylist.OwnerAccount = account
return videoPlaylist
}
// ---------------------------------------------------------------------------
export {
createWatchLaterPlaylist
}

View File

@ -1,6 +1,6 @@
import * as express from 'express'
import { body, param, ValidationChain } from 'express-validator/check'
import { UserRight, VideoPrivacy } from '../../../../shared'
import { body, param, query, ValidationChain } from 'express-validator/check'
import { UserRight } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { UserModel } from '../../../models/account/user'
import { areValidationErrors } from '../utils'
@ -11,7 +11,9 @@ import {
isVideoPlaylistDescriptionValid,
isVideoPlaylistExist,
isVideoPlaylistNameValid,
isVideoPlaylistPrivacyValid
isVideoPlaylistPrivacyValid,
isVideoPlaylistTimestampValid,
isVideoPlaylistTypeValid
} from '../../../helpers/custom-validators/video-playlists'
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
import { cleanUpReqFiles } from '../../../helpers/express-utils'
@ -20,6 +22,7 @@ import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-
import { VideoModel } from '../../../models/video/video'
import { authenticatePromiseIfNeeded } from '../../oauth'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@ -56,6 +59,12 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
.json({ error: 'Cannot set "private" a video playlist that was not private.' })
}
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
cleanUpReqFiles(req)
return res.status(409)
.json({ error: 'Cannot update a watch later playlist.' })
}
if (req.body.videoChannelId && !await isVideoChannelIdExist(req.body.videoChannelId, res)) return cleanUpReqFiles(req)
return next()
@ -72,6 +81,13 @@ const videoPlaylistsDeleteValidator = [
if (areValidationErrors(req, res)) return
if (!await isVideoPlaylistExist(req.params.playlistId, res)) return
const videoPlaylist: VideoPlaylistModel = res.locals.videoPlaylist
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
return res.status(409)
.json({ error: 'Cannot delete a watch later playlist.' })
}
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, res.locals.videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
return
}
@ -127,10 +143,10 @@ const videoPlaylistsAddVideoValidator = [
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid'),
body('startTimestamp')
.optional()
.isInt({ min: 0 }).withMessage('Should have a valid start timestamp'),
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
body('stopTimestamp')
.optional()
.isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'),
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoPlaylistsAddVideoValidator parameters', { parameters: req.params })
@ -167,10 +183,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
.custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
body('startTimestamp')
.optional()
.isInt({ min: 0 }).withMessage('Should have a valid start timestamp'),
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
body('stopTimestamp')
.optional()
.isInt({ min: 0 }).withMessage('Should have a valid stop timestamp'),
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid stop timestamp'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoPlaylistsRemoveVideoValidator parameters', { parameters: req.params })
@ -275,6 +291,20 @@ const videoPlaylistsReorderVideosValidator = [
}
]
const commonVideoPlaylistFiltersValidator = [
query('playlistType')
.optional()
.custom(isVideoPlaylistTypeValid).withMessage('Should have a valid playlist type'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking commonVideoPlaylistFiltersValidator parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
@ -287,7 +317,9 @@ export {
videoPlaylistsUpdateOrRemoveVideoValidator,
videoPlaylistsReorderVideosValidator,
videoPlaylistElementAPGetValidator
videoPlaylistElementAPGetValidator,
commonVideoPlaylistFiltersValidator
}
// ---------------------------------------------------------------------------

View File

@ -67,9 +67,9 @@ type AvailableForListOptions = {
]
})
@Scopes({
[ScopeNames.SUMMARY]: (required: boolean, withAccount: boolean) => {
[ScopeNames.SUMMARY]: (withAccount = false) => {
const base: IFindOptions<VideoChannelModel> = {
attributes: [ 'name', 'description', 'id' ],
attributes: [ 'name', 'description', 'id', 'actorId' ],
include: [
{
attributes: [ 'uuid', 'preferredUsername', 'url', 'serverId', 'avatarId' ],
@ -225,7 +225,7 @@ export class VideoChannelModel extends Model<VideoChannelModel> {
foreignKey: {
allowNull: true
},
onDelete: 'cascade',
onDelete: 'CASCADE',
hooks: true
})
VideoPlaylists: VideoPlaylistModel[]

View File

@ -20,6 +20,7 @@ import { getSort, throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS } from '../../initializers'
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
import * as validator from 'validator'
@Table({
tableName: 'videoPlaylistElement',
@ -34,10 +35,6 @@ import { PlaylistElementObject } from '../../../shared/models/activitypub/object
fields: [ 'videoPlaylistId', 'videoId' ],
unique: true
},
{
fields: [ 'videoPlaylistId', 'position' ],
unique: true
},
{
fields: [ 'url' ],
unique: true
@ -143,7 +140,7 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
return VideoPlaylistElementModel.findOne(query)
}
static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number) {
static listUrlsOfForAP (videoPlaylistId: number, start: number, count: number, t?: Sequelize.Transaction) {
const query = {
attributes: [ 'url' ],
offset: start,
@ -151,7 +148,8 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
order: getSort('position'),
where: {
videoPlaylistId
}
},
transaction: t
}
return VideoPlaylistElementModel

View File

@ -24,7 +24,14 @@ import {
isVideoPlaylistPrivacyValid
} from '../../helpers/custom-validators/video-playlists'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, THUMBNAILS_SIZE, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers'
import {
CONFIG,
CONSTRAINTS_FIELDS,
STATIC_PATHS,
THUMBNAILS_SIZE,
VIDEO_PLAYLIST_PRIVACIES,
VIDEO_PLAYLIST_TYPES
} from '../../initializers'
import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
@ -34,22 +41,25 @@ import { PlaylistObject } from '../../../shared/models/activitypub/objects/playl
import { activityPubCollectionPagination } from '../../helpers/activitypub'
import { remove } from 'fs-extra'
import { logger } from '../../helpers/logger'
import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model'
enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
WITH_ACCOUNT = 'WITH_ACCOUNT'
}
type AvailableForListOptions = {
followerActorId: number
accountId?: number,
type?: VideoPlaylistType
accountId?: number
videoChannelId?: number
privateAndUnlisted?: boolean
}
@Scopes({
[ScopeNames.WITH_VIDEOS_LENGTH]: {
[ ScopeNames.WITH_VIDEOS_LENGTH ]: {
attributes: {
include: [
[
@ -59,7 +69,15 @@ type AvailableForListOptions = {
]
}
},
[ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
[ ScopeNames.WITH_ACCOUNT ]: {
include: [
{
model: () => AccountModel,
required: true
}
]
},
[ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY ]: {
include: [
{
model: () => AccountModel.scope(AccountScopeNames.SUMMARY),
@ -71,7 +89,7 @@ type AvailableForListOptions = {
}
]
},
[ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
[ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
// Only list local playlists OR playlists that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
const actorWhere = {
@ -107,6 +125,12 @@ type AvailableForListOptions = {
})
}
if (options.type) {
whereAnd.push({
type: options.type
})
}
const where = {
[Sequelize.Op.and]: whereAnd
}
@ -179,6 +203,11 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
@Column(DataType.UUID)
uuid: string
@AllowNull(false)
@Default(VideoPlaylistType.REGULAR)
@Column
type: VideoPlaylistType
@ForeignKey(() => AccountModel)
@Column
ownerAccountId: number
@ -208,13 +237,10 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
name: 'videoPlaylistId',
allowNull: false
},
onDelete: 'cascade'
onDelete: 'CASCADE'
})
VideoPlaylistElements: VideoPlaylistElementModel[]
// Calculated field
videosLength?: number
@BeforeDestroy
static async removeFiles (instance: VideoPlaylistModel) {
logger.info('Removing files of video playlist %s.', instance.url)
@ -227,6 +253,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
start: number,
count: number,
sort: string,
type?: VideoPlaylistType,
accountId?: number,
videoChannelId?: number,
privateAndUnlisted?: boolean
@ -242,6 +269,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
method: [
ScopeNames.AVAILABLE_FOR_LIST,
{
type: options.type,
followerActorId: options.followerActorId,
accountId: options.accountId,
videoChannelId: options.videoChannelId,
@ -289,7 +317,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
.then(e => !!e)
}
static load (id: number | string, transaction: Sequelize.Transaction) {
static loadWithAccountAndChannel (id: number | string, transaction: Sequelize.Transaction) {
const where = buildWhereIdOrUUID(id)
const query = {
@ -298,14 +326,39 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
}
return VideoPlaylistModel
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH ])
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH ])
.findOne(query)
}
static loadByUrlAndPopulateAccount (url: string) {
const query = {
where: {
url
}
}
return VideoPlaylistModel.scope(ScopeNames.WITH_ACCOUNT).findOne(query)
}
static getPrivacyLabel (privacy: VideoPlaylistPrivacy) {
return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
}
static getTypeLabel (type: VideoPlaylistType) {
return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
}
static resetPlaylistsOfChannel (videoChannelId: number, transaction: Sequelize.Transaction) {
const query = {
where: {
videoChannelId
},
transaction
}
return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
}
getThumbnailName () {
const extension = '.jpg'
@ -345,7 +398,12 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
thumbnailPath: this.getThumbnailStaticPath(),
videosLength: this.videosLength,
type: {
id: this.type,
label: VideoPlaylistModel.getTypeLabel(this.type)
},
videosLength: this.get('videosLength'),
createdAt: this.createdAt,
updatedAt: this.updatedAt,
@ -355,18 +413,20 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
}
}
toActivityPubObject (): Promise<PlaylistObject> {
toActivityPubObject (page: number, t: Sequelize.Transaction): Promise<PlaylistObject> {
const handler = (start: number, count: number) => {
return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count)
return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
}
return activityPubCollectionPagination(this.url, handler, null)
return activityPubCollectionPagination(this.url, handler, page)
.then(o => {
return Object.assign(o, {
type: 'Playlist' as 'Playlist',
name: this.name,
content: this.description,
uuid: this.uuid,
published: this.createdAt.toISOString(),
updated: this.updatedAt.toISOString(),
attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
icon: {
type: 'Image' as 'Image',

View File

@ -125,7 +125,7 @@ export class VideoShareModel extends Model<VideoShareModel> {
.then(res => res.map(r => r.Actor))
}
static loadActorsByVideoOwner (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
static loadActorsWhoSharedVideosOf (actorOwnerId: number, t: Sequelize.Transaction): Bluebird<ActorModel[]> {
const query = {
attributes: [],
include: [

View File

@ -225,7 +225,7 @@ type AvailableForListIDsOptions = {
},
include: [
{
model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY)
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] })
}
]
}
@ -1535,18 +1535,7 @@ export class VideoModel extends Model<VideoModel> {
if (ids.length === 0) return { data: [], total: count }
// FIXME: typings
const apiScope: any[] = [
{
method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
}
]
if (options.user) {
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
}
const secondQuery = {
const secondQuery: IFindOptions<VideoModel> = {
offset: 0,
limit: query.limit,
attributes: query.attributes,
@ -1556,6 +1545,29 @@ export class VideoModel extends Model<VideoModel> {
)
]
}
// FIXME: typing
const apiScope: any[] = []
if (options.user) {
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
// Even if the relation is n:m, we know that a user only have 0..1 video history
// So we won't have multiple rows for the same video
// A subquery adds some bugs in our query so disable it
secondQuery.subQuery = false
}
apiScope.push({
method: [
ScopeNames.FOR_API, {
ids, withFiles:
options.withFiles,
videoPlaylistId: options.videoPlaylistId
} as ForAPIOptions
]
})
const rows = await VideoModel.scope(apiScope).findAll(secondQuery)
return {

View File

@ -16,6 +16,7 @@ import './video-captions'
import './video-channels'
import './video-comments'
import './video-imports'
import './video-playlists'
import './videos'
import './videos-filter'
import './videos-history'

File diff suppressed because it is too large Load Diff

View File

@ -2,152 +2,768 @@
import * as chai from 'chai'
import 'mocha'
import { join } from 'path'
import * as request from 'supertest'
import { VideoPrivacy } from '../../../../shared/models/videos'
import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
import {
addVideoChannel,
checkTmpIsEmpty,
checkVideoFilesWereRemoved,
completeVideoCheck,
addVideoInPlaylist,
checkPlaylistFilesWereRemoved,
createUser,
dateIsValid,
createVideoPlaylist,
deleteVideoChannel,
deleteVideoPlaylist,
doubleFollow,
flushAndRunMultipleServers,
flushTests,
getLocalVideos,
getVideo,
getVideoChannelsList,
getVideosList,
getAccountPlaylistsList,
getAccountPlaylistsListWithToken,
getPlaylistVideos,
getVideoChannelPlaylistsList,
getVideoPlaylist,
getVideoPlaylistsList,
getVideoPlaylistWithToken,
killallServers,
rateVideo,
removeVideo,
removeUser,
removeVideoFromPlaylist,
reorderVideosPlaylist,
ServerInfo,
setAccessTokensToServers,
setDefaultVideoChannel,
testImage,
updateVideo,
unfollow,
updateVideoPlaylist,
updateVideoPlaylistElement,
uploadVideo,
uploadVideoAndGetId,
userLogin,
viewVideo,
wait,
webtorrentAdd
waitJobs
} from '../../../../shared/utils'
import {
addVideoCommentReply,
addVideoCommentThread,
deleteVideoComment,
getVideoCommentThreads,
getVideoThreadComments
} from '../../../../shared/utils/videos/video-comments'
import { waitJobs } from '../../../../shared/utils/server/jobs'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model'
import { Video } from '../../../../shared/models/videos'
import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
const expect = chai.expect
describe('Test video playlists', function () {
let servers: ServerInfo[] = []
let playlistServer2Id1: number
let playlistServer2Id2: number
let playlistServer2UUID2: number
let playlistServer1Id: number
let playlistServer1UUID: string
let nsfwVideoServer1: number
before(async function () {
this.timeout(120000)
servers = await flushAndRunMultipleServers(3)
servers = await flushAndRunMultipleServers(3, { transcoding: { enabled: false } })
// Get the access tokens
await setAccessTokensToServers(servers)
await setDefaultVideoChannel(servers)
// Server 1 and server 2 follow each other
await doubleFollow(servers[0], servers[1])
// Server 1 and server 3 follow each other
await doubleFollow(servers[0], servers[2])
{
const serverPromises: Promise<any>[][] = []
for (const server of servers) {
const videoPromises: Promise<any>[] = []
for (let i = 0; i < 7; i++) {
videoPromises.push(
uploadVideo(server.url, server.accessToken, { name: `video ${i} server ${server.serverNumber}`, nsfw: false })
.then(res => res.body.video)
)
}
serverPromises.push(videoPromises)
}
servers[0].videos = await Promise.all(serverPromises[0])
servers[1].videos = await Promise.all(serverPromises[1])
servers[2].videos = await Promise.all(serverPromises[2])
}
nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id
await waitJobs(servers)
})
it('Should list watch later playlist', async function () {
const url = servers[ 0 ].url
const accessToken = servers[ 0 ].accessToken
{
const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
const playlist: VideoPlaylist = res.body.data[ 0 ]
expect(playlist.displayName).to.equal('Watch later')
expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER)
expect(playlist.type.label).to.equal('Watch later')
}
{
const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR)
expect(res.body.total).to.equal(0)
expect(res.body.data).to.have.lengthOf(0)
}
{
const res = await getAccountPlaylistsList(url, 'root', 0, 5)
expect(res.body.total).to.equal(0)
expect(res.body.data).to.have.lengthOf(0)
}
})
it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
this.timeout(30000)
await createVideoPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistAttrs: {
displayName: 'my super playlist',
privacy: VideoPlaylistPrivacy.PUBLIC,
description: 'my super description',
thumbnailfile: 'thumbnail.jpg',
videoChannelId: servers[0].videoChannel.id
}
})
await waitJobs(servers)
for (const server of servers) {
const res = await getVideoPlaylistsList(server.url, 0, 5)
expect(res.body.total).to.equal(1)
expect(res.body.data).to.have.lengthOf(1)
const playlistFromList = res.body.data[0] as VideoPlaylist
const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid)
const playlistFromGet = res2.body
for (const playlist of [ playlistFromGet, playlistFromList ]) {
expect(playlist.id).to.be.a('number')
expect(playlist.uuid).to.be.a('string')
expect(playlist.isLocal).to.equal(server.serverNumber === 1)
expect(playlist.displayName).to.equal('my super playlist')
expect(playlist.description).to.equal('my super description')
expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
expect(playlist.privacy.label).to.equal('Public')
expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
expect(playlist.type.label).to.equal('Regular')
expect(playlist.videosLength).to.equal(0)
expect(playlist.ownerAccount.name).to.equal('root')
expect(playlist.ownerAccount.displayName).to.equal('root')
expect(playlist.videoChannel.name).to.equal('root_channel')
expect(playlist.videoChannel.displayName).to.equal('Main root channel')
}
}
})
it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () {
// create 2 playlists (with videos and no videos)
// With thumbnail and no thumbnail
this.timeout(30000)
{
const res = await createVideoPlaylist({
url: servers[1].url,
token: servers[1].accessToken,
playlistAttrs: {
displayName: 'playlist 2',
privacy: VideoPlaylistPrivacy.PUBLIC
}
})
playlistServer2Id1 = res.body.videoPlaylist.id
}
{
const res = await createVideoPlaylist({
url: servers[ 1 ].url,
token: servers[ 1 ].accessToken,
playlistAttrs: {
displayName: 'playlist 3',
privacy: VideoPlaylistPrivacy.PUBLIC,
thumbnailfile: 'thumbnail.jpg'
}
})
playlistServer2Id2 = res.body.videoPlaylist.id
playlistServer2UUID2 = res.body.videoPlaylist.uuid
}
for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) {
await addVideoInPlaylist({
url: servers[ 1 ].url,
token: servers[ 1 ].accessToken,
playlistId: id,
elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 }
})
await addVideoInPlaylist({
url: servers[ 1 ].url,
token: servers[ 1 ].accessToken,
playlistId: id,
elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id }
})
}
await waitJobs(servers)
for (const server of [ servers[0], servers[1] ]) {
const res = await getVideoPlaylistsList(server.url, 0, 5)
const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
expect(playlist2).to.not.be.undefined
await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath)
const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3')
expect(playlist3).to.not.be.undefined
await testImage(server.url, 'thumbnail', playlist3.thumbnailPath)
}
const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined
expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined
})
it('Should have the playlist on server 3 after a new follow', async function () {
this.timeout(30000)
// Server 2 and server 3 follow each other
await doubleFollow(servers[1], servers[2])
const res = await getVideoPlaylistsList(servers[2].url, 0, 5)
const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2')
expect(playlist2).to.not.be.undefined
await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath)
expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined
})
it('Should create some playlists and list them correctly', async function () {
// create 3 playlists with some videos in it
// check pagination
// check sort
// check empty
it('Should correctly list the playlists', async function () {
this.timeout(30000)
{
const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt')
expect(res.body.total).to.equal(3)
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(2)
expect(data[ 0 ].displayName).to.equal('playlist 2')
expect(data[ 1 ].displayName).to.equal('playlist 3')
}
{
const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt')
expect(res.body.total).to.equal(3)
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(2)
expect(data[ 0 ].displayName).to.equal('playlist 2')
expect(data[ 1 ].displayName).to.equal('my super playlist')
}
})
it('Should list video channel playlists', async function () {
// check pagination
// check sort
// check empty
this.timeout(30000)
{
const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt')
expect(res.body.total).to.equal(1)
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(1)
expect(data[ 0 ].displayName).to.equal('my super playlist')
}
})
it('Should list account playlists', async function () {
// check pagination
// check sort
// check empty
this.timeout(30000)
{
const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt')
expect(res.body.total).to.equal(2)
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(1)
expect(data[ 0 ].displayName).to.equal('playlist 2')
}
{
const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt')
expect(res.body.total).to.equal(2)
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(1)
expect(data[ 0 ].displayName).to.equal('playlist 3')
}
})
it('Should get a playlist', async function () {
// get empty playlist
// get non empty playlist
it('Should not list unlisted or private playlists', async function () {
this.timeout(30000)
await createVideoPlaylist({
url: servers[ 1 ].url,
token: servers[ 1 ].accessToken,
playlistAttrs: {
displayName: 'playlist unlisted',
privacy: VideoPlaylistPrivacy.UNLISTED
}
})
await createVideoPlaylist({
url: servers[ 1 ].url,
token: servers[ 1 ].accessToken,
playlistAttrs: {
displayName: 'playlist private',
privacy: VideoPlaylistPrivacy.PRIVATE
}
})
await waitJobs(servers)
for (const server of servers) {
const results = [
await getAccountPlaylistsList(server.url, 'root@localhost:9002', 0, 5, '-createdAt'),
await getVideoPlaylistsList(server.url, 0, 2, '-createdAt')
]
expect(results[0].body.total).to.equal(2)
expect(results[1].body.total).to.equal(3)
for (const res of results) {
const data: VideoPlaylist[] = res.body.data
expect(data).to.have.lengthOf(2)
expect(data[ 0 ].displayName).to.equal('playlist 3')
expect(data[ 1 ].displayName).to.equal('playlist 2')
}
}
})
it('Should update a playlist', async function () {
// update thumbnail
this.timeout(30000)
// update other details
await updateVideoPlaylist({
url: servers[1].url,
token: servers[1].accessToken,
playlistAttrs: {
displayName: 'playlist 3 updated',
description: 'description updated',
privacy: VideoPlaylistPrivacy.UNLISTED,
thumbnailfile: 'thumbnail.jpg',
videoChannelId: servers[1].videoChannel.id
},
playlistId: playlistServer2Id2
})
await waitJobs(servers)
for (const server of servers) {
const res = await getVideoPlaylist(server.url, playlistServer2UUID2)
const playlist: VideoPlaylist = res.body
expect(playlist.displayName).to.equal('playlist 3 updated')
expect(playlist.description).to.equal('description updated')
expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED)
expect(playlist.privacy.label).to.equal('Unlisted')
expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
expect(playlist.type.label).to.equal('Regular')
expect(playlist.videosLength).to.equal(2)
expect(playlist.ownerAccount.name).to.equal('root')
expect(playlist.ownerAccount.displayName).to.equal('root')
expect(playlist.videoChannel.name).to.equal('root_channel')
expect(playlist.videoChannel.displayName).to.equal('Main root channel')
}
})
it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
this.timeout(30000)
const addVideo = (elementAttrs: any) => {
return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs })
}
const res = await createVideoPlaylist({
url: servers[ 0 ].url,
token: servers[ 0 ].accessToken,
playlistAttrs: {
displayName: 'playlist 4',
privacy: VideoPlaylistPrivacy.PUBLIC
}
})
playlistServer1Id = res.body.videoPlaylist.id
playlistServer1UUID = res.body.videoPlaylist.uuid
await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 })
await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 })
await addVideo({ videoId: servers[2].videos[2].uuid })
await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 })
await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 })
await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
await waitJobs(servers)
})
it('Should correctly list playlist videos', async function () {
// empty
// some filters?
this.timeout(30000)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
expect(res.body.total).to.equal(6)
const videos: Video[] = res.body.data
expect(videos).to.have.lengthOf(6)
expect(videos[0].name).to.equal('video 0 server 1')
expect(videos[0].playlistElement.position).to.equal(1)
expect(videos[0].playlistElement.startTimestamp).to.equal(15)
expect(videos[0].playlistElement.stopTimestamp).to.equal(28)
expect(videos[1].name).to.equal('video 1 server 3')
expect(videos[1].playlistElement.position).to.equal(2)
expect(videos[1].playlistElement.startTimestamp).to.equal(35)
expect(videos[1].playlistElement.stopTimestamp).to.be.null
expect(videos[2].name).to.equal('video 2 server 3')
expect(videos[2].playlistElement.position).to.equal(3)
expect(videos[2].playlistElement.startTimestamp).to.be.null
expect(videos[2].playlistElement.stopTimestamp).to.be.null
expect(videos[3].name).to.equal('video 3 server 1')
expect(videos[3].playlistElement.position).to.equal(4)
expect(videos[3].playlistElement.startTimestamp).to.be.null
expect(videos[3].playlistElement.stopTimestamp).to.equal(35)
expect(videos[4].name).to.equal('video 4 server 1')
expect(videos[4].playlistElement.position).to.equal(5)
expect(videos[4].playlistElement.startTimestamp).to.equal(45)
expect(videos[4].playlistElement.stopTimestamp).to.equal(60)
expect(videos[5].name).to.equal('NSFW video')
expect(videos[5].playlistElement.position).to.equal(6)
expect(videos[5].playlistElement.startTimestamp).to.equal(5)
expect(videos[5].playlistElement.stopTimestamp).to.be.null
const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false })
expect(res2.body.total).to.equal(5)
expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined
const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
expect(res3.body.data).to.have.lengthOf(2)
}
})
it('Should reorder the playlist', async function () {
// reorder 1 element
// reorder 3 elements
// reorder at the beginning
// reorder at the end
// reorder before/after
this.timeout(30000)
{
await reorderVideosPlaylist({
url: servers[ 0 ].url,
token: servers[ 0 ].accessToken,
playlistId: playlistServer1Id,
elementAttrs: {
startPosition: 2,
insertAfterPosition: 3
}
})
await waitJobs(servers)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const names = res.body.data.map(v => v.name)
expect(names).to.deep.equal([
'video 0 server 1',
'video 2 server 3',
'video 1 server 3',
'video 3 server 1',
'video 4 server 1',
'NSFW video'
])
}
}
{
await reorderVideosPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
elementAttrs: {
startPosition: 1,
reorderLength: 3,
insertAfterPosition: 4
}
})
await waitJobs(servers)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const names = res.body.data.map(v => v.name)
expect(names).to.deep.equal([
'video 3 server 1',
'video 0 server 1',
'video 2 server 3',
'video 1 server 3',
'video 4 server 1',
'NSFW video'
])
}
}
{
await reorderVideosPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
elementAttrs: {
startPosition: 6,
insertAfterPosition: 3
}
})
await waitJobs(servers)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const videos: Video[] = res.body.data
const names = videos.map(v => v.name)
expect(names).to.deep.equal([
'video 3 server 1',
'video 0 server 1',
'video 2 server 3',
'NSFW video',
'video 1 server 3',
'video 4 server 1'
])
for (let i = 1; i <= videos.length; i++) {
expect(videos[i - 1].playlistElement.position).to.equal(i)
}
}
}
})
it('Should update startTimestamp/endTimestamp of some elements', async function () {
this.timeout(30000)
await updateVideoPlaylistElement({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
videoId: servers[0].videos[3].uuid,
elementAttrs: {
startTimestamp: 1
}
})
await updateVideoPlaylistElement({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
videoId: servers[0].videos[4].uuid,
elementAttrs: {
stopTimestamp: null
}
})
await waitJobs(servers)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const videos: Video[] = res.body.data
expect(videos[0].name).to.equal('video 3 server 1')
expect(videos[0].playlistElement.position).to.equal(1)
expect(videos[0].playlistElement.startTimestamp).to.equal(1)
expect(videos[0].playlistElement.stopTimestamp).to.equal(35)
expect(videos[5].name).to.equal('video 4 server 1')
expect(videos[5].playlistElement.position).to.equal(6)
expect(videos[5].playlistElement.startTimestamp).to.equal(45)
expect(videos[5].playlistElement.stopTimestamp).to.be.null
}
})
it('Should delete some elements', async function () {
this.timeout(30000)
await removeVideoFromPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
videoId: servers[0].videos[3].uuid
})
await removeVideoFromPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistId: playlistServer1Id,
videoId: nsfwVideoServer1
})
await waitJobs(servers)
for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
expect(res.body.total).to.equal(4)
const videos: Video[] = res.body.data
expect(videos).to.have.lengthOf(4)
expect(videos[ 0 ].name).to.equal('video 0 server 1')
expect(videos[ 0 ].playlistElement.position).to.equal(1)
expect(videos[ 1 ].name).to.equal('video 2 server 3')
expect(videos[ 1 ].playlistElement.position).to.equal(2)
expect(videos[ 2 ].name).to.equal('video 1 server 3')
expect(videos[ 2 ].playlistElement.position).to.equal(3)
expect(videos[ 3 ].name).to.equal('video 4 server 1')
expect(videos[ 3 ].playlistElement.position).to.equal(4)
}
})
it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
this.timeout(30000)
await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id)
await waitJobs(servers)
for (const server of servers) {
await getVideoPlaylist(server.url, playlistServer1UUID, 404)
}
})
it('Should have deleted the thumbnail on server 1, 2 and 3', async function () {
this.timeout(30000)
for (const server of servers) {
await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.serverNumber)
}
})
it('Should unfollow servers 1 and 2 and hide their playlists', async function () {
this.timeout(30000)
const finder = data => data.find(p => p.displayName === 'my super playlist')
{
const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
expect(res.body.total).to.equal(2)
expect(finder(res.body.data)).to.not.be.undefined
}
await unfollow(servers[2].url, servers[2].accessToken, servers[0])
{
const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
expect(res.body.total).to.equal(1)
expect(finder(res.body.data)).to.be.undefined
}
})
it('Should delete a channel and remove the associated playlist', async function () {
it('Should delete a channel and put the associated playlist in private mode', async function () {
this.timeout(30000)
const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' })
const videoChannelId = res.body.videoChannel.id
const res2 = await createVideoPlaylist({
url: servers[0].url,
token: servers[0].accessToken,
playlistAttrs: {
displayName: 'channel playlist',
privacy: VideoPlaylistPrivacy.PUBLIC,
videoChannelId
}
})
const videoPlaylistUUID = res2.body.videoPlaylist.uuid
await waitJobs(servers)
await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel')
await waitJobs(servers)
const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID)
expect(res3.body.displayName).to.equal('channel playlist')
expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404)
})
it('Should delete an account and delete its playlists', async function () {
this.timeout(30000)
const user = { username: 'user_1', password: 'password' }
const res = await createUser(servers[0].url, servers[0].accessToken, user.username, user.password)
const userId = res.body.user.id
const userAccessToken = await userLogin(servers[0], user)
await createVideoPlaylist({
url: servers[0].url,
token: userAccessToken,
playlistAttrs: {
displayName: 'playlist to be deleted',
privacy: VideoPlaylistPrivacy.PUBLIC
}
})
await waitJobs(servers)
const finder = data => data.find(p => p.displayName === 'playlist to be deleted')
{
for (const server of [ servers[0], servers[1] ]) {
const res = await getVideoPlaylistsList(server.url, 0, 15)
expect(finder(res.body.data)).to.not.be.undefined
}
}
await removeUser(servers[0].url, userId, servers[0].accessToken)
await waitJobs(servers)
{
for (const server of [ servers[0], servers[1] ]) {
const res = await getVideoPlaylistsList(server.url, 0, 15)
expect(finder(res.body.data)).to.be.undefined
}
}
})
after(async function () {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -13,6 +13,9 @@ export interface PlaylistObject {
icon: ActivityIconObject
published: string
updated: string
orderedItems?: string[]
partOf?: string

View File

@ -0,0 +1,4 @@
export enum VideoPlaylistType {
REGULAR = 1,
WATCH_LATER = 2
}

View File

@ -1,6 +1,7 @@
import { AccountSummary } from '../../actors/index'
import { VideoChannelSummary, VideoConstant } from '..'
import { VideoPlaylistPrivacy } from './video-playlist-privacy.model'
import { VideoPlaylistType } from './video-playlist-type.model'
export interface VideoPlaylist {
id: number
@ -15,6 +16,8 @@ export interface VideoPlaylist {
videosLength: number
type: VideoConstant<VideoPlaylistType>
createdAt: Date | string
updatedAt: Date | string

View File

@ -77,6 +77,8 @@ function makeUploadRequest (options: {
Object.keys(options.fields).forEach(field => {
const value = options.fields[field]
if (value === undefined) return
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
req.field(field + '[' + i + ']', value[i])

View File

@ -6,6 +6,7 @@ import { root, wait } from '../miscs/miscs'
import { readdir, readFile } from 'fs-extra'
import { existsSync } from 'fs'
import { expect } from 'chai'
import { VideoChannel } from '../../models/videos'
interface ServerInfo {
app: ChildProcess,
@ -25,6 +26,7 @@ interface ServerInfo {
}
accessToken?: string
videoChannel?: VideoChannel
video?: {
id: number
@ -39,6 +41,8 @@ interface ServerInfo {
id: number
uuid: string
}
videos?: { id: number, uuid: string }[]
}
function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {

View File

@ -3,6 +3,7 @@ import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '..
import { UserRole } from '../../index'
import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type'
import { ServerInfo, userLogin } from '..'
function createUser (
url: string,
@ -32,6 +33,13 @@ function createUser (
.expect(specialStatus)
}
async function generateUserAccessToken (server: ServerInfo, username: string) {
const password = 'my super password'
await createUser(server.url, server.accessToken, username, password)
return userLogin(server, { username, password })
}
function registerUser (url: string, username: string, password: string, specialStatus = 204) {
const path = '/api/v1/users/register'
const body = {
@ -300,5 +308,6 @@ export {
resetPassword,
updateMyAvatar,
askSendVerifyEmail,
generateUserAccessToken,
verifyEmail
}

View File

@ -1,6 +1,8 @@
import * as request from 'supertest'
import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos'
import { updateAvatarRequest } from '../requests/requests'
import { getMyUserInformation, ServerInfo } from '..'
import { User } from '../..'
function getVideoChannelsList (url: string, start: number, count: number, sort?: string) {
const path = '/api/v1/video-channels'
@ -105,6 +107,19 @@ function updateVideoChannelAvatar (options: {
return updateAvatarRequest(Object.assign(options, { path }))
}
function setDefaultVideoChannel (servers: ServerInfo[]) {
const tasks: Promise<any>[] = []
for (const server of servers) {
const p = getMyUserInformation(server.url, server.accessToken)
.then(res => server.videoChannel = (res.body as User).videoChannels[0])
tasks.push(p)
}
return Promise.all(tasks)
}
// ---------------------------------------------------------------------------
export {
@ -114,5 +129,6 @@ export {
addVideoChannel,
updateVideoChannel,
deleteVideoChannel,
getVideoChannel
getVideoChannel,
setDefaultVideoChannel
}

View File

@ -4,6 +4,12 @@ import { omit } from 'lodash'
import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
import { videoUUIDToId } from './videos'
import { join } from 'path'
import { root } from '..'
import { readdir } from 'fs-extra'
import { expect } from 'chai'
import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
const path = '/api/v1/video-playlists'
@ -17,7 +23,67 @@ function getVideoPlaylistsList (url: string, start: number, count: number, sort?
return makeGetRequest({
url,
path,
query
query,
statusCodeExpected: 200
})
}
function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
const query = {
start,
count,
sort
}
return makeGetRequest({
url,
path,
query,
statusCodeExpected: 200
})
}
function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string) {
const path = '/api/v1/accounts/' + accountName + '/video-playlists'
const query = {
start,
count,
sort
}
return makeGetRequest({
url,
path,
query,
statusCodeExpected: 200
})
}
function getAccountPlaylistsListWithToken (
url: string,
token: string,
accountName: string,
start: number,
count: number,
playlistType?: VideoPlaylistType
) {
const path = '/api/v1/accounts/' + accountName + '/video-playlists'
const query = {
start,
count,
playlistType
}
return makeGetRequest({
url,
token,
path,
query,
statusCodeExpected: 200
})
}
@ -31,6 +97,17 @@ function getVideoPlaylist (url: string, playlistId: number | string, statusCodeE
})
}
function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = 200) {
const path = '/api/v1/video-playlists/' + playlistId
return makeGetRequest({
url,
token,
path,
statusCodeExpected
})
}
function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = 204) {
const path = '/api/v1/video-playlists/' + playlistId
@ -93,13 +170,15 @@ function updateVideoPlaylist (options: {
})
}
function addVideoInPlaylist (options: {
async function addVideoInPlaylist (options: {
url: string,
token: string,
playlistId: number | string,
elementAttrs: VideoPlaylistElementCreate
elementAttrs: VideoPlaylistElementCreate | { videoId: string }
expectedStatus?: number
}) {
options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
return makePostBodyRequest({
@ -135,7 +214,7 @@ function removeVideoFromPlaylist (options: {
token: string,
playlistId: number | string,
videoId: number | string,
expectedStatus: number
expectedStatus?: number
}) {
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
@ -156,7 +235,7 @@ function reorderVideosPlaylist (options: {
insertAfterPosition: number,
reorderLength?: number
},
expectedStatus: number
expectedStatus?: number
}) {
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
@ -165,15 +244,37 @@ function reorderVideosPlaylist (options: {
path,
token: options.token,
fields: options.elementAttrs,
statusCodeExpected: options.expectedStatus
statusCodeExpected: options.expectedStatus || 204
})
}
async function checkPlaylistFilesWereRemoved (
playlistUUID: string,
serverNumber: number,
directories = [ 'thumbnails' ]
) {
const testDirectory = 'test' + serverNumber
for (const directory of directories) {
const directoryPath = join(root(), testDirectory, directory)
const files = await readdir(directoryPath)
for (const file of files) {
expect(file).to.not.contain(playlistUUID)
}
}
}
// ---------------------------------------------------------------------------
export {
getVideoPlaylistsList,
getVideoChannelPlaylistsList,
getAccountPlaylistsList,
getAccountPlaylistsListWithToken,
getVideoPlaylist,
getVideoPlaylistWithToken,
createVideoPlaylist,
updateVideoPlaylist,
@ -183,5 +284,7 @@ export {
updateVideoPlaylistElement,
removeVideoFromPlaylist,
reorderVideosPlaylist
reorderVideosPlaylist,
checkPlaylistFilesWereRemoved
}

View File

@ -1,7 +1,7 @@
/* tslint:disable:no-unused-expression */
import { expect } from 'chai'
import { existsSync, readdir, readFile } from 'fs-extra'
import { pathExists, readdir, readFile } from 'fs-extra'
import * as parseTorrent from 'parse-torrent'
import { extname, join } from 'path'
import * as request from 'supertest'
@ -16,7 +16,7 @@ import {
ServerInfo,
testImage
} from '../'
import * as validator from 'validator'
import { VideoDetails, VideoPrivacy } from '../../models/videos'
import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
import { dateIsValid, webtorrentAdd } from '../miscs/miscs'
@ -311,8 +311,8 @@ async function checkVideoFilesWereRemoved (
for (const directory of directories) {
const directoryPath = join(root(), testDirectory, directory)
const directoryExists = existsSync(directoryPath)
if (!directoryExists) continue
const directoryExists = await pathExists(directoryPath)
if (directoryExists === false) continue
const files = await readdir(directoryPath)
for (const file of files) {
@ -597,12 +597,30 @@ async function completeVideoCheck (
}
}
async function videoUUIDToId (url: string, id: number | string) {
if (validator.isUUID('' + id) === false) return id
const res = await getVideo(url, id)
return res.body.id
}
async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
const videoAttrs: any = { name: options.videoName }
if (options.nsfw) videoAttrs.nsfw = options.nsfw
const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
return { id: res.body.video.id, uuid: res.body.video.uuid }
}
// ---------------------------------------------------------------------------
export {
getVideoDescription,
getVideoCategories,
getVideoLicences,
videoUUIDToId,
getVideoPrivacies,
getVideoLanguages,
getMyVideos,
@ -624,5 +642,6 @@ export {
getLocalVideos,
completeVideoCheck,
checkVideoFilesWereRemoved,
getPlaylistVideos
getPlaylistVideos,
uploadVideoAndGetId
}