Explorar el Código

小红书自动发笔记完美

yichael hace 2 meses
padre
commit
4dfdf41762
Se han modificado 100 ficheros con 1505 adiciones y 17 borrados
  1. 4 1
      nodejs/ai/ai.js
  2. 0 1
      nodejs/ef-compiler/actions/adb/send-img-to-device.js
  3. 2 3
      nodejs/ef-compiler/actions/for-parser.js
  4. 87 0
      nodejs/ef-compiler/actions/fun/download-img.js
  5. 2 0
      nodejs/ef-compiler/actions/fun/fun-node-registry.js
  6. 17 2
      nodejs/ef-compiler/actions/fun/fun-parser.js
  7. 23 9
      nodejs/ef-compiler/actions/fun/img-center-point-location.js
  8. 25 0
      nodejs/ef-compiler/actions/json/json-to-arr.js
  9. 8 1
      nodejs/ef-compiler/sequence-runner.js
  10. 3 0
      python/imagedl/.github/FUNDING.yml
  11. 91 0
      python/imagedl/.github/ISSUE_TEMPLATE/bug_report.en.yml
  12. 90 0
      python/imagedl/.github/ISSUE_TEMPLATE/bug_report.zh.yml
  13. 8 0
      python/imagedl/.github/ISSUE_TEMPLATE/config.yml
  14. 28 0
      python/imagedl/.github/ISSUE_TEMPLATE/documentation.en.yml
  15. 28 0
      python/imagedl/.github/ISSUE_TEMPLATE/documentation.zh.yml
  16. 49 0
      python/imagedl/.github/ISSUE_TEMPLATE/feature_request.en.yml
  17. 49 0
      python/imagedl/.github/ISSUE_TEMPLATE/feature_request.zh.yml
  18. 39 0
      python/imagedl/.github/ISSUE_TEMPLATE/performance.en.yml
  19. 39 0
      python/imagedl/.github/ISSUE_TEMPLATE/performance.zh.yml
  20. 28 0
      python/imagedl/.github/ISSUE_TEMPLATE/question.en.yml
  21. 28 0
      python/imagedl/.github/ISSUE_TEMPLATE/question.zh.yml
  22. 33 0
      python/imagedl/.github/ISSUE_TEMPLATE/task.en.yml
  23. 33 0
      python/imagedl/.github/ISSUE_TEMPLATE/task.zh.yml
  24. BIN
      python/imagedl/.github/pictures/alipay_reward.png
  25. BIN
      python/imagedl/.github/pictures/wechat_reward.jpg
  26. 54 0
      python/imagedl/.github/workflows/check-imagedl.yml
  27. 129 0
      python/imagedl/.gitignore
  28. 17 0
      python/imagedl/.readthedocs.yaml
  29. 8 0
      python/imagedl/CITATION.cff
  30. 201 0
      python/imagedl/LICENSE
  31. 12 0
      python/imagedl/Makefile
  32. 370 0
      python/imagedl/README.md
  33. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000001.jpg
  34. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000002.jpeg
  35. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000003.jpeg
  36. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000004.jpg
  37. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000005.webp
  38. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000006.jpeg
  39. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000007.jpg
  40. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000008.jpeg
  41. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000009.jpeg
  42. BIN
      python/imagedl/daily_test_results/BaiduImageClient/00000010.jpg
  43. BIN
      python/imagedl/daily_test_results/BingImageClient/00000001.webp
  44. BIN
      python/imagedl/daily_test_results/BingImageClient/00000002.jpeg
  45. BIN
      python/imagedl/daily_test_results/BingImageClient/00000003.jpeg
  46. BIN
      python/imagedl/daily_test_results/BingImageClient/00000004.png
  47. BIN
      python/imagedl/daily_test_results/BingImageClient/00000005.jpeg
  48. BIN
      python/imagedl/daily_test_results/BingImageClient/00000006.jpeg
  49. BIN
      python/imagedl/daily_test_results/BingImageClient/00000007.jpg
  50. BIN
      python/imagedl/daily_test_results/BingImageClient/00000008.jpeg
  51. BIN
      python/imagedl/daily_test_results/BingImageClient/00000009.jpeg
  52. BIN
      python/imagedl/daily_test_results/BingImageClient/00000010.jpeg
  53. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000001.jpeg
  54. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000002.jpeg
  55. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000003.png
  56. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000004.jpeg
  57. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000005.jpeg
  58. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000006.jpeg
  59. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000007.jpeg
  60. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000008.jpeg
  61. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000009.jpeg
  62. BIN
      python/imagedl/daily_test_results/DanbooruImageClient/00000010.jpeg
  63. BIN
      python/imagedl/daily_test_results/DimTownImageClient/00000001.jpeg
  64. BIN
      python/imagedl/daily_test_results/DimTownImageClient/00000002.jpeg
  65. BIN
      python/imagedl/daily_test_results/DimTownImageClient/00000003.jpeg
  66. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000001.jpeg
  67. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000002.png
  68. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000003.webp
  69. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000004.jpg
  70. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000005.webp
  71. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000006.jpeg
  72. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000007.jpeg
  73. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000008.jpg
  74. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000009.png
  75. BIN
      python/imagedl/daily_test_results/DuckduckgoImageClient/00000010.jpeg
  76. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000001.jpeg
  77. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000002.jpeg
  78. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000003.jpeg
  79. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000004.jpeg
  80. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000005.jpeg
  81. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000006.jpeg
  82. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000007.jpeg
  83. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000008.jpeg
  84. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000009.jpeg
  85. BIN
      python/imagedl/daily_test_results/EverypixelImageClient/00000010.jpeg
  86. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000001.jpeg
  87. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000002.jpeg
  88. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000003.jpeg
  89. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000004.jpeg
  90. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000005.jpeg
  91. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000006.jpeg
  92. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000007.jpeg
  93. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000008.jpeg
  94. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000009.jpeg
  95. BIN
      python/imagedl/daily_test_results/FoodiesfeedImageClient/00000010.jpeg
  96. BIN
      python/imagedl/daily_test_results/FreeNatureStockImageClient/00000001.jpeg
  97. BIN
      python/imagedl/daily_test_results/FreeNatureStockImageClient/00000002.jpeg
  98. BIN
      python/imagedl/daily_test_results/FreeNatureStockImageClient/00000003.jpeg
  99. BIN
      python/imagedl/daily_test_results/FreeNatureStockImageClient/00000004.jpeg
  100. BIN
      python/imagedl/daily_test_results/FreeNatureStockImageClient/00000005.jpeg

+ 4 - 1
nodejs/ai/ai.js

@@ -96,7 +96,10 @@ async function doRequest(action, isDoubao, args) {
       : await request(path, body, timeoutMs);
     return { success: true, data };
   } catch (e) {
-    return { success: false, error: e && (e.message || String(e)) };
+    const msg = e && (e.message || String(e));
+    const cause = e && e.cause && (e.cause.message || String(e.cause));
+    const err = cause ? `${msg} (${cause})` : msg;
+    return { success: false, error: err || '网络请求失败' };
   }
 }
 

+ 0 - 1
nodejs/ef-compiler/actions/adb/send-img-to-device.js

@@ -60,7 +60,6 @@ async function run(action, ctx) {
   if (!fs.existsSync(fullPath)) return { success: false, error: `本地文件不存在: ${fullPath}(流程目录: ${folderPath})` }
   const result = doSendImageToDevice(fullPath, device, adbPath)
   if (!result.success) return result
-  if (logMessage && folderPath) await logMessage(`[send-img-to-device] pushed: ${result.devicePath}`, folderPath).catch(() => {})
   return { success: true, devicePath: result.devicePath }
 }
 

+ 2 - 3
nodejs/ef-compiler/actions/for-parser.js

