ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

constants.ts 44 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563
  1. import { maxBy, minBy, randomInt } from '@peertube/peertube-core-utils'
  2. import {
  3. AbuseState,
  4. AbuseStateType,
  5. ActivityPubActorType,
  6. ActorImageType,
  7. ActorImageType_Type,
  8. FollowState,
  9. JobType,
  10. NSFWPolicyType,
  11. RunnerJobState,
  12. RunnerJobStateType,
  13. UserExportState,
  14. UserExportStateType,
  15. UserImportState,
  16. UserImportStateType,
  17. UserRegistrationState,
  18. UserRegistrationStateType,
  19. VideoChannelSyncState,
  20. VideoChannelSyncStateType,
  21. VideoCommentPolicy,
  22. VideoCommentPolicyType,
  23. VideoImportState,
  24. VideoImportStateType,
  25. VideoPlaylistPrivacy,
  26. VideoPlaylistPrivacyType,
  27. VideoPlaylistType,
  28. VideoPlaylistType_Type,
  29. VideoPrivacy,
  30. VideoPrivacyType,
  31. VideoRateType,
  32. VideoResolution,
  33. VideoState,
  34. VideoStateType,
  35. VideoTranscodingFPS
  36. } from '@peertube/peertube-models'
  37. import { isTestInstance, isTestOrDevInstance, root } from '@peertube/peertube-node-utils'
  38. import { RepeatOptions } from 'bullmq'
  39. import { Encoding, randomBytes } from 'crypto'
  40. import { readJsonSync } from 'fs-extra/esm'
  41. import invert from 'lodash-es/invert.js'
  42. import { join } from 'path'
  43. // Do not use barrels, remain constants as independent as possible
  44. import { parseDurationToMs, sanitizeHost, sanitizeUrl } from '../helpers/core-utils.js'
  45. import { CONFIG, registerConfigChangedHandler } from './config.js'
  46. import { cpus } from 'os'
  47. // ---------------------------------------------------------------------------
  48. const LAST_MIGRATION_VERSION = 860
  49. // ---------------------------------------------------------------------------
  50. const API_VERSION = 'v1'
  51. const PEERTUBE_VERSION: string = readJsonSync(join(root(), 'package.json')).version
  52. const PAGINATION = {
  53. GLOBAL: {
  54. COUNT: {
  55. DEFAULT: 15,
  56. MAX: 100
  57. }
  58. },
  59. OUTBOX: {
  60. COUNT: {
  61. MAX: 50
  62. }
  63. }
  64. }
  65. const WEBSERVER = {
  66. URL: '',
  67. HOST: '',
  68. SCHEME: '',
  69. WS: '',
  70. HOSTNAME: '',
  71. PORT: 0,
  72. RTMP_URL: '',
  73. RTMPS_URL: '',
  74. RTMP_BASE_LIVE_URL: '',
  75. RTMPS_BASE_LIVE_URL: ''
  76. }
  77. // Sortable columns per schema
  78. const SORTABLE_COLUMNS = {
  79. ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
  80. USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
  81. ACCOUNTS: [ 'createdAt' ],
  82. JOBS: [ 'createdAt' ],
  83. VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
  84. VIDEO_IMPORTS: [ 'createdAt' ],
  85. VIDEO_CHANNEL_SYNCS: [ 'externalChannelUrl', 'videoChannel', 'createdAt', 'lastSyncAt', 'state' ],
  86. VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
  87. VIDEO_COMMENTS: [ 'createdAt' ],
  88. VIDEO_PASSWORDS: [ 'createdAt' ],
  89. VIDEO_RATES: [ 'createdAt' ],
  90. BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
  91. INSTANCE_FOLLOWERS: [ 'createdAt', 'state', 'score' ],
  92. INSTANCE_FOLLOWING: [ 'createdAt', 'redundancyAllowed', 'state' ],
  93. ACCOUNT_FOLLOWERS: [ 'createdAt' ],
  94. CHANNEL_FOLLOWERS: [ 'createdAt' ],
  95. USER_REGISTRATIONS: [ 'createdAt', 'state' ],
  96. RUNNERS: [ 'createdAt' ],
  97. RUNNER_REGISTRATION_TOKENS: [ 'createdAt' ],
  98. RUNNER_JOBS: [ 'updatedAt', 'createdAt', 'priority', 'state', 'progress' ],
  99. VIDEOS: [
  100. 'name',
  101. 'duration',
  102. 'createdAt',
  103. 'publishedAt',
  104. 'originallyPublishedAt',
  105. 'views',
  106. 'likes',
  107. 'trending',
  108. 'hot',
  109. 'best',
  110. 'localVideoFilesSize'
  111. ],
  112. // Don't forget to update peertube-search-index with the same values
  113. VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
  114. VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
  115. VIDEO_PLAYLISTS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
  116. ABUSES: [ 'id', 'createdAt', 'state' ],
  117. ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
  118. SERVERS_BLOCKLIST: [ 'createdAt' ],
  119. WATCHED_WORDS_LISTS: [ 'createdAt', 'updatedAt', 'listName' ],
  120. USER_NOTIFICATIONS: [ 'createdAt', 'read' ],
  121. VIDEO_PLAYLISTS: [ 'name', 'displayName', 'createdAt', 'updatedAt' ],
  122. PLUGINS: [ 'name', 'createdAt', 'updatedAt' ],
  123. AVAILABLE_PLUGINS: [ 'npmName', 'popularity', 'trending' ],
  124. VIDEO_REDUNDANCIES: [ 'name' ]
  125. }
  126. const ROUTE_CACHE_LIFETIME = {
  127. FEEDS: '15 minutes',
  128. ROBOTS: '2 hours',
  129. SITEMAP: '1 day',
  130. SECURITYTXT: '2 hours',
  131. NODEINFO: '10 minutes',
  132. DNT_POLICY: '1 week',
  133. ACTIVITY_PUB: {
  134. VIDEOS: '1 second' // 1 second, cache concurrent requests after a broadcast for example
  135. },
  136. STATS: '4 hours',
  137. WELL_KNOWN: '1 day'
  138. }
  139. // ---------------------------------------------------------------------------
  140. // Number of points we add/remove after a successful/bad request
  141. const ACTOR_FOLLOW_SCORE = {
  142. PENALTY: -10,
  143. BONUS: 10,
  144. BASE: 1000,
  145. MAX: 10000
  146. }
  147. const FOLLOW_STATES: { [ id: string ]: FollowState } = {
  148. PENDING: 'pending',
  149. ACCEPTED: 'accepted',
  150. REJECTED: 'rejected'
  151. }
  152. const REMOTE_SCHEME = {
  153. HTTP: 'https',
  154. WS: 'wss'
  155. }
  156. // ---------------------------------------------------------------------------
  157. const JOB_ATTEMPTS: { [id in JobType]: number } = {
  158. 'activitypub-http-broadcast': 1,
  159. 'activitypub-http-broadcast-parallel': 1,
  160. 'activitypub-http-unicast': 1,
  161. 'activitypub-http-fetcher': 2,
  162. 'activitypub-follow': 5,
  163. 'activitypub-cleaner': 1,
  164. 'video-file-import': 1,
  165. 'video-transcoding': 1,
  166. 'video-import': 1,
  167. 'email': 5,
  168. 'actor-keys': 3,
  169. 'videos-views-stats': 1,
  170. 'activitypub-refresher': 1,
  171. 'video-redundancy': 1,
  172. 'video-live-ending': 1,
  173. 'video-studio-edition': 1,
  174. 'manage-video-torrent': 1,
  175. 'video-channel-import': 1,
  176. 'after-video-channel-import': 1,
  177. 'move-to-object-storage': 3,
  178. 'move-to-file-system': 3,
  179. 'transcoding-job-builder': 1,
  180. 'generate-video-storyboard': 1,
  181. 'notify': 1,
  182. 'federate-video': 1,
  183. 'create-user-export': 1,
  184. 'import-user-archive': 1,
  185. 'video-transcription': 1
  186. }
  187. // Excluded keys are jobs that can be configured by admins
  188. const JOB_CONCURRENCY: { [id in Exclude<JobType, 'video-transcoding' | 'video-import'>]: number } = {
  189. 'activitypub-http-broadcast': 1,
  190. 'activitypub-http-broadcast-parallel': 30,
  191. 'activitypub-http-unicast': 30,
  192. 'activitypub-http-fetcher': 3,
  193. 'activitypub-cleaner': 1,
  194. 'activitypub-follow': 1,
  195. 'video-file-import': 1,
  196. 'email': 5,
  197. 'actor-keys': 1,
  198. 'videos-views-stats': 1,
  199. 'activitypub-refresher': 1,
  200. 'video-redundancy': 1,
  201. 'video-live-ending': 10,
  202. 'video-studio-edition': 1,
  203. 'manage-video-torrent': 1,
  204. 'move-to-object-storage': 1,
  205. 'move-to-file-system': 1,
  206. 'video-channel-import': 1,
  207. 'after-video-channel-import': 1,
  208. 'transcoding-job-builder': 1,
  209. 'generate-video-storyboard': 1,
  210. 'notify': 5,
  211. 'federate-video': 3,
  212. 'create-user-export': 1,
  213. 'import-user-archive': 1,
  214. 'video-transcription': 1
  215. }
  216. const JOB_TTL: { [id in JobType]: number } = {
  217. 'activitypub-http-broadcast': 60000 * 10, // 10 minutes
  218. 'activitypub-http-broadcast-parallel': 60000 * 10, // 10 minutes
  219. 'activitypub-http-unicast': 60000 * 10, // 10 minutes
  220. 'activitypub-http-fetcher': 1000 * 3600 * 10, // 10 hours
  221. 'activitypub-follow': 60000 * 10, // 10 minutes
  222. 'activitypub-cleaner': 1000 * 3600, // 1 hour
  223. 'video-file-import': 1000 * 3600, // 1 hour
  224. 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
  225. 'video-studio-edition': 1000 * 3600 * 10, // 10 hours
  226. 'video-import': CONFIG.IMPORT.VIDEOS.TIMEOUT,
  227. 'email': 60000 * 10, // 10 minutes
  228. 'actor-keys': 60000 * 20, // 20 minutes
  229. 'videos-views-stats': undefined, // Unlimited
  230. 'activitypub-refresher': 60000 * 10, // 10 minutes
  231. 'video-redundancy': 1000 * 3600 * 3, // 3 hours
  232. 'video-live-ending': 1000 * 60 * 10, // 10 minutes
  233. 'generate-video-storyboard': 1000 * 3600 * 6, // 6 hours
  234. 'manage-video-torrent': 1000 * 3600 * 3, // 3 hours
  235. 'move-to-object-storage': 1000 * 60 * 60 * 3, // 3 hours
  236. 'move-to-file-system': 1000 * 60 * 60 * 3, // 3 hours
  237. 'video-channel-import': 1000 * 60 * 60 * 4, // 4 hours
  238. 'after-video-channel-import': 60000 * 5, // 5 minutes
  239. 'transcoding-job-builder': 60000, // 1 minute
  240. 'notify': 60000 * 5, // 5 minutes
  241. 'federate-video': 60000 * 5, // 5 minutes,
  242. 'create-user-export': 60000 * 60 * 24, // 24 hours
  243. 'import-user-archive': 60000 * 60 * 24, // 24 hours
  244. 'video-transcription': 1000 * 3600 * 6 // 6 hours
  245. }
  246. const REPEAT_JOBS: { [ id in JobType ]?: RepeatOptions } = {
  247. 'videos-views-stats': {
  248. pattern: randomInt(1, 20) + ' * * * *' // Between 1-20 minutes past the hour
  249. },
  250. 'activitypub-cleaner': {
  251. pattern: '30 5 * * ' + randomInt(0, 7) // 1 time per week (random day) at 5:30 AM
  252. }
  253. }
  254. const JOB_PRIORITY = {
  255. TRANSCODING: 100,
  256. VIDEO_STUDIO: 150,
  257. TRANSCRIPTION: 200
  258. }
  259. const JOB_REMOVAL_OPTIONS = {
  260. COUNT: 10000, // Max jobs to store
  261. SUCCESS: { // Success jobs
  262. 'DEFAULT': parseDurationToMs('2 days'),
  263. 'activitypub-http-broadcast-parallel': parseDurationToMs('10 minutes'),
  264. 'activitypub-http-unicast': parseDurationToMs('1 hour'),
  265. 'videos-views-stats': parseDurationToMs('3 hours'),
  266. 'activitypub-refresher': parseDurationToMs('10 hours')
  267. },
  268. FAILURE: { // Failed job
  269. DEFAULT: parseDurationToMs('7 days')
  270. }
  271. }
  272. const VIDEO_IMPORT_TIMEOUT = Math.floor(JOB_TTL['video-import'] * 0.9)
  273. const RUNNER_JOBS = {
  274. MAX_FAILURES: 5,
  275. LAST_CONTACT_UPDATE_INTERVAL: 30000
  276. }
  277. // ---------------------------------------------------------------------------
  278. const BROADCAST_CONCURRENCY = 30 // How many requests in parallel we do in activitypub-http-broadcast job
  279. const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch remote data (likes, shares...)
  280. const AP_CLEANER = {
  281. CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
  282. UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
  283. PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
  284. }
  285. const REQUEST_TIMEOUTS = {
  286. DEFAULT: 7000, // 7 seconds
  287. FILE: 30000, // 30 seconds
  288. REDUNDANCY: JOB_TTL['video-redundancy']
  289. }
  290. const SCHEDULER_INTERVALS_MS = {
  291. RUNNER_JOB_WATCH_DOG: Math.min(CONFIG.REMOTE_RUNNERS.STALLED_JOBS.VOD, CONFIG.REMOTE_RUNNERS.STALLED_JOBS.LIVE),
  292. ACTOR_FOLLOW_SCORES: 60000 * 60, // 1 hour
  293. REMOVE_OLD_JOBS: 60000 * 60, // 1 hour
  294. UPDATE_VIDEOS: 60000, // 1 minute
  295. YOUTUBE_DL_UPDATE: 60000 * 60 * 24, // 1 day
  296. GEO_IP_UPDATE: 60000 * 60 * 24, // 1 day
  297. VIDEO_VIEWS_BUFFER_UPDATE: CONFIG.VIEWS.VIDEOS.LOCAL_BUFFER_UPDATE_INTERVAL,
  298. CHECK_PLUGINS: CONFIG.PLUGINS.INDEX.CHECK_LATEST_VERSIONS_INTERVAL,
  299. CHECK_PEERTUBE_VERSION: 60000 * 60 * 24, // 1 day
  300. AUTO_FOLLOW_INDEX_INSTANCES: 60000 * 60 * 24, // 1 day
  301. REMOVE_OLD_VIEWS: 60000 * 60 * 24, // 1 day
  302. REMOVE_OLD_HISTORY: 60000 * 60 * 24, // 1 day
  303. REMOVE_EXPIRED_USER_EXPORTS: 1000 * 3600, // 1 hour
  304. UPDATE_INBOX_STATS: 1000 * 60, // 1 minute
  305. REMOVE_DANGLING_RESUMABLE_UPLOADS: 60000 * 60, // 1 hour
  306. CHANNEL_SYNC_CHECK_INTERVAL: CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.CHECK_INTERVAL
  307. }
  308. // ---------------------------------------------------------------------------
  309. const CONSTRAINTS_FIELDS = {
  310. USERS: {
  311. NAME: { min: 1, max: 120 }, // Length
  312. DESCRIPTION: { min: 3, max: 1000 }, // Length
  313. USERNAME: { min: 1, max: 50 }, // Length
  314. PASSWORD: { min: 6, max: 255 }, // Length
  315. VIDEO_QUOTA: { min: -1 },
  316. VIDEO_QUOTA_DAILY: { min: -1 },
  317. VIDEO_LANGUAGES: { max: 500 }, // Array length
  318. BLOCKED_REASON: { min: 3, max: 250 } // Length
  319. },
  320. ABUSES: {
  321. REASON: { min: 2, max: 3000 }, // Length
  322. MODERATION_COMMENT: { min: 2, max: 3000 } // Length
  323. },
  324. ABUSE_MESSAGES: {
  325. MESSAGE: { min: 2, max: 3000 } // Length
  326. },
  327. USER_REGISTRATIONS: {
  328. REASON_MESSAGE: { min: 2, max: 3000 }, // Length
  329. MODERATOR_MESSAGE: { min: 2, max: 3000 } // Length
  330. },
  331. VIDEO_BLACKLIST: {
  332. REASON: { min: 2, max: 300 } // Length
  333. },
  334. VIDEO_CHANNELS: {
  335. NAME: { min: 1, max: 120 }, // Length
  336. DESCRIPTION: { min: 3, max: 1000 }, // Length
  337. SUPPORT: { min: 3, max: 1000 }, // Length
  338. EXTERNAL_CHANNEL_URL: { min: 3, max: 2000 }, // Length
  339. URL: { min: 3, max: 2000 } // Length
  340. },
  341. VIDEO_CHANNEL_SYNCS: {
  342. EXTERNAL_CHANNEL_URL: { min: 3, max: 2000 } // Length
  343. },
  344. VIDEO_CAPTIONS: {
  345. CAPTION_FILE: {
  346. EXTNAME: [ '.vtt', '.srt' ],
  347. FILE_SIZE: {
  348. max: 20 * 1024 * 1024 // 20MB
  349. }
  350. }
  351. },
  352. VIDEO_IMPORTS: {
  353. URL: { min: 3, max: 2000 }, // Length
  354. TORRENT_NAME: { min: 3, max: 255 }, // Length
  355. TORRENT_FILE: {
  356. EXTNAME: [ '.torrent' ],
  357. FILE_SIZE: {
  358. max: 1024 * 200 // 200 KB
  359. }
  360. }
  361. },
  362. VIDEOS_REDUNDANCY: {
  363. URL: { min: 3, max: 2000 } // Length
  364. },
  365. VIDEO_RATES: {
  366. URL: { min: 3, max: 2000 } // Length
  367. },
  368. VIDEOS: {
  369. NAME: { min: 3, max: 120 }, // Length
  370. LANGUAGE: { min: 1, max: 10 }, // Length
  371. TRUNCATED_DESCRIPTION: { min: 3, max: 250 }, // Length
  372. DESCRIPTION: { min: 3, max: 10000 }, // Length
  373. SUPPORT: { min: 3, max: 1000 }, // Length
  374. IMAGE: {
  375. EXTNAME: [ '.png', '.jpg', '.jpeg', '.webp' ],
  376. FILE_SIZE: {
  377. max: 4 * 1024 * 1024 // 4MB
  378. }
  379. },
  380. EXTNAME: [] as string[],
  381. INFO_HASH: { min: 40, max: 40 }, // Length, info hash is 20 bytes length but we represent it in hexadecimal so 20 * 2
  382. DURATION: { min: 0 }, // Number
  383. TAGS: { min: 0, max: 5 }, // Number of total tags
  384. TAG: { min: 2, max: 30 }, // Length
  385. VIEWS: { min: 0 },
  386. LIKES: { min: 0 },
  387. DISLIKES: { min: 0 },
  388. FILE_SIZE: { min: -1 },
  389. PARTIAL_UPLOAD_SIZE: { max: 50 * 1024 * 1024 * 1024 }, // 50GB
  390. URL: { min: 3, max: 2000 } // Length
  391. },
  392. VIDEO_SOURCE: {
  393. FILENAME: { min: 1, max: 1000 } // Length
  394. },
  395. VIDEO_PLAYLISTS: {
  396. NAME: { min: 1, max: 120 }, // Length
  397. DESCRIPTION: { min: 3, max: 1000 }, // Length
  398. URL: { min: 3, max: 2000 }, // Length
  399. IMAGE: {
  400. EXTNAME: [ '.jpg', '.jpeg' ],
  401. FILE_SIZE: {
  402. max: 4 * 1024 * 1024 // 4MB
  403. }
  404. }
  405. },
  406. ACTORS: {
  407. PUBLIC_KEY: { min: 10, max: 5000 }, // Length
  408. PRIVATE_KEY: { min: 10, max: 5000 }, // Length
  409. URL: { min: 3, max: 2000 }, // Length
  410. IMAGE: {
  411. EXTNAME: [ '.png', '.jpeg', '.jpg', '.gif', '.webp' ],
  412. FILE_SIZE: {
  413. max: 4 * 1024 * 1024 // 4MB
  414. }
  415. }
  416. },
  417. VIDEO_EVENTS: {
  418. COUNT: { min: 0 }
  419. },
  420. VIDEO_COMMENTS: {
  421. TEXT: { min: 1, max: 10000 }, // Length
  422. URL: { min: 3, max: 2000 } // Length
  423. },
  424. VIDEO_SHARE: {
  425. URL: { min: 3, max: 2000 } // Length
  426. },
  427. CONTACT_FORM: {
  428. FROM_NAME: { min: 1, max: 120 }, // Length
  429. BODY: { min: 3, max: 5000 } // Length
  430. },
  431. PLUGINS: {
  432. NAME: { min: 1, max: 214 }, // Length
  433. DESCRIPTION: { min: 1, max: 20000 } // Length
  434. },
  435. COMMONS: {
  436. URL: { min: 5, max: 2000 } // Length
  437. },
  438. VIDEO_STUDIO: {
  439. TASKS: { min: 1, max: 10 }, // Number of tasks
  440. CUT_TIME: { min: 0 } // Value
  441. },
  442. LOGS: {
  443. CLIENT_MESSAGE: { min: 1, max: 1000 }, // Length
  444. CLIENT_STACK_TRACE: { min: 1, max: 15000 }, // Length
  445. CLIENT_META: { min: 1, max: 15000 }, // Length
  446. CLIENT_USER_AGENT: { min: 1, max: 200 } // Length
  447. },
  448. RUNNERS: {
  449. TOKEN: { min: 1, max: 1000 }, // Length
  450. NAME: { min: 1, max: 100 }, // Length
  451. DESCRIPTION: { min: 1, max: 1000 } // Length
  452. },
  453. RUNNER_JOBS: {
  454. TOKEN: { min: 1, max: 1000 }, // Length
  455. REASON: { min: 1, max: 5000 }, // Length
  456. ERROR_MESSAGE: { min: 1, max: 5000 }, // Length
  457. PROGRESS: { min: 0, max: 100 } // Value
  458. },
  459. VIDEO_PASSWORD: {
  460. LENGTH: { min: 2, max: 100 }
  461. },
  462. VIDEO_CHAPTERS: {
  463. TITLE: { min: 1, max: 100 } // Length
  464. },
  465. WATCHED_WORDS: {
  466. LIST_NAME: { min: 1, max: 100 }, // Length
  467. WORDS: { min: 1, max: 500 }, // Number of total words
  468. WORD: { min: 1, max: 100 } // Length
  469. }
  470. }
  471. const VIEW_LIFETIME = {
  472. VIEW: CONFIG.VIEWS.VIDEOS.VIEW_EXPIRATION,
  473. VIEWER_COUNTER: 60000 * 2, // 2 minutes
  474. VIEWER_STATS: 60000 * 60 // 1 hour
  475. }
  476. let VIEWER_SYNC_REDIS = 30000 // Sync viewer into redis
  477. const MAX_LOCAL_VIEWER_WATCH_SECTIONS = 100
  478. let CONTACT_FORM_LIFETIME = 60000 * 60 // 1 hour
  479. const VIDEO_TRANSCODING_FPS: VideoTranscodingFPS = {
  480. HARD_MIN: 0.1,
  481. SOFT_MIN: 1,
  482. STANDARD: [ 24, 25, 30 ],
  483. HD_STANDARD: [ 50, 60 ],
  484. AUDIO_MERGE: 25,
  485. AVERAGE: 30,
  486. SOFT_MAX: 60,
  487. KEEP_ORIGIN_FPS_RESOLUTION_MIN: 720 // We keep the original FPS on high resolutions (720 minimum)
  488. }
  489. const DEFAULT_AUDIO_RESOLUTION = VideoResolution.H_480P
  490. const VIDEO_RATE_TYPES: { [ id: string ]: VideoRateType } = {
  491. LIKE: 'like',
  492. DISLIKE: 'dislike'
  493. }
  494. const USER_IMPORT = {
  495. MAX_PLAYLIST_ELEMENTS: 1000
  496. }
  497. const FFMPEG_NICE = {
  498. // parent process defaults to niceness = 0
  499. // reminder: lower = higher priority, max value is 19, lowest is -20
  500. LIVE: 5, // prioritize over VOD and THUMBNAIL
  501. THUMBNAIL: 10,
  502. VOD: 15
  503. }
  504. const VIDEO_CATEGORIES = {
  505. 1: 'Music',
  506. 2: 'Films',
  507. 3: 'Vehicles',
  508. 4: 'Art',
  509. 5: 'Sports',
  510. 6: 'Travels',
  511. 7: 'Gaming',
  512. 8: 'People',
  513. 9: 'Comedy',
  514. 10: 'Entertainment',
  515. 11: 'News & Politics',
  516. 12: 'How To',
  517. 13: 'Education',
  518. 14: 'Activism',
  519. 15: 'Science & Technology',
  520. 16: 'Animals',
  521. 17: 'Kids',
  522. 18: 'Food'
  523. }
  524. // See https://creativecommons.org/licenses/?lang=en
  525. const VIDEO_LICENCES = {
  526. 1: 'Attribution',
  527. 2: 'Attribution - Share Alike',
  528. 3: 'Attribution - No Derivatives',
  529. 4: 'Attribution - Non Commercial',
  530. 5: 'Attribution - Non Commercial - Share Alike',
  531. 6: 'Attribution - Non Commercial - No Derivatives',
  532. 7: 'Public Domain Dedication'
  533. }
  534. const VIDEO_LANGUAGES: { [id: string]: string } = {}
  535. const VIDEO_PRIVACIES: { [ id in VideoPrivacyType ]: string } = {
  536. [VideoPrivacy.PUBLIC]: 'Public',
  537. [VideoPrivacy.UNLISTED]: 'Unlisted',
  538. [VideoPrivacy.PRIVATE]: 'Private',
  539. [VideoPrivacy.INTERNAL]: 'Internal',
  540. [VideoPrivacy.PASSWORD_PROTECTED]: 'Password protected'
  541. }
  542. const VIDEO_STATES: { [ id in VideoStateType ]: string } = {
  543. [VideoState.PUBLISHED]: 'Published',
  544. [VideoState.TO_TRANSCODE]: 'To transcode',
  545. [VideoState.TO_IMPORT]: 'To import',
  546. [VideoState.WAITING_FOR_LIVE]: 'Waiting for livestream',
  547. [VideoState.LIVE_ENDED]: 'Livestream ended',
  548. [VideoState.TO_MOVE_TO_EXTERNAL_STORAGE]: 'To move to an external storage',
  549. [VideoState.TRANSCODING_FAILED]: 'Transcoding failed',
  550. [VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED]: 'External storage move failed',
  551. [VideoState.TO_EDIT]: 'To edit',
  552. [VideoState.TO_MOVE_TO_FILE_SYSTEM]: 'To move to file system',
  553. [VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED]: 'Move to file system failed'
  554. }
  555. const VIDEO_IMPORT_STATES: { [ id in VideoImportStateType ]: string } = {
  556. [VideoImportState.FAILED]: 'Failed',
  557. [VideoImportState.PENDING]: 'Pending',
  558. [VideoImportState.SUCCESS]: 'Success',
  559. [VideoImportState.REJECTED]: 'Rejected',
  560. [VideoImportState.CANCELLED]: 'Cancelled',
  561. [VideoImportState.PROCESSING]: 'Processing'
  562. }
  563. const VIDEO_CHANNEL_SYNC_STATE: { [ id in VideoChannelSyncStateType ]: string } = {
  564. [VideoChannelSyncState.FAILED]: 'Failed',
  565. [VideoChannelSyncState.SYNCED]: 'Synchronized',
  566. [VideoChannelSyncState.PROCESSING]: 'Processing',
  567. [VideoChannelSyncState.WAITING_FIRST_RUN]: 'Waiting first run'
  568. }
  569. const ABUSE_STATES: { [ id in AbuseStateType ]: string } = {
  570. [AbuseState.PENDING]: 'Pending',
  571. [AbuseState.REJECTED]: 'Rejected',
  572. [AbuseState.ACCEPTED]: 'Accepted'
  573. }
  574. const USER_REGISTRATION_STATES: { [ id in UserRegistrationStateType ]: string } = {
  575. [UserRegistrationState.PENDING]: 'Pending',
  576. [UserRegistrationState.REJECTED]: 'Rejected',
  577. [UserRegistrationState.ACCEPTED]: 'Accepted'
  578. }
  579. const VIDEO_PLAYLIST_PRIVACIES: { [ id in VideoPlaylistPrivacyType ]: string } = {
  580. [VideoPlaylistPrivacy.PUBLIC]: 'Public',
  581. [VideoPlaylistPrivacy.UNLISTED]: 'Unlisted',
  582. [VideoPlaylistPrivacy.PRIVATE]: 'Private'
  583. }
  584. const VIDEO_PLAYLIST_TYPES: { [ id in VideoPlaylistType_Type ]: string } = {
  585. [VideoPlaylistType.REGULAR]: 'Regular',
  586. [VideoPlaylistType.WATCH_LATER]: 'Watch later'
  587. }
  588. const RUNNER_JOB_STATES: { [ id in RunnerJobStateType ]: string } = {
  589. [RunnerJobState.PROCESSING]: 'Processing',
  590. [RunnerJobState.COMPLETED]: 'Completed',
  591. [RunnerJobState.COMPLETING]: 'Completing',
  592. [RunnerJobState.PENDING]: 'Pending',
  593. [RunnerJobState.ERRORED]: 'Errored',
  594. [RunnerJobState.WAITING_FOR_PARENT_JOB]: 'Waiting for parent job to finish',
  595. [RunnerJobState.CANCELLED]: 'Cancelled',
  596. [RunnerJobState.PARENT_ERRORED]: 'Parent job failed',
  597. [RunnerJobState.PARENT_CANCELLED]: 'Parent job cancelled'
  598. }
  599. const USER_EXPORT_STATES: { [ id in UserExportStateType ]: string } = {
  600. [UserExportState.PENDING]: 'Pending',
  601. [UserExportState.PROCESSING]: 'Processing',
  602. [UserExportState.COMPLETED]: 'Completed',
  603. [UserExportState.ERRORED]: 'Failed'
  604. }
  605. const USER_IMPORT_STATES: { [ id in UserImportStateType ]: string } = {
  606. [UserImportState.PENDING]: 'Pending',
  607. [UserImportState.PROCESSING]: 'Processing',
  608. [UserImportState.COMPLETED]: 'Completed',
  609. [UserImportState.ERRORED]: 'Failed'
  610. }
  611. const VIDEO_COMMENTS_POLICY: { [ id in VideoCommentPolicyType ]: string } = {
  612. [VideoCommentPolicy.DISABLED]: 'Disabled',
  613. [VideoCommentPolicy.ENABLED]: 'Enabled',
  614. [VideoCommentPolicy.REQUIRES_APPROVAL]: 'Requires approval'
  615. }
  616. const MIMETYPES = {
  617. AUDIO: {
  618. MIMETYPE_EXT: {
  619. 'audio/mpeg': '.mp3',
  620. 'audio/mp3': '.mp3',
  621. 'application/ogg': '.ogg',
  622. 'audio/ogg': '.ogg',
  623. 'audio/x-ms-wma': '.wma',
  624. 'audio/wav': '.wav',
  625. 'audio/x-wav': '.wav',
  626. 'audio/x-flac': '.flac',
  627. 'audio/flac': '.flac',
  628. 'audio/vnd.dlna.adts': '.aac',
  629. 'audio/aac': '.aac',
  630. // Keep priority for preferred mime type
  631. 'audio/m4a': '.m4a',
  632. 'audio/x-m4a': '.m4a',
  633. 'audio/mp4': '.m4a',
  634. 'audio/vnd.dolby.dd-raw': '.ac3',
  635. 'audio/ac3': '.ac3'
  636. },
  637. EXT_MIMETYPE: null as { [ id: string ]: string }
  638. },
  639. VIDEO: {
  640. MIMETYPE_EXT: null as { [ id: string ]: string | string[] },
  641. MIMETYPES_REGEX: null as string,
  642. EXT_MIMETYPE: null as { [ id: string ]: string }
  643. },
  644. IMAGE: {
  645. MIMETYPE_EXT: {
  646. 'image/png': '.png',
  647. 'image/gif': '.gif',
  648. 'image/webp': '.webp',
  649. 'image/jpg': '.jpg',
  650. 'image/jpeg': '.jpg'
  651. },
  652. EXT_MIMETYPE: null as { [ id: string ]: string }
  653. },
  654. VIDEO_CAPTIONS: {
  655. MIMETYPE_EXT: {
  656. 'text/vtt': '.vtt',
  657. 'application/x-subrip': '.srt',
  658. 'text/plain': '.srt'
  659. },
  660. EXT_MIMETYPE: null as { [ id: string ]: string }
  661. },
  662. TORRENT: {
  663. MIMETYPE_EXT: {
  664. 'application/x-bittorrent': '.torrent'
  665. }
  666. },
  667. M3U8: {
  668. MIMETYPE_EXT: {
  669. 'application/vnd.apple.mpegurl': '.m3u8'
  670. }
  671. },
  672. AP_VIDEO: {
  673. MIMETYPE_EXT: {
  674. 'video/mp4': '.mp4',
  675. 'video/ogg': '.ogv',
  676. 'video/webm': '.webm',
  677. 'audio/mp4': '.mp4'
  678. }
  679. },
  680. AP_TORRENT: {
  681. MIMETYPE_EXT: {
  682. 'application/x-bittorrent': '.torrent'
  683. }
  684. },
  685. AP_MAGNET: {
  686. MIMETYPE_EXT: {
  687. 'application/x-bittorrent;x-scheme-handler/magnet': '.magnet'
  688. }
  689. }
  690. }
  691. MIMETYPES.AUDIO.EXT_MIMETYPE = invert(MIMETYPES.AUDIO.MIMETYPE_EXT)
  692. MIMETYPES.IMAGE.EXT_MIMETYPE = invert(MIMETYPES.IMAGE.MIMETYPE_EXT)
  693. MIMETYPES.VIDEO_CAPTIONS.EXT_MIMETYPE = invert(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT)
  694. const BINARY_CONTENT_TYPES = new Set([
  695. 'binary/octet-stream',
  696. 'application/octet-stream',
  697. 'application/x-binary'
  698. ])
  699. // ---------------------------------------------------------------------------
  700. const OVERVIEWS = {
  701. VIDEOS: {
  702. SAMPLE_THRESHOLD: 6,
  703. SAMPLES_COUNT: 20
  704. }
  705. }
  706. // ---------------------------------------------------------------------------
  707. const SERVER_ACTOR_NAME = 'peertube'
  708. const ACTIVITY_PUB = {
  709. POTENTIAL_ACCEPT_HEADERS: [
  710. 'application/activity+json',
  711. 'application/ld+json',
  712. 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
  713. ],
  714. ACCEPT_HEADER: 'application/activity+json, application/ld+json',
  715. COLLECTION_ITEMS_PER_PAGE: 10,
  716. FETCH_PAGE_LIMIT: 2000,
  717. MAX_RECURSION_COMMENTS: 100,
  718. ACTOR_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
  719. VIDEO_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2, // 2 days
  720. VIDEO_PLAYLIST_REFRESH_INTERVAL: 3600 * 24 * 1000 * 2 // 2 days
  721. }
  722. const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
  723. GROUP: 'Group',
  724. PERSON: 'Person',
  725. APPLICATION: 'Application',
  726. ORGANIZATION: 'Organization',
  727. SERVICE: 'Service'
  728. }
  729. const HTTP_SIGNATURE = {
  730. HEADER_NAME: 'signature',
  731. ALGORITHM: 'rsa-sha256',
  732. HEADERS_TO_SIGN_WITH_PAYLOAD: [ '(request-target)', 'host', 'date', 'digest' ],
  733. HEADERS_TO_SIGN_WITHOUT_PAYLOAD: [ '(request-target)', 'host', 'date' ],
  734. CLOCK_SKEW_SECONDS: 1800
  735. }
  736. // ---------------------------------------------------------------------------
  737. let PRIVATE_RSA_KEY_SIZE = 2048
  738. // Password encryption
  739. const BCRYPT_SALT_SIZE = 10
  740. const ENCRYPTION = {
  741. ALGORITHM: 'aes-256-cbc',
  742. IV: 16,
  743. SALT: 'peertube',
  744. ENCODING: 'hex' as Encoding
  745. }
  746. const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
  747. const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
  748. const TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME = 60000 * 10 // 10 minutes
  749. let JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '15 minutes'
  750. const EMAIL_VERIFY_LIFETIME = 60000 * 60 // 60 minutes
  751. const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
  752. DO_NOT_LIST: 'do_not_list',
  753. BLUR: 'blur',
  754. DISPLAY: 'display'
  755. }
  756. // ---------------------------------------------------------------------------
  757. const USER_EXPORT_MAX_ITEMS = 1000
  758. const USER_EXPORT_FILE_PREFIX = 'user-export-'
  759. // ---------------------------------------------------------------------------
  760. // Express static paths (router)
  761. const STATIC_PATHS = {
  762. // TODO: deprecated in v6, to remove
  763. THUMBNAILS: '/static/thumbnails/',
  764. // Need to keep this legacy path for previously generated torrents
  765. LEGACY_WEB_VIDEOS: '/static/webseed/',
  766. WEB_VIDEOS: '/static/web-videos/',
  767. // Need to keep this legacy path for previously generated torrents
  768. LEGACY_PRIVATE_WEB_VIDEOS: '/static/webseed/private/',
  769. PRIVATE_WEB_VIDEOS: '/static/web-videos/private/',
  770. REDUNDANCY: '/static/redundancy/',
  771. STREAMING_PLAYLISTS: {
  772. HLS: '/static/streaming-playlists/hls',
  773. PRIVATE_HLS: '/static/streaming-playlists/hls/private/'
  774. }
  775. }
  776. const STATIC_DOWNLOAD_PATHS = {
  777. TORRENTS: '/download/torrents/',
  778. VIDEOS: '/download/videos/',
  779. HLS_VIDEOS: '/download/streaming-playlists/hls/videos/',
  780. USER_EXPORTS: '/download/user-exports/',
  781. ORIGINAL_VIDEO_FILE: '/download/original-video-files/'
  782. }
  783. const LAZY_STATIC_PATHS = {
  784. THUMBNAILS: '/lazy-static/thumbnails/',
  785. BANNERS: '/lazy-static/banners/',
  786. AVATARS: '/lazy-static/avatars/',
  787. PREVIEWS: '/lazy-static/previews/',
  788. VIDEO_CAPTIONS: '/lazy-static/video-captions/',
  789. TORRENTS: '/lazy-static/torrents/',
  790. STORYBOARDS: '/lazy-static/storyboards/'
  791. }
  792. const OBJECT_STORAGE_PROXY_PATHS = {
  793. // Need to keep this legacy path for previously generated torrents
  794. LEGACY_PRIVATE_WEB_VIDEOS: '/object-storage-proxy/webseed/private/',
  795. PRIVATE_WEB_VIDEOS: '/object-storage-proxy/web-videos/private/',
  796. STREAMING_PLAYLISTS: {
  797. PRIVATE_HLS: '/object-storage-proxy/streaming-playlists/hls/private/'
  798. }
  799. }
  800. // Cache control
  801. const STATIC_MAX_AGE = {
  802. SERVER: '2h',
  803. LAZY_SERVER: '2d',
  804. CLIENT: '30d'
  805. }
  806. // Videos thumbnail size
  807. const THUMBNAILS_SIZE = {
  808. width: minBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
  809. height: minBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
  810. minRemoteWidth: 150
  811. }
  812. const PREVIEWS_SIZE = {
  813. width: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').width,
  814. height: maxBy(CONFIG.THUMBNAILS.SIZES, 'width').height,
  815. minRemoteWidth: 400
  816. }
  817. const ACTOR_IMAGES_SIZE: { [key in ActorImageType_Type]: { width: number, height: number }[] } = {
  818. [ActorImageType.AVATAR]: [ // 1/1 ratio
  819. {
  820. width: 1500,
  821. height: 1500
  822. },
  823. {
  824. width: 600,
  825. height: 600
  826. },
  827. {
  828. width: 120,
  829. height: 120
  830. },
  831. {
  832. width: 48,
  833. height: 48
  834. }
  835. ],
  836. [ActorImageType.BANNER]: [ // 6/1 ratio
  837. {
  838. width: 1920,
  839. height: 317
  840. },
  841. {
  842. width: 600,
  843. height: 100
  844. }
  845. ]
  846. }
  847. const STORYBOARD = {
  848. SPRITE_MAX_SIZE: 192,
  849. SPRITES_MAX_EDGE_COUNT: 10
  850. }
  851. const EMBED_SIZE = {
  852. width: 560,
  853. height: 315
  854. }
  855. // Sub folders of cache directory
  856. const FILES_CACHE = {
  857. PREVIEWS: {
  858. DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'previews'),
  859. MAX_AGE: 1000 * 3600 * 3 // 3 hours
  860. },
  861. STORYBOARDS: {
  862. DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'storyboards'),
  863. MAX_AGE: 1000 * 3600 * 24 // 24 hours
  864. },
  865. VIDEO_CAPTIONS: {
  866. DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'video-captions'),
  867. MAX_AGE: 1000 * 3600 * 3 // 3 hours
  868. },
  869. TORRENTS: {
  870. DIRECTORY: join(CONFIG.STORAGE.CACHE_DIR, 'torrents'),
  871. MAX_AGE: 1000 * 3600 * 3 // 3 hours
  872. }
  873. }
  874. const LRU_CACHE = {
  875. USER_TOKENS: {
  876. MAX_SIZE: 1000
  877. },
  878. FILENAME_TO_PATH_PERMANENT_FILE_CACHE: {
  879. MAX_SIZE: 1000
  880. },
  881. STATIC_VIDEO_FILES_RIGHTS_CHECK: {
  882. MAX_SIZE: 5000,
  883. TTL: parseDurationToMs('10 seconds')
  884. },
  885. VIDEO_TOKENS: {
  886. MAX_SIZE: 100_000,
  887. TTL: parseDurationToMs('8 hours')
  888. },
  889. WATCHED_WORDS_REGEX: {
  890. MAX_SIZE: 100,
  891. TTL: parseDurationToMs('24 hours')
  892. },
  893. TRACKER_IPS: {
  894. MAX_SIZE: 100_000
  895. }
  896. }
  897. const DIRECTORIES = {
  898. RESUMABLE_UPLOAD: join(CONFIG.STORAGE.TMP_DIR, 'resumable-uploads'),
  899. HLS_STREAMING_PLAYLIST: {
  900. PUBLIC: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls'),
  901. PRIVATE: join(CONFIG.STORAGE.STREAMING_PLAYLISTS_DIR, 'hls', 'private')
  902. },
  903. WEB_VIDEOS: {
  904. PUBLIC: CONFIG.STORAGE.WEB_VIDEOS_DIR,
  905. PRIVATE: join(CONFIG.STORAGE.WEB_VIDEOS_DIR, 'private')
  906. },
  907. ORIGINAL_VIDEOS: CONFIG.STORAGE.ORIGINAL_VIDEO_FILES_DIR,
  908. HLS_REDUNDANCY: join(CONFIG.STORAGE.REDUNDANCY_DIR, 'hls'),
  909. LOCAL_PIP_DIRECTORY: join(CONFIG.STORAGE.BIN_DIR, 'pip')
  910. }
  911. const RESUMABLE_UPLOAD_SESSION_LIFETIME = SCHEDULER_INTERVALS_MS.REMOVE_DANGLING_RESUMABLE_UPLOADS
  912. const VIDEO_LIVE = {
  913. EXTENSION: '.ts',
  914. CLEANUP_DELAY: 1000 * 60 * 5, // 5 minutes
  915. SEGMENT_TIME_SECONDS: {
  916. DEFAULT_LATENCY: 4, // 4 seconds
  917. SMALL_LATENCY: 2 // 2 seconds
  918. },
  919. SEGMENTS_LIST_SIZE: 15, // 15 maximum segments in live playlist
  920. REPLAY_DIRECTORY: 'replay',
  921. EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION: 4,
  922. MAX_SOCKET_WAITING_DATA: 1024 * 1000 * 100, // 100MB
  923. RTMP: {
  924. CHUNK_SIZE: 60000,
  925. GOP_CACHE: true,
  926. PING: 60,
  927. PING_TIMEOUT: 30,
  928. BASE_PATH: 'live'
  929. }
  930. }
  931. const MEMOIZE_TTL = {
  932. OVERVIEWS_SAMPLE: 1000 * 3600 * 4, // 4 hours
  933. INFO_HASH_EXISTS: 1000 * 60, // 1 minute
  934. VIDEO_DURATION: 1000 * 10, // 10 seconds
  935. LIVE_ABLE_TO_UPLOAD: 1000 * 60, // 1 minute
  936. LIVE_CHECK_SOCKET_HEALTH: 1000 * 60, // 1 minute
  937. GET_STATS_FOR_OPEN_TELEMETRY_METRICS: 1000 * 60, // 1 minute
  938. EMBED_HTML: 1000 * 10 // 10 seconds
  939. }
  940. const MEMOIZE_LENGTH = {
  941. INFO_HASH_EXISTS: 200,
  942. VIDEO_DURATION: 200
  943. }
  944. const totalCPUs = Math.max(cpus().length, 1)
  945. const WORKER_THREADS = {
  946. DOWNLOAD_IMAGE: {
  947. CONCURRENCY: 3,
  948. MAX_THREADS: 1
  949. },
  950. PROCESS_IMAGE: {
  951. CONCURRENCY: 1,
  952. MAX_THREADS: Math.min(totalCPUs, 5)
  953. },
  954. GET_IMAGE_SIZE: {
  955. CONCURRENCY: 1,
  956. MAX_THREADS: Math.min(totalCPUs, 5)
  957. },
  958. SIGN_JSON_LD_OBJECT: {
  959. CONCURRENCY: 1,
  960. MAX_THREADS: 1 // FIXME: we would want 2 threads but there is an issue with JSONLD in worker thread where CPU jumps and stays at 100%
  961. },
  962. BUILD_DIGEST: {
  963. CONCURRENCY: 1,
  964. MAX_THREADS: 1
  965. }
  966. }
  967. const REDUNDANCY = {
  968. VIDEOS: {
  969. RANDOMIZED_FACTOR: 5
  970. }
  971. }
  972. const ACCEPT_HEADERS = [ 'html', 'application/json' ].concat(ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS)
  973. const OTP = {
  974. HEADER_NAME: 'x-peertube-otp',
  975. HEADER_REQUIRED_VALUE: 'required; app'
  976. }
  977. const ASSETS_PATH = {
  978. DEFAULT_AUDIO_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-audio-background.jpg'),
  979. DEFAULT_LIVE_BACKGROUND: join(root(), 'dist', 'core', 'assets', 'default-live-background.jpg')
  980. }
  981. // ---------------------------------------------------------------------------
  982. const CUSTOM_HTML_TAG_COMMENTS = {
  983. TITLE: '<!-- title tag -->',
  984. DESCRIPTION: '<!-- description tag -->',
  985. CUSTOM_CSS: '<!-- custom css tag -->',
  986. META_TAGS: '<!-- meta tags -->',
  987. SERVER_CONFIG: '<!-- server config -->'
  988. }
  989. const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
  990. const LOG_FILENAME = 'peertube.log'
  991. const AUDIT_LOG_FILENAME = 'peertube-audit.log'
  992. // ---------------------------------------------------------------------------
  993. const TRACKER_RATE_LIMITS = {
  994. INTERVAL: 60000 * 5, // 5 minutes
  995. ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval
  996. ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval
  997. BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes')
  998. }
  999. const P2P_MEDIA_LOADER_PEER_VERSION = 2
  1000. // ---------------------------------------------------------------------------
  1001. const PLUGIN_GLOBAL_CSS_FILE_NAME = 'plugins-global.css'
  1002. const PLUGIN_GLOBAL_CSS_PATH = join(CONFIG.STORAGE.TMP_DIR, PLUGIN_GLOBAL_CSS_FILE_NAME)
  1003. let PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 1000 * 60 * 5 // 5 minutes
  1004. const DEFAULT_THEME_NAME = 'default'
  1005. const DEFAULT_USER_THEME_NAME = 'instance-default'
  1006. // ---------------------------------------------------------------------------
  1007. const SEARCH_INDEX = {
  1008. ROUTES: {
  1009. VIDEOS: '/api/v1/search/videos',
  1010. VIDEO_CHANNELS: '/api/v1/search/video-channels'
  1011. }
  1012. }
  1013. // ---------------------------------------------------------------------------
  1014. const STATS_TIMESERIE = {
  1015. MAX_DAYS: 365 * 10 // Around 10 years
  1016. }
  1017. // ---------------------------------------------------------------------------
  1018. // Special constants for a test instance
  1019. if (process.env.PRODUCTION_CONSTANTS !== 'true') {
  1020. if (isTestOrDevInstance()) {
  1021. PRIVATE_RSA_KEY_SIZE = 1024
  1022. ACTOR_FOLLOW_SCORE.BASE = 20
  1023. REMOTE_SCHEME.HTTP = 'http'
  1024. REMOTE_SCHEME.WS = 'ws'
  1025. STATIC_MAX_AGE.SERVER = '0'
  1026. SCHEDULER_INTERVALS_MS.ACTOR_FOLLOW_SCORES = 1000
  1027. SCHEDULER_INTERVALS_MS.REMOVE_OLD_JOBS = 10000
  1028. SCHEDULER_INTERVALS_MS.REMOVE_OLD_HISTORY = 5000
  1029. SCHEDULER_INTERVALS_MS.REMOVE_OLD_VIEWS = 5000
  1030. SCHEDULER_INTERVALS_MS.UPDATE_VIDEOS = 5000
  1031. SCHEDULER_INTERVALS_MS.AUTO_FOLLOW_INDEX_INSTANCES = 5000
  1032. SCHEDULER_INTERVALS_MS.UPDATE_INBOX_STATS = 5000
  1033. SCHEDULER_INTERVALS_MS.CHECK_PEERTUBE_VERSION = 2000
  1034. REPEAT_JOBS['videos-views-stats'] = { every: 5000 }
  1035. REPEAT_JOBS['activitypub-cleaner'] = { every: 5000 }
  1036. AP_CLEANER.PERIOD = 5000
  1037. REDUNDANCY.VIDEOS.RANDOMIZED_FACTOR = 1
  1038. CONTACT_FORM_LIFETIME = 1000 // 1 second
  1039. JOB_ATTEMPTS['email'] = 1
  1040. FILES_CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
  1041. MEMOIZE_TTL.OVERVIEWS_SAMPLE = 3000
  1042. MEMOIZE_TTL.LIVE_ABLE_TO_UPLOAD = 3000
  1043. MEMOIZE_TTL.EMBED_HTML = 1
  1044. OVERVIEWS.VIDEOS.SAMPLE_THRESHOLD = 2
  1045. PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME = 5000
  1046. JOB_REMOVAL_OPTIONS.SUCCESS['videos-views-stats'] = 10000
  1047. VIEWER_SYNC_REDIS = 1000
  1048. }
  1049. if (isTestInstance()) {
  1050. ACTIVITY_PUB.COLLECTION_ITEMS_PER_PAGE = 2
  1051. ACTIVITY_PUB.ACTOR_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
  1052. ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
  1053. ACTIVITY_PUB.VIDEO_PLAYLIST_REFRESH_INTERVAL = 10 * 1000 // 10 seconds
  1054. CONSTRAINTS_FIELDS.ACTORS.IMAGE.FILE_SIZE.max = 100 * 1024 // 100KB
  1055. CONSTRAINTS_FIELDS.VIDEOS.IMAGE.FILE_SIZE.max = 400 * 1024 // 400KB
  1056. VIEW_LIFETIME.VIEWER_COUNTER = 1000 * 5 // 5 second
  1057. VIEW_LIFETIME.VIEWER_STATS = 1000 * 5 // 5 second
  1058. VIDEO_LIVE.CLEANUP_DELAY = getIntEnv('PEERTUBE_TEST_CONSTANTS_VIDEO_LIVE_CLEANUP_DELAY') ?? 5000
  1059. VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY = 2
  1060. VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY = 1
  1061. VIDEO_LIVE.EDGE_LIVE_DELAY_SEGMENTS_NOTIFICATION = 1
  1062. RUNNER_JOBS.LAST_CONTACT_UPDATE_INTERVAL = 2000
  1063. JWT_TOKEN_USER_EXPORT_FILE_LIFETIME = '2 seconds'
  1064. }
  1065. }
  1066. updateWebserverUrls()
  1067. updateWebserverConfig()
  1068. registerConfigChangedHandler(() => {
  1069. updateWebserverUrls()
  1070. updateWebserverConfig()
  1071. })
  1072. // ---------------------------------------------------------------------------
  1073. const FILES_CONTENT_HASH = {
  1074. MANIFEST: generateContentHash(),
  1075. FAVICON: generateContentHash(),
  1076. LOGO: generateContentHash()
  1077. }
  1078. // ---------------------------------------------------------------------------
  1079. const VIDEO_FILTERS = {
  1080. WATERMARK: {
  1081. SIZE_RATIO: 1 / 10,
  1082. HORIZONTAL_MARGIN_RATIO: 1 / 20,
  1083. VERTICAL_MARGIN_RATIO: 1 / 20
  1084. }
  1085. }
  1086. // ---------------------------------------------------------------------------
  1087. export {
  1088. WEBSERVER,
  1089. API_VERSION,
  1090. ENCRYPTION,
  1091. VIDEO_LIVE,
  1092. PEERTUBE_VERSION,
  1093. LAZY_STATIC_PATHS,
  1094. OBJECT_STORAGE_PROXY_PATHS,
  1095. SEARCH_INDEX,
  1096. DIRECTORIES,
  1097. RESUMABLE_UPLOAD_SESSION_LIFETIME,
  1098. RUNNER_JOB_STATES,
  1099. USER_EXPORT_STATES,
  1100. USER_IMPORT_STATES,
  1101. P2P_MEDIA_LOADER_PEER_VERSION,
  1102. STORYBOARD,
  1103. ACTOR_IMAGES_SIZE,
  1104. ACCEPT_HEADERS,
  1105. BCRYPT_SALT_SIZE,
  1106. TRACKER_RATE_LIMITS,
  1107. VIDEO_COMMENTS_POLICY,
  1108. FILES_CACHE,
  1109. LOG_FILENAME,
  1110. CONSTRAINTS_FIELDS,
  1111. EMBED_SIZE,
  1112. REDUNDANCY,
  1113. USER_EXPORT_FILE_PREFIX,
  1114. JOB_CONCURRENCY,
  1115. JOB_ATTEMPTS,
  1116. AP_CLEANER,
  1117. LAST_MIGRATION_VERSION,
  1118. CUSTOM_HTML_TAG_COMMENTS,
  1119. STATS_TIMESERIE,
  1120. BROADCAST_CONCURRENCY,
  1121. AUDIT_LOG_FILENAME,
  1122. USER_IMPORT,
  1123. PAGINATION,
  1124. ACTOR_FOLLOW_SCORE,
  1125. PREVIEWS_SIZE,
  1126. REMOTE_SCHEME,
  1127. FOLLOW_STATES,
  1128. DEFAULT_USER_THEME_NAME,
  1129. SERVER_ACTOR_NAME,
  1130. TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME,
  1131. JWT_TOKEN_USER_EXPORT_FILE_LIFETIME,
  1132. PLUGIN_GLOBAL_CSS_FILE_NAME,
  1133. PLUGIN_GLOBAL_CSS_PATH,
  1134. PRIVATE_RSA_KEY_SIZE,
  1135. VIDEO_FILTERS,
  1136. ROUTE_CACHE_LIFETIME,
  1137. SORTABLE_COLUMNS,
  1138. JOB_TTL,
  1139. DEFAULT_THEME_NAME,
  1140. NSFW_POLICY_TYPES,
  1141. STATIC_MAX_AGE,
  1142. VIEWER_SYNC_REDIS,
  1143. STATIC_PATHS,
  1144. USER_EXPORT_MAX_ITEMS,
  1145. VIDEO_IMPORT_TIMEOUT,
  1146. VIDEO_PLAYLIST_TYPES,
  1147. MAX_LOGS_OUTPUT_CHARACTERS,
  1148. ACTIVITY_PUB,
  1149. ACTIVITY_PUB_ACTOR_TYPES,
  1150. THUMBNAILS_SIZE,
  1151. VIDEO_CATEGORIES,
  1152. MEMOIZE_LENGTH,
  1153. VIDEO_LANGUAGES,
  1154. VIDEO_PRIVACIES,
  1155. VIDEO_LICENCES,
  1156. VIDEO_STATES,
  1157. WORKER_THREADS,
  1158. VIDEO_RATE_TYPES,
  1159. JOB_PRIORITY,
  1160. VIDEO_TRANSCODING_FPS,
  1161. FFMPEG_NICE,
  1162. ABUSE_STATES,
  1163. USER_REGISTRATION_STATES,
  1164. LRU_CACHE,
  1165. REQUEST_TIMEOUTS,
  1166. RUNNER_JOBS,
  1167. MAX_LOCAL_VIEWER_WATCH_SECTIONS,
  1168. USER_PASSWORD_RESET_LIFETIME,
  1169. USER_PASSWORD_CREATE_LIFETIME,
  1170. MEMOIZE_TTL,
  1171. EMAIL_VERIFY_LIFETIME,
  1172. OVERVIEWS,
  1173. SCHEDULER_INTERVALS_MS,
  1174. REPEAT_JOBS,
  1175. STATIC_DOWNLOAD_PATHS,
  1176. MIMETYPES,
  1177. CRAWL_REQUEST_CONCURRENCY,
  1178. DEFAULT_AUDIO_RESOLUTION,
  1179. BINARY_CONTENT_TYPES,
  1180. JOB_REMOVAL_OPTIONS,
  1181. HTTP_SIGNATURE,
  1182. VIDEO_IMPORT_STATES,
  1183. VIDEO_CHANNEL_SYNC_STATE,
  1184. VIEW_LIFETIME,
  1185. CONTACT_FORM_LIFETIME,
  1186. VIDEO_PLAYLIST_PRIVACIES,
  1187. PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
  1188. ASSETS_PATH,
  1189. FILES_CONTENT_HASH,
  1190. OTP,
  1191. loadLanguages,
  1192. buildLanguages,
  1193. generateContentHash
  1194. }
  1195. // ---------------------------------------------------------------------------
  1196. function buildVideoMimetypeExt () {
  1197. const data = {
  1198. // streamable formats that warrant cross-browser compatibility
  1199. 'video/webm': '.webm',
  1200. // We'll add .ogg if additional extensions are enabled
  1201. // We could add .ogg here but since it could be an audio file,
  1202. // it would be confusing for users because PeerTube will refuse their file (based on the mimetype)
  1203. 'video/ogg': [ '.ogv' ],
  1204. 'video/mp4': '.mp4'
  1205. }
  1206. if (CONFIG.TRANSCODING.ENABLED) {
  1207. if (CONFIG.TRANSCODING.ALLOW_ADDITIONAL_EXTENSIONS) {
  1208. data['video/ogg'].push('.ogg')
  1209. Object.assign(data, {
  1210. 'video/x-matroska': '.mkv',
  1211. // Developed by Apple
  1212. 'video/quicktime': [ '.mov', '.qt', '.mqv' ], // often used as output format by editing software
  1213. 'video/x-m4v': '.m4v',
  1214. 'video/m4v': '.m4v',
  1215. // Developed by the Adobe Flash Platform
  1216. 'video/x-flv': '.flv',
  1217. 'video/x-f4v': '.f4v', // replacement for flv
  1218. // Developed by Microsoft
  1219. 'video/x-ms-wmv': '.wmv',
  1220. 'video/x-msvideo': '.avi',
  1221. 'video/avi': '.avi',
  1222. // Developed by 3GPP
  1223. // common video formats for cell phones
  1224. 'video/3gpp': [ '.3gp', '.3gpp' ],
  1225. 'video/3gpp2': [ '.3g2', '.3gpp2' ],
  1226. // Developed by FFmpeg/Mplayer
  1227. 'application/x-nut': '.nut',
  1228. // The standard video format used by many Sony and Panasonic HD camcorders.
  1229. // It is also used for storing high definition video on Blu-ray discs.
  1230. 'video/mp2t': '.mts',
  1231. 'video/vnd.dlna.mpeg-tts': '.mts',
  1232. 'video/m2ts': '.m2ts',
  1233. // Old formats reliant on MPEG-1/MPEG-2
  1234. 'video/mpv': '.mpv',
  1235. 'video/mpeg2': '.m2v',
  1236. 'video/mpeg': [ '.m1v', '.mpg', '.mpe', '.mpeg', '.vob' ],
  1237. 'video/dvd': '.vob',
  1238. // Could be anything
  1239. 'application/octet-stream': null,
  1240. 'application/mxf': '.mxf' // often used as exchange format by editing software
  1241. })
  1242. }
  1243. if (CONFIG.TRANSCODING.ALLOW_AUDIO_FILES) {
  1244. Object.assign(data, MIMETYPES.AUDIO.MIMETYPE_EXT)
  1245. }
  1246. }
  1247. return data
  1248. }
  1249. function updateWebserverUrls () {
  1250. WEBSERVER.URL = sanitizeUrl(CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT)
  1251. WEBSERVER.HOST = sanitizeHost(CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT, REMOTE_SCHEME.HTTP)
  1252. WEBSERVER.WS = CONFIG.WEBSERVER.WS
  1253. WEBSERVER.SCHEME = CONFIG.WEBSERVER.SCHEME
  1254. WEBSERVER.HOSTNAME = CONFIG.WEBSERVER.HOSTNAME
  1255. WEBSERVER.PORT = CONFIG.WEBSERVER.PORT
  1256. const rtmpHostname = CONFIG.LIVE.RTMP.PUBLIC_HOSTNAME || CONFIG.WEBSERVER.HOSTNAME
  1257. const rtmpsHostname = CONFIG.LIVE.RTMPS.PUBLIC_HOSTNAME || CONFIG.WEBSERVER.HOSTNAME
  1258. WEBSERVER.RTMP_URL = 'rtmp://' + rtmpHostname + ':' + CONFIG.LIVE.RTMP.PORT
  1259. WEBSERVER.RTMPS_URL = 'rtmps://' + rtmpsHostname + ':' + CONFIG.LIVE.RTMPS.PORT
  1260. WEBSERVER.RTMP_BASE_LIVE_URL = WEBSERVER.RTMP_URL + '/' + VIDEO_LIVE.RTMP.BASE_PATH
  1261. WEBSERVER.RTMPS_BASE_LIVE_URL = WEBSERVER.RTMPS_URL + '/' + VIDEO_LIVE.RTMP.BASE_PATH
  1262. }
  1263. function updateWebserverConfig () {
  1264. MIMETYPES.VIDEO.MIMETYPE_EXT = buildVideoMimetypeExt()
  1265. MIMETYPES.VIDEO.MIMETYPES_REGEX = buildMimetypesRegex(MIMETYPES.VIDEO.MIMETYPE_EXT)
  1266. MIMETYPES.VIDEO.EXT_MIMETYPE = buildVideoExtMimetype(MIMETYPES.VIDEO.MIMETYPE_EXT)
  1267. CONSTRAINTS_FIELDS.VIDEOS.EXTNAME = Object.keys(MIMETYPES.VIDEO.EXT_MIMETYPE)
  1268. }
  1269. function buildVideoExtMimetype (obj: { [ id: string ]: string | string[] }) {
  1270. const result: { [id: string]: string } = {}
  1271. for (const mimetype of Object.keys(obj)) {
  1272. const value = obj[mimetype]
  1273. if (!value) continue
  1274. const extensions = Array.isArray(value) ? value : [ value ]
  1275. for (const extension of extensions) {
  1276. result[extension] = mimetype
  1277. }
  1278. }
  1279. return result
  1280. }
  1281. function buildMimetypesRegex (obj: { [id: string]: string | string[] }) {
  1282. return Object.keys(obj)
  1283. .map(m => `(${m})`)
  1284. .join('|')
  1285. }
  1286. async function loadLanguages () {
  1287. if (Object.keys(VIDEO_LANGUAGES).length !== 0) return
  1288. Object.assign(VIDEO_LANGUAGES, await buildLanguages())
  1289. }
  1290. async function buildLanguages () {
  1291. const { iso6393 } = await import('iso-639-3')
  1292. const languages: { [id: string]: string } = {}
  1293. const additionalLanguages = {
  1294. sgn: true, // Sign languages (macro language)
  1295. ase: true, // American sign language
  1296. asq: true, // Austrian sign language
  1297. sdl: true, // Arabian sign language
  1298. bfi: true, // British sign language
  1299. bzs: true, // Brazilian sign language
  1300. csl: true, // Chinese sign language
  1301. cse: true, // Czech sign language
  1302. dsl: true, // Danish sign language
  1303. fsl: true, // French sign language
  1304. gsg: true, // German sign language
  1305. pks: true, // Pakistan sign language
  1306. jsl: true, // Japanese sign language
  1307. sfs: true, // South African sign language
  1308. swl: true, // Swedish sign language
  1309. rsl: true, // Russian sign language
  1310. fse: true, // Finnish sign language
  1311. kab: true, // Kabyle
  1312. gcf: true, // Guadeloupean
  1313. lat: true, // Latin
  1314. epo: true, // Esperanto
  1315. tlh: true, // Klingon
  1316. jbo: true, // Lojban
  1317. avk: true, // Kotava
  1318. zxx: true // No linguistic content (ISO-639-2)
  1319. }
  1320. // Only add ISO639-1 languages and some sign languages (ISO639-3)
  1321. iso6393
  1322. .filter(l => {
  1323. return (l.iso6391 !== undefined && l.type === 'living') ||
  1324. additionalLanguages[l.iso6393] === true
  1325. })
  1326. .forEach(l => { languages[l.iso6391 || l.iso6393] = l.name })
  1327. // Override Occitan label
  1328. languages['oc'] = 'Occitan'
  1329. languages['el'] = 'Greek'
  1330. languages['tok'] = 'Toki Pona'
  1331. // Override Portuguese label
  1332. languages['pt'] = 'Portuguese (Brazilian)'
  1333. languages['pt-PT'] = 'Portuguese (Portugal)'
  1334. // Override Spanish labels
  1335. languages['es'] = 'Spanish (Spain)'
  1336. languages['es-419'] = 'Spanish (Latin America)'
  1337. // Chinese languages
  1338. languages['zh-Hans'] = 'Simplified Chinese'
  1339. languages['zh-Hant'] = 'Traditional Chinese'
  1340. // Catalan languages
  1341. languages['ca-valencia'] = 'Valencian'
  1342. return languages
  1343. }
  1344. function generateContentHash () {
  1345. return randomBytes(20).toString('hex')
  1346. }
  1347. function getIntEnv (path: string) {
  1348. if (process.env[path]) return parseInt(process.env[path])
  1349. return undefined
  1350. }