diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2018-04-17 00:49:04 +0200 |
---|---|---|
committer | Rigel <sendmemail@rigelk.eu> | 2018-04-17 01:09:06 +0200 |
commit | 244e76a552ef05a5067134b1065d26dd89246d8c (patch) | |
tree | a15fcd52bce99797fc9366572fea62a7a44aaabe /client/src/app/shared | |
parent | c36d5a6b98056ef7fec3db43fbee880ee7332dcf (diff) | |
download | PeerTube-244e76a552ef05a5067134b1065d26dd89246d8c.tar.gz PeerTube-244e76a552ef05a5067134b1065d26dd89246d8c.tar.zst PeerTube-244e76a552ef05a5067134b1065d26dd89246d8c.zip |
feature: initial syndication feeds support
Provides rss 2.0, atom 1.0 and json 1.0 feeds for videos (instance and account-wide) on listings and video-watch views.
* still lacks redis caching
* still lacks lastBuildDate support
* still lacks channel-wide support
* still lacks semantic annotation (for licenses, NSFW warnings, etc.)
* still lacks love ( ˘ ³˘)
* RSS: has MRSS support for torrent lists!
* RSS: includes the first torrent in an enclosure
* JSON: lists all torrents in the 'attachments' object
* ATOM: lacking torrent listing support
Advances #23
Partial implementation for the accountId generation in the client, which will need a hotfix to add a way to get the proper account id.
Diffstat (limited to 'client/src/app/shared')
-rw-r--r-- | client/src/app/shared/misc/object-length.pipe.ts | 8 | ||||
-rw-r--r-- | client/src/app/shared/shared.module.ts | 9 | ||||
-rw-r--r-- | client/src/app/shared/video/abstract-video-list.html | 2 | ||||
-rw-r--r-- | client/src/app/shared/video/abstract-video-list.scss | 7 | ||||
-rw-r--r-- | client/src/app/shared/video/abstract-video-list.ts | 6 | ||||
-rw-r--r-- | client/src/app/shared/video/video-feed.component.html | 14 | ||||
-rw-r--r-- | client/src/app/shared/video/video-feed.component.scss | 19 | ||||
-rw-r--r-- | client/src/app/shared/video/video-feed.component.ts | 14 | ||||
-rw-r--r-- | client/src/app/shared/video/video.service.ts | 43 |
9 files changed, 121 insertions, 1 deletions
diff --git a/client/src/app/shared/misc/object-length.pipe.ts b/client/src/app/shared/misc/object-length.pipe.ts new file mode 100644 index 000000000..84d182052 --- /dev/null +++ b/client/src/app/shared/misc/object-length.pipe.ts | |||
@@ -0,0 +1,8 @@ | |||
1 | import { Pipe, PipeTransform } from '@angular/core' | ||
2 | |||
3 | @Pipe({ name: 'myObjectLength' }) | ||
4 | export class ObjectLengthPipe implements PipeTransform { | ||
5 | transform (value: Object) { | ||
6 | return Object.keys(value).length | ||
7 | } | ||
8 | } | ||
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index eb50d45a9..74730e2aa 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts | |||
@@ -10,6 +10,7 @@ import { MarkdownService } from '@app/videos/shared' | |||
10 | 10 | ||
11 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' | 11 | import { BsDropdownModule } from 'ngx-bootstrap/dropdown' |
12 | import { ModalModule } from 'ngx-bootstrap/modal' | 12 | import { ModalModule } from 'ngx-bootstrap/modal' |
13 | import { PopoverModule } from 'ngx-bootstrap/popover' | ||
13 | import { TabsModule } from 'ngx-bootstrap/tabs' | 14 | import { TabsModule } from 'ngx-bootstrap/tabs' |
14 | import { TooltipModule } from 'ngx-bootstrap/tooltip' | 15 | import { TooltipModule } from 'ngx-bootstrap/tooltip' |
15 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' | 16 | import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes' |
@@ -21,11 +22,13 @@ import { EditButtonComponent } from './misc/edit-button.component' | |||
21 | import { FromNowPipe } from './misc/from-now.pipe' | 22 | import { FromNowPipe } from './misc/from-now.pipe' |
22 | import { LoaderComponent } from './misc/loader.component' | 23 | import { LoaderComponent } from './misc/loader.component' |
23 | import { NumberFormatterPipe } from './misc/number-formatter.pipe' | 24 | import { NumberFormatterPipe } from './misc/number-formatter.pipe' |
25 | import { ObjectLengthPipe } from './misc/object-length.pipe' | ||
24 | import { RestExtractor, RestService } from './rest' | 26 | import { RestExtractor, RestService } from './rest' |
25 | import { UserService } from './users' | 27 | import { UserService } from './users' |
26 | import { VideoAbuseService } from './video-abuse' | 28 | import { VideoAbuseService } from './video-abuse' |
27 | import { VideoBlacklistService } from './video-blacklist' | 29 | import { VideoBlacklistService } from './video-blacklist' |
28 | import { VideoMiniatureComponent } from './video/video-miniature.component' | 30 | import { VideoMiniatureComponent } from './video/video-miniature.component' |
31 | import { VideoFeedComponent } from './video/video-feed.component' | ||
29 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' | 32 | import { VideoThumbnailComponent } from './video/video-thumbnail.component' |
30 | import { VideoService } from './video/video.service' | 33 | import { VideoService } from './video/video.service' |
31 | 34 | ||
@@ -39,6 +42,7 @@ import { VideoService } from './video/video.service' | |||
39 | 42 | ||
40 | BsDropdownModule.forRoot(), | 43 | BsDropdownModule.forRoot(), |
41 | ModalModule.forRoot(), | 44 | ModalModule.forRoot(), |
45 | PopoverModule.forRoot(), | ||
42 | TabsModule.forRoot(), | 46 | TabsModule.forRoot(), |
43 | TooltipModule.forRoot(), | 47 | TooltipModule.forRoot(), |
44 | 48 | ||
@@ -50,9 +54,11 @@ import { VideoService } from './video/video.service' | |||
50 | LoaderComponent, | 54 | LoaderComponent, |
51 | VideoThumbnailComponent, | 55 | VideoThumbnailComponent, |
52 | VideoMiniatureComponent, | 56 | VideoMiniatureComponent, |
57 | VideoFeedComponent, | ||
53 | DeleteButtonComponent, | 58 | DeleteButtonComponent, |
54 | EditButtonComponent, | 59 | EditButtonComponent, |
55 | NumberFormatterPipe, | 60 | NumberFormatterPipe, |
61 | ObjectLengthPipe, | ||
56 | FromNowPipe, | 62 | FromNowPipe, |
57 | MarkdownTextareaComponent, | 63 | MarkdownTextareaComponent, |
58 | InfiniteScrollerDirective, | 64 | InfiniteScrollerDirective, |
@@ -68,6 +74,7 @@ import { VideoService } from './video/video.service' | |||
68 | 74 | ||
69 | BsDropdownModule, | 75 | BsDropdownModule, |
70 | ModalModule, | 76 | ModalModule, |
77 | PopoverModule, | ||
71 | TabsModule, | 78 | TabsModule, |
72 | TooltipModule, | 79 | TooltipModule, |
73 | PrimeSharedModule, | 80 | PrimeSharedModule, |
@@ -77,6 +84,7 @@ import { VideoService } from './video/video.service' | |||
77 | LoaderComponent, | 84 | LoaderComponent, |
78 | VideoThumbnailComponent, | 85 | VideoThumbnailComponent, |
79 | VideoMiniatureComponent, | 86 | VideoMiniatureComponent, |
87 | VideoFeedComponent, | ||
80 | DeleteButtonComponent, | 88 | DeleteButtonComponent, |
81 | EditButtonComponent, | 89 | EditButtonComponent, |
82 | MarkdownTextareaComponent, | 90 | MarkdownTextareaComponent, |
@@ -84,6 +92,7 @@ import { VideoService } from './video/video.service' | |||
84 | HelpComponent, | 92 | HelpComponent, |
85 | 93 | ||
86 | NumberFormatterPipe, | 94 | NumberFormatterPipe, |
95 | ObjectLengthPipe, | ||
87 | FromNowPipe | 96 | FromNowPipe |
88 | ], | 97 | ], |
89 | 98 | ||
diff --git a/client/src/app/shared/video/abstract-video-list.html b/client/src/app/shared/video/abstract-video-list.html index 94a38019d..cb04e07b4 100644 --- a/client/src/app/shared/video/abstract-video-list.html +++ b/client/src/app/shared/video/abstract-video-list.html | |||
@@ -2,9 +2,9 @@ | |||
2 | <div class="title-page title-page-single"> | 2 | <div class="title-page title-page-single"> |
3 | {{ titlePage }} | 3 | {{ titlePage }} |
4 | </div> | 4 | </div> |
5 | <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed> | ||
5 | 6 | ||
6 | <div *ngIf="pagination.totalItems === 0">No results.</div> | 7 | <div *ngIf="pagination.totalItems === 0">No results.</div> |
7 | |||
8 | <div | 8 | <div |
9 | myInfiniteScroller | 9 | myInfiniteScroller |
10 | [pageHeight]="pageHeight" | 10 | [pageHeight]="pageHeight" |
diff --git a/client/src/app/shared/video/abstract-video-list.scss b/client/src/app/shared/video/abstract-video-list.scss index 63538a089..b75907dc9 100644 --- a/client/src/app/shared/video/abstract-video-list.scss +++ b/client/src/app/shared/video/abstract-video-list.scss | |||
@@ -1,3 +1,5 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
1 | .videos { | 3 | .videos { |
2 | text-align: center; | 4 | text-align: center; |
3 | 5 | ||
@@ -6,6 +8,11 @@ | |||
6 | } | 8 | } |
7 | } | 9 | } |
8 | 10 | ||
11 | my-video-feed { | ||
12 | display: inline-block; | ||
13 | margin-left: -45px; | ||
14 | } | ||
15 | |||
9 | @media screen and (max-width: 500px) { | 16 | @media screen and (max-width: 500px) { |
10 | .videos { | 17 | .videos { |
11 | text-align: center; | 18 | text-align: center; |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 1b9a519bd..024834dfc 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
3 | import { isInMobileView } from '@app/shared/misc/utils' | 3 | import { isInMobileView } from '@app/shared/misc/utils' |
4 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' | 4 | import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive' |
5 | import { NotificationsService } from 'angular2-notifications' | 5 | import { NotificationsService } from 'angular2-notifications' |
6 | import { PopoverModule } from 'ngx-bootstrap/popover' | ||
6 | import 'rxjs/add/operator/debounceTime' | 7 | import 'rxjs/add/operator/debounceTime' |
7 | import { Observable } from 'rxjs/Observable' | 8 | import { Observable } from 'rxjs/Observable' |
8 | import { fromEvent } from 'rxjs/observable/fromEvent' | 9 | import { fromEvent } from 'rxjs/observable/fromEvent' |
@@ -11,6 +12,8 @@ import { AuthService } from '../../core/auth' | |||
11 | import { ComponentPagination } from '../rest/component-pagination.model' | 12 | import { ComponentPagination } from '../rest/component-pagination.model' |
12 | import { SortField } from './sort-field.type' | 13 | import { SortField } from './sort-field.type' |
13 | import { Video } from './video.model' | 14 | import { Video } from './video.model' |
15 | import { FeedFormat } from '../../../../../shared' | ||
16 | import { VideoFeedComponent } from '@app/shared/video/video-feed.component' | ||
14 | 17 | ||
15 | export abstract class AbstractVideoList implements OnInit, OnDestroy { | 18 | export abstract class AbstractVideoList implements OnInit, OnDestroy { |
16 | private static LINES_PER_PAGE = 4 | 19 | private static LINES_PER_PAGE = 4 |
@@ -25,6 +28,8 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
25 | } | 28 | } |
26 | sort: SortField = '-createdAt' | 29 | sort: SortField = '-createdAt' |
27 | defaultSort: SortField = '-createdAt' | 30 | defaultSort: SortField = '-createdAt' |
31 | syndicationItems = {} | ||
32 | |||
28 | loadOnInit = true | 33 | loadOnInit = true |
29 | pageHeight: number | 34 | pageHeight: number |
30 | videoWidth: number | 35 | videoWidth: number |
@@ -47,6 +52,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy { | |||
47 | private resizeSubscription: Subscription | 52 | private resizeSubscription: Subscription |
48 | 53 | ||
49 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> | 54 | abstract getVideosObservable (page: number): Observable<{ videos: Video[], totalVideos: number}> |
55 | abstract generateSyndicationList () | ||
50 | 56 | ||
51 | get user () { | 57 | get user () { |
52 | return this.authService.getUser() | 58 | return this.authService.getUser() |
diff --git a/client/src/app/shared/video/video-feed.component.html b/client/src/app/shared/video/video-feed.component.html new file mode 100644 index 000000000..7733ef221 --- /dev/null +++ b/client/src/app/shared/video/video-feed.component.html | |||
@@ -0,0 +1,14 @@ | |||
1 | <div class="video-feed"> | ||
2 | <span *ngIf="(syndicationItems | myObjectLength) >= 1" class="icon icon-syndication" | ||
3 | [popover]="feedsList" | ||
4 | placement="bottom" | ||
5 | [outsideClick]="true"> | ||
6 | </span> | ||
7 | |||
8 | <ng-template #feedsList> | ||
9 | <div *ngFor="let key of syndicationItems | keys"> | ||
10 | <a [href]="syndicationItems[key]">{{ key }}</a> | ||
11 | </div> | ||
12 | </ng-template> | ||
13 | </div> | ||
14 | \ No newline at end of file | ||
diff --git a/client/src/app/shared/video/video-feed.component.scss b/client/src/app/shared/video/video-feed.component.scss new file mode 100644 index 000000000..2efeb405e --- /dev/null +++ b/client/src/app/shared/video/video-feed.component.scss | |||
@@ -0,0 +1,19 @@ | |||
1 | @import '_mixins'; | ||
2 | |||
3 | .video-feed { | ||
4 | a { | ||
5 | @include disable-default-a-behaviour; | ||
6 | |||
7 | color: black; | ||
8 | } | ||
9 | |||
10 | .icon { | ||
11 | @include icon(12px); | ||
12 | |||
13 | &.icon-syndication { | ||
14 | position: relative; | ||
15 | top: -2px; | ||
16 | background-image: url('../../../assets/images/global/syndication.svg'); | ||
17 | } | ||
18 | } | ||
19 | } \ No newline at end of file | ||
diff --git a/client/src/app/shared/video/video-feed.component.ts b/client/src/app/shared/video/video-feed.component.ts new file mode 100644 index 000000000..41257ca99 --- /dev/null +++ b/client/src/app/shared/video/video-feed.component.ts | |||
@@ -0,0 +1,14 @@ | |||
1 | import { Component, Input, OnChanges, SimpleChanges } from '@angular/core' | ||
2 | |||
3 | @Component({ | ||
4 | selector: 'my-video-feed', | ||
5 | styleUrls: [ './video-feed.component.scss' ], | ||
6 | templateUrl: './video-feed.component.html' | ||
7 | }) | ||
8 | export class VideoFeedComponent implements OnChanges { | ||
9 | @Input() syndicationItems | ||
10 | |||
11 | ngOnChanges (changes: SimpleChanges) { | ||
12 | this.syndicationItems = changes.syndicationItems.currentValue | ||
13 | } | ||
14 | } | ||
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 0a8894fd9..009155410 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -8,6 +8,7 @@ import { ResultList } from '../../../../../shared/models/result-list.model' | |||
8 | import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model' | 8 | import { UserVideoRateUpdate } from '../../../../../shared/models/videos/user-video-rate-update.model' |
9 | import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model' | 9 | import { UserVideoRate } from '../../../../../shared/models/videos/user-video-rate.model' |
10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' | 10 | import { VideoFilter } from '../../../../../shared/models/videos/video-query.type' |
11 | import { FeedFormat } from '../../../../../shared/models/feeds/feed-format.enum' | ||
11 | import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' | 12 | import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' |
12 | import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' | 13 | import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' |
13 | import { environment } from '../../../environments/environment' | 14 | import { environment } from '../../../environments/environment' |
@@ -24,6 +25,7 @@ import { objectToFormData } from '@app/shared/misc/utils' | |||
24 | @Injectable() | 25 | @Injectable() |
25 | export class VideoService { | 26 | export class VideoService { |
26 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | 27 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' |
28 | private static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' | ||
27 | 29 | ||
28 | constructor ( | 30 | constructor ( |
29 | private authHttp: HttpClient, | 31 | private authHttp: HttpClient, |
@@ -115,6 +117,47 @@ export class VideoService { | |||
115 | .catch((res) => this.restExtractor.handleError(res)) | 117 | .catch((res) => this.restExtractor.handleError(res)) |
116 | } | 118 | } |
117 | 119 | ||
120 | baseFeed () { | ||
121 | const feed = {} | ||
122 | |||
123 | for (let item in FeedFormat) { | ||
124 | feed[FeedFormat[item]] = VideoService.BASE_FEEDS_URL + item.toLowerCase() | ||
125 | } | ||
126 | |||
127 | return feed | ||
128 | } | ||
129 | |||
130 | getFeed ( | ||
131 | filter?: VideoFilter | ||
132 | ) { | ||
133 | let params = this.restService.addRestGetParams(new HttpParams()) | ||
134 | const feed = this.baseFeed() | ||
135 | |||
136 | if (filter) { | ||
137 | params = params.set('filter', filter) | ||
138 | } | ||
139 | for (let item in feed) { | ||
140 | feed[item] = feed[item] + ((params.toString().length === 0) ? '' : '?') + params.toString() | ||
141 | } | ||
142 | |||
143 | return feed | ||
144 | } | ||
145 | |||
146 | getAccountFeed ( | ||
147 | accountId: number, | ||
148 | host?: string | ||
149 | ) { | ||
150 | let params = this.restService.addRestGetParams(new HttpParams()) | ||
151 | const feed = this.baseFeed() | ||
152 | |||
153 | params = params.set('accountId', accountId.toString()) | ||
154 | for (let item in feed) { | ||
155 | feed[item] = feed[item] + ((params.toString().length === 0) ? '' : '?') + params.toString() | ||
156 | } | ||
157 | |||
158 | return feed | ||
159 | } | ||
160 | |||
118 | searchVideos ( | 161 | searchVideos ( |
119 | search: string, | 162 | search: string, |
120 | videoPagination: ComponentPagination, | 163 | videoPagination: ComponentPagination, |