videos.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649
  1. /* tslint:disable:no-unused-expression */
  2. import { expect } from 'chai'
  3. import { pathExists, readdir, readFile } from 'fs-extra'
  4. import * as parseTorrent from 'parse-torrent'
  5. import { extname, join } from 'path'
  6. import * as request from 'supertest'
  7. import {
  8. buildAbsoluteFixturePath,
  9. getMyUserInformation,
  10. immutableAssign,
  11. makeGetRequest,
  12. makePutBodyRequest,
  13. makeUploadRequest,
  14. root,
  15. ServerInfo,
  16. testImage
  17. } from '../'
  18. import * as validator from 'validator'
  19. import { VideoDetails, VideoPrivacy } from '../../models/videos'
  20. import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, loadLanguages, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
  21. import { dateIsValid, webtorrentAdd, buildServerDirectory } from '../miscs/miscs'
  22. loadLanguages()
  23. type VideoAttributes = {
  24. name?: string
  25. category?: number
  26. licence?: number
  27. language?: string
  28. nsfw?: boolean
  29. commentsEnabled?: boolean
  30. downloadEnabled?: boolean
  31. waitTranscoding?: boolean
  32. description?: string
  33. originallyPublishedAt?: string
  34. tags?: string[]
  35. channelId?: number
  36. privacy?: VideoPrivacy
  37. fixture?: string
  38. thumbnailfile?: string
  39. previewfile?: string
  40. scheduleUpdate?: {
  41. updateAt: string
  42. privacy?: VideoPrivacy
  43. }
  44. }
  45. function getVideoCategories (url: string) {
  46. const path = '/api/v1/videos/categories'
  47. return makeGetRequest({
  48. url,
  49. path,
  50. statusCodeExpected: 200
  51. })
  52. }
  53. function getVideoLicences (url: string) {
  54. const path = '/api/v1/videos/licences'
  55. return makeGetRequest({
  56. url,
  57. path,
  58. statusCodeExpected: 200
  59. })
  60. }
  61. function getVideoLanguages (url: string) {
  62. const path = '/api/v1/videos/languages'
  63. return makeGetRequest({
  64. url,
  65. path,
  66. statusCodeExpected: 200
  67. })
  68. }
  69. function getVideoPrivacies (url: string) {
  70. const path = '/api/v1/videos/privacies'
  71. return makeGetRequest({
  72. url,
  73. path,
  74. statusCodeExpected: 200
  75. })
  76. }
  77. function getVideo (url: string, id: number | string, expectedStatus = 200) {
  78. const path = '/api/v1/videos/' + id
  79. return request(url)
  80. .get(path)
  81. .set('Accept', 'application/json')
  82. .expect(expectedStatus)
  83. }
  84. function viewVideo (url: string, id: number | string, expectedStatus = 204, xForwardedFor?: string) {
  85. const path = '/api/v1/videos/' + id + '/views'
  86. const req = request(url)
  87. .post(path)
  88. .set('Accept', 'application/json')
  89. if (xForwardedFor) {
  90. req.set('X-Forwarded-For', xForwardedFor)
  91. }
  92. return req.expect(expectedStatus)
  93. }
  94. function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = 200) {
  95. const path = '/api/v1/videos/' + id
  96. return request(url)
  97. .get(path)
  98. .set('Authorization', 'Bearer ' + token)
  99. .set('Accept', 'application/json')
  100. .expect(expectedStatus)
  101. }
  102. function getVideoDescription (url: string, descriptionPath: string) {
  103. return request(url)
  104. .get(descriptionPath)
  105. .set('Accept', 'application/json')
  106. .expect(200)
  107. .expect('Content-Type', /json/)
  108. }
  109. function getVideosList (url: string) {
  110. const path = '/api/v1/videos'
  111. return request(url)
  112. .get(path)
  113. .query({ sort: 'name' })
  114. .set('Accept', 'application/json')
  115. .expect(200)
  116. .expect('Content-Type', /json/)
  117. }
  118. function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
  119. const path = '/api/v1/videos'
  120. return request(url)
  121. .get(path)
  122. .set('Authorization', 'Bearer ' + token)
  123. .query(immutableAssign(query, { sort: 'name' }))
  124. .set('Accept', 'application/json')
  125. .expect(200)
  126. .expect('Content-Type', /json/)
  127. }
  128. function getLocalVideos (url: string) {
  129. const path = '/api/v1/videos'
  130. return request(url)
  131. .get(path)
  132. .query({ sort: 'name', filter: 'local' })
  133. .set('Accept', 'application/json')
  134. .expect(200)
  135. .expect('Content-Type', /json/)
  136. }
  137. function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string) {
  138. const path = '/api/v1/users/me/videos'
  139. const req = request(url)
  140. .get(path)
  141. .query({ start: start })
  142. .query({ count: count })
  143. if (sort) req.query({ sort })
  144. return req.set('Accept', 'application/json')
  145. .set('Authorization', 'Bearer ' + accessToken)
  146. .expect(200)
  147. .expect('Content-Type', /json/)
  148. }
  149. function getAccountVideos (
  150. url: string,
  151. accessToken: string,
  152. accountName: string,
  153. start: number,
  154. count: number,
  155. sort?: string,
  156. query: { nsfw?: boolean } = {}
  157. ) {
  158. const path = '/api/v1/accounts/' + accountName + '/videos'
  159. return makeGetRequest({
  160. url,
  161. path,
  162. query: immutableAssign(query, {
  163. start,
  164. count,
  165. sort
  166. }),
  167. token: accessToken,
  168. statusCodeExpected: 200
  169. })
  170. }
  171. function getVideoChannelVideos (
  172. url: string,
  173. accessToken: string,
  174. videoChannelName: string,
  175. start: number,
  176. count: number,
  177. sort?: string,
  178. query: { nsfw?: boolean } = {}
  179. ) {
  180. const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
  181. return makeGetRequest({
  182. url,
  183. path,
  184. query: immutableAssign(query, {
  185. start,
  186. count,
  187. sort
  188. }),
  189. token: accessToken,
  190. statusCodeExpected: 200
  191. })
  192. }
  193. function getPlaylistVideos (
  194. url: string,
  195. accessToken: string,
  196. playlistId: number | string,
  197. start: number,
  198. count: number,
  199. query: { nsfw?: boolean } = {}
  200. ) {
  201. const path = '/api/v1/video-playlists/' + playlistId + '/videos'
  202. return makeGetRequest({
  203. url,
  204. path,
  205. query: immutableAssign(query, {
  206. start,
  207. count
  208. }),
  209. token: accessToken,
  210. statusCodeExpected: 200
  211. })
  212. }
  213. function getVideosListPagination (url: string, start: number, count: number, sort?: string) {
  214. const path = '/api/v1/videos'
  215. const req = request(url)
  216. .get(path)
  217. .query({ start: start })
  218. .query({ count: count })
  219. if (sort) req.query({ sort })
  220. return req.set('Accept', 'application/json')
  221. .expect(200)
  222. .expect('Content-Type', /json/)
  223. }
  224. function getVideosListSort (url: string, sort: string) {
  225. const path = '/api/v1/videos'
  226. return request(url)
  227. .get(path)
  228. .query({ sort: sort })
  229. .set('Accept', 'application/json')
  230. .expect(200)
  231. .expect('Content-Type', /json/)
  232. }
  233. function getVideosWithFilters (url: string, query: { tagsAllOf: string[], categoryOneOf: number[] | number }) {
  234. const path = '/api/v1/videos'
  235. return request(url)
  236. .get(path)
  237. .query(query)
  238. .set('Accept', 'application/json')
  239. .expect(200)
  240. .expect('Content-Type', /json/)
  241. }
  242. function removeVideo (url: string, token: string, id: number | string, expectedStatus = 204) {
  243. const path = '/api/v1/videos'
  244. return request(url)
  245. .delete(path + '/' + id)
  246. .set('Accept', 'application/json')
  247. .set('Authorization', 'Bearer ' + token)
  248. .expect(expectedStatus)
  249. }
  250. async function checkVideoFilesWereRemoved (
  251. videoUUID: string,
  252. serverNumber: number,
  253. directories = [
  254. 'redundancy',
  255. 'videos',
  256. 'thumbnails',
  257. 'torrents',
  258. 'previews',
  259. 'captions',
  260. join('playlists', 'hls'),
  261. join('redundancy', 'hls')
  262. ]
  263. ) {
  264. for (const directory of directories) {
  265. const directoryPath = buildServerDirectory(serverNumber, directory)
  266. const directoryExists = await pathExists(directoryPath)
  267. if (directoryExists === false) continue
  268. const files = await readdir(directoryPath)
  269. for (const file of files) {
  270. expect(file).to.not.contain(videoUUID)
  271. }
  272. }
  273. }
  274. async function uploadVideo (url: string, accessToken: string, videoAttributesArg: VideoAttributes, specialStatus = 200) {
  275. const path = '/api/v1/videos/upload'
  276. let defaultChannelId = '1'
  277. try {
  278. const res = await getMyUserInformation(url, accessToken)
  279. defaultChannelId = res.body.videoChannels[0].id
  280. } catch (e) { /* empty */ }
  281. // Override default attributes
  282. const attributes = Object.assign({
  283. name: 'my super video',
  284. category: 5,
  285. licence: 4,
  286. language: 'zh',
  287. channelId: defaultChannelId,
  288. nsfw: true,
  289. waitTranscoding: false,
  290. description: 'my super description',
  291. support: 'my super support text',
  292. tags: [ 'tag' ],
  293. privacy: VideoPrivacy.PUBLIC,
  294. commentsEnabled: true,
  295. downloadEnabled: true,
  296. fixture: 'video_short.webm'
  297. }, videoAttributesArg)
  298. const req = request(url)
  299. .post(path)
  300. .set('Accept', 'application/json')
  301. .set('Authorization', 'Bearer ' + accessToken)
  302. .field('name', attributes.name)
  303. .field('nsfw', JSON.stringify(attributes.nsfw))
  304. .field('commentsEnabled', JSON.stringify(attributes.commentsEnabled))
  305. .field('downloadEnabled', JSON.stringify(attributes.downloadEnabled))
  306. .field('waitTranscoding', JSON.stringify(attributes.waitTranscoding))
  307. .field('privacy', attributes.privacy.toString())
  308. .field('channelId', attributes.channelId)
  309. if (attributes.support !== undefined) {
  310. req.field('support', attributes.support)
  311. }
  312. if (attributes.description !== undefined) {
  313. req.field('description', attributes.description)
  314. }
  315. if (attributes.language !== undefined) {
  316. req.field('language', attributes.language.toString())
  317. }
  318. if (attributes.category !== undefined) {
  319. req.field('category', attributes.category.toString())
  320. }
  321. if (attributes.licence !== undefined) {
  322. req.field('licence', attributes.licence.toString())
  323. }
  324. const tags = attributes.tags || []
  325. for (let i = 0; i < tags.length; i++) {
  326. req.field('tags[' + i + ']', attributes.tags[i])
  327. }
  328. if (attributes.thumbnailfile !== undefined) {
  329. req.attach('thumbnailfile', buildAbsoluteFixturePath(attributes.thumbnailfile))
  330. }
  331. if (attributes.previewfile !== undefined) {
  332. req.attach('previewfile', buildAbsoluteFixturePath(attributes.previewfile))
  333. }
  334. if (attributes.scheduleUpdate) {
  335. req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
  336. if (attributes.scheduleUpdate.privacy) {
  337. req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
  338. }
  339. }
  340. if (attributes.originallyPublishedAt !== undefined) {
  341. req.field('originallyPublishedAt', attributes.originallyPublishedAt)
  342. }
  343. return req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
  344. .expect(specialStatus)
  345. }
  346. function updateVideo (url: string, accessToken: string, id: number | string, attributes: VideoAttributes, statusCodeExpected = 204) {
  347. const path = '/api/v1/videos/' + id
  348. const body = {}
  349. if (attributes.name) body['name'] = attributes.name
  350. if (attributes.category) body['category'] = attributes.category
  351. if (attributes.licence) body['licence'] = attributes.licence
  352. if (attributes.language) body['language'] = attributes.language
  353. if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
  354. if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
  355. if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
  356. if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
  357. if (attributes.description) body['description'] = attributes.description
  358. if (attributes.tags) body['tags'] = attributes.tags
  359. if (attributes.privacy) body['privacy'] = attributes.privacy
  360. if (attributes.channelId) body['channelId'] = attributes.channelId
  361. if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
  362. // Upload request
  363. if (attributes.thumbnailfile || attributes.previewfile) {
  364. const attaches: any = {}
  365. if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
  366. if (attributes.previewfile) attaches.previewfile = attributes.previewfile
  367. return makeUploadRequest({
  368. url,
  369. method: 'PUT',
  370. path,
  371. token: accessToken,
  372. fields: body,
  373. attaches,
  374. statusCodeExpected
  375. })
  376. }
  377. return makePutBodyRequest({
  378. url,
  379. path,
  380. fields: body,
  381. token: accessToken,
  382. statusCodeExpected
  383. })
  384. }
  385. function rateVideo (url: string, accessToken: string, id: number, rating: string, specialStatus = 204) {
  386. const path = '/api/v1/videos/' + id + '/rate'
  387. return request(url)
  388. .put(path)
  389. .set('Accept', 'application/json')
  390. .set('Authorization', 'Bearer ' + accessToken)
  391. .send({ rating })
  392. .expect(specialStatus)
  393. }
  394. function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
  395. return new Promise<any>((res, rej) => {
  396. const torrentName = videoUUID + '-' + resolution + '.torrent'
  397. const torrentPath = join(root(), 'test' + server.serverNumber, 'torrents', torrentName)
  398. readFile(torrentPath, (err, data) => {
  399. if (err) return rej(err)
  400. return res(parseTorrent(data))
  401. })
  402. })
  403. }
  404. async function completeVideoCheck (
  405. url: string,
  406. video: any,
  407. attributes: {
  408. name: string
  409. category: number
  410. licence: number
  411. language: string
  412. nsfw: boolean
  413. commentsEnabled: boolean
  414. downloadEnabled: boolean
  415. description: string
  416. publishedAt?: string
  417. support: string
  418. originallyPublishedAt?: string,
  419. account: {
  420. name: string
  421. host: string
  422. }
  423. isLocal: boolean
  424. tags: string[]
  425. privacy: number
  426. likes?: number
  427. dislikes?: number
  428. duration: number
  429. channel: {
  430. displayName: string
  431. name: string
  432. description
  433. isLocal: boolean
  434. }
  435. fixture: string
  436. files: {
  437. resolution: number
  438. size: number
  439. }[],
  440. thumbnailfile?: string
  441. previewfile?: string
  442. }
  443. ) {
  444. if (!attributes.likes) attributes.likes = 0
  445. if (!attributes.dislikes) attributes.dislikes = 0
  446. expect(video.name).to.equal(attributes.name)
  447. expect(video.category.id).to.equal(attributes.category)
  448. expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
  449. expect(video.licence.id).to.equal(attributes.licence)
  450. expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
  451. expect(video.language.id).to.equal(attributes.language)
  452. expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
  453. expect(video.privacy.id).to.deep.equal(attributes.privacy)
  454. expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
  455. expect(video.nsfw).to.equal(attributes.nsfw)
  456. expect(video.description).to.equal(attributes.description)
  457. expect(video.account.id).to.be.a('number')
  458. expect(video.account.host).to.equal(attributes.account.host)
  459. expect(video.account.name).to.equal(attributes.account.name)
  460. expect(video.channel.displayName).to.equal(attributes.channel.displayName)
  461. expect(video.channel.name).to.equal(attributes.channel.name)
  462. expect(video.likes).to.equal(attributes.likes)
  463. expect(video.dislikes).to.equal(attributes.dislikes)
  464. expect(video.isLocal).to.equal(attributes.isLocal)
  465. expect(video.duration).to.equal(attributes.duration)
  466. expect(dateIsValid(video.createdAt)).to.be.true
  467. expect(dateIsValid(video.publishedAt)).to.be.true
  468. expect(dateIsValid(video.updatedAt)).to.be.true
  469. if (attributes.publishedAt) {
  470. expect(video.publishedAt).to.equal(attributes.publishedAt)
  471. }
  472. if (attributes.originallyPublishedAt) {
  473. expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
  474. } else {
  475. expect(video.originallyPublishedAt).to.be.null
  476. }
  477. const res = await getVideo(url, video.uuid)
  478. const videoDetails: VideoDetails = res.body
  479. expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
  480. expect(videoDetails.tags).to.deep.equal(attributes.tags)
  481. expect(videoDetails.account.name).to.equal(attributes.account.name)
  482. expect(videoDetails.account.host).to.equal(attributes.account.host)
  483. expect(video.channel.displayName).to.equal(attributes.channel.displayName)
  484. expect(video.channel.name).to.equal(attributes.channel.name)
  485. expect(videoDetails.channel.host).to.equal(attributes.account.host)
  486. expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
  487. expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
  488. expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
  489. expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
  490. expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
  491. for (const attributeFile of attributes.files) {
  492. const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
  493. expect(file).not.to.be.undefined
  494. let extension = extname(attributes.fixture)
  495. // Transcoding enabled: extension will always be .mp4
  496. if (attributes.files.length > 1) extension = '.mp4'
  497. expect(file.magnetUri).to.have.lengthOf.above(2)
  498. expect(file.torrentUrl).to.equal(`http://${attributes.account.host}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`)
  499. expect(file.fileUrl).to.equal(`http://${attributes.account.host}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`)
  500. expect(file.resolution.id).to.equal(attributeFile.resolution)
  501. expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
  502. const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
  503. const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
  504. expect(file.size,
  505. 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')')
  506. .to.be.above(minSize).and.below(maxSize)
  507. {
  508. await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
  509. }
  510. if (attributes.previewfile) {
  511. await testImage(url, attributes.previewfile, videoDetails.previewPath)
  512. }
  513. const torrent = await webtorrentAdd(file.magnetUri, true)
  514. expect(torrent.files).to.be.an('array')
  515. expect(torrent.files.length).to.equal(1)
  516. expect(torrent.files[0].path).to.exist.and.to.not.equal('')
  517. }
  518. }
  519. async function videoUUIDToId (url: string, id: number | string) {
  520. if (validator.isUUID('' + id) === false) return id
  521. const res = await getVideo(url, id)
  522. return res.body.id
  523. }
  524. async function uploadVideoAndGetId (options: { server: ServerInfo, videoName: string, nsfw?: boolean, token?: string }) {
  525. const videoAttrs: any = { name: options.videoName }
  526. if (options.nsfw) videoAttrs.nsfw = options.nsfw
  527. const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
  528. return { id: res.body.video.id, uuid: res.body.video.uuid }
  529. }
  530. // ---------------------------------------------------------------------------
  531. export {
  532. getVideoDescription,
  533. getVideoCategories,
  534. getVideoLicences,
  535. videoUUIDToId,
  536. getVideoPrivacies,
  537. getVideoLanguages,
  538. getMyVideos,
  539. getAccountVideos,
  540. getVideoChannelVideos,
  541. getVideo,
  542. getVideoWithToken,
  543. getVideosList,
  544. getVideosListPagination,
  545. getVideosListSort,
  546. removeVideo,
  547. getVideosListWithToken,
  548. uploadVideo,
  549. getVideosWithFilters,
  550. updateVideo,
  551. rateVideo,
  552. viewVideo,
  553. parseTorrentVideo,
  554. getLocalVideos,
  555. completeVideoCheck,
  556. checkVideoFilesWereRemoved,
  557. getPlaylistVideos,
  558. uploadVideoAndGetId
  559. }