Stop loop recommendation for anonymous users

This commit is contained in:
Chocobozzz 2024-05-30 08:33:07 +02:00
parent 671c6c1f96
commit ada73259ff
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
10 changed files with 99 additions and 125 deletions

View File

@ -9,7 +9,7 @@ import { VideoCommentService } from '@app/shared/shared-video-comment/video-comm
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
import { OverviewService } from '../video-list'
import { RecentVideosRecommendationService, RecommendedVideosStore } from './shared'
import { VideoRecommendationService } from './shared'
import { VideoWatchComponent } from './video-watch.component'
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
@ -24,8 +24,7 @@ export default [
VideoBlockService,
LiveVideoService,
VideoCommentService,
RecentVideosRecommendationService,
RecommendedVideosStore,
VideoRecommendationService,
SearchService,
AbuseService,
UserAdminService,

View File

@ -1,4 +1,2 @@
export * from './recent-videos-recommendation.service'
export * from './recommendation-info.model'
export * from './video-recommendation.service'
export * from './recommended-videos.component'
export * from './recommended-videos.store'

View File

@ -1,4 +0,0 @@
export interface RecommendationInfo {
uuid: string
tags?: string[]
}

View File

@ -1,7 +0,0 @@
import { Observable } from 'rxjs'
import { RecommendationInfo } from './recommendation-info.model'
import { Video } from '@app/shared/shared-main/video/video.model'
export interface RecommendationService {
getRecommendations (recommendation: RecommendationInfo): Observable<Video[]>
}

View File

@ -1,12 +1,14 @@
<div class="other-videos" [ngClass]="{ 'display-as-row': displayAsRow }">
<ng-container *ngIf="hasVideos$ | async">
@if (videos.length !== 0) {
<div class="title-page-container">
<h2 i18n class="title-page">Other videos</h2>
<div *ngIf="!playlist" class="title-page-autoplay"
<div
*ngIf="!playlist" class="title-page-autoplay"
[ngbTooltip]="autoPlayNextVideoTooltip" placement="bottom-right auto"
>
<span i18n>AUTOPLAY</span>
<my-input-switch
i18n-label label="Toggle autoplay next video"
class="small" inputName="autoplay-next-video"
@ -15,8 +17,9 @@
</div>
</div>
<ng-container *ngFor="let video of (videos$ | async); let i = index; let length = count">
<ng-container *ngFor="let video of videos; let i = index; let length = count">
<span i18n *ngIf="!playlist && i === 0 && length !== 0 && autoPlayNextVideo" class="title-page-next-video-label">Next video to be played</span>
<my-video-miniature
[displayOptions]="displayOptions" [video]="video" [user]="user" [displayAsRow]="displayAsRow"
(videoBlocked)="onVideoRemoved()" (videoRemoved)="onVideoRemoved()" (videoAccountMuted)="onVideoRemoved()"
@ -26,5 +29,5 @@
<hr *ngIf="!playlist && i === 0 && length > 1 && autoPlayNextVideo" />
</ng-container>
</ng-container>
}
</div>

View File

@ -1,15 +1,15 @@
import { Observable, startWith, Subscription, switchMap } from 'rxjs'
import { AsyncPipe, NgClass, NgFor, NgIf } from '@angular/common'
import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'
import { AuthService, Notifier, User, UserService } from '@app/core'
import { Video } from '@app/shared/shared-main/video/video.model'
import { RecommendationInfo } from './recommendation-info.model'
import { RecommendedVideosStore } from './recommended-videos.store'
import { MiniatureDisplayOptions, VideoMiniatureComponent } from '../../../../shared/shared-video-miniature/video-miniature.component'
import { FormsModule } from '@angular/forms'
import { InputSwitchComponent } from '../../../../shared/shared-forms/input-switch.component'
import { AuthService, Notifier, User, UserService } from '@app/core'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { Video } from '@app/shared/shared-main/video/video.model'
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { NgClass, NgIf, NgFor, AsyncPipe } from '@angular/common'
import { VideoPlaylist } from '@peertube/peertube-models'
import { Subscription, startWith, switchMap } from 'rxjs'
import { InputSwitchComponent } from '../../../../shared/shared-forms/input-switch.component'
import { MiniatureDisplayOptions, VideoMiniatureComponent } from '../../../../shared/shared-video-miniature/video-miniature.component'
import { VideoRecommendationService } from './video-recommendation.service'
@Component({
selector: 'my-recommended-videos',
@ -19,12 +19,14 @@ import { VideoPlaylist } from '@peertube/peertube-models'
imports: [ NgClass, NgIf, NgbTooltip, InputSwitchComponent, FormsModule, NgFor, VideoMiniatureComponent, AsyncPipe ]
})
export class RecommendedVideosComponent implements OnInit, OnChanges, OnDestroy {
@Input() inputRecommendation: RecommendationInfo
@Input() currentVideo: VideoDetails
@Input() playlist: VideoPlaylist
@Input() displayAsRow: boolean
@Output() gotRecommendations = new EventEmitter<Video[]>()
videos: Video[] = []
autoPlayNextVideo: boolean
autoPlayNextVideoTooltip: string
@ -39,19 +41,12 @@ export class RecommendedVideosComponent implements OnInit, OnChanges, OnDestroy
private userSub: Subscription
readonly hasVideos$: Observable<boolean>
readonly videos$: Observable<Video[]>
constructor (
private userService: UserService,
private authService: AuthService,
private notifier: Notifier,
private store: RecommendedVideosStore
private videoRecommendation: VideoRecommendationService
) {
this.videos$ = this.store.recommendations$
this.hasVideos$ = this.store.hasRecommendations$
this.videos$.subscribe(videos => this.gotRecommendations.emit(videos))
this.autoPlayNextVideoTooltip = $localize`When active, the next video is automatically played after the current one.`
}
@ -68,8 +63,8 @@ export class RecommendedVideosComponent implements OnInit, OnChanges, OnDestroy
}
ngOnChanges () {
if (this.inputRecommendation) {
this.store.requestNewRecommendations(this.inputRecommendation)
if (this.currentVideo) {
this.loadRecommendations()
}
}
@ -78,7 +73,7 @@ export class RecommendedVideosComponent implements OnInit, OnChanges, OnDestroy
}
onVideoRemoved () {
this.store.requestNewRecommendations(this.inputRecommendation)
this.loadRecommendations()
}
switchAutoPlayNextVideo () {
@ -97,4 +92,17 @@ export class RecommendedVideosComponent implements OnInit, OnChanges, OnDestroy
this.userService.updateMyAnonymousProfile(details)
}
}
private loadRecommendations () {
this.videoRecommendation.getRecommendations(this.currentVideo, this.videoRecommendation.getRecommentationHistory())
.subscribe({
next: videos => {
this.videos = videos
this.gotRecommendations.emit(this.videos)
},
error: err => this.notifier.error(err.message)
})
}
}

