Skip to content

Commit 189d846

Browse files
committed
Fix duplicates when syncing a channel
1 parent d8368b8 commit 189d846

File tree

3 files changed

+74
-22
lines changed

3 files changed

+74
-22
lines changed

packages/tests/src/api/videos/video-channel-syncs.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,15 @@ describe('Test channel synchronizations', function () {
2222
if (areYoutubeImportTestsDisabled()) return
2323

2424
function runSuite (mode: 'youtube-dl' | 'yt-dlp') {
25-
2625
describe('Sync using ' + mode, function () {
2726
let servers: PeerTubeServer[]
2827
let sqlCommands: SQLCommand[] = []
2928

3029
let startTestDate: Date
3130

3231
let rootChannelSyncId: number
32+
let videoToDelete: number
33+
3334
const userInfo = {
3435
accessToken: '',
3536
username: 'user1',
@@ -41,8 +42,8 @@ describe('Test channel synchronizations', function () {
4142
async function changeDateForSync (channelSyncId: number, newDate: string) {
4243
await sqlCommands[0].updateQuery(
4344
`UPDATE "videoChannelSync" ` +
44-
`SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
45-
`WHERE id=${channelSyncId}`
45+
`SET "createdAt"='${newDate}', "lastSyncAt"='${newDate}' ` +
46+
`WHERE id=${channelSyncId}`
4647
)
4748
}
4849

@@ -288,21 +289,34 @@ describe('Test channel synchronizations', function () {
288289
}
289290
})
290291

291-
const { videoChannelSync: { id: videoChannelSyncId } } = await servers[0].channelSyncs.create({
292+
const { videoChannelSync } = await servers[0].channelSyncs.create({
292293
attributes: {
293294
externalChannelUrl: FIXTURE_URLS.youtubePlaylist,
294295
videoChannelId: channelId
295296
}
296297
})
298+
rootChannelSyncId = videoChannelSync.id
297299

298-
await forceSyncAll(videoChannelSyncId)
300+
await forceSyncAll(rootChannelSyncId)
299301

300302
{
301-
302303
const { total, data } = await listAllVideosOfChannel('channel2')
303304
expect(total).to.equal(2)
304305
expect(data[0].name).to.equal('test')
305306
expect(data[1].name).to.equal('small video - youtube')
307+
308+
videoToDelete = data[1].id
309+
}
310+
})
311+
312+
it('Should not re-import deleted videos', async function () {
313+
await servers[0].videos.remove({ id: videoToDelete })
314+
await forceSyncAll(rootChannelSyncId)
315+
316+
{
317+
const { total, data } = await listAllVideosOfChannel('channel2')
318+
expect(total).to.equal(1)
319+
expect(data[0].name).to.equal('test')
306320
}
307321
})
308322

server/core/lib/sync-channel.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1+
import { VideoChannelSyncState, VideoPrivacy } from '@peertube/peertube-models'
12
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
23
import { YoutubeDLWrapper } from '@server/helpers/youtube-dl/index.js'
34
import { CONFIG } from '@server/initializers/config.js'
45
import { buildYoutubeDLImport } from '@server/lib/video-pre-import.js'
56
import { UserModel } from '@server/models/user/user.js'
67
import { VideoImportModel } from '@server/models/video/video-import.js'
7-
import { MChannel, MChannelAccountDefault, MChannelSync } from '@server/types/models/index.js'
8-
import { VideoChannelSyncState, VideoPrivacy } from '@peertube/peertube-models'
8+
import { MChannelAccountDefault, MChannelSync } from '@server/types/models/index.js'
99
import { CreateJobArgument, JobQueue } from './job-queue/index.js'
1010
import { ServerConfigManager } from './server-config-manager.js'
1111

@@ -38,7 +38,9 @@ export async function synchronizeChannel (options: {
3838

3939
logger.info(
4040
'Fetched %d candidate URLs for sync channel %s.',
41-
targetUrls.length, channel.Actor.preferredUsername, { targetUrls, ...lTags() }
41+
targetUrls.length,
42+
channel.Actor.preferredUsername,
43+
{ targetUrls, ...lTags() }
4244
)
4345

4446
if (targetUrls.length === 0) {
@@ -56,7 +58,7 @@ export async function synchronizeChannel (options: {
5658
logger.debug(`Import candidate: ${targetUrl}`, lTags())
5759

5860
try {
59-
if (await skipImport(channel, targetUrl, onlyAfter)) continue
61+
if (await skipImport({ channel, channelSync, targetUrl, onlyAfter })) continue
6062

6163
const { job } = await buildYoutubeDLImport({
6264
user,
@@ -92,9 +94,19 @@ export async function synchronizeChannel (options: {
9294

9395
// ---------------------------------------------------------------------------
9496

95-
async function skipImport (channel: MChannel, targetUrl: string, onlyAfter?: Date) {
96-
if (await VideoImportModel.urlAlreadyImported(channel.id, targetUrl)) {
97-
logger.debug('%s is already imported for channel %s, skipping video channel synchronization.', targetUrl, channel.name, lTags())
97+
async function skipImport (options: {
98+
channel: MChannelAccountDefault
99+
channelSync: MChannelSync
100+
targetUrl: string
101+
onlyAfter?: Date
102+
}) {
103+
const { channel, channelSync, targetUrl, onlyAfter } = options
104+
105+
if (await VideoImportModel.urlAlreadyImported({ channelId: channel.id, channelSyncId: channelSync?.id, targetUrl })) {
106+
logger.debug(
107+
`${targetUrl} is already imported for channel ${channel.name}, skipping video channel synchronization.`,
108+
{ channelSync, ...lTags() }
109+
)
98110
return true
99111
}
100112

server/core/models/video/video-import.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -208,18 +208,44 @@ export class VideoImportModel extends SequelizeModel<VideoImportModel> {
208208
]).then(([ total, data ]) => ({ total, data }))
209209
}
210210

211-
static async urlAlreadyImported (channelId: number, targetUrl: string): Promise<boolean> {
212-
const element = await VideoImportModel.unscoped().findOne({
213-
where: {
214-
targetUrl,
215-
state: {
216-
[Op.in]: [ VideoImportState.PENDING, VideoImportState.PROCESSING, VideoImportState.SUCCESS ]
217-
},
218-
videoChannelSyncId: channelId
211+
static async urlAlreadyImported (options: {
212+
targetUrl: string
213+
channelId: number
214+
channelSyncId?: number
215+
}): Promise<boolean> {
216+
const { channelSyncId, channelId, targetUrl } = options
217+
218+
const baseWhere = {
219+
targetUrl,
220+
state: {
221+
[Op.in]: [ VideoImportState.PENDING, VideoImportState.PROCESSING, VideoImportState.SUCCESS ]
219222
}
223+
}
224+
225+
const bySyncId = channelSyncId
226+
? VideoImportModel.unscoped().findOne({
227+
where: {
228+
...baseWhere,
229+
230+
videoChannelSyncId: channelSyncId
231+
}
232+
})
233+
: Promise.resolve(undefined)
234+
235+
const byChannelId = VideoImportModel.unscoped().findOne({
236+
where: baseWhere,
237+
include: [
238+
{
239+
model: VideoModel.unscoped(),
240+
required: true,
241+
where: {
242+
channelId
243+
}
244+
}
245+
]
220246
})
221247

222-
return !!element
248+
return (await Promise.all([ bySyncId, byChannelId ])).some(e => !!e)
223249
}
224250

225251
getTargetIdentifier () {

0 commit comments

Comments
 (0)