<div i18n class="account-title">Profile</div>
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
+<div i18n class="account-title">Video settings</div>
+<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
+
<div i18n class="account-title" id="notifications">Notifications</div>
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
<div i18n class="account-title">Email</div>
<my-account-change-email></my-account-change-email>
-<div i18n class="account-title">Video settings</div>
-<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
-
<div i18n class="account-title">Danger zone</div>
<my-account-danger-zone [user]="user"></my-account-danger-zone>
</div>
</div>
+ <div class="form-group">
+ <label i18n for="videoLanguages">Only display videos in the following languages</label>
+ <my-help i18n-customHtml
+ customHtml="In Recently added, Trending, Local and Search pages"
+ ></my-help>
+
+ <div>
+ <p-multiSelect
+ [options]="languageItems" formControlName="videoLanguages" showToggleAll="true"
+ [defaultLabel]="getDefaultVideoLanguageLabel()" [selectedItemsLabel]="getSelectedVideoLanguageLabel()"
+ emptyFilterMessage="No results found" i18n-emptyFilterMessage
+ ></p-multiSelect>
+ </div>
+ </div>
+
<div class="form-group">
<my-peertube-checkbox
inputName="webTorrentEnabled" formControlName="webTorrentEnabled"
import { Component, Input, OnInit } from '@angular/core'
-import { Notifier } from '@app/core'
+import { Notifier, ServerService } from '@app/core'
import { UserUpdateMe } from '../../../../../../shared'
import { AuthService } from '../../../core'
import { FormReactive, User, UserService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { Subject } from 'rxjs'
+import { SelectItem } from 'primeng/api'
+import { switchMap } from 'rxjs/operators'
@Component({
selector: 'my-account-video-settings',
@Input() user: User = null
@Input() userInformationLoaded: Subject<any>
+ languageItems: SelectItem[] = []
+
constructor (
protected formValidatorService: FormValidatorService,
private authService: AuthService,
private notifier: Notifier,
private userService: UserService,
+ private serverService: ServerService,
private i18n: I18n
) {
super()
this.buildForm({
nsfwPolicy: null,
webTorrentEnabled: null,
- autoPlayVideo: null
+ autoPlayVideo: null,
+ videoLanguages: null
})
- this.userInformationLoaded.subscribe(() => {
- this.form.patchValue({
- nsfwPolicy: this.user.nsfwPolicy,
- webTorrentEnabled: this.user.webTorrentEnabled,
- autoPlayVideo: this.user.autoPlayVideo === true
- })
- })
+ this.serverService.videoLanguagesLoaded
+ .pipe(switchMap(() => this.userInformationLoaded))
+ .subscribe(() => {
+ const languages = this.serverService.getVideoLanguages()
+
+ this.languageItems = [ { label: this.i18n('Unknown language'), value: '_unknown' } ]
+ this.languageItems = this.languageItems
+ .concat(languages.map(l => ({ label: l.label, value: l.id })))
+
+ const videoLanguages = this.user.videoLanguages
+ ? this.user.videoLanguages
+ : this.languageItems.map(l => l.value)
+
+ this.form.patchValue({
+ nsfwPolicy: this.user.nsfwPolicy,
+ webTorrentEnabled: this.user.webTorrentEnabled,
+ autoPlayVideo: this.user.autoPlayVideo === true,
+ videoLanguages
+ })
+ })
}
updateDetails () {
const nsfwPolicy = this.form.value['nsfwPolicy']
const webTorrentEnabled = this.form.value['webTorrentEnabled']
const autoPlayVideo = this.form.value['autoPlayVideo']
+
+ let videoLanguages: string[] = this.form.value['videoLanguages']
+ if (Array.isArray(videoLanguages)) {
+ if (videoLanguages.length === this.languageItems.length) {
+ videoLanguages = null // null means "All"
+ } else if (videoLanguages.length > 20) {
+ this.notifier.error('Too many languages are enabled. Please enable them all or stay below 20 enabled languages.')
+ return
+ } else if (videoLanguages.length === 0) {
+ this.notifier.error('You need to enabled at least 1 video language.')
+ return
+ }
+ }
+
const details: UserUpdateMe = {
nsfwPolicy,
webTorrentEnabled,
- autoPlayVideo
+ autoPlayVideo,
+ videoLanguages
}
this.userService.updateMyProfile(details).subscribe(
() => {
- this.notifier.success(this.i18n('Information updated.'))
+ this.notifier.success(this.i18n('Video settings updated.'))
this.authService.refreshUserInformation()
},
err => this.notifier.error(err.message)
)
}
+
+ getDefaultVideoLanguageLabel () {
+ return this.i18n('No language')
+ }
+
+ getSelectedVideoLanguageLabel () {
+ return this.i18n('{{\'{0} languages selected')
+ }
}
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences'
-import {
- MyAccountVideoPlaylistCreateComponent
-} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
-import {
- MyAccountVideoPlaylistUpdateComponent
-} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
+import { MyAccountVideoPlaylistCreateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-create.component'
+import { MyAccountVideoPlaylistUpdateComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-update.component'
import { MyAccountVideoPlaylistsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlists.component'
-import {
- MyAccountVideoPlaylistElementsComponent
-} from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
+import { MyAccountVideoPlaylistElementsComponent } from '@app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { MyAccountChangeEmailComponent } from '@app/+my-account/my-account-settings/my-account-change-email'
+import { MultiSelectModule } from 'primeng/primeng'
@NgModule({
imports: [
SharedModule,
TableModule,
InputSwitchModule,
- DragDropModule
+ DragDropModule,
+ MultiSelectModule
],
declarations: [
webTorrentEnabled: boolean
autoPlayVideo: boolean
videosHistoryEnabled: boolean
+ videoLanguages: string[]
videoQuota: number
videoQuotaDaily: number
-import { debounceTime } from 'rxjs/operators'
+import { debounceTime, first, tap } from 'rxjs/operators'
import { OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
-import { fromEvent, Observable, Subscription } from 'rxjs'
+import { fromEvent, Observable, of, Subscription } from 'rxjs'
import { AuthService } from '../../core/auth'
import { ComponentPagination } from '../rest/component-pagination.model'
import { VideoSortField } from './sort-field.type'
sort: VideoSortField = '-publishedAt'
categoryOneOf?: number
+ languageOneOf?: string[]
defaultSort: VideoSortField = '-publishedAt'
syndicationItems: Syndication[] = []
loadOnInit = true
- videos: Video[] = []
+ useUserVideoLanguagePreferences = false
ownerDisplayType: OwnerDisplayType = 'account'
displayModerationBlock = false
titleTooltip: string
displayVideoActions = true
groupByDate = false
+ videos: Video[] = []
disabled = false
displayOptions: MiniatureDisplayOptions = {
.subscribe(() => this.calcPageSizes())
this.calcPageSizes()
- if (this.loadOnInit === true) this.loadMoreVideos()
+
+ const loadUserObservable = this.loadUserVideoLanguagesIfNeeded()
+
+ if (this.loadOnInit === true) {
+ loadUserObservable.subscribe(() => this.loadMoreVideos())
+ }
}
ngOnDestroy () {
this.router.navigate([ path ], { queryParams, replaceUrl: true, queryParamsHandling: 'merge' })
}
+
+ private loadUserVideoLanguagesIfNeeded () {
+ if (!this.authService.isLoggedIn() || !this.useUserVideoLanguagePreferences) {
+ return of(true)
+ }
+
+ return this.authService.userInformationLoaded
+ .pipe(
+ first(),
+ tap(() => this.languageOneOf = this.user.videoLanguages)
+ )
+ }
}
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
export interface VideosProvider {
- getVideos (
+ getVideos (parameters: {
videoPagination: ComponentPagination,
sort: VideoSortField,
filter?: VideoFilter,
- categoryOneOf?: number
- ): Observable<{ videos: Video[], totalVideos: number }>
+ categoryOneOf?: number,
+ languageOneOf?: string[]
+ }): Observable<{ videos: Video[], totalVideos: number }>
}
@Injectable()
)
}
- getVideos (
+ getVideos (parameters: {
videoPagination: ComponentPagination,
sort: VideoSortField,
filter?: VideoFilter,
- categoryOneOf?: number
- ): Observable<{ videos: Video[], totalVideos: number }> {
+ categoryOneOf?: number,
+ languageOneOf?: string[]
+ }): Observable<{ videos: Video[], totalVideos: number }> {
+ const { videoPagination, sort, filter, categoryOneOf, languageOneOf } = parameters
+
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
params = params.set('categoryOneOf', categoryOneOf + '')
}
+ if (languageOneOf) {
+ for (const l of languageOneOf) {
+ params = params.append('languageOneOf[]', l)
+ }
+ }
+
return this.authHttp
.get<ResultList<Video>>(VideoService.BASE_VIDEO_URL, { params })
.pipe(
private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
- const defaultSubscription = this.videos.getVideos(pagination, '-createdAt')
+ const defaultSubscription = this.videos.getVideos({ videoPagination: pagination, sort: '-createdAt' })
.pipe(map(v => v.videos))
if (!recommendation.tags || recommendation.tags.length === 0) return defaultSubscription
sort = '-publishedAt' as VideoSortField
filter: VideoFilter = 'local'
+ useUserVideoLanguagePreferences = true
+
constructor (
protected i18n: I18n,
protected router: Router,
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
- return this.videoService.getVideos(newPagination, this.sort, this.filter, this.categoryOneOf)
+ return this.videoService.getVideos({
+ videoPagination: newPagination,
+ sort: this.sort,
+ filter: this.filter,
+ categoryOneOf: this.categoryOneOf,
+ languageOneOf: this.languageOneOf
+ })
}
generateSyndicationList () {
sort: VideoSortField = '-publishedAt'
groupByDate = true
+ useUserVideoLanguagePreferences = true
+
constructor (
protected i18n: I18n,
protected route: ActivatedRoute,
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
- return this.videoService.getVideos(newPagination, this.sort, undefined, this.categoryOneOf)
+ return this.videoService.getVideos({
+ videoPagination: newPagination,
+ sort: this.sort,
+ filter: undefined,
+ categoryOneOf: this.categoryOneOf,
+ languageOneOf: this.languageOneOf
+ })
}
generateSyndicationList () {
titlePage: string
defaultSort: VideoSortField = '-trending'
+ useUserVideoLanguagePreferences = true
+
constructor (
protected i18n: I18n,
protected router: Router,
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
- return this.videoService.getVideos(newPagination, this.sort, undefined, this.categoryOneOf)
+ return this.videoService.getVideos({
+ videoPagination: newPagination,
+ sort: this.sort,
+ filter: undefined,
+ categoryOneOf: this.categoryOneOf,
+ languageOneOf: this.languageOneOf
+ })
}
generateSyndicationList () {
cursor: pointer;
}
+@mixin select-arrow-down {
+ top: 50%;
+ right: calc(0% + 15px);
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border: 5px solid rgba(0, 0, 0, 0);
+ border-top-color: #000;
+ margin-top: -2px;
+ z-index: 100;
+}
+
@mixin peertube-select-container ($width) {
padding: 0;
margin: 0;
}
&:after {
- top: 50%;
- right: calc(0% + 15px);
- content: " ";
- height: 0;
- width: 0;
- position: absolute;
- pointer-events: none;
- border: 5px solid rgba(0, 0, 0, 0);
- border-top-color: #000;
- margin-top: -2px;
- z-index: 100;
+ @include select-arrow-down;
}
select {
}
}
+// multiselect customizations
+p-multiselect {
+ .ui-multiselect-label {
+ font-size: 15px !important;
+ padding: 4px 30px 4px 12px !important;
+
+ $width: 338px;
+ width: $width !important;
+
+ @media screen and (max-width: $width) {
+ width: 100% !important;
+ }
+ }
+
+ .pi.pi-chevron-down{
+ margin-left: 0 !important;
+
+ &::after {
+ @include select-arrow-down;
+
+ right: 0;
+ margin-top: 6px;
+ }
+ }
+
+ .ui-chkbox-icon {
+ //position: absolute !important;
+ width: 18px;
+ height: 18px;
+ //left: 0;
+
+ //&::after {
+ // left: -2px !important;
+ //}
+ }
+}
+
// PrimeNG calendar tweaks
p-calendar .ui-datepicker {
a {
if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
+ if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
if (body.email !== undefined) {
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
import * as validator from 'validator'
import { UserRole } from '../../../shared'
import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers/constants'
-import { exists, isBooleanValid, isFileValid } from './misc'
+import { exists, isArray, isBooleanValid, isFileValid } from './misc'
import { values } from 'lodash'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
return isBooleanValid(value)
}
+function isUserVideoLanguages (value: any) {
+ return value === null || (isArray(value) && value.length < CONSTRAINTS_FIELDS.USERS.VIDEO_LANGUAGES.max)
+}
+
function isUserAdminFlagsValid (value: any) {
return exists(value) && validator.isInt('' + value)
}
isUserVideosHistoryEnabledValid,
isUserBlockedValid,
isUserPasswordValid,
+ isUserVideoLanguages,
isUserBlockedReasonValid,
isUserRoleValid,
isUserVideoQuotaValid,
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 390
+const LAST_MIGRATION_VERSION = 395
// ---------------------------------------------------------------------------
PASSWORD: { min: 6, max: 255 }, // Length
VIDEO_QUOTA: { min: -1 },
VIDEO_QUOTA_DAILY: { min: -1 },
+ VIDEO_LANGUAGES: { max: 500 }, // Array length
BLOCKED_REASON: { min: 3, max: 250 } // Length
},
VIDEO_ABUSES: {
--- /dev/null
+import * as Sequelize from 'sequelize'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction,
+ queryInterface: Sequelize.QueryInterface,
+ sequelize: Sequelize.Sequelize,
+ db: any
+}): Promise<void> {
+ const data = {
+ type: Sequelize.ARRAY(Sequelize.STRING),
+ allowNull: true,
+ defaultValue: null
+ }
+
+ await utils.queryInterface.addColumn('user', 'videoLanguages', data)
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export {
+ up,
+ down
+}
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
- isUserUsernameValid,
+ isUserUsernameValid, isUserVideoLanguages,
isUserVideoQuotaDailyValid,
isUserVideoQuotaValid,
isUserVideosHistoryEnabledValid
body('autoPlayVideo')
.optional()
.custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
+ body('videoLanguages')
+ .optional()
+ .custom(isUserVideoLanguages).withMessage('Should have a valid video languages attribute'),
body('videosHistoryEnabled')
.optional()
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled attribute'),
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
+ isUserVideoLanguages,
isUserVideoQuotaDailyValid,
isUserVideoQuotaValid,
isUserVideosHistoryEnabledValid,
@Column
autoPlayVideo: boolean
+ @AllowNull(true)
+ @Default(null)
+ @Is('UserVideoLanguages', value => throwIfNotValid(value, isUserVideoLanguages, 'video languages'))
+ @Column(DataType.ARRAY(DataType.STRING))
+ videoLanguages: string[]
+
@AllowNull(false)
@Default(UserAdminFlag.NONE)
@Is('UserAdminFlags', value => throwIfNotValid(value, isUserAdminFlagsValid, 'user admin flags'))
webTorrentEnabled: this.webTorrentEnabled,
videosHistoryEnabled: this.videosHistoryEnabled,
autoPlayVideo: this.autoPlayVideo,
+ videoLanguages: this.videoLanguages,
role: this.role,
roleLabel: USER_ROLE_LABELS[ this.role ],
videoQuota: this.videoQuota,
-import { Sequelize } from 'sequelize-typescript'
+import { Model, Sequelize } from 'sequelize-typescript'
import * as validator from 'validator'
-import { OrderItem } from 'sequelize'
import { Col } from 'sequelize/types/lib/utils'
+import { OrderItem } from 'sequelize/types'
type SortType = { sortModel: any, sortValue: string }
return total
}
+const createSafeIn = (model: typeof Model, stringArr: string[]) => {
+ return stringArr.map(t => model.sequelize.escape(t))
+ .join(', ')
+}
+
// ---------------------------------------------------------------------------
export {
buildTrigramSearchIndex,
buildWhereIdOrUUID,
isOutdated,
- parseAggregateResult
+ parseAggregateResult,
+ createSafeIn
}
// ---------------------------------------------------------------------------
buildBlockedAccountSQL,
buildTrigramSearchIndex,
buildWhereIdOrUUID,
+ createSafeIn,
createSimilarityAttribute,
getVideoSort,
isOutdated,
trendingDays?: number
user?: UserModel,
historyOfUser?: UserModel
+
+ baseWhere?: WhereOptions[]
}
@Scopes(() => ({
return query
},
[ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => {
- const attributes = options.withoutId === true ? [] : [ 'id' ]
+ const whereAnd = options.baseWhere ? options.baseWhere : []
const query: FindOptions = {
raw: true,
- attributes,
- where: {
- id: {
- [ Op.and ]: [
- {
- [ Op.notIn ]: Sequelize.literal(
- '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
- )
- }
- ]
- },
- channelId: {
- [ Op.notIn ]: Sequelize.literal(
- '(' +
- 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
- buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
- ')' +
- ')'
- )
- }
- },
+ attributes: options.withoutId === true ? [] : [ 'id' ],
include: []
}
+ whereAnd.push({
+ id: {
+ [ Op.notIn ]: Sequelize.literal(
+ '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
+ )
+ }
+ })
+
+ whereAnd.push({
+ channelId: {
+ [ Op.notIn ]: Sequelize.literal(
+ '(' +
+ 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
+ buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
+ ')' +
+ ')'
+ )
+ }
+ })
+
// Only list public/published videos
if (!options.filter || options.filter !== 'all-local') {
const privacyWhere = {
]
}
- Object.assign(query.where, privacyWhere)
+ whereAnd.push(privacyWhere)
}
if (options.videoPlaylistId) {
// Force actorId to be a number to avoid SQL injections
const actorIdNumber = parseInt(options.followerActorId.toString(), 10)
- query.where[ 'id' ][ Op.and ].push({
- [ Op.in ]: Sequelize.literal(
- '(' +
- 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
- 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
- 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
- ' UNION ALL ' +
- 'SELECT "video"."id" AS "id" FROM "video" ' +
- 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
- 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
- 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
- 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
- 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
- localVideosReq +
- ')'
- )
+ whereAnd.push({
+ id: {
+ [ Op.in ]: Sequelize.literal(
+ '(' +
+ 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
+ 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
+ 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
+ ' UNION ALL ' +
+ 'SELECT "video"."id" AS "id" FROM "video" ' +
+ 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+ 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
+ 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
+ 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
+ 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
+ localVideosReq +
+ ')'
+ )
+ }
})
}
if (options.withFiles === true) {
- query.where[ 'id' ][ Op.and ].push({
- [ Op.in ]: Sequelize.literal(
- '(SELECT "videoId" FROM "videoFile")'
- )
+ whereAnd.push({
+ id: {
+ [ Op.in ]: Sequelize.literal(
+ '(SELECT "videoId" FROM "videoFile")'
+ )
+ }
})
}
// FIXME: issues with sequelize count when making a join on n:m relation, so we just make a IN()
if (options.tagsAllOf || options.tagsOneOf) {
- const createTagsIn = (tags: string[]) => {
- return tags.map(t => VideoModel.sequelize.escape(t))
- .join(', ')
- }
-
if (options.tagsOneOf) {
- query.where[ 'id' ][ Op.and ].push({
- [ Op.in ]: Sequelize.literal(
- '(' +
- 'SELECT "videoId" FROM "videoTag" ' +
- 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
- 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsOneOf) + ')' +
- ')'
- )
+ whereAnd.push({
+ id: {
+ [ Op.in ]: Sequelize.literal(
+ '(' +
+ 'SELECT "videoId" FROM "videoTag" ' +
+ 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
+ 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsOneOf) + ')' +
+ ')'
+ )
+ }
})
}
if (options.tagsAllOf) {
- query.where[ 'id' ][ Op.and ].push({
- [ Op.in ]: Sequelize.literal(
- '(' +
- 'SELECT "videoId" FROM "videoTag" ' +
- 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
- 'WHERE "tag"."name" IN (' + createTagsIn(options.tagsAllOf) + ')' +
- 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
- ')'
- )
+ whereAnd.push({
+ id: {
+ [ Op.in ]: Sequelize.literal(
+ '(' +
+ 'SELECT "videoId" FROM "videoTag" ' +
+ 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
+ 'WHERE "tag"."name" IN (' + createSafeIn(VideoModel, options.tagsAllOf) + ')' +
+ 'GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + options.tagsAllOf.length +
+ ')'
+ )
+ }
})
}
}
if (options.nsfw === true || options.nsfw === false) {
- query.where[ 'nsfw' ] = options.nsfw
+ whereAnd.push({ nsfw: options.nsfw })
}
if (options.categoryOneOf) {
- query.where[ 'category' ] = {
- [ Op.or ]: options.categoryOneOf
- }
+ whereAnd.push({
+ category: {
+ [ Op.or ]: options.categoryOneOf
+ }
+ })
}
if (options.licenceOneOf) {
- query.where[ 'licence' ] = {
- [ Op.or ]: options.licenceOneOf
- }
+ whereAnd.push({
+ licence: {
+ [ Op.or ]: options.licenceOneOf
+ }
+ })
}
if (options.languageOneOf) {
- query.where[ 'language' ] = {
- [ Op.or ]: options.languageOneOf
+ let videoLanguages = options.languageOneOf
+ if (options.languageOneOf.find(l => l === '_unknown')) {
+ videoLanguages = videoLanguages.concat([ null ])
}
+
+ whereAnd.push({
+ [Op.or]: [
+ {
+ language: {
+ [ Op.or ]: videoLanguages
+ }
+ },
+ {
+ id: {
+ [ Op.in ]: Sequelize.literal(
+ '(' +
+ 'SELECT "videoId" FROM "videoCaption" ' +
+ 'WHERE "language" IN (' + createSafeIn(VideoModel, options.languageOneOf) + ') ' +
+ ')'
+ )
+ }
+ }
+ ]
+ })
}
if (options.trendingDays) {
query.subQuery = false
}
+ query.where = {
+ [ Op.and ]: whereAnd
+ }
+
return query
},
[ ScopeNames.WITH_THUMBNAILS ]: {
throw new Error('Try to filter all-local but no user has not the see all videos right')
}
- const query: FindOptions = {
+ const query: FindOptions & { where?: null } = {
offset: options.start,
limit: options.count,
order: getVideoSort(options.sort)
)
}
- const query: FindOptions = {
+ const query = {
attributes: {
include: attributesInclude
},
offset: options.start,
limit: options.count,
- order: getVideoSort(options.sort),
- where: {
- [ Op.and ]: whereAnd
- }
+ order: getVideoSort(options.sort)
}
const serverActor = await getServerActor()
tagsOneOf: options.tagsOneOf,
tagsAllOf: options.tagsAllOf,
user: options.user,
- filter: options.filter
+ filter: options.filter,
+ baseWhere: whereAnd
}
return VideoModel.getAvailableForApi(query, queryOptions)
}
private static async getAvailableForApi (
- query: FindOptions,
+ query: FindOptions & { where?: null }, // Forbid where field in query
options: AvailableForListIDsOptions,
countVideos = true
) {
]
}
- const [ count, rowsId ] = await Promise.all([
- countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve<number>(undefined),
- VideoModel.scope(idsScope).findAll(query)
+ const [ count, ids ] = await Promise.all([
+ countVideos
+ ? VideoModel.scope(countScope).count(countQuery)
+ : Promise.resolve<number>(undefined),
+
+ VideoModel.scope(idsScope)
+ .findAll(query)
+ .then(rows => rows.map(r => r.id))
])
- const ids = rowsId.map(r => r.id)
if (ids.length === 0) return { data: [], total: count }
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
})
+ it('Should fail with an invalid videoLanguages attribute', async function () {
+ {
+ const fields = {
+ videoLanguages: 'toto'
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+ }
+
+ {
+ const languages = []
+ for (let i = 0; i < 1000; i++) {
+ languages.push('fr')
+ }
+
+ const fields = {
+ videoLanguages: languages
+ }
+
+ await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
+ }
+ })
+
it('Should succeed to change password with the correct params', async function () {
const fields = {
currentPassword: 'my super password',
uploadVideo,
wait
} from '../../../../shared/extra-utils'
+import { createVideoCaption } from '../../../../shared/extra-utils/videos/video-captions'
const expect = chai.expect
const attributes2 = immutableAssign(attributes1, { name: attributes1.name + ' - 2', fixture: 'video_short.mp4' })
await uploadVideo(server.url, server.accessToken, attributes2)
- const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: 'en' })
- await uploadVideo(server.url, server.accessToken, attributes3)
+ {
+ const attributes3 = immutableAssign(attributes1, { name: attributes1.name + ' - 3', language: undefined })
+ const res = await uploadVideo(server.url, server.accessToken, attributes3)
+ const videoId = res.body.video.id
+
+ await createVideoCaption({
+ url: server.url,
+ accessToken: server.accessToken,
+ language: 'en',
+ videoId,
+ fixture: 'subtitle-good2.vtt',
+ mimeType: 'application/octet-stream'
+ })
+
+ await createVideoCaption({
+ url: server.url,
+ accessToken: server.accessToken,
+ language: 'aa',
+ videoId,
+ fixture: 'subtitle-good2.vtt',
+ mimeType: 'application/octet-stream'
+ })
+ }
const attributes4 = immutableAssign(attributes1, { name: attributes1.name + ' - 4', language: 'pl', nsfw: true })
await uploadVideo(server.url, server.accessToken, attributes4)
startDate = new Date().toISOString()
- const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2 })
+ const attributes5 = immutableAssign(attributes1, { name: attributes1.name + ' - 5', licence: 2, language: undefined })
await uploadVideo(server.url, server.accessToken, attributes5)
const attributes6 = immutableAssign(attributes1, { name: attributes1.name + ' - 6', tags: [ 't1', 't2 '] })
search: '1111 2222 3333',
languageOneOf: [ 'pl', 'en' ]
}
- const res1 = await advancedVideosSearch(server.url, query)
- expect(res1.body.total).to.equal(2)
- expect(res1.body.data[0].name).to.equal('1111 2222 3333 - 3')
- expect(res1.body.data[1].name).to.equal('1111 2222 3333 - 4')
- const res2 = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
- expect(res2.body.total).to.equal(0)
+ {
+ const res = await advancedVideosSearch(server.url, query)
+ expect(res.body.total).to.equal(2)
+ expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3')
+ expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4')
+ }
+
+ {
+ const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'pl', 'en', '_unknown' ] }))
+ expect(res.body.total).to.equal(3)
+ expect(res.body.data[ 0 ].name).to.equal('1111 2222 3333 - 3')
+ expect(res.body.data[ 1 ].name).to.equal('1111 2222 3333 - 4')
+ expect(res.body.data[ 2 ].name).to.equal('1111 2222 3333 - 5')
+ }
+
+ {
+ const res = await advancedVideosSearch(server.url, immutableAssign(query, { languageOneOf: [ 'eo' ] }))
+ expect(res.body.total).to.equal(0)
+ }
})
it('Should search by start date', async function () {
webTorrentEnabled?: boolean
autoPlayVideo?: boolean
videosHistoryEnabled?: boolean
+ videoLanguages?: string[]
email?: string
currentPassword?: string