View File

@ -1,37 +0,0 @@
import { Observable, ReplaySubject } from 'rxjs'
import { map, shareReplay, switchMap, take } from 'rxjs/operators'
import { Inject, Injectable } from '@angular/core'
import { Video } from '@app/shared/shared-main/video/video.model'
import { RecentVideosRecommendationService } from './recent-videos-recommendation.service'
import { RecommendationInfo } from './recommendation-info.model'
import { RecommendationService } from './recommendations.service'
/**
* This store is intended to provide data for the RecommendedVideosComponent.
*/
@Injectable()
export class RecommendedVideosStore {
public readonly recommendations$: Observable<Video[]>
public readonly hasRecommendations$: Observable<boolean>
private readonly requestsForLoad$$ = new ReplaySubject<RecommendationInfo>(1)
constructor (
@Inject(RecentVideosRecommendationService) private recommendations: RecommendationService
) {
this.recommendations$ = this.requestsForLoad$$.pipe(
switchMap(requestedRecommendation => {
return this.recommendations.getRecommendations(requestedRecommendation)
.pipe(take(1))
}),
shareReplay()
)
this.hasRecommendations$ = this.recommendations$.pipe(
map(otherVideos => otherVideos.length > 0)
)
}
requestNewRecommendations (recommend: RecommendationInfo) {
this.requestsForLoad$$.next(recommend)
}
}

