import { VideoChannelSync, VideoChannelSyncState, type VideoChannelSyncStateType } from '@peertube/peertube-models' import { AttributesOnly } from '@peertube/peertube-typescript-utils' import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js' import { isVideoChannelSyncStateValid } from '@server/helpers/custom-validators/video-channel-syncs.js' import { CONSTRAINTS_FIELDS, VIDEO_CHANNEL_SYNC_STATE } from '@server/initializers/constants.js' import { MChannelSync, MChannelSyncChannel, MChannelSyncFormattable } from '@server/types/models/index.js' import { Op } from 'sequelize' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AccountModel } from '../account/account.js' import { getChannelSyncSort, throwIfNotValid } from '../shared/index.js' import { UserModel } from '../user/user.js' import { VideoChannelModel } from './video-channel.js' @DefaultScope(() => ({ include: [ { model: VideoChannelModel, // Default scope includes avatar and server required: true } ] })) @Table({ tableName: 'videoChannelSync', indexes: [ { fields: [ 'videoChannelId' ] } ] }) export class VideoChannelSyncModel extends Model>> { @AllowNull(false) @Default(null) @Is('VideoChannelExternalChannelUrl', value => throwIfNotValid(value, isUrlValid, 'externalChannelUrl', true)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_CHANNEL_SYNCS.EXTERNAL_CHANNEL_URL.max)) externalChannelUrl: string @AllowNull(false) @Default(VideoChannelSyncState.WAITING_FIRST_RUN) @Is('VideoChannelSyncState', value => throwIfNotValid(value, isVideoChannelSyncStateValid, 'state')) @Column state: VideoChannelSyncStateType @AllowNull(true) @Column(DataType.DATE) lastSyncAt: Date @CreatedAt createdAt: Date @UpdatedAt updatedAt: Date @ForeignKey(() => VideoChannelModel) @Column videoChannelId: number @BelongsTo(() => VideoChannelModel, { foreignKey: { allowNull: false }, onDelete: 'cascade' }) VideoChannel: Awaited static listByAccountForAPI (options: { accountId: number start: number count: number sort: string }) { const getQuery = (forCount: boolean) => { const videoChannelModel = forCount ? VideoChannelModel.unscoped() : VideoChannelModel return { offset: options.start, limit: options.count, order: getChannelSyncSort(options.sort), include: [ { model: videoChannelModel, required: true, where: { accountId: options.accountId } } ] } } return Promise.all([ VideoChannelSyncModel.unscoped().count(getQuery(true)), VideoChannelSyncModel.unscoped().findAll(getQuery(false)) ]).then(([ total, data ]) => ({ total, data })) } static countByAccount (accountId: number) { const query = { include: [ { model: VideoChannelModel.unscoped(), required: true, where: { accountId } } ] } return VideoChannelSyncModel.unscoped().count(query) } static loadWithChannel (id: number): Promise { return VideoChannelSyncModel.findByPk(id) } static async listSyncs (): Promise { const query = { include: [ { model: VideoChannelModel.unscoped(), required: true, include: [ { model: AccountModel.unscoped(), required: true, include: [ { attributes: [], model: UserModel.unscoped(), required: true, where: { videoQuota: { [Op.ne]: 0 }, videoQuotaDaily: { [Op.ne]: 0 } } } ] } ] } ] } return VideoChannelSyncModel.unscoped().findAll(query) } toFormattedJSON (this: MChannelSyncFormattable): VideoChannelSync { return { id: this.id, state: { id: this.state, label: VIDEO_CHANNEL_SYNC_STATE[this.state] }, externalChannelUrl: this.externalChannelUrl, createdAt: this.createdAt.toISOString(), channel: this.VideoChannel.toFormattedSummaryJSON(), lastSyncAt: this.lastSyncAt?.toISOString() } } }