#search-video {
@include peertube-input-text($search-input-width);
margin-right: 15px;
- padding-right: 25px; // For the search icon
+ padding-right: 40px; // For the search icon
&::placeholder {
color: #000;
export * from './user-subscription.service'
-export * from './subscribe-button.component'
\ No newline at end of file
+export * from './subscribe-button.component'
videoChannelLoaded = new ReplaySubject<VideoChannel>(1)
- constructor (
- private authHttp: HttpClient,
- private restExtractor: RestExtractor
- ) {}
-
static extractVideoChannels (result: ResultList<VideoChannelServer>) {
const videoChannels: VideoChannel[] = []
return { data: videoChannels, total: result.total }
}
+ constructor (
+ private authHttp: HttpClient,
+ private restExtractor: RestExtractor
+ ) { }
+
getVideoChannel (videoChannelName: string) {
return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
.pipe(
videosSearchSortValidator
} from '../../middlewares'
import { VideosSearchQuery } from '../../../shared/models/search'
+import { getOrCreateAccountAndVideoAndChannel } from '../../lib/activitypub'
+import { logger } from '../../helpers/logger'
const searchRouter = express.Router()
// ---------------------------------------------------------------------------
-async function searchVideos (req: express.Request, res: express.Response) {
+function searchVideos (req: express.Request, res: express.Response) {
const query: VideosSearchQuery = req.query
+ if (query.search.startsWith('http://') || query.search.startsWith('https://')) {
+ return searchVideoUrl(query.search, res)
+ }
+ return searchVideosDB(query, res)
+}
+
+async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
const options = Object.assign(query, {
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
+
+async function searchVideoUrl (url: string, res: express.Response) {
+ let video: VideoModel
+
+ try {
+ const syncParam = {
+ likes: false,
+ dislikes: false,
+ shares: false,
+ comments: false,
+ thumbnail: true
+ }
+
+ const res = await getOrCreateAccountAndVideoAndChannel(url, syncParam)
+ video = res ? res.video : undefined
+ } catch (err) {
+ logger.info('Cannot search remote video %s.', url)
+ }
+
+ return res.json({
+ total: video ? 1 : 0,
+ data: video ? [ video.toFormattedJSON() ] : []
+ })
+}
'email': 60000 * 10 // 10 minutes
}
const BROADCAST_CONCURRENCY = 10 // How many requests in parallel we do in activitypub-http-broadcast job
+const CRAWL_REQUEST_CONCURRENCY = 5 // How many requests in parallel to fetch remote data (likes, shares...)
const JOB_REQUEST_TIMEOUT = 3000 // 3 seconds
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
STATIC_DOWNLOAD_PATHS,
RATES_LIMIT,
VIDEO_EXT_MIMETYPE,
+ CRAWL_REQUEST_CONCURRENCY,
JOB_COMPLETED_LIFETIME,
VIDEO_IMPORT_STATES,
VIDEO_VIEW_LIFETIME,
}
const payload = {
- uris: [ actor.outboxUrl ]
+ uri: actor.outboxUrl,
+ type: 'activity' as 'activity'
}
return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
} else if (actorCreated.type === 'Group') { // Video channel
actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
actorCreated.VideoChannel.Actor = actorCreated
+ actorCreated.VideoChannel.Account = ownerActor.Account
}
return actorCreated
import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
import { doRequest } from '../../helpers/requests'
import { logger } from '../../helpers/logger'
+import Bluebird = require('bluebird')
-async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any>) {
+async function crawlCollectionPage <T> (uri: string, handler: (items: T[]) => Promise<any> | Bluebird<any>) {
logger.info('Crawling ActivityPub data on %s.', uri)
const options = {
async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
- let video: VideoModel
- const res = await getOrCreateAccountAndVideoAndChannel(objectUri)
- video = res.video
+ const { video } = await getOrCreateAccountAndVideoAndChannel(objectUri)
return sequelizeTypescript.transaction(async t => {
// Add share entry
} else if (activityType === 'Dislike') {
return retryTransactionWrapper(processCreateDislike, actor, activity)
} else if (activityType === 'Video') {
- return processCreateVideo(actor, activity)
+ return processCreateVideo(activity)
} else if (activityType === 'Flag') {
return retryTransactionWrapper(processCreateVideoAbuse, actor, activityObject as VideoAbuseObject)
} else if (activityType === 'Note') {
// ---------------------------------------------------------------------------
-async function processCreateVideo (
- actor: ActorModel,
- activity: ActivityCreate
-) {
+async function processCreateVideo (activity: ActivityCreate) {
const videoToCreateData = activity.object as VideoTorrentObject
- const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData, actor)
+ const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData)
return video
}
import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
import { logger } from '../../helpers/logger'
import { doRequest } from '../../helpers/requests'
-import { ACTIVITY_PUB } from '../../initializers'
+import { ACTIVITY_PUB, CRAWL_REQUEST_CONCURRENCY } from '../../initializers'
import { ActorModel } from '../../models/activitypub/actor'
import { VideoModel } from '../../models/video/video'
import { VideoCommentModel } from '../../models/video/video-comment'
import { getOrCreateActorAndServerAndModel } from './actor'
import { getOrCreateAccountAndVideoAndChannel } from './videos'
+import * as Bluebird from 'bluebird'
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
let originCommentId: number = null
}
async function addVideoComments (commentUrls: string[], instance: VideoModel) {
- for (const commentUrl of commentUrls) {
- await addVideoComment(instance, commentUrl)
- }
+ return Bluebird.map(commentUrls, commentUrl => {
+ return addVideoComment(instance, commentUrl)
+ }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
}
async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
import { retryTransactionWrapper } from '../../helpers/database-utils'
import { logger } from '../../helpers/logger'
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
-import { ACTIVITY_PUB, CONFIG, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
+import { ACTIVITY_PUB, CONFIG, CRAWL_REQUEST_CONCURRENCY, REMOTE_SCHEME, sequelizeTypescript, VIDEO_MIMETYPE_EXT } from '../../initializers'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { ActorModel } from '../../models/activitypub/actor'
import { TagModel } from '../../models/video/tag'
import { shareVideoByServerAndChannel } from './index'
import { isArray } from '../../helpers/custom-validators/misc'
import { VideoCaptionModel } from '../../models/video/video-caption'
+import { JobQueue } from '../job-queue'
+import { ActivitypubHttpFetcherPayload } from '../job-queue/handlers/activitypub-http-fetcher'
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
// If the video is not private and published, we federate it
return getOrCreateActorAndServerAndModel(channel.id)
}
-async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel) {
+async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor: ActorModel, waitThumbnail = false) {
logger.debug('Adding remote video %s.', videoObject.id)
- return sequelizeTypescript.transaction(async t => {
+ const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
const sequelizeOptions = {
transaction: t
}
const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
const video = VideoModel.build(videoData)
- // Don't block on remote HTTP request (we are in a transaction!)
- generateThumbnailFromUrl(video, videoObject.icon)
- .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
-
const videoCreated = await video.save(sequelizeOptions)
// Process files
videoCreated.VideoChannel = channelActor.VideoChannel
return videoCreated
})
+
+ const p = generateThumbnailFromUrl(videoCreated, videoObject.icon)
+ .catch(err => logger.warn('Cannot generate thumbnail of %s.', videoObject.id, { err }))
+
+ if (waitThumbnail === true) await p
+
+ return videoCreated
}
-async function getOrCreateAccountAndVideoAndChannel (videoObject: VideoTorrentObject | string, actor?: ActorModel) {
+type SyncParam = {
+ likes: boolean,
+ dislikes: boolean,
+ shares: boolean,
+ comments: boolean,
+ thumbnail: boolean
+}
+async function getOrCreateAccountAndVideoAndChannel (
+ videoObject: VideoTorrentObject | string,
+ syncParam: SyncParam = { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true }
+) {
const videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
- if (videoFromDatabase) {
- return {
- video: videoFromDatabase,
- actor: videoFromDatabase.VideoChannel.Account.Actor,
- channelActor: videoFromDatabase.VideoChannel.Actor
- }
- }
+ if (videoFromDatabase) return { video: videoFromDatabase }
- videoObject = await fetchRemoteVideo(videoUrl)
- if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
+ const fetchedVideo = await fetchRemoteVideo(videoUrl)
+ if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
- if (!actor) {
- const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
- if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
+ const channelActor = await getOrCreateVideoChannel(fetchedVideo)
+ const video = await retryTransactionWrapper(getOrCreateVideo, fetchedVideo, channelActor, syncParam.thumbnail)
- actor = await getOrCreateActorAndServerAndModel(actorObj.id)
- }
+ // Process outside the transaction because we could fetch remote data
- const channelActor = await getOrCreateVideoChannel(videoObject)
+ logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
- const video = await retryTransactionWrapper(getOrCreateVideo, videoObject, channelActor)
+ const jobPayloads: ActivitypubHttpFetcherPayload[] = []
- // Process outside the transaction because we could fetch remote data
- logger.info('Adding likes of video %s.', video.uuid)
- await crawlCollectionPage<string>(videoObject.likes, (items) => createRates(items, video, 'like'))
+ if (syncParam.likes === true) {
+ await crawlCollectionPage<string>(fetchedVideo.likes, items => createRates(items, video, 'like'))
+ .catch(err => logger.error('Cannot add likes of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.likes, videoId: video.id, type: 'video-likes' as 'video-likes' })
+ }
- logger.info('Adding dislikes of video %s.', video.uuid)
- await crawlCollectionPage<string>(videoObject.dislikes, (items) => createRates(items, video, 'dislike'))
+ if (syncParam.dislikes === true) {
+ await crawlCollectionPage<string>(fetchedVideo.dislikes, items => createRates(items, video, 'dislike'))
+ .catch(err => logger.error('Cannot add dislikes of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.dislikes, videoId: video.id, type: 'video-dislikes' as 'video-dislikes' })
+ }
+
+ if (syncParam.shares === true) {
+ await crawlCollectionPage<string>(fetchedVideo.shares, items => addVideoShares(items, video))
+ .catch(err => logger.error('Cannot add shares of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+ }
- logger.info('Adding shares of video %s.', video.uuid)
- await crawlCollectionPage<string>(videoObject.shares, (items) => addVideoShares(items, video))
+ if (syncParam.comments === true) {
+ await crawlCollectionPage<string>(fetchedVideo.comments, items => addVideoComments(items, video))
+ .catch(err => logger.error('Cannot add comments of video %s.', video.uuid, { err }))
+ } else {
+ jobPayloads.push({ uri: fetchedVideo.shares, videoId: video.id, type: 'video-shares' as 'video-shares' })
+ }
- logger.info('Adding comments of video %s.', video.uuid)
- await crawlCollectionPage<string>(videoObject.comments, (items) => addVideoComments(items, video))
+ await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
- return { actor, channelActor, video }
+ return { video }
}
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
let rateCounts = 0
- const tasks: Bluebird<number>[] = []
-
- for (const actorUrl of actorUrls) {
- const actor = await getOrCreateActorAndServerAndModel(actorUrl)
- const p = AccountVideoRateModel
- .create({
- videoId: video.id,
- accountId: actor.Account.id,
- type: rate
- })
- .then(() => rateCounts += 1)
-
- tasks.push(p)
- }
- await Promise.all(tasks)
+ await Bluebird.map(actorUrls, async actorUrl => {
+ try {
+ const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+ const [ , created ] = await AccountVideoRateModel
+ .findOrCreate({
+ where: {
+ videoId: video.id,
+ accountId: actor.Account.id
+ },
+ defaults: {
+ videoId: video.id,
+ accountId: actor.Account.id,
+ type: rate
+ }
+ })
+
+ if (created) rateCounts += 1
+ } catch (err) {
+ logger.warn('Cannot add rate %s for actor %s.', rate, actorUrl, { err })
+ }
+ }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
}
async function addVideoShares (shareUrls: string[], instance: VideoModel) {
- for (const shareUrl of shareUrls) {
- // Fetch url
- const { body } = await doRequest({
- uri: shareUrl,
- json: true,
- activityPub: true
- })
- if (!body || !body.actor) {
- logger.warn('Cannot add remote share with url: %s, skipping...', shareUrl)
- continue
- }
-
- const actorUrl = body.actor
- const actor = await getOrCreateActorAndServerAndModel(actorUrl)
+ await Bluebird.map(shareUrls, async shareUrl => {
+ try {
+ // Fetch url
+ const { body } = await doRequest({
+ uri: shareUrl,
+ json: true,
+ activityPub: true
+ })
+ if (!body || !body.actor) throw new Error('Body of body actor is invalid')
- const entry = {
- actorId: actor.id,
- videoId: instance.id,
- url: shareUrl
- }
+ const actorUrl = body.actor
+ const actor = await getOrCreateActorAndServerAndModel(actorUrl)
- await VideoShareModel.findOrCreate({
- where: {
+ const entry = {
+ actorId: actor.id,
+ videoId: instance.id,
url: shareUrl
- },
- defaults: entry
- })
- }
+ }
+
+ await VideoShareModel.findOrCreate({
+ where: {
+ url: shareUrl
+ },
+ defaults: entry
+ })
+ } catch (err) {
+ logger.warn('Cannot add share %s.', shareUrl, { err })
+ }
+ }, { concurrency: CRAWL_REQUEST_CONCURRENCY })
}
async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
videoFileActivityUrlToDBAttributes,
getOrCreateVideo,
getOrCreateVideoChannel,
- addVideoShares
+ addVideoShares,
+ createRates
}
import * as Bull from 'bull'
import { logger } from '../../../helpers/logger'
import { processActivities } from '../../activitypub/process'
-import { ActivitypubHttpBroadcastPayload } from './activitypub-http-broadcast'
+import { VideoModel } from '../../../models/video/video'
+import { addVideoShares, createRates } from '../../activitypub/videos'
+import { addVideoComments } from '../../activitypub/video-comments'
import { crawlCollectionPage } from '../../activitypub/crawl'
-import { Activity } from '../../../../shared/models/activitypub'
+
+type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments'
export type ActivitypubHttpFetcherPayload = {
- uris: string[]
+ uri: string
+ type: FetchType
+ videoId?: number
}
async function processActivityPubHttpFetcher (job: Bull.Job) {
logger.info('Processing ActivityPub fetcher in job %d.', job.id)
- const payload = job.data as ActivitypubHttpBroadcastPayload
+ const payload = job.data as ActivitypubHttpFetcherPayload
+
+ let video: VideoModel
+ if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
- for (const uri of payload.uris) {
- await crawlCollectionPage<Activity>(uri, (items) => processActivities(items))
+ const fetcherType: { [ id in FetchType ]: (items: any[]) => Promise<any> } = {
+ 'activity': items => processActivities(items),
+ 'video-likes': items => createRates(items, video, 'like'),
+ 'video-dislikes': items => createRates(items, video, 'dislike'),
+ 'video-shares': items => addVideoShares(items, video),
+ 'video-comments': items => addVideoComments(items, video)
}
+
+ return crawlCollectionPage(payload.uri, fetcherType[payload.type])
}
// ---------------------------------------------------------------------------
let userAccessToken = ''
let accountName: string
let channelId: number
- let channelUUID: string
+ let channelName: string
let videoId
// ---------------------------------------------------------------
{
const res = await getMyUserInformation(server.url, server.accessToken)
channelId = res.body.videoChannels[ 0 ].id
- channelUUID = res.body.videoChannels[ 0 ].uuid
+ channelName = res.body.videoChannels[ 0 ].name
accountName = res.body.account.name + '@' + res.body.account.host
}
})
let path: string
before(async function () {
- path = '/api/v1/video-channels/' + channelUUID + '/videos'
+ path = '/api/v1/video-channels/' + channelName + '/videos'
})
it('Should fail with a bad start pagination', async function () {
likes: 1,
dislikes: 1,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal
},
privacy: VideoPrivacy.PUBLIC,
commentsEnabled: true,
channel: {
- name: 'Main root channel',
+ name: 'root_channel',
+ displayName: 'Main root channel',
description: '',
isLocal: false
},
import * as chai from 'chai'
import 'mocha'
-import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, userLogin } from '../../utils'
+import { createUser, doubleFollow, flushAndRunMultipleServers, follow, getVideosList, unfollow, updateVideo, userLogin } from '../../utils'
import { killallServers, ServerInfo, uploadVideo } from '../../utils/index'
import { setAccessTokensToServers } from '../../utils/users/login'
import { Video, VideoChannel } from '../../../../shared/models/videos'
describe('Test users subscriptions', function () {
let servers: ServerInfo[] = []
const users: { accessToken: string }[] = []
+ let video3UUID: string
before(async function () {
this.timeout(120000)
await waitJobs(servers)
- await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
+ const res = await uploadVideo(servers[2].url, users[2].accessToken, { name: 'video server 3 added after follow' })
+ video3UUID = res.body.video.uuid
await waitJobs(servers)
})
}
})
+ it('Should update a video of server 3 and see the updated video on server 1', async function () {
+ this.timeout(30000)
+
+ await updateVideo(servers[2].url, users[2].accessToken, video3UUID, { name: 'video server 3 added after follow updated' })
+
+ await waitJobs(servers)
+
+ const res = await listUserSubscriptionVideos(servers[0].url, users[0].accessToken, 'createdAt')
+ const videos: Video[] = res.body.data
+ expect(videos[2].name).to.equal('video server 3 added after follow updated')
+ })
+
it('Should remove user of server 3 subscription', async function () {
+ this.timeout(30000)
+
await removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003')
await waitJobs(servers)
})
it('Should remove the root subscription and not display the videos anymore', async function () {
+ this.timeout(30000)
+
await removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:9001')
await waitJobs(servers)
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
- expect(video.name).to.not.contain('video server 3 added after follow')
+ expect(video.name).to.not.contain('video server 3 added after follow updated')
}
})
expect(videos[0].name).to.equal('video 1-3')
expect(videos[1].name).to.equal('video 2-3')
- expect(videos[2].name).to.equal('video server 3 added after follow')
+ expect(videos[2].name).to.equal('video server 3 added after follow updated')
}
{
for (const video of res.body.data) {
expect(video.name).to.not.contain('1-3')
expect(video.name).to.not.contain('2-3')
- expect(video.name).to.not.contain('video server 3 added after follow')
+ expect(video.name).to.not.contain('video server 3 added after follow updated')
}
}
})
privacy: VideoPrivacy.PUBLIC,
commentsEnabled: true,
channel: {
- name: 'my channel',
+ displayName: 'my channel',
+ name: 'super_channel_name',
description: 'super channel',
isLocal
},
tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
privacy: VideoPrivacy.PUBLIC,
channel: {
- name: 'Main user1 channel',
+ displayName: 'Main user1 channel',
+ name: 'user1_channel',
description: 'super channel',
isLocal
},
tags: [ 'tag1p3' ],
privacy: VideoPrivacy.PUBLIC,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal
},
tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
privacy: VideoPrivacy.PUBLIC,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal
},
tags: [ 'tag_up_1', 'tag_up_2' ],
privacy: VideoPrivacy.PUBLIC,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal
},
tags: [ ],
privacy: VideoPrivacy.PUBLIC,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal
},
privacy: VideoPrivacy.PUBLIC,
commentsEnabled: true,
channel: {
- name: 'Main root channel',
+ displayName: 'Main root channel',
+ name: 'root_channel',
description: '',
isLocal: true
},
duration: 5,
commentsEnabled: false,
channel: {
- name: 'Main root channel',
+ name: 'root_channel',
+ displayName: 'Main root channel',
description: '',
isLocal: true
},
name: string
host: string
}
- isLocal: boolean,
- tags: string[],
- privacy: number,
- likes?: number,
- dislikes?: number,
- duration: number,
+ isLocal: boolean
+ tags: string[]
+ privacy: number
+ likes?: number
+ dislikes?: number
+ duration: number
channel: {
- name: string,
+ displayName: string
+ name: string
description
isLocal: boolean
}
- fixture: string,
+ fixture: string
files: {
resolution: number
size: number
expect(video.account.uuid).to.be.a('string')
expect(video.account.host).to.equal(attributes.account.host)
expect(video.account.name).to.equal(attributes.account.name)
- expect(video.channel.displayName).to.equal(attributes.channel.name)
- expect(video.channel.name).to.have.lengthOf(36)
+ expect(video.channel.displayName).to.equal(attributes.channel.displayName)
+ expect(video.channel.name).to.equal(attributes.channel.name)
expect(video.likes).to.equal(attributes.likes)
expect(video.dislikes).to.equal(attributes.dislikes)
expect(video.isLocal).to.equal(attributes.isLocal)
expect(videoDetails.tags).to.deep.equal(attributes.tags)
expect(videoDetails.account.name).to.equal(attributes.account.name)
expect(videoDetails.account.host).to.equal(attributes.account.host)
- expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
- expect(videoDetails.channel.name).to.have.lengthOf(36)
+ expect(video.channel.displayName).to.equal(attributes.channel.displayName)
+ expect(video.channel.name).to.equal(attributes.channel.name)
expect(videoDetails.channel.host).to.equal(attributes.account.host)
expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true