aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/shared/video
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/shared/video')
-rw-r--r--client/src/app/shared/video/abstract-video-list.html19
-rw-r--r--client/src/app/shared/video/abstract-video-list.scss0
-rw-r--r--client/src/app/shared/video/abstract-video-list.ts121
-rw-r--r--client/src/app/shared/video/sort-field.type.ts5
-rw-r--r--client/src/app/shared/video/video-details.model.ts84
-rw-r--r--client/src/app/shared/video/video-edit.model.ts50
-rw-r--r--client/src/app/shared/video/video-pagination.model.ts5
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.html10
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.scss28
-rw-r--r--client/src/app/shared/video/video-thumbnail.component.ts12
-rw-r--r--client/src/app/shared/video/video.model.ts90
-rw-r--r--client/src/app/shared/video/video.service.ts170
12 files changed, 594 insertions, 0 deletions
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html
new file mode 100644
index 000000000..bd4f6b1f8
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.html
@@ -0,0 +1,19 @@
1<div class="margin-content">
2 <div class="title-page title-page-single">
3 {{ titlePage }}
4 </div>
5
6 <div
7 infiniteScroll
8 [infiniteScrollUpDistance]="1.5"
9 [infiniteScrollDistance]="0.5"
10 (scrolled)="onNearOfBottom()"
11 (scrolledUp)="onNearOfTop()"
12 >
13 <my-video-miniature
14 class="ng-animate"
15 *ngFor="let video of videos" [video]="video" [user]="user" [currentSort]="sort"
16 >
17 </my-video-miniature>
18 </div>
19</div>
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.scss
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts
new file mode 100644
index 000000000..cf717cf4c
--- /dev/null
+++ b/client/src/app/shared/video/abstract-video-list.ts
@@ -0,0 +1,121 @@
1import { OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { NotificationsService } from 'angular2-notifications'
4import { Observable } from 'rxjs/Observable'
5import { Subscription } from 'rxjs/Subscription'
6import { SortField } from './sort-field.type'
7import { VideoPagination } from './video-pagination.model'
8import { Video } from './video.model'
9
10export abstract class AbstractVideoList implements OnInit, OnDestroy {
11 pagination: VideoPagination = {
12 currentPage: 1,
13 itemsPerPage: 25,
14 totalItems: null
15 }
16 sort: SortField = '-createdAt'
17 videos: Video[] = []
18
19 protected notificationsService: NotificationsService
20 protected router: Router
21 protected route: ActivatedRoute
22 protected subActivatedRoute: Subscription
23
24 protected abstract currentRoute: string
25
26 abstract titlePage: string
27 private loadedPages: { [ id: number ]: boolean } = {}
28
29 abstract getVideosObservable (): Observable<{ videos: Video[], totalVideos: number}>
30
31 ngOnInit () {
32 // Subscribe to route changes
33 const routeParams = this.route.snapshot.params
34 this.loadRouteParams(routeParams)
35 this.loadMoreVideos('after')
36 }
37
38 ngOnDestroy () {
39 if (this.subActivatedRoute) {
40 this.subActivatedRoute.unsubscribe()
41 }
42 }
43
44 onNearOfTop () {
45 if (this.pagination.currentPage > 1) {
46 this.previousPage()
47 }
48 }
49
50 onNearOfBottom () {
51 if (this.hasMoreVideos()) {
52 this.nextPage()
53 }
54 }
55
56 loadMoreVideos (where: 'before' | 'after') {
57 if (this.loadedPages[this.pagination.currentPage] === true) return
58
59 const observable = this.getVideosObservable()
60
61 observable.subscribe(
62 ({ videos, totalVideos }) => {
63 this.loadedPages[this.pagination.currentPage] = true
64 this.pagination.totalItems = totalVideos
65
66 if (where === 'before') {
67 this.videos = videos.concat(this.videos)
68 } else {
69 this.videos = this.videos.concat(videos)
70 }
71 },
72 error => this.notificationsService.error('Error', error.text)
73 )
74 }
75
76 protected hasMoreVideos () {
77 if (!this.pagination.totalItems) return true
78
79 const maxPage = this.pagination.totalItems / this.pagination.itemsPerPage
80 return maxPage > this.pagination.currentPage
81 }
82
83 protected previousPage () {
84 this.pagination.currentPage--
85
86 this.setNewRouteParams()
87 this.loadMoreVideos('before')
88 }
89
90 protected nextPage () {
91 this.pagination.currentPage++
92
93 this.setNewRouteParams()
94 this.loadMoreVideos('after')
95 }
96
97 protected buildRouteParams () {
98 // There is always a sort and a current page
99 const params = {
100 sort: this.sort,
101 page: this.pagination.currentPage
102 }
103
104 return params
105 }
106
107 protected loadRouteParams (routeParams: { [ key: string ]: any }) {
108 this.sort = routeParams['sort'] as SortField || '-createdAt'
109
110 if (routeParams['page'] !== undefined) {
111 this.pagination.currentPage = parseInt(routeParams['page'], 10)
112 } else {
113 this.pagination.currentPage = 1
114 }
115 }
116
117 protected setNewRouteParams () {
118 const routeParams = this.buildRouteParams()
119 this.router.navigate([ this.currentRoute, routeParams ])
120 }
121}
diff --git a/client/src/app/shared/video/sort-field.type.ts b/client/src/app/shared/video/sort-field.type.ts
new file mode 100644
index 000000000..776f360f8
--- /dev/null
+++ b/client/src/app/shared/video/sort-field.type.ts
@@ -0,0 +1,5 @@
1export type SortField = 'name' | '-name'
2 | 'duration' | '-duration'
3 | 'createdAt' | '-createdAt'
4 | 'views' | '-views'
5 | 'likes' | '-likes'
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
new file mode 100644
index 000000000..93c380b73
--- /dev/null
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -0,0 +1,84 @@
1import { Video } from '../../shared/video/video.model'
2import { AuthUser } from '../../core'
3import {
4 VideoDetails as VideoDetailsServerModel,
5 VideoFile,
6 VideoChannel,
7 VideoResolution,
8 UserRight,
9 VideoPrivacy
10} from '../../../../../shared'
11
12export class VideoDetails extends Video implements VideoDetailsServerModel {
13 account: string
14 by: string
15 createdAt: Date
16 updatedAt: Date
17 categoryLabel: string
18 category: number
19 licenceLabel: string
20 licence: number
21 languageLabel: string
22 language: number
23 description: string
24 duration: number
25 durationLabel: string
26 id: number
27 uuid: string
28 isLocal: boolean
29 name: string
30 serverHost: string
31 tags: string[]
32 thumbnailPath: string
33 thumbnailUrl: string
34 previewPath: string
35 previewUrl: string
36 embedPath: string
37 embedUrl: string
38 views: number
39 likes: number
40 dislikes: number
41 nsfw: boolean
42 descriptionPath: string
43 files: VideoFile[]
44 channel: VideoChannel
45 privacy: VideoPrivacy
46 privacyLabel: string
47
48 constructor (hash: VideoDetailsServerModel) {
49 super(hash)
50
51 this.privacy = hash.privacy
52 this.privacyLabel = hash.privacyLabel
53 this.descriptionPath = hash.descriptionPath
54 this.files = hash.files
55 this.channel = hash.channel
56 }
57
58 getAppropriateMagnetUri (actualDownloadSpeed = 0) {
59 if (this.files === undefined || this.files.length === 0) return ''
60 if (this.files.length === 1) return this.files[0].magnetUri
61
62 // Find first video that is good for our download speed (remember they are sorted)
63 let betterResolutionFile = this.files.find(f => actualDownloadSpeed > (f.size / this.duration))
64
65 // If the download speed is too bad, return the lowest resolution we have
66 if (betterResolutionFile === undefined) {
67 betterResolutionFile = this.files.find(f => f.resolution === VideoResolution.H_240P)
68 }
69
70 return betterResolutionFile.magnetUri
71 }
72
73 isRemovableBy (user: AuthUser) {
74 return user && this.isLocal === true && (this.account === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
75 }
76
77 isBlackistableBy (user: AuthUser) {
78 return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false
79 }
80
81 isUpdatableBy (user: AuthUser) {
82 return user && this.isLocal === true && user.username === this.account
83 }
84}
diff --git a/client/src/app/shared/video/video-edit.model.ts b/client/src/app/shared/video/video-edit.model.ts
new file mode 100644
index 000000000..88d23a59f
--- /dev/null
+++ b/client/src/app/shared/video/video-edit.model.ts
@@ -0,0 +1,50 @@
1import { VideoDetails } from './video-details.model'
2import { VideoPrivacy } from '../../../../../shared/models/videos/video-privacy.enum'
3
4export class VideoEdit {
5 category: number
6 licence: number
7 language: number
8 description: string
9 name: string
10 tags: string[]
11 nsfw: boolean
12 channel: number
13 privacy: VideoPrivacy
14 uuid?: string
15 id?: number
16
17 constructor (videoDetails: VideoDetails) {
18 this.id = videoDetails.id
19 this.uuid = videoDetails.uuid
20 this.category = videoDetails.category
21 this.licence = videoDetails.licence
22 this.language = videoDetails.language
23 this.description = videoDetails.description
24 this.name = videoDetails.name
25 this.tags = videoDetails.tags
26 this.nsfw = videoDetails.nsfw
27 this.channel = videoDetails.channel.id
28 this.privacy = videoDetails.privacy
29 }
30
31 patch (values: Object) {
32 Object.keys(values).forEach((key) => {
33 this[key] = values[key]
34 })
35 }
36
37 toJSON () {
38 return {
39 category: this.category,
40 licence: this.licence,
41 language: this.language,
42 description: this.description,
43 name: this.name,
44 tags: this.tags,
45 nsfw: this.nsfw,
46 channel: this.channel,
47 privacy: this.privacy
48 }
49 }
50}
diff --git a/client/src/app/shared/video/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts
new file mode 100644
index 000000000..9e71769cb
--- /dev/null
+++ b/client/src/app/shared/video/video-pagination.model.ts
@@ -0,0 +1,5 @@
1export interface VideoPagination {
2 currentPage: number
3 itemsPerPage: number
4 totalItems: number
5}
diff --git a/client/src/app/shared/video/video-thumbnail.component.html b/client/src/app/shared/video/video-thumbnail.component.html
new file mode 100644
index 000000000..5c698e8f6
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.html
@@ -0,0 +1,10 @@
1<a
2 [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
3class="video-thumbnail"
4>
5<img [attr.src]="video.thumbnailUrl" alt="video thumbnail" [ngClass]="{ 'blur-filter': nsfw }" />
6
7<div class="video-thumbnail-overlay">
8 {{ video.durationLabel }}
9</div>
10</a>
diff --git a/client/src/app/shared/video/video-thumbnail.component.scss b/client/src/app/shared/video/video-thumbnail.component.scss
new file mode 100644
index 000000000..ab4f9bcb1
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.scss
@@ -0,0 +1,28 @@
1.video-thumbnail {
2 display: inline-block;
3 position: relative;
4 border-radius: 4px;
5 overflow: hidden;
6
7 &:hover {
8 text-decoration: none !important;
9 }
10
11 img.blur-filter {
12 filter: blur(5px);
13 transform : scale(1.03);
14 }
15
16 .video-thumbnail-overlay {
17 position: absolute;
18 right: 5px;
19 bottom: 5px;
20 display: inline-block;
21 background-color: rgba(0, 0, 0, 0.7);
22 color: #fff;
23 font-size: 12px;
24 font-weight: $font-bold;
25 border-radius: 3px;
26 padding: 0 5px;
27 }
28}
diff --git a/client/src/app/shared/video/video-thumbnail.component.ts b/client/src/app/shared/video/video-thumbnail.component.ts
new file mode 100644
index 000000000..e543e9903
--- /dev/null
+++ b/client/src/app/shared/video/video-thumbnail.component.ts
@@ -0,0 +1,12 @@
1import { Component, Input } from '@angular/core'
2import { Video } from './video.model'
3
4@Component({
5 selector: 'my-video-thumbnail',
6 styleUrls: [ './video-thumbnail.component.scss' ],
7 templateUrl: './video-thumbnail.component.html'
8})
9export class VideoThumbnailComponent {
10 @Input() video: Video
11 @Input() nsfw = false
12}
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
new file mode 100644
index 000000000..6929c8755
--- /dev/null
+++ b/client/src/app/shared/video/video.model.ts
@@ -0,0 +1,90 @@
1import { Video as VideoServerModel } from '../../../../../shared'
2import { User } from '../'
3
4export class Video implements VideoServerModel {
5 account: string
6 by: string
7 createdAt: Date
8 updatedAt: Date
9 categoryLabel: string
10 category: number
11 licenceLabel: string
12 licence: number
13 languageLabel: string
14 language: number
15 description: string
16 duration: number
17 durationLabel: string
18 id: number
19 uuid: string
20 isLocal: boolean
21 name: string
22 serverHost: string
23 tags: string[]
24 thumbnailPath: string
25 thumbnailUrl: string
26 previewPath: string
27 previewUrl: string
28 embedPath: string
29 embedUrl: string
30 views: number
31 likes: number
32 dislikes: number
33 nsfw: boolean
34
35 private static createByString (account: string, serverHost: string) {
36 return account + '@' + serverHost
37 }
38
39 private static createDurationString (duration: number) {
40 const minutes = Math.floor(duration / 60)
41 const seconds = duration % 60
42 const minutesPadding = minutes >= 10 ? '' : '0'
43 const secondsPadding = seconds >= 10 ? '' : '0'
44
45 return minutesPadding + minutes.toString() + ':' + secondsPadding + seconds.toString()
46 }
47
48 constructor (hash: VideoServerModel) {
49 let absoluteAPIUrl = API_URL
50 if (!absoluteAPIUrl) {
51 // The API is on the same domain
52 absoluteAPIUrl = window.location.origin
53 }
54
55 this.account = hash.account
56 this.createdAt = new Date(hash.createdAt.toString())
57 this.categoryLabel = hash.categoryLabel
58 this.category = hash.category
59 this.licenceLabel = hash.licenceLabel
60 this.licence = hash.licence
61 this.languageLabel = hash.languageLabel
62 this.language = hash.language
63 this.description = hash.description
64 this.duration = hash.duration
65 this.durationLabel = Video.createDurationString(hash.duration)
66 this.id = hash.id
67 this.uuid = hash.uuid
68 this.isLocal = hash.isLocal
69 this.name = hash.name
70 this.serverHost = hash.serverHost
71 this.tags = hash.tags
72 this.thumbnailPath = hash.thumbnailPath
73 this.thumbnailUrl = absoluteAPIUrl + hash.thumbnailPath
74 this.previewPath = hash.previewPath
75 this.previewUrl = absoluteAPIUrl + hash.previewPath
76 this.embedPath = hash.embedPath
77 this.embedUrl = absoluteAPIUrl + hash.embedPath
78 this.views = hash.views
79 this.likes = hash.likes
80 this.dislikes = hash.dislikes
81 this.nsfw = hash.nsfw
82
83 this.by = Video.createByString(hash.account, hash.serverHost)
84 }
85
86 isVideoNSFWForUser (user: User) {
87 // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
88 return (this.nsfw && (!user || user.displayNSFW === false))
89 }
90}
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts
new file mode 100644
index 000000000..b2a26417c
--- /dev/null
+++ b/client/src/app/shared/video/video.service.ts
@@ -0,0 +1,170 @@
1import { HttpClient, HttpParams, HttpRequest } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import 'rxjs/add/operator/catch'
4import 'rxjs/add/operator/map'
5import { Observable } from 'rxjs/Observable'
6import { Video as VideoServerModel, VideoDetails as VideoDetailsServerModel } from '../../../../../shared'
7import { ResultList } from '../../../../../shared/models/result-list.model'
8import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model'
9import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model'
10import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
11import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
12import { RestExtractor } from '../rest/rest-extractor.service'
13import { RestService } from '../rest/rest.service'
14import { Search } from '../search/search.model'
15import { UserService } from '../users/user.service'
16import { SortField } from './sort-field.type'
17import { VideoDetails } from './video-details.model'
18import { VideoEdit } from './video-edit.model'
19import { VideoPagination } from './video-pagination.model'
20import { Video } from './video.model'
21
22@Injectable()
23export class VideoService {
24 private static BASE_VIDEO_URL = API_URL + '/api/v1/videos/'
25
26 constructor (
27 private authHttp: HttpClient,
28 private restExtractor: RestExtractor,
29 private restService: RestService
30 ) {}
31
32 getVideo (uuid: string): Observable<VideoDetails> {
33 return this.authHttp.get<VideoDetailsServerModel>(VideoService.BASE_VIDEO_URL + uuid)
34 .map(videoHash => new VideoDetails(videoHash))
35 .catch((res) => this.restExtractor.handleError(res))
36 }
37
38 viewVideo (uuid: string): Observable<VideoDetails> {
39 return this.authHttp.post(VideoService.BASE_VIDEO_URL + uuid + '/views', {})
40 .map(this.restExtractor.extractDataBool)
41 .catch(this.restExtractor.handleError)
42 }
43
44 updateVideo (video: VideoEdit) {
45 const language = video.language ? video.language : null
46
47 const body: VideoUpdate = {
48 name: video.name,
49 category: video.category,
50 licence: video.licence,
51 language,
52 description: video.description,
53 privacy: video.privacy,
54 tags: video.tags,
55 nsfw: video.nsfw
56 }
57
58 return this.authHttp.put(VideoService.BASE_VIDEO_URL + video.id, body)
59 .map(this.restExtractor.extractDataBool)
60 .catch(this.restExtractor.handleError)
61 }
62
63 uploadVideo (video: FormData) {
64 const req = new HttpRequest('POST', VideoService.BASE_VIDEO_URL + 'upload', video, { reportProgress: true })
65
66 return this.authHttp
67 .request(req)
68 .catch(this.restExtractor.handleError)
69 }
70
71 getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
72 const pagination = this.videoPaginationToRestPagination(videoPagination)
73
74 let params = new HttpParams()
75 params = this.restService.addRestGetParams(params, pagination, sort)
76
77 return this.authHttp.get(UserService.BASE_USERS_URL + '/me/videos', { params })
78 .map(this.extractVideos)
79 .catch((res) => this.restExtractor.handleError(res))
80 }
81
82 getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
83 const pagination = this.videoPaginationToRestPagination(videoPagination)
84
85 let params = new HttpParams()
86 params = this.restService.addRestGetParams(params, pagination, sort)
87
88 return this.authHttp
89 .get(VideoService.BASE_VIDEO_URL, { params })
90 .map(this.extractVideos)
91 .catch((res) => this.restExtractor.handleError(res))
92 }
93
94 searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
95 const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
96
97 const pagination = this.videoPaginationToRestPagination(videoPagination)
98
99 let params = new HttpParams()
100 params = this.restService.addRestGetParams(params, pagination, sort)
101
102 if (search.field) params.set('field', search.field)
103
104 return this.authHttp
105 .get<ResultList<VideoServerModel>>(url, { params })
106 .map(this.extractVideos)
107 .catch((res) => this.restExtractor.handleError(res))
108 }
109
110 removeVideo (id: number) {
111 return this.authHttp
112 .delete(VideoService.BASE_VIDEO_URL + id)
113 .map(this.restExtractor.extractDataBool)
114 .catch((res) => this.restExtractor.handleError(res))
115 }
116
117 loadCompleteDescription (descriptionPath: string) {
118 return this.authHttp
119 .get(API_URL + descriptionPath)
120 .map(res => res['description'])
121 .catch((res) => this.restExtractor.handleError(res))
122 }
123
124 setVideoLike (id: number) {
125 return this.setVideoRate(id, 'like')
126 }
127
128 setVideoDislike (id: number) {
129 return this.setVideoRate(id, 'dislike')
130 }
131
132 getUserVideoRating (id: number): Observable<UserVideoRate> {
133 const url = UserService.BASE_USERS_URL + 'me/videos/' + id + '/rating'
134
135 return this.authHttp
136 .get(url)
137 .catch(res => this.restExtractor.handleError(res))
138 }
139
140 private videoPaginationToRestPagination (videoPagination: VideoPagination) {
141 const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage
142 const count: number = videoPagination.itemsPerPage
143
144 return { start, count }
145 }
146
147 private setVideoRate (id: number, rateType: VideoRateType) {
148 const url = VideoService.BASE_VIDEO_URL + id + '/rate'
149 const body: UserVideoRateUpdate = {
150 rating: rateType
151 }
152
153 return this.authHttp
154 .put(url, body)
155 .map(this.restExtractor.extractDataBool)
156 .catch(res => this.restExtractor.handleError(res))
157 }
158
159 private extractVideos (result: ResultList<VideoServerModel>) {
160 const videosJson = result.data
161 const totalVideos = result.total
162 const videos = []
163
164 for (const videoJson of videosJson) {
165 videos.push(new Video(videoJson))
166 }
167
168 return { videos, totalVideos }
169 }
170}