返回 AiToEarn
material.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-server / src / core / content / material.service.ts
1 import { Injectable, Logger } from '@nestjs/common'
2 import { AssetsService, VideoMetadataService } from '@yikart/assets'
3 import { AccountType, FileUtil, TableDto, UserType } from '@yikart/common'
4 import { AssetType, Material, MaterialRepository, MaterialStatus, MaterialType, MediaType } from '@yikart/mongodb'
5 import { NewMaterial, UpMaterial } from './common'
6 import { MediaService } from './media.service'
7
8 @Injectable()
9 export class MaterialService {
10 private readonly logger = new Logger(MaterialService.name)
11
12 constructor(
13 private readonly materialRepository: MaterialRepository,
14 private readonly mediaService: MediaService,
15 private readonly videoMetadataService: VideoMetadataService,
16 private readonly assetsService: AssetsService,
17 ) { }
18
19 async create(newData: NewMaterial) {
20 const dataWithCover = await this.ensureVideoCover(newData)
21 const res = await this.materialRepository.create(dataWithCover)
22 return res
23 }
24
25 /**
26 * 视频类型草稿无封面时,自动从视频截帧生成封面
27 */
28 private async ensureVideoCover(data: NewMaterial): Promise<NewMaterial> {
29 if (data.type !== MaterialType.VIDEO || data.coverUrl) {
30 return data
31 }
32
33 const videoMedia = data.mediaList.find(
34 m => m.type === MediaType.VIDEO && m.url,
35 )
36 if (!videoMedia) {
37 return data
38 }
39
40 try {
41 const fullUrl = FileUtil.buildUrl(videoMedia.url)
42 const thumbnailBuffer = await this.videoMetadataService.extractThumbnailFromUrl(fullUrl, 2)
43 const uploadResult = await this.assetsService.uploadFromBuffer(data.userId, thumbnailBuffer, {
44 type: AssetType.VideoThumbnail,
45 mimeType: 'image/png',
46 filename: 'thumbnail.png',
47 })
48 const coverUrl = uploadResult.asset.path
49
50 return {
51 ...data,
52 coverUrl,
53 mediaList: data.mediaList.map(m =>
54 m === videoMedia ? { ...m, thumbUrl: m.thumbUrl || coverUrl } : m,
55 ),
56 }
57 }
58 catch (error) {
59 this.logger.warn({ error }, 'Failed to extract video thumbnail for cover, proceeding without cover')
60 return data
61 }
62 }
63
64 async del(id: string) {
65 const material = await this.getInfo(id)
66 if (!material)
67 return true
68 const res = await this.materialRepository.deleteById(id)
69 if (!res)
70 return false
71 if (material.autoDeleteMedia) {
72 for (const item of material.mediaList) {
73 if (!item.mediaId)
74 continue
75 this.mediaService.del(item.mediaId)
76 }
77 }
78 return res
79 }
80
81 async delByIds(userId: string, ids: string[]): Promise<boolean> {
82 const res = await this.materialRepository.deleteManyByIds(ids, { userId })
83 return res
84 }
85
86 async delByFilter(
87 userId: string,
88 inFilter: {
89 title?: string
90 groupId?: string
91 status?: MaterialStatus
92 useCount?: number
93 type?: MaterialType
94 },
95 ) {
96 const { groupId, type, useCount, title, status } = inFilter
97 const filter = {
98 userId,
99 userType: UserType.User,
100 ...(groupId && { groupId }),
101 ...(type && { type }),
102 ...(useCount !== undefined && { useCount: { $gte: useCount } }),
103 ...(title && { title: { $regex: title, $options: 'i' } }),
104 ...(status !== undefined && { status }),
105 }
106 const res = await this.materialRepository.deleteByFilter(filter)
107 return res
108 }
109
110 async deleteByUseCount(userId: string, groupId: string, minUseCount: number): Promise<boolean> {
111 const filter = {
112 userId,
113 userType: UserType.User,
114 groupId,
115 useCount: { $gte: minUseCount },
116 }
117 return this.materialRepository.deleteByFilter(filter)
118 }
119
120 async updateInfo(id: string, data: UpMaterial): Promise<boolean> {
121 const res = await this.materialRepository.updateInfo(id, data)
122 return res
123 }
124
125 async getInfo(id: string): Promise<Material | null> {
126 const res = await this.materialRepository.getInfo(id)
127 return res
128 }
129
130 async optimalInGroup(groupId: string, type?: MaterialType, accountType?: AccountType): Promise<Material | null> {
131 const res = await this.materialRepository.getOptimalByGroup(groupId, type, accountType)
132 return res
133 }
134
135 async getList(
136 page: TableDto,
137 filter: {
138 userId?: string
139 userType?: UserType
140 title?: string
141 groupId?: string
142 status?: MaterialStatus
143 ids?: string[]
144 useCount?: number
145 },
146 ) {
147 const res = await this.materialRepository.getList(filter, page)
148 return res
149 }
150
151 async transferToGroup(
152 userId: string,
153 ids: string[],
154 targetGroupId: string,
155 mode: 'move' | 'copy',
156 ): Promise<number> {
157 if (mode === 'move') {
158 return this.materialRepository.updateGroupIdByIds(ids, targetGroupId, userId)
159 }
160
161 const materials = await this.materialRepository.listByIdsAndUserId(ids, userId)
162 if (materials.length === 0) {
163 return 0
164 }
165
166 const copies = materials.map(({ id: _id, ...material }) => ({
167 ...material,
168 groupId: targetGroupId,
169 useCount: 0,
170 }))
171 const created = await this.materialRepository.createMany(copies)
172 return created.length
173 }
174
175 async optimalByIds(materialIds: string[]) {
176 const res = await this.materialRepository.getOptimalByIds(materialIds)
177 return res
178 }
179
180 async getListByIds(ids: string[]) {
181 const res = await this.materialRepository.listByIds(ids)
182 return res
183 }
184
185 async updateStatus(id: string, status: MaterialStatus, message: string) {
186 const res = await this.materialRepository.updateStatus(id, status, message)
187 return res
188 }
189
190 async addUseCount(id: string): Promise<boolean> {
191 const material = await this.materialRepository.updateUseCountById(id)
192 if (!material) {
193 return false
194 }
195
196 // 检查是否达到最大使用次数
197 if (
198 material.maxUseCount !== null
199 && material.maxUseCount !== undefined
200 && material.useCount >= material.maxUseCount
201 ) {
202 await this.del(id)
203 }
204
205 return true
206 }
207 }
208
208 lines TYPESCRIPT