return idHost && actorHost && idHost.toLowerCase() === actorHost.toLowerCase()
}
-function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string) {
+function buildRemoteVideoBaseUrl (video: MVideoWithHost, path: string, scheme?: string) {
+ if (!scheme) scheme = REMOTE_SCHEME.HTTP
+
const host = video.VideoChannel.Actor.Server.host
- return REMOTE_SCHEME.HTTP + '://' + host + path
+ return scheme + '://' + host + path
}
// ---------------------------------------------------------------------------
import validator from 'validator'
+import { logger } from '@server/helpers/logger'
+import { ActivityTrackerUrlObject, ActivityVideoFileMetadataUrlObject } from '@shared/models'
+import { VideoState } from '../../../../shared/models/videos'
import { ACTIVITY_PUB, CONSTRAINTS_FIELDS } from '../../../initializers/constants'
import { peertubeTruncate } from '../../core-utils'
import { exists, isArray, isBooleanValid, isDateValid, isUUIDValid } from '../misc'
isVideoViewsValid
} from '../videos'
import { isActivityPubUrlValid, isBaseActivityValid, setValidAttributedTo } from './misc'
-import { VideoState } from '../../../../shared/models/videos'
-import { logger } from '@server/helpers/logger'
-import { ActivityVideoFileMetadataObject } from '@shared/models'
function sanitizeAndCheckVideoTorrentUpdateActivity (activity: any) {
return isBaseActivityValid(activity, 'Update') &&
function isRemoteVideoUrlValid (url: any) {
return url.type === 'Link' &&
+ // Video file link
(
ACTIVITY_PUB.URL_MIME_TYPES.VIDEO.includes(url.mediaType) &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.size + '', { min: 0 }) &&
(!url.fps || validator.isInt(url.fps + '', { min: -1 }))
) ||
+ // Torrent link
(
ACTIVITY_PUB.URL_MIME_TYPES.TORRENT.includes(url.mediaType) &&
isActivityPubUrlValid(url.href) &&
validator.isInt(url.height + '', { min: 0 })
) ||
+ // Magnet link
(
ACTIVITY_PUB.URL_MIME_TYPES.MAGNET.includes(url.mediaType) &&
validator.isLength(url.href, { min: 5 }) &&
validator.isInt(url.height + '', { min: 0 })
) ||
+ // HLS playlist link
(
(url.mediaType || url.mimeType) === 'application/x-mpegURL' &&
isActivityPubUrlValid(url.href) &&
isArray(url.tag)
) ||
- isAPVideoFileMetadataObject(url)
+ isAPVideoTrackerUrlObject(url) ||
+ isAPVideoFileUrlMetadataObject(url)
}
-function isAPVideoFileMetadataObject (url: any): url is ActivityVideoFileMetadataObject {
+function isAPVideoFileUrlMetadataObject (url: any): url is ActivityVideoFileMetadataUrlObject {
return url &&
url.type === 'Link' &&
url.mediaType === 'application/json' &&
isArray(url.rel) && url.rel.includes('metadata')
}
+function isAPVideoTrackerUrlObject (url: any): url is ActivityTrackerUrlObject {
+ return isArray(url.rel) &&
+ url.rel.includes('tracker') &&
+ isActivityPubUrlValid(url.href)
+}
+
// ---------------------------------------------------------------------------
export {
isRemoteStringIdentifierValid,
sanitizeAndCheckVideoTorrentObject,
isRemoteVideoUrlValid,
- isAPVideoFileMetadataObject
+ isAPVideoFileUrlMetadataObject,
+ isAPVideoTrackerUrlObject
}
// ---------------------------------------------------------------------------
videoFile.torrentFilename = torrentFilename
}
-// FIXME: merge/refactor videoOrPlaylist and video arguments
function generateMagnetUri (
- videoOrPlaylist: MVideo | MStreamingPlaylistVideo,
video: MVideoWithHost,
videoFile: MVideoFileRedundanciesOpt,
- baseUrlHttp: string,
- baseUrlWs: string
+ trackerUrls: string[]
) {
const xs = videoFile.getTorrentUrl()
- const announce = videoOrPlaylist.getTrackerUrls(baseUrlHttp, baseUrlWs)
+ const announce = trackerUrls
let urlList = [ videoFile.getFileUrl(video) ]
const redundancies = videoFile.RedundancyVideos
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 585
+const LAST_MIGRATION_VERSION = 595
// ---------------------------------------------------------------------------
+import { TrackerModel } from '@server/models/server/tracker'
+import { VideoTrackerModel } from '@server/models/server/video-tracker'
import { QueryTypes, Transaction } from 'sequelize'
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
import { isTestInstance } from '../helpers/core-utils'
VideoPlaylistModel,
VideoPlaylistElementModel,
ThumbnailModel,
+ TrackerModel,
+ VideoTrackerModel,
PluginModel
])
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+ {
+ const query = `CREATE TABLE IF NOT EXISTS "tracker" (
+ "id" serial,
+ "url" varchar(255) NOT NULL,
+ "createdAt" timestamp WITH time zone NOT NULL,
+ "updatedAt" timestamp WITH time zone NOT NULL,
+ PRIMARY KEY ("id")
+ );`
+
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const query = `CREATE TABLE IF NOT EXISTS "videoTracker" (
+ "videoId" integer REFERENCES "video" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ "trackerId" integer REFERENCES "tracker" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+ "createdAt" timestamp WITH time zone NOT NULL,
+ "updatedAt" timestamp WITH time zone NOT NULL,
+ UNIQUE ("videoId", "trackerId"),
+ PRIMARY KEY ("videoId", "trackerId")
+ );`
+
+ await utils.sequelize.query(query)
+ }
+
+ await utils.sequelize.query(`CREATE UNIQUE INDEX "tracker_url" ON "tracker" ("url")`)
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+ db: any
+}): Promise<void> {
+
+ // Torrent and file URLs
+ {
+ const fromQueryWebtorrent = `SELECT 'https://' || server.host AS "serverUrl", '/static/webseed/' AS "filePath", "videoFile".id ` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `INNER JOIN "videoFile" ON "videoFile"."videoId" = video.id ` +
+ `WHERE video.remote IS TRUE`
+
+ const fromQueryHLS = `SELECT 'https://' || server.host AS "serverUrl", ` +
+ `'/static/streaming-playlists/hls/' || video.uuid || '/' AS "filePath", "videoFile".id ` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `INNER JOIN "videoStreamingPlaylist" ON "videoStreamingPlaylist"."videoId" = video.id ` +
+ `INNER JOIN "videoFile" ON "videoFile"."videoStreamingPlaylistId" = "videoStreamingPlaylist".id ` +
+ `WHERE video.remote IS TRUE`
+
+ for (const fromQuery of [ fromQueryWebtorrent, fromQueryHLS ]) {
+ const query = `UPDATE "videoFile" ` +
+ `SET "torrentUrl" = t."serverUrl" || '/static/torrents/' || "videoFile"."torrentFilename", ` +
+ `"fileUrl" = t."serverUrl" || t."filePath" || "videoFile"."filename" ` +
+ `FROM (${fromQuery}) AS t WHERE t.id = "videoFile"."id" AND "videoFile"."fileUrl" IS NULL`
+
+ await utils.sequelize.query(query)
+ }
+ }
+
+ // Caption URLs
+ {
+ const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "videoCaption".id ` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `INNER JOIN "videoCaption" ON "videoCaption"."videoId" = video.id ` +
+ `WHERE video.remote IS TRUE`
+
+ const query = `UPDATE "videoCaption" ` +
+ `SET "fileUrl" = t."serverUrl" || '/lazy-static/video-captions/' || t.uuid || '-' || "videoCaption"."language" || '.vtt' ` +
+ `FROM (${fromQuery}) AS t WHERE t.id = "videoCaption"."id" AND "videoCaption"."fileUrl" IS NULL`
+
+ await utils.sequelize.query(query)
+ }
+
+ // Thumbnail URLs
+ {
+ const fromQuery = `SELECT 'https://' || server.host AS "serverUrl", "video".uuid, "thumbnail".id ` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `INNER JOIN "thumbnail" ON "thumbnail"."videoId" = video.id ` +
+ `WHERE video.remote IS TRUE`
+
+ // Thumbnails
+ {
+ const query = `UPDATE "thumbnail" ` +
+ `SET "fileUrl" = t."serverUrl" || '/static/thumbnails/' || t.uuid || '.jpg' ` +
+ `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 1`
+
+ await utils.sequelize.query(query)
+ }
+
+ {
+ // Previews
+ const query = `UPDATE "thumbnail" ` +
+ `SET "fileUrl" = t."serverUrl" || '/lazy-static/previews/' || t.uuid || '.jpg' ` +
+ `FROM (${fromQuery}) AS t WHERE t.id = "thumbnail"."id" AND "thumbnail"."fileUrl" IS NULL AND thumbnail.type = 2`
+
+ await utils.sequelize.query(query)
+ }
+ }
+
+ // Trackers
+ {
+ const trackerUrls = [
+ `'https://' || server.host || '/tracker/announce'`,
+ `'wss://' || server.host || '/tracker/socket'`
+ ]
+
+ for (const trackerUrl of trackerUrls) {
+ {
+ const query = `INSERT INTO "tracker" ("url", "createdAt", "updatedAt") ` +
+ `SELECT ${trackerUrl} AS "url", NOW(), NOW() ` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `WHERE video.remote IS TRUE ` +
+ `ON CONFLICT DO NOTHING`
+
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const query = `INSERT INTO "videoTracker" ("videoId", "trackerId", "createdAt", "updatedAt") ` +
+ `SELECT video.id, (SELECT tracker.id FROM tracker WHERE url = ${trackerUrl}) AS "trackerId", NOW(), NOW()` +
+ `FROM video ` +
+ `INNER JOIN "videoChannel" ON "videoChannel".id = video."channelId" ` +
+ `INNER JOIN actor ON actor.id = "videoChannel"."actorId" ` +
+ `INNER JOIN server ON server.id = actor."serverId" ` +
+ `WHERE video.remote IS TRUE`
+
+ await utils.sequelize.query(query)
+ }
+ }
+ }
+
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
import * as magnetUtil from 'magnet-uri'
import { basename, join } from 'path'
import * as request from 'request'
-import * as sequelize from 'sequelize'
+import { Transaction } from 'sequelize/types'
+import { TrackerModel } from '@server/models/server/tracker'
import { VideoLiveModel } from '@server/models/video/video-live'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import {
ActivityUrlObject,
ActivityVideoUrlObject
} from '../../../shared/index'
-import { ActivityIconObject, VideoObject } from '../../../shared/models/activitypub/objects'
+import { ActivityIconObject, ActivityTrackerUrlObject, VideoObject } from '../../../shared/models/activitypub/objects'
import { VideoPrivacy } from '../../../shared/models/videos'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { buildRemoteVideoBaseUrl, checkUrlsSameHost, getAPId } from '../../helpers/activitypub'
-import { isAPVideoFileMetadataObject, sanitizeAndCheckVideoTorrentObject } from '../../helpers/custom-validators/activitypub/videos'
+import {
+ isAPVideoFileUrlMetadataObject,
+ isAPVideoTrackerUrlObject,
+ sanitizeAndCheckVideoTorrentObject
+} from '../../helpers/custom-validators/activitypub/videos'
import { isArray } from '../../helpers/custom-validators/misc'
import { isVideoFileInfoHashValid } from '../../helpers/custom-validators/videos'
import { deleteNonExistingModels, resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils'
import { addVideoComments } from './video-comments'
import { createRates } from './video-rates'
-async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: sequelize.Transaction) {
+async function federateVideoIfNeeded (videoArg: MVideoAPWithoutCaption, isNewVideo: boolean, transaction?: Transaction) {
const video = videoArg as MVideoAP
if (
await setVideoTags({ video: videoUpdated, tags, transaction: t, defaultValue: videoUpdated.Tags })
}
+ // Update trackers
+ {
+ const trackers = getTrackerUrls(videoObject, videoUpdated)
+ await setVideoTrackers({ video: videoUpdated, trackers, transaction: t })
+ }
+
{
// Update captions
await VideoCaptionModel.deleteAllCaptionsOfRemoteVideo(videoUpdated.id, t)
return MIMETYPES.VIDEO.MIMETYPE_EXT[urlMediaType] && urlMediaType.startsWith('video/')
}
-function isAPStreamingPlaylistUrlObject (url: ActivityUrlObject): url is ActivityPlaylistUrlObject {
+function isAPStreamingPlaylistUrlObject (url: any): url is ActivityPlaylistUrlObject {
return url && url.mediaType === 'application/x-mpegURL'
}
})
await Promise.all(videoCaptionsPromises)
+ // Process trackers
+ {
+ const trackers = getTrackerUrls(videoObject, videoCreated)
+ await setVideoTrackers({ video: videoCreated, trackers, transaction: t })
+ }
+
videoCreated.VideoFiles = videoFiles
if (videoCreated.isLive) {
: parsed.xs
// Fetch associated metadata url, if any
- const metadata = urls.filter(isAPVideoFileMetadataObject)
+ const metadata = urls.filter(isAPVideoFileUrlMetadataObject)
.find(u => {
return u.height === fileUrl.height &&
u.fps === fileUrl.fps &&
? previewIcon.url
: buildRemoteVideoBaseUrl(video, join(LAZY_STATIC_PATHS.PREVIEWS, video.generatePreviewName()))
}
+
+function getTrackerUrls (object: VideoObject, video: MVideoWithHost) {
+ let wsFound = false
+
+ const trackers = object.url.filter(u => isAPVideoTrackerUrlObject(u))
+ .map((u: ActivityTrackerUrlObject) => {
+ if (u.rel.includes('websocket')) wsFound = true
+
+ return u.href
+ })
+
+ if (wsFound) return trackers
+
+ return [
+ buildRemoteVideoBaseUrl(video, '/tracker/socket', REMOTE_SCHEME.WS),
+ buildRemoteVideoBaseUrl(video, '/tracker/announce')
+ ]
+}
+
+async function setVideoTrackers (options: {
+ video: MVideo
+ trackers: string[]
+ transaction?: Transaction
+}) {
+ const { video, trackers, transaction } = options
+
+ const trackerInstances = await TrackerModel.findOrCreateTrackers(trackers, transaction)
+
+ await video.$set('Trackers', trackerInstances, { transaction })
+}
import { move } from 'fs-extra'
import { join } from 'path'
import { getServerActor } from '@server/models/application/application'
+import { TrackerModel } from '@server/models/server/tracker'
import { VideoModel } from '@server/models/video/video'
import {
MStreamingPlaylist,
logger.info('Duplicating %s - %d in videos redundancy with "%s" strategy.', video.url, file.resolution, strategy)
- const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
- const magnetUri = generateMagnetUri(video, video, file, baseUrlHttp, baseUrlWs)
+ const trackerUrls = await TrackerModel.listUrlsByVideoId(video.id)
+ const magnetUri = generateMagnetUri(video, file, trackerUrls)
const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT)
import { copy } from 'fs-extra'
import { join } from 'path'
+import { logger } from '@server/helpers/logger'
import { ThumbnailType } from '../../shared/models/videos/thumbnail.type'
import { generateImageFromVideoFile } from '../helpers/ffmpeg-utils'
import { processImage } from '../helpers/image-utils'
size?: ImageSize
}) {
const { downloadUrl, video, type, size } = options
- const { filename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
+ const { filename: updatedFilename, basePath, height, width, existingThumbnail } = buildMetadataFromVideo(video, type, size)
// Only save the file URL if it is a remote video
const fileUrl = video.isOwned()
// If the thumbnail URL did not change and has a unique filename (introduced in 3.2), avoid thumbnail processing
const thumbnailUrlChanged = !existingUrl || existingUrl !== downloadUrl || downloadUrl.endsWith(`${video.uuid}.jpg`)
+
+ // Do not change the thumbnail filename if the file did not change
+ const filename = thumbnailUrlChanged
+ ? updatedFilename
+ : existingThumbnail.filename
+
const thumbnailCreator = () => {
if (thumbnailUrlChanged) return downloadImage(downloadUrl, basePath, filename, { width, height })
- return copy(existingThumbnail.getPath(), ThumbnailModel.buildPath(type, filename))
+ return Promise.resolve()
}
return createThumbnailFromFunction({ thumbnailCreator, filename, height, width, type, existingThumbnail, fileUrl })
fileUrl = null
} = parameters
- const oldFilename = existingThumbnail
+ const oldFilename = existingThumbnail && existingThumbnail.filename !== filename
? existingThumbnail.filename
: undefined
thumbnail.type = type
thumbnail.fileUrl = fileUrl
thumbnail.automaticallyGenerated = automaticallyGenerated
- thumbnail.previousThumbnailFilename = oldFilename
+
+ if (oldFilename) thumbnail.previousThumbnailFilename = oldFilename
await thumbnailCreator()
--- /dev/null
+import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { Transaction } from 'sequelize/types'
+import { MTracker } from '@server/types/models/server/tracker'
+import { VideoModel } from '../video/video'
+import { VideoTrackerModel } from './video-tracker'
+
+@Table({
+ tableName: 'tracker',
+ indexes: [
+ {
+ fields: [ 'url' ],
+ unique: true
+ }
+ ]
+})
+export class TrackerModel extends Model {
+
+ @AllowNull(false)
+ @Column
+ url: string
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @BelongsToMany(() => VideoModel, {
+ foreignKey: 'trackerId',
+ through: () => VideoTrackerModel,
+ onDelete: 'CASCADE'
+ })
+ Videos: VideoModel[]
+
+ static listUrlsByVideoId (videoId: number) {
+ const query = {
+ include: [
+ {
+ attributes: [ 'id', 'trackerId' ],
+ model: VideoModel.unscoped(),
+ required: true,
+ where: { id: videoId }
+ }
+ ]
+ }
+
+ return TrackerModel.findAll(query)
+ .then(rows => rows.map(rows => rows.url))
+ }
+
+ static findOrCreateTrackers (trackers: string[], transaction: Transaction): Promise<MTracker[]> {
+ if (trackers === null) return Promise.resolve([])
+
+ const tasks: Promise<MTracker>[] = []
+ trackers.forEach(tracker => {
+ const query = {
+ where: {
+ url: tracker
+ },
+ defaults: {
+ url: tracker
+ },
+ transaction
+ }
+
+ const promise = TrackerModel.findOrCreate<MTracker>(query)
+ .then(([ trackerInstance ]) => trackerInstance)
+ tasks.push(promise)
+ })
+
+ return Promise.all(tasks)
+ }
+}
--- /dev/null
+import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { VideoModel } from '../video/video'
+import { TrackerModel } from './tracker'
+
+@Table({
+ tableName: 'videoTracker',
+ indexes: [
+ {
+ fields: [ 'videoId' ]
+ },
+ {
+ fields: [ 'trackerId' ]
+ }
+ ]
+})
+export class VideoTrackerModel extends Model {
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @ForeignKey(() => VideoModel)
+ @Column
+ videoId: number
+
+ @ForeignKey(() => TrackerModel)
+ @Column
+ trackerId: number
+}
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { afterCommitIfTransaction } from '@server/helpers/database-utils'
import { MThumbnail, MThumbnailVideo, MVideoWithHost } from '@server/types/models'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
const staticPath = ThumbnailModel.types[this.type].staticPath + this.filename
if (video.isOwned()) return WEBSERVER.URL + staticPath
- if (this.fileUrl) return this.fileUrl
- // Fallback if we don't have a file URL
- return buildRemoteVideoBaseUrl(video, staticPath)
+ return this.fileUrl
}
getPath () {
UpdatedAt
} from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid'
-import { buildRemoteVideoBaseUrl } from '@server/helpers/activitypub'
import { MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo, MVideoWithHost } from '@server/types/models'
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
if (!this.Video) this.Video = video as VideoModel
if (video.isOwned()) return WEBSERVER.URL + this.getCaptionStaticPath()
- if (this.fileUrl) return this.fileUrl
- // Fallback if we don't have a file URL
- return buildRemoteVideoBaseUrl(video, this.getCaptionStaticPath())
+ return this.fileUrl
}
}
if (!this.Video) this.Video = video as VideoModel
if (video.isOwned()) return WEBSERVER.URL + this.getFileStaticPath(video)
- if (this.fileUrl) return this.fileUrl
- // Fallback if we don't have a file URL
- return buildRemoteVideoBaseUrl(video, this.getFileStaticPath(video))
+ return this.fileUrl
}
getFileStaticPath (video: MVideo) {
getRemoteTorrentUrl (video: MVideoWithHost) {
if (video.isOwned()) throw new Error(`Video ${video.url} is not a remote video`)
- if (this.torrentUrl) return this.torrentUrl
-
- // Fallback if we don't have a torrent URL
- return buildRemoteVideoBaseUrl(video, this.getTorrentStaticPath())
+ return this.torrentUrl
}
// We proxify torrent requests so use a local URL
} from '../../lib/activitypub/url'
import {
MStreamingPlaylistRedundanciesOpt,
- MStreamingPlaylistVideo,
- MVideo,
MVideoAP,
MVideoFile,
MVideoFormattable,
}
})
- const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
-
const tags = video.Tags ? video.Tags.map(t => t.name) : []
const streamingPlaylists = streamingPlaylistsModelToFormattedJSON(video, video.VideoStreamingPlaylists)
label: VideoModel.getStateLabel(video.state)
},
- trackerUrls: video.getTrackerUrls(baseUrlHttp, baseUrlWs),
+ trackerUrls: video.getTrackerUrls(),
files: [],
streamingPlaylists
}
// Format and sort video files
- detailsJson.files = videoFilesModelToFormattedJSON(video, video, baseUrlHttp, baseUrlWs, video.VideoFiles)
+ detailsJson.files = videoFilesModelToFormattedJSON(video, video.VideoFiles)
return Object.assign(formattedJson, detailsJson)
}
): VideoStreamingPlaylist[] {
if (isArray(playlists) === false) return []
- const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
-
return playlists
.map(playlist => {
- const playlistWithVideo = Object.assign(playlist, { Video: video })
-
const redundancies = isArray(playlist.RedundancyVideos)
? playlist.RedundancyVideos.map(r => ({ baseUrl: r.fileUrl }))
: []
- const files = videoFilesModelToFormattedJSON(playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles)
+ const files = videoFilesModelToFormattedJSON(video, playlist.VideoFiles)
return {
id: playlist.id,
return -1
}
-// FIXME: refactor/merge model and video arguments
function videoFilesModelToFormattedJSON (
- model: MVideo | MStreamingPlaylistVideo,
video: MVideoFormattableDetails,
- baseUrlHttp: string,
- baseUrlWs: string,
videoFiles: MVideoFileRedundanciesOpt[]
): VideoFile[] {
+ const trackerUrls = video.getTrackerUrls()
+
return [ ...videoFiles ]
.filter(f => !f.isLive())
.sort(sortByResolutionDesc)
},
// FIXME: deprecated in 3.2
- magnetUri: generateMagnetUri(model, video, videoFile, baseUrlHttp, baseUrlWs),
+ magnetUri: generateMagnetUri(video, videoFile, trackerUrls),
size: videoFile.size,
fps: videoFile.fps,
})
}
-// FIXME: refactor/merge model and video arguments
function addVideoFilesInAPAcc (
acc: ActivityUrlObject[] | ActivityTagObject[],
- model: MVideoAP | MStreamingPlaylistVideo,
video: MVideoWithHost,
- baseUrlHttp: string,
- baseUrlWs: string,
files: MVideoFile[]
) {
+ const trackerUrls = video.getTrackerUrls()
+
const sortedFiles = [ ...files ]
.filter(f => !f.isLive())
.sort(sortByResolutionDesc)
acc.push({
type: 'Link',
mediaType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
- href: generateMagnetUri(model, video, file, baseUrlHttp, baseUrlWs),
+ href: generateMagnetUri(video, file, trackerUrls),
height: file.resolution
})
}
}
function videoModelToActivityPubObject (video: MVideoAP): VideoObject {
- const { baseUrlHttp, baseUrlWs } = video.getBaseUrls()
if (!video.Tags) video.Tags = []
const tag = video.Tags.map(t => ({
}
]
- addVideoFilesInAPAcc(url, video, video, baseUrlHttp, baseUrlWs, video.VideoFiles || [])
+ addVideoFilesInAPAcc(url, video, video.VideoFiles || [])
for (const playlist of (video.VideoStreamingPlaylists || [])) {
const tag = playlist.p2pMediaLoaderInfohashes
href: playlist.segmentsSha256Url
})
- const playlistWithVideo = Object.assign(playlist, { Video: video })
- addVideoFilesInAPAcc(tag, playlistWithVideo, video, baseUrlHttp, baseUrlWs, playlist.VideoFiles || [])
+ addVideoFilesInAPAcc(tag, video, playlist.VideoFiles || [])
url.push({
type: 'Link',
})
}
+ for (const trackerUrl of video.getTrackerUrls()) {
+ const rel2 = trackerUrl.startsWith('http')
+ ? 'http'
+ : 'websocket'
+
+ url.push({
+ type: 'Link',
+ name: `tracker-${rel2}`,
+ rel: [ 'tracker', rel2 ],
+ href: trackerUrl
+ })
+ }
+
const subtitleLanguage = []
for (const caption of video.VideoCaptions) {
subtitleLanguage.push({
API_VERSION,
CONSTRAINTS_FIELDS,
LAZY_STATIC_PATHS,
- REMOTE_SCHEME,
STATIC_PATHS,
VIDEO_CATEGORIES,
VIDEO_LANGUAGES,
import { AvatarModel } from '../avatar/avatar'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server'
+import { TrackerModel } from '../server/tracker'
+import { VideoTrackerModel } from '../server/video-tracker'
import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { TagModel } from './tag'
FOR_API = 'FOR_API',
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
+ WITH_TRACKERS = 'WITH_TRACKERS',
WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
[ScopeNames.WITH_TAGS]: {
include: [ TagModel ]
},
+ [ScopeNames.WITH_TRACKERS]: {
+ include: [
+ {
+ attributes: [ 'id', 'url' ],
+ model: TrackerModel
+ }
+ ]
+ },
[ScopeNames.WITH_BLACKLISTED]: {
include: [
{
})
Tags: TagModel[]
+ @BelongsToMany(() => TrackerModel, {
+ foreignKey: 'videoId',
+ through: () => VideoTrackerModel,
+ onDelete: 'CASCADE'
+ })
+ Trackers: TrackerModel[]
+
@HasMany(() => ThumbnailModel, {
foreignKey: {
name: 'videoId',
ScopeNames.WITH_SCHEDULED_UPDATE,
ScopeNames.WITH_THUMBNAILS,
ScopeNames.WITH_LIVE,
+ ScopeNames.WITH_TRACKERS,
{ method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
{ method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
]
}
getFormattedVideoFilesJSON (): VideoFile[] {
- const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
let files: VideoFile[] = []
if (Array.isArray(this.VideoFiles)) {
- const result = videoFilesModelToFormattedJSON(this, this, baseUrlHttp, baseUrlWs, this.VideoFiles)
+ const result = videoFilesModelToFormattedJSON(this, this.VideoFiles)
files = files.concat(result)
}
for (const p of (this.VideoStreamingPlaylists || [])) {
- p.Video = this
-
- const result = videoFilesModelToFormattedJSON(p, this, baseUrlHttp, baseUrlWs, p.VideoFiles)
+ const result = videoFilesModelToFormattedJSON(this, p.VideoFiles)
files = files.concat(result)
}
return false
}
- getBaseUrls () {
- if (this.isOwned()) {
- return {
- baseUrlHttp: WEBSERVER.URL,
- baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
- }
- }
-
- return {
- baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
- baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
- }
+ getBandwidthBits (videoFile: MVideoFile) {
+ return Math.ceil((videoFile.size * 8) / this.duration)
}
- getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
- return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
- }
+ getTrackerUrls () {
+ if (this.isOwned()) {
+ return [
+ WEBSERVER.URL + '/tracker/announce',
+ WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT + '/tracker/socket'
+ ]
+ }
- getBandwidthBits (videoFile: MVideoFile) {
- return Math.ceil((videoFile.size * 8) / this.duration)
+ return this.Trackers.map(t => t.url)
}
}
--- /dev/null
+import { TrackerModel } from '../../../models/server/tracker'
+
+export type MTracker = Omit<TrackerModel, 'Videos'>
+
+// ############################################################################
+
+export type MTrackerUrl = Pick<MTracker, 'url'>
import { PickWith, PickWithOpt } from '@shared/core-utils'
import { VideoModel } from '../../../models/video/video'
+import { MTrackerUrl } from '../server/tracker'
import { MUserVideoHistoryTime } from '../user/user-video-history'
import { MScheduleVideoUpdate } from './schedule-video-update'
import { MTag } from './tag'
Use<'VideoChannel', MChannelFormattable> &
Use<'Tags', MTag[]> &
Use<'VideoStreamingPlaylists', MStreamingPlaylistRedundanciesOpt[]> &
- Use<'VideoFiles', MVideoFileRedundanciesOpt[]>
+ Use<'VideoFiles', MVideoFileRedundanciesOpt[]> &
+ Use<'Trackers', MTrackerUrl[]>
href: string
}
-export type ActivityVideoFileMetadataObject = {
+export type ActivityVideoFileMetadataUrlObject = {
type: 'Link'
rel: [ 'metadata', any ]
mediaType: 'application/json'
fps: number
}
+export type ActivityTrackerUrlObject = {
+ type: 'Link'
+ rel: [ 'tracker', 'websocket' | 'http' ]
+ name: string
+ href: string
+}
+
export type ActivityPlaylistInfohashesObject = {
type: 'Infohash'
name: string
| ActivityMentionObject
| ActivityBitTorrentUrlObject
| ActivityMagnetUrlObject
- | ActivityVideoFileMetadataObject
+ | ActivityVideoFileMetadataUrlObject
export type ActivityUrlObject =
ActivityVideoUrlObject
| ActivityBitTorrentUrlObject
| ActivityMagnetUrlObject
| ActivityHtmlUrlObject
- | ActivityVideoFileMetadataObject
+ | ActivityVideoFileMetadataUrlObject
+ | ActivityTrackerUrlObject
export interface ActivityPubAttributedTo {
type: 'Group' | 'Person'
icon: ActivityIconObject[]
url: ActivityUrlObject[]
+
likes: string
dislikes: string
shares: string
comments: string
+
attributedTo: ActivityPubAttributedTo[]
+
to?: string[]
cc?: string[]
}