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 /server/models | |
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 'server/models')
-rw-r--r-- | server/models/account/account.ts | 6 | ||||
-rw-r--r-- | server/models/video/video.ts | 166 |
2 files changed, 100 insertions, 72 deletions
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index c5955ef3b..3ff59887d 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -246,7 +246,7 @@ export class AccountModel extends Model<AccountModel> { | |||
246 | const actor = this.Actor.toFormattedJSON() | 246 | const actor = this.Actor.toFormattedJSON() |
247 | const account = { | 247 | const account = { |
248 | id: this.id, | 248 | id: this.id, |
249 | displayName: this.name, | 249 | displayName: this.getDisplayName(), |
250 | description: this.description, | 250 | description: this.description, |
251 | createdAt: this.createdAt, | 251 | createdAt: this.createdAt, |
252 | updatedAt: this.updatedAt | 252 | updatedAt: this.updatedAt |
@@ -266,4 +266,8 @@ export class AccountModel extends Model<AccountModel> { | |||
266 | isOwned () { | 266 | isOwned () { |
267 | return this.Actor.isOwned() | 267 | return this.Actor.isOwned() |
268 | } | 268 | } |
269 | |||
270 | getDisplayName () { | ||
271 | return this.name | ||
272 | } | ||
269 | } | 273 | } |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 54fe54535..240a2b5a2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -95,14 +95,15 @@ enum ScopeNames { | |||
95 | } | 95 | } |
96 | 96 | ||
97 | @Scopes({ | 97 | @Scopes({ |
98 | [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter) => ({ | 98 | [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => { |
99 | where: { | 99 | const query: IFindOptions<VideoModel> = { |
100 | id: { | 100 | where: { |
101 | [Sequelize.Op.notIn]: Sequelize.literal( | 101 | id: { |
102 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' | 102 | [Sequelize.Op.notIn]: Sequelize.literal( |
103 | ), | 103 | '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' |
104 | [ Sequelize.Op.in ]: Sequelize.literal( | 104 | ), |
105 | '(' + | 105 | [ Sequelize.Op.in ]: Sequelize.literal( |
106 | '(' + | ||
106 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + | 107 | 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + |
107 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + | 108 | 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + |
108 | 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + | 109 | 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + |
@@ -113,45 +114,55 @@ enum ScopeNames { | |||
113 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + | 114 | 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + |
114 | 'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + | 115 | 'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + |
115 | 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + | 116 | 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + |
116 | ')' | 117 | ')' |
117 | ) | 118 | ) |
119 | }, | ||
120 | privacy: VideoPrivacy.PUBLIC | ||
118 | }, | 121 | }, |
119 | privacy: VideoPrivacy.PUBLIC | 122 | include: [ |
120 | }, | 123 | { |
121 | include: [ | 124 | attributes: [ 'name', 'description' ], |
122 | { | 125 | model: VideoChannelModel.unscoped(), |
123 | attributes: [ 'name', 'description' ], | 126 | required: true, |
124 | model: VideoChannelModel.unscoped(), | 127 | include: [ |
125 | required: true, | 128 | { |
126 | include: [ | 129 | attributes: [ 'name' ], |
127 | { | 130 | model: AccountModel.unscoped(), |
128 | attributes: [ 'name' ], | 131 | required: true, |
129 | model: AccountModel.unscoped(), | 132 | include: [ |
130 | required: true, | 133 | { |
131 | include: [ | 134 | attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ], |
132 | { | 135 | model: ActorModel.unscoped(), |
133 | attributes: [ 'preferredUsername', 'url', 'serverId' ], | 136 | required: true, |
134 | model: ActorModel.unscoped(), | 137 | where: VideoModel.buildActorWhereWithFilter(filter), |
135 | required: true, | 138 | include: [ |
136 | where: VideoModel.buildActorWhereWithFilter(filter), | 139 | { |
137 | include: [ | 140 | attributes: [ 'host' ], |
138 | { | 141 | model: ServerModel.unscoped(), |
139 | attributes: [ 'host' ], | 142 | required: false |
140 | model: ServerModel.unscoped(), | 143 | }, |
141 | required: false | 144 | { |
142 | }, | 145 | model: AvatarModel.unscoped(), |
143 | { | 146 | required: false |
144 | model: AvatarModel.unscoped(), | 147 | } |
145 | required: false | 148 | ] |
146 | } | 149 | } |
147 | ] | 150 | ] |
148 | } | 151 | } |
149 | ] | 152 | ] |
150 | } | 153 | } |
151 | ] | 154 | ] |
152 | } | 155 | } |
153 | ] | 156 | |
154 | }), | 157 | if (withFiles === true) { |
158 | query.include.push({ | ||
159 | model: VideoFileModel.unscoped(), | ||
160 | required: true | ||
161 | }) | ||
162 | } | ||
163 | |||
164 | return query | ||
165 | }, | ||
155 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | 166 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
156 | include: [ | 167 | include: [ |
157 | { | 168 | { |
@@ -629,8 +640,8 @@ export class VideoModel extends Model<VideoModel> { | |||
629 | }) | 640 | }) |
630 | } | 641 | } |
631 | 642 | ||
632 | static listUserVideosForApi (userId: number, start: number, count: number, sort: string) { | 643 | static listUserVideosForApi (userId: number, start: number, count: number, sort: string, withFiles = false) { |
633 | const query = { | 644 | const query: IFindOptions<VideoModel> = { |
634 | offset: start, | 645 | offset: start, |
635 | limit: count, | 646 | limit: count, |
636 | order: getSort(sort), | 647 | order: getSort(sort), |
@@ -651,6 +662,13 @@ export class VideoModel extends Model<VideoModel> { | |||
651 | ] | 662 | ] |
652 | } | 663 | } |
653 | 664 | ||
665 | if (withFiles === true) { | ||
666 | query.include.push({ | ||
667 | model: VideoFileModel.unscoped(), | ||
668 | required: true | ||
669 | }) | ||
670 | } | ||
671 | |||
654 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { | 672 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { |
655 | return { | 673 | return { |
656 | data: rows, | 674 | data: rows, |
@@ -659,7 +677,7 @@ export class VideoModel extends Model<VideoModel> { | |||
659 | }) | 677 | }) |
660 | } | 678 | } |
661 | 679 | ||
662 | static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter) { | 680 | static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) { |
663 | const query = { | 681 | const query = { |
664 | offset: start, | 682 | offset: start, |
665 | limit: count, | 683 | limit: count, |
@@ -668,7 +686,7 @@ export class VideoModel extends Model<VideoModel> { | |||
668 | 686 | ||
669 | const serverActor = await getServerActor() | 687 | const serverActor = await getServerActor() |
670 | 688 | ||
671 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter ] }) | 689 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] }) |
672 | .findAndCountAll(query) | 690 | .findAndCountAll(query) |
673 | .then(({ rows, count }) => { | 691 | .then(({ rows, count }) => { |
674 | return { | 692 | return { |
@@ -707,7 +725,8 @@ export class VideoModel extends Model<VideoModel> { | |||
707 | const serverActor = await getServerActor() | 725 | const serverActor = await getServerActor() |
708 | 726 | ||
709 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) | 727 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) |
710 | .findAndCountAll(query).then(({ rows, count }) => { | 728 | .findAndCountAll(query) |
729 | .then(({ rows, count }) => { | ||
711 | return { | 730 | return { |
712 | data: rows, | 731 | data: rows, |
713 | total: count | 732 | total: count |
@@ -1006,29 +1025,34 @@ export class VideoModel extends Model<VideoModel> { | |||
1006 | } | 1025 | } |
1007 | 1026 | ||
1008 | // Format and sort video files | 1027 | // Format and sort video files |
1028 | detailsJson.files = this.getFormattedVideoFilesJSON() | ||
1029 | |||
1030 | return Object.assign(formattedJson, detailsJson) | ||
1031 | } | ||
1032 | |||
1033 | getFormattedVideoFilesJSON (): VideoFile[] { | ||
1009 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() | 1034 | const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() |
1010 | detailsJson.files = this.VideoFiles | ||
1011 | .map(videoFile => { | ||
1012 | let resolutionLabel = videoFile.resolution + 'p' | ||
1013 | 1035 | ||
1014 | return { | 1036 | return this.VideoFiles |
1015 | resolution: { | 1037 | .map(videoFile => { |
1016 | id: videoFile.resolution, | 1038 | let resolutionLabel = videoFile.resolution + 'p' |
1017 | label: resolutionLabel | ||
1018 | }, | ||
1019 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
1020 | size: videoFile.size, | ||
1021 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | ||
1022 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp) | ||
1023 | } as VideoFile | ||
1024 | }) | ||
1025 | .sort((a, b) => { | ||
1026 | if (a.resolution.id < b.resolution.id) return 1 | ||
1027 | if (a.resolution.id === b.resolution.id) return 0 | ||
1028 | return -1 | ||
1029 | }) | ||
1030 | 1039 | ||
1031 | return Object.assign(formattedJson, detailsJson) | 1040 | return { |
1041 | resolution: { | ||
1042 | id: videoFile.resolution, | ||
1043 | label: resolutionLabel | ||
1044 | }, | ||
1045 | magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), | ||
1046 | size: videoFile.size, | ||
1047 | torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), | ||
1048 | fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp) | ||
1049 | } as VideoFile | ||
1050 | }) | ||
1051 | .sort((a, b) => { | ||
1052 | if (a.resolution.id < b.resolution.id) return 1 | ||
1053 | if (a.resolution.id === b.resolution.id) return 0 | ||
1054 | return -1 | ||
1055 | }) | ||
1032 | } | 1056 | } |
1033 | 1057 | ||
1034 | toActivityPubObject (): VideoTorrentObject { | 1058 | toActivityPubObject (): VideoTorrentObject { |