<div class="peertube-select-container">
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute" class="form-control">
<option i18n value="/videos/overview">Discover videos</option>
- <option i18n value="/videos/trending">Trending videos</option>
- <option i18n value="/videos/hot">Hot videos</option>
- <option i18n value="/videos/most-liked">Most liked videos</option>
+ <optgroup i18n-label label="Trending pages">
+ <option i18n value="/videos/trending">Default trending page</option>
+ <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
+ <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
+ <option i18n value="/videos/most-viewed">Most viewed videos</option>
+ <option i18n value="/videos/most-liked">Most liked videos</option>
+ </optgroup>
<option i18n value="/videos/recently-added">Recently added videos</option>
<option i18n value="/videos/local">Local videos</option>
</select>
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
</div>
+ <div class="form-group" formGroupName="instance">
+ <label i18n for="instanceDefaultTrendingRoute">Default trending page</label>
+ <div class="peertube-select-container">
+ <select id="instanceDefaultTrendingRoute" formControlName="defaultTrendingRoute" class="form-control">
+ <option i18n value="/videos/hot" *ngIf="isTrendingHotEnabled()">Hot videos</option>
+ <option i18n value="/videos/hot" *ngIf="!isTrendingHotEnabled()" disabled>Hot videos</option>
+ <option i18n value="/videos/trending">Most viewed videos</option>
+ <option i18n value="/videos/most-liked">Most liked videos</option>
+ </select>
+ </div>
+ <div *ngIf="formErrors.instance.defaultTrendingRoute" class="form-error">{{ formErrors.instance.defaultTrendingRoute }}</div>
+ </div>
+
</div>
</div>
languages: null,
defaultClientRoute: null,
+ defaultTrendingRoute: null,
+ pages: {
+ hot: {
+ enabled: null
+ }
+ },
customizations: {
javascript: null,
return this.form.value['followings']['instance']['autoFollowIndex']['enabled'] === true
}
+ isTrendingHotEnabled () {
+ return this.form.value['instance']['pages']['hot']['enabled'] === true
+ }
+
async formValidated () {
const value: CustomConfig = this.form.getRawValue()
export * from './video-trending-header.component'
-export * from './video-trending.component'
export * from './video-hot.component'
+export * from './video-most-viewed.component'
export * from './video-most-liked.component'
import { VideoTrendingHeaderComponent } from './video-trending-header.component'
@Component({
- selector: 'my-videos-trending',
+ selector: 'my-videos-most-viewed',
styleUrls: [ '../../../shared/shared-video-miniature/abstract-video-list.scss' ],
templateUrl: '../../../shared/shared-video-miniature/abstract-video-list.html'
})
-export class VideoTrendingComponent extends AbstractVideoList implements OnInit, OnDestroy {
+export class VideoMostViewedComponent extends AbstractVideoList implements OnInit, OnDestroy {
HeaderComponent = VideoTrendingHeaderComponent
titlePage: string
defaultSort: VideoSortField = '-trending'
<div class="btn-group btn-group-toggle" ngbRadioGroup name="radioBasic" [(ngModel)]="data.model" (ngModelChange)="setSort()">
- <label *ngFor="let button of buttons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
+ <label *ngFor="let button of visibleButtons" ngbButtonLabel class="btn-light" placement="bottom" [ngbTooltip]="button.tooltip" container="body">
<my-global-icon [iconName]="button.iconName"></my-global-icon>
<input ngbButton type="radio" [value]="button.value"> {{ button.label }}
</label>
-import { Component, Inject } from '@angular/core'
+import { Component, Inject, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { VideoListHeaderComponent } from '@app/shared/shared-video-miniature'
import { GlobalIconName } from '@app/shared/shared-icons'
import { VideoSortField } from '@shared/models'
+import { ServerService } from '@app/core/server/server.service'
interface VideoTrendingHeaderItem {
label: string
value: VideoSortField
path: string
tooltip?: string
+ hidden?: boolean
}
@Component({
styleUrls: [ './video-trending-header.component.scss' ],
templateUrl: './video-trending-header.component.html'
})
-export class VideoTrendingHeaderComponent extends VideoListHeaderComponent {
+export class VideoTrendingHeaderComponent extends VideoListHeaderComponent implements OnInit {
buttons: VideoTrendingHeaderItem[]
constructor (
@Inject('data') public data: any,
- private router: Router
+ private router: Router,
+ private serverService: ServerService
) {
super(data)
value: '-hot',
path: 'hot',
tooltip: $localize`Videos totalizing the most interactions for recent videos`,
+ hidden: true
},
{
label: $localize`:Main variant of Trending videos based on number of recent views:Views`,
iconName: 'trending',
value: '-trending',
- path: 'trending',
+ path: 'most-viewed',
tooltip: $localize`Videos totalizing the most views during the last 24 hours`,
},
{
- label: $localize`:a variant of Trending videos based on the number of likes:Likes`,
+ label: $localize`:A variant of Trending videos based on the number of likes:Likes`,
iconName: 'like',
value: '-likes',
path: 'most-liked',
]
}
+ ngOnInit () {
+ this.serverService.getConfig()
+ .subscribe(config => {
+ // don't filter if auto-blacklist is not enabled as this will be the only list
+ if (config.instance.pages.hot.enabled) {
+ const index = this.buttons.findIndex(b => b.path === 'hot')
+ this.buttons[index].hidden = false
+ }
+ })
+ }
+
+ get visibleButtons () {
+ return this.buttons.filter(b => !b.hidden)
+ }
+
setSort () {
const path = this.buttons.find(b => b.value === this.data.model).path
this.router.navigate([ `/videos/${path}` ])
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
-import { LoginGuard } from '@app/core'
+import { LoginGuard, TrendingGuard } from '@app/core'
import { MetaGuard } from '@ngx-meta/core'
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
import { VideoHotComponent } from './video-list/trending/video-hot.component'
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
-import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
+import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
import { VideoLocalComponent } from './video-list/video-local.component'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
import { VideoUserSubscriptionsComponent } from './video-list/video-user-subscriptions.component'
},
{
path: 'trending',
- component: VideoTrendingComponent,
+ canActivate: [ TrendingGuard ]
+ },
+ {
+ path: 'hot',
+ component: VideoHotComponent,
data: {
meta: {
- title: $localize`Trending videos`
+ title: $localize`Hot videos`
},
reuse: {
enabled: true,
- key: 'trending-videos-list'
+ key: 'hot-videos-list'
}
}
},
{
- path: 'hot',
- component: VideoHotComponent,
+ path: 'most-viewed',
+ component: VideoMostViewedComponent,
data: {
meta: {
- title: $localize`Hot videos`
+ title: $localize`Most viewed videos`
},
reuse: {
enabled: true,
- key: 'hot-videos-list'
+ key: 'most-viewed-videos-list'
}
}
},
import { VideoOverviewComponent } from './video-list/overview/video-overview.component'
import { VideoTrendingHeaderComponent } from './video-list/trending/video-trending-header.component'
import { VideoHotComponent } from './video-list/trending/video-hot.component'
-import { VideoTrendingComponent } from './video-list/trending/video-trending.component'
+import { VideoMostViewedComponent } from './video-list/trending/video-most-viewed.component'
import { VideoMostLikedComponent } from './video-list/trending/video-most-liked.component'
import { VideoLocalComponent } from './video-list/video-local.component'
import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
VideosComponent,
VideoTrendingHeaderComponent,
- VideoTrendingComponent,
+ VideoMostViewedComponent,
VideoHotComponent,
VideoMostLikedComponent,
VideoRecentlyAddedComponent,
import { PeerTubeSocket } from '@app/core/notification/peertube-socket.service'
import { HooksService } from '@app/core/plugins/hooks.service'
import { PluginService } from '@app/core/plugins/plugin.service'
-import { UnloggedGuard } from '@app/core/routing/unlogged-guard.service'
import { AuthService } from './auth'
import { ConfirmService } from './confirm'
import { CheatSheetComponent } from './hotkeys'
import { Notifier } from './notification'
import { HtmlRendererService, LinkifierService, MarkdownService } from './renderer'
import { RestExtractor, RestService } from './rest'
-import { LoginGuard, RedirectService, UserRightGuard } from './routing'
+import { LoginGuard, RedirectService, UserRightGuard, UnloggedGuard, TrendingGuard } from './routing'
import { CanDeactivateGuard } from './routing/can-deactivate-guard.service'
import { ServerConfigResolver } from './routing/server-config-resolver.service'
import { ScopedTokensService } from './scoped-tokens'
LoginGuard,
UserRightGuard,
UnloggedGuard,
+ TrendingGuard,
PluginService,
HooksService,
export * from './server-config-resolver.service'
export * from './unlogged-guard.service'
export * from './user-right-guard.service'
+export * from './trending-guard.service'
// Default route could change according to the instance configuration
static INIT_DEFAULT_ROUTE = '/videos/trending'
static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
+ static INIT_DEFAULT_TRENDING_ROUTE = '/videos/most-viewed'
+ static DEFAULT_TRENDING_ROUTE = RedirectService.INIT_DEFAULT_TRENDING_ROUTE
private previousUrl: string
private currentUrl: string
private redirectingToHomepage = false
+ private redirectingToTrending = false
constructor (
private router: Router,
) {
// The config is first loaded from the cache so try to get the default route
const tmpConfig = this.serverService.getTmpConfig()
- if (tmpConfig && tmpConfig.instance && tmpConfig.instance.defaultClientRoute) {
- RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+ if (tmpConfig && tmpConfig.instance) {
+ if (tmpConfig.instance.defaultClientRoute) {
+ RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+ }
+ if (tmpConfig.instance.defaultTrendingRoute) {
+ RedirectService.DEFAULT_TRENDING_ROUTE = tmpConfig.instance.defaultTrendingRoute
+ }
}
// Load default route
this.serverService.getConfig()
.subscribe(config => {
const defaultRouteConfig = config.instance.defaultClientRoute
+ const defaultTrendingConfig = config.instance.defaultTrendingRoute
if (defaultRouteConfig) {
RedirectService.DEFAULT_ROUTE = defaultRouteConfig
}
+
+ if (defaultTrendingConfig) {
+ RedirectService.DEFAULT_TRENDING_ROUTE = defaultTrendingConfig
+ }
})
// Track previous url
return this.redirectToHomepage()
}
+ redirectToTrending () {
+ if (this.redirectingToTrending) return
+
+ this.redirectingToTrending = true
+
+ this.router.navigate([ RedirectService.DEFAULT_TRENDING_ROUTE ])
+ .then(() => this.redirectingToTrending = false)
+ }
+
redirectToHomepage (skipLocationChange = false) {
if (this.redirectingToHomepage) return
--- /dev/null
+import { Injectable } from '@angular/core'
+import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'
+import { RedirectService } from './redirect.service'
+
+@Injectable()
+export class TrendingGuard implements CanActivate {
+
+ constructor (private redirectService: RedirectService) {}
+
+ canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+ this.redirectService.redirectToTrending()
+ return false
+ }
+}
name: 'PeerTube',
shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' +
'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.',
- defaultClientRoute: '',
isNSFW: false,
defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
+ defaultClientRoute: '',
+ defaultTrendingRoute: '',
+ pages: {
+ hot: {
+ enabled: true
+ }
+ },
customizations: {
javascript: '',
css: ''
<ng-container i18n>Discover</ng-container>
</a>
- <a routerLink="/videos/trending" routerLinkActive="active">
+ <a routerLink="/videos/trending" routerLinkActive="active" [ngClass]="{ 'active': hot.isActive || mostViewed.isActive || mostLiked.isActive }">
<my-global-icon iconName="trending" aria-hidden="true"></my-global-icon>
<ng-container i18n>Trending</ng-container>
</a>
+ <a routerLink="/videos/hot" routerLinkActive #hot="routerLinkActive" hidden></a>
+ <a routerLink="/videos/most-viewed" routerLinkActive #mostViewed="routerLinkActive" hidden></a>
+ <a routerLink="/videos/most-liked" routerLinkActive #mostLiked="routerLinkActive" hidden></a>
+
<a routerLink="/videos/recently-added" routerLinkActive="active">
<my-global-icon iconName="recently-added" aria-hidden="true"></my-global-icon>
$iconSize: 16px;
-::ng-deep .title-page.title-page-single {
+::ng-deep my-video-list-header {
display: flex;
flex-grow: 1;
}
--- /dev/null
+<h1 class="title-page title-page-single">
+ <div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
+ {{ data.titlePage }}
+ </div>
+</h1>
\ No newline at end of file
-import { Component, Inject } from '@angular/core'
+import { Component, Inject, ViewEncapsulation } from '@angular/core'
export abstract class GenericHeaderComponent {
constructor (@Inject('data') public data: any) {}
}
@Component({
- selector: 'h1',
- host: { 'class': 'title-page title-page-single' },
- template: `
-<div placement="bottom" [ngbTooltip]="data.titleTooltip" container="body">
- {{ data.titlePage }}
-</div>
- `
+ selector: 'my-video-list-header',
+ encapsulation: ViewEncapsulation.None,
+ templateUrl: './video-list-header.component.html'
})
export class VideoListHeaderComponent extends GenericHeaderComponent {
constructor (@Inject('data') public data: any) {
# - 18 # Food
default_client_route: '/videos/trending'
+ default_trending_route: '/videos/most-viewed'
+ pages:
+ hot:
+ enabled: true
# Whether or not the instance is dedicated to NSFW content
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
# - 18 # Food
default_client_route: '/videos/trending'
+ default_trending_route: '/videos/most-viewed'
+ pages:
+ hot:
+ enabled: true
# Whether or not the instance is dedicated to NSFW content
# Enabling it will allow other administrators to know that you are mainly federating sensitive content
instance: {
name: CONFIG.INSTANCE.NAME,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
- defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
isNSFW: CONFIG.INSTANCE.IS_NSFW,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
+ defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
+ defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
+ pages: {
+ hot: {
+ enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
+ }
+ },
customizations: {
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
categories: CONFIG.INSTANCE.CATEGORIES,
isNSFW: CONFIG.INSTANCE.IS_NSFW,
- defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
+
+ defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
+ defaultTrendingRoute: CONFIG.INSTANCE.DEFAULT_TRENDING_ROUTE,
+ pages: {
+ hot: {
+ enabled: CONFIG.INSTANCE.PAGES.HOT.ENABLED
+ }
+ },
+
customizations: {
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
'instance-description',
'instance-terms',
'instance-defaultClientRoute',
+ 'instance-defaultTrendingRoute',
'instance-defaultNSFWPolicy',
'instance-customizations-javascript',
'instance-customizations-css',
get CATEGORIES () { return config.get<number[]>('instance.categories') || [] },
get IS_NSFW () { return config.get<boolean>('instance.is_nsfw') },
- get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
+
+ get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
+ get DEFAULT_TRENDING_ROUTE () { return config.get<string>('instance.default_trending_route') },
+ PAGES: {
+ HOT: {
+ get ENABLED () { return config.get<boolean>('instance.pages.hot.enabled') }
+ }
+ },
+
CUSTOMIZATIONS: {
get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
get CSS () { return config.get<string>('instance.customizations.css') }
body('instance.shortDescription').exists().withMessage('Should have a valid instance short description'),
body('instance.description').exists().withMessage('Should have a valid instance description'),
body('instance.terms').exists().withMessage('Should have a valid instance terms'),
- body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
+ body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
+ body('instance.defaultTrendingRoute').exists().withMessage('Should have a valid instance default trending route'),
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
}
// We don't exclude results in this so if we do a count we don't need to add this complex clause
- if (options.trendingDays && options.isCount !== true) {
- const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
-
- joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
- replacements.viewsGteDate = viewsGteDate
-
- attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
-
- group = 'GROUP BY "video"."id"'
- } else if (options.hot && options.isCount !== true) {
- /**
- * "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
- * with fixed weights only applied to their log values.
- *
- * This algorithm gives little chance for an old video to have a good score,
- * for which recent spikes in interactions could be a sign of "hotness" and
- * justify a better score. However there are multiple ways to achieve that
- * goal, which is left for later. Yes, this is a TODO :)
- *
- * note: weights and base score are in number of half-days.
- * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58
- */
- const weights = {
- like: 3,
- dislike: 3,
- view: 1 / 12,
- comment: 2 // a comment takes more time than a like to do, but can be done multiple times
- }
-
- cte.push( // TODO: exclude blocklisted comments
- '"totalCommentsWithoutVideoAuthor" AS (' +
- 'SELECT "video"."id", ' +
- 'COUNT("replies"."id") - (' +
- 'SELECT COUNT("authorReplies"."id") ' +
- 'FROM "videoComment" AS "authorReplies" ' +
- 'LEFT JOIN "account" ON "account"."id" = "authorReplies"."accountId" ' +
- 'LEFT JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
- 'WHERE "video"."channelId" = "videoChannel"."id" ' +
- ') as "value" ' +
- 'FROM "videoComment" AS "replies" ' +
- 'LEFT JOIN "video" ON "video"."id" = "replies"."videoId" ' +
- 'WHERE "replies"."videoId" = "video"."id" ' +
- 'GROUP BY "video"."id"' +
- ')'
- )
+ if (options.isCount !== true) {
+ if (options.trendingDays) {
+ const viewsGteDate = new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
+
+ joins.push('LEFT JOIN "videoView" ON "video"."id" = "videoView"."videoId" AND "videoView"."startDate" >= :viewsGteDate')
+ replacements.viewsGteDate = viewsGteDate
+
+ attributes.push('COALESCE(SUM("videoView"."views"), 0) AS "score"')
+
+ group = 'GROUP BY "video"."id"'
+ } else if (options.hot) {
+ /**
+ * "Hotness" is a measure based on absolute view/comment/like/dislike numbers,
+ * with fixed weights only applied to their log values.
+ *
+ * This algorithm gives little chance for an old video to have a good score,
+ * for which recent spikes in interactions could be a sign of "hotness" and
+ * justify a better score. However there are multiple ways to achieve that
+ * goal, which is left for later. Yes, this is a TODO :)
+ *
+ * note: weights and base score are in number of half-days.
+ * see https://github.com/reddit-archive/reddit/blob/master/r2/r2/lib/db/_sorts.pyx#L47-L58
+ */
+ const weights = {
+ like: 3,
+ dislike: 3,
+ view: 1 / 12,
+ comment: 2 // a comment takes more time than a like to do, but can be done multiple times
+ }
- joins.push('LEFT JOIN "totalCommentsWithoutVideoAuthor" ON "video"."id" = "totalCommentsWithoutVideoAuthor"."id"')
+ joins.push('LEFT JOIN "videoComment" ON "video"."id" = "videoComment"."videoId"')
- attributes.push(
- `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
- `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
- `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
- `+ LOG(GREATEST(1, "totalCommentsWithoutVideoAuthor"."value")) * ${weights.comment} ` + // comments (+)
- '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
- 'AS "score"'
- )
+ attributes.push(
+ `LOG(GREATEST(1, "video"."likes" - 1)) * ${weights.like} ` + // likes (+)
+ `- LOG(GREATEST(1, "video"."dislikes" - 1)) * ${weights.dislike} ` + // dislikes (-)
+ `+ LOG("video"."views" + 1) * ${weights.view} ` + // views (+)
+ `+ LOG(GREATEST(1, COUNT(DISTINCT "videoComment"."id"))) * ${weights.comment} ` + // comments (+)
+ '+ (SELECT EXTRACT(epoch FROM "video"."publishedAt") / 47000) ' + // base score (in number of half-days)
+ 'AS "score"'
+ )
- group = 'GROUP BY "video"."id", "totalCommentsWithoutVideoAuthor"."value"'
+ group = 'GROUP BY "video"."id"'
+ }
}
if (options.historyOfUser) {
categories: [ 1, 2 ],
isNSFW: true,
- defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur',
+
+ defaultClientRoute: '/videos/recently-added',
+ defaultTrendingRoute: '/videos/trending',
+ pages: {
+ hot: {
+ enabled: true
+ }
+ },
+
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
languages: [ 'en', 'es' ],
categories: [ 1, 2 ],
- defaultClientRoute: '/videos/recently-added',
isNSFW: true,
defaultNSFWPolicy: 'blur' as 'blur',
+
+ defaultClientRoute: '/videos/recently-added',
+ defaultTrendingRoute: '/videos/trending',
+ pages: {
+ hot: {
+ enabled: true
+ }
+ },
+
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
shortDescription: 'my short description',
description: 'my super description',
terms: 'my super terms',
- defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur',
+ defaultClientRoute: '/videos/recently-added',
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
languages: [ 'en', 'es' ],
categories: [ 1, 2 ],
- defaultClientRoute: '/videos/recently-added',
isNSFW: true,
defaultNSFWPolicy: 'blur',
+
+ defaultClientRoute: '/videos/recently-added',
+ defaultTrendingRoute: '/videos/trending',
+ pages: {
+ hot: {
+ enabled: true
+ }
+ },
+
customizations: {
javascript: 'alert("coucou")',
css: 'body { background-color: red; }'
categories: number[]
isNSFW: boolean
- defaultClientRoute: string
defaultNSFWPolicy: NSFWPolicyType
+
+ defaultClientRoute: string
+ defaultTrendingRoute: string
+ pages: {
+ hot: {
+ enabled: boolean
+ }
+ }
+
customizations: {
javascript?: string
css?: string
instance: {
name: string
shortDescription: string
- defaultClientRoute: string
isNSFW: boolean
defaultNSFWPolicy: NSFWPolicyType
+ defaultClientRoute: string
+ defaultTrendingRoute: string
+ pages: {
+ hot: {
+ enabled: boolean
+ }
+ }
customizations: {
javascript: string
css: string