@@ -2,14 +2,13 @@
 const types = ['for']
 
 function parse(action, parseContext) {
-  const { resolveValue, parseActions } = parseContext
-  const variableContext = parseContext.variableContext || {}
+  const { parseActions } = parseContext
   const parsed = {
     type: 'for',
     variable: action.variable,
     indexVariable: action.indexVariable,
     times: action.times,
-    items: action.items != null ? resolveValue(action.items, variableContext) : null,
+    array: action.array != null ? action.array : null,
     body: action.body ? parseActions(action.body) : [],
   }
   return Object.assign({}, action, parsed)

+ 87 - 0
nodejs/ef-compiler/actions/fun/download-img.js

@@ -0,0 +1,87 @@
+/**
+ * fun 结点:根据描述词(prompt)用 python/imagedl 搜索并下载一张图片到 savePath
+ * 入参:prompt(描述词), savePath(相对路径时基于 folderPath)
+ */
+const path = require('path')
+const fs = require('fs')
+const { spawnSync } = require('child_process')
+
+const configPath = process.env.STATIC_ROOT
+  ? path.join(path.dirname(process.env.STATIC_ROOT), 'configs', 'config.js')
+  : path.join(__dirname, '..', '..', '..', '..', 'configs', 'config.js')
+const projectRoot = path.dirname(path.dirname(path.resolve(configPath)))
+const config = fs.existsSync(configPath) ? require(configPath) : {}
+const scriptPath = path.join(projectRoot, 'python', 'scripts', 'download-img-by-prompt.py')
+const imagedlParent = path.join(projectRoot, 'python')
+const imagedlRequirements = path.join(projectRoot, 'python', 'imagedl', 'requirements.txt')
+
+function getPythonPath() {
+  const base = config.pythonPath?.path || config.pythonVenvPath || path.join(projectRoot, 'python', process.arch === 'arm64' ? 'arm64' : 'x64')
+  const envPy = path.join(base, 'env', 'Scripts', 'python.exe')
+  const scriptsPy = path.join(base, 'Scripts', 'python.exe')
+  const pyEmbedded = path.join(base, 'py', 'python.exe')
+  if (fs.existsSync(envPy)) return envPy
+  if (fs.existsSync(scriptsPy)) return scriptsPy
+  if (fs.existsSync(pyEmbedded)) return pyEmbedded
+  return 'python'
+}
+
+function buildSavePath(savePath, folderPath) {
+  if (!savePath || typeof savePath !== 'string') return null
+  const trimmed = savePath.trim()
+  if (path.isAbsolute(trimmed) || trimmed.match(/^[A-Za-z]:/)) return trimmed
+  return folderPath ? path.join(folderPath, trimmed) : path.resolve(projectRoot, trimmed)
+}
+
+async function executeDownloadImg({ prompt, savePath, folderPath }) {
+  if (!prompt || typeof prompt !== 'string' || !prompt.trim()) return { success: false, error: 'download-img 缺少 prompt 参数' }
+  if (savePath == null) return { success: false, error: 'download-img 缺少 savePath 参数' }
+  const absolutePath = buildSavePath(String(savePath).trim(), folderPath)
+  if (!absolutePath) return { success: false, error: 'download-img savePath 无效' }
+  if (!fs.existsSync(scriptPath)) return { success: false, error: `脚本不存在: ${scriptPath}` }
+
+  const pythonPath = getPythonPath()
+  const args = [scriptPath, '--prompt', prompt.trim(), '--save-path', absolutePath.replace(/\\/g, '/')]
+  const runScript = () => spawnSync(pythonPath, args, {
+    encoding: 'utf-8',
+    timeout: 120000,
+    env: { ...process.env, PYTHONIOENCODING: 'utf-8', IMAGEDL_PARENT: imagedlParent },
+    cwd: projectRoot
+  })
+
+  let r = runScript()
+  let out = (r.stdout || '').trim()
+  const err = (r.stderr || '').trim()
+  const fullOut = out + (err ? '\n' + err : '')
+
+  if (r.status !== 0 && fs.existsSync(imagedlRequirements) && (fullOut.includes('No module named') || fullOut.includes('ModuleNotFoundError'))) {
+    spawnSync(pythonPath, ['-m', 'pip', 'install', '-r', imagedlRequirements, '-q'], {
+      encoding: 'utf-8',
+      timeout: 180000,
+      cwd: projectRoot
+    })
+    r = runScript()
+    out = (r.stdout || '').trim()
+  }
+
+  if (r.status !== 0) {
+    return { success: false, error: (r.stderr || r.stdout || '').trim() || 'download-img 执行失败' }
+  }
+  // stdout 可能包含进度条等,取最后一行以 { 开头的行作为 JSON
+  const lines = out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)
+  let jsonStr = lines[lines.length - 1]
+  if (!jsonStr || !jsonStr.startsWith('{')) {
+    const lastJson = lines.filter(l => l.startsWith('{')).pop()
+    jsonStr = lastJson || out
+  }
+  let result
+  try {
+    result = JSON.parse(jsonStr)
+  } catch (_) {
+    return { success: false, error: out.slice(-300) || '无法解析输出' }
+  }
+  if (!result.success) return { success: false, error: result.error || '未下载到图片' }
+  return { success: true, path: result.path }
+}
+
+module.exports = { executeDownloadImg }

+ 2 - 0
nodejs/ef-compiler/actions/fun/fun-node-registry.js

@@ -17,4 +17,6 @@ module.exports = [
   { type: 'img2text', category: 'io', in: ['prompt', 'model', 'imageUrl'], execute: 'executeImg2text', script: 'ai/img2text.js', displayName: 'ai img2text' },
   { type: 'text2img', category: 'io', in: ['prompt', 'model', 'savePath'], execute: 'executeText2img', script: 'ai/text2img.js', displayName: 'ai text2img' },
   { type: 'img2img', category: 'io', in: ['prompt', 'model', 'imageUrl', 'savePath'], execute: 'executeImg2img', script: 'ai/img2img.js', displayName: 'ai img2img' },
+  { type: 'json', category: 'io', in: ['jsonString'], execute: 'executeJsonToArr', script: '../json/json-to-arr.js', displayName: 'json to arr' },
+  { type: 'download-img', category: 'io', in: ['prompt', 'savePath'], inAlt: { savePath: 'save-path' }, execute: 'executeDownloadImg', script: 'download-img.js', displayName: 'download img by prompt' },
 ]

+ 17 - 2
nodejs/ef-compiler/actions/fun/fun-parser.js

@@ -332,8 +332,20 @@ async function run(actionType, action, ctx, device, folderPath) {
       const minS = Number(scaleRange[0])
       const maxS = Number(scaleRange[1])
       if (Number.isNaN(minS) || Number.isNaN(maxS) || minS >= maxS) return { success: false, error: 'img-center-point-location inVars[1] 缩放比范围无效,需为两个数字且 min < max' }
+      let centerRatio = 1
+      if (action.inVars?.length > 2 && action.inVars[2] != null) {
+        const third = action.inVars[2]
+        if (Array.isArray(third) && third.length >= 2) {
+          const p = Number(third[0])
+          const b = (third[1] != null && String(third[1]).trim().toLowerCase()) || ''
+          if (!Number.isNaN(p) && p > 0 && (b === 'w' || b === 'h')) centerRatio = [p, b]
+        } else {
+          const v = Number(third)
+          if (!Number.isNaN(v) && v > 0 && v <= 1) centerRatio = v
+        }
+      }
       if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
-      const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath, scaleRange: [minS, maxS] })
+      const result = await executeImgCenterPointLocation({ device, template: templatePath, folderPath, scaleRange: [minS, maxS], centerRatio })
       if (!result.success) return { success: false, error: `图像中心点定位失败: ${result.error}` }
       const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : (action.variable ? extractVarName(action.variable) : null)
       if (outputVarName) {
@@ -698,7 +710,10 @@ async function run(actionType, action, ctx, device, folderPath) {
         const outputVarName = action.outVars?.[0] != null ? extractVarName(String(action.outVars[0]).trim()) : null
         if (outputVarName && result != null) {
           const outVal = result.path ?? result.value ?? result.result
-          if (outVal !== undefined && outVal !== null) variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
+          if (outVal !== undefined && outVal !== null) {
+            if (actionType === 'json' && Array.isArray(outVal)) variableContext[outputVarName] = outVal
+            else variableContext[outputVarName] = typeof outVal === 'string' ? outVal : String(outVal)
+          }
         }
         await logOutVars(action, variableContext, folderPath)
         return { success: true, ...result }

+ 23 - 9
nodejs/ef-compiler/actions/fun/img-center-point-location.js

@@ -18,8 +18,8 @@ const imageMatchScriptPath = path.join(projectRoot, 'python', 'scripts', 'image-
 const tagName = 'img-center-point-location'
 
 const schema = {
-  description: '在屏幕截图中查找模板图片的位置并返回中心点坐标(可用于定位/点击)。inVars[1] 为缩放比范围 [min, max],必填。',
-  inputs: { template: '模板图片路径', scaleRange: '缩放比范围数组 [min, max],如 [0.2, 1.6]', variable: '输出变量名' },
+  description: '在屏幕截图中查找模板图片的位置并返回中心点坐标。inVars[1] 缩放比 [min,max]。inVars[2] 可选:数字 0–1 为旧版中心比例;或 [裁剪百分比, "w"|"h"] 表示以模板宽/高的该比例作为正方形边长取中心方形裁剪后匹配,如 [1,"w"]、[0.1,"h"]。',
+  inputs: { template: '模板图片路径', scaleRange: '缩放比范围数组 [min, max]', centerRatio: '可选:数字 0–1 或 [percent, "w"|"h"] 方形裁剪', variable: '输出变量名' },
   outputs: { variable: '中心点坐标(JSON 字符串格式,如:{"x":123,"y":456})' },
 }
 
@@ -35,8 +35,8 @@ function getPythonPath() {
   return 'python'
 }
 
-/** 在设备截图中匹配模板,返回坐标与中心点。scaleRange 为 [min, max] 缩放比范围,必填。 */
-function matchImageAndGetCoordinate(device, imagePath, scaleRange) {
+/** 在设备截图中匹配模板,返回坐标与中心点。scaleRange 为 [min, max]。centerRatio 为数字 0–1(旧版)或 [percent, 'w'|'h'] 方形裁剪。 */
+function matchImageAndGetCoordinate(device, imagePath, scaleRange, centerRatio) {
   if (!imagePath || typeof imagePath !== 'string') return { success: false, error: '模板路径为空' }
   if (!Array.isArray(scaleRange) || scaleRange.length < 2) return { success: false, error: '缩放比范围 scaleRange 必填,且为 [min, max] 数组,如 [0.2, 1.6]' }
   const minScale = Number(scaleRange[0])
@@ -44,7 +44,10 @@ function matchImageAndGetCoordinate(device, imagePath, scaleRange) {
   if (Number.isNaN(minScale) || Number.isNaN(maxScale) || minScale >= maxScale) return { success: false, error: '缩放比范围无效,需为两个数字且 min < max' }
   const templatePath = path.isAbsolute(imagePath) ? imagePath : path.resolve(projectRoot, imagePath)
   const ts = Date.now()
-  const screenshotPath = path.join(os.tmpdir(), `ef-screenshot-${ts}.png`)
+  const templateDir = path.dirname(templatePath)
+  const templateBase = path.basename(templatePath, path.extname(templatePath))
+  const screenshotPath = path.join(templateDir, `Screenshot-${templateBase}.png`)
+  try { fs.mkdirSync(templateDir, { recursive: true }) } catch (_) {}
   const templateCopyPath = path.join(os.tmpdir(), `ef-template-${ts}.png`)
   fs.copyFileSync(templatePath, templateCopyPath)
 
@@ -52,15 +55,26 @@ function matchImageAndGetCoordinate(device, imagePath, scaleRange) {
   const adbPath = config.adbPath?.path
     ? (path.isAbsolute(config.adbPath.path) ? config.adbPath.path : path.resolve(projectRoot, config.adbPath.path))
     : path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
-  const args = [imageMatchScriptPath, '--adb', adbPath, '--device', device, '--screenshot', screenshotPath.replace(/\\/g, '/'), '--template', templateCopyPath.replace(/\\/g, '/'), '--method', 'feature', '--scale-min', String(minScale), '--scale-max', String(maxScale)]
+  const cropOutputPath = path.join(templateDir, `Matched-${templateBase}.png`)
+  const args = [imageMatchScriptPath, '--adb', adbPath, '--device', device, '--screenshot', screenshotPath.replace(/\\/g, '/'), '--template', templateCopyPath.replace(/\\/g, '/'), '--method', 'feature', '--scale-min', String(minScale), '--scale-max', String(maxScale), '--crop-output', cropOutputPath.replace(/\\/g, '/')]
+  const isCropSquare = Array.isArray(centerRatio) && centerRatio.length >= 2 && typeof centerRatio[0] === 'number' && centerRatio[0] > 0
+  const cropBase = isCropSquare ? String(centerRatio[1]).trim().toLowerCase() : ''
+  const hasCrop = isCropSquare && (cropBase === 'w' || cropBase === 'h') || (centerRatio != null && typeof centerRatio === 'number' && centerRatio > 0 && centerRatio < 1)
+  if (isCropSquare && (cropBase === 'w' || cropBase === 'h')) {
+    args.push('--crop-square', String(centerRatio[0]), cropBase)
+  } else {
+    const ratio = centerRatio != null && typeof centerRatio === 'number' && centerRatio > 0 && centerRatio <= 1 ? centerRatio : 1
+    if (ratio < 1) args.push('--center-ratio', String(ratio))
+  }
+  if (hasCrop) args.push('--template-output', templatePath.replace(/\\/g, '/'))
   const r = spawnSync(pythonPath, args, {
     encoding: 'utf-8',
     timeout: 20000,
     env: { ...process.env, PYTHONIOENCODING: 'utf-8' },
     cwd: projectRoot
   })
-  try { fs.unlinkSync(screenshotPath) } catch (_) {}
   try { fs.unlinkSync(templateCopyPath) } catch (_) {}
+  // 截图保留在模板同级目录(Screenshot-pic0.png 等),便于排查匹配失败原因
 
   if (r.status !== 0) return { success: false, error: (r.stderr || r.stdout || '').trim() || '图像匹配失败' }
   const out = JSON.parse(r.stdout.trim())
@@ -72,7 +86,7 @@ function matchImageAndGetCoordinate(device, imagePath, scaleRange) {
   }
 }
 
-async function executeImgCenterPointLocation({ device, template, folderPath, scaleRange }) {
+async function executeImgCenterPointLocation({ device, template, folderPath, scaleRange, centerRatio }) {
   if (!device) return { success: false, error: '缺少设备 ID,无法自动获取截图' }
   if (!template || typeof template !== 'string') return { success: false, error: '缺少模板图片路径' }
   if (!Array.isArray(scaleRange) || scaleRange.length < 2) return { success: false, error: 'img-center-point-location 必须填写 inVars[1] 缩放比范围 [min, max],如 [0.2, 1.6]' }
@@ -81,7 +95,7 @@ async function executeImgCenterPointLocation({ device, template, folderPath, sca
   const isAbsoluteOrDrive = template.startsWith('/') || template.includes(':')
   const hasSubPath = template.includes('/') || template.includes(path.sep)
   const templatePath = isAbsoluteOrDrive ? template : (hasSubPath ? path.join(baseDir, template) : path.join(baseDir, 'resources', template))
-  const result = matchImageAndGetCoordinate(device, templatePath, scaleRange)
+  const result = matchImageAndGetCoordinate(device, templatePath, scaleRange, centerRatio)
   if (!result.success) return { success: false, error: result.error }
   const center = result.clickPosition || { x: result.coordinate.x + result.coordinate.width / 2, y: result.coordinate.y + result.coordinate.height / 2 }
   return { success: true, center, coordinate: result.coordinate }

+ 25 - 0
nodejs/ef-compiler/actions/json/json-to-arr.js

@@ -0,0 +1,25 @@
+/**
+ * json 结点:将 JSON 字符串解析为数组(如 img-url-json -> img-url-arr)
+ * 对应 process:type: "json", method: "json-to-arr", inVars: ["{img-url-json}"], outVars: ["{img-url-arr}"]
+ */
+
+/**
+ * 将 JSON 字符串解析为数组并返回,供后续 for 等使用。
+ * @param {{ jsonString: string }} input - jsonString 为变量取值后的 JSON 字符串
+ * @returns {{ success: boolean, result?: any[], error?: string }}
+ */
+async function executeJsonToArr({ jsonString }) {
+  if (jsonString == null) return { success: false, error: 'json-to-arr 缺少输入(如 inVars[0])' }
+  const str = typeof jsonString === 'string' ? jsonString.trim() : String(jsonString).trim()
+  if (!str) return { success: false, error: 'json-to-arr 输入为空' }
+  let parsed
+  try {
+    parsed = JSON.parse(str)
+  } catch (e) {
+    return { success: false, error: `JSON 解析失败: ${e && e.message ? e.message : String(e)}` }
+  }
+  const arr = Array.isArray(parsed) ? parsed : (parsed != null ? [parsed] : [])
+  return { success: true, result: arr }
+}
+
+module.exports = { executeJsonToArr }

+ 8 - 1
nodejs/ef-compiler/sequence-runner.js

@@ -107,12 +107,19 @@ async function executeActionSequence(
         }
         if (timesVarKey) state.declaredVariableNames = originalDeclaredForTimes
       } else {
-        const items = Array.isArray(action.items) ? action.items : []
+        let items = action.array
+        if (typeof items === 'string' && items.startsWith('{') && items.endsWith('}')) {
+          const varName = items.slice(1, -1).trim()
+          items = variableContext[varName]
+        }
+        items = Array.isArray(items) ? items : []
         const indexKey = action.indexVariable != null ? String(action.indexVariable).replace(/^\{|\}$/g, '').trim() : null
         const variableKey = action.variable != null ? String(action.variable).replace(/^\{|\}$/g, '').trim() : null
         const originalDeclared = state.declaredVariableNames
         const forLocals = [indexKey, variableKey].filter(Boolean)
         if (forLocals.length > 0) state.declaredVariableNames = (originalDeclared || []).concat(forLocals)
+        // 每次进入 for 都先重置索引变量,避免沿用上一个 for 循环的 idx 等(多个 for 共用同一 variableContext)
+        if (indexKey !== null) variableContext[indexKey] = 0
         for (let i = 0; i < items.length; i++) {
           if (shouldStop && shouldStop()) return { success: false, error: 'Execution stopped', completedSteps }
           if (indexKey !== null) variableContext[indexKey] = i

+ 3 - 0
python/imagedl/.github/FUNDING.yml

@@ -0,0 +1,3 @@
+patreon: CharlesPikachu
+ko_fi: charlespikachu
+github: CharlesPikachu

+ 91 - 0
python/imagedl/.github/ISSUE_TEMPLATE/bug_report.en.yml

@@ -0,0 +1,91 @@
+name: "🐛 Bug report"
+description: "Report a reproducible bug."
+title: "[Bug]: "
+labels: ["bug"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        Thanks for taking the time to report a bug.
+        Please fill in the information below so we can reproduce and fix it faster.
+
+  - type: dropdown
+    id: severity
+    attributes:
+      label: Severity
+      options:
+        - "S1 - Critical (crash/data loss/security)"
+        - "S2 - High (major feature broken)"
+        - "S3 - Medium (workaround exists)"
+        - "S4 - Low (minor / cosmetic)"
+    validations:
+      required: true
+
+  - type: input
+    id: version
+    attributes:
+      label: Version / Commit
+      description: "Which version are you using? (e.g., v1.2.3, commit SHA)"
+      placeholder: "v1.2.3 / 1a2b3c4"
+    validations:
+      required: false
+
+  - type: textarea
+    id: what_happened
+    attributes:
+      label: What happened?
+      description: "Describe the bug clearly."
+      placeholder: "A clear and concise description..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: expected
+    attributes:
+      label: What did you expect?
+      placeholder: "Expected behavior..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: steps
+    attributes:
+      label: Steps to reproduce
+      description: "Provide minimal steps to reproduce."
+      placeholder: |
+        1. Go to ...
+        2. Run ...
+        3. See error ...
+    validations:
+      required: true
+
+  - type: textarea
+    id: logs
+    attributes:
+      label: Logs / Screenshots
+      description: "Paste logs or attach screenshots if applicable."
+      render: shell
+    validations:
+      required: false
+
+  - type: textarea
+    id: env
+    attributes:
+      label: Environment
+      description: "OS / runtime / dependencies"
+      placeholder: |
+        - OS: Windows 11 / Ubuntu 22.04 / macOS 14
+        - Runtime: Python 3.11 / Node 20
+        - Package manager: pip/conda/npm
+    validations:
+      required: false
+
+  - type: checkboxes
+    id: checklist
+    attributes:
+      label: Checklist
+      options:
+        - label: "I searched existing issues and did not find a duplicate."
+          required: true
+        - label: "I can reproduce this consistently."
+          required: false

+ 90 - 0
python/imagedl/.github/ISSUE_TEMPLATE/bug_report.zh.yml

@@ -0,0 +1,90 @@
+name: "🐛 缺陷反馈"
+description: "报告一个可复现的 Bug。"
+title: "[Bug]: "
+labels: ["bug"]
+body:
+  - type: markdown
+    attributes:
+      value: |
+        感谢反馈!请尽量补全信息,便于我们快速复现与修复。
+
+  - type: dropdown
+    id: severity
+    attributes:
+      label: 严重程度
+      options:
+        - "S1 - 致命 (崩溃/数据丢失/安全)"
+        - "S2 - 高 (核心功能不可用)"
+        - "S3 - 中 (有替代方案)"
+        - "S4 - 低 (小问题/界面问题)"
+    validations:
+      required: true
+
+  - type: input
+    id: version
+    attributes:
+      label: 版本 / 提交号
+      description: "你使用的版本 (例如 v1.2.3 或 commit SHA)"
+      placeholder: "v1.2.3 / 1a2b3c4"
+    validations:
+      required: false
+
+  - type: textarea
+    id: what_happened
+    attributes:
+      label: 实际发生了什么?
+      description: "清晰描述 Bug 现象。"
+      placeholder: "现象描述..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: expected
+    attributes:
+      label: 期望结果是什么?
+      placeholder: "期望行为..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: steps
+    attributes:
+      label: 复现步骤
+      description: "给出最小可复现步骤。"
+      placeholder: |
+        1. 打开 ...
+        2. 执行 ...
+        3. 出现 ...
+    validations:
+      required: true
+
+  - type: textarea
+    id: logs
+    attributes:
+      label: 日志 / 截图
+      description: "粘贴日志或附上截图 (如适用)。"
+      render: shell
+    validations:
+      required: false
+
+  - type: textarea
+    id: env
+    attributes:
+      label: 环境信息
+      description: "OS / 运行时 / 依赖版本等"
+      placeholder: |
+        - OS: Windows 11 / Ubuntu 22.04 / macOS 14
+        - Runtime: Python 3.11 / Node 20
+        - 包管理: pip/conda/npm
+    validations:
+      required: false
+
+  - type: checkboxes
+    id: checklist
+    attributes:
+      label: 检查项
+      options:
+        - label: "我已搜索现有 issues, 未发现重复。"
+          required: true
+        - label: "我可以稳定复现该问题。"
+          required: false

+ 8 - 0
python/imagedl/.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+  - name: 💬 Discussions / 讨论区
+    url: https://github.com/CharlesPikachu/imagedl/discussions
+    about: Please ask and answer questions here. / 建议在讨论区提问与交流。
+  - name: 🔒 Security / 安全问题
+    url: https://github.com/CharlesPikachu/imagedl/security/policy
+    about: Please report security vulnerabilities privately. / 安全漏洞请私下提交。

+ 28 - 0
python/imagedl/.github/ISSUE_TEMPLATE/documentation.en.yml

@@ -0,0 +1,28 @@
+name: "📚 Documentation"
+description: "Report missing/incorrect docs or propose improvements."
+title: "[Docs]: "
+labels: ["documentation"]
+body:
+  - type: textarea
+    id: issue
+    attributes:
+      label: What's wrong / missing?
+      placeholder: "The docs are unclear about..."
+    validations:
+      required: true
+
+  - type: input
+    id: page
+    attributes:
+      label: Related page / section
+      placeholder: "Link or path to the docs page"
+    validations:
+      required: false
+
+  - type: textarea
+    id: suggestion
+    attributes:
+      label: Suggested change
+      placeholder: "Proposed wording / examples..."
+    validations:
+      required: false

+ 28 - 0
python/imagedl/.github/ISSUE_TEMPLATE/documentation.zh.yml

@@ -0,0 +1,28 @@
+name: "📚 文档问题"
+description: "反馈文档缺失/错误或提出改进建议。"
+title: "[Docs]: "
+labels: ["documentation"]
+body:
+  - type: textarea
+    id: issue
+    attributes:
+      label: 哪里不对 / 缺什么?
+      placeholder: "文档在...部分不清晰/错误..."
+    validations:
+      required: true
+
+  - type: input
+    id: page
+    attributes:
+      label: 相关页面 / 章节
+      placeholder: "文档链接或路径"
+    validations:
+      required: false
+
+  - type: textarea
+    id: suggestion
+    attributes:
+      label: 建议修改
+      placeholder: "建议的表述 / 示例..."
+    validations:
+      required: false

+ 49 - 0
python/imagedl/.github/ISSUE_TEMPLATE/feature_request.en.yml

@@ -0,0 +1,49 @@
+name: "✨ Feature request"
+description: "Suggest an idea or improvement."
+title: "[Feature]: "
+labels: ["enhancement"]
+body:
+  - type: textarea
+    id: problem
+    attributes:
+      label: Problem statement
+      description: "What problem are you trying to solve?"
+      placeholder: "I'm frustrated when..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: proposal
+    attributes:
+      label: Proposed solution
+      description: "What would you like to happen?"
+      placeholder: "It would be great if..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: alternatives
+    attributes:
+      label: Alternatives considered
+      placeholder: "Other approaches..."
+    validations:
+      required: false
+
+  - type: dropdown
+    id: priority
+    attributes:
+      label: Priority
+      options:
+        - "P0 - Must have"
+        - "P1 - Should have"
+        - "P2 - Nice to have"
+    validations:
+      required: true
+
+  - type: textarea
+    id: context
+    attributes:
+      label: Additional context
+      description: "Mockups, references, related issues/PRs."
+    validations:
+      required: false

+ 49 - 0
python/imagedl/.github/ISSUE_TEMPLATE/feature_request.zh.yml

@@ -0,0 +1,49 @@
+name: "✨ 功能需求"
+description: "提出新功能或改进建议。"
+title: "[Feature]: "
+labels: ["enhancement"]
+body:
+  - type: textarea
+    id: problem
+    attributes:
+      label: 需求背景 / 痛点
+      description: "你想解决什么问题?"
+      placeholder: "目前困扰是..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: proposal
+    attributes:
+      label: 期望方案
+      description: "你希望系统如何改进?"
+      placeholder: "希望可以..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: alternatives
+    attributes:
+      label: 备选方案
+      placeholder: "其他可能的实现方式..."
+    validations:
+      required: false
+
+  - type: dropdown
+    id: priority
+    attributes:
+      label: 优先级
+      options:
+        - "P0 - 必须"
+        - "P1 - 应该"
+        - "P2 - 可选"
+    validations:
+      required: true
+
+  - type: textarea
+    id: context
+    attributes:
+      label: 补充信息
+      description: "原型图、参考链接、相关 issue/PR 等。"
+    validations:
+      required: false

+ 39 - 0
python/imagedl/.github/ISSUE_TEMPLATE/performance.en.yml

@@ -0,0 +1,39 @@
+name: "🚀 Performance"
+description: "Report a performance issue or regression."
+title: "[Perf]: "
+labels: ["performance"]
+body:
+  - type: textarea
+    id: summary
+    attributes:
+      label: Summary
+      placeholder: "What is slow and where?"
+    validations:
+      required: true
+
+  - type: textarea
+    id: baseline
+    attributes:
+      label: Baseline vs Current
+      placeholder: |
+        Baseline: 120ms
+        Current: 600ms
+        Regression since: v1.2.0
+    validations:
+      required: false
+
+  - type: textarea
+    id: steps
+    attributes:
+      label: Steps to reproduce / benchmark
+      placeholder: "Command / script / dataset / input size..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: profiling
+    attributes:
+      label: Profiling info
+      placeholder: "Profiler output, flamegraph, timing breakdown..."
+    validations:
+      required: false

+ 39 - 0
python/imagedl/.github/ISSUE_TEMPLATE/performance.zh.yml

@@ -0,0 +1,39 @@
+name: "🚀 性能问题"
+description: "反馈性能下降或耗时过长的问题。"
+title: "[Perf]: "
+labels: ["performance"]
+body:
+  - type: textarea
+    id: summary
+    attributes:
+      label: 问题概述
+      placeholder: "哪里慢?慢到什么程度?"
+    validations:
+      required: true
+
+  - type: textarea
+    id: baseline
+    attributes:
+      label: 基线 vs 当前
+      placeholder: |
+        基线: 120ms
+        当前: 600ms
+        从哪个版本开始变慢: v1.2.0
+    validations:
+      required: false
+
+  - type: textarea
+    id: steps
+    attributes:
+      label: 复现步骤 / benchmark
+      placeholder: "命令 / 脚本 / 数据集 / 输入大小..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: profiling
+    attributes:
+      label: Profiling 信息
+      placeholder: "Profiler 输出、火焰图、耗时拆分..."
+    validations:
+      required: false

+ 28 - 0
python/imagedl/.github/ISSUE_TEMPLATE/question.en.yml

@@ -0,0 +1,28 @@
+name: "❓ Question"
+description: "Ask a question or request support."
+title: "[Question]: "
+labels: ["question"]
+body:
+  - type: textarea
+    id: question
+    attributes:
+      label: Your question
+      placeholder: "How do I..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: context
+    attributes:
+      label: Context
+      description: "What are you trying to achieve?"
+    validations:
+      required: false
+
+  - type: textarea
+    id: tried
+    attributes:
+      label: What you've tried
+      placeholder: "I tried..."
+    validations:
+      required: false

+ 28 - 0
python/imagedl/.github/ISSUE_TEMPLATE/question.zh.yml

@@ -0,0 +1,28 @@
+name: "❓ 问题 / 支持"
+description: "提问或寻求使用帮助。"
+title: "[Question]: "
+labels: ["question"]
+body:
+  - type: textarea
+    id: question
+    attributes:
+      label: 你的问题
+      placeholder: "我想知道如何..."
+    validations:
+      required: true
+
+  - type: textarea
+    id: context
+    attributes:
+      label: 背景说明
+      description: "你想达成什么目标?"
+    validations:
+      required: false
+
+  - type: textarea
+    id: tried
+    attributes:
+      label: 你已经尝试过什么
+      placeholder: "我尝试了..."
+    validations:
+      required: false

+ 33 - 0
python/imagedl/.github/ISSUE_TEMPLATE/task.en.yml

@@ -0,0 +1,33 @@
+name: "🧹 Task"
+description: "Engineering task / refactor / maintenance."
+title: "[Task]: "
+labels: ["task"]
+body:
+  - type: textarea
+    id: goal
+    attributes:
+      label: Goal
+      placeholder: "What needs to be done and why?"
+    validations:
+      required: true
+
+  - type: textarea
+    id: scope
+    attributes:
+      label: Scope / checklist
+      placeholder: |
+        - [ ] ...
+        - [ ] ...
+    validations:
+      required: false
+
+  - type: dropdown
+    id: risk
+    attributes:
+      label: Risk level
+      options:
+        - "Low"
+        - "Medium"
+        - "High"
+    validations:
+      required: true

+ 33 - 0
python/imagedl/.github/ISSUE_TEMPLATE/task.zh.yml

@@ -0,0 +1,33 @@
+name: "🧹 任务 / 杂项"
+description: "工程任务 / 重构 / 维护。"
+title: "[Task]: "
+labels: ["task"]
+body:
+  - type: textarea
+    id: goal
+    attributes:
+      label: 目标
+      placeholder: "需要做什么?为什么要做?"
+    validations:
+      required: true
+
+  - type: textarea
+    id: scope
+    attributes:
+      label: 范围 / checklist
+      placeholder: |
+        - [ ] ...
+        - [ ] ...
+    validations:
+      required: false
+
+  - type: dropdown
+    id: risk
+    attributes:
+      label: 风险等级
+      options:
+        - "低"
+        - "中"
+        - "高"
+    validations:
+      required: true

BIN
python/imagedl/.github/pictures/alipay_reward.png


BIN
python/imagedl/.github/pictures/wechat_reward.jpg


+ 54 - 0
python/imagedl/.github/workflows/check-imagedl.yml

@@ -0,0 +1,54 @@
+name: daily imagedl check
+
+on:
+  schedule:
+    - cron: "0 1 * * *"
+  workflow_dispatch: {}
+
+permissions:
+  contents: write
+
+jobs:
+  check-imagedl:
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repo
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 0
+
+      - name: Set up Python
+        uses: actions/setup-python@v5
+        with:
+          python-version: "3.10"
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install git+https://github.com/CharlesPikachu/imagedl.git@main
+
+      # env:
+      #   SOME_API_KEY: ${{ secrets.SOME_API_KEY }}
+
+      - name: Run imagedl daily check
+        run: |
+          python scripts/quick_check_imagedl.py
+
+      - name: Commit and push daily_test_results
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
+          AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
+        run: |
+          git config user.name  "github-actions[bot]"
+          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+          git add daily_test_results
+
+          if git diff --cached --quiet; then
+            echo "No changes to commit"
+          else
+            git commit -m "Update imagedl check $(date -u '+%Y-%m-%d %H:%M:%S')" --author="${AUTHOR_NAME} <${AUTHOR_EMAIL}>"
+            git push
+          fi

+ 129 - 0
python/imagedl/.gitignore

@@ -0,0 +1,129 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/

+ 17 - 0
python/imagedl/.readthedocs.yaml

@@ -0,0 +1,17 @@
+# required
+version: 2
+
+# set the OS, python version and other tools you might need
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+
+# build documentation in the "docs/" directory with Sphinx
+sphinx:
+  configuration: docs/conf.py
+
+# explicitly set the version of Python and its requirements
+python:
+  install:
+    - requirements: docs/requirements.txt

+ 8 - 0
python/imagedl/CITATION.cff

@@ -0,0 +1,8 @@
+cff-version: 1.2.0
+message: "If you use this software, please cite it as below."
+authors:
+  - name: "Zhenchao Jin"
+title: "Imagedl: Search and download images from specific websites"
+date-released: 2022-03-21
+url: "https://github.com/CharlesPikachu/imagedl"
+license: Apache-2.0

+ 201 - 0
python/imagedl/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 12 - 0
python/imagedl/Makefile

@@ -0,0 +1,12 @@
+.PHONY: install
+
+install:
+	python setup.py install
+
+develop:
+	python setup.py develop
+
+publish:
+	pip install 'twine>=1.5.0' --upgrade
+	python setup.py sdist bdist_wheel
+	twine upload dist/*

+ 370 - 0
python/imagedl/README.md

@@ -0,0 +1,370 @@
+<div align="center">
+  <img src="https://raw.githubusercontent.com/CharlesPikachu/imagedl/main/docs/logo.png" width="600"/>
+</div>
+<br />
+
+<p align="center">
+  <a href="https://imagedl.readthedocs.io/">
+    <img src="https://img.shields.io/badge/docs-latest-blue" alt="Docs">
+  </a>
+  <a href="https://pypi.org/project/pyimagedl/">
+    <img src="https://img.shields.io/pypi/pyversions/pyimagedl" alt="PyPI - Python Version">
+  </a>
+  <a href="https://pypi.org/project/pyimagedl/">
+    <img src="https://img.shields.io/pypi/v/pyimagedl" alt="PyPI">
+  </a>
+  <a href="https://github.com/CharlesPikachu/imagedl/blob/main/LICENSE">
+    <img src="https://badgen.net/github/license/CharlesPikachu/imagedl" alt="License" >
+  </a>
+  <a href="https://pypi.org/project/pyimagedl/">
+    <img src="https://static.pepy.tech/badge/pyimagedl" alt="PyPI - Downloads (total)">
+  </a>
+  <a href="https://pypi.org/project/pyimagedl/">
+    <img src="https://static.pepy.tech/badge/pyimagedl/month" alt="PyPI - Downloads (month)">
+  </a>
+  <a href="https://pypi.org/project/pyimagedl/">
+    <img src="https://static.pepy.tech/badge/pyimagedl/week" alt="PyPI - Downloads (week)">
+  </a>
+  <a href="https://github.com/CharlesPikachu/imagedl/actions/workflows/check-imagedl.yml">
+    <img src="https://github.com/CharlesPikachu/imagedl/actions/workflows/check-imagedl.yml/badge.svg" alt="Daily Imagedl Check">
+  </a>
+  <a href="https://github.com/CharlesPikachu/imagedl/issues">
+    <img src="https://isitmaintained.com/badge/resolution/CharlesPikachu/imagedl.svg" alt="Issue Resolution">
+  </a>
+  <a href="https://github.com/CharlesPikachu/imagedl/issues">
+    <img src="https://isitmaintained.com/badge/open/CharlesPikachu/imagedl.svg" alt="Open Issues">
+  </a>
+</p>
+
+<p align="center">
+  <a href="https://imagedl.readthedocs.io/">
+    <strong>📚 Documents: imagedl.readthedocs.io</strong>
+  </a>
+</p>
+
+<p align="center">
+  <a href="https://charlespikachu.github.io/imagedl/">
+    <strong>🧪 Online API Health &amp; Demo: charlespikachu.github.io/imagedl</strong>
+  </a>
+  <br />
+  <sub>
+    <em>
+      Automatically runs daily checks on all registered imagedl modules (search + download)
+      via GitHub Actions and visualizes the latest results on this page.
+    </em>
+  </sub>
+  <br /><br />
+  <a href="https://charlespikachu.github.io/imagedl/">
+    <img
+      alt="demo"
+      src="https://img.shields.io/badge/demo-online-brightgreen?style=for-the-badge"
+    />
+  </a>
+</p>
+
+<p align="center">
+  <strong>学习收获更多有趣的内容, 欢迎关注微信公众号:Charles的皮卡丘</strong>
+</p>
+
+
+# 🆕 What's New
+
+- 2026-03-04: Released pyimagedl v0.3.8 — added image search and download functionality for Weibo; integrated the cloudscraper library to better download certain restricted images; code optimizations and bug fixes.
+- 2026-02-23: Released pyimagedl v0.3.7 — resolved underlying issues to restore reliable image search and download functionality across multiple supported platforms.
+- 2026-02-12: Released pyimagedl v0.3.6 — added support for searching and downloading images from FreeNatureStock, and optimized other parts of the code.
+
+
+# 📘 Introduction
+
+Imagedl lets you search for and download images from specific websites. If you find it useful, please consider starring the repository to follow updates—thank you for your support!
+
+
+# 🖼️ Supported Image Client
+
+|  ImageClient (EN)                                            |  ImageClient (CN)                                         |   Search           |  Download            |    Code Snippet                                                                                                            |
+|  :----:                                                      |  :----:                                                   |   :----:           |  :----:              |    :----:                                                                                                                  |
+|  [BaiduImageClient](https://image.baidu.com/)                |  [百度图片](https://image.baidu.com/)                     |   ✔️               |  ✔️                  |    [baidu.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/baidu.py)                        |
+|  [BingImageClient](https://www.bing.com/images)              |  [必应图片](https://www.bing.com/images)                  |   ✔️               |  ✔️                  |    [bing.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/bing.py)                          |
+|  [DuckduckgoImageClient](https://duckduckgo.com/)            |  [DuckDuckGo图片](https://duckduckgo.com/)                |   ✔️               |  ✔️                  |    [duckduckgo.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/duckduckgo.py)              |
+|  [DanbooruImageClient](https://danbooru.donmai.us/)          |  [Danbooru动漫图片](https://danbooru.donmai.us/)          |   ✔️               |  ✔️                  |    [danbooru.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/danbooru.py)                  |
+|  [DimTownImageClient](https://dimtown.com/home)              |  [次元小镇](https://dimtown.com/home)                     |   ✔️               |  ✔️                  |    [dimtown.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/dimtown.py)                    |
+|  [EverypixelImageClient](https://www.everypixel.com/)        |  [Everypixel](https://www.everypixel.com/)                |   ✔️               |  ✔️                  |    [everypixel.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/everypixel.py)              |
+|  [FoodiesfeedImageClient](https://www.foodiesfeed.com/)      |  [Foodiesfeed美食图片](https://www.foodiesfeed.com/)      |   ✔️               |  ✔️                  |    [foodiesfeed.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/foodiesfeed.py)            |
+|  [FreeNatureStockImageClient](https://freenaturestock.com/)  |  [FreeNatureStock自然图片](https://freenaturestock.com/)  |   ✔️               |  ✔️                  |    [freenaturestock.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/freenaturestock.py)    |
+|  [GoogleImageClient](https://images.google.com/)             |  [谷歌图片](https://images.google.com/)                   |   ✔️               |  ✔️                  |    [google.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/google.py)                      |
+|  [GelbooruImageClient](https://gelbooru.com/)                |  [Gelbooru动漫图片](https://gelbooru.com/)                |   ✔️               |  ✔️                  |    [gelbooru.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/gelbooru.py)                  |
+|  [HuabanImageClient](https://huaban.com/)                    |  [花瓣网](https://huaban.com/)                            |   ✔️               |  ✔️                  |    [huaban.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/huaban.py)                      |
+|  [I360ImageClient](https://image.so.com/)                    |  [360图片](https://image.so.com/)                         |   ✔️               |  ✔️                  |    [i360.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/i360.py)                          |
+|  [PixabayImageClient](https://pixabay.com/zh/photos/)        |  [Pixabay高清图片](https://pixabay.com/zh/photos/)        |   ✔️               |  ✔️                  |    [pixabay.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/pixabay.py)                    |
+|  [PexelsImageClient](https://www.pexels.com/zh-cn/)          |  [Pexels高清图片](https://www.pexels.com/zh-cn/)          |   ✔️               |  ✔️                  |    [pexels.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/pexels.py)                      |
+|  [SogouImageClient](https://pic.sogou.com/)                  |  [搜狗图片](https://pic.sogou.com/)                       |   ✔️               |  ✔️                  |    [sogou.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/sogou.py)                        |
+|  [SafebooruImageClient](https://safebooru.org/)              |  [Safebooru动漫图片](https://safebooru.org/)              |   ✔️               |  ✔️                  |    [safebooru.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/safebooru.py)                |
+|  [UnsplashImageClient](https://unsplash.com/)                |  [Unsplash图片](https://unsplash.com/)                    |   ✔️               |  ✔️                  |    [unsplash.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/unsplash.py)                  |
+|  [WeiboImageClient](https://m.weibo.cn/)                     |  [微博图片](https://m.weibo.cn/)                          |   ✔️               |  ✔️                  |    [weibo.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/weibo.py)                        |
+|  [YandexImageClient](https://yandex.com/images/)             |  [Yandex图片](https://yandex.com/images/)                 |   ✔️               |  ✔️                  |    [yandex.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/yandex.py)                      |
+|  [YahooImageClient](https://images.search.yahoo.com/)        |  [雅虎图片](https://images.search.yahoo.com/)             |   ✔️               |  ✔️                  |    [yahoo.py](https://github.com/CharlesPikachu/imagedl/blob/main/imagedl/modules/sources/yahoo.py)                        |
+
+
+# 📦 Install
+
+You have three installation methods to choose from,
+
+```sh
+# from pip
+pip install pyimagedl
+# from github repo method-1
+pip install git+https://github.com/CharlesPikachu/imagedl.git@main
+# from github repo method-2
+git clone https://github.com/CharlesPikachu/imagedl.git
+cd imagedl
+python setup.py install
+```
+
+
+# ⚡ Quick Start
+
+After installing imagedl, you can use the following few lines of code to quickly get started with it,
+
+```python
+from imagedl import imagedl
+
+image_client = imagedl.ImageClient(image_source='BaiduImageClient')
+image_client.startcmdui()
+```
+
+where `image_source` is used to specify the image search and download engine.
+Of course, you can equivalently enter `imagedl -i "BaiduImageClient"` in the terminal to execute the above code.
+`imagedl --help` displays the basic usage of the command-line tool.
+
+```bash
+Usage: imagedl [OPTIONS]
+
+Options:
+  --version                       Show the version and exit.
+  -k, --keyword TEXT              The keywords for the image search. If left
+                                  empty, an interactive terminal will open
+                                  automatically.
+  -i, --image-source, --image_source [bingimageclient|baiduimageclient|googleimageclient|
+                                  i360imageclient|pixabayimageclient|yandeximageclient|
+                                  duckduckgoimageclient|sogouimageclient|yahooimageclient|
+                                  unsplashimageclient|danbooruimageclient|safebooruimageclient|
+                                  gelbooruimageclient|pexelsimageclient|huabanimageclient|
+                                  foodiesfeedimageclient|everypixelimageclient|weiboimageclient|
+                                  freenaturestockimageclient]
+                                  The image search and download source.
+                                  [default: BaiduImageClient]
+  -s, --search-limits, --search_limits INTEGER RANGE
+                                  Scale of image downloads.  [default: 1000;
+                                  1<=x<=100000000.0]
+  -n, --num-threadings, --num_threadings INTEGER RANGE
+                                  Number of threads used.  [default: 5;
+                                  1<=x<=256]
+  -c, --init-image-client-cfg, --init_image_client_cfg TEXT
+                                  Client config such as `work_dir` as a JSON
+                                  string.
+  -r, --request-overrides, --request_overrides TEXT
+                                  Requests.get (or Requests.post) kwargs such
+                                  as `headers` and `proxies` as a JSON string.
+  --help                          Show this message and exit.
+```
+
+For class `imagedl.ImageClient`, the acceptable arguments include,
+
+- `image_source` (`str`, default: `'BaiduImageClient'`): The image search and download source, including `['BaiduImageClient', 'BingImageClient', 'GoogleImageClient', 'I360ImageClient', 'PixabayImageClient', 'YandexImageClient', 'DuckduckgoImageClient', 'SogouImageClient', 'YahooImageClient', 'UnsplashImageClient', 'GelbooruImageClient', 'SafebooruImageClient', 'DanbooruImageClient', 'PexelsImageClient', 'DimTownImageClient', 'HuabanImageClient', 'FoodiesfeedImageClient', 'EverypixelImageClient', 'FreeNatureStockImageClient', 'WeiboImageClient']`.
+- `init_image_client_cfg` (`dict`, default: `{}`): Client initialization configuration such as `{'work_dir': 'images', 'max_retries': 5}`.
+- `search_limits` (`int`, default: `1000`): Scale of image downloads.
+- `num_threadings` (`int`, default: `5`): Number of threads used.
+- `request_overrides` (`dict`, default: `{}`): `requests.get` (or `requests.post`) kwargs such as `{'headers': {'User-Agent': xxx}, 'proxies': {}}`.
+
+The demonstration is as follows,
+
+<div align="center">
+  <img src="https://github.com/CharlesPikachu/imagedl/raw/main/docs/screenshot.gif" width="600"/>
+</div>
+<br />
+
+If you just want to do an image search, you can also do it like this,
+
+```python
+from imagedl import imagedl
+
+image_client = imagedl.ImageClient(image_source='DuckduckgoImageClient', search_limits=1000, num_threadings=5)
+image_infos = image_client.search('cut animals', search_limits_overrides=10, num_threadings_overrides=1)
+print(image_infos)
+```
+
+In the code above, `search_limits_overrides` overrides the `search_limits` argument set when initializing `imagedl.ImageClient`, and `num_threadings_overrides` works in the same way.
+The output of this code looks like,
+
+```python
+[
+    {
+        "candidate_urls": [
+            "https://img.freepik.com/.../cut-animal-cartoon-bundle-set_508290-2349.jpg",
+            "https://tse2.mm.bing.net/th/id/OIP.vD-8G0MjAMREv1bYbKaqEwHaHa..."
+        ],
+        "raw_data": {
+            "height": 626,
+            "width": 626,
+            "image": "https://img.freepik.com/.../cut-animal-cartoon-bundle-set_508290-2349.jpg",
+            "image_token": "fbff471d31328...",
+            "thumbnail": "https://tse2.mm.bing.net/th/id/OIP.vD-8G0MjAMREv1bYbKaqEwHaHa...",
+            "thumbnail_token": "4ca07ad2aab9...",
+            "source": "Bing",
+            "title": "Premium Vector | Cut animal cartoon bundle set",
+            "url": "https://www.freepik.com/premium-vector/cut-animal-cartoon-bundle-set_25750969.htm"
+        },
+        "identifier": "fbff471d31328...",
+        "work_dir": "imagedl_outputs\\DuckduckgoImageClient\\2025-11-16-22-34-25 cutanimals",
+        "file_path": "imagedl_outputs\\DuckduckgoImageClient\\2025-11-16-22-34-25 cutanimals\\00000001"
+    },
+    ...
+]
+```
+
+Then you can also call the image downloading function to download the images found by the search. The code is as follows,
+
+```python
+from imagedl import imagedl
+
+image_client = imagedl.ImageClient(image_source='DuckduckgoImageClient', search_limits=1000, num_threadings=5)
+image_infos = image_client.search('cut animals', search_limits_overrides=10, num_threadings_overrides=1)
+image_client.download(image_infos=image_infos)
+```
+
+If you prefer not to use the unified interface, you can also import a specific image search engine directly, as in the following code,
+
+```python
+from imagedl.modules.sources import (
+    BingImageClient, I360ImageClient, YahooImageClient, BaiduImageClient, SogouImageClient, GoogleImageClient, YandexImageClient, PixabayImageClient, 
+    DuckduckgoImageClient, UnsplashImageClient, GelbooruImageClient, SafebooruImageClient, DanbooruImageClient, PexelsImageClient, DimTownImageClient,
+    HuabanImageClient, FoodiesfeedImageClient, EverypixelImageClient, FreeNatureStockImageClient, WeiboImageClient
+)
+
+# bing tests
+client = BingImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# 360 tests
+client = I360ImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# baidu tests
+client = BaiduImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# sogou tests
+client = SogouImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# google tests
+client = GoogleImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# yandex tests
+client = YandexImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# pixabay tests
+client = PixabayImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# duckduckgo tests
+client = DuckduckgoImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# yahoo tests
+client = YahooImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# unsplash tests
+client = UnsplashImageClient()
+image_infos = client.search('Cute Dogs', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# gelbooru tests
+client = GelbooruImageClient()
+image_infos = client.search('pikachu', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# safebooru tests
+client = SafebooruImageClient()
+image_infos = client.search('pikachu', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# danbooru tests
+client = DanbooruImageClient()
+image_infos = client.search('pikachu', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# pexels tests
+client = PexelsImageClient()
+image_infos = client.search('animals', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# dimtown tests 
+client = DimTownImageClient()
+image_infos = client.search('JK', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# huaban tests 
+client = HuabanImageClient()
+image_infos = client.search('JK', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# foodiesfeed tests 
+client = FoodiesfeedImageClient()
+image_infos = client.search('pizza', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# everypixel tests (cookies required)
+client = EverypixelImageClient(default_search_cookies='xxxx')
+image_infos = client.search('animals', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# freenaturestock tests 
+client = FreeNatureStockImageClient()
+image_infos = client.search('mountains', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+# weibo tests (cookies required)
+client = WeiboImageClient(default_search_cookies='xxxx')
+image_infos = client.search('animals', search_limits=10, num_threadings=1)
+client.download(image_infos, num_threadings=1)
+```
+
+
+# 💡 Recommended Projects
+
+| Project                                                    | ⭐ Stars                                                                                                                                               | 📦 Version                                                                                                 | ⏱ Last Update                                                                                                                                                                   | 🛠 Repository                                                        |
+| -------------                                              | ---------                                                                                                                                             | -----------                                                                                                | ----------------                                                                                                                                                                 | --------                                                             |
+| 🎵 **Musicdl**<br/>轻量级无损音乐下载器                    | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/musicdl?style=flat-square)](https://github.com/CharlesPikachu/musicdl)                   | [![Version](https://img.shields.io/pypi/v/musicdl)](https://pypi.org/project/musicdl)                      | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/musicdl?style=flat-square)](https://github.com/CharlesPikachu/musicdl/commits/master)                   | [🛠 Repository](https://github.com/CharlesPikachu/musicdl)           |
+| 🎬 **Videodl**<br/>轻量级高清无水印视频下载器              | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/videodl?style=flat-square)](https://github.com/CharlesPikachu/videodl)                   | [![Version](https://img.shields.io/pypi/v/videofetch)](https://pypi.org/project/videofetch)                | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/videodl?style=flat-square)](https://github.com/CharlesPikachu/videodl/commits/master)                   | [🛠 Repository](https://github.com/CharlesPikachu/videodl)           |
+| 🖼️ **Imagedl**<br/>轻量级海量图片搜索下载器                | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/imagedl?style=flat-square)](https://github.com/CharlesPikachu/imagedl)                   | [![Version](https://img.shields.io/pypi/v/pyimagedl)](https://pypi.org/project/pyimagedl)                  | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/imagedl?style=flat-square)](https://github.com/CharlesPikachu/imagedl/commits/main)                     | [🛠 Repository](https://github.com/CharlesPikachu/imagedl)           |
+| 🌐 **FreeProxy**<br/>全球海量高质量免费代理采集器          | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/freeproxy?style=flat-square)](https://github.com/CharlesPikachu/freeproxy)               | [![Version](https://img.shields.io/pypi/v/pyfreeproxy)](https://pypi.org/project/pyfreeproxy)              | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/freeproxy?style=flat-square)](https://github.com/CharlesPikachu/freeproxy/commits/master)               | [🛠 Repository](https://github.com/CharlesPikachu/freeproxy)         |
+| 🌐 **MusicSquare**<br/>简易音乐搜索下载和播放网页          | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/musicsquare?style=flat-square)](https://github.com/CharlesPikachu/musicsquare)           | [![Version](https://img.shields.io/pypi/v/musicdl)](https://pypi.org/project/musicdl)                      | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/musicsquare?style=flat-square)](https://github.com/CharlesPikachu/musicsquare/commits/main)             | [🛠 Repository](https://github.com/CharlesPikachu/musicsquare)       |
+| 🌐 **FreeGPTHub**<br/>真正免费的GPT统一接口                | [![Stars](https://img.shields.io/github/stars/CharlesPikachu/FreeGPTHub?style=flat-square)](https://github.com/CharlesPikachu/FreeGPTHub)             | [![Version](https://img.shields.io/pypi/v/freegpthub)](https://pypi.org/project/freegpthub)                | [![Last Commit](https://img.shields.io/github/last-commit/CharlesPikachu/FreeGPTHub?style=flat-square)](https://github.com/CharlesPikachu/FreeGPTHub/commits/main)               | [🛠 Repository](https://github.com/CharlesPikachu/FreeGPTHub)        |
+
+
+# 📚 Citation
+
+If you use this project in your research, please cite the repository.
+
+```
+@misc{imagedl2022,
+    author = {Zhenchao Jin},
+    title = {Imagedl: Search and download images from specific websites},
+    year = {2022},
+    publisher = {GitHub},
+    journal = {GitHub repository},
+    howpublished = {\url{https://github.com/CharlesPikachu/imagedl/}},
+}
+```
+
+
+# 🌟 Star History
+
+[![Star History Chart](https://api.star-history.com/svg?repos=CharlesPikachu/imagedl&type=date&legend=top-left)](https://www.star-history.com/#CharlesPikachu/imagedl&type=date&legend=top-left)
+
+
+# ☕ Appreciation (赞赏 / 打赏)
+
+| WeChat Appreciation QR Code (微信赞赏码)                                                                                       | Alipay Appreciation QR Code (支付宝赞赏码)                                                                                     |
+| :--------:                                                                                                                     | :----------:                                                                                                                   |
+| <img src="https://raw.githubusercontent.com/CharlesPikachu/imagedl/main/.github/pictures/wechat_reward.jpg" width="260" />     | <img src="https://raw.githubusercontent.com/CharlesPikachu/imagedl/main/.github/pictures/alipay_reward.png" width="260" />     |
+
+
+# 📱 WeChat Official Account (微信公众号):
+
+Charles的皮卡丘 (*Charles_pikachu*)  
+![img](https://raw.githubusercontent.com/CharlesPikachu/imagedl/main/docs/pikachu.jpg)

BIN
python/imagedl/daily_test_results/BaiduImageClient/00000001.jpg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000004.jpg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000005.webp


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000007.jpg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000008.jpeg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000009.jpeg


BIN
python/imagedl/daily_test_results/BaiduImageClient/00000010.jpg


BIN
python/imagedl/daily_test_results/BingImageClient/00000001.webp


BIN
python/imagedl/daily_test_results/BingImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000004.png


BIN
python/imagedl/daily_test_results/BingImageClient/00000005.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000007.jpg


BIN
python/imagedl/daily_test_results/BingImageClient/00000008.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000009.jpeg


BIN
python/imagedl/daily_test_results/BingImageClient/00000010.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000003.png


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000004.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000005.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000007.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000008.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000009.jpeg


BIN
python/imagedl/daily_test_results/DanbooruImageClient/00000010.jpeg


BIN
python/imagedl/daily_test_results/DimTownImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/DimTownImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/DimTownImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000002.png


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000003.webp


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000004.jpg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000005.webp


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000007.jpeg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000008.jpg


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000009.png


BIN
python/imagedl/daily_test_results/DuckduckgoImageClient/00000010.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000004.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000005.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000007.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000008.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000009.jpeg


BIN
python/imagedl/daily_test_results/EverypixelImageClient/00000010.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000004.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000005.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000006.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000007.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000008.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000009.jpeg


BIN
python/imagedl/daily_test_results/FoodiesfeedImageClient/00000010.jpeg


BIN
python/imagedl/daily_test_results/FreeNatureStockImageClient/00000001.jpeg


BIN
python/imagedl/daily_test_results/FreeNatureStockImageClient/00000002.jpeg


BIN
python/imagedl/daily_test_results/FreeNatureStockImageClient/00000003.jpeg


BIN
python/imagedl/daily_test_results/FreeNatureStockImageClient/00000004.jpeg


BIN
python/imagedl/daily_test_results/FreeNatureStockImageClient/00000005.jpeg


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio