video-playlist.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@peertube/peertube-core-utils'
  2. import {
  3. ActivityIconObject,
  4. PlaylistObject,
  5. VideoPlaylist,
  6. VideoPlaylistPrivacy,
  7. VideoPlaylistType,
  8. type VideoPlaylistPrivacyType,
  9. type VideoPlaylistType_Type
  10. } from '@peertube/peertube-models'
  11. import { buildUUID, uuidToShort } from '@peertube/peertube-node-utils'
  12. import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
  13. import { MAccountId, MChannelId, MVideoPlaylistElement } from '@server/types/models/index.js'
  14. import { join } from 'path'
  15. import { FindOptions, Includeable, Op, ScopeOptions, Sequelize, Transaction, WhereOptions, literal } from 'sequelize'
  16. import {
  17. AllowNull,
  18. BelongsTo,
  19. Column,
  20. CreatedAt,
  21. DataType,
  22. Default,
  23. ForeignKey,
  24. HasMany,
  25. HasOne,
  26. Is,
  27. IsUUID, Scopes,
  28. Table,
  29. UpdatedAt
  30. } from 'sequelize-typescript'
  31. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
  32. import {
  33. isVideoPlaylistDescriptionValid,
  34. isVideoPlaylistNameValid,
  35. isVideoPlaylistPrivacyValid
  36. } from '../../helpers/custom-validators/video-playlists.js'
  37. import {
  38. ACTIVITY_PUB,
  39. CONSTRAINTS_FIELDS,
  40. LAZY_STATIC_PATHS,
  41. THUMBNAILS_SIZE,
  42. USER_EXPORT_MAX_ITEMS,
  43. VIDEO_PLAYLIST_PRIVACIES,
  44. VIDEO_PLAYLIST_TYPES,
  45. WEBSERVER
  46. } from '../../initializers/constants.js'
  47. import { MThumbnail } from '../../types/models/video/thumbnail.js'
  48. import {
  49. MVideoPlaylist,
  50. MVideoPlaylistAP,
  51. MVideoPlaylistAccountThumbnail,
  52. MVideoPlaylistFormattable,
  53. MVideoPlaylistFull,
  54. MVideoPlaylistFullSummary,
  55. MVideoPlaylistSummaryWithElements
  56. } from '../../types/models/video/video-playlist.js'
  57. import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account.js'
  58. import { ActorModel } from '../actor/actor.js'
  59. import {
  60. SequelizeModel,
  61. buildServerIdsFollowedBy,
  62. buildTrigramSearchIndex,
  63. buildWhereIdOrUUID,
  64. createSimilarityAttribute,
  65. getPlaylistSort,
  66. isOutdated,
  67. setAsUpdated,
  68. throwIfNotValid
  69. } from '../shared/index.js'
  70. import { ThumbnailModel } from './thumbnail.js'
  71. import { VideoChannelModel, ScopeNames as VideoChannelScopeNames } from './video-channel.js'
  72. import { VideoPlaylistElementModel } from './video-playlist-element.js'
  73. enum ScopeNames {
  74. AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
  75. WITH_VIDEOS_LENGTH = 'WITH_VIDEOS_LENGTH',
  76. WITH_ACCOUNT_AND_CHANNEL_SUMMARY = 'WITH_ACCOUNT_AND_CHANNEL_SUMMARY',
  77. WITH_ACCOUNT = 'WITH_ACCOUNT',
  78. WITH_THUMBNAIL = 'WITH_THUMBNAIL',
  79. WITH_ACCOUNT_AND_CHANNEL = 'WITH_ACCOUNT_AND_CHANNEL'
  80. }
  81. type AvailableForListOptions = {
  82. followerActorId?: number
  83. type?: VideoPlaylistType_Type
  84. accountId?: number
  85. videoChannelId?: number
  86. listMyPlaylists?: boolean
  87. search?: string
  88. host?: string
  89. uuids?: string[]
  90. withVideos?: boolean
  91. forCount?: boolean
  92. }
  93. function getVideoLengthSelect () {
  94. return 'SELECT COUNT("id") FROM "videoPlaylistElement" WHERE "videoPlaylistId" = "VideoPlaylistModel"."id"'
  95. }
  96. @Scopes(() => ({
  97. [ScopeNames.WITH_THUMBNAIL]: {
  98. include: [
  99. {
  100. model: ThumbnailModel,
  101. required: false
  102. }
  103. ]
  104. },
  105. [ScopeNames.WITH_VIDEOS_LENGTH]: {
  106. attributes: {
  107. include: [
  108. [
  109. literal(`(${getVideoLengthSelect()})`),
  110. 'videosLength'
  111. ]
  112. ]
  113. }
  114. } as FindOptions,
  115. [ScopeNames.WITH_ACCOUNT]: {
  116. include: [
  117. {
  118. model: AccountModel,
  119. required: true
  120. }
  121. ]
  122. },
  123. [ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY]: {
  124. include: [
  125. {
  126. model: AccountModel.scope(AccountScopeNames.SUMMARY),
  127. required: true
  128. },
  129. {
  130. model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
  131. required: false
  132. }
  133. ]
  134. },
  135. [ScopeNames.WITH_ACCOUNT_AND_CHANNEL]: {
  136. include: [
  137. {
  138. model: AccountModel,
  139. required: true
  140. },
  141. {
  142. model: VideoChannelModel,
  143. required: false
  144. }
  145. ]
  146. },
  147. [ScopeNames.AVAILABLE_FOR_LIST]: (options: AvailableForListOptions) => {
  148. const whereAnd: WhereOptions[] = []
  149. const whereServer = options.host && options.host !== WEBSERVER.HOST
  150. ? { host: options.host }
  151. : undefined
  152. let whereActor: WhereOptions = {}
  153. if (options.host === WEBSERVER.HOST) {
  154. whereActor = {
  155. [Op.and]: [ { serverId: null } ]
  156. }
  157. }
  158. if (options.listMyPlaylists !== true) {
  159. whereAnd.push({
  160. privacy: VideoPlaylistPrivacy.PUBLIC
  161. })
  162. // … OR playlists that are on an instance followed by actorId
  163. if (options.followerActorId) {
  164. // Only list local playlists
  165. const whereActorOr: WhereOptions[] = [
  166. {
  167. serverId: null
  168. }
  169. ]
  170. const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
  171. whereActorOr.push({
  172. serverId: {
  173. [Op.in]: literal(inQueryInstanceFollow)
  174. }
  175. })
  176. Object.assign(whereActor, { [Op.or]: whereActorOr })
  177. }
  178. }
  179. if (options.accountId) {
  180. whereAnd.push({
  181. ownerAccountId: options.accountId
  182. })
  183. }
  184. if (options.videoChannelId) {
  185. whereAnd.push({
  186. videoChannelId: options.videoChannelId
  187. })
  188. }
  189. if (options.type) {
  190. whereAnd.push({
  191. type: options.type
  192. })
  193. }
  194. if (options.uuids) {
  195. whereAnd.push({
  196. uuid: {
  197. [Op.in]: options.uuids
  198. }
  199. })
  200. }
  201. if (options.withVideos === true) {
  202. whereAnd.push(
  203. literal(`(${getVideoLengthSelect()}) != 0`)
  204. )
  205. }
  206. let attributesInclude: any[] = [ literal('0 as similarity') ]
  207. if (options.search) {
  208. const escapedSearch = VideoPlaylistModel.sequelize.escape(options.search)
  209. const escapedLikeSearch = VideoPlaylistModel.sequelize.escape('%' + options.search + '%')
  210. attributesInclude = [ createSimilarityAttribute('VideoPlaylistModel.name', options.search) ]
  211. whereAnd.push({
  212. [Op.or]: [
  213. Sequelize.literal(
  214. 'lower(immutable_unaccent("VideoPlaylistModel"."name")) % lower(immutable_unaccent(' + escapedSearch + '))'
  215. ),
  216. Sequelize.literal(
  217. 'lower(immutable_unaccent("VideoPlaylistModel"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))'
  218. )
  219. ]
  220. })
  221. }
  222. const where = {
  223. [Op.and]: whereAnd
  224. }
  225. const include: Includeable[] = [
  226. {
  227. model: AccountModel.scope({
  228. method: [ AccountScopeNames.SUMMARY, { whereActor, whereServer, forCount: options.forCount } as SummaryOptions ]
  229. }),
  230. required: true
  231. }
  232. ]
  233. if (options.forCount !== true) {
  234. include.push({
  235. model: VideoChannelModel.scope(VideoChannelScopeNames.SUMMARY),
  236. required: false
  237. })
  238. }
  239. return {
  240. attributes: {
  241. include: attributesInclude
  242. },
  243. where,
  244. include
  245. } as FindOptions
  246. }
  247. }))
  248. @Table({
  249. tableName: 'videoPlaylist',
  250. indexes: [
  251. buildTrigramSearchIndex('video_playlist_name_trigram', 'name'),
  252. {
  253. fields: [ 'ownerAccountId' ]
  254. },
  255. {
  256. fields: [ 'videoChannelId' ]
  257. },
  258. {
  259. fields: [ 'url' ],
  260. unique: true
  261. }
  262. ]
  263. })
  264. export class VideoPlaylistModel extends SequelizeModel<VideoPlaylistModel> {
  265. @CreatedAt
  266. createdAt: Date
  267. @UpdatedAt
  268. updatedAt: Date
  269. @AllowNull(false)
  270. @Is('VideoPlaylistName', value => throwIfNotValid(value, isVideoPlaylistNameValid, 'name'))
  271. @Column
  272. name: string
  273. @AllowNull(true)
  274. @Is('VideoPlaylistDescription', value => throwIfNotValid(value, isVideoPlaylistDescriptionValid, 'description', true))
  275. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.DESCRIPTION.max))
  276. description: string
  277. @AllowNull(false)
  278. @Is('VideoPlaylistPrivacy', value => throwIfNotValid(value, isVideoPlaylistPrivacyValid, 'privacy'))
  279. @Column
  280. privacy: VideoPlaylistPrivacyType
  281. @AllowNull(false)
  282. @Is('VideoPlaylistUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  283. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.URL.max))
  284. url: string
  285. @AllowNull(false)
  286. @Default(DataType.UUIDV4)
  287. @IsUUID(4)
  288. @Column(DataType.UUID)
  289. uuid: string
  290. @AllowNull(false)
  291. @Default(VideoPlaylistType.REGULAR)
  292. @Column
  293. type: VideoPlaylistType_Type
  294. @ForeignKey(() => AccountModel)
  295. @Column
  296. ownerAccountId: number
  297. @BelongsTo(() => AccountModel, {
  298. foreignKey: {
  299. allowNull: false
  300. },
  301. onDelete: 'CASCADE'
  302. })
  303. OwnerAccount: Awaited<AccountModel>
  304. @ForeignKey(() => VideoChannelModel)
  305. @Column
  306. videoChannelId: number
  307. @BelongsTo(() => VideoChannelModel, {
  308. foreignKey: {
  309. allowNull: true
  310. },
  311. onDelete: 'CASCADE'
  312. })
  313. VideoChannel: Awaited<VideoChannelModel>
  314. @HasMany(() => VideoPlaylistElementModel, {
  315. foreignKey: {
  316. name: 'videoPlaylistId',
  317. allowNull: false
  318. },
  319. onDelete: 'CASCADE'
  320. })
  321. VideoPlaylistElements: Awaited<VideoPlaylistElementModel>[]
  322. @HasOne(() => ThumbnailModel, {
  323. foreignKey: {
  324. name: 'videoPlaylistId',
  325. allowNull: true
  326. },
  327. onDelete: 'CASCADE',
  328. hooks: true
  329. })
  330. Thumbnail: Awaited<ThumbnailModel>
  331. static listForApi (options: AvailableForListOptions & {
  332. start: number
  333. count: number
  334. sort: string
  335. }) {
  336. const query = {
  337. offset: options.start,
  338. limit: options.count,
  339. order: getPlaylistSort(options.sort)
  340. }
  341. const commonAvailableForListOptions = pick(options, [
  342. 'type',
  343. 'followerActorId',
  344. 'accountId',
  345. 'videoChannelId',
  346. 'listMyPlaylists',
  347. 'search',
  348. 'host',
  349. 'uuids'
  350. ])
  351. const scopesFind: (string | ScopeOptions)[] = [
  352. {
  353. method: [
  354. ScopeNames.AVAILABLE_FOR_LIST,
  355. {
  356. ...commonAvailableForListOptions,
  357. withVideos: options.withVideos || false
  358. } as AvailableForListOptions
  359. ]
  360. },
  361. ScopeNames.WITH_VIDEOS_LENGTH,
  362. ScopeNames.WITH_THUMBNAIL
  363. ]
  364. const scopesCount: (string | ScopeOptions)[] = [
  365. {
  366. method: [
  367. ScopeNames.AVAILABLE_FOR_LIST,
  368. {
  369. ...commonAvailableForListOptions,
  370. withVideos: options.withVideos || false,
  371. forCount: true
  372. } as AvailableForListOptions
  373. ]
  374. },
  375. ScopeNames.WITH_VIDEOS_LENGTH
  376. ]
  377. return Promise.all([
  378. VideoPlaylistModel.scope(scopesCount).count(),
  379. VideoPlaylistModel.scope(scopesFind).findAll(query)
  380. ]).then(([ count, rows ]) => ({ total: count, data: rows }))
  381. }
  382. static searchForApi (options: Pick<AvailableForListOptions, 'followerActorId' | 'search' | 'host' | 'uuids'> & {
  383. start: number
  384. count: number
  385. sort: string
  386. }) {
  387. return VideoPlaylistModel.listForApi({
  388. ...options,
  389. type: VideoPlaylistType.REGULAR,
  390. listMyPlaylists: false,
  391. withVideos: true
  392. })
  393. }
  394. static listPublicUrlsOfForAP (options: { account?: MAccountId, channel?: MChannelId }, start: number, count: number) {
  395. const where = {
  396. privacy: VideoPlaylistPrivacy.PUBLIC
  397. }
  398. if (options.account) {
  399. Object.assign(where, { ownerAccountId: options.account.id })
  400. }
  401. if (options.channel) {
  402. Object.assign(where, { videoChannelId: options.channel.id })
  403. }
  404. const getQuery = (forCount: boolean) => {
  405. return {
  406. attributes: forCount === true
  407. ? []
  408. : [ 'url' ],
  409. offset: start,
  410. limit: count,
  411. where
  412. }
  413. }
  414. return Promise.all([
  415. VideoPlaylistModel.count(getQuery(true)),
  416. VideoPlaylistModel.findAll(getQuery(false))
  417. ]).then(([ total, rows ]) => ({
  418. total,
  419. data: rows.map(p => p.url)
  420. }))
  421. }
  422. static listPlaylistSummariesOf (accountId: number, videoIds: number[]): Promise<MVideoPlaylistSummaryWithElements[]> {
  423. const query = {
  424. attributes: [ 'id', 'name', 'uuid' ],
  425. where: {
  426. ownerAccountId: accountId
  427. },
  428. include: [
  429. {
  430. attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ],
  431. model: VideoPlaylistElementModel.unscoped(),
  432. where: {
  433. videoId: {
  434. [Op.in]: videoIds
  435. }
  436. },
  437. required: true
  438. }
  439. ]
  440. }
  441. return VideoPlaylistModel.findAll(query)
  442. }
  443. static listPlaylistForExport (accountId: number): Promise<MVideoPlaylistFull[]> {
  444. return VideoPlaylistModel
  445. .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
  446. .findAll({
  447. where: {
  448. ownerAccountId: accountId
  449. },
  450. limit: USER_EXPORT_MAX_ITEMS
  451. })
  452. }
  453. // ---------------------------------------------------------------------------
  454. static doesPlaylistExist (url: string) {
  455. const query = {
  456. attributes: [ 'id' ],
  457. where: {
  458. url
  459. }
  460. }
  461. return VideoPlaylistModel
  462. .findOne(query)
  463. .then(e => !!e)
  464. }
  465. static loadWithAccountAndChannelSummary (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFullSummary> {
  466. const where = buildWhereIdOrUUID(id)
  467. const query = {
  468. where,
  469. transaction
  470. }
  471. return VideoPlaylistModel
  472. .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
  473. .findOne(query)
  474. }
  475. static loadWithAccountAndChannel (id: number | string, transaction: Transaction): Promise<MVideoPlaylistFull> {
  476. const where = buildWhereIdOrUUID(id)
  477. const query = {
  478. where,
  479. transaction
  480. }
  481. return VideoPlaylistModel
  482. .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
  483. .findOne(query)
  484. }
  485. static loadByUrlAndPopulateAccount (url: string): Promise<MVideoPlaylistAccountThumbnail> {
  486. const query = {
  487. where: {
  488. url
  489. }
  490. }
  491. return VideoPlaylistModel.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_THUMBNAIL ]).findOne(query)
  492. }
  493. static loadByUrlWithAccountAndChannelSummary (url: string): Promise<MVideoPlaylistFullSummary> {
  494. const query = {
  495. where: {
  496. url
  497. }
  498. }
  499. return VideoPlaylistModel
  500. .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL_SUMMARY, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
  501. .findOne(query)
  502. }
  503. static loadWatchLaterOf (account: MAccountId): Promise<MVideoPlaylistFull> {
  504. const query = {
  505. where: {
  506. type: VideoPlaylistType.WATCH_LATER,
  507. ownerAccountId: account.id
  508. }
  509. }
  510. return VideoPlaylistModel
  511. .scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
  512. .findOne(query)
  513. }
  514. static loadRegularByAccountAndName (account: MAccountId, name: string): Promise<MVideoPlaylist> {
  515. const query = {
  516. where: {
  517. type: VideoPlaylistType.REGULAR,
  518. name,
  519. ownerAccountId: account.id
  520. }
  521. }
  522. return VideoPlaylistModel
  523. .findOne(query)
  524. }
  525. static getPrivacyLabel (privacy: VideoPlaylistPrivacyType) {
  526. return VIDEO_PLAYLIST_PRIVACIES[privacy] || 'Unknown'
  527. }
  528. static getTypeLabel (type: VideoPlaylistType_Type) {
  529. return VIDEO_PLAYLIST_TYPES[type] || 'Unknown'
  530. }
  531. static resetPlaylistsOfChannel (videoChannelId: number, transaction: Transaction) {
  532. const query = {
  533. where: {
  534. videoChannelId
  535. },
  536. transaction
  537. }
  538. return VideoPlaylistModel.update({ privacy: VideoPlaylistPrivacy.PRIVATE, videoChannelId: null }, query)
  539. }
  540. async setAndSaveThumbnail (thumbnail: MThumbnail, t: Transaction) {
  541. thumbnail.videoPlaylistId = this.id
  542. this.Thumbnail = await thumbnail.save({ transaction: t })
  543. }
  544. hasThumbnail () {
  545. return !!this.Thumbnail
  546. }
  547. hasGeneratedThumbnail () {
  548. return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
  549. }
  550. shouldGenerateThumbnailWithNewElement (newElement: MVideoPlaylistElement) {
  551. if (this.hasThumbnail() === false) return true
  552. if (newElement.position === 1 && this.hasGeneratedThumbnail()) return true
  553. return false
  554. }
  555. generateThumbnailName () {
  556. const extension = '.jpg'
  557. return 'playlist-' + buildUUID() + extension
  558. }
  559. getThumbnailUrl () {
  560. if (!this.hasThumbnail()) return null
  561. return WEBSERVER.URL + LAZY_STATIC_PATHS.THUMBNAILS + this.Thumbnail.filename
  562. }
  563. getThumbnailStaticPath () {
  564. if (!this.hasThumbnail()) return null
  565. return join(LAZY_STATIC_PATHS.THUMBNAILS, this.Thumbnail.filename)
  566. }
  567. getWatchStaticPath () {
  568. return buildPlaylistWatchPath({ shortUUID: uuidToShort(this.uuid) })
  569. }
  570. getEmbedStaticPath () {
  571. return buildPlaylistEmbedPath(this)
  572. }
  573. static async getStats () {
  574. const totalLocalPlaylists = await VideoPlaylistModel.count({
  575. include: [
  576. {
  577. model: AccountModel.unscoped(),
  578. required: true,
  579. include: [
  580. {
  581. model: ActorModel.unscoped(),
  582. required: true,
  583. where: {
  584. serverId: null
  585. }
  586. }
  587. ]
  588. }
  589. ],
  590. where: {
  591. privacy: VideoPlaylistPrivacy.PUBLIC
  592. }
  593. })
  594. return {
  595. totalLocalPlaylists
  596. }
  597. }
  598. setAsRefreshed () {
  599. return setAsUpdated({ sequelize: this.sequelize, table: 'videoPlaylist', id: this.id })
  600. }
  601. setVideosLength (videosLength: number) {
  602. this.set('videosLength' as any, videosLength, { raw: true })
  603. }
  604. isOwned () {
  605. return this.OwnerAccount.isOwned()
  606. }
  607. isOutdated () {
  608. if (this.isOwned()) return false
  609. return isOutdated(this, ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL)
  610. }
  611. toFormattedJSON (this: MVideoPlaylistFormattable): VideoPlaylist {
  612. return {
  613. id: this.id,
  614. uuid: this.uuid,
  615. shortUUID: uuidToShort(this.uuid),
  616. isLocal: this.isOwned(),
  617. url: this.url,
  618. displayName: this.name,
  619. description: this.description,
  620. privacy: {
  621. id: this.privacy,
  622. label: VideoPlaylistModel.getPrivacyLabel(this.privacy)
  623. },
  624. thumbnailPath: this.getThumbnailStaticPath(),
  625. embedPath: this.getEmbedStaticPath(),
  626. type: {
  627. id: this.type,
  628. label: VideoPlaylistModel.getTypeLabel(this.type)
  629. },
  630. videosLength: this.get('videosLength') as number,
  631. createdAt: this.createdAt,
  632. updatedAt: this.updatedAt,
  633. ownerAccount: this.OwnerAccount.toFormattedSummaryJSON(),
  634. videoChannel: this.VideoChannel
  635. ? this.VideoChannel.toFormattedSummaryJSON()
  636. : null
  637. }
  638. }
  639. toActivityPubObject (this: MVideoPlaylistAP, page: number, t: Transaction): Promise<PlaylistObject> {
  640. const handler = (start: number, count: number) => {
  641. return VideoPlaylistElementModel.listUrlsOfForAP(this.id, start, count, t)
  642. }
  643. let icon: ActivityIconObject
  644. if (this.hasThumbnail()) {
  645. icon = {
  646. type: 'Image' as 'Image',
  647. url: this.getThumbnailUrl(),
  648. mediaType: 'image/jpeg' as 'image/jpeg',
  649. width: THUMBNAILS_SIZE.width,
  650. height: THUMBNAILS_SIZE.height
  651. }
  652. }
  653. return activityPubCollectionPagination(this.url, handler, page)
  654. .then(o => {
  655. return Object.assign(o, {
  656. type: 'Playlist' as 'Playlist',
  657. name: this.name,
  658. content: this.description,
  659. mediaType: 'text/markdown' as 'text/markdown',
  660. uuid: this.uuid,
  661. published: this.createdAt.toISOString(),
  662. updated: this.updatedAt.toISOString(),
  663. attributedTo: this.VideoChannel ? [ this.VideoChannel.Actor.url ] : [],
  664. icon
  665. })
  666. })
  667. }
  668. }