From b33a5b057fd77a40843d4a53e495e04e6a2aaec8 Mon Sep 17 00:00:00 2001 From: kontrollanten <6680299+kontrollanten@users.noreply.github.com> Date: Wed, 13 Jul 2022 00:01:04 +0200 Subject: [PATCH] feat: show contained playlists under My videos closes #4769 --- .../my-videos/my-videos.component.html | 1 + .../my-videos/my-videos.component.ts | 20 ++++++++++++++++--- .../video-miniature.component.html | 6 ++++++ .../video-miniature.component.scss | 4 ++++ .../video-miniature.component.ts | 5 ++++- .../videos-selection.component.html | 1 + .../videos-selection.component.ts | 3 ++- .../video-playlist.service.ts | 12 ++++------- .../api/users/my-video-playlists.ts | 5 ++++- server/models/video/video-playlist.ts | 6 +++--- server/tests/api/videos/video-playlists.ts | 12 ++++++++++- server/types/models/video/video-playlist.ts | 8 ++++++++ .../playlist/video-exist-in-playlist.model.ts | 2 ++ 13 files changed, 67 insertions(+), 18 deletions(-) diff --git a/client/src/app/+my-library/my-videos/my-videos.component.html b/client/src/app/+my-library/my-videos/my-videos.component.html index 146dcf41e..995f6b75b 100644 --- a/client/src/app/+my-library/my-videos/my-videos.component.html +++ b/client/src/app/+my-library/my-videos/my-videos.component.html @@ -34,6 +34,7 @@ this.playlistService.listPlaylistsForVideos(data.map(v => v.id)))) + .subscribe(result => { + this.videosContainedInPlaylists = Object.keys(result).reduce((acc, videoId) => ({ + ...acc, + [videoId]: uniqBy(result[videoId], (p: VideoExistInPlaylist) => p.playlistId) + }), {}) + }) + this.authService.userInformationLoaded.subscribe(() => { this.user = this.authService.getUser() this.userChannels = this.user.videoChannels diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html index e8d2ca1c4..052fdb4cd 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html @@ -52,6 +52,12 @@ - {{ getStateLabel(video) }} + +
+ + {{ playlist.playlistDisplayName }} + +
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss index a397efdca..ba2adfc5a 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.scss +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.scss @@ -4,6 +4,10 @@ $more-button-width: 40px; +.chip { + @include chip; +} + .video-miniature { font-size: 14px; } diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 534a78b3f..632422e1e 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts @@ -11,7 +11,7 @@ import { Output } from '@angular/core' import { AuthService, ScreenService, ServerService, User } from '@app/core' -import { HTMLServerConfig, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' +import { HTMLServerConfig, VideoExistInPlaylist, VideoPlaylistType, VideoPrivacy, VideoState } from '@shared/models' import { LinkType } from '../../../types/link.type' import { ActorAvatarSize } from '../shared-actor-image/actor-avatar.component' import { Video } from '../shared-main' @@ -19,6 +19,7 @@ import { VideoPlaylistService } from '../shared-video-playlist' import { VideoActionsDisplayType } from './video-actions-dropdown.component' export type MiniatureDisplayOptions = { + containedInPlaylists?: boolean date?: boolean views?: boolean by?: boolean @@ -38,8 +39,10 @@ export type MiniatureDisplayOptions = { export class VideoMiniatureComponent implements OnInit { @Input() user: User @Input() video: Video + @Input() containedInPlaylists: VideoExistInPlaylist[] @Input() displayOptions: MiniatureDisplayOptions = { + containedInPlaylists: false, date: true, views: true, by: true, diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.html b/client/src/app/shared/shared-video-miniature/videos-selection.component.html index 6ea2661e4..6c6db4b96 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.html +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.html @@ -12,6 +12,7 @@ diff --git a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts index bac828fba..77ad9d6d5 100644 --- a/client/src/app/shared/shared-video-miniature/videos-selection.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-selection.component.ts @@ -1,7 +1,7 @@ import { Observable, Subject } from 'rxjs' import { AfterContentInit, Component, ContentChildren, EventEmitter, Input, Output, QueryList, TemplateRef } from '@angular/core' import { ComponentPagination, Notifier, User } from '@app/core' -import { ResultList, VideoSortField } from '@shared/models' +import { ResultList, VideosExistInPlaylists, VideoSortField } from '@shared/models' import { PeerTubeTemplateDirective, Video } from '../shared-main' import { MiniatureDisplayOptions } from './video-miniature.component' @@ -13,6 +13,7 @@ export type SelectionType = { [ id: number ]: boolean } styleUrls: [ './videos-selection.component.scss' ] }) export class VideosSelectionComponent implements AfterContentInit { + @Input() videosContainedInPlaylists: VideosExistInPlaylists @Input() user: User @Input() pagination: ComponentPagination diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index db9f78a7a..7b0864033 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts @@ -75,6 +75,10 @@ export class VideoPlaylistService { ) } + listPlaylistsForVideos (videoIds: number[]) { + return this.doVideosExistInPlaylist(videoIds) + } + listMyPlaylistWithCache (user: AuthUser, search?: string) { if (!search) { if (this.myAccountPlaylistCacheRunning) return this.myAccountPlaylistCacheRunning @@ -188,14 +192,6 @@ export class VideoPlaylistService { return this.authHttp.post<{ videoPlaylistElement: { id: number } }>(url, body) .pipe( tap(res => { - const existsResult = this.videoExistsCache[body.videoId] - existsResult.push({ - playlistId, - playlistElementId: res.videoPlaylistElement.id, - startTimestamp: body.startTimestamp, - stopTimestamp: body.stopTimestamp - }) - this.runPlaylistCheck(body.videoId) }), catchError(err => this.restExtractor.handleError(err)) diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts index f55ea2ec4..715717610 100644 --- a/server/controllers/api/users/my-video-playlists.ts +++ b/server/controllers/api/users/my-video-playlists.ts @@ -1,3 +1,4 @@ +import { uuidToShort } from '@shared/extra-utils' import express from 'express' import { VideosExistInPlaylists } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' import { asyncMiddleware, authenticate } from '../../../middlewares' @@ -24,7 +25,7 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo const videoIds = req.query.videoIds.map(i => parseInt(i + '', 10)) const user = res.locals.oauth.token.User - const results = await VideoPlaylistModel.listPlaylistIdsOf(user.Account.id, videoIds) + const results = await VideoPlaylistModel.listPlaylistSummariesOf(user.Account.id, videoIds) const existObject: VideosExistInPlaylists = {} @@ -37,6 +38,8 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo existObject[element.videoId].push({ playlistElementId: element.id, playlistId: result.id, + playlistDisplayName: result.name, + playlistShortUUID: uuidToShort(result.uuid), startTimestamp: element.startTimestamp, stopTimestamp: element.stopTimestamp }) diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 00cca0549..9ad14b114 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -49,7 +49,7 @@ import { MVideoPlaylistFormattable, MVideoPlaylistFull, MVideoPlaylistFullSummary, - MVideoPlaylistIdWithElements + MVideoPlaylistSummaryWithElements } from '../../types/models/video/video-playlist' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { ActorModel } from '../actor/actor' @@ -470,9 +470,9 @@ export class VideoPlaylistModel extends Model { + static listPlaylistSummariesOf (accountId: number, videoIds: number[]): Promise { const query = { - attributes: [ 'id' ], + attributes: [ 'id', 'name', 'uuid' ], where: { ownerAccountId: accountId }, diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index c33a63df0..6300a0058 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts @@ -24,6 +24,7 @@ import { setDefaultVideoChannel, waitJobs } from '@shared/server-commands' +import { uuidToShort } from '@shared/extra-utils' const expect = chai.expect @@ -59,6 +60,7 @@ describe('Test video playlists', function () { let playlistServer2UUID2: string let playlistServer1Id: number + let playlistServer1DisplayName: string let playlistServer1UUID: string let playlistServer1UUID2: string @@ -492,15 +494,17 @@ describe('Test video playlists', function () { return commands[0].addElement({ playlistId: playlistServer1Id, attributes }) } + const playlistDisplayName = 'playlist 4' const playlist = await commands[0].create({ attributes: { - displayName: 'playlist 4', + displayName: playlistDisplayName, privacy: VideoPlaylistPrivacy.PUBLIC, videoChannelId: servers[0].store.channel.id } }) playlistServer1Id = playlist.id + playlistServer1DisplayName = playlistDisplayName playlistServer1UUID = playlist.uuid await addVideo({ videoId: servers[0].store.videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 }) @@ -911,6 +915,8 @@ describe('Test video playlists', function () { const elem = obj[servers[0].store.videos[0].id] expect(elem).to.have.lengthOf(1) expect(elem[0].playlistElementId).to.exist + expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) + expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) expect(elem[0].playlistId).to.equal(playlistServer1Id) expect(elem[0].startTimestamp).to.equal(15) expect(elem[0].stopTimestamp).to.equal(28) @@ -920,6 +926,8 @@ describe('Test video playlists', function () { const elem = obj[servers[0].store.videos[3].id] expect(elem).to.have.lengthOf(1) expect(elem[0].playlistElementId).to.equal(playlistElementServer1Video4) + expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) + expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) expect(elem[0].playlistId).to.equal(playlistServer1Id) expect(elem[0].startTimestamp).to.equal(1) expect(elem[0].stopTimestamp).to.equal(35) @@ -929,6 +937,8 @@ describe('Test video playlists', function () { const elem = obj[servers[0].store.videos[4].id] expect(elem).to.have.lengthOf(1) expect(elem[0].playlistId).to.equal(playlistServer1Id) + expect(elem[0].playlistDisplayName).to.equal(playlistServer1DisplayName) + expect(elem[0].playlistShortUUID).to.equal(uuidToShort(playlistServer1UUID)) expect(elem[0].startTimestamp).to.equal(45) expect(elem[0].stopTimestamp).to.equal(null) } diff --git a/server/types/models/video/video-playlist.ts b/server/types/models/video/video-playlist.ts index 33fe5416a..c8612d103 100644 --- a/server/types/models/video/video-playlist.ts +++ b/server/types/models/video/video-playlist.ts @@ -14,6 +14,10 @@ export type MVideoPlaylist = Omit +export type MVideoPlaylistSummary = + Pick & + Pick & + Pick export type MVideoPlaylistPrivacy = Pick export type MVideoPlaylistUUID = Pick export type MVideoPlaylistVideosLength = MVideoPlaylist & { videosLength?: number } @@ -26,6 +30,10 @@ export type MVideoPlaylistWithElements = MVideoPlaylist & Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]> +export type MVideoPlaylistSummaryWithElements = + MVideoPlaylistSummary & + Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]> + export type MVideoPlaylistIdWithElements = MVideoPlaylistId & Use<'VideoPlaylistElements', MVideoPlaylistElementLight[]> diff --git a/shared/models/videos/playlist/video-exist-in-playlist.model.ts b/shared/models/videos/playlist/video-exist-in-playlist.model.ts index fc979c8c0..c29b1ee38 100644 --- a/shared/models/videos/playlist/video-exist-in-playlist.model.ts +++ b/shared/models/videos/playlist/video-exist-in-playlist.model.ts @@ -5,6 +5,8 @@ export type VideosExistInPlaylists = { export type VideoExistInPlaylist = { playlistElementId: number playlistId: number + playlistDisplayName: string + playlistShortUUID: string startTimestamp?: number stopTimestamp?: number }