Add user history in import/export
This commit is contained in:
parent
7be401ac76
commit
98781f353d
|
@ -8,5 +8,6 @@ export * from './followers-export.model.js'
|
||||||
export * from './following-export.model.js'
|
export * from './following-export.model.js'
|
||||||
export * from './likes-export.model.js'
|
export * from './likes-export.model.js'
|
||||||
export * from './user-settings-export.model.js'
|
export * from './user-settings-export.model.js'
|
||||||
|
export * from './user-video-history-export.js'
|
||||||
export * from './video-export.model.js'
|
export * from './video-export.model.js'
|
||||||
export * from './video-playlists-export.model.js'
|
export * from './video-playlists-export.model.js'
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
export interface UserVideoHistoryExportJSON {
|
||||||
|
watchedVideos: {
|
||||||
|
videoUrl: string
|
||||||
|
lastTimecode: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
archiveFiles?: never
|
||||||
|
}
|
|
@ -16,5 +16,7 @@ export interface UserImportResultSummary {
|
||||||
|
|
||||||
account: Summary
|
account: Summary
|
||||||
userSettings: Summary
|
userSettings: Summary
|
||||||
|
|
||||||
|
userVideoHistory: Summary
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
UserExportState,
|
UserExportState,
|
||||||
UserNotificationSettingValue,
|
UserNotificationSettingValue,
|
||||||
UserSettingsExportJSON,
|
UserSettingsExportJSON,
|
||||||
|
UserVideoHistoryExportJSON,
|
||||||
VideoChapterObject,
|
VideoChapterObject,
|
||||||
VideoCommentObject,
|
VideoCommentObject,
|
||||||
VideoCreateResult,
|
VideoCreateResult,
|
||||||
|
@ -468,6 +469,22 @@ function runTest (withObjectStorage: boolean) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const json = await parseZIPJSONFile<UserVideoHistoryExportJSON>(zip, 'peertube/video-history.json')
|
||||||
|
|
||||||
|
expect(json.watchedVideos).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(json.watchedVideos[0].createdAt).to.exist
|
||||||
|
expect(json.watchedVideos[0].updatedAt).to.exist
|
||||||
|
expect(json.watchedVideos[0].lastTimecode).to.equal(4)
|
||||||
|
expect(json.watchedVideos[0].videoUrl).to.equal(server.url + '/videos/watch/' + noahVideo.uuid)
|
||||||
|
|
||||||
|
expect(json.watchedVideos[1].createdAt).to.exist
|
||||||
|
expect(json.watchedVideos[1].updatedAt).to.exist
|
||||||
|
expect(json.watchedVideos[1].lastTimecode).to.equal(2)
|
||||||
|
expect(json.watchedVideos[1].videoUrl).to.equal(remoteServer.url + '/videos/watch/' + externalVideo.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const json = await parseZIPJSONFile<VideoExportJSON>(zip, 'peertube/videos.json')
|
const json = await parseZIPJSONFile<VideoExportJSON>(zip, 'peertube/videos.json')
|
||||||
|
|
||||||
|
|
|
@ -330,6 +330,18 @@ function runTest (withObjectStorage: boolean) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should have correctly imported user video history', async function () {
|
||||||
|
const { data } = await remoteServer.history.list({ token: remoteNoahToken })
|
||||||
|
|
||||||
|
expect(data).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
expect(data[0].userHistory.currentTime).to.equal(2)
|
||||||
|
expect(data[0].url).to.equal(remoteServer.url + '/videos/watch/' + externalVideo.uuid)
|
||||||
|
|
||||||
|
expect(data[1].userHistory.currentTime).to.equal(4)
|
||||||
|
expect(data[1].url).to.equal(server.url + '/videos/watch/' + noahVideo.uuid)
|
||||||
|
})
|
||||||
|
|
||||||
it('Should have correctly imported user videos', async function () {
|
it('Should have correctly imported user videos', async function () {
|
||||||
const { data } = await remoteServer.videos.listMyVideos({ token: remoteNoahToken })
|
const { data } = await remoteServer.videos.listMyVideos({ token: remoteNoahToken })
|
||||||
expect(data).to.have.lengthOf(5)
|
expect(data).to.have.lengthOf(5)
|
||||||
|
|
|
@ -311,6 +311,10 @@ export async function prepareImportExportTests (options: {
|
||||||
token: noahToken
|
token: noahToken
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Views
|
||||||
|
await server.views.view({ id: noahVideo.uuid, token: noahToken, currentTime: 4 })
|
||||||
|
await server.views.view({ id: externalVideo.uuid, token: noahToken, currentTime: 2 })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
rootId,
|
rootId,
|
||||||
|
|
||||||
|
|
|
@ -817,6 +817,10 @@ const NSFW_POLICY_TYPES: { [ id: string ]: NSFWPolicyType } = {
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const USER_EXPORT_MAX_ITEMS = 1000
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Express static paths (router)
|
// Express static paths (router)
|
||||||
const STATIC_PATHS = {
|
const STATIC_PATHS = {
|
||||||
// TODO: deprecated in v6, to remove
|
// TODO: deprecated in v6, to remove
|
||||||
|
@ -1255,6 +1259,7 @@ export {
|
||||||
STATIC_MAX_AGE,
|
STATIC_MAX_AGE,
|
||||||
VIEWER_SYNC_REDIS,
|
VIEWER_SYNC_REDIS,
|
||||||
STATIC_PATHS,
|
STATIC_PATHS,
|
||||||
|
USER_EXPORT_MAX_ITEMS,
|
||||||
VIDEO_IMPORT_TIMEOUT,
|
VIDEO_IMPORT_TIMEOUT,
|
||||||
VIDEO_PLAYLIST_TYPES,
|
VIDEO_PLAYLIST_TYPES,
|
||||||
MAX_LOGS_OUTPUT_CHARACTERS,
|
MAX_LOGS_OUTPUT_CHARACTERS,
|
||||||
|
|
|
@ -44,3 +44,6 @@ block content
|
||||||
li
|
li
|
||||||
strong Videos:
|
strong Videos:
|
||||||
+displaySummary(resultStats.videos)
|
+displaySummary(resultStats.videos)
|
||||||
|
li
|
||||||
|
strong Video history:
|
||||||
|
+displaySummary(resultStats.userVideoHistory)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { activityPubContextify } from '@server/helpers/activity-pub-utils.js'
|
||||||
export class DislikesExporter extends AbstractUserExporter <DislikesExportJSON> {
|
export class DislikesExporter extends AbstractUserExporter <DislikesExportJSON> {
|
||||||
|
|
||||||
async export () {
|
async export () {
|
||||||
const dislikes = await AccountVideoRateModel.listRatesOfAccountId(this.user.Account.id, 'dislike')
|
const dislikes = await AccountVideoRateModel.listRatesOfAccountIdForExport(this.user.Account.id, 'dislike')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
json: {
|
json: {
|
||||||
|
|
|
@ -8,5 +8,6 @@ export * from './following-exporter.js'
|
||||||
export * from './likes-exporter.js'
|
export * from './likes-exporter.js'
|
||||||
export * from './abstract-user-exporter.js'
|
export * from './abstract-user-exporter.js'
|
||||||
export * from './user-settings-exporter.js'
|
export * from './user-settings-exporter.js'
|
||||||
|
export * from './user-video-history-exporter.js'
|
||||||
export * from './video-playlists-exporter.js'
|
export * from './video-playlists-exporter.js'
|
||||||
export * from './videos-exporter.js'
|
export * from './videos-exporter.js'
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { getContextFilter } from '@server/lib/activitypub/context.js'
|
||||||
export class LikesExporter extends AbstractUserExporter <LikesExportJSON> {
|
export class LikesExporter extends AbstractUserExporter <LikesExportJSON> {
|
||||||
|
|
||||||
async export () {
|
async export () {
|
||||||
const likes = await AccountVideoRateModel.listRatesOfAccountId(this.user.Account.id, 'like')
|
const likes = await AccountVideoRateModel.listRatesOfAccountIdForExport(this.user.Account.id, 'like')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
json: {
|
json: {
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { UserVideoHistoryExportJSON } from '@peertube/peertube-models'
|
||||||
|
import { AbstractUserExporter } from './abstract-user-exporter.js'
|
||||||
|
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
||||||
|
|
||||||
|
export class UserVideoHistoryExporter extends AbstractUserExporter <UserVideoHistoryExportJSON> {
|
||||||
|
|
||||||
|
async export () {
|
||||||
|
const videos = await UserVideoHistoryModel.listForExport(this.user)
|
||||||
|
|
||||||
|
return {
|
||||||
|
json: {
|
||||||
|
watchedVideos: videos.map(v => ({
|
||||||
|
videoUrl: v.videoUrl,
|
||||||
|
lastTimecode: v.currentTime,
|
||||||
|
createdAt: v.createdAt.toISOString(),
|
||||||
|
updatedAt: v.updatedAt.toISOString()
|
||||||
|
}))
|
||||||
|
} as UserVideoHistoryExportJSON,
|
||||||
|
|
||||||
|
staticFiles: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ import { MVideoSource } from '@server/types/models/video/video-source.js'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||||
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
import { buildChaptersAPHasPart } from '@server/lib/activitypub/video-chapters.js'
|
||||||
|
import { USER_EXPORT_MAX_ITEMS } from '@server/initializers/constants.js'
|
||||||
|
|
||||||
export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ export class VideosExporter extends AbstractUserExporter <VideoExportJSON> {
|
||||||
const channels = await VideoChannelModel.listAllByAccount(this.user.Account.id)
|
const channels = await VideoChannelModel.listAllByAccount(this.user.Account.id)
|
||||||
|
|
||||||
for (const channel of channels) {
|
for (const channel of channels) {
|
||||||
const videoIds = await VideoModel.getAllIdsFromChannel(channel)
|
const videoIds = await VideoModel.getAllIdsFromChannel(channel, USER_EXPORT_MAX_ITEMS)
|
||||||
|
|
||||||
await Bluebird.map(videoIds, async id => {
|
await Bluebird.map(videoIds, async id => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,5 +5,6 @@ export * from './dislikes-importer.js'
|
||||||
export * from './following-importer.js'
|
export * from './following-importer.js'
|
||||||
export * from './likes-importer.js'
|
export * from './likes-importer.js'
|
||||||
export * from './user-settings-importer.js'
|
export * from './user-settings-importer.js'
|
||||||
|
export * from './user-video-history-importer.js'
|
||||||
export * from './video-playlists-importer.js'
|
export * from './video-playlists-importer.js'
|
||||||
export * from './videos-importer.js'
|
export * from './videos-importer.js'
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { UserVideoHistoryExportJSON } from '@peertube/peertube-models'
|
||||||
|
import { AbstractRatesImporter } from './abstract-rates-importer.js'
|
||||||
|
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||||
|
import { pick } from '@peertube/peertube-core-utils'
|
||||||
|
import { loadOrCreateVideoIfAllowedForUser } from '@server/lib/model-loaders/video.js'
|
||||||
|
import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js'
|
||||||
|
|
||||||
|
type SanitizedObject = Pick<UserVideoHistoryExportJSON['watchedVideos'][0], 'videoUrl' | 'lastTimecode'>
|
||||||
|
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
export class UserVideoHistoryImporter extends AbstractRatesImporter <UserVideoHistoryExportJSON, UserVideoHistoryExportJSON['watchedVideos'][0]> {
|
||||||
|
|
||||||
|
protected getImportObjects (json: UserVideoHistoryExportJSON) {
|
||||||
|
return json.watchedVideos
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sanitize (data: UserVideoHistoryExportJSON['watchedVideos'][0]) {
|
||||||
|
if (!isUrlValid(data.videoUrl)) return undefined
|
||||||
|
|
||||||
|
return pick(data, [ 'videoUrl', 'lastTimecode' ])
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async importObject (data: SanitizedObject) {
|
||||||
|
if (!this.user.videosHistoryEnabled) return { duplicate: false }
|
||||||
|
|
||||||
|
const videoUrl = data.videoUrl
|
||||||
|
const videoImmutable = await loadOrCreateVideoIfAllowedForUser(videoUrl)
|
||||||
|
|
||||||
|
if (!videoImmutable) {
|
||||||
|
throw new Error(`Cannot get or create video ${videoUrl} to import user history`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserVideoHistoryModel.upsert({
|
||||||
|
videoId: videoImmutable.id,
|
||||||
|
userId: this.user.id,
|
||||||
|
currentTime: data.lastTimecode
|
||||||
|
})
|
||||||
|
|
||||||
|
return { duplicate: false }
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,8 @@ import {
|
||||||
LikesExporter, AbstractUserExporter,
|
LikesExporter, AbstractUserExporter,
|
||||||
UserSettingsExporter,
|
UserSettingsExporter,
|
||||||
VideoPlaylistsExporter,
|
VideoPlaylistsExporter,
|
||||||
VideosExporter
|
VideosExporter,
|
||||||
|
UserVideoHistoryExporter
|
||||||
} from './exporters/index.js'
|
} from './exporters/index.js'
|
||||||
import { MUserDefault, MUserExport } from '@server/types/models/index.js'
|
import { MUserDefault, MUserExport } from '@server/types/models/index.js'
|
||||||
import archiver, { Archiver } from 'archiver'
|
import archiver, { Archiver } from 'archiver'
|
||||||
|
@ -236,6 +237,10 @@ export class UserExporter {
|
||||||
|
|
||||||
relativeStaticDirPath: '../files/video-playlists'
|
relativeStaticDirPath: '../files/video-playlists'
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
jsonFilename: 'video-history.json',
|
||||||
|
exporter: new UserVideoHistoryExporter(options)
|
||||||
}
|
}
|
||||||
] as { jsonFilename: string, exporter: AbstractUserExporter<any> }[]
|
] as { jsonFilename: string, exporter: AbstractUserExporter<any> }[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { FollowingImporter } from './importers/following-importer.js'
|
||||||
import { LikesImporter } from './importers/likes-importer.js'
|
import { LikesImporter } from './importers/likes-importer.js'
|
||||||
import { DislikesImporter } from './importers/dislikes-importer.js'
|
import { DislikesImporter } from './importers/dislikes-importer.js'
|
||||||
import { VideoPlaylistsImporter } from './importers/video-playlists-importer.js'
|
import { VideoPlaylistsImporter } from './importers/video-playlists-importer.js'
|
||||||
|
import { UserVideoHistoryImporter } from './importers/user-video-history-importer.js'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('user-import')
|
const lTags = loggerTagsFactory('user-import')
|
||||||
|
|
||||||
|
@ -34,7 +35,8 @@ export class UserImporter {
|
||||||
videoPlaylists: this.buildSummary(),
|
videoPlaylists: this.buildSummary(),
|
||||||
videos: this.buildSummary(),
|
videos: this.buildSummary(),
|
||||||
account: this.buildSummary(),
|
account: this.buildSummary(),
|
||||||
userSettings: this.buildSummary()
|
userSettings: this.buildSummary(),
|
||||||
|
userVideoHistory: this.buildSummary()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +129,10 @@ export class UserImporter {
|
||||||
{
|
{
|
||||||
name: 'videoPlaylists' as 'videoPlaylists',
|
name: 'videoPlaylists' as 'videoPlaylists',
|
||||||
importer: new VideoPlaylistsImporter(this.buildImporterOptions(user, 'video-playlists.json'))
|
importer: new VideoPlaylistsImporter(this.buildImporterOptions(user, 'video-playlists.json'))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userVideoHistory' as 'userVideoHistory',
|
||||||
|
importer: new UserVideoHistoryImporter(this.buildImporterOptions(user, 'video-history.json'))
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
|
import { FindOptions, Op, QueryTypes, Transaction } from 'sequelize'
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS, VIDEO_RATE_TYPES } from '../../initializers/constants.js'
|
||||||
import { ActorModel } from '../actor/actor.js'
|
import { ActorModel } from '../actor/actor.js'
|
||||||
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
||||||
import { SummaryOptions, VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel.js'
|
import { SummaryOptions, VideoChannelModel, ScopeNames as VideoChannelScopeNames } from '../video/video-channel.js'
|
||||||
|
@ -252,8 +252,8 @@ export class AccountVideoRateModel extends SequelizeModel<AccountVideoRateModel>
|
||||||
]).then(([ total, data ]) => ({ total, data }))
|
]).then(([ total, data ]) => ({ total, data }))
|
||||||
}
|
}
|
||||||
|
|
||||||
static listRatesOfAccountId (accountId: number, rateType: VideoRateType): Promise<MAccountVideoRateVideoUrl[]> {
|
static listRatesOfAccountIdForExport (accountId: number, rateType: VideoRateType): Promise<MAccountVideoRateVideoUrl[]> {
|
||||||
const query = {
|
return AccountVideoRateModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
accountId,
|
accountId,
|
||||||
type: rateType
|
type: rateType
|
||||||
|
@ -264,10 +264,9 @@ export class AccountVideoRateModel extends SequelizeModel<AccountVideoRateModel>
|
||||||
model: VideoModel,
|
model: VideoModel,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
return AccountVideoRateModel.findAll(query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -30,7 +30,14 @@ import {
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { logger } from '../../helpers/logger.js'
|
import { logger } from '../../helpers/logger.js'
|
||||||
import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAME, SORTABLE_COLUMNS } from '../../initializers/constants.js'
|
import {
|
||||||
|
ACTOR_FOLLOW_SCORE,
|
||||||
|
CONSTRAINTS_FIELDS,
|
||||||
|
FOLLOW_STATES,
|
||||||
|
SERVER_ACTOR_NAME,
|
||||||
|
SORTABLE_COLUMNS,
|
||||||
|
USER_EXPORT_MAX_ITEMS
|
||||||
|
} from '../../initializers/constants.js'
|
||||||
import { AccountModel } from '../account/account.js'
|
import { AccountModel } from '../account/account.js'
|
||||||
import { ServerModel } from '../server/server.js'
|
import { ServerModel } from '../server/server.js'
|
||||||
import { SequelizeModel, buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared/index.js'
|
||||||
|
@ -510,8 +517,8 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
|
||||||
}).then(({ data, total }) => ({ total, data: data.map(d => d.selectionUrl) }))
|
}).then(({ data, total }) => ({ total, data: data.map(d => d.selectionUrl) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
static listAcceptedFollowersForExport (targetActorId: number) {
|
static async listAcceptedFollowersForExport (targetActorId: number) {
|
||||||
const query = {
|
const data = await ActorFollowModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
state: 'accepted',
|
state: 'accepted',
|
||||||
targetActorId
|
targetActorId
|
||||||
|
@ -530,17 +537,15 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
|
|
||||||
return ActorFollowModel.findAll(query)
|
|
||||||
.then(data => {
|
|
||||||
return data.map(f => ({
|
return data.map(f => ({
|
||||||
createdAt: f.createdAt,
|
createdAt: f.createdAt,
|
||||||
followerHandle: f.ActorFollower.getFullIdentifier(),
|
followerHandle: f.ActorFollower.getFullIdentifier(),
|
||||||
followerUrl: f.ActorFollower.url
|
followerUrl: f.ActorFollower.url
|
||||||
}))
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -550,8 +555,8 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
|
||||||
.then(({ data, total }) => ({ total, data: data.map(d => d.selectionUrl) }))
|
.then(({ data, total }) => ({ total, data: data.map(d => d.selectionUrl) }))
|
||||||
}
|
}
|
||||||
|
|
||||||
static listAcceptedFollowingForExport (actorId: number) {
|
static async listAcceptedFollowingForExport (actorId: number) {
|
||||||
const query = {
|
const data = await ActorFollowModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
state: 'accepted',
|
state: 'accepted',
|
||||||
actorId
|
actorId
|
||||||
|
@ -570,17 +575,15 @@ export class ActorFollowModel extends SequelizeModel<ActorFollowModel> {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
|
|
||||||
return ActorFollowModel.findAll(query)
|
|
||||||
.then(data => {
|
|
||||||
return data.map(f => ({
|
return data.map(f => ({
|
||||||
createdAt: f.createdAt,
|
createdAt: f.createdAt,
|
||||||
followingHandle: f.ActorFollowing.getFullIdentifier(),
|
followingHandle: f.ActorFollowing.getFullIdentifier(),
|
||||||
followingUrl: f.ActorFollowing.url
|
followingUrl: f.ActorFollowing.url
|
||||||
}))
|
}))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -5,6 +5,8 @@ import { MUserAccountId, MUserId } from '@server/types/models/index.js'
|
||||||
import { VideoModel } from '../video/video.js'
|
import { VideoModel } from '../video/video.js'
|
||||||
import { UserModel } from './user.js'
|
import { UserModel } from './user.js'
|
||||||
import { SequelizeModel } from '../shared/sequelize-type.js'
|
import { SequelizeModel } from '../shared/sequelize-type.js'
|
||||||
|
import { USER_EXPORT_MAX_ITEMS } from '@server/initializers/constants.js'
|
||||||
|
import { getSort } from '../shared/sort.js'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'userVideoHistory',
|
tableName: 'userVideoHistory',
|
||||||
|
@ -71,6 +73,26 @@ export class UserVideoHistoryModel extends SequelizeModel<UserVideoHistoryModel>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async listForExport (user: MUserId) {
|
||||||
|
const rows = await UserVideoHistoryModel.findAll({
|
||||||
|
attributes: [ 'createdAt', 'updatedAt', 'currentTime' ],
|
||||||
|
where: {
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
limit: USER_EXPORT_MAX_ITEMS,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'url' ],
|
||||||
|
model: VideoModel.unscoped(),
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
order: getSort('updatedAt')
|
||||||
|
})
|
||||||
|
|
||||||
|
return rows.map(r => ({ createdAt: r.createdAt, updatedAt: r.updatedAt, currentTime: r.currentTime, videoUrl: r.Video.url }))
|
||||||
|
}
|
||||||
|
|
||||||
static removeUserHistoryElement (user: MUserId, videoId: number) {
|
static removeUserHistoryElement (user: MUserId, videoId: number) {
|
||||||
const query: DestroyOptions = {
|
const query: DestroyOptions = {
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import { extractMentions } from '@server/helpers/mentions.js'
|
||||||
import { getServerActor } from '@server/models/application/application.js'
|
import { getServerActor } from '@server/models/application/application.js'
|
||||||
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
|
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models/index.js'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS } from '../../initializers/constants.js'
|
||||||
import {
|
import {
|
||||||
MComment,
|
MComment,
|
||||||
MCommentAdminFormattable,
|
MCommentAdminFormattable,
|
||||||
|
@ -456,7 +456,7 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static listForExport (ofAccountId: number): Promise<MCommentExport[]> {
|
static listForExport (ofAccountId: number): Promise<MCommentExport[]> {
|
||||||
const query = {
|
return VideoCommentModel.findAll({
|
||||||
attributes: [ 'url', 'text', 'createdAt' ],
|
attributes: [ 'url', 'text', 'createdAt' ],
|
||||||
where: {
|
where: {
|
||||||
accountId: ofAccountId,
|
accountId: ofAccountId,
|
||||||
|
@ -474,10 +474,9 @@ export class VideoCommentModel extends SequelizeModel<VideoCommentModel> {
|
||||||
model: VideoCommentModel,
|
model: VideoCommentModel,
|
||||||
as: 'InReplyToVideoComment'
|
as: 'InReplyToVideoComment'
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
return VideoCommentModel.findAll(query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getStats () {
|
static async getStats () {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
MVideoPlaylistElementVideoUrl
|
MVideoPlaylistElementVideoUrl
|
||||||
} from '@server/types/models/video/video-playlist-element.js'
|
} from '@server/types/models/video/video-playlist-element.js'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc.js'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
|
import { CONSTRAINTS_FIELDS, USER_EXPORT_MAX_ITEMS } from '../../initializers/constants.js'
|
||||||
import { AccountModel } from '../account/account.js'
|
import { AccountModel } from '../account/account.js'
|
||||||
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
import { SequelizeModel, getSort, throwIfNotValid } from '../shared/index.js'
|
||||||
import { VideoPlaylistModel } from './video-playlist.js'
|
import { VideoPlaylistModel } from './video-playlist.js'
|
||||||
|
@ -258,7 +258,7 @@ export class VideoPlaylistElementModel extends SequelizeModel<VideoPlaylistEleme
|
||||||
}
|
}
|
||||||
|
|
||||||
static listElementsForExport (videoPlaylistId: number): Promise<MVideoPlaylistElementVideoUrl[]> {
|
static listElementsForExport (videoPlaylistId: number): Promise<MVideoPlaylistElementVideoUrl[]> {
|
||||||
const query = {
|
return VideoPlaylistElementModel.findAll({
|
||||||
where: {
|
where: {
|
||||||
videoPlaylistId
|
videoPlaylistId
|
||||||
},
|
},
|
||||||
|
@ -269,10 +269,9 @@ export class VideoPlaylistElementModel extends SequelizeModel<VideoPlaylistEleme
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: getSort('position')
|
order: getSort('position'),
|
||||||
}
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
return VideoPlaylistElementModel.findAll(query)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
CONSTRAINTS_FIELDS,
|
CONSTRAINTS_FIELDS,
|
||||||
LAZY_STATIC_PATHS,
|
LAZY_STATIC_PATHS,
|
||||||
THUMBNAILS_SIZE,
|
THUMBNAILS_SIZE,
|
||||||
|
USER_EXPORT_MAX_ITEMS,
|
||||||
VIDEO_PLAYLIST_PRIVACIES,
|
VIDEO_PLAYLIST_PRIVACIES,
|
||||||
VIDEO_PLAYLIST_TYPES,
|
VIDEO_PLAYLIST_TYPES,
|
||||||
WEBSERVER
|
WEBSERVER
|
||||||
|
@ -496,15 +497,14 @@ export class VideoPlaylistModel extends SequelizeModel<VideoPlaylistModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
static listPlaylistForExport (accountId: number): Promise<MVideoPlaylistFull[]> {
|
static listPlaylistForExport (accountId: number): Promise<MVideoPlaylistFull[]> {
|
||||||
const query = {
|
|
||||||
where: {
|
|
||||||
ownerAccountId: accountId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return VideoPlaylistModel
|
return VideoPlaylistModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
|
.scope([ ScopeNames.WITH_ACCOUNT_AND_CHANNEL, ScopeNames.WITH_VIDEOS_LENGTH, ScopeNames.WITH_THUMBNAIL ])
|
||||||
.findAll(query)
|
.findAll({
|
||||||
|
where: {
|
||||||
|
ownerAccountId: accountId
|
||||||
|
},
|
||||||
|
limit: USER_EXPORT_MAX_ITEMS
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -1593,16 +1593,16 @@ export class VideoModel extends SequelizeModel<VideoModel> {
|
||||||
return VideoModel.update({ support: ofChannel.support }, options)
|
return VideoModel.update({ support: ofChannel.support }, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> {
|
static async getAllIdsFromChannel (videoChannel: MChannelId, limit?: number): Promise<number[]> {
|
||||||
const query = {
|
const videos = await VideoModel.findAll({
|
||||||
attributes: [ 'id' ],
|
attributes: [ 'id' ],
|
||||||
where: {
|
where: {
|
||||||
channelId: videoChannel.id
|
channelId: videoChannel.id
|
||||||
}
|
},
|
||||||
}
|
limit
|
||||||
|
})
|
||||||
|
|
||||||
return VideoModel.findAll(query)
|
return videos.map(v => v.id)
|
||||||
.then(videos => videos.map(v => v.id))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// threshold corresponds to how many video the field should have to be returned
|
// threshold corresponds to how many video the field should have to be returned
|
||||||
|
|
Loading…
Reference in New Issue