View File

@ -1,24 +1,20 @@
import { Observable, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
import { Injectable } from '@angular/core'
import { ServerService, UserService } from '@app/core'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { RecommendationInfo } from './recommendation-info.model'
import { RecommendationService } from './recommendations.service'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { Video } from '@app/shared/shared-main/video/video.model'
import { VideoService } from '@app/shared/shared-main/video/video.service'
import { SearchService } from '@app/shared/shared-search/search.service'
import { AdvancedSearch } from '@app/shared/shared-search/advanced-search.model'
import { SearchService } from '@app/shared/shared-search/search.service'
import { HTMLServerConfig } from '@peertube/peertube-models'
import { Observable, of } from 'rxjs'
import { map, switchMap } from 'rxjs/operators'
/**
* Provides "recommendations" by providing the most recently uploaded videos.
*/
@Injectable()
export class RecentVideosRecommendationService implements RecommendationService {
readonly pageSize = 5
export class VideoRecommendationService {
private config: HTMLServerConfig
private readonly videoIdsHistory = new Set<number>()
constructor (
private videos: VideoService,
private searchService: SearchService,
@ -28,19 +24,37 @@ export class RecentVideosRecommendationService implements RecommendationService
this.config = this.serverService.getHTMLConfig()
}
getRecommendations (recommendation: RecommendationInfo): Observable<Video[]> {
getRecommentationHistory () {
return this.videoIdsHistory
}
return this.fetchPage(1, recommendation)
getRecommendations (currentVideo: VideoDetails, exceptions = new Set<number>()): Observable<Video[]> {
this.videoIdsHistory.add(currentVideo.id)
// We want 5 results max
// +1 to exclude the currentVideo if needed
// +exceptions.size to exclude the videos we don't want to include
// Cap to 30 results maximum
const totalVideos = 5
const internalTotalVideos = Math.min(totalVideos + 1 + exceptions.size, 30)
return this.fetchPage(currentVideo, internalTotalVideos)
.pipe(
map(videos => {
const otherVideos = videos.filter(v => v.uuid !== recommendation.uuid)
return otherVideos.slice(0, this.pageSize)
let otherVideos = videos.filter(v => v.uuid !== currentVideo.uuid && !exceptions.has(v.id))
// Stop using exclude list if we excluded all videos
if (otherVideos.length === 0 && videos.length !== 0) {
otherVideos = videos.filter(v => v.uuid !== currentVideo.uuid)
}
return otherVideos.slice(0, totalVideos)
})
)
}
private fetchPage (page: number, recommendation: RecommendationInfo): Observable<Video[]> {
const pagination = { currentPage: page, itemsPerPage: this.pageSize + 1 }
private fetchPage (currentVideo: VideoDetails, totalItems: number): Observable<Video[]> {
const pagination = { currentPage: 1, itemsPerPage: totalItems }
return this.userService.getAnonymousOrLoggedUser()
.pipe(
@ -66,7 +80,7 @@ export class RecentVideosRecommendationService implements RecommendationService
componentPagination: pagination,
skipCount: true,
advancedSearch: new AdvancedSearch({
tagsOneOf: recommendation.tags.join(','),
tagsOneOf: currentVideo.tags.join(','),
sort: '-publishedAt',
searchTarget: 'local',
nsfw,

View File

@ -102,7 +102,7 @@
<my-recommended-videos
[displayAsRow]="displayOtherVideosAsRow()"
[inputRecommendation]="{ uuid: video.uuid, tags: video.tags }"
[currentVideo]="video"
[playlist]="playlist"
(gotRecommendations)="onRecommendations($event)"
></my-recommended-videos>

View File

@ -1,25 +1,34 @@
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
import { PlatformLocation, NgClass, NgIf, NgTemplateOutlet } from '@angular/common'
import { NgClass, NgIf, NgTemplateOutlet, PlatformLocation } from '@angular/common'
import { Component, ElementRef, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Router, RouterLink } from '@angular/router'
import {
AuthService,
AuthUser,
ConfirmService,
Hotkey,
HotkeysService,
MetaService,
Notifier,
PeerTubeSocket,
PluginService,
RestExtractor,
ScreenService,
ServerService,
Hotkey,
HotkeysService,
User,
UserService,
MetaService
UserService
} from '@app/core'
import { HooksService } from '@app/core/plugins/hooks.service'
import { isXPercentInViewport, scrollToTop, toBoolean } from '@app/helpers'
import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video-caption.service'
import { VideoChapterService } from '@app/shared/shared-main/video/video-chapter.service'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { VideoFileTokenService } from '@app/shared/shared-main/video/video-file-token.service'
import { Video } from '@app/shared/shared-main/video/video.model'
import { VideoService } from '@app/shared/shared-main/video/video.service'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription/subscribe-button.component'
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
import { timeToInt } from '@peertube/peertube-core-utils'
import {
HTMLServerConfig,
@ -36,6 +45,7 @@ import {
} from '@peertube/peertube-models'
import { logger } from '@root-helpers/logger'
import { isP2PEnabled, videoRequiresFileToken, videoRequiresUserAuth } from '@root-helpers/video'
import { forkJoin, map, Observable, of, Subscription, switchMap } from 'rxjs'
import {
HLSOptions,
PeerTubePlayer,
@ -46,29 +56,19 @@ import {
} from '../../../assets/player'
import { cleanupVideoWatch, getStoredTheater, getStoredVideoWatchHistory } from '../../../assets/player/peertube-player-local-storage'
import { environment } from '../../../environments/environment'
import { VideoWatchPlaylistComponent } from './shared'
import { PlayerStylesComponent } from './player-styles.component'
import { PrivacyConcernsComponent } from './shared/information/privacy-concerns.component'
import { RecommendedVideosComponent } from './shared/recommendations/recommended-videos.component'
import { VideoCommentsComponent } from './shared/comment/video-comments.component'
import { VideoAttributesComponent } from './shared/metadata/video-attributes.component'
import { VideoDescriptionComponent } from './shared/metadata/video-description.component'
import { VideoAvatarChannelComponent } from './shared/metadata/video-avatar-channel.component'
import { ActionButtonsComponent } from './shared/action-buttons/action-buttons.component'
import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component'
import { DateToggleComponent } from '../../shared/shared-main/date/date-toggle.component'
import { VideoAlertComponent } from './shared/information/video-alert.component'
import { PluginPlaceholderComponent } from '../../shared/shared-main/plugins/plugin-placeholder.component'
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video-caption.service'
import { VideoChapterService } from '@app/shared/shared-main/video/video-chapter.service'
import { VideoFileTokenService } from '@app/shared/shared-main/video/video-file-token.service'
import { VideoService } from '@app/shared/shared-main/video/video.service'
import { Video } from '@app/shared/shared-main/video/video.model'
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription/subscribe-button.component'
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
import { VideoViewsCounterComponent } from '../../shared/shared-video/video-views-counter.component'
import { PlayerStylesComponent } from './player-styles.component'
import { VideoWatchPlaylistComponent } from './shared'
import { ActionButtonsComponent } from './shared/action-buttons/action-buttons.component'
import { VideoCommentsComponent } from './shared/comment/video-comments.component'
import { PrivacyConcernsComponent } from './shared/information/privacy-concerns.component'
import { VideoAlertComponent } from './shared/information/video-alert.component'
import { VideoAttributesComponent } from './shared/metadata/video-attributes.component'
import { VideoAvatarChannelComponent } from './shared/metadata/video-avatar-channel.component'
import { VideoDescriptionComponent } from './shared/metadata/video-description.component'
import { RecommendedVideosComponent } from './shared/recommendations/recommended-videos.component'
type URLOptions = {
playerMode: PlayerMode