diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index 33d77e62c..ff321fdbc 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -176,7 +176,7 @@ $other-videos-width: 260px;
display: flex;
align-items: center;
- .action-button:not(:first-child), .action-more {
+ .action-button:not(:first-child), .action-dropdown {
margin-left: 10px;
}
@@ -212,12 +212,19 @@ $other-videos-width: 260px;
}
}
+ &.action-button-save {
+ my-global-icon {
+ top: 0 !important;
+ right: -1px;
+ }
+ }
+
.icon-text {
margin-left: 3px;
}
}
- .action-more {
+ .action-dropdown {
display: inline-block;
.dropdown-menu .dropdown-item {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 0f04441ba..359217f3b 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -59,6 +59,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
remoteServerDown = false
hotkeys: Hotkey[]
+ private currentTime: number
private paramsSub: Subscription
constructor (
@@ -114,10 +115,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
)
.subscribe(([ video, captionsResult ]) => {
const startTime = this.route.snapshot.queryParams.start
+ const stopTime = this.route.snapshot.queryParams.stop
const subtitle = this.route.snapshot.queryParams.subtitle
const playerMode = this.route.snapshot.queryParams.mode
- this.onVideoFetched(video, captionsResult.data, { startTime, subtitle, playerMode })
+ this.onVideoFetched(video, captionsResult.data, { startTime, stopTime, subtitle, playerMode })
.catch(err => this.handleError(err))
})
})
@@ -219,7 +221,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
showShareModal () {
const currentTime = this.player ? this.player.currentTime() : undefined
- this.videoShareModal.show(currentTime)
+ this.videoShareModal.show(this.currentTime)
}
showDownloadModal (event: Event) {
@@ -371,7 +373,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private async onVideoFetched (
video: VideoDetails,
videoCaptions: VideoCaption[],
- urlOptions: { startTime?: number, subtitle?: string, playerMode?: string }
+ urlOptions: { startTime?: number, stopTime?: number, subtitle?: string, playerMode?: string }
) {
this.video = video
@@ -379,6 +381,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.descriptionLoading = false
this.completeDescriptionShown = false
this.remoteServerDown = false
+ this.currentTime = undefined
let startTime = urlOptions.startTime || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
// If we are at the end of the video, reset the timer
@@ -420,6 +423,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
inactivityTimeout: 2500,
poster: this.video.previewUrl,
startTime,
+ stopTime: urlOptions.stopTime,
theaterMode: true,
captions: videoCaptions.length !== 0,
@@ -466,6 +470,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.zone.runOutsideAngular(async () => {
this.player = await PeertubePlayerManager.initialize(mode, options)
this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
+
+ this.player.on('timeupdate', () => {
+ this.currentTime = Math.floor(this.player.currentTime())
+ })
})
this.setVideoDescriptionHTML()
diff --git a/client/src/app/videos/videos-routing.module.ts b/client/src/app/videos/videos-routing.module.ts
index 58988ffd1..69a9232ce 100644
--- a/client/src/app/videos/videos-routing.module.ts
+++ b/client/src/app/videos/videos-routing.module.ts
@@ -78,11 +78,7 @@ const videosRoutes: Routes = [
}
},
{
- path: 'watch/:uuid/comments/:commentId',
- redirectTo: 'watch/:uuid'
- },
- {
- path: 'watch/:uuid',
+ path: 'watch',
loadChildren: 'app/videos/+video-watch/video-watch.module#VideoWatchModule',
data: {
preload: 3000
diff --git a/client/src/assets/images/global/add.html b/client/src/assets/images/global/add.html
index bfb0a52bc..34f497056 100644
--- a/client/src/assets/images/global/add.html
+++ b/client/src/assets/images/global/add.html
@@ -2,9 +2,9 @@
-
-
-
+
+
+
diff --git a/client/src/assets/images/video/playlist-add.html b/client/src/assets/images/video/playlist-add.html
new file mode 100644
index 000000000..ada845c75
--- /dev/null
+++ b/client/src/assets/images/video/playlist-add.html
@@ -0,0 +1,10 @@
+
diff --git a/client/src/assets/images/video/watch-later.html b/client/src/assets/images/video/watch-later.html
new file mode 100644
index 000000000..927afebe4
--- /dev/null
+++ b/client/src/assets/images/video/watch-later.html
@@ -0,0 +1,11 @@
+
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts
index 7631d095f..6cdd54372 100644
--- a/client/src/assets/player/peertube-player-manager.ts
+++ b/client/src/assets/player/peertube-player-manager.ts
@@ -49,6 +49,7 @@ export type CommonOptions = {
inactivityTimeout: number
poster: string
startTime: number | string
+ stopTime: number | string
theaterMode: boolean
captions: boolean
@@ -199,10 +200,10 @@ export class PeertubePlayerManager {
autoplay, // Use peertube plugin autoplay because we get the file by webtorrent
videoViewUrl: commonOptions.videoViewUrl,
videoDuration: commonOptions.videoDuration,
- startTime: commonOptions.startTime,
userWatching: commonOptions.userWatching,
subtitle: commonOptions.subtitle,
- videoCaptions: commonOptions.videoCaptions
+ videoCaptions: commonOptions.videoCaptions,
+ stopTime: commonOptions.stopTime
}
}
@@ -210,6 +211,7 @@ export class PeertubePlayerManager {
const p2pMediaLoader: P2PMediaLoaderPluginOptions = {
redundancyBaseUrls: options.p2pMediaLoader.redundancyBaseUrls,
type: 'application/x-mpegURL',
+ startTime: commonOptions.startTime,
src: p2pMediaLoaderOptions.playlistUrl
}
@@ -254,7 +256,8 @@ export class PeertubePlayerManager {
autoplay,
videoDuration: commonOptions.videoDuration,
playerElement: commonOptions.playerElement,
- videoFiles: webtorrentOptions.videoFiles
+ videoFiles: webtorrentOptions.videoFiles,
+ startTime: commonOptions.startTime
}
Object.assign(plugins, { webtorrent })
diff --git a/client/src/assets/player/peertube-plugin.ts b/client/src/assets/player/peertube-plugin.ts
index 92ac57cf5..3991e4627 100644
--- a/client/src/assets/player/peertube-plugin.ts
+++ b/client/src/assets/player/peertube-plugin.ts
@@ -22,7 +22,6 @@ import {
const Plugin: VideoJSComponentInterface = videojs.getPlugin('plugin')
class PeerTubePlugin extends Plugin {
- private readonly startTime: number = 0
private readonly videoViewUrl: string
private readonly videoDuration: number
private readonly CONSTANTS = {
@@ -35,13 +34,11 @@ class PeerTubePlugin extends Plugin {
private videoViewInterval: any
private userWatchingVideoInterval: any
- private qualityObservationTimer: any
private lastResolutionChange: ResolutionUpdateData
constructor (player: videojs.Player, options: PeerTubePluginOptions) {
super(player, options)
- this.startTime = timeToInt(options.startTime)
this.videoViewUrl = options.videoViewUrl
this.videoDuration = options.videoDuration
this.videoCaptions = options.videoCaptions
@@ -84,6 +81,14 @@ class PeerTubePlugin extends Plugin {
saveMuteInStore(this.player.muted())
})
+ if (options.stopTime) {
+ const stopTime = timeToInt(options.stopTime)
+
+ this.player.on('timeupdate', () => {
+ if (this.player.currentTime() > stopTime) this.player.pause()
+ })
+ }
+
this.player.textTracks().on('change', () => {
const showing = this.player.textTracks().tracks_.find((t: { kind: string, mode: string }) => {
return t.kind === 'captions' && t.mode === 'showing'
@@ -109,10 +114,7 @@ class PeerTubePlugin extends Plugin {
}
dispose () {
- clearTimeout(this.qualityObservationTimer)
-
- clearInterval(this.videoViewInterval)
-
+ if (this.videoViewInterval) clearInterval(this.videoViewInterval)
if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
}
diff --git a/client/src/assets/player/peertube-videojs-typings.ts b/client/src/assets/player/peertube-videojs-typings.ts
index 79a5a6c4d..a96b0bc8c 100644
--- a/client/src/assets/player/peertube-videojs-typings.ts
+++ b/client/src/assets/player/peertube-videojs-typings.ts
@@ -41,12 +41,13 @@ type PeerTubePluginOptions = {
autoplay: boolean
videoViewUrl: string
videoDuration: number
- startTime: number | string
userWatching?: UserWatching
subtitle?: string
videoCaptions: VideoJSCaption[]
+
+ stopTime: number | string
}
type WebtorrentPluginOptions = {
@@ -56,12 +57,16 @@ type WebtorrentPluginOptions = {
videoDuration: number
videoFiles: VideoFile[]
+
+ startTime: number | string
}
type P2PMediaLoaderPluginOptions = {
redundancyBaseUrls: string[]
type: string
src: string
+
+ startTime: number | string
}
type VideoJSPluginOptions = {
diff --git a/client/src/assets/player/utils.ts b/client/src/assets/player/utils.ts
index 8d87567c2..54f131310 100644
--- a/client/src/assets/player/utils.ts
+++ b/client/src/assets/player/utils.ts
@@ -42,7 +42,7 @@ function timeToInt (time: number | string) {
if (!time) return 0
if (typeof time === 'number') return time
- const reg = /^((\d+)h)?((\d+)m)?((\d+)s?)?$/
+ const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
const matches = time.match(reg)
if (!matches) return 0
@@ -54,18 +54,27 @@ function timeToInt (time: number | string) {
return hours * 3600 + minutes * 60 + seconds
}
-function secondsToTime (seconds: number) {
+function secondsToTime (seconds: number, full = false, symbol?: string) {
let time = ''
+ const hourSymbol = (symbol || 'h')
+ const minuteSymbol = (symbol || 'm')
+ const secondsSymbol = full ? '' : 's'
+
let hours = Math.floor(seconds / 3600)
- if (hours >= 1) time = hours + 'h'
+ if (hours >= 1) time = hours + hourSymbol
+ else if (full) time = '0' + hourSymbol
seconds %= 3600
let minutes = Math.floor(seconds / 60)
- if (minutes >= 1) time += minutes + 'm'
+ if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
+ else if (minutes >= 1) time += minutes + minuteSymbol
+ else if (full) time += '00' + minuteSymbol
seconds %= 60
- if (seconds >= 1) time += seconds + 's'
+ if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
+ else if (seconds >= 1) time += seconds + secondsSymbol
+ else if (full) time += '00'
return time
}
@@ -131,6 +140,7 @@ export {
getRtcConfig,
toTitleCase,
timeToInt,
+ secondsToTime,
buildVideoLink,
buildVideoEmbed,
videoFileMaxByResolution,
diff --git a/client/src/assets/player/webtorrent/webtorrent-plugin.ts b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
index c69bf31fa..c7182acc9 100644
--- a/client/src/assets/player/webtorrent/webtorrent-plugin.ts
+++ b/client/src/assets/player/webtorrent/webtorrent-plugin.ts
@@ -6,7 +6,7 @@ import * as WebTorrent from 'webtorrent'
import { VideoFile } from '../../../../../shared/models/videos/video.model'
import { renderVideo } from './video-renderer'
import { LoadedQualityData, PlayerNetworkInfo, VideoJSComponentInterface, WebtorrentPluginOptions } from '../peertube-videojs-typings'
-import { getRtcConfig, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
+import { getRtcConfig, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from '../utils'
import { PeertubeChunkStore } from './peertube-chunk-store'
import {
getAverageBandwidthInStore,
@@ -73,6 +73,8 @@ class WebTorrentPlugin extends Plugin {
constructor (player: videojs.Player, options: WebtorrentPluginOptions) {
super(player, options)
+ this.startTime = timeToInt(options.startTime)
+
// Disable auto play on iOS
this.autoplay = options.autoplay && this.isIOS() === false
this.playerRefusedP2P = !getStoredWebTorrentEnabled()
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 59b2f42a5..3eefdb6fb 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -515,4 +515,3 @@
align-items: center;
}
}
-
diff --git a/client/src/sass/include/_variables.scss b/client/src/sass/include/_variables.scss
index 56ca4c2d3..deabbf6d4 100644
--- a/client/src/sass/include/_variables.scss
+++ b/client/src/sass/include/_variables.scss
@@ -44,6 +44,8 @@ $footer-margin: 30px;
$footer-border-color: $header-border-color;
+$separator-border-color: rgba(0, 0, 0, 0.10);
+
$video-thumbnail-height: 122px;
$video-thumbnail-width: 223px;
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts
index 32bf42e12..28c10c75c 100644
--- a/client/src/standalone/videos/embed.ts
+++ b/client/src/standalone/videos/embed.ts
@@ -168,6 +168,7 @@ class PeerTubeEmbed {
subtitle: string
enableApi = false
startTime: number | string = 0
+ stopTime: number | string
mode: PlayerMode
scope = 'peertube'
@@ -262,6 +263,7 @@ class PeerTubeEmbed {
this.scope = this.getParamString(params, 'scope', this.scope)
this.subtitle = this.getParamString(params, 'subtitle')
this.startTime = this.getParamString(params, 'start')
+ this.stopTime = this.getParamString(params, 'stop')
this.mode = this.getParamString(params, 'mode') === 'p2p-media-loader' ? 'p2p-media-loader' : 'webtorrent'
} catch (err) {
@@ -306,6 +308,7 @@ class PeerTubeEmbed {
loop: this.loop,
captions: videoCaptions.length !== 0,
startTime: this.startTime,
+ stopTime: this.stopTime,
subtitle: this.subtitle,
videoCaptions,
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 5758c8227..f7edbddf3 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -38,6 +38,7 @@ import { auditLoggerFactory, getAuditIdFromRes, UserAuditView } from '../../../h
import { meRouter } from './me'
import { deleteUserToken } from '../../../lib/oauth-model'
import { myBlocklistRouter } from './my-blocklist'
+import { myVideoPlaylistsRouter } from './my-video-playlists'
import { myVideosHistoryRouter } from './my-history'
import { myNotificationsRouter } from './my-notifications'
import { Notifier } from '../../../lib/notifier'
@@ -60,6 +61,7 @@ usersRouter.use('/', myNotificationsRouter)
usersRouter.use('/', mySubscriptionsRouter)
usersRouter.use('/', myBlocklistRouter)
usersRouter.use('/', myVideosHistoryRouter)
+usersRouter.use('/', myVideoPlaylistsRouter)
usersRouter.use('/', meRouter)
usersRouter.get('/autocomplete',
diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts
new file mode 100644
index 000000000..1ec175f64
--- /dev/null
+++ b/server/controllers/api/users/my-video-playlists.ts
@@ -0,0 +1,47 @@
+import * as express from 'express'
+import { asyncMiddleware, authenticate } from '../../../middlewares'
+import { UserModel } from '../../../models/account/user'
+import { doVideosInPlaylistExistValidator } from '../../../middlewares/validators/videos/video-playlists'
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
+import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
+
+const myVideoPlaylistsRouter = express.Router()
+
+myVideoPlaylistsRouter.get('/me/video-playlists/videos-exist',
+ authenticate,
+ doVideosInPlaylistExistValidator,
+ asyncMiddleware(doVideosInPlaylistExist)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ myVideoPlaylistsRouter
+}
+
+// ---------------------------------------------------------------------------
+
+async function doVideosInPlaylistExist (req: express.Request, res: express.Response) {
+ const videoIds = req.query.videoIds as number[]
+ const user = res.locals.oauth.token.User as UserModel
+
+ const results = await VideoPlaylistModel.listPlaylistIdsOf(user.Account.id, videoIds)
+
+ const existObject: VideoExistInPlaylist = {}
+
+ for (const videoId of videoIds) {
+ existObject[videoId] = []
+ }
+
+ for (const result of results) {
+ for (const element of result.VideoPlaylistElements) {
+ existObject[element.videoId].push({
+ playlistId: result.id,
+ startTimestamp: element.startTimestamp,
+ stopTimestamp: element.stopTimestamp
+ })
+ }
+ }
+
+ return res.json(existObject)
+}
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts
index 145764d35..49432d3aa 100644
--- a/server/controllers/api/video-playlist.ts
+++ b/server/controllers/api/video-playlist.ts
@@ -291,23 +291,26 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
videoId: video.id
}, { transaction: t })
- // If the user did not set a thumbnail, automatically take the video thumbnail
- if (playlistElement.position === 1) {
- const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
-
- if (await pathExists(playlistThumbnailPath) === false) {
- logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
-
- const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
- await copy(videoThumbnailPath, playlistThumbnailPath)
- }
- }
+ videoPlaylist.updatedAt = new Date()
+ await videoPlaylist.save({ transaction: t })
await sendUpdateVideoPlaylist(videoPlaylist, t)
return playlistElement
})
+ // If the user did not set a thumbnail, automatically take the video thumbnail
+ if (playlistElement.position === 1) {
+ const playlistThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoPlaylist.getThumbnailName())
+
+ if (await pathExists(playlistThumbnailPath) === false) {
+ logger.info('Generating default thumbnail to playlist %s.', videoPlaylist.url)
+
+ const videoThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, video.getThumbnailName())
+ await copy(videoThumbnailPath, playlistThumbnailPath)
+ }
+ }
+
logger.info('Video added in playlist %s at position %d.', videoPlaylist.uuid, playlistElement.position)
return res.json({
@@ -328,6 +331,9 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
const element = await videoPlaylistElement.save({ transaction: t })
+ videoPlaylist.updatedAt = new Date()
+ await videoPlaylist.save({ transaction: t })
+
await sendUpdateVideoPlaylist(videoPlaylist, t)
return element
@@ -349,6 +355,9 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
// Decrease position of the next elements
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, positionToDelete, null, -1, t)
+ videoPlaylist.updatedAt = new Date()
+ await videoPlaylist.save({ transaction: t })
+
await sendUpdateVideoPlaylist(videoPlaylist, t)
logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
@@ -390,6 +399,9 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
// Decrease positions of elements after the old position of our ordered elements (decrease)
await VideoPlaylistElementModel.increasePositionOf(videoPlaylist.id, oldPosition, null, -reorderLength, t)
+ videoPlaylist.updatedAt = new Date()
+ await videoPlaylist.save({ transaction: t })
+
await sendUpdateVideoPlaylist(videoPlaylist, t)
})
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts
index 76647fea2..3a3deab0c 100644
--- a/server/helpers/custom-validators/misc.ts
+++ b/server/helpers/custom-validators/misc.ts
@@ -49,12 +49,19 @@ function toValueOrNull (value: string) {
return value
}
-function toArray (value: string) {
+function toArray (value: any) {
if (value && isArray(value) === false) return [ value ]
return value
}
+function toIntArray (value: any) {
+ if (!value) return []
+ if (isArray(value) === false) return [ validator.toInt(value) ]
+
+ return value.map(v => validator.toInt(v))
+}
+
function isFileValid (
files: { [ fieldname: string ]: Express.Multer.File[] } | Express.Multer.File[],
mimeTypeRegex: string,
@@ -97,5 +104,6 @@ export {
isBooleanValid,
toIntOrNull,
toArray,
+ toIntArray,
isFileValid
}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 54c390540..169a98ceb 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -56,7 +56,7 @@ const SORTABLE_COLUMNS = {
USER_NOTIFICATIONS: [ 'createdAt' ],
- VIDEO_PLAYLISTS: [ 'createdAt' ]
+ VIDEO_PLAYLISTS: [ 'displayName', 'createdAt', 'updatedAt' ]
}
const OAUTH_LIFETIME = {
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts
index 22b8b8ff1..87d2c7b51 100644
--- a/server/middlewares/validators/videos/video-playlists.ts
+++ b/server/middlewares/validators/videos/video-playlists.ts
@@ -4,9 +4,9 @@ import { UserRight } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { UserModel } from '../../../models/account/user'
import { areValidationErrors } from '../utils'
-import { isVideoExist, isVideoImage } from '../../../helpers/custom-validators/videos'
+import { isVideoExist, isVideoFileInfoHashValid, isVideoImage } from '../../../helpers/custom-validators/videos'
import { CONSTRAINTS_FIELDS } from '../../../initializers'
-import { isIdOrUUIDValid, isUUIDValid, toValueOrNull } from '../../../helpers/custom-validators/misc'
+import { isArrayOf, isIdOrUUIDValid, isIdValid, isUUIDValid, toArray, toValueOrNull, toIntArray } from '../../../helpers/custom-validators/misc'
import {
isVideoPlaylistDescriptionValid,
isVideoPlaylistExist,
@@ -23,6 +23,7 @@ import { VideoModel } from '../../../models/video/video'
import { authenticatePromiseIfNeeded } from '../../oauth'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
+import { areValidActorHandles } from '../../../helpers/custom-validators/activitypub/actor'
const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
@@ -305,6 +306,20 @@ const commonVideoPlaylistFiltersValidator = [
}
]
+const doVideosInPlaylistExistValidator = [
+ query('videoIds')
+ .customSanitizer(toIntArray)
+ .custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking areVideosInPlaylistExistValidator parameters', { parameters: req.query })
+
+ if (areValidationErrors(req, res)) return
+
+ return next()
+ }
+]
+
// ---------------------------------------------------------------------------
export {
@@ -319,7 +334,9 @@ export {
videoPlaylistElementAPGetValidator,
- commonVideoPlaylistFiltersValidator
+ commonVideoPlaylistFiltersValidator,
+
+ doVideosInPlaylistExistValidator
}
// ---------------------------------------------------------------------------
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 4d2ea0a66..aa42687cd 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -317,6 +317,29 @@ export class VideoPlaylistModel extends Model
{
})
}
+ static listPlaylistIdsOf (accountId: number, videoIds: number[]) {
+ const query = {
+ attributes: [ 'id' ],
+ where: {
+ ownerAccountId: accountId
+ },
+ include: [
+ {
+ attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
+ model: VideoPlaylistElementModel.unscoped(),
+ where: {
+ videoId: {
+ [Sequelize.Op.any]: videoIds
+ }
+ },
+ required: true
+ }
+ ]
+ }
+
+ return VideoPlaylistModel.findAll(query)
+ }
+
static doesPlaylistExist (url: string) {
const query = {
attributes: [],
diff --git a/shared/models/videos/playlist/video-exist-in-playlist.model.ts b/shared/models/videos/playlist/video-exist-in-playlist.model.ts
new file mode 100644
index 000000000..71240f51d
--- /dev/null
+++ b/shared/models/videos/playlist/video-exist-in-playlist.model.ts
@@ -0,0 +1,7 @@
+export type VideoExistInPlaylist = {
+ [videoId: number ]: {
+ playlistId: number
+ startTimestamp?: number
+ stopTimestamp?: number
+ }[]
+}