mirror of https://github.com/Chocobozzz/PeerTube
Fix playlist elements merge on import
parent
aff87c12ff
commit
7be401ac76
|
@ -14,6 +14,7 @@ import {
|
||||||
VideoPlaylistPrivacy,
|
VideoPlaylistPrivacy,
|
||||||
VideoPlaylistPrivacyType,
|
VideoPlaylistPrivacyType,
|
||||||
VideoPlaylistReorder,
|
VideoPlaylistReorder,
|
||||||
|
VideoPlaylistType,
|
||||||
VideoPlaylistType_Type,
|
VideoPlaylistType_Type,
|
||||||
VideoPlaylistUpdate
|
VideoPlaylistUpdate
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
|
@ -82,6 +83,8 @@ export class PlaylistsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
get (options: OverrideCommandOptions & {
|
get (options: OverrideCommandOptions & {
|
||||||
playlistId: number | string
|
playlistId: number | string
|
||||||
}) {
|
}) {
|
||||||
|
@ -97,6 +100,20 @@ export class PlaylistsCommand extends AbstractCommand {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getWatchLater (options: OverrideCommandOptions & {
|
||||||
|
handle: string
|
||||||
|
}) {
|
||||||
|
const { data: playlists } = await this.listByAccount({
|
||||||
|
...options,
|
||||||
|
|
||||||
|
playlistType: VideoPlaylistType.WATCH_LATER
|
||||||
|
})
|
||||||
|
|
||||||
|
return playlists[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
listVideos (options: OverrideCommandOptions & {
|
listVideos (options: OverrideCommandOptions & {
|
||||||
playlistId: number | string
|
playlistId: number | string
|
||||||
start?: number
|
start?: number
|
||||||
|
|
|
@ -97,15 +97,14 @@ function runTest (withObjectStorage: boolean) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add a video in watch later playlist
|
// Add a video in watch later playlist
|
||||||
const { data: playlists } = await server.playlists.listByAccount({
|
await server.playlists.addElement({
|
||||||
token: noahToken,
|
playlistId: (await server.playlists.getWatchLater({ token: noahToken, handle: 'noah' })).id,
|
||||||
handle: 'noah',
|
attributes: { videoId: noahVideo.uuid }
|
||||||
playlistType: VideoPlaylistType.WATCH_LATER
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await server.playlists.addElement({
|
await remoteServer.playlists.addElement({
|
||||||
playlistId: playlists[0].id,
|
playlistId: (await remoteServer.playlists.getWatchLater({ token: remoteNoahToken, handle: 'noah_remote' })).id,
|
||||||
attributes: { videoId: noahVideo.uuid }
|
attributes: { videoId: mouskaVideo.uuid }
|
||||||
})
|
})
|
||||||
|
|
||||||
await waitJobs([ server, remoteServer, blockedServer ])
|
await waitJobs([ server, remoteServer, blockedServer ])
|
||||||
|
@ -285,11 +284,15 @@ function runTest (withObjectStorage: boolean) {
|
||||||
expect(watchLater.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
|
expect(watchLater.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE)
|
||||||
|
|
||||||
// Playlists were merged
|
// Playlists were merged
|
||||||
expect(watchLater.videosLength).to.equal(1)
|
expect(watchLater.videosLength).to.equal(2)
|
||||||
|
|
||||||
const { data: videos } = await remoteServer.playlists.listVideos({ playlistId: watchLater.id, token: remoteNoahToken })
|
const { data: videos } = await remoteServer.playlists.listVideos({ playlistId: watchLater.id, token: remoteNoahToken })
|
||||||
|
|
||||||
expect(videos[0].position).to.equal(1)
|
expect(videos[0].position).to.equal(1)
|
||||||
expect(videos[0].video.uuid).to.equal(noahVideo.uuid)
|
// Mouska is muted
|
||||||
|
expect(videos[0].video).to.not.exist
|
||||||
|
expect(videos[1].position).to.equal(2)
|
||||||
|
expect(videos[1].video.uuid).to.equal(noahVideo.uuid)
|
||||||
|
|
||||||
// Not federated
|
// Not federated
|
||||||
await server.playlists.get({ playlistId: watchLater.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
await server.playlists.get({ playlistId: watchLater.uuid, expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||||
|
|
|
@ -13,10 +13,9 @@ import {
|
||||||
VideoPlaylistUpdate
|
VideoPlaylistUpdate
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists/index.js'
|
import { scheduleRefreshIfNeeded } from '@server/lib/activitypub/playlists/index.js'
|
||||||
import { VideoMiniaturePermanentFileCache } from '@server/lib/files-cache/index.js'
|
|
||||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { MVideoPlaylistFull, MVideoPlaylistThumbnail, MVideoThumbnail } from '@server/types/models/index.js'
|
import { MVideoPlaylistFull, MVideoPlaylistThumbnail } from '@server/types/models/index.js'
|
||||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||||
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
|
import { resetSequelizeInstance } from '../../helpers/database-utils.js'
|
||||||
import { createReqFiles } from '../../helpers/express-utils.js'
|
import { createReqFiles } from '../../helpers/express-utils.js'
|
||||||
|
@ -51,6 +50,7 @@ import {
|
||||||
import { AccountModel } from '../../models/account/account.js'
|
import { AccountModel } from '../../models/account/account.js'
|
||||||
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element.js'
|
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element.js'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
|
import { VideoPlaylistModel } from '../../models/video/video-playlist.js'
|
||||||
|
import { generateThumbnailForPlaylist } from '@server/lib/video-playlist.js'
|
||||||
|
|
||||||
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
const reqThumbnailFile = createReqFiles([ 'thumbnailfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||||
|
|
||||||
|
@ -329,7 +329,7 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If the user did not set a thumbnail, automatically take the video thumbnail
|
// If the user did not set a thumbnail, automatically take the video thumbnail
|
||||||
if (videoPlaylist.hasThumbnail() === false || (videoPlaylist.hasGeneratedThumbnail() && playlistElement.position === 1)) {
|
if (videoPlaylist.shouldGenerateThumbnailWithNewElement(playlistElement)) {
|
||||||
await generateThumbnailForPlaylist(videoPlaylist, video)
|
await generateThumbnailForPlaylist(videoPlaylist, video)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,30 +488,3 @@ async function regeneratePlaylistThumbnail (videoPlaylist: MVideoPlaylistThumbna
|
||||||
const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id)
|
const firstElement = await VideoPlaylistElementModel.loadFirstElementWithVideoThumbnail(videoPlaylist.id)
|
||||||
if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video)
|
if (firstElement) await generateThumbnailForPlaylist(videoPlaylist, firstElement.Video)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) {
|
|
||||||
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
|
|
||||||
|
|
||||||
const videoMiniature = video.getMiniature()
|
|
||||||
if (!videoMiniature) {
|
|
||||||
logger.info('Cannot generate thumbnail for playlist %s because video %s does not have any.', videoPlaylist.url, video.url)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the file is on disk
|
|
||||||
const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache()
|
|
||||||
const inputPath = videoMiniature.isOwned()
|
|
||||||
? videoMiniature.getPath()
|
|
||||||
: await videoMiniaturePermanentFileCache.downloadRemoteFile(videoMiniature)
|
|
||||||
|
|
||||||
const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({
|
|
||||||
inputPath,
|
|
||||||
playlist: videoPlaylist,
|
|
||||||
automaticallyGenerated: true,
|
|
||||||
keepOriginal: true
|
|
||||||
})
|
|
||||||
|
|
||||||
thumbnailModel.videoPlaylistId = videoPlaylist.id
|
|
||||||
|
|
||||||
videoPlaylist.Thumbnail = await thumbnailModel.save()
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { VideoPlaylistPrivacy, VideoPlaylistType, VideoPlaylistsExportJSON } fro
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||||
import {
|
import {
|
||||||
MChannelBannerAccountDefault, MVideoPlaylist,
|
MChannelBannerAccountDefault, MVideoPlaylistFull,
|
||||||
MVideoPlaylistFull,
|
|
||||||
MVideoPlaylistThumbnail
|
MVideoPlaylistThumbnail
|
||||||
} from '@server/types/models/index.js'
|
} from '@server/types/models/index.js'
|
||||||
import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '@server/lib/activitypub/url.js'
|
import { getLocalVideoPlaylistActivityPubUrl, getLocalVideoPlaylistElementActivityPubUrl } from '@server/lib/activitypub/url.js'
|
||||||
|
@ -28,6 +27,8 @@ import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js'
|
||||||
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
import { isArray } from '@server/helpers/custom-validators/misc.js'
|
||||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||||
import { pick } from '@peertube/peertube-core-utils'
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
|
import { VideoModel } from '@server/models/video/video.js'
|
||||||
|
import { generateThumbnailForPlaylist } from '@server/lib/video-playlist.js'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('user-import')
|
const lTags = loggerTagsFactory('user-import')
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
|
||||||
await playlist.setAndSaveThumbnail(thumbnail, undefined)
|
await playlist.setAndSaveThumbnail(thumbnail, undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createElements (playlist: MVideoPlaylist, playlistImportData: SanitizedObject) {
|
private async createElements (playlist: MVideoPlaylistThumbnail, playlistImportData: SanitizedObject) {
|
||||||
const elementsToCreate: { videoId: number, startTimestamp: number, stopTimestamp: number }[] = []
|
const elementsToCreate: { videoId: number, startTimestamp: number, stopTimestamp: number }[] = []
|
||||||
|
|
||||||
for (const element of playlistImportData.elements.slice(0, USER_IMPORT.MAX_PLAYLIST_ELEMENTS)) {
|
for (const element of playlistImportData.elements.slice(0, USER_IMPORT.MAX_PLAYLIST_ELEMENTS)) {
|
||||||
|
@ -154,9 +155,9 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
|
||||||
}
|
}
|
||||||
|
|
||||||
await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
for (let position = 1; position <= elementsToCreate.length; position++) {
|
let position = await VideoPlaylistElementModel.getNextPositionOf(playlist.id, t)
|
||||||
const elementToCreate = elementsToCreate[position - 1]
|
|
||||||
|
|
||||||
|
for (const elementToCreate of elementsToCreate) {
|
||||||
const playlistElement = new VideoPlaylistElementModel({
|
const playlistElement = new VideoPlaylistElementModel({
|
||||||
position,
|
position,
|
||||||
startTimestamp: elementToCreate.startTimestamp,
|
startTimestamp: elementToCreate.startTimestamp,
|
||||||
|
@ -168,6 +169,15 @@ export class VideoPlaylistsImporter extends AbstractUserImporter <VideoPlaylists
|
||||||
|
|
||||||
playlistElement.url = getLocalVideoPlaylistElementActivityPubUrl(playlist, playlistElement)
|
playlistElement.url = getLocalVideoPlaylistElementActivityPubUrl(playlist, playlistElement)
|
||||||
await playlistElement.save({ transaction: t })
|
await playlistElement.save({ transaction: t })
|
||||||
|
|
||||||
|
if (playlist.shouldGenerateThumbnailWithNewElement(playlistElement)) {
|
||||||
|
const video = await VideoModel.loadFull(elementToCreate.videoId)
|
||||||
|
|
||||||
|
generateThumbnailForPlaylist(playlist, video)
|
||||||
|
.catch(err => logger.error('Cannot generate thumbnail from playlist', { err }))
|
||||||
|
}
|
||||||
|
|
||||||
|
position++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { VideoPlaylistPrivacy, VideoPlaylistType } from '@peertube/peertube-models'
|
import { VideoPlaylistPrivacy, VideoPlaylistType } from '@peertube/peertube-models'
|
||||||
import { VideoPlaylistModel } from '../models/video/video-playlist.js'
|
import { VideoPlaylistModel } from '../models/video/video-playlist.js'
|
||||||
import { MAccount } from '../types/models/index.js'
|
import { MAccount, MVideoThumbnail } from '../types/models/index.js'
|
||||||
import { MVideoPlaylistOwner } from '../types/models/video/video-playlist.js'
|
import { MVideoPlaylistOwner, MVideoPlaylistThumbnail } from '../types/models/video/video-playlist.js'
|
||||||
import { getLocalVideoPlaylistActivityPubUrl } from './activitypub/url.js'
|
import { getLocalVideoPlaylistActivityPubUrl } from './activitypub/url.js'
|
||||||
|
import { VideoMiniaturePermanentFileCache } from './files-cache/video-miniature-permanent-file-cache.js'
|
||||||
|
import { updateLocalPlaylistMiniatureFromExisting } from './thumbnail.js'
|
||||||
|
import { logger } from '@server/helpers/logger.js'
|
||||||
|
|
||||||
async function createWatchLaterPlaylist (account: MAccount, t: Sequelize.Transaction) {
|
export async function createWatchLaterPlaylist (account: MAccount, t: Sequelize.Transaction) {
|
||||||
const videoPlaylist: MVideoPlaylistOwner = new VideoPlaylistModel({
|
const videoPlaylist: MVideoPlaylistOwner = new VideoPlaylistModel({
|
||||||
name: 'Watch later',
|
name: 'Watch later',
|
||||||
privacy: VideoPlaylistPrivacy.PRIVATE,
|
privacy: VideoPlaylistPrivacy.PRIVATE,
|
||||||
|
@ -22,8 +25,29 @@ async function createWatchLaterPlaylist (account: MAccount, t: Sequelize.Transac
|
||||||
return videoPlaylist
|
return videoPlaylist
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
export async function generateThumbnailForPlaylist (videoPlaylist: MVideoPlaylistThumbnail, video: MVideoThumbnail) {
|
||||||
|
logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
|
||||||
|
|
||||||
export {
|
const videoMiniature = video.getMiniature()
|
||||||
createWatchLaterPlaylist
|
if (!videoMiniature) {
|
||||||
|
logger.info('Cannot generate thumbnail for playlist %s because video %s does not have any.', videoPlaylist.url, video.url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the file is on disk
|
||||||
|
const videoMiniaturePermanentFileCache = new VideoMiniaturePermanentFileCache()
|
||||||
|
const inputPath = videoMiniature.isOwned()
|
||||||
|
? videoMiniature.getPath()
|
||||||
|
: await videoMiniaturePermanentFileCache.downloadRemoteFile(videoMiniature)
|
||||||
|
|
||||||
|
const thumbnailModel = await updateLocalPlaylistMiniatureFromExisting({
|
||||||
|
inputPath,
|
||||||
|
playlist: videoPlaylist,
|
||||||
|
automaticallyGenerated: true,
|
||||||
|
keepOriginal: true
|
||||||
|
})
|
||||||
|
|
||||||
|
thumbnailModel.videoPlaylistId = videoPlaylist.id
|
||||||
|
|
||||||
|
videoPlaylist.Thumbnail = await thumbnailModel.save()
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '@peertube/peertube-models'
|
} from '@peertube/peertube-models'
|
||||||
import { buildUUID, uuidToShort } from '@peertube/peertube-node-utils'
|
import { buildUUID, uuidToShort } from '@peertube/peertube-node-utils'
|
||||||
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
|
import { activityPubCollectionPagination } from '@server/lib/activitypub/collection.js'
|
||||||
import { MAccountId, MChannelId } from '@server/types/models/index.js'
|
import { MAccountId, MChannelId, MVideoPlaylistElement } from '@server/types/models/index.js'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
import { FindOptions, Includeable, literal, Op, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
|
@ -629,6 +629,13 @@ export class VideoPlaylistModel extends SequelizeModel<VideoPlaylistModel> {
|
||||||
return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
|
return this.hasThumbnail() && this.Thumbnail.automaticallyGenerated === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldGenerateThumbnailWithNewElement (newElement: MVideoPlaylistElement) {
|
||||||
|
if (this.hasThumbnail() === false) return true
|
||||||
|
if (newElement.position === 1 && this.hasGeneratedThumbnail()) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
generateThumbnailName () {
|
generateThumbnailName () {
|
||||||
const extension = '.jpg'
|
const extension = '.jpg'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue