diff options
Diffstat (limited to 'server')
34 files changed, 347 insertions, 96 deletions
diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 86ef2aed1..a69a83acf 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts | |||
@@ -74,10 +74,10 @@ async function listVideoAccountChannels (req: express.Request, res: express.Resp | |||
74 | 74 | ||
75 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 75 | async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
76 | const account: AccountModel = res.locals.account | 76 | const account: AccountModel = res.locals.account |
77 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 77 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
78 | 78 | ||
79 | const resultList = await VideoModel.listForApi({ | 79 | const resultList = await VideoModel.listForApi({ |
80 | actorId, | 80 | followerActorId, |
81 | start: req.query.start, | 81 | start: req.query.start, |
82 | count: req.query.count, | 82 | count: req.query.count, |
83 | sort: req.query.sort, | 83 | sort: req.query.sort, |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 5233e9f68..d65e321e9 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -11,6 +11,7 @@ import { ClientHtml } from '../../lib/client-html' | |||
11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' | 11 | import { auditLoggerFactory, CustomConfigAuditView, getAuditIdFromRes } from '../../helpers/audit-logger' |
12 | import { remove, writeJSON } from 'fs-extra' | 12 | import { remove, writeJSON } from 'fs-extra' |
13 | import { getServerCommit } from '../../helpers/utils' | 13 | import { getServerCommit } from '../../helpers/utils' |
14 | import { Emailer } from '../../lib/emailer' | ||
14 | 15 | ||
15 | const packageJSON = require('../../../../package.json') | 16 | const packageJSON = require('../../../../package.json') |
16 | const configRouter = express.Router() | 17 | const configRouter = express.Router() |
@@ -61,6 +62,9 @@ async function getConfig (req: express.Request, res: express.Response) { | |||
61 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | 62 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS |
62 | } | 63 | } |
63 | }, | 64 | }, |
65 | email: { | ||
66 | enabled: Emailer.Instance.isEnabled() | ||
67 | }, | ||
64 | serverVersion: packageJSON.version, | 68 | serverVersion: packageJSON.version, |
65 | serverCommit, | 69 | serverCommit, |
66 | signup: { | 70 | signup: { |
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts index 82299747d..d2456346b 100644 --- a/server/controllers/api/users/me.ts +++ b/server/controllers/api/users/me.ts | |||
@@ -42,7 +42,7 @@ import { AccountModel } from '../../../models/account/account' | |||
42 | 42 | ||
43 | const auditLogger = auditLoggerFactory('users-me') | 43 | const auditLogger = auditLoggerFactory('users-me') |
44 | 44 | ||
45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 45 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
46 | 46 | ||
47 | const meRouter = express.Router() | 47 | const meRouter = express.Router() |
48 | 48 | ||
@@ -238,7 +238,7 @@ async function getUserSubscriptionVideos (req: express.Request, res: express.Res | |||
238 | nsfw: buildNSFWFilter(res, req.query.nsfw), | 238 | nsfw: buildNSFWFilter(res, req.query.nsfw), |
239 | filter: req.query.filter as VideoFilter, | 239 | filter: req.query.filter as VideoFilter, |
240 | withFiles: false, | 240 | withFiles: false, |
241 | actorId: user.Account.Actor.id, | 241 | followerActorId: user.Account.Actor.id, |
242 | user | 242 | user |
243 | }) | 243 | }) |
244 | 244 | ||
@@ -348,7 +348,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
348 | return res.sendStatus(204) | 348 | return res.sendStatus(204) |
349 | } | 349 | } |
350 | 350 | ||
351 | async function updateMyAvatar (req: express.Request, res: express.Response, next: express.NextFunction) { | 351 | async function updateMyAvatar (req: express.Request, res: express.Response) { |
352 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] | 352 | const avatarPhysicalFile = req.files[ 'avatarfile' ][ 0 ] |
353 | const user: UserModel = res.locals.oauth.token.user | 353 | const user: UserModel = res.locals.oauth.token.user |
354 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) | 354 | const oldUserAuditView = new UserAuditView(user.toFormattedJSON()) |
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts index 9bf3c5fd8..fd143a139 100644 --- a/server/controllers/api/video-channel.ts +++ b/server/controllers/api/video-channel.ts | |||
@@ -32,7 +32,7 @@ import { resetSequelizeInstance } from '../../helpers/database-utils' | |||
32 | import { UserModel } from '../../models/account/user' | 32 | import { UserModel } from '../../models/account/user' |
33 | 33 | ||
34 | const auditLogger = auditLoggerFactory('channels') | 34 | const auditLogger = auditLoggerFactory('channels') |
35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 35 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.TMP_DIR }) |
36 | 36 | ||
37 | const videoChannelRouter = express.Router() | 37 | const videoChannelRouter = express.Router() |
38 | 38 | ||
@@ -202,10 +202,10 @@ async function getVideoChannel (req: express.Request, res: express.Response, nex | |||
202 | 202 | ||
203 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 203 | async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
204 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel | 204 | const videoChannelInstance: VideoChannelModel = res.locals.videoChannel |
205 | const actorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 205 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined |
206 | 206 | ||
207 | const resultList = await VideoModel.listForApi({ | 207 | const resultList = await VideoModel.listForApi({ |
208 | actorId, | 208 | followerActorId, |
209 | start: req.query.start, | 209 | start: req.query.start, |
210 | count: req.query.count, | 210 | count: req.query.count, |
211 | sort: req.query.sort, | 211 | sort: req.query.sort, |
diff --git a/server/controllers/api/videos/import.ts b/server/controllers/api/videos/import.ts index 398fd5a7f..f27d648c7 100644 --- a/server/controllers/api/videos/import.ts +++ b/server/controllers/api/videos/import.ts | |||
@@ -37,9 +37,9 @@ const reqVideoFileImport = createReqFiles( | |||
37 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], | 37 | [ 'thumbnailfile', 'previewfile', 'torrentfile' ], |
38 | Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), | 38 | Object.assign({}, TORRENT_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), |
39 | { | 39 | { |
40 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 40 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
41 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR, | 41 | previewfile: CONFIG.STORAGE.TMP_DIR, |
42 | torrentfile: CONFIG.STORAGE.TORRENTS_DIR | 42 | torrentfile: CONFIG.STORAGE.TMP_DIR |
43 | } | 43 | } |
44 | ) | 44 | ) |
45 | 45 | ||
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 3d1b2e1a2..4e4697ef4 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -67,17 +67,17 @@ const reqVideoFileAdd = createReqFiles( | |||
67 | [ 'videofile', 'thumbnailfile', 'previewfile' ], | 67 | [ 'videofile', 'thumbnailfile', 'previewfile' ], |
68 | Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), | 68 | Object.assign({}, VIDEO_MIMETYPE_EXT, IMAGE_MIMETYPE_EXT), |
69 | { | 69 | { |
70 | videofile: CONFIG.STORAGE.VIDEOS_DIR, | 70 | videofile: CONFIG.STORAGE.TMP_DIR, |
71 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 71 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
72 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 72 | previewfile: CONFIG.STORAGE.TMP_DIR |
73 | } | 73 | } |
74 | ) | 74 | ) |
75 | const reqVideoFileUpdate = createReqFiles( | 75 | const reqVideoFileUpdate = createReqFiles( |
76 | [ 'thumbnailfile', 'previewfile' ], | 76 | [ 'thumbnailfile', 'previewfile' ], |
77 | IMAGE_MIMETYPE_EXT, | 77 | IMAGE_MIMETYPE_EXT, |
78 | { | 78 | { |
79 | thumbnailfile: CONFIG.STORAGE.THUMBNAILS_DIR, | 79 | thumbnailfile: CONFIG.STORAGE.TMP_DIR, |
80 | previewfile: CONFIG.STORAGE.PREVIEWS_DIR | 80 | previewfile: CONFIG.STORAGE.TMP_DIR |
81 | } | 81 | } |
82 | ) | 82 | ) |
83 | 83 | ||
diff --git a/server/controllers/bots.ts b/server/controllers/bots.ts new file mode 100644 index 000000000..2db86a2d8 --- /dev/null +++ b/server/controllers/bots.ts | |||
@@ -0,0 +1,101 @@ | |||
1 | import * as express from 'express' | ||
2 | import { asyncMiddleware } from '../middlewares' | ||
3 | import { CONFIG, ROUTE_CACHE_LIFETIME } from '../initializers' | ||
4 | import * as sitemapModule from 'sitemap' | ||
5 | import { logger } from '../helpers/logger' | ||
6 | import { VideoModel } from '../models/video/video' | ||
7 | import { VideoChannelModel } from '../models/video/video-channel' | ||
8 | import { AccountModel } from '../models/account/account' | ||
9 | import { cacheRoute } from '../middlewares/cache' | ||
10 | import { buildNSFWFilter } from '../helpers/express-utils' | ||
11 | import { truncate } from 'lodash' | ||
12 | |||
13 | const botsRouter = express.Router() | ||
14 | |||
15 | // Special route that add OpenGraph and oEmbed tags | ||
16 | // Do not use a template engine for a so little thing | ||
17 | botsRouter.use('/sitemap.xml', | ||
18 | asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.SITEMAP)), | ||
19 | asyncMiddleware(getSitemap) | ||
20 | ) | ||
21 | |||
22 | // --------------------------------------------------------------------------- | ||
23 | |||
24 | export { | ||
25 | botsRouter | ||
26 | } | ||
27 | |||
28 | // --------------------------------------------------------------------------- | ||
29 | |||
30 | async function getSitemap (req: express.Request, res: express.Response) { | ||
31 | let urls = getSitemapBasicUrls() | ||
32 | |||
33 | urls = urls.concat(await getSitemapLocalVideoUrls()) | ||
34 | urls = urls.concat(await getSitemapVideoChannelUrls()) | ||
35 | urls = urls.concat(await getSitemapAccountUrls()) | ||
36 | |||
37 | const sitemap = sitemapModule.createSitemap({ | ||
38 | hostname: CONFIG.WEBSERVER.URL, | ||
39 | urls: urls | ||
40 | }) | ||
41 | |||
42 | sitemap.toXML((err, xml) => { | ||
43 | if (err) { | ||
44 | logger.error('Cannot generate sitemap.', { err }) | ||
45 | return res.sendStatus(500) | ||
46 | } | ||
47 | |||
48 | res.header('Content-Type', 'application/xml') | ||
49 | res.send(xml) | ||
50 | }) | ||
51 | } | ||
52 | |||
53 | async function getSitemapVideoChannelUrls () { | ||
54 | const rows = await VideoChannelModel.listLocalsForSitemap('createdAt') | ||
55 | |||
56 | return rows.map(channel => ({ | ||
57 | url: CONFIG.WEBSERVER.URL + '/video-channels/' + channel.Actor.preferredUsername | ||
58 | })) | ||
59 | } | ||
60 | |||
61 | async function getSitemapAccountUrls () { | ||
62 | const rows = await AccountModel.listLocalsForSitemap('createdAt') | ||
63 | |||
64 | return rows.map(channel => ({ | ||
65 | url: CONFIG.WEBSERVER.URL + '/accounts/' + channel.Actor.preferredUsername | ||
66 | })) | ||
67 | } | ||
68 | |||
69 | async function getSitemapLocalVideoUrls () { | ||
70 | const resultList = await VideoModel.listForApi({ | ||
71 | start: 0, | ||
72 | count: undefined, | ||
73 | sort: 'createdAt', | ||
74 | includeLocalVideos: true, | ||
75 | nsfw: buildNSFWFilter(), | ||
76 | filter: 'local', | ||
77 | withFiles: false | ||
78 | }) | ||
79 | |||
80 | return resultList.data.map(v => ({ | ||
81 | url: CONFIG.WEBSERVER.URL + '/videos/watch/' + v.uuid, | ||
82 | video: [ | ||
83 | { | ||
84 | title: v.name, | ||
85 | // Sitemap description should be < 2000 characters | ||
86 | description: truncate(v.description || v.name, { length: 2000, omission: '...' }), | ||
87 | player_loc: CONFIG.WEBSERVER.URL + '/videos/embed/' + v.uuid, | ||
88 | thumbnail_loc: CONFIG.WEBSERVER.URL + v.getThumbnailStaticPath() | ||
89 | } | ||
90 | ] | ||
91 | })) | ||
92 | } | ||
93 | |||
94 | function getSitemapBasicUrls () { | ||
95 | const paths = [ | ||
96 | '/about/instance', | ||
97 | '/videos/local' | ||
98 | ] | ||
99 | |||
100 | return paths.map(p => ({ url: CONFIG.WEBSERVER.URL + p })) | ||
101 | } | ||
diff --git a/server/controllers/index.ts b/server/controllers/index.ts index 197fa897a..a88a03c79 100644 --- a/server/controllers/index.ts +++ b/server/controllers/index.ts | |||
@@ -6,3 +6,4 @@ export * from './services' | |||
6 | export * from './static' | 6 | export * from './static' |
7 | export * from './webfinger' | 7 | export * from './webfinger' |
8 | export * from './tracker' | 8 | export * from './tracker' |
9 | export * from './bots' | ||
diff --git a/server/controllers/static.ts b/server/controllers/static.ts index 75e30353c..55e7392a1 100644 --- a/server/controllers/static.ts +++ b/server/controllers/static.ts | |||
@@ -34,13 +34,18 @@ staticRouter.use( | |||
34 | ) | 34 | ) |
35 | 35 | ||
36 | // Videos path for webseeding | 36 | // Videos path for webseeding |
37 | const videosPhysicalPath = CONFIG.STORAGE.VIDEOS_DIR | ||
38 | staticRouter.use( | 37 | staticRouter.use( |
39 | STATIC_PATHS.WEBSEED, | 38 | STATIC_PATHS.WEBSEED, |
40 | cors(), | 39 | cors(), |
41 | express.static(videosPhysicalPath) | 40 | express.static(CONFIG.STORAGE.VIDEOS_DIR, { fallthrough: false }) // 404 because we don't have this video |
42 | ) | 41 | ) |
43 | staticRouter.use( | 42 | staticRouter.use( |
43 | STATIC_PATHS.REDUNDANCY, | ||
44 | cors(), | ||
45 | express.static(CONFIG.STORAGE.REDUNDANCY_DIR, { fallthrough: false }) // 404 because we don't have this video | ||
46 | ) | ||
47 | |||
48 | staticRouter.use( | ||
44 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', | 49 | STATIC_DOWNLOAD_PATHS.VIDEOS + ':id-:resolution([0-9]+).:extension', |
45 | asyncMiddleware(videosGetValidator), | 50 | asyncMiddleware(videosGetValidator), |
46 | asyncMiddleware(downloadVideoFile) | 51 | asyncMiddleware(downloadVideoFile) |
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts index 162fe2244..9a72ee96d 100644 --- a/server/helpers/express-utils.ts +++ b/server/helpers/express-utils.ts | |||
@@ -7,12 +7,12 @@ import { extname } from 'path' | |||
7 | import { isArray } from './custom-validators/misc' | 7 | import { isArray } from './custom-validators/misc' |
8 | import { UserModel } from '../models/account/user' | 8 | import { UserModel } from '../models/account/user' |
9 | 9 | ||
10 | function buildNSFWFilter (res: express.Response, paramNSFW?: string) { | 10 | function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { |
11 | if (paramNSFW === 'true') return true | 11 | if (paramNSFW === 'true') return true |
12 | if (paramNSFW === 'false') return false | 12 | if (paramNSFW === 'false') return false |
13 | if (paramNSFW === 'both') return undefined | 13 | if (paramNSFW === 'both') return undefined |
14 | 14 | ||
15 | if (res.locals.oauth) { | 15 | if (res && res.locals.oauth) { |
16 | const user: UserModel = res.locals.oauth.token.User | 16 | const user: UserModel = res.locals.oauth.token.User |
17 | 17 | ||
18 | // User does not want NSFW videos | 18 | // User does not want NSFW videos |
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts index da3285b13..e43ea3f1d 100644 --- a/server/helpers/image-utils.ts +++ b/server/helpers/image-utils.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import * as sharp from 'sharp' | 2 | import * as sharp from 'sharp' |
3 | import { move, remove } from 'fs-extra' | 3 | import { readFile, remove } from 'fs-extra' |
4 | import { logger } from './logger' | ||
4 | 5 | ||
5 | async function processImage ( | 6 | async function processImage ( |
6 | physicalFile: { path: string }, | 7 | physicalFile: { path: string }, |
@@ -11,14 +12,11 @@ async function processImage ( | |||
11 | throw new Error('Sharp needs an input path different that the output path.') | 12 | throw new Error('Sharp needs an input path different that the output path.') |
12 | } | 13 | } |
13 | 14 | ||
14 | const sharpInstance = sharp(physicalFile.path) | 15 | logger.debug('Processing image %s to %s.', physicalFile.path, destination) |
15 | const metadata = await sharpInstance.metadata() | ||
16 | 16 | ||
17 | // No need to resize | 17 | // Avoid sharp cache |
18 | if (metadata.width === newSize.width && metadata.height === newSize.height) { | 18 | const buf = await readFile(physicalFile.path) |
19 | await move(physicalFile.path, destination, { overwrite: true }) | 19 | const sharpInstance = sharp(buf) |
20 | return | ||
21 | } | ||
22 | 20 | ||
23 | await remove(destination) | 21 | await remove(destination) |
24 | 22 | ||
diff --git a/server/helpers/requests.ts b/server/helpers/requests.ts index 805930a9f..3fc776f1a 100644 --- a/server/helpers/requests.ts +++ b/server/helpers/requests.ts | |||
@@ -1,8 +1,9 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { createWriteStream } from 'fs-extra' | 2 | import { createWriteStream } from 'fs-extra' |
3 | import * as request from 'request' | 3 | import * as request from 'request' |
4 | import { ACTIVITY_PUB } from '../initializers' | 4 | import { ACTIVITY_PUB, CONFIG } from '../initializers' |
5 | import { processImage } from './image-utils' | 5 | import { processImage } from './image-utils' |
6 | import { join } from 'path' | ||
6 | 7 | ||
7 | function doRequest <T> ( | 8 | function doRequest <T> ( |
8 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } | 9 | requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean } |
@@ -28,11 +29,11 @@ function doRequestAndSaveToFile (requestOptions: request.CoreOptions & request.U | |||
28 | }) | 29 | }) |
29 | } | 30 | } |
30 | 31 | ||
31 | async function downloadImage (url: string, destPath: string, size: { width: number, height: number }) { | 32 | async function downloadImage (url: string, destDir: string, destName: string, size: { width: number, height: number }) { |
32 | const tmpPath = destPath + '.tmp' | 33 | const tmpPath = join(CONFIG.STORAGE.TMP_DIR, 'pending-' + destName) |
33 | |||
34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) | 34 | await doRequestAndSaveToFile({ method: 'GET', uri: url }, tmpPath) |
35 | 35 | ||
36 | const destPath = join(destDir, destName) | ||
36 | await processImage({ path: tmpPath }, destPath, size) | 37 | await processImage({ path: tmpPath }, destPath, size) |
37 | } | 38 | } |
38 | 39 | ||
diff --git a/server/helpers/utils.ts b/server/helpers/utils.ts index 5c9d6fe2f..9b89e3e61 100644 --- a/server/helpers/utils.ts +++ b/server/helpers/utils.ts | |||
@@ -46,11 +46,11 @@ const getServerActor = memoizee(async function () { | |||
46 | return actor | 46 | return actor |
47 | }) | 47 | }) |
48 | 48 | ||
49 | function generateVideoTmpPath (target: string | ParseTorrent) { | 49 | function generateVideoImportTmpPath (target: string | ParseTorrent) { |
50 | const id = typeof target === 'string' ? target : target.infoHash | 50 | const id = typeof target === 'string' ? target : target.infoHash |
51 | 51 | ||
52 | const hash = sha256(id) | 52 | const hash = sha256(id) |
53 | return join(CONFIG.STORAGE.VIDEOS_DIR, hash + '-import.mp4') | 53 | return join(CONFIG.STORAGE.TMP_DIR, hash + '-import.mp4') |
54 | } | 54 | } |
55 | 55 | ||
56 | function getSecureTorrentName (originalName: string) { | 56 | function getSecureTorrentName (originalName: string) { |
@@ -103,6 +103,6 @@ export { | |||
103 | getSecureTorrentName, | 103 | getSecureTorrentName, |
104 | getServerActor, | 104 | getServerActor, |
105 | getServerCommit, | 105 | getServerCommit, |
106 | generateVideoTmpPath, | 106 | generateVideoImportTmpPath, |
107 | getUUIDFromFilename | 107 | getUUIDFromFilename |
108 | } | 108 | } |
diff --git a/server/helpers/webtorrent.ts b/server/helpers/webtorrent.ts index ce35b87da..3c9a0b96a 100644 --- a/server/helpers/webtorrent.ts +++ b/server/helpers/webtorrent.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { logger } from './logger' | 1 | import { logger } from './logger' |
2 | import { generateVideoTmpPath } from './utils' | 2 | import { generateVideoImportTmpPath } from './utils' |
3 | import * as WebTorrent from 'webtorrent' | 3 | import * as WebTorrent from 'webtorrent' |
4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' | 4 | import { createWriteStream, ensureDir, remove } from 'fs-extra' |
5 | import { CONFIG } from '../initializers' | 5 | import { CONFIG } from '../initializers' |
@@ -9,10 +9,10 @@ async function downloadWebTorrentVideo (target: { magnetUri: string, torrentName | |||
9 | const id = target.magnetUri || target.torrentName | 9 | const id = target.magnetUri || target.torrentName |
10 | let timer | 10 | let timer |
11 | 11 | ||
12 | const path = generateVideoTmpPath(id) | 12 | const path = generateVideoImportTmpPath(id) |
13 | logger.info('Importing torrent video %s', id) | 13 | logger.info('Importing torrent video %s', id) |
14 | 14 | ||
15 | const directoryPath = join(CONFIG.STORAGE.VIDEOS_DIR, 'import') | 15 | const directoryPath = join(CONFIG.STORAGE.TMP_DIR, 'webtorrent') |
16 | await ensureDir(directoryPath) | 16 | await ensureDir(directoryPath) |
17 | 17 | ||
18 | return new Promise<string>((res, rej) => { | 18 | return new Promise<string>((res, rej) => { |
diff --git a/server/helpers/youtube-dl.ts b/server/helpers/youtube-dl.ts index 2a5663042..b74351b42 100644 --- a/server/helpers/youtube-dl.ts +++ b/server/helpers/youtube-dl.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { truncate } from 'lodash' | 1 | import { truncate } from 'lodash' |
2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' | 2 | import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES } from '../initializers' |
3 | import { logger } from './logger' | 3 | import { logger } from './logger' |
4 | import { generateVideoTmpPath } from './utils' | 4 | import { generateVideoImportTmpPath } from './utils' |
5 | import { join } from 'path' | 5 | import { join } from 'path' |
6 | import { root } from './core-utils' | 6 | import { root } from './core-utils' |
7 | import { ensureDir, writeFile, remove } from 'fs-extra' | 7 | import { ensureDir, writeFile, remove } from 'fs-extra' |
@@ -40,7 +40,7 @@ function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> | |||
40 | } | 40 | } |
41 | 41 | ||
42 | function downloadYoutubeDLVideo (url: string, timeout: number) { | 42 | function downloadYoutubeDLVideo (url: string, timeout: number) { |
43 | const path = generateVideoTmpPath(url) | 43 | const path = generateVideoImportTmpPath(url) |
44 | let timer | 44 | let timer |
45 | 45 | ||
46 | logger.info('Importing youtubeDL video %s', url) | 46 | logger.info('Importing youtubeDL video %s', url) |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 9dfb5d68c..b51c7cfba 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -12,6 +12,7 @@ function checkMissedConfig () { | |||
12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', | 12 | 'database.hostname', 'database.port', 'database.suffix', 'database.username', 'database.password', 'database.pool.max', |
13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 13 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', | 14 | 'storage.avatars', 'storage.videos', 'storage.logs', 'storage.previews', 'storage.thumbnails', 'storage.torrents', 'storage.cache', |
15 | 'storage.redundancy', 'storage.tmp', | ||
15 | 'log.level', | 16 | 'log.level', |
16 | 'user.video_quota', 'user.video_quota_daily', | 17 | 'user.video_quota', 'user.video_quota_daily', |
17 | 'cache.previews.size', 'admin.email', | 18 | 'cache.previews.size', 'admin.email', |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index aa243859c..6b798875c 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -61,6 +61,7 @@ const OAUTH_LIFETIME = { | |||
61 | const ROUTE_CACHE_LIFETIME = { | 61 | const ROUTE_CACHE_LIFETIME = { |
62 | FEEDS: '15 minutes', | 62 | FEEDS: '15 minutes', |
63 | ROBOTS: '2 hours', | 63 | ROBOTS: '2 hours', |
64 | SITEMAP: '1 day', | ||
64 | SECURITYTXT: '2 hours', | 65 | SECURITYTXT: '2 hours', |
65 | NODEINFO: '10 minutes', | 66 | NODEINFO: '10 minutes', |
66 | DNT_POLICY: '1 week', | 67 | DNT_POLICY: '1 week', |
@@ -185,9 +186,11 @@ const CONFIG = { | |||
185 | FROM_ADDRESS: config.get<string>('smtp.from_address') | 186 | FROM_ADDRESS: config.get<string>('smtp.from_address') |
186 | }, | 187 | }, |
187 | STORAGE: { | 188 | STORAGE: { |
189 | TMP_DIR: buildPath(config.get<string>('storage.tmp')), | ||
188 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), | 190 | AVATARS_DIR: buildPath(config.get<string>('storage.avatars')), |
189 | LOG_DIR: buildPath(config.get<string>('storage.logs')), | 191 | LOG_DIR: buildPath(config.get<string>('storage.logs')), |
190 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), | 192 | VIDEOS_DIR: buildPath(config.get<string>('storage.videos')), |
193 | REDUNDANCY_DIR: buildPath(config.get<string>('storage.redundancy')), | ||
191 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), | 194 | THUMBNAILS_DIR: buildPath(config.get<string>('storage.thumbnails')), |
192 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), | 195 | PREVIEWS_DIR: buildPath(config.get<string>('storage.previews')), |
193 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 196 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
@@ -569,6 +572,7 @@ const STATIC_PATHS = { | |||
569 | THUMBNAILS: '/static/thumbnails/', | 572 | THUMBNAILS: '/static/thumbnails/', |
570 | TORRENTS: '/static/torrents/', | 573 | TORRENTS: '/static/torrents/', |
571 | WEBSEED: '/static/webseed/', | 574 | WEBSEED: '/static/webseed/', |
575 | REDUNDANCY: '/static/redundancy/', | ||
572 | AVATARS: '/static/avatars/', | 576 | AVATARS: '/static/avatars/', |
573 | VIDEO_CAPTIONS: '/static/video-captions/' | 577 | VIDEO_CAPTIONS: '/static/video-captions/' |
574 | } | 578 | } |
@@ -773,7 +777,7 @@ function buildVideosRedundancy (objs: any[]): VideosRedundancy[] { | |||
773 | if (!objs) return [] | 777 | if (!objs) return [] |
774 | 778 | ||
775 | return objs.map(obj => { | 779 | return objs.map(obj => { |
776 | return Object.assign(obj, { | 780 | return Object.assign({}, obj, { |
777 | minLifetime: parseDuration(obj.min_lifetime), | 781 | minLifetime: parseDuration(obj.min_lifetime), |
778 | size: bytes.parse(obj.size), | 782 | size: bytes.parse(obj.size), |
779 | minViews: obj.min_views | 783 | minViews: obj.min_views |
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts index 504263c99..bbe48833d 100644 --- a/server/lib/activitypub/actor.ts +++ b/server/lib/activitypub/actor.ts | |||
@@ -178,9 +178,7 @@ async function fetchAvatarIfExists (actorJSON: ActivityPubActor) { | |||
178 | const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] | 178 | const extension = IMAGE_MIMETYPE_EXT[actorJSON.icon.mediaType] |
179 | 179 | ||
180 | const avatarName = uuidv4() + extension | 180 | const avatarName = uuidv4() + extension |
181 | const destPath = join(CONFIG.STORAGE.AVATARS_DIR, avatarName) | 181 | await downloadImage(actorJSON.icon.url, CONFIG.STORAGE.AVATARS_DIR, avatarName, AVATARS_SIZE) |
182 | |||
183 | await downloadImage(actorJSON.icon.url, destPath, AVATARS_SIZE) | ||
184 | 182 | ||
185 | return avatarName | 183 | return avatarName |
186 | } | 184 | } |
diff --git a/server/lib/activitypub/process/process-update.ts b/server/lib/activitypub/process/process-update.ts index 03831a00e..c6b42d846 100644 --- a/server/lib/activitypub/process/process-update.ts +++ b/server/lib/activitypub/process/process-update.ts | |||
@@ -51,7 +51,7 @@ async function processUpdateVideo (actor: ActorModel, activity: ActivityUpdate) | |||
51 | return undefined | 51 | return undefined |
52 | } | 52 | } |
53 | 53 | ||
54 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id }) | 54 | const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: videoObject.id, allowRefresh: false }) |
55 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) | 55 | const channelActor = await getOrCreateVideoChannelFromVideoObject(videoObject) |
56 | 56 | ||
57 | const updateOptions = { | 57 | const updateOptions = { |
diff --git a/server/lib/activitypub/videos.ts b/server/lib/activitypub/videos.ts index 998f90330..3d17e6846 100644 --- a/server/lib/activitypub/videos.ts +++ b/server/lib/activitypub/videos.ts | |||
@@ -95,9 +95,8 @@ function fetchRemoteVideoStaticFile (video: VideoModel, path: string, reject: Fu | |||
95 | 95 | ||
96 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { | 96 | function generateThumbnailFromUrl (video: VideoModel, icon: ActivityIconObject) { |
97 | const thumbnailName = video.getThumbnailName() | 97 | const thumbnailName = video.getThumbnailName() |
98 | const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName) | ||
99 | 98 | ||
100 | return downloadImage(icon.url, thumbnailPath, THUMBNAILS_SIZE) | 99 | return downloadImage(icon.url, CONFIG.STORAGE.THUMBNAILS_DIR, thumbnailName, THUMBNAILS_SIZE) |
101 | } | 100 | } |
102 | 101 | ||
103 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { | 102 | function getOrCreateVideoChannelFromVideoObject (videoObject: VideoTorrentObject) { |
@@ -158,25 +157,30 @@ async function syncVideoExternalAttributes (video: VideoModel, fetchedVideo: Vid | |||
158 | async function getOrCreateVideoAndAccountAndChannel (options: { | 157 | async function getOrCreateVideoAndAccountAndChannel (options: { |
159 | videoObject: VideoTorrentObject | string, | 158 | videoObject: VideoTorrentObject | string, |
160 | syncParam?: SyncParam, | 159 | syncParam?: SyncParam, |
161 | fetchType?: VideoFetchByUrlType | 160 | fetchType?: VideoFetchByUrlType, |
161 | allowRefresh?: boolean // true by default | ||
162 | }) { | 162 | }) { |
163 | // Default params | 163 | // Default params |
164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } | 164 | const syncParam = options.syncParam || { likes: true, dislikes: true, shares: true, comments: true, thumbnail: true, refreshVideo: false } |
165 | const fetchType = options.fetchType || 'all' | 165 | const fetchType = options.fetchType || 'all' |
166 | const allowRefresh = options.allowRefresh !== false | ||
166 | 167 | ||
167 | // Get video url | 168 | // Get video url |
168 | const videoUrl = getAPUrl(options.videoObject) | 169 | const videoUrl = getAPUrl(options.videoObject) |
169 | 170 | ||
170 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) | 171 | let videoFromDatabase = await fetchVideoByUrl(videoUrl, fetchType) |
171 | if (videoFromDatabase) { | 172 | if (videoFromDatabase) { |
172 | const refreshOptions = { | ||
173 | video: videoFromDatabase, | ||
174 | fetchedType: fetchType, | ||
175 | syncParam | ||
176 | } | ||
177 | 173 | ||
178 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | 174 | if (allowRefresh === true) { |
179 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) | 175 | const refreshOptions = { |
176 | video: videoFromDatabase, | ||
177 | fetchedType: fetchType, | ||
178 | syncParam | ||
179 | } | ||
180 | |||
181 | if (syncParam.refreshVideo === true) videoFromDatabase = await refreshVideoIfNeeded(refreshOptions) | ||
182 | else await JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'video', videoUrl: videoFromDatabase.url } }) | ||
183 | } | ||
180 | 184 | ||
181 | return { video: videoFromDatabase } | 185 | return { video: videoFromDatabase } |
182 | } | 186 | } |
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 9327792fb..074d4ad44 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts | |||
@@ -14,6 +14,7 @@ class Emailer { | |||
14 | private static instance: Emailer | 14 | private static instance: Emailer |
15 | private initialized = false | 15 | private initialized = false |
16 | private transporter: Transporter | 16 | private transporter: Transporter |
17 | private enabled = false | ||
17 | 18 | ||
18 | private constructor () {} | 19 | private constructor () {} |
19 | 20 | ||
@@ -50,6 +51,8 @@ class Emailer { | |||
50 | tls, | 51 | tls, |
51 | auth | 52 | auth |
52 | }) | 53 | }) |
54 | |||
55 | this.enabled = true | ||
53 | } else { | 56 | } else { |
54 | if (!isTestInstance()) { | 57 | if (!isTestInstance()) { |
55 | logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') | 58 | logger.error('Cannot use SMTP server because of lack of configuration. PeerTube will not be able to send mails!') |
@@ -57,6 +60,10 @@ class Emailer { | |||
57 | } | 60 | } |
58 | } | 61 | } |
59 | 62 | ||
63 | isEnabled () { | ||
64 | return this.enabled | ||
65 | } | ||
66 | |||
60 | async checkConnectionOrDie () { | 67 | async checkConnectionOrDie () { |
61 | if (!this.transporter) return | 68 | if (!this.transporter) return |
62 | 69 | ||
diff --git a/server/lib/job-queue/handlers/activitypub-refresher.ts b/server/lib/job-queue/handlers/activitypub-refresher.ts index 7752b3b40..671b0f487 100644 --- a/server/lib/job-queue/handlers/activitypub-refresher.ts +++ b/server/lib/job-queue/handlers/activitypub-refresher.ts | |||
@@ -10,7 +10,8 @@ export type RefreshPayload = { | |||
10 | 10 | ||
11 | async function refreshAPObject (job: Bull.Job) { | 11 | async function refreshAPObject (job: Bull.Job) { |
12 | const payload = job.data as RefreshPayload | 12 | const payload = job.data as RefreshPayload |
13 | logger.info('Processing AP refresher in job %d.', job.id) | 13 | |
14 | logger.info('Processing AP refresher in job %d for video %s.', job.id, payload.videoUrl) | ||
14 | 15 | ||
15 | if (payload.type === 'video') return refreshAPVideo(payload.videoUrl) | 16 | if (payload.type === 'video') return refreshAPVideo(payload.videoUrl) |
16 | } | 17 | } |
diff --git a/server/lib/job-queue/handlers/video-file.ts b/server/lib/job-queue/handlers/video-file.ts index adc0a2a15..ddbf6d1c2 100644 --- a/server/lib/job-queue/handlers/video-file.ts +++ b/server/lib/job-queue/handlers/video-file.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { VideoResolution, VideoState } from '../../../../shared' | 2 | import { VideoResolution, VideoState, Job } from '../../../../shared' |
3 | import { logger } from '../../../helpers/logger' | 3 | import { logger } from '../../../helpers/logger' |
4 | import { VideoModel } from '../../../models/video/video' | 4 | import { VideoModel } from '../../../models/video/video' |
5 | import { JobQueue } from '../job-queue' | 5 | import { JobQueue } from '../job-queue' |
@@ -111,7 +111,7 @@ async function onVideoFileOptimizerSuccess (video: VideoModel, isNewVideo: boole | |||
111 | ) | 111 | ) |
112 | 112 | ||
113 | if (resolutionsEnabled.length !== 0) { | 113 | if (resolutionsEnabled.length !== 0) { |
114 | const tasks: Bluebird<any>[] = [] | 114 | const tasks: Bluebird<Bull.Job<any>>[] = [] |
115 | 115 | ||
116 | for (const resolution of resolutionsEnabled) { | 116 | for (const resolution of resolutionsEnabled) { |
117 | const dataInput = { | 117 | const dataInput = { |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 4de901c0c..51a0b5faf 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -7,7 +7,7 @@ import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } fro | |||
7 | import { extname, join } from 'path' | 7 | import { extname, join } from 'path' |
8 | import { VideoFileModel } from '../../../models/video/video-file' | 8 | import { VideoFileModel } from '../../../models/video/video-file' |
9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' | 9 | import { CONFIG, PREVIEWS_SIZE, sequelizeTypescript, THUMBNAILS_SIZE, VIDEO_IMPORT_TIMEOUT } from '../../../initializers' |
10 | import { doRequestAndSaveToFile, downloadImage } from '../../../helpers/requests' | 10 | import { downloadImage } from '../../../helpers/requests' |
11 | import { VideoState } from '../../../../shared' | 11 | import { VideoState } from '../../../../shared' |
12 | import { JobQueue } from '../index' | 12 | import { JobQueue } from '../index' |
13 | import { federateVideoIfNeeded } from '../../activitypub' | 13 | import { federateVideoIfNeeded } from '../../activitypub' |
@@ -109,6 +109,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
109 | let tempVideoPath: string | 109 | let tempVideoPath: string |
110 | let videoDestFile: string | 110 | let videoDestFile: string |
111 | let videoFile: VideoFileModel | 111 | let videoFile: VideoFileModel |
112 | |||
112 | try { | 113 | try { |
113 | // Download video from youtubeDL | 114 | // Download video from youtubeDL |
114 | tempVideoPath = await downloader() | 115 | tempVideoPath = await downloader() |
@@ -144,8 +145,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
144 | // Process thumbnail | 145 | // Process thumbnail |
145 | if (options.downloadThumbnail) { | 146 | if (options.downloadThumbnail) { |
146 | if (options.thumbnailUrl) { | 147 | if (options.thumbnailUrl) { |
147 | const destThumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName()) | 148 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.THUMBNAILS_DIR, videoImport.Video.getThumbnailName(), THUMBNAILS_SIZE) |
148 | await downloadImage(options.thumbnailUrl, destThumbnailPath, THUMBNAILS_SIZE) | ||
149 | } else { | 149 | } else { |
150 | await videoImport.Video.createThumbnail(videoFile) | 150 | await videoImport.Video.createThumbnail(videoFile) |
151 | } | 151 | } |
@@ -156,8 +156,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: Vide | |||
156 | // Process preview | 156 | // Process preview |
157 | if (options.downloadPreview) { | 157 | if (options.downloadPreview) { |
158 | if (options.thumbnailUrl) { | 158 | if (options.thumbnailUrl) { |
159 | const destPreviewPath = join(CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName()) | 159 | await downloadImage(options.thumbnailUrl, CONFIG.STORAGE.PREVIEWS_DIR, videoImport.Video.getPreviewName(), PREVIEWS_SIZE) |
160 | await downloadImage(options.thumbnailUrl, destPreviewPath, PREVIEWS_SIZE) | ||
161 | } else { | 160 | } else { |
162 | await videoImport.Video.createPreview(videoFile) | 161 | await videoImport.Video.createPreview(videoFile) |
163 | } | 162 | } |
diff --git a/server/lib/job-queue/handlers/video-views.ts b/server/lib/job-queue/handlers/video-views.ts index 038ef43e2..fa1fd13b3 100644 --- a/server/lib/job-queue/handlers/video-views.ts +++ b/server/lib/job-queue/handlers/video-views.ts | |||
@@ -23,9 +23,7 @@ async function processVideosViews () { | |||
23 | for (const videoId of videoIds) { | 23 | for (const videoId of videoIds) { |
24 | try { | 24 | try { |
25 | const views = await Redis.Instance.getVideoViews(videoId, hour) | 25 | const views = await Redis.Instance.getVideoViews(videoId, hour) |
26 | if (isNaN(views)) { | 26 | if (views) { |
27 | logger.error('Cannot process videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, views) | ||
28 | } else { | ||
29 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) | 27 | logger.debug('Adding %d views to video %d in hour %d.', views, videoId, hour) |
30 | 28 | ||
31 | try { | 29 | try { |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index abd75d512..3e25e6a2c 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -121,7 +121,14 @@ class Redis { | |||
121 | const key = this.generateVideoViewKey(videoId, hour) | 121 | const key = this.generateVideoViewKey(videoId, hour) |
122 | 122 | ||
123 | const valueString = await this.getValue(key) | 123 | const valueString = await this.getValue(key) |
124 | return parseInt(valueString, 10) | 124 | const valueInt = parseInt(valueString, 10) |
125 | |||
126 | if (isNaN(valueInt)) { | ||
127 | logger.error('Cannot get videos views of video %d in hour %d: views number is NaN (%s).', videoId, hour, valueString) | ||
128 | return undefined | ||
129 | } | ||
130 | |||
131 | return valueInt | ||
125 | } | 132 | } |
126 | 133 | ||
127 | async getVideosIdViewed (hour: number) { | 134 | async getVideosIdViewed (hour: number) { |
diff --git a/server/lib/schedulers/videos-redundancy-scheduler.ts b/server/lib/schedulers/videos-redundancy-scheduler.ts index 8b7f33539..2a99a665d 100644 --- a/server/lib/schedulers/videos-redundancy-scheduler.ts +++ b/server/lib/schedulers/videos-redundancy-scheduler.ts | |||
@@ -145,13 +145,13 @@ export class VideosRedundancyScheduler extends AbstractScheduler { | |||
145 | 145 | ||
146 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) | 146 | const tmpPath = await downloadWebTorrentVideo({ magnetUri }, VIDEO_IMPORT_TIMEOUT) |
147 | 147 | ||
148 | const destPath = join(CONFIG.STORAGE.VIDEOS_DIR, video.getVideoFilename(file)) | 148 | const destPath = join(CONFIG.STORAGE.REDUNDANCY_DIR, video.getVideoFilename(file)) |
149 | await rename(tmpPath, destPath) | 149 | await rename(tmpPath, destPath) |
150 | 150 | ||
151 | const createdModel = await VideoRedundancyModel.create({ | 151 | const createdModel = await VideoRedundancyModel.create({ |
152 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), | 152 | expiresOn: this.buildNewExpiration(redundancy.minLifetime), |
153 | url: getVideoCacheFileActivityPubUrl(file), | 153 | url: getVideoCacheFileActivityPubUrl(file), |
154 | fileUrl: video.getVideoFileUrl(file, CONFIG.WEBSERVER.URL), | 154 | fileUrl: video.getVideoRedundancyUrl(file, CONFIG.WEBSERVER.URL), |
155 | strategy: redundancy.strategy, | 155 | strategy: redundancy.strategy, |
156 | videoFileId: file.id, | 156 | videoFileId: file.id, |
157 | actorId: serverActor.id | 157 | actorId: serverActor.id |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 5a237d733..a99e9b1ad 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -241,6 +241,27 @@ export class AccountModel extends Model<AccountModel> { | |||
241 | }) | 241 | }) |
242 | } | 242 | } |
243 | 243 | ||
244 | static listLocalsForSitemap (sort: string) { | ||
245 | const query = { | ||
246 | attributes: [ ], | ||
247 | offset: 0, | ||
248 | order: getSort(sort), | ||
249 | include: [ | ||
250 | { | ||
251 | attributes: [ 'preferredUsername', 'serverId' ], | ||
252 | model: ActorModel.unscoped(), | ||
253 | where: { | ||
254 | serverId: null | ||
255 | } | ||
256 | } | ||
257 | ] | ||
258 | } | ||
259 | |||
260 | return AccountModel | ||
261 | .unscoped() | ||
262 | .findAll(query) | ||
263 | } | ||
264 | |||
244 | toFormattedJSON (): Account { | 265 | toFormattedJSON (): Account { |
245 | const actor = this.Actor.toFormattedJSON() | 266 | const actor = this.Actor.toFormattedJSON() |
246 | const account = { | 267 | const account = { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 9de4356b4..dd37dad22 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -15,7 +15,7 @@ import { | |||
15 | import { ActorModel } from '../activitypub/actor' | 15 | import { ActorModel } from '../activitypub/actor' |
16 | import { getVideoSort, throwIfNotValid } from '../utils' | 16 | import { getVideoSort, throwIfNotValid } from '../utils' |
17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 17 | import { isActivityPubUrlValid, isUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
18 | import { CONFIG, CONSTRAINTS_FIELDS, VIDEO_EXT_MIMETYPE } from '../../initializers' | 18 | import { CONFIG, CONSTRAINTS_FIELDS, STATIC_PATHS, VIDEO_EXT_MIMETYPE } from '../../initializers' |
19 | import { VideoFileModel } from '../video/video-file' | 19 | import { VideoFileModel } from '../video/video-file' |
20 | import { getServerActor } from '../../helpers/utils' | 20 | import { getServerActor } from '../../helpers/utils' |
21 | import { VideoModel } from '../video/video' | 21 | import { VideoModel } from '../video/video' |
@@ -124,7 +124,7 @@ export class VideoRedundancyModel extends Model<VideoRedundancyModel> { | |||
124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` | 124 | const logIdentifier = `${videoFile.Video.uuid}-${videoFile.resolution}` |
125 | logger.info('Removing duplicated video file %s.', logIdentifier) | 125 | logger.info('Removing duplicated video file %s.', logIdentifier) |
126 | 126 | ||
127 | videoFile.Video.removeFile(videoFile) | 127 | videoFile.Video.removeFile(videoFile, true) |
128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) | 128 | .catch(err => logger.error('Cannot delete %s files.', logIdentifier, { err })) |
129 | 129 | ||
130 | return undefined | 130 | return undefined |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index f4586917e..86bf0461a 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -233,6 +233,27 @@ export class VideoChannelModel extends Model<VideoChannelModel> { | |||
233 | }) | 233 | }) |
234 | } | 234 | } |
235 | 235 | ||
236 | static listLocalsForSitemap (sort: string) { | ||
237 | const query = { | ||
238 | attributes: [ ], | ||
239 | offset: 0, | ||
240 | order: getSort(sort), | ||
241 | include: [ | ||
242 | { | ||
243 | attributes: [ 'preferredUsername', 'serverId' ], | ||
244 | model: ActorModel.unscoped(), | ||
245 | where: { | ||
246 | serverId: null | ||
247 | } | ||
248 | } | ||
249 | ] | ||
250 | } | ||
251 | |||
252 | return VideoChannelModel | ||
253 | .unscoped() | ||
254 | .findAll(query) | ||
255 | } | ||
256 | |||
236 | static searchForApi (options: { | 257 | static searchForApi (options: { |
237 | actorId: number | 258 | actorId: number |
238 | search: string | 259 | search: string |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0f18d9f0c..adef37937 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -140,7 +140,7 @@ type ForAPIOptions = { | |||
140 | 140 | ||
141 | type AvailableForListIDsOptions = { | 141 | type AvailableForListIDsOptions = { |
142 | serverAccountId: number | 142 | serverAccountId: number |
143 | actorId: number | 143 | followerActorId: number |
144 | includeLocalVideos: boolean | 144 | includeLocalVideos: boolean |
145 | filter?: VideoFilter | 145 | filter?: VideoFilter |
146 | categoryOneOf?: number[] | 146 | categoryOneOf?: number[] |
@@ -315,7 +315,7 @@ type AvailableForListIDsOptions = { | |||
315 | query.include.push(videoChannelInclude) | 315 | query.include.push(videoChannelInclude) |
316 | } | 316 | } |
317 | 317 | ||
318 | if (options.actorId) { | 318 | if (options.followerActorId) { |
319 | let localVideosReq = '' | 319 | let localVideosReq = '' |
320 | if (options.includeLocalVideos === true) { | 320 | if (options.includeLocalVideos === true) { |
321 | localVideosReq = ' UNION ALL ' + | 321 | localVideosReq = ' UNION ALL ' + |
@@ -327,7 +327,7 @@ type AvailableForListIDsOptions = { | |||
327 | } | 327 | } |
328 | 328 | ||
329 | // Force actorId to be a number to avoid SQL injections | 329 | // Force actorId to be a number to avoid SQL injections |
330 | const actorIdNumber = parseInt(options.actorId.toString(), 10) | 330 | const actorIdNumber = parseInt(options.followerActorId.toString(), 10) |
331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ | 331 | query.where[ 'id' ][ Sequelize.Op.and ].push({ |
332 | [ Sequelize.Op.in ]: Sequelize.literal( | 332 | [ Sequelize.Op.in ]: Sequelize.literal( |
333 | '(' + | 333 | '(' + |
@@ -985,7 +985,7 @@ export class VideoModel extends Model<VideoModel> { | |||
985 | filter?: VideoFilter, | 985 | filter?: VideoFilter, |
986 | accountId?: number, | 986 | accountId?: number, |
987 | videoChannelId?: number, | 987 | videoChannelId?: number, |
988 | actorId?: number | 988 | followerActorId?: number |
989 | trendingDays?: number, | 989 | trendingDays?: number, |
990 | user?: UserModel | 990 | user?: UserModel |
991 | }, countVideos = true) { | 991 | }, countVideos = true) { |
@@ -1008,11 +1008,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1008 | 1008 | ||
1009 | const serverActor = await getServerActor() | 1009 | const serverActor = await getServerActor() |
1010 | 1010 | ||
1011 | // actorId === null has a meaning, so just check undefined | 1011 | // followerActorId === null has a meaning, so just check undefined |
1012 | const actorId = options.actorId !== undefined ? options.actorId : serverActor.id | 1012 | const followerActorId = options.followerActorId !== undefined ? options.followerActorId : serverActor.id |
1013 | 1013 | ||
1014 | const queryOptions = { | 1014 | const queryOptions = { |
1015 | actorId, | 1015 | followerActorId, |
1016 | serverAccountId: serverActor.Account.id, | 1016 | serverAccountId: serverActor.Account.id, |
1017 | nsfw: options.nsfw, | 1017 | nsfw: options.nsfw, |
1018 | categoryOneOf: options.categoryOneOf, | 1018 | categoryOneOf: options.categoryOneOf, |
@@ -1118,7 +1118,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1118 | 1118 | ||
1119 | const serverActor = await getServerActor() | 1119 | const serverActor = await getServerActor() |
1120 | const queryOptions = { | 1120 | const queryOptions = { |
1121 | actorId: serverActor.id, | 1121 | followerActorId: serverActor.id, |
1122 | serverAccountId: serverActor.Account.id, | 1122 | serverAccountId: serverActor.Account.id, |
1123 | includeLocalVideos: options.includeLocalVideos, | 1123 | includeLocalVideos: options.includeLocalVideos, |
1124 | nsfw: options.nsfw, | 1124 | nsfw: options.nsfw, |
@@ -1273,11 +1273,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1273 | // threshold corresponds to how many video the field should have to be returned | 1273 | // threshold corresponds to how many video the field should have to be returned |
1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { | 1274 | static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { |
1275 | const serverActor = await getServerActor() | 1275 | const serverActor = await getServerActor() |
1276 | const actorId = serverActor.id | 1276 | const followerActorId = serverActor.id |
1277 | 1277 | ||
1278 | const scopeOptions: AvailableForListIDsOptions = { | 1278 | const scopeOptions: AvailableForListIDsOptions = { |
1279 | serverAccountId: serverActor.Account.id, | 1279 | serverAccountId: serverActor.Account.id, |
1280 | actorId, | 1280 | followerActorId, |
1281 | includeLocalVideos: true | 1281 | includeLocalVideos: true |
1282 | } | 1282 | } |
1283 | 1283 | ||
@@ -1538,8 +1538,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) | 1538 | .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err })) |
1539 | } | 1539 | } |
1540 | 1540 | ||
1541 | removeFile (videoFile: VideoFileModel) { | 1541 | removeFile (videoFile: VideoFileModel, isRedundancy = false) { |
1542 | const filePath = join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 1542 | const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR |
1543 | |||
1544 | const filePath = join(baseDir, this.getVideoFilename(videoFile)) | ||
1543 | return remove(filePath) | 1545 | return remove(filePath) |
1544 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) | 1546 | .catch(err => logger.warn('Cannot delete file %s.', filePath, { err })) |
1545 | } | 1547 | } |
@@ -1617,6 +1619,10 @@ export class VideoModel extends Model<VideoModel> { | |||
1617 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) | 1619 | return baseUrlHttp + STATIC_PATHS.WEBSEED + this.getVideoFilename(videoFile) |
1618 | } | 1620 | } |
1619 | 1621 | ||
1622 | getVideoRedundancyUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | ||
1623 | return baseUrlHttp + STATIC_PATHS.REDUNDANCY + this.getVideoFilename(videoFile) | ||
1624 | } | ||
1625 | |||
1620 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { | 1626 | getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { |
1621 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) | 1627 | return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) |
1622 | } | 1628 | } |
diff --git a/server/tests/api/redundancy/redundancy.ts b/server/tests/api/redundancy/redundancy.ts index a8a2f305f..5b29a503a 100644 --- a/server/tests/api/redundancy/redundancy.ts +++ b/server/tests/api/redundancy/redundancy.ts | |||
@@ -136,7 +136,7 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
136 | if (!videoUUID) videoUUID = video1Server2UUID | 136 | if (!videoUUID) videoUUID = video1Server2UUID |
137 | 137 | ||
138 | const webseeds = [ | 138 | const webseeds = [ |
139 | 'http://localhost:9001/static/webseed/' + videoUUID, | 139 | 'http://localhost:9001/static/redundancy/' + videoUUID, |
140 | 'http://localhost:9002/static/webseed/' + videoUUID | 140 | 'http://localhost:9002/static/webseed/' + videoUUID |
141 | ] | 141 | ] |
142 | 142 | ||
@@ -148,20 +148,23 @@ async function check2Webseeds (strategy: VideoRedundancyStrategy, videoUUID?: st | |||
148 | for (const file of video.files) { | 148 | for (const file of video.files) { |
149 | checkMagnetWebseeds(file, webseeds, server) | 149 | checkMagnetWebseeds(file, webseeds, server) |
150 | 150 | ||
151 | // Only servers 1 and 2 have the video | 151 | await makeGetRequest({ |
152 | if (server.serverNumber !== 3) { | 152 | url: servers[0].url, |
153 | await makeGetRequest({ | 153 | statusCodeExpected: 200, |
154 | url: server.url, | 154 | path: '/static/redundancy/' + `${videoUUID}-${file.resolution.id}.mp4`, |
155 | statusCodeExpected: 200, | 155 | contentType: null |
156 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | 156 | }) |
157 | contentType: null | 157 | await makeGetRequest({ |
158 | }) | 158 | url: servers[1].url, |
159 | } | 159 | statusCodeExpected: 200, |
160 | path: '/static/webseed/' + `${videoUUID}-${file.resolution.id}.mp4`, | ||
161 | contentType: null | ||
162 | }) | ||
160 | } | 163 | } |
161 | } | 164 | } |
162 | 165 | ||
163 | for (const directory of [ 'test1', 'test2' ]) { | 166 | for (const directory of [ 'test1/redundancy', 'test2/videos' ]) { |
164 | const files = await readdir(join(root(), directory, 'videos')) | 167 | const files = await readdir(join(root(), directory)) |
165 | expect(files).to.have.length.at.least(4) | 168 | expect(files).to.have.length.at.least(4) |
166 | 169 | ||
167 | for (const resolution of [ 240, 360, 480, 720 ]) { | 170 | for (const resolution of [ 240, 360, 480, 720 ]) { |
diff --git a/server/tests/cli/index.ts b/server/tests/cli/index.ts index 6201314ce..c6b7ec078 100644 --- a/server/tests/cli/index.ts +++ b/server/tests/cli/index.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | // Order of the tests we want to execute | 1 | // Order of the tests we want to execute |
2 | import './create-import-video-file-job' | 2 | import './create-import-video-file-job' |
3 | import './create-transcoding-job' | 3 | import './create-transcoding-job' |
4 | import './optimize-old-videos' | ||
4 | import './peertube' | 5 | import './peertube' |
5 | import './reset-password' | 6 | import './reset-password' |
6 | import './update-host' | 7 | import './update-host' |
diff --git a/server/tests/misc-endpoints.ts b/server/tests/misc-endpoints.ts index 8fab20971..b53803ee1 100644 --- a/server/tests/misc-endpoints.ts +++ b/server/tests/misc-endpoints.ts | |||
@@ -2,7 +2,18 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { flushTests, killallServers, makeGetRequest, runServer, ServerInfo } from './utils' | 5 | import { |
6 | addVideoChannel, | ||
7 | createUser, | ||
8 | flushTests, | ||
9 | killallServers, | ||
10 | makeGetRequest, | ||
11 | runServer, | ||
12 | ServerInfo, | ||
13 | setAccessTokensToServers, | ||
14 | uploadVideo | ||
15 | } from './utils' | ||
16 | import { VideoPrivacy } from '../../shared/models/videos' | ||
6 | 17 | ||
7 | const expect = chai.expect | 18 | const expect = chai.expect |
8 | 19 | ||
@@ -15,6 +26,7 @@ describe('Test misc endpoints', function () { | |||
15 | await flushTests() | 26 | await flushTests() |
16 | 27 | ||
17 | server = await runServer(1) | 28 | server = await runServer(1) |
29 | await setAccessTokensToServers([ server ]) | ||
18 | }) | 30 | }) |
19 | 31 | ||
20 | describe('Test a well known endpoints', function () { | 32 | describe('Test a well known endpoints', function () { |
@@ -93,6 +105,64 @@ describe('Test misc endpoints', function () { | |||
93 | }) | 105 | }) |
94 | }) | 106 | }) |
95 | 107 | ||
108 | describe('Test bots endpoints', function () { | ||
109 | |||
110 | it('Should get the empty sitemap', async function () { | ||
111 | const res = await makeGetRequest({ | ||
112 | url: server.url, | ||
113 | path: '/sitemap.xml', | ||
114 | statusCodeExpected: 200 | ||
115 | }) | ||
116 | |||
117 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
118 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
119 | }) | ||
120 | |||
121 | it('Should get the empty cached sitemap', async function () { | ||
122 | const res = await makeGetRequest({ | ||
123 | url: server.url, | ||
124 | path: '/sitemap.xml', | ||
125 | statusCodeExpected: 200 | ||
126 | }) | ||
127 | |||
128 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
129 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
130 | }) | ||
131 | |||
132 | it('Should add videos, channel and accounts and get sitemap', async function () { | ||
133 | this.timeout(35000) | ||
134 | |||
135 | await uploadVideo(server.url, server.accessToken, { name: 'video 1', nsfw: false }) | ||
136 | await uploadVideo(server.url, server.accessToken, { name: 'video 2', nsfw: false }) | ||
137 | await uploadVideo(server.url, server.accessToken, { name: 'video 3', privacy: VideoPrivacy.PRIVATE }) | ||
138 | |||
139 | await addVideoChannel(server.url, server.accessToken, { name: 'channel1', displayName: 'channel 1' }) | ||
140 | await addVideoChannel(server.url, server.accessToken, { name: 'channel2', displayName: 'channel 2' }) | ||
141 | |||
142 | await createUser(server.url, server.accessToken, 'user1', 'password') | ||
143 | await createUser(server.url, server.accessToken, 'user2', 'password') | ||
144 | |||
145 | const res = await makeGetRequest({ | ||
146 | url: server.url, | ||
147 | path: '/sitemap.xml?t=1', // avoid using cache | ||
148 | statusCodeExpected: 200 | ||
149 | }) | ||
150 | |||
151 | expect(res.text).to.contain('xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"') | ||
152 | expect(res.text).to.contain('<url><loc>http://localhost:9001/about/instance</loc></url>') | ||
153 | |||
154 | expect(res.text).to.contain('<video:title><![CDATA[video 1]]></video:title>') | ||
155 | expect(res.text).to.contain('<video:title><![CDATA[video 2]]></video:title>') | ||
156 | expect(res.text).to.not.contain('<video:title><![CDATA[video 3]]></video:title>') | ||
157 | |||
158 | expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel1</loc></url>') | ||
159 | expect(res.text).to.contain('<url><loc>http://localhost:9001/video-channels/channel2</loc></url>') | ||
160 | |||
161 | expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user1</loc></url>') | ||
162 | expect(res.text).to.contain('<url><loc>http://localhost:9001/accounts/user2</loc></url>') | ||
163 | }) | ||
164 | }) | ||
165 | |||
96 | after(async function () { | 166 | after(async function () { |
97 | killallServers([ server ]) | 167 | killallServers([ server ]) |
98 | }) | 168 | }) |