-
Notifications
You must be signed in to change notification settings - Fork 50
Expand file tree
/
Copy pathparser.js
More file actions
455 lines (394 loc) · 13.5 KB
/
parser.js
File metadata and controls
455 lines (394 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
/**
* 解析器服务
* 处理JSON类型和嗅探类型的解析逻辑
*/
import axios from 'axios'
import { API_CONFIG } from '@/api/config'
class ParserService {
/**
* 验证解析器配置
* @param {Object} parser - 解析器配置
* @returns {Object} 验证结果
*/
static validateParserConfig(parser) {
const errors = []
if (!parser) {
return { valid: false, errors: ['解析器配置不能为空'] }
}
if (!parser.name) {
errors.push('解析器名称不能为空')
}
if (!parser.url) {
errors.push('解析器URL不能为空')
} else {
// 验证URL格式
try {
new URL(parser.url)
} catch (e) {
errors.push('解析器URL格式无效')
}
}
if (!parser.type || !['json', 'sniffer'].includes(parser.type)) {
errors.push('解析器类型必须是 json 或 sniffer')
}
// JSON类型特定验证
if (parser.type === 'json') {
if (!parser.urlPath) {
errors.push('JSON解析器必须配置URL提取路径(urlPath)')
}
}
// 嗅探类型特定验证
if (parser.type === 'sniffer') {
// 嗅探解析器直接拼接URL,不需要占位符验证
}
return {
valid: errors.length === 0,
errors
}
}
/**
* 测试解析器配置
* @param {Object} parser - 解析器配置
* @param {string} testUrl - 测试URL
* @returns {Promise<Object>} 测试结果
*/
static async testParserConfig(parser, testUrl = 'https://example.com/test.mp4') {
try {
console.log('🧪 [解析器测试] 开始测试解析器配置:', {
parser: parser.name,
testUrl,
isDefaultTestUrl: testUrl === 'https://example.com/test.mp4'
})
// 首先验证配置
const validation = this.validateParserConfig(parser)
if (!validation.valid) {
return {
success: false,
message: '配置验证失败: ' + validation.errors.join(', ')
}
}
// 执行测试解析
let result
if (parser.type === 'json') {
console.log('🧪 [解析器测试] 使用JSON解析器测试')
result = await this.parseWithJsonParser(parser, { url: testUrl })
} else if (parser.type === 'sniffer') {
console.log('🧪 [解析器测试] 使用嗅探解析器测试')
result = await this.parseWithSnifferParser(parser, { url: testUrl })
}
return {
success: result.success,
message: result.success ? '解析器测试成功' : result.message,
testResult: result
}
} catch (error) {
return {
success: false,
message: '解析器测试失败: ' + error.message
}
}
}
/**
* 使用JSON类型解析器解析视频
* @param {Object} parser - 解析器配置
* @param {Object} data - 需要解析的数据
* @returns {Promise<Object>} 解析结果
*/
static async parseWithJsonParser(parser, data) {
try {
console.log('🔍 [JSON解析] 开始解析:', {
parser: parser.name,
data,
dataType: typeof data,
isTestUrl: data && typeof data === 'object' && data.url === 'https://example.com/test.mp4'
})
if (!parser.url) {
throw new Error('解析器URL未配置')
}
// 提取要解析的URL - 优先处理T4接口返回的数据结构
let videoUrl
if (data && typeof data === 'object') {
// T4接口返回的数据结构:{ jx: 1, url: "视频地址", headers: {...} }
videoUrl = data.url || data.play_url || data
console.log('从T4数据结构提取的目标URL:', videoUrl)
} else {
// 简单字符串格式
videoUrl = data
console.log('直接使用的目标URL:', videoUrl)
}
// 验证URL有效性
if (!videoUrl || typeof videoUrl !== 'string') {
throw new Error('无效的视频URL')
}
console.log('要解析的视频URL:', videoUrl)
// 构建完整的解析地址:解析器URL + 待解析URL
const fullParseUrl = parser.url + encodeURIComponent(videoUrl)
console.log('拼接后的解析地址:', fullParseUrl)
// 获取代理访问接口配置
const savedAddresses = JSON.parse(localStorage.getItem('addressSettings') || '{}')
const proxyAccessEnabled = savedAddresses.proxyAccessEnabled || false
const proxyAccess = savedAddresses.proxyAccess || ''
let requestUrl = fullParseUrl
// 如果启用了代理访问接口,使用代理访问链接
if (proxyAccessEnabled && proxyAccess) {
console.log('🔄 [代理访问] 使用代理访问接口:', proxyAccess)
if (proxyAccess.includes('${url}')) {
// 替换代理访问链接中的${url}占位符
requestUrl = proxyAccess.replace(/\$\{url\}/g, encodeURIComponent(fullParseUrl))
console.log('🔄 [代理访问] 替换占位符后的最终URL:', requestUrl)
} else {
console.warn('⚠️ [代理访问] 代理访问链接中未找到${url}占位符,将直接访问原地址')
}
} else {
console.log('🔄 [直接访问] 代理访问接口未启用,直接访问解析地址')
}
// 发送解析请求
const axiosConfig = {
method: parser.method || 'GET',
url: requestUrl,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': fullParseUrl, // 使用拼接后的解析地址作为Referer
...parser.headers
},
timeout: API_CONFIG.TIMEOUT
}
const response = await axios(axiosConfig)
console.log('JSON解析响应:', response.data)
// 解析响应数据
const result = this.parseJsonResponse(response.data, parser)
return {
success: true,
url: result.url,
headers: result.headers || {},
qualities: result.qualities || [],
message: '解析成功'
}
} catch (error) {
console.error('JSON解析失败:', error)
return {
success: false,
message: error.message || 'JSON解析失败'
}
}
}
/**
* 使用嗅探类型解析器解析视频
* @param {Object} parser - 解析器配置
* @param {Object} data - 需要解析的数据
* @returns {Promise<Object>} 解析结果
*/
static async parseWithSnifferParser(parser, data) {
try {
console.log('开始嗅探解析:', { parser: parser.name, data })
if (!parser.url) {
throw new Error('解析器URL未配置')
}
// 构建嗅探URL
const sniffUrl = this.buildSnifferUrl(parser, data)
console.log('嗅探URL:', sniffUrl)
// 发送嗅探请求
const response = await axios({
method: 'GET',
url: sniffUrl,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Referer': data.referer || '',
...parser.headers
},
timeout: API_CONFIG.TIMEOUT,
maxRedirects: 5
})
console.log('嗅探解析响应状态:', response.status)
// 从响应中提取视频URL
const videoUrl = this.extractVideoUrlFromSniffer(response, parser)
if (!videoUrl) {
throw new Error('未能从嗅探响应中提取到视频URL')
}
return {
success: true,
url: videoUrl,
headers: {
'Referer': parser.url,
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
qualities: [],
message: '嗅探解析成功'
}
} catch (error) {
console.error('嗅探解析失败:', error)
return {
success: false,
message: error.message || '嗅探解析失败'
}
}
}
/**
* 解析JSON响应数据
* @param {Object} responseData - 响应数据
* @param {Object} parser - 解析器配置
* @returns {Object} 解析结果
*/
static parseJsonResponse(responseData, parser) {
try {
let data = responseData
// 如果响应是字符串,尝试解析为JSON
if (typeof data === 'string') {
data = JSON.parse(data)
}
// 根据解析器配置提取数据
const result = {
url: this.extractValueByPath(data, parser.urlPath || 'url'),
headers: {},
qualities: []
}
// 提取请求头
if (parser.headersPath) {
result.headers = this.extractValueByPath(data, parser.headersPath) || {}
}
// 提取多画质数据
if (parser.qualitiesPath) {
const qualities = this.extractValueByPath(data, parser.qualitiesPath)
if (Array.isArray(qualities)) {
result.qualities = qualities.map(q => ({
name: q.name || q.quality || 'Unknown',
url: q.url || q.playUrl || q.src
}))
}
}
return result
} catch (error) {
console.error('解析JSON响应失败:', error)
throw new Error('解析JSON响应失败: ' + error.message)
}
}
/**
* 构建嗅探器URL
* @param {Object} parser - 解析器配置
* @param {Object} data - 数据
* @returns {string} 嗅探URL
*/
static buildSnifferUrl(parser, data) {
let url = parser.url
// 提取要解析的URL - 优先处理T4接口返回的数据结构
let videoUrl
if (data && typeof data === 'object') {
// T4接口返回的数据结构:{ jx: 1, url: "视频地址", headers: {...} }
videoUrl = data.url || data.play_url || data
console.log('从T4数据结构提取的嗅探目标URL:', videoUrl)
} else {
// 简单字符串格式
videoUrl = data
console.log('直接使用的嗅探目标URL:', videoUrl)
}
// 验证URL有效性
if (!videoUrl || typeof videoUrl !== 'string') {
throw new Error('无效的视频URL')
}
// 直接将解析器URL与待解析URL相加
if (url.includes('{url}')) {
// 如果包含占位符,替换它(兼容旧格式)
url = url.replace(/\{url\}/g, encodeURIComponent(videoUrl))
} else {
// 直接字符串相加:解析器URL + 待解析URL
url = url + encodeURIComponent(videoUrl)
}
// 替换时间占位符
url = url.replace(/\{time\}/g, Date.now())
// 添加额外参数
if (parser.params) {
const params = new URLSearchParams()
Object.entries(parser.params).forEach(([key, value]) => {
params.append(key, value)
})
url += (url.includes('?') ? '&' : '?') + params.toString()
}
return url
}
/**
* 从嗅探响应中提取视频URL
* @param {Object} response - HTTP响应
* @param {Object} parser - 解析器配置
* @returns {string|null} 视频URL
*/
static extractVideoUrlFromSniffer(response, parser) {
try {
// 方法1: 从响应头的Location中获取
if (response.headers.location) {
const location = response.headers.location
if (this.isVideoUrl(location)) {
return location
}
}
// 方法2: 从响应体中提取
if (response.data) {
let content = response.data
// 如果是JSON响应
if (typeof content === 'object') {
const url = this.extractValueByPath(content, parser.urlPath || 'url')
if (url && this.isVideoUrl(url)) {
return url
}
}
// 如果是HTML响应,使用正则提取
if (typeof content === 'string') {
const videoUrlRegex = /(https?:\/\/[^\s"'<>]+\.(?:mp4|m3u8|flv|avi|mkv|mov|wmv|webm)(?:\?[^\s"'<>]*)?)/gi
const matches = content.match(videoUrlRegex)
if (matches && matches.length > 0) {
return matches[0]
}
}
}
// 方法3: 如果配置了自定义提取规则
if (parser.extractRule) {
const regex = new RegExp(parser.extractRule, 'gi')
const matches = response.data.match(regex)
if (matches && matches.length > 0) {
return matches[0]
}
}
return null
} catch (error) {
console.error('提取视频URL失败:', error)
return null
}
}
/**
* 根据路径提取对象中的值
* @param {Object} obj - 对象
* @param {string} path - 路径,如 'data.url' 或 'result[0].playUrl'
* @returns {any} 提取的值
*/
static extractValueByPath(obj, path) {
try {
return path.split('.').reduce((current, key) => {
// 处理数组索引,如 result[0]
const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/)
if (arrayMatch) {
const [, arrayKey, index] = arrayMatch
return current?.[arrayKey]?.[parseInt(index)]
}
return current?.[key]
}, obj)
} catch (error) {
console.error('提取路径值失败:', error, { path, obj })
return null
}
}
/**
* 检查URL是否为视频URL
* @param {string} url - URL
* @returns {boolean} 是否为视频URL
*/
static isVideoUrl(url) {
if (!url || typeof url !== 'string') {
return false
}
const videoExtensions = ['.mp4', '.m3u8', '.flv', '.avi', '.mkv', '.mov', '.wmv', '.webm']
const lowerUrl = url.toLowerCase()
return videoExtensions.some(ext => lowerUrl.includes(ext)) ||
lowerUrl.includes('video') ||
lowerUrl.includes('stream')
}
}
export default ParserService