aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html23
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts10
-rw-r--r--client/src/app/+videos/video-list/trending/index.ts2
-rw-r--r--client/src/app/+videos/video-list/trending/video-most-viewed.component.ts (renamed from client/src/app/+videos/video-list/trending/video-trending.component.ts)4
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.html2
-rw-r--r--client/src/app/+videos/video-list/trending/video-trending-header.component.ts29
-rw-r--r--client/src/app/+videos/videos-routing.module.ts22
-rw-r--r--client/src/app/+videos/videos.module.ts4
-rw-r--r--client/src/app/core/core.module.ts4
-rw-r--r--client/src/app/core/routing/index.ts1
-rw-r--r--client/src/app/core/routing/redirect.service.ts26
-rw-r--r--client/src/app/core/routing/trending-guard.service.ts14
-rw-r--r--client/src/app/core/server/server.service.ts8
-rw-r--r--client/src/app/menu/menu.component.html6
-rw-r--r--client/src/app/shared/shared-video-miniature/abstract-video-list.scss2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-list-header.component.html5
-rw-r--r--client/src/app/shared/shared-video-miniature/video-list-header.component.ts12
-rw-r--r--config/default.yaml4
-rw-r--r--config/production.yaml.example4
-rw-r--r--server/controllers/api/config.ts18
-rw-r--r--server/helpers/audit-logger.ts1
-rw-r--r--server/initializers/config.ts10
-rw-r--r--server/middlewares/validators/config.ts3
-rw-r--r--server/models/video/video-query-builder.ts95
-rw-r--r--server/tests/api/check-params/config.ts10
-rw-r--r--server/tests/api/server/config.ts10
-rw-r--r--server/tests/client.ts2
-rw-r--r--shared/extra-utils/server/config.ts10
-rw-r--r--shared/models/server/custom-config.model.ts10
-rw-r--r--shared/models/server/server-config.model.ts8
30 files changed, 256 insertions, 103 deletions
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
index 9e4691670..83b1c6a31 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
@@ -269,9 +269,13 @@
269 <div class="peertube-select-container"> 269 <div class="peertube-select-container">
270 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control"> 270 <select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
271 <option i18n value="/videos/overview">Discover videos</option> 271 <option i18n value="/videos/overview">Discover videos</option>
272 <option i18n value="/videos/trending">Trending videos</option> 272 <optgroup i18n-label label="Trending pages">
273 <option i18n value="/videos/hot">Hot videos</option> 273 <option i18n value="/videos/trending">Default trending page</option>
274 <option i18n value="/videos/most-liked">Most liked videos</option> 274 <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
275 <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
276 <option i18n value="/videos/most-viewed">Most viewed videos</option>
277 <option i18n value="/videos/most-liked">Most liked videos</option>
278 </optgroup>
275 <option i18n value="/videos/recently-added">Recently added videos</option> 279 <option i18n value="/videos/recently-added">Recently added videos</option>
276 <option i18n value="/videos/local">Local videos</option> 280 <option i18n value="/videos/local">Local videos</option>
277 </select> 281 </select>
@@ -279,6 +283,19 @@
279 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div> 283 <div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
280 </div> 284 </div>
281 285
286 <div class="form-group" formGroupName="instance">
287 <label i18n for="instanceDefaultTrendingRoute">Default trending page</label>
288 <div class="peertube-select-container">
289 <select id="instanceDefaultTrendingRoute" formControlName="defaultTrendingRoute" class="form-control">
290 <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
291 <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
292 <option i18n value="/videos/trending">Most viewed videos</option>
293 <option i18n value="/videos/most-liked">Most liked videos</option>
294 </select>
295 </div>
296 <div *ngIf="formErrors.instance.defaultTrendingRoute" class="form-error">{{ formErrors.instance.defaultTrendingRoute }}</div>
297 </div>
298
282 </div> 299 </div>
283 </div> 300 </div>
284 301
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
index 330ab075a..e6fc4582b 100644
--- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
+++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
@@ -186,6 +186,12 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
186 languages: null, 186 languages: null,
187 187
188 defaultClientRoute: null, 188 defaultClientRoute: null,
189 defaultTrendingRoute: null,
190 pages: {
191 hot: {
192 enabled: null
193 }
194 },
189 195
190 customizations: { 196 customizations: {
191 javascript: null, 197 javascript: null,
@@ -364,6 +370,10 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit, A
364 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true 370 return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
365 } 371 }
366 372
373 isTrendingHotEnabled () {
374 return this.form.value['instance']['pages']['hot']['enabled'] === true
375 }
376
367 async formValidated () { 377 async formValidated () {
368 const value: CustomConfig = this.form.getRawValue() 378 const value: CustomConfig = this.form.getRawValue()
369 379
diff --git a/client/src/app/+videos/video-list/trending/index.ts b/client/src/app/+videos/video-list/trending/index.ts
index 8bae205a5..93f4b1df6 100644
--- a/client/src/app/+videos/video-list/trending/index.ts
+++ b/client/src/app/+videos/video-list/trending/index.ts
@@ -1,4 +1,4 @@
1export * from './video-trending-header.component' 1export * from './video-trending-header.component'
2export * from './video-trending.component'
3export * from './video-hot.component' 2export * from './video-hot.component'
3export * from './video-most-viewed.component'
4export * from './video-most-liked.component' 4export * from './video-most-liked.component'
diff --git a/client/src/app/+videos/video-list/trending/video-trending.component.ts b/client/src/app/+videos/video-list/trending/video-most-viewed.component.ts
index e77231586..98ced42d6 100644
--- a/client/src/app/+videos/video-list/trending/video-trending.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-most-viewed.component.ts
@@ -9,11 +9,11 @@ import { VideoSortField } from '@shared/models'
9import { VideoTrendingHeaderComponent } from './video-trending-header.component' 9import { VideoTrendingHeaderComponent } from './video-trending-header.component'
10 10
11@Component({ 11@Component({
12 selector: 'my-videos-trending', 12 selector: 'my-videos-most-viewed',
13 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ], 13 styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
14 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html' 14 templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
15}) 15})
16export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy { 16export class VideoMostViewedComponent extends AbstractVideoList implements OnInit, OnDestroy {
17 HeaderComponent = VideoTrendingHeaderComponent 17 HeaderComponent = VideoTrendingHeaderComponent
18 titlePage: string 18 titlePage: string
19 defaultSort: VideoSortField = '-trending' 19 defaultSort: VideoSortField = '-trending'
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.html b/client/src/app/+videos/video-list/trending/video-trending-header.component.html
index 6319ee6d3..a025bf1a2 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.html
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.html
@@ -1,5 +1,5 @@
1<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()"> 1<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
2 <label *ngFor="let button of buttons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body"> 2 <label *ngFor="let button of visibleButtons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
3 <my-global-icon [iconName]="button.iconName"></my-global-icon> 3 <my-global-icon [iconName]="button.iconName"></my-global-icon>
4 <input ngbButton type="radio" [value]="button.value"> {{ button.label }} 4 <input ngbButton type="radio" [value]="button.value"> {{ button.label }}
5 </label> 5 </label>
diff --git a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
index 125f14e33..e49b61c68 100644
--- a/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
+++ b/client/src/app/+videos/video-list/trending/video-trending-header.component.ts
@@ -1,8 +1,9 @@
1import { Component, Inject } from '@angular/core' 1import { Component, Inject, OnInit } from '@angular/core'
2import { Router } from '@angular/router' 2import { Router } from '@angular/router'
3import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature' 3import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
4import { GlobalIconName } from '@app/shared/shared-icons' 4import { GlobalIconName } from '@app/shared/shared-icons'
5import { VideoSortField } from '@shared/models' 5import { VideoSortField } from '@shared/models'
6import { ServerService } from '@app/core/server/server.service'
6 7
7interface VideoTrendingHeaderItem { 8interface VideoTrendingHeaderItem {
8 label: string 9 label: string
@@ -10,6 +11,7 @@ interface VideoTrendingHeaderItem {
10 value: VideoSortField 11 value: VideoSortField
11 path: string 12 path: string
12 tooltip?: string 13 tooltip?: string
14 hidden?: boolean
13} 15}
14 16
15@Component({ 17@Component({
@@ -18,12 +20,13 @@ interface VideoTrendingHeaderItem {
18 styleUrls: [ './video-trending-header.component.scss' ], 20 styleUrls: [ './video-trending-header.component.scss' ],
19 templateUrl: './video-trending-header.component.html' 21 templateUrl: './video-trending-header.component.html'
20}) 22})
21export class VideoTrendingHeaderComponent extends VideoListHeaderComponent { 23export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit {
22 buttons: VideoTrendingHeaderItem[] 24 buttons: VideoTrendingHeaderItem[]
23 25
24 constructor ( 26 constructor (
25 @Inject('data') public data: any, 27 @Inject('data') public data: any,
26 private router: Router 28 private router: Router,
29 private serverService: ServerService
27 ) { 30 ) {
28 super(data) 31 super(data)
29 32
@@ -34,16 +37,17 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
34 value: '-hot', 37 value: '-hot',
35 path: 'hot', 38 path: 'hot',
36 tooltip: $localize`Videos totalizing the most interactions for recent videos`, 39 tooltip: $localize`Videos totalizing the most interactions for recent videos`,
40 hidden: true
37 }, 41 },
38 { 42 {
39 label: $localize`:Main variant of Trending videos based on number of recent views:Views`, 43 label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
40 iconName: 'trending', 44 iconName: 'trending',
41 value: '-trending', 45 value: '-trending',
42 path: 'trending', 46 path: 'most-viewed',
43 tooltip: $localize`Videos totalizing the most views during the last 24 hours`, 47 tooltip: $localize`Videos totalizing the most views during the last 24 hours`,
44 }, 48 },
45 { 49 {
46 label: $localize`:a variant of Trending videos based on the number of likes:Likes`, 50 label: $localize`:A variant of Trending videos based on the number of likes:Likes`,
47 iconName: 'like', 51 iconName: 'like',
48 value: '-likes', 52 value: '-likes',
49 path: 'most-liked', 53 path: 'most-liked',
@@ -52,6 +56,21 @@ export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
52 ] 56 ]
53 } 57 }
54 58
59 ngOnInit () {
60 this.serverService.getConfig()
61 .subscribe(config => {
62 // don't filter if auto-blacklist is not enabled as this will be the only list
63 if (config.instance.pages.hot.enabled) {
64 const index = this.buttons.findIndex(b => b.path === 'hot')
65 this.buttons[index].hidden = false
66 }
67 })
68 }
69
70 get visibleButtons () {
71 return this.buttons.filter(b => !b.hidden)
72 }
73
55 setSort () { 74 setSort () {
56 const path = this.buttons.find(b => b.value === this.data.model).path 75 const path = this.buttons.find(b => b.value === this.data.model).path
57 this.router.navigate([ `/videos/${path}` ]) 76 this.router.navigate([ `/videos/${path}` ])
diff --git a/client/src/app/+videos/videos-routing.module.ts b/client/src/app/+videos/videos-routing.module.ts
index b6850b436..973935af8 100644
--- a/client/src/app/+videos/videos-routing.module.ts
+++ b/client/src/app/+videos/videos-routing.module.ts
@@ -1,11 +1,11 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouterModule, Routes } from '@angular/router' 2import { RouterModule, Routes } from '@angular/router'
3import { LoginGuard } from '@app/core' 3import { LoginGuard, TrendingGuard } from '@app/core'
4import { MetaGuard } from '@ngx-meta/core' 4import { MetaGuard } from '@ngx-meta/core'
5import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 5import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
6import { VideoHotComponent } from './video-list/trending/video-hot.component' 6import { VideoHotComponent } from './video-list/trending/video-hot.component'
7import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component' 7import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
8import { VideoTrendingComponent } from './video-list/trending/video-trending.component' 8import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
9import { VideoLocalComponent } from './video-list/video-local.component' 9import { VideoLocalComponent } from './video-list/video-local.component'
10import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' 10import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
11import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component' 11import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
@@ -28,27 +28,31 @@ const videosRoutes: Routes = [
28 }, 28 },
29 { 29 {
30 path: 'trending', 30 path: 'trending',
31 component: VideoTrendingComponent, 31 canActivate: [ TrendingGuard ]
32 },
33 {
34 path: 'hot',
35 component: VideoHotComponent,
32 data: { 36 data: {
33 meta: { 37 meta: {
34 title: $localize`Trending videos` 38 title: $localize`Hot videos`
35 }, 39 },
36 reuse: { 40 reuse: {
37 enabled: true, 41 enabled: true,
38 key: 'trending-videos-list' 42 key: 'hot-videos-list'
39 } 43 }
40 } 44 }
41 }, 45 },
42 { 46 {
43 path: 'hot', 47 path: 'most-viewed',
44 component: VideoHotComponent, 48 component: VideoMostViewedComponent,
45 data: { 49 data: {
46 meta: { 50 meta: {
47 title: $localize`Hot videos` 51 title: $localize`Most viewed videos`
48 }, 52 },
49 reuse: { 53 reuse: {
50 enabled: true, 54 enabled: true,
51 key: 'hot-videos-list' 55 key: 'most-viewed-videos-list'
52 } 56 }
53 } 57 }
54 }, 58 },
diff --git a/client/src/app/+videos/videos.module.ts b/client/src/app/+videos/videos.module.ts
index 4c88a0397..ae9c680eb 100644
--- a/client/src/app/+videos/videos.module.ts
+++ b/client/src/app/+videos/videos.module.ts
@@ -9,7 +9,7 @@ import { OverviewService } from './video-list'
9import { VideoOverviewComponent } from './video-list/overview/video-overview.component' 9import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
10import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component' 10import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
11import { VideoHotComponent } from './video-list/trending/video-hot.component' 11import { VideoHotComponent } from './video-list/trending/video-hot.component'
12import { VideoTrendingComponent } from './video-list/trending/video-trending.component' 12import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
13import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component' 13import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
14import { VideoLocalComponent } from './video-list/video-local.component' 14import { VideoLocalComponent } from './video-list/video-local.component'
15import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component' 15import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
@@ -32,7 +32,7 @@ import { VideosComponent } from './videos.component'
32 VideosComponent, 32 VideosComponent,
33 33
34 VideoTrendingHeaderComponent, 34 VideoTrendingHeaderComponent,
35 VideoTrendingComponent, 35 VideoMostViewedComponent,
36 VideoHotComponent, 36 VideoHotComponent,
37 VideoMostLikedComponent, 37 VideoMostLikedComponent,
38 VideoRecentlyAddedComponent, 38 VideoRecentlyAddedComponent,
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts
index c4fc9995e..32dfc8f36 100644
--- a/client/src/app/core/core.module.ts
+++ b/client/src/app/core/core.module.ts
@@ -7,7 +7,6 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
7import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service' 7import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
8import { HooksService } from '@app/core/plugins/hooks.service' 8import { HooksService } from '@app/core/plugins/hooks.service'
9import { PluginService } from '@app/core/plugins/plugin.service' 9import { PluginService } from '@app/core/plugins/plugin.service'
10import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
11import { AuthService } from './auth' 10import { AuthService } from './auth'
12import { ConfirmService } from './confirm' 11import { ConfirmService } from './confirm'
13import { CheatSheetComponent } from './hotkeys' 12import { CheatSheetComponent } from './hotkeys'
@@ -16,7 +15,7 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
16import { Notifier } from './notification' 15import { Notifier } from './notification'
17import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer' 16import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
18import { RestExtractor, RestService } from './rest' 17import { RestExtractor, RestService } from './rest'
19import { LoginGuard, RedirectService, UserRightGuard } from './routing' 18import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard, TrendingGuard } from './routing'
20import { CanDeactivateGuard } from './routing/can-deactivate-guard.service' 19import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
21import { ServerConfigResolver } from './routing/server-config-resolver.service' 20import { ServerConfigResolver } from './routing/server-config-resolver.service'
22import { ScopedTokensService } from './scoped-tokens' 21import { ScopedTokensService } from './scoped-tokens'
@@ -57,6 +56,7 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra
57 LoginGuard, 56 LoginGuard,
58 UserRightGuard, 57 UserRightGuard,
59 UnloggedGuard, 58 UnloggedGuard,
59 TrendingGuard,
60 60
61 PluginService, 61 PluginService,
62 HooksService, 62 HooksService,
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts
index 239c27caf..b3985d870 100644
--- a/client/src/app/core/routing/index.ts
+++ b/client/src/app/core/routing/index.ts
@@ -8,3 +8,4 @@ export * from './redirect.service'
8export * from './server-config-resolver.service' 8export * from './server-config-resolver.service'
9export * from './unlogged-guard.service' 9export * from './unlogged-guard.service'
10export * from './user-right-guard.service' 10export * from './user-right-guard.service'
11export * from './trending-guard.service'
diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts
index 3218040bf..76e28e461 100644
--- a/client/src/app/core/routing/redirect.service.ts
+++ b/client/src/app/core/routing/redirect.service.ts
@@ -7,11 +7,14 @@ export class RedirectService {
7 // Default route could change according to the instance configuration 7 // Default route could change according to the instance configuration
8 static INIT_DEFAULT_ROUTE = '/videos/trending' 8 static INIT_DEFAULT_ROUTE = '/videos/trending'
9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE 9 static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
10 static INIT_DEFAULT_TRENDING_ROUTE = '/videos/most-viewed'
11 static DEFAULT_TRENDING_ROUTE = RedirectService.INIT_DEFAULT_TRENDING_ROUTE
10 12
11 private previousUrl: string 13 private previousUrl: string
12 private currentUrl: string 14 private currentUrl: string
13 15
14 private redirectingToHomepage = false 16 private redirectingToHomepage = false
17 private redirectingToTrending = false
15 18
16 constructor ( 19 constructor (
17 private router: Router, 20 private router: Router,
@@ -19,18 +22,28 @@ export class RedirectService {
19 ) { 22 ) {
20 // The config is first loaded from the cache so try to get the default route 23 // The config is first loaded from the cache so try to get the default route
21 const tmpConfig = this.serverService.getTmpConfig() 24 const tmpConfig = this.serverService.getTmpConfig()
22 if (tmpConfig && tmpConfig.instance && tmpConfig.instance.defaultClientRoute) { 25 if (tmpConfig && tmpConfig.instance) {
23 RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute 26 if (tmpConfig.instance.defaultClientRoute) {
27 RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
28 }
29 if (tmpConfig.instance.defaultTrendingRoute) {
30 RedirectService.DEFAULT_TRENDING_ROUTE = tmpConfig.instance.defaultTrendingRoute
31 }
24 } 32 }
25 33
26 // Load default route 34 // Load default route
27 this.serverService.getConfig() 35 this.serverService.getConfig()
28 .subscribe(config => { 36 .subscribe(config => {
29 const defaultRouteConfig = config.instance.defaultClientRoute 37 const defaultRouteConfig = config.instance.defaultClientRoute
38 const defaultTrendingConfig = config.instance.defaultTrendingRoute
30 39
31 if (defaultRouteConfig) { 40 if (defaultRouteConfig) {
32 RedirectService.DEFAULT_ROUTE = defaultRouteConfig 41 RedirectService.DEFAULT_ROUTE = defaultRouteConfig
33 } 42 }
43
44 if (defaultTrendingConfig) {
45 RedirectService.DEFAULT_TRENDING_ROUTE = defaultTrendingConfig
46 }
34 }) 47 })
35 48
36 // Track previous url 49 // Track previous url
@@ -57,6 +70,15 @@ export class RedirectService {
57 return this.redirectToHomepage() 70 return this.redirectToHomepage()
58 } 71 }
59 72
73 redirectToTrending () {
74 if (this.redirectingToTrending) return
75
76 this.redirectingToTrending = true
77
78 this.router.navigate([ RedirectService.DEFAULT_TRENDING_ROUTE ])
79 .then(() => this.redirectingToTrending = false)
80 }
81
60 redirectToHomepage (skipLocationChange = false) { 82 redirectToHomepage (skipLocationChange = false) {
61 if (this.redirectingToHomepage) return 83 if (this.redirectingToHomepage) return
62 84
diff --git a/client/src/app/core/routing/trending-guard.service.ts b/client/src/app/core/routing/trending-guard.service.ts
new file mode 100644
index 000000000..7db7fe994
--- /dev/null
+++ b/client/src/app/core/routing/trending-guard.service.ts
@@ -0,0 +1,14 @@
1import { Injectable } from '@angular/core'
2import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'
3import { RedirectService } from './redirect.service'
4
5@Injectable()
6export class TrendingGuard implements CanActivate {
7
8 constructor (private redirectService: RedirectService) {}
9
10 canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
11 this.redirectService.redirectToTrending()
12 return false
13 }
14}
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts
index b1d8fcf83..5f13190b4 100644
--- a/client/src/app/core/server/server.service.ts
+++ b/client/src/app/core/server/server.service.ts
@@ -36,9 +36,15 @@ export class ServerService {
36 name: 'PeerTube', 36 name: 'PeerTube',
37 shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + 37 shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' +
38 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', 38 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.',
39 defaultClientRoute: '',
40 isNSFW: false, 39 isNSFW: false,
41 defaultNSFWPolicy: 'do_not_list' as 'do_not_list', 40 defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
41 defaultClientRoute: '',
42 defaultTrendingRoute: '',
43 pages: {
44 hot: {
45 enabled: true
46 }
47 },
42 customizations: { 48 customizations: {
43 javascript: '', 49 javascript: '',
44 css: '' 50 css: ''
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index 9aa397edd..fc57b970b 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -127,10 +127,14 @@
127 <ng-container i18n>Discover</ng-container> 127 <ng-container i18n>Discover</ng-container>
128 </a> 128 </a>
129 129
130 <a routerLink="/videos/trending" routerLinkActive="active"> 130 <a routerLink="/videos/trending" routerLinkActive="active" [ngClass]="{ 'active': hot.isActive || mostViewed.isActive || mostLiked.isActive }">
131 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon> 131 <my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
132 <ng-container i18n>Trending</ng-container> 132 <ng-container i18n>Trending</ng-container>
133 </a> 133 </a>
134 <a routerLink="/videos/hot" routerLinkActive #hot="routerLinkActive" hidden></a>
135 <a routerLink="/videos/most-viewed" routerLinkActive #mostViewed="routerLinkActive" hidden></a>
136 <a routerLink="/videos/most-liked" routerLinkActive #mostLiked="routerLinkActive" hidden></a>
137
134 138
135 <a routerLink="/videos/recently-added" routerLinkActive="active"> 139 <a routerLink="/videos/recently-added" routerLinkActive="active">
136 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon> 140 <my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
diff --git a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
index 2eaf0dc70..0a8aa8fa4 100644
--- a/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
+++ b/client/src/app/shared/shared-video-miniature/abstract-video-list.scss
@@ -5,7 +5,7 @@
5 5
6$iconSize: 16px; 6$iconSize: 16px;
7 7
8::ng-deep .title-page.title-page-single { 8::ng-deep my-video-list-header {
9 display: flex; 9 display: flex;
10 flex-grow: 1; 10 flex-grow: 1;
11} 11}
diff --git a/client/src/app/shared/shared-video-miniature/video-list-header.component.html b/client/src/app/shared/shared-video-miniature/video-list-header.component.html
new file mode 100644
index 000000000..58db437b8
--- /dev/null
+++ b/client/src/app/shared/shared-video-miniature/video-list-header.component.html
@@ -0,0 +1,5 @@
1<h1 class="title-page title-page-single">
2 <div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
3 {{ data.titlePage }}
4 </div>
5</h1> \ No newline at end of file
diff --git a/client/src/app/shared/shared-video-miniature/video-list-header.component.ts b/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
index a07248b96..67bbf7d7a 100644
--- a/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
+++ b/client/src/app/shared/shared-video-miniature/video-list-header.component.ts
@@ -1,17 +1,13 @@
1import { Component, Inject } from '@angular/core' 1import { Component, Inject, ViewEncapsulation } from '@angular/core'
2 2
3export abstract class GenericHeaderComponent { 3export abstract class GenericHeaderComponent {
4 constructor (@Inject('data') public data: any) {} 4 constructor (@Inject('data') public data: any) {}
5} 5}
6 6
7@Component({ 7@Component({
8 selector: 'h1', 8 selector: 'my-video-list-header',
9 host: { 'class': 'title-page title-page-single' }, 9 encapsulation: ViewEncapsulation.None,
10 template: ` 10 templateUrl: './video-list-header.component.html'
11<div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
12 {{ data.titlePage }}
13</div>
14 `
15}) 11})
16export class VideoListHeaderComponent extends GenericHeaderComponent { 12export class VideoListHeaderComponent extends GenericHeaderComponent {
17 constructor (@Inject('data') public data: any) { 13 constructor (@Inject('data') public data: any) {
diff --git a/config/default.yaml b/config/default.yaml
index e4a5ee727..43c7f4a53 100644
--- a/config/default.yaml
+++ b/config/default.yaml
@@ -366,6 +366,10 @@ instance:
366# - 18 # Food 366# - 18 # Food
367 367
368 default_client_route: '/videos/trending' 368 default_client_route: '/videos/trending'
369 default_trending_route: '/videos/most-viewed'
370 pages:
371 hot:
372 enabled: true
369 373
370 # Whether or not the instance is dedicated to NSFW content 374 # Whether or not the instance is dedicated to NSFW content
371 # Enabling it will allow other administrators to know that you are mainly federating sensitive content 375 # Enabling it will allow other administrators to know that you are mainly federating sensitive content
diff --git a/config/production.yaml.example b/config/production.yaml.example
index f7b56cc4a..f9f3abc18 100644
--- a/config/production.yaml.example
+++ b/config/production.yaml.example
@@ -380,6 +380,10 @@ instance:
380# - 18 # Food 380# - 18 # Food
381 381
382 default_client_route: '/videos/trending' 382 default_client_route: '/videos/trending'
383 default_trending_route: '/videos/most-viewed'
384 pages:
385 hot:
386 enabled: true
383 387
384 # Whether or not the instance is dedicated to NSFW content 388 # Whether or not the instance is dedicated to NSFW content
385 # Enabling it will allow other administrators to know that you are mainly federating sensitive content 389 # Enabling it will allow other administrators to know that you are mainly federating sensitive content
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 44f3d3ef7..24e7601ec 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -65,9 +65,15 @@ async function getConfig (req: express.Request, res: express.Response) {
65 instance: { 65 instance: {
66 name: CONFIG.INSTANCE.NAME, 66 name: CONFIG.INSTANCE.NAME,
67 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 67 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
68 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
69 isNSFW: CONFIG.INSTANCE.IS_NSFW, 68 isNSFW: CONFIG.INSTANCE.IS_NSFW,
70 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 69 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
70 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
71 defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
72 pages: {
73 hot: {
74 enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
75 }
76 },
71 customizations: { 77 customizations: {
72 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, 78 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
73 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 79 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
@@ -362,8 +368,16 @@ function customConfig (): CustomConfig {
362 categories: CONFIG.INSTANCE.CATEGORIES, 368 categories: CONFIG.INSTANCE.CATEGORIES,
363 369
364 isNSFW: CONFIG.INSTANCE.IS_NSFW, 370 isNSFW: CONFIG.INSTANCE.IS_NSFW,
365 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
366 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, 371 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
372
373 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
374 defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
375 pages: {
376 hot: {
377 enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
378 }
379 },
380
367 customizations: { 381 customizations: {
368 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, 382 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
369 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT 383 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 6aae5e821..e474959b2 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -230,6 +230,7 @@ const customConfigKeysToKeep = [
230 'instance-description', 230 'instance-description',
231 'instance-terms', 231 'instance-terms',
232 'instance-defaultClientRoute', 232 'instance-defaultClientRoute',
233 'instance-defaultTrendingRoute',
233 'instance-defaultNSFWPolicy', 234 'instance-defaultNSFWPolicy',
234 'instance-customizations-javascript', 235 'instance-customizations-javascript',
235 'instance-customizations-css', 236 'instance-customizations-css',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index c7ef9b497..e1f807752 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -278,8 +278,16 @@ const CONFIG = {
278 get CATEGORIES () { return config.get<number[]>('instance.categories') || [] }, 278 get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
279 279
280 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') }, 280 get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
281 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
282 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') }, 281 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
282
283 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
284 get DEFAULT_TRENDING_ROUTE () { return config.get<string>('instance.default_trending_route') },
285 PAGES: {
286 HOT: {
287 get ENABLED () { return config.get<boolean>('instance.pages.hot.enabled') }
288 }
289 },
290
283 CUSTOMIZATIONS: { 291 CUSTOMIZATIONS: {
284 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, 292 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
285 get CSS () { return config.get<string>('instance.customizations.css') } 293 get CSS () { return config.get<string>('instance.customizations.css') }
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index faabf17d7..0efe1157f 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -15,8 +15,9 @@ const customConfigUpdateValidator = [
15 body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'), 15 body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
16 body('instance.description').exists().withMessage('Should have a valid instance description'), 16 body('instance.description').exists().withMessage('Should have a valid instance description'),
17 body('instance.terms').exists().withMessage('Should have a valid instance terms'), 17 body('instance.terms').exists().withMessage('Should have a valid instance terms'),
18 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
19 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), 18 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
19 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
20 body('instance.defaultTrendingRoute').exists().withMessage('Should have a valid instance default trending route'),
20 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), 21 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
21 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), 22 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
22 23
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 8e0965244..3f31ac862 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -242,64 +242,49 @@ function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions)
242 } 242 }
243 243
244 // We don't exclude results in this so if we do a count we don't need to add this complex clause 244 // We don't exclude results in this so if we do a count we don't need to add this complex clause
245 if (options.trendingDays && options.isCount !== true) { 245 if (options.isCount !== true) {
246 const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays) 246 if (options.trendingDays) {
247 247 const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
248 joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate') 248
249 replacements.viewsGteDate = viewsGteDate 249 joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
250 250 replacements.viewsGteDate = viewsGteDate
251 attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"') 251
252 252 attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
253 group = 'GROUP BY "video"."id"' 253
254 } else if (options.hot && options.isCount !== true) { 254 group = 'GROUP BY "video"."id"'
255 /** 255 } else if (options.hot) {
256 * "Hotness" is a measure based on absolute view/comment/like/dislike numbers, 256 /**
257 * with fixed weights only applied to their log values. 257 * "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
258 * 258 * with fixed weights only applied to their log values.
259 * This algorithm gives little chance for an old video to have a good score, 259 *
260 * for which recent spikes in interactions could be a sign of "hotness" and 260 * This algorithm gives little chance for an old video to have a good score,
261 * justify a better score. However there are multiple ways to achieve that 261 * for which recent spikes in interactions could be a sign of "hotness" and
262 * goal, which is left for later. Yes, this is a TODO :) 262 * justify a better score. However there are multiple ways to achieve that
263 * 263 * goal, which is left for later. Yes, this is a TODO :)
264 * note: weights and base score are in number of half-days. 264 *
265 * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58 265 * note: weights and base score are in number of half-days.
266 */ 266 * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58
267 const weights = { 267 */
268 like: 3, 268 const weights = {
269 dislike: 3, 269 like: 3,
270 view: 1 / 12, 270 dislike: 3,
271 comment: 2 // a comment takes more time than a like to do, but can be done multiple times 271 view: 1 / 12,
272 } 272 comment: 2 // a comment takes more time than a like to do, but can be done multiple times
273 273 }
274 cte.push( // TODO: exclude blocklisted comments
275 '"totalCommentsWithoutVideoAuthor" AS (' +
276 'SELECT "video"."id", ' +
277 'COUNT("replies"."id") - (' +
278 'SELECT COUNT("authorReplies"."id") ' +
279 'FROM "videoComment" AS "authorReplies" ' +
280 'LEFT JOIN "account" ON "account"."id" = "authorReplies"."accountId" ' +
281 'LEFT JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
282 'WHERE "video"."channelId" = "videoChannel"."id" ' +
283 ') as "value" ' +
284 'FROM "videoComment" AS "replies" ' +
285 'LEFT JOIN "video" ON "video"."id" = "replies"."videoId" ' +
286 'WHERE "replies"."videoId" = "video"."id" ' +
287 'GROUP BY "video"."id"' +
288 ')'
289 )
290 274
291 joins.push('LEFT JOIN "totalCommentsWithoutVideoAuthor" ON "video"."id" = "totalCommentsWithoutVideoAuthor"."id"') 275 joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
292 276
293 attributes.push( 277 attributes.push(
294 `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+) 278 `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
295 `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-) 279 `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
296 `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+) 280 `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
297 `+ LOG(GREATEST(1, "totalCommentsWithoutVideoAuthor"."value")) * ${weights.comment} ` + // comments (+) 281 `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+)
298 '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days) 282 '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
299 'AS "score"' 283 'AS "score"'
300 ) 284 )
301 285
302 group = 'GROUP BY "video"."id", "totalCommentsWithoutVideoAuthor"."value"' 286 group = 'GROUP BY "video"."id"'
287 }
303 } 288 }
304 289
305 if (options.historyOfUser) { 290 if (options.historyOfUser) {
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index e36cdeab2..e58e0cd9f 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -41,8 +41,16 @@ describe('Test config API validators', function () {
41 categories: [ 1, 2 ], 41 categories: [ 1, 2 ],
42 42
43 isNSFW: true, 43 isNSFW: true,
44 defaultClientRoute: '/videos/recently-added',
45 defaultNSFWPolicy: 'blur', 44 defaultNSFWPolicy: 'blur',
45
46 defaultClientRoute: '/videos/recently-added',
47 defaultTrendingRoute: '/videos/trending',
48 pages: {
49 hot: {
50 enabled: true
51 }
52 },
53
46 customizations: { 54 customizations: {
47 javascript: 'alert("coucou")', 55 javascript: 'alert("coucou")',
48 css: 'body { background-color: red; }' 56 css: 'body { background-color: red; }'
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index af25f4800..328f4852a 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -272,9 +272,17 @@ describe('Test config', function () {
272 languages: [ 'en', 'es' ], 272 languages: [ 'en', 'es' ],
273 categories: [ 1, 2 ], 273 categories: [ 1, 2 ],
274 274
275 defaultClientRoute: '/videos/recently-added',
276 isNSFW: true, 275 isNSFW: true,
277 defaultNSFWPolicy: 'blur' as 'blur', 276 defaultNSFWPolicy: 'blur' as 'blur',
277
278 defaultClientRoute: '/videos/recently-added',
279 defaultTrendingRoute: '/videos/trending',
280 pages: {
281 hot: {
282 enabled: true
283 }
284 },
285
278 customizations: { 286 customizations: {
279 javascript: 'alert("coucou")', 287 javascript: 'alert("coucou")',
280 css: 'body { background-color: red; }' 288 css: 'body { background-color: red; }'
diff --git a/server/tests/client.ts b/server/tests/client.ts
index 7572fd34a..d608764ee 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -308,8 +308,8 @@ describe('Test a client controllers', function () {
308 shortDescription: 'my short description', 308 shortDescription: 'my short description',
309 description: 'my super description', 309 description: 'my super description',
310 terms: 'my super terms', 310 terms: 'my super terms',
311 defaultClientRoute: '/videos/recently-added',
312 defaultNSFWPolicy: 'blur', 311 defaultNSFWPolicy: 'blur',
312 defaultClientRoute: '/videos/recently-added',
313 customizations: { 313 customizations: {
314 javascript: 'alert("coucou")', 314 javascript: 'alert("coucou")',
315 css: 'body { background-color: red; }' 315 css: 'body { background-color: red; }'
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
index f7c488c0b..5152ec693 100644
--- a/shared/extra-utils/server/config.ts
+++ b/shared/extra-utils/server/config.ts
@@ -65,9 +65,17 @@ function updateCustomSubConfig (url: string, token: string, newConfig: DeepParti
65 languages: [ 'en', 'es' ], 65 languages: [ 'en', 'es' ],
66 categories: [ 1, 2 ], 66 categories: [ 1, 2 ],
67 67
68 defaultClientRoute: '/videos/recently-added',
69 isNSFW: true, 68 isNSFW: true,
70 defaultNSFWPolicy: 'blur', 69 defaultNSFWPolicy: 'blur',
70
71 defaultClientRoute: '/videos/recently-added',
72 defaultTrendingRoute: '/videos/trending',
73 pages: {
74 hot: {
75 enabled: true
76 }
77 },
78
71 customizations: { 79 customizations: {
72 javascript: 'alert("coucou")', 80 javascript: 'alert("coucou")',
73 css: 'body { background-color: red; }' 81 css: 'body { background-color: red; }'
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts
index 9a6a24923..fcc29e5d7 100644
--- a/shared/models/server/custom-config.model.ts
+++ b/shared/models/server/custom-config.model.ts
@@ -30,8 +30,16 @@ export interface CustomConfig {
30 categories: number[] 30 categories: number[]
31 31
32 isNSFW: boolean 32 isNSFW: boolean
33 defaultClientRoute: string
34 defaultNSFWPolicy: NSFWPolicyType 33 defaultNSFWPolicy: NSFWPolicyType
34
35 defaultClientRoute: string
36 defaultTrendingRoute: string
37 pages: {
38 hot: {
39 enabled: boolean
40 }
41 }
42
35 customizations: { 43 customizations: {
36 javascript?: string 44 javascript?: string
37 css?: string 45 css?: string
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts
index 2dcf98f4f..a2d93ce73 100644
--- a/shared/models/server/server-config.model.ts
+++ b/shared/models/server/server-config.model.ts
@@ -36,9 +36,15 @@ export interface ServerConfig {
36 instance: { 36 instance: {
37 name: string 37 name: string
38 shortDescription: string 38 shortDescription: string
39 defaultClientRoute: string
40 isNSFW: boolean 39 isNSFW: boolean
41 defaultNSFWPolicy: NSFWPolicyType 40 defaultNSFWPolicy: NSFWPolicyType
41 defaultClientRoute: string
42 defaultTrendingRoute: string
43 pages: {
44 hot: {
45 enabled: boolean
46 }
47 }
42 customizations: { 48 customizations: {
43 javascript: string 49 javascript: string
44 css: string 50 css: string