1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 10:49:28 +02:00

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.
This commit is contained in:
Rigel Kent 2018-04-17 00:49:04 +02:00 committed by Rigel
parent c36d5a6b98
commit 244e76a552
No known key found for this signature in database
GPG key ID: EA12971B0E438F36
33 changed files with 608 additions and 84 deletions

View file

@ -246,7 +246,7 @@ export class AccountModel extends Model<AccountModel> {
const actor = this.Actor.toFormattedJSON()
const account = {
id: this.id,
displayName: this.name,
displayName: this.getDisplayName(),
description: this.description,
createdAt: this.createdAt,
updatedAt: this.updatedAt
@ -266,4 +266,8 @@ export class AccountModel extends Model<AccountModel> {
isOwned () {
return this.Actor.isOwned()
}
getDisplayName () {
return this.name
}
}

View file

@ -95,14 +95,15 @@ enum ScopeNames {
}
@Scopes({
[ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter) => ({
where: {
id: {
[Sequelize.Op.notIn]: Sequelize.literal(
'(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
),
[ Sequelize.Op.in ]: Sequelize.literal(
'(' +
[ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => {
const query: IFindOptions<VideoModel> = {
where: {
id: {
[Sequelize.Op.notIn]: Sequelize.literal(
'(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
),
[ Sequelize.Op.in ]: Sequelize.literal(
'(' +
'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) +
@ -113,45 +114,55 @@ enum ScopeNames {
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) +
')'
)
')'
)
},
privacy: VideoPrivacy.PUBLIC
},
privacy: VideoPrivacy.PUBLIC
},
include: [
{
attributes: [ 'name', 'description' ],
model: VideoChannelModel.unscoped(),
required: true,
include: [
{
attributes: [ 'name' ],
model: AccountModel.unscoped(),
required: true,
include: [
{
attributes: [ 'preferredUsername', 'url', 'serverId' ],
model: ActorModel.unscoped(),
required: true,
where: VideoModel.buildActorWhereWithFilter(filter),
include: [
{
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
},
{
model: AvatarModel.unscoped(),
required: false
}
]
}
]
}
]
}
]
}),
include: [
{
attributes: [ 'name', 'description' ],
model: VideoChannelModel.unscoped(),
required: true,
include: [
{
attributes: [ 'name' ],
model: AccountModel.unscoped(),
required: true,
include: [
{
attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ],
model: ActorModel.unscoped(),
required: true,
where: VideoModel.buildActorWhereWithFilter(filter),
include: [
{
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
},
{
model: AvatarModel.unscoped(),
required: false
}
]
}
]
}
]
}
]
}
if (withFiles === true) {
query.include.push({
model: VideoFileModel.unscoped(),
required: true
})
}
return query
},
[ScopeNames.WITH_ACCOUNT_DETAILS]: {
include: [
{
@ -629,8 +640,8 @@ export class VideoModel extends Model<VideoModel> {
})
}
static listUserVideosForApi (userId: number, start: number, count: number, sort: string) {
const query = {
static listUserVideosForApi (userId: number, start: number, count: number, sort: string, withFiles = false) {
const query: IFindOptions<VideoModel> = {
offset: start,
limit: count,
order: getSort(sort),
@ -651,6 +662,13 @@ export class VideoModel extends Model<VideoModel> {
]
}
if (withFiles === true) {
query.include.push({
model: VideoFileModel.unscoped(),
required: true
})
}
return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
return {
data: rows,
@ -659,7 +677,7 @@ export class VideoModel extends Model<VideoModel> {
})
}
static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter) {
static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) {
const query = {
offset: start,
limit: count,
@ -668,7 +686,7 @@ export class VideoModel extends Model<VideoModel> {
const serverActor = await getServerActor()
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter ] })
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] })
.findAndCountAll(query)
.then(({ rows, count }) => {
return {
@ -707,7 +725,8 @@ export class VideoModel extends Model<VideoModel> {
const serverActor = await getServerActor()
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] })
.findAndCountAll(query).then(({ rows, count }) => {
.findAndCountAll(query)
.then(({ rows, count }) => {
return {
data: rows,
total: count
@ -1006,31 +1025,36 @@ export class VideoModel extends Model<VideoModel> {
}
// Format and sort video files
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
detailsJson.files = this.VideoFiles
.map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p'
return {
resolution: {
id: videoFile.resolution,
label: resolutionLabel
},
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size,
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp)
} as VideoFile
})
.sort((a, b) => {
if (a.resolution.id < b.resolution.id) return 1
if (a.resolution.id === b.resolution.id) return 0
return -1
})
detailsJson.files = this.getFormattedVideoFilesJSON()
return Object.assign(formattedJson, detailsJson)
}
getFormattedVideoFilesJSON (): VideoFile[] {
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
return this.VideoFiles
.map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p'
return {
resolution: {
id: videoFile.resolution,
label: resolutionLabel
},
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size,
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp)
} as VideoFile
})
.sort((a, b) => {
if (a.resolution.id < b.resolution.id) return 1
if (a.resolution.id === b.resolution.id) return 0
return -1
})
}
toActivityPubObject (): VideoTorrentObject {
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
if (!this.Tags) this.Tags = []