Add ability to search a video with an URL
This commit is contained in:
parent
22a16e36f6
commit
f6eebcb336
|
@ -4,7 +4,7 @@
|
||||||
#search-video {
|
#search-video {
|
||||||
@include peertube-input-text($search-input-width);
|
@include peertube-input-text($search-input-width);
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
padding-right: 25px; // For the search icon
|
padding-right: 40px; // For the search icon
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: #000;
|
color: #000;
|
||||||
|
|
|
@ -17,11 +17,6 @@ export class VideoChannelService {
|
||||||
|
|
||||||
videoChannelLoaded = new ReplaySubject<VideoChannel>(1)
|
videoChannelLoaded = new ReplaySubject<VideoChannel>(1)
|
||||||
|
|
||||||
constructor (
|
|
||||||
private authHttp: HttpClient,
|
|
||||||
private restExtractor: RestExtractor
|
|
||||||
) {}
|
|
||||||
|
|
||||||
static extractVideoChannels (result: ResultList<VideoChannelServer>) {
|
static extractVideoChannels (result: ResultList<VideoChannelServer>) {
|
||||||
const videoChannels: VideoChannel[] = []
|
const videoChannels: VideoChannel[] = []
|
||||||
|
|
||||||
|
@ -32,6 +27,11 @@ export class VideoChannelService {
|
||||||
return { data: videoChannels, total: result.total }
|
return { data: videoChannels, total: result.total }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private authHttp: HttpClient,
|
||||||
|
private restExtractor: RestExtractor
|
||||||
|
) { }
|
||||||
|
|
||||||
getVideoChannel (videoChannelName: string) {
|
getVideoChannel (videoChannelName: string) {
|
||||||
return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
|
return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
videosSearchSortValidator
|
videosSearchSortValidator
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import { VideosSearchQuery } from '../../../shared/models/search'
|
import { VideosSearchQuery } from '../../../shared/models/search'
|
||||||
|
import { getOrCreateAccountAndVideoAndChannel } from '../../lib/activitypub'
|
||||||
|
import { logger } from '../../helpers/logger'
|
||||||
|
|
||||||
const searchRouter = express.Router()
|
const searchRouter = express.Router()
|
||||||
|
|
||||||
|
@ -33,9 +35,16 @@ export { searchRouter }
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function searchVideos (req: express.Request, res: express.Response) {
|
function searchVideos (req: express.Request, res: express.Response) {
|
||||||
const query: VideosSearchQuery = req.query
|
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, {
|
const options = Object.assign(query, {
|
||||||
includeLocalVideos: true,
|
includeLocalVideos: true,
|
||||||
nsfw: buildNSFWFilter(res, query.nsfw)
|
nsfw: buildNSFWFilter(res, query.nsfw)
|
||||||
|
@ -44,3 +53,27 @@ async function searchVideos (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
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() ] : []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -112,6 +112,7 @@ const JOB_TTL: { [ id in JobType ]: number } = {
|
||||||
'email': 60000 * 10 // 10 minutes
|
'email': 60000 * 10 // 10 minutes
|
||||||
}
|
}
|
||||||
const BROADCAST_CONCURRENCY = 10 // How many requests in parallel we do in activitypub-http-broadcast job
|
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_REQUEST_TIMEOUT = 3000 // 3 seconds
|
||||||
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
|
const JOB_COMPLETED_LIFETIME = 60000 * 60 * 24 * 2 // 2 days
|
||||||
|
|
||||||
|
@ -643,6 +644,7 @@ export {
|
||||||
STATIC_DOWNLOAD_PATHS,
|
STATIC_DOWNLOAD_PATHS,
|
||||||
RATES_LIMIT,
|
RATES_LIMIT,
|
||||||
VIDEO_EXT_MIMETYPE,
|
VIDEO_EXT_MIMETYPE,
|
||||||
|
CRAWL_REQUEST_CONCURRENCY,
|
||||||
JOB_COMPLETED_LIFETIME,
|
JOB_COMPLETED_LIFETIME,
|
||||||
VIDEO_IMPORT_STATES,
|
VIDEO_IMPORT_STATES,
|
||||||
VIDEO_VIEW_LIFETIME,
|
VIDEO_VIEW_LIFETIME,
|
||||||
|
|
|
@ -177,7 +177,8 @@ async function addFetchOutboxJob (actor: ActorModel) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
uris: [ actor.outboxUrl ]
|
uri: actor.outboxUrl,
|
||||||
|
type: 'activity' as 'activity'
|
||||||
}
|
}
|
||||||
|
|
||||||
return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
|
return JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload })
|
||||||
|
@ -248,6 +249,7 @@ function saveActorAndServerAndModelIfNotExist (
|
||||||
} else if (actorCreated.type === 'Group') { // Video channel
|
} else if (actorCreated.type === 'Group') { // Video channel
|
||||||
actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
|
actorCreated.VideoChannel = await saveVideoChannel(actorCreated, result, ownerActor, t)
|
||||||
actorCreated.VideoChannel.Actor = actorCreated
|
actorCreated.VideoChannel.Actor = actorCreated
|
||||||
|
actorCreated.VideoChannel.Account = ownerActor.Account
|
||||||
}
|
}
|
||||||
|
|
||||||
return actorCreated
|
return actorCreated
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
|
import { ACTIVITY_PUB, JOB_REQUEST_TIMEOUT } from '../../initializers'
|
||||||
import { doRequest } from '../../helpers/requests'
|
import { doRequest } from '../../helpers/requests'
|
||||||
import { logger } from '../../helpers/logger'
|
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)
|
logger.info('Crawling ActivityPub data on %s.', uri)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
@ -24,10 +24,8 @@ export {
|
||||||
|
|
||||||
async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
|
async function processVideoShare (actorAnnouncer: ActorModel, activity: ActivityAnnounce) {
|
||||||
const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
|
const objectUri = typeof activity.object === 'string' ? activity.object : activity.object.id
|
||||||
let video: VideoModel
|
|
||||||
|
|
||||||
const res = await getOrCreateAccountAndVideoAndChannel(objectUri)
|
const { video } = await getOrCreateAccountAndVideoAndChannel(objectUri)
|
||||||
video = res.video
|
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(async t => {
|
return sequelizeTypescript.transaction(async t => {
|
||||||
// Add share entry
|
// Add share entry
|
||||||
|
|
|
@ -23,7 +23,7 @@ async function processCreateActivity (activity: ActivityCreate) {
|
||||||
} else if (activityType === 'Dislike') {
|
} else if (activityType === 'Dislike') {
|
||||||
return retryTransactionWrapper(processCreateDislike, actor, activity)
|
return retryTransactionWrapper(processCreateDislike, actor, activity)
|
||||||
} else if (activityType === 'Video') {
|
} else if (activityType === 'Video') {
|
||||||
return processCreateVideo(actor, activity)
|
return processCreateVideo(activity)
|
||||||
} else if (activityType === 'Flag') {
|
} else if (activityType === 'Flag') {
|
||||||
return retryTransactionWrapper(processCreateVideoAbuse, actor, activityObject as VideoAbuseObject)
|
return retryTransactionWrapper(processCreateVideoAbuse, actor, activityObject as VideoAbuseObject)
|
||||||
} else if (activityType === 'Note') {
|
} else if (activityType === 'Note') {
|
||||||
|
@ -42,13 +42,10 @@ export {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function processCreateVideo (
|
async function processCreateVideo (activity: ActivityCreate) {
|
||||||
actor: ActorModel,
|
|
||||||
activity: ActivityCreate
|
|
||||||
) {
|
|
||||||
const videoToCreateData = activity.object as VideoTorrentObject
|
const videoToCreateData = activity.object as VideoTorrentObject
|
||||||
|
|
||||||
const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData, actor)
|
const { video } = await getOrCreateAccountAndVideoAndChannel(videoToCreateData)
|
||||||
|
|
||||||
return video
|
return video
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { VideoCommentObject } from '../../../shared/models/activitypub/objects/v
|
||||||
import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
|
import { sanitizeAndCheckVideoCommentObject } from '../../helpers/custom-validators/activitypub/video-comments'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { doRequest } from '../../helpers/requests'
|
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 { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../models/video/video'
|
||||||
import { VideoCommentModel } from '../../models/video/video-comment'
|
import { VideoCommentModel } from '../../models/video/video-comment'
|
||||||
import { getOrCreateActorAndServerAndModel } from './actor'
|
import { getOrCreateActorAndServerAndModel } from './actor'
|
||||||
import { getOrCreateAccountAndVideoAndChannel } from './videos'
|
import { getOrCreateAccountAndVideoAndChannel } from './videos'
|
||||||
|
import * as Bluebird from 'bluebird'
|
||||||
|
|
||||||
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
|
async function videoCommentActivityObjectToDBAttributes (video: VideoModel, actor: ActorModel, comment: VideoCommentObject) {
|
||||||
let originCommentId: number = null
|
let originCommentId: number = null
|
||||||
|
@ -38,9 +39,9 @@ async function videoCommentActivityObjectToDBAttributes (video: VideoModel, acto
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addVideoComments (commentUrls: string[], instance: VideoModel) {
|
async function addVideoComments (commentUrls: string[], instance: VideoModel) {
|
||||||
for (const commentUrl of commentUrls) {
|
return Bluebird.map(commentUrls, commentUrl => {
|
||||||
await addVideoComment(instance, commentUrl)
|
return addVideoComment(instance, commentUrl)
|
||||||
}
|
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
|
async function addVideoComment (videoInstance: VideoModel, commentUrl: string) {
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos
|
||||||
import { retryTransactionWrapper } from '../../helpers/database-utils'
|
import { retryTransactionWrapper } from '../../helpers/database-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { doRequest, doRequestAndSaveToFile } from '../../helpers/requests'
|
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 { AccountVideoRateModel } from '../../models/account/account-video-rate'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { TagModel } from '../../models/video/tag'
|
import { TagModel } from '../../models/video/tag'
|
||||||
|
@ -26,6 +26,8 @@ import { sendCreateVideo, sendUpdateVideo } from './send'
|
||||||
import { shareVideoByServerAndChannel } from './index'
|
import { shareVideoByServerAndChannel } from './index'
|
||||||
import { isArray } from '../../helpers/custom-validators/misc'
|
import { isArray } from '../../helpers/custom-validators/misc'
|
||||||
import { VideoCaptionModel } from '../../models/video/video-caption'
|
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) {
|
async function federateVideoIfNeeded (video: VideoModel, isNewVideo: boolean, transaction?: sequelize.Transaction) {
|
||||||
// If the video is not private and published, we federate it
|
// If the video is not private and published, we federate it
|
||||||
|
@ -178,10 +180,10 @@ function getOrCreateVideoChannel (videoObject: VideoTorrentObject) {
|
||||||
return getOrCreateActorAndServerAndModel(channel.id)
|
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)
|
logger.debug('Adding remote video %s.', videoObject.id)
|
||||||
|
|
||||||
return sequelizeTypescript.transaction(async t => {
|
const videoCreated: VideoModel = await sequelizeTypescript.transaction(async t => {
|
||||||
const sequelizeOptions = {
|
const sequelizeOptions = {
|
||||||
transaction: t
|
transaction: t
|
||||||
}
|
}
|
||||||
|
@ -191,10 +193,6 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
|
||||||
const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
|
const videoData = await videoActivityObjectToDBAttributes(channelActor.VideoChannel, videoObject, videoObject.to)
|
||||||
const video = VideoModel.build(videoData)
|
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)
|
const videoCreated = await video.save(sequelizeOptions)
|
||||||
|
|
||||||
// Process files
|
// Process files
|
||||||
|
@ -222,68 +220,100 @@ async function getOrCreateVideo (videoObject: VideoTorrentObject, channelActor:
|
||||||
videoCreated.VideoChannel = channelActor.VideoChannel
|
videoCreated.VideoChannel = channelActor.VideoChannel
|
||||||
return videoCreated
|
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 videoUrl = typeof videoObject === 'string' ? videoObject : videoObject.id
|
||||||
|
|
||||||
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
|
const videoFromDatabase = await VideoModel.loadByUrlAndPopulateAccount(videoUrl)
|
||||||
if (videoFromDatabase) {
|
if (videoFromDatabase) return { video: videoFromDatabase }
|
||||||
return {
|
|
||||||
video: videoFromDatabase,
|
|
||||||
actor: videoFromDatabase.VideoChannel.Account.Actor,
|
|
||||||
channelActor: videoFromDatabase.VideoChannel.Actor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
videoObject = await fetchRemoteVideo(videoUrl)
|
const fetchedVideo = await fetchRemoteVideo(videoUrl)
|
||||||
if (!videoObject) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
|
if (!fetchedVideo) throw new Error('Cannot fetch remote video with url: ' + videoUrl)
|
||||||
|
|
||||||
if (!actor) {
|
const channelActor = await getOrCreateVideoChannel(fetchedVideo)
|
||||||
const actorObj = videoObject.attributedTo.find(a => a.type === 'Person')
|
const video = await retryTransactionWrapper(getOrCreateVideo, fetchedVideo, channelActor, syncParam.thumbnail)
|
||||||
if (!actorObj) throw new Error('Cannot find associated actor to video ' + videoObject.url)
|
|
||||||
|
|
||||||
actor = await getOrCreateActorAndServerAndModel(actorObj.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const channelActor = await getOrCreateVideoChannel(videoObject)
|
|
||||||
|
|
||||||
const video = await retryTransactionWrapper(getOrCreateVideo, videoObject, channelActor)
|
|
||||||
|
|
||||||
// Process outside the transaction because we could fetch remote data
|
// 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'))
|
|
||||||
|
|
||||||
logger.info('Adding dislikes of video %s.', video.uuid)
|
logger.info('Adding likes/dislikes/shares/comments of video %s.', video.uuid)
|
||||||
await crawlCollectionPage<string>(videoObject.dislikes, (items) => createRates(items, video, 'dislike'))
|
|
||||||
|
|
||||||
logger.info('Adding shares of video %s.', video.uuid)
|
const jobPayloads: ActivitypubHttpFetcherPayload[] = []
|
||||||
await crawlCollectionPage<string>(videoObject.shares, (items) => addVideoShares(items, video))
|
|
||||||
|
|
||||||
logger.info('Adding comments of video %s.', video.uuid)
|
if (syncParam.likes === true) {
|
||||||
await crawlCollectionPage<string>(videoObject.comments, (items) => addVideoComments(items, video))
|
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' })
|
||||||
|
}
|
||||||
|
|
||||||
return { actor, channelActor, video }
|
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' })
|
||||||
|
}
|
||||||
|
|
||||||
|
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' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await Bluebird.map(jobPayloads, payload => JobQueue.Instance.createJob({ type: 'activitypub-http-fetcher', payload }))
|
||||||
|
|
||||||
|
return { video }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
|
async function createRates (actorUrls: string[], video: VideoModel, rate: VideoRateType) {
|
||||||
let rateCounts = 0
|
let rateCounts = 0
|
||||||
const tasks: Bluebird<number>[] = []
|
|
||||||
|
|
||||||
for (const actorUrl of actorUrls) {
|
await Bluebird.map(actorUrls, async actorUrl => {
|
||||||
|
try {
|
||||||
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
|
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
|
||||||
const p = AccountVideoRateModel
|
const [ , created ] = await AccountVideoRateModel
|
||||||
.create({
|
.findOrCreate({
|
||||||
|
where: {
|
||||||
|
videoId: video.id,
|
||||||
|
accountId: actor.Account.id
|
||||||
|
},
|
||||||
|
defaults: {
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
accountId: actor.Account.id,
|
accountId: actor.Account.id,
|
||||||
type: rate
|
type: rate
|
||||||
})
|
|
||||||
.then(() => rateCounts += 1)
|
|
||||||
|
|
||||||
tasks.push(p)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
await Promise.all(tasks)
|
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)
|
logger.info('Adding %d %s to video %s.', rateCounts, rate, video.uuid)
|
||||||
|
|
||||||
|
@ -294,17 +324,15 @@ async function createRates (actorUrls: string[], video: VideoModel, rate: VideoR
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
||||||
for (const shareUrl of shareUrls) {
|
await Bluebird.map(shareUrls, async shareUrl => {
|
||||||
|
try {
|
||||||
// Fetch url
|
// Fetch url
|
||||||
const { body } = await doRequest({
|
const { body } = await doRequest({
|
||||||
uri: shareUrl,
|
uri: shareUrl,
|
||||||
json: true,
|
json: true,
|
||||||
activityPub: true
|
activityPub: true
|
||||||
})
|
})
|
||||||
if (!body || !body.actor) {
|
if (!body || !body.actor) throw new Error('Body of body actor is invalid')
|
||||||
logger.warn('Cannot add remote share with url: %s, skipping...', shareUrl)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const actorUrl = body.actor
|
const actorUrl = body.actor
|
||||||
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
|
const actor = await getOrCreateActorAndServerAndModel(actorUrl)
|
||||||
|
@ -321,7 +349,10 @@ async function addVideoShares (shareUrls: string[], instance: VideoModel) {
|
||||||
},
|
},
|
||||||
defaults: entry
|
defaults: entry
|
||||||
})
|
})
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Cannot add share %s.', shareUrl, { err })
|
||||||
}
|
}
|
||||||
|
}, { concurrency: CRAWL_REQUEST_CONCURRENCY })
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
|
async function fetchRemoteVideo (videoUrl: string): Promise<VideoTorrentObject> {
|
||||||
|
@ -355,5 +386,6 @@ export {
|
||||||
videoFileActivityUrlToDBAttributes,
|
videoFileActivityUrlToDBAttributes,
|
||||||
getOrCreateVideo,
|
getOrCreateVideo,
|
||||||
getOrCreateVideoChannel,
|
getOrCreateVideoChannel,
|
||||||
addVideoShares
|
addVideoShares,
|
||||||
|
createRates
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,36 @@
|
||||||
import * as Bull from 'bull'
|
import * as Bull from 'bull'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { processActivities } from '../../activitypub/process'
|
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 { crawlCollectionPage } from '../../activitypub/crawl'
|
||||||
import { Activity } from '../../../../shared/models/activitypub'
|
|
||||||
|
type FetchType = 'activity' | 'video-likes' | 'video-dislikes' | 'video-shares' | 'video-comments'
|
||||||
|
|
||||||
export type ActivitypubHttpFetcherPayload = {
|
export type ActivitypubHttpFetcherPayload = {
|
||||||
uris: string[]
|
uri: string
|
||||||
|
type: FetchType
|
||||||
|
videoId?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processActivityPubHttpFetcher (job: Bull.Job) {
|
async function processActivityPubHttpFetcher (job: Bull.Job) {
|
||||||
logger.info('Processing ActivityPub fetcher in job %d.', job.id)
|
logger.info('Processing ActivityPub fetcher in job %d.', job.id)
|
||||||
|
|
||||||
const payload = job.data as ActivitypubHttpBroadcastPayload
|
const payload = job.data as ActivitypubHttpFetcherPayload
|
||||||
|
|
||||||
for (const uri of payload.uris) {
|
let video: VideoModel
|
||||||
await crawlCollectionPage<Activity>(uri, (items) => processActivities(items))
|
if (payload.videoId) video = await VideoModel.loadAndPopulateAccountAndServerAndTags(payload.videoId)
|
||||||
|
|
||||||
|
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])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -20,7 +20,7 @@ describe('Test videos API validator', function () {
|
||||||
let userAccessToken = ''
|
let userAccessToken = ''
|
||||||
let accountName: string
|
let accountName: string
|
||||||
let channelId: number
|
let channelId: number
|
||||||
let channelUUID: string
|
let channelName: string
|
||||||
let videoId
|
let videoId
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
@ -42,7 +42,7 @@ describe('Test videos API validator', function () {
|
||||||
{
|
{
|
||||||
const res = await getMyUserInformation(server.url, server.accessToken)
|
const res = await getMyUserInformation(server.url, server.accessToken)
|
||||||
channelId = res.body.videoChannels[ 0 ].id
|
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
|
accountName = res.body.account.name + '@' + res.body.account.host
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -140,7 +140,7 @@ describe('Test videos API validator', function () {
|
||||||
let path: string
|
let path: string
|
||||||
|
|
||||||
before(async function () {
|
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 () {
|
it('Should fail with a bad start pagination', async function () {
|
||||||
|
|
|
@ -311,7 +311,8 @@ describe('Test follows', function () {
|
||||||
likes: 1,
|
likes: 1,
|
||||||
dislikes: 1,
|
dislikes: 1,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
|
|
@ -71,7 +71,8 @@ describe('Test handle downs', function () {
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
commentsEnabled: true,
|
commentsEnabled: true,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
name: 'root_channel',
|
||||||
|
displayName: 'Main root channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal: false
|
isLocal: false
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
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 { killallServers, ServerInfo, uploadVideo } from '../../utils/index'
|
||||||
import { setAccessTokensToServers } from '../../utils/users/login'
|
import { setAccessTokensToServers } from '../../utils/users/login'
|
||||||
import { Video, VideoChannel } from '../../../../shared/models/videos'
|
import { Video, VideoChannel } from '../../../../shared/models/videos'
|
||||||
|
@ -20,6 +20,7 @@ const expect = chai.expect
|
||||||
describe('Test users subscriptions', function () {
|
describe('Test users subscriptions', function () {
|
||||||
let servers: ServerInfo[] = []
|
let servers: ServerInfo[] = []
|
||||||
const users: { accessToken: string }[] = []
|
const users: { accessToken: string }[] = []
|
||||||
|
let video3UUID: string
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(120000)
|
this.timeout(120000)
|
||||||
|
@ -65,7 +66,8 @@ describe('Test users subscriptions', function () {
|
||||||
|
|
||||||
await waitJobs(servers)
|
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)
|
await waitJobs(servers)
|
||||||
})
|
})
|
||||||
|
@ -247,7 +249,21 @@ describe('Test users subscriptions', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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 () {
|
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 removeUserSubscription(servers[0].url, users[0].accessToken, 'user3_channel@localhost:9003')
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
@ -267,6 +283,8 @@ describe('Test users subscriptions', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should remove the root subscription and not display the videos anymore', async function () {
|
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 removeUserSubscription(servers[0].url, users[0].accessToken, 'root_channel@localhost:9001')
|
||||||
|
|
||||||
await waitJobs(servers)
|
await waitJobs(servers)
|
||||||
|
@ -288,7 +306,7 @@ describe('Test users subscriptions', function () {
|
||||||
for (const video of res.body.data) {
|
for (const video of res.body.data) {
|
||||||
expect(video.name).to.not.contain('1-3')
|
expect(video.name).to.not.contain('1-3')
|
||||||
expect(video.name).to.not.contain('2-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')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -309,7 +327,7 @@ describe('Test users subscriptions', function () {
|
||||||
|
|
||||||
expect(videos[0].name).to.equal('video 1-3')
|
expect(videos[0].name).to.equal('video 1-3')
|
||||||
expect(videos[1].name).to.equal('video 2-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')
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -319,7 +337,7 @@ describe('Test users subscriptions', function () {
|
||||||
for (const video of res.body.data) {
|
for (const video of res.body.data) {
|
||||||
expect(video.name).to.not.contain('1-3')
|
expect(video.name).to.not.contain('1-3')
|
||||||
expect(video.name).to.not.contain('2-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')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -128,7 +128,8 @@ describe('Test multiple servers', function () {
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
commentsEnabled: true,
|
commentsEnabled: true,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'my channel',
|
displayName: 'my channel',
|
||||||
|
name: 'super_channel_name',
|
||||||
description: 'super channel',
|
description: 'super channel',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
@ -201,7 +202,8 @@ describe('Test multiple servers', function () {
|
||||||
tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
|
tags: [ 'tag1p2', 'tag2p2', 'tag3p2' ],
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main user1 channel',
|
displayName: 'Main user1 channel',
|
||||||
|
name: 'user1_channel',
|
||||||
description: 'super channel',
|
description: 'super channel',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
@ -307,7 +309,8 @@ describe('Test multiple servers', function () {
|
||||||
tags: [ 'tag1p3' ],
|
tags: [ 'tag1p3' ],
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
@ -339,7 +342,8 @@ describe('Test multiple servers', function () {
|
||||||
tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
|
tags: [ 'tag2p3', 'tag3p3', 'tag4p3' ],
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
@ -647,7 +651,8 @@ describe('Test multiple servers', function () {
|
||||||
tags: [ 'tag_up_1', 'tag_up_2' ],
|
tags: [ 'tag_up_1', 'tag_up_2' ],
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
@ -967,7 +972,8 @@ describe('Test multiple servers', function () {
|
||||||
tags: [ ],
|
tags: [ ],
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal
|
isLocal
|
||||||
},
|
},
|
||||||
|
|
|
@ -56,7 +56,8 @@ describe('Test a single server', function () {
|
||||||
privacy: VideoPrivacy.PUBLIC,
|
privacy: VideoPrivacy.PUBLIC,
|
||||||
commentsEnabled: true,
|
commentsEnabled: true,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
displayName: 'Main root channel',
|
||||||
|
name: 'root_channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal: true
|
isLocal: true
|
||||||
},
|
},
|
||||||
|
@ -87,7 +88,8 @@ describe('Test a single server', function () {
|
||||||
duration: 5,
|
duration: 5,
|
||||||
commentsEnabled: false,
|
commentsEnabled: false,
|
||||||
channel: {
|
channel: {
|
||||||
name: 'Main root channel',
|
name: 'root_channel',
|
||||||
|
displayName: 'Main root channel',
|
||||||
description: '',
|
description: '',
|
||||||
isLocal: true
|
isLocal: true
|
||||||
},
|
},
|
||||||
|
|
|
@ -438,18 +438,19 @@ async function completeVideoCheck (
|
||||||
name: string
|
name: string
|
||||||
host: string
|
host: string
|
||||||
}
|
}
|
||||||
isLocal: boolean,
|
isLocal: boolean
|
||||||
tags: string[],
|
tags: string[]
|
||||||
privacy: number,
|
privacy: number
|
||||||
likes?: number,
|
likes?: number
|
||||||
dislikes?: number,
|
dislikes?: number
|
||||||
duration: number,
|
duration: number
|
||||||
channel: {
|
channel: {
|
||||||
name: string,
|
displayName: string
|
||||||
|
name: string
|
||||||
description
|
description
|
||||||
isLocal: boolean
|
isLocal: boolean
|
||||||
}
|
}
|
||||||
fixture: string,
|
fixture: string
|
||||||
files: {
|
files: {
|
||||||
resolution: number
|
resolution: number
|
||||||
size: number
|
size: number
|
||||||
|
@ -476,8 +477,8 @@ async function completeVideoCheck (
|
||||||
expect(video.account.uuid).to.be.a('string')
|
expect(video.account.uuid).to.be.a('string')
|
||||||
expect(video.account.host).to.equal(attributes.account.host)
|
expect(video.account.host).to.equal(attributes.account.host)
|
||||||
expect(video.account.name).to.equal(attributes.account.name)
|
expect(video.account.name).to.equal(attributes.account.name)
|
||||||
expect(video.channel.displayName).to.equal(attributes.channel.name)
|
expect(video.channel.displayName).to.equal(attributes.channel.displayName)
|
||||||
expect(video.channel.name).to.have.lengthOf(36)
|
expect(video.channel.name).to.equal(attributes.channel.name)
|
||||||
expect(video.likes).to.equal(attributes.likes)
|
expect(video.likes).to.equal(attributes.likes)
|
||||||
expect(video.dislikes).to.equal(attributes.dislikes)
|
expect(video.dislikes).to.equal(attributes.dislikes)
|
||||||
expect(video.isLocal).to.equal(attributes.isLocal)
|
expect(video.isLocal).to.equal(attributes.isLocal)
|
||||||
|
@ -497,8 +498,8 @@ async function completeVideoCheck (
|
||||||
expect(videoDetails.tags).to.deep.equal(attributes.tags)
|
expect(videoDetails.tags).to.deep.equal(attributes.tags)
|
||||||
expect(videoDetails.account.name).to.equal(attributes.account.name)
|
expect(videoDetails.account.name).to.equal(attributes.account.name)
|
||||||
expect(videoDetails.account.host).to.equal(attributes.account.host)
|
expect(videoDetails.account.host).to.equal(attributes.account.host)
|
||||||
expect(videoDetails.channel.displayName).to.equal(attributes.channel.name)
|
expect(video.channel.displayName).to.equal(attributes.channel.displayName)
|
||||||
expect(videoDetails.channel.name).to.have.lengthOf(36)
|
expect(video.channel.name).to.equal(attributes.channel.name)
|
||||||
expect(videoDetails.channel.host).to.equal(attributes.account.host)
|
expect(videoDetails.channel.host).to.equal(attributes.account.host)
|
||||||
expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
|
expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
|
||||||
expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
|
expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
|
||||||
|
|
Loading…
Reference in New Issue