系列
Electron + Vue3开源跨平台壁纸工具实战(二)本地运行
Electron + Vue3开源跨平台壁纸工具实战(三)主进程
Electron + Vue3开源跨平台壁纸工具实战(四)主进程-数据管理(1)
Electron + Vue3开源跨平台壁纸工具实战(五)主进程-数据管理(2)
Electron + Vue3开源跨平台壁纸工具实战(六)子进程服务
Electron + Vue3开源跨平台壁纸工具实战(七)进程通信
Electron + Vue3开源跨平台壁纸工具实战(八)主进程-核心功能
Electron + Vue3开源跨平台壁纸工具实战(九)子进程服务(2)
Electron + Vue3开源跨平台壁纸工具实战(十)渲染进程
源码
省流点我进Github获取源码,欢迎fork、star、PR
数据管理
主进程中的数据管理统一在store目录下
- index.mjs: Store类,数据管理入口,用于实例化各个管理类,启动定时任务、子服务等
- DatabaseManager.mjs: 数据库管理类,用于建表、初始化数据库
- FileManager.mjs: 文件管理类,用于启动文件子服务、处理文件
- ApiManager.mjs: 资源接口管理类,用于加载应用自带资源API、用户自定义资源API
- ResourcesManager.mjs: 资源查询管理类,用于查询、操作资源数据
- SettingManager.mjs: 设置数据管理类,用于获取、更新设置数据
- TaskScheduler.mjs: 定时任务管理类,用于管理应用内各种定时任务的启停清理
- WallpaperManager.mjs: 壁纸管理类,用于壁纸切换、下载、清理
- WordsManager.mjs: 词库管理类,用于查询、处理分词
定时任务管理类
TaskScheduler主要功能:
- 定时任务调度管理,这里使用了简单的
setInterval,也可以考虑使用node-schedule这类更规范的定时任务库
// main/store/TaskScheduler.mjs
/**
* 任务调度器
* 负责管理所有定时任务
*/
export default class TaskScheduler {
// 单例实例
static _instance = null
// 获取单例实例
static getInstance(logger) {
if (!TaskScheduler._instance) {
TaskScheduler._instance = new TaskScheduler(logger)
}
return TaskScheduler._instance
}
constructor(logger) {
// 防止直接实例化
if (TaskScheduler._instance) {
return TaskScheduler._instance
}
this.logger = logger
// 初始化任务列表
this.tasks = {}
// 添加定时器管理
this.timers = {
autoSwitchWallpaper: null,
autoRefreshDirectory: null,
autoDownload: null,
autoClearDownloaded: null,
handleQuality: null,
checkPrivacyPassword: null,
handleWords: null,
monitorMemory: null
}
// 保存实例
TaskScheduler._instance = this
}
// 调度任务
scheduleTask(timerKey, interval, callback, initialDelay = 0) {
// 清除已存在的定时器
if (this.timers[timerKey]) {
clearInterval(this.timers[timerKey])
}
// 如果有初始延迟
if (initialDelay > 0) {
setTimeout(() => {
callback()
this.timers[timerKey] = setInterval(callback, interval)
}, initialDelay)
} else {
this.timers[timerKey] = setInterval(callback, interval)
}
}
// 清除定时器
clearTask(timerKey) {
if (this.timers[timerKey]) {
clearInterval(this.timers[timerKey])
this.timers[timerKey] = null
}
}
// 清除所有定时器
clearAllTasks() {
Object.keys(this.timers).forEach((key) => {
this.clearTask(key)
})
}
}
壁纸管理类
WallpaperManager主要功能:
- 切换壁纸,写入历史记录
- 设置壁纸,调用wallpaper设置壁纸
- 下载远程资源壁纸,支持通过定时任务执行搜索下载远程资源壁纸到本地
// main/store/WallpaperManager.mjs
import fs from 'fs'
import path from 'path'
import { setWallpaper } from 'wallpaper'
import axios from 'axios'
import { t } from '../../i18n/server.js'
import { isMac, handleTimeByUnit } from '../utils/utils.mjs'
export default class WallpaperManager {
// 单例实例
static _instance = null
// 获取单例实例
static getInstance(logger, dbManager, settingManager, fileManager, apiManager) {
if (!WallpaperManager._instance) {
WallpaperManager._instance = new WallpaperManager(
logger,
dbManager,
settingManager,
fileManager,
apiManager
)
}
return WallpaperManager._instance
}
constructor(logger, dbManager, settingManager, fileManager, apiManager) {
// 防止直接实例化
if (WallpaperManager._instance) {
return WallpaperManager._instance
}
this.logger = logger
this.dbManager = dbManager
this.db = dbManager.db
this.settingManager = settingManager
this.fileManager = fileManager
this.apiManager = apiManager
// 重置参数
this.resetParams()
WallpaperManager._instance = this
}
// 使用 settingManager 获取设置
get settingData() {
return this.settingManager.settingData
}
resetParams(keys = ['switchToPrevWallpaper', 'autoDownload']) {
keys = Array.isArray(keys) ? keys : keys ? [keys] : []
this.params = this.params || {}
if (keys.includes('switchToPrevWallpaper')) {
this.params.switchToPrevWallpaper = {
// 切换上一个壁纸时,默认索引为0
index: 0,
count: 0
}
}
if (keys.includes('autoDownload')) {
this.params.autoDownload = {
// 记录下载的页数,用于顺序下载时查找索引
startPage: 1,
pageSize: 10,
// 添加当前下载源索引
currentSourceIndex: 0
}
}
}
// 执行切换壁纸
async doSwitchToNextWallpaper() {
const {
wallpaperResource = 'resources',
filterKeywords,
orientation,
quality,
switchType,
sortField = 'created_at',
sortType = -1
} = this.settingData
const isResources = wallpaperResource === 'resources'
const isFavorites = wallpaperResource === 'favorites'
// 获取最近使用的壁纸ID列表
const recent_stmt = this.db.prepare(
`SELECT resourceId FROM fbw_history ORDER BY created_at DESC LIMIT 10`
)
const recent_results = recent_stmt.all()
const recentIds = recent_results.map((item) => item.resourceId)
const prevSourceId = recentIds[0]
const query_where = []
let query_where_str = ''
let query_params = []
let query_sql = ''
let query_stmt
if (!isFavorites && !isResources) {
query_where.push(`resourceName = ?`)
query_params.push(wallpaperResource)
}
if (orientation.length === 1) {
query_where.push(`isLandscape = ?`)
query_params.push(orientation[0])
}
if (filterKeywords) {
const keywords = `%${filterKeywords}%`
query_where.push(`(filePath LIKE ? OR title LIKE ? OR desc LIKE ?)`)
query_params.push(keywords, keywords, keywords)
}
if (quality.length) {
const placeholders = quality.map(() => '?').join(',')
query_where.push(`quality IN (${placeholders})`)
query_params.push(...quality)
}
if (query_where.length) {
query_where_str = `WHERE ${query_where.join(' AND ')}`
}
// 随机切换
if (switchType === 1) {
// 处理收藏夹查询
if (isFavorites) {
// 如果有最近使用记录,尝试排除这些记录
if (recentIds.length > 0) {
// 先检查排除最近记录后是否还有壁纸可用
const check_stmt = this.db.prepare(
`SELECT COUNT(*) AS count
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
r.id NOT IN (${recentIds.map(() => '?').join(',')})`
)
const check_params = [...query_params, ...recentIds]
const check_result = check_stmt.get(...check_params)
// 如果排除后还有壁纸,则从未使用的壁纸中随机选择
if (check_result && check_result.count > 0) {
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
r.id NOT IN (${recentIds.map(() => '?').join(',')})
ORDER BY RANDOM() LIMIT 1
`
query_params.push(...recentIds)
} else {
// 如果排除后没有壁纸了,则从所有符合条件的壁纸中随机选择
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
ORDER BY RANDOM() LIMIT 1
`
}
} else {
// 没有历史记录,直接随机选择
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
ORDER BY RANDOM() LIMIT 1
`
}
} else {
// 如果有最近使用记录,尝试排除这些记录
if (recentIds.length > 0) {
// 先检查排除最近记录后是否还有壁纸可用
const check_stmt = this.db.prepare(
`SELECT COUNT(*) AS count
FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
id NOT IN (${recentIds.map(() => '?').join(',')})`
)
const check_params = [...query_params, ...recentIds]
const check_result = check_stmt.get(...check_params)
// 如果排除后还有壁纸,则从未使用的壁纸中随机选择
if (check_result && check_result.count > 0) {
query_sql = `
SELECT * FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
id NOT IN (${recentIds.map(() => '?').join(',')})
ORDER BY RANDOM() LIMIT 1
`
query_params.push(...recentIds)
} else {
// 如果排除后没有壁纸了,则从所有符合条件的壁纸中随机选择
query_sql = `SELECT * FROM fbw_resources ${query_where_str} ORDER BY RANDOM() LIMIT 1`
}
} else {
// 没有历史记录,直接随机选择
query_sql = `SELECT * FROM fbw_resources ${query_where_str} ORDER BY RANDOM() LIMIT 1`
}
}
// 顺序切换
} else {
// 处理收藏夹查询
if (isFavorites) {
// 如果有上一次切换的ID,则从该ID之后开始查询
if (prevSourceId) {
// 修改为使用收藏表的created_at字段作为排序依据
const favSortField = 'created_at' // 使用收藏表的创建时间
// 直接查询上一个壁纸在收藏表中的创建时间
const index_stmt = this.db.prepare(
`SELECT f.${favSortField}
FROM fbw_favorites f
WHERE f.resourceId = ?`
)
const index_result = index_stmt.get(prevSourceId)
if (index_result && index_result[favSortField] !== undefined) {
// 检查是否有符合条件的下一个壁纸
const check_stmt = this.db.prepare(
`SELECT COUNT(*) AS count
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
f.${favSortField} ${sortType === -1 ? '<=' : '>='} ?`
)
const check_params = [...query_params, index_result[favSortField]]
const check_result = check_stmt.get(...check_params)
// 如果有符合条件的下一个壁纸,则查询下一个壁纸
if (check_result && check_result.count > 0) {
// 查询下一个壁纸,按照收藏时间排序
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id) AND
f.${favSortField} ${sortType === -1 ? '<=' : '>='} ?
ORDER BY f.${favSortField} ${sortType === -1 ? 'DESC' : 'ASC'}
LIMIT 1
`
// 与检查语句的参数相同
query_params = [...check_params]
} else {
// 如果没有下一个壁纸,则从头开始
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
ORDER BY f.${favSortField} ${sortType === -1 ? 'DESC' : 'ASC'}
LIMIT 1
`
}
} else {
// 如果获取不到上一个壁纸的排序字段值,则从头开始
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
ORDER BY f.created_at ${sortType === -1 ? 'DESC' : 'ASC'}
LIMIT 1
`
}
} else {
// 没有上一次切换的ID,从头开始,按收藏时间排序
query_sql = `
SELECT r.*
FROM fbw_resources r
JOIN fbw_favorites f ON r.id = f.resourceId
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
NOT EXISTS (SELECT 1 FROM fbw_privacy_space p WHERE p.resourceId = r.id)
ORDER BY f.created_at ${sortType === -1 ? 'DESC' : 'ASC'}
LIMIT 1
`
}
} else {
// 顺序切换时也排除最近10条历史记录,排序加id,循环切换
if (prevSourceId) {
// 获取上一个壁纸的排序字段值
const index_stmt = this.db.prepare(`SELECT ${sortField} FROM fbw_resources WHERE id = ?`)
const index_result = index_stmt.get(prevSourceId)
const sortFieldVal = index_result[sortField]
if (index_result && sortFieldVal !== undefined) {
// 排除最近10条历史记录
const excludeIds =
recentIds.length > 0 ? `id NOT IN (${recentIds.map(() => '?').join(',')})` : ''
const excludeParams = recentIds
// 查找下一个
const check_stmt = this.db.prepare(
`SELECT COUNT(*) AS count
FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
${excludeIds ? excludeIds + ' AND' : ''}
(${sortField} ${sortType === -1 ? '<' : '>'} ? OR (${sortField} = ? AND id > ?))`
)
const check_params = [
...query_params,
...excludeParams,
sortFieldVal,
sortFieldVal,
prevSourceId
]
const check_result = check_stmt.get(...check_params)
if (check_result && check_result.count > 0) {
// 查找下一个
query_sql = `
SELECT *
FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
${excludeIds ? excludeIds + ' AND' : ''}
(${sortField} ${sortType === -1 ? '<' : '>'} ? OR (${sortField} = ? AND id > ?))
ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
LIMIT 1
`
query_params = [
...query_params,
...excludeParams,
sortFieldVal,
sortFieldVal,
prevSourceId
]
} else {
// 循环到头部,排除最近10条
query_sql = `
SELECT *
FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
${excludeIds}
ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
LIMIT 1
`
query_params = [...query_params, ...excludeParams]
}
} else {
// 如果获取不到上一个壁纸的排序字段值,则从头开始
query_sql = `
SELECT *
FROM fbw_resources
${query_where_str ? query_where_str + ' AND' : 'WHERE'}
ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
LIMIT 1
`
}
} else {
// 没有上一次切换的ID,从头开始
query_sql = `
SELECT *
FROM fbw_resources
${query_where_str}
ORDER BY ${sortField} ${sortType === -1 ? 'DESC' : 'ASC'}, id
LIMIT 1
`
}
}
}
// 执行查询
if (query_sql) {
query_stmt = this.db.prepare(query_sql)
const query_result = query_stmt.get(...query_params)
if (query_result && query_result.id !== prevSourceId) {
return await this.setAsWallpaper(query_result, true, true)
}
}
return {
success: false,
msg: t('messages.operationFail')
}
}
// 切换到上一个壁纸
async doSwitchToPrevWallpaper() {
const { index } = this.params.switchToPrevWallpaper
// 查询历史记录总数
const count_stmt = this.db.prepare(`SELECT COUNT(*) AS total FROM fbw_history`)
const count_result = count_stmt.get()
this.params.switchToPrevWallpaper.count =
count_result && count_result.total ? count_result.total : 0
// 支持循环切换
if (this.params.switchToPrevWallpaper.count) {
const nextIndex = index + 1 < this.params.switchToPrevWallpaper.count ? index + 1 : 0
// 查询历史记录
const query_stmt = this.db.prepare(
`SELECT h.id as hid, r.* FROM fbw_history h LEFT JOIN fbw_resources r ON h.resourceId = r.id ORDER BY h.id DESC LIMIT ? OFFSET ?`
)
const query_result = query_stmt.get(1, nextIndex)
if (query_result) {
// 更新索引
this.params.switchToPrevWallpaper.index = nextIndex
return await this.setAsWallpaper(query_result, false, false)
}
}
return {
success: false,
msg: t('messages.operationFail')
}
}
// 设置为壁纸
async setAsWallpaper(item, isAddToHistory = false, isResetParams = false) {
if (!item || !item.filePath || !fs.existsSync(item.filePath)) {
return {
success: false,
msg: t('messages.fileNotExist')
}
}
try {
// 设置壁纸
await setWallpaper(item.filePath, {
screen: this.settingData.allScreen && isMac() ? 'all' : 'main',
scale: this.settingData.scaleType
})
// 记录到历史记录
if (isAddToHistory) {
const insert_stmt = this.db.prepare(`INSERT INTO fbw_history (resourceId) VALUES (?)`)
insert_stmt.run(item.id)
}
// 重置参数
if (isResetParams) {
this.resetParams('switchToPrevWallpaper')
}
return {
success: true,
msg: t('messages.setWallpaperSuccess')
}
} catch (err) {
this.logger.error(`设置壁纸失败: error => ${err}`)
return {
success: false,
msg: t('messages.setWallpaperFail')
}
}
}
// 设置静态壁纸
async setStaticWallpaper(imgPath) {
if (!imgPath || !fs.existsSync(imgPath)) {
return {
success: false,
msg: t('messages.fileNotExist')
}
}
try {
// 设置壁纸
await setWallpaper(imgPath, {
screen: this.settingData.allScreen && isMac() ? 'all' : 'main',
scale: this.settingData.scaleType
})
return {
success: true,
msg: t('messages.setWallpaperSuccess')
}
} catch (err) {
this.logger.error(`设置壁纸失败: error => ${err}`)
return {
success: false,
msg: t('messages.setWallpaperFail')
}
}
}
// 下载并设置为壁纸
async setAsWallpaperWithDownload(item) {
if (!item) {
return {
success: false,
msg: t('messages.paramsError')
}
}
try {
// 处理不同的 srcType
if (item.srcType === 'file' && item.filePath) {
// 处理本地文件
const filePath = item.filePath
const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE filePath = ?`)
const query_result = query_stmt.get(filePath)
if (query_result) {
return await this.setAsWallpaper(query_result, true, true)
} else {
return {
success: false,
msg: t('messages.fileNotExist')
}
}
} else if (item.srcType === 'url' && item.url) {
// 下载壁纸
const { downloadFolder } = this.settingData
if (!downloadFolder || !fs.existsSync(downloadFolder)) {
return {
success: false,
msg: t('messages.downloadFolderNotExistOrNotSet')
}
}
// 确保下载目录存在
if (!fs.existsSync(downloadFolder)) {
fs.mkdirSync(downloadFolder, { recursive: true })
}
// 生成文件名
const fileName = `${item.fileName}.${item.fileExt}`
const filePath = path.join(downloadFolder, fileName)
if (fs.existsSync(filePath)) {
// 文件已存在,取消写入
this.logger.warn(`文件 ${filePath} 已存在,跳过写入`)
} else {
// 下载文件
const response = await axios({
method: 'GET',
url: item.url,
responseType: 'stream'
})
const writer = fs.createWriteStream(filePath)
response.data.pipe(writer)
await new Promise((resolve, reject) => {
writer.on('finish', resolve)
writer.on('error', reject)
})
}
// 获取文件信息
const stats = fs.statSync(filePath)
// 插入到数据库
try {
const insert_stmt = this.db.prepare(
`INSERT INTO fbw_resources
(resourceName, fileName, filePath, fileExt, fileSize, url, author, link, title, desc, quality, width, height, isLandscape, atimeMs, mtimeMs, ctimeMs) VALUES
(@resourceName, @fileName, @filePath, @fileExt, @fileSize, @url, @author, @link, @title, @desc, @quality, @width, @height, @isLandscape, @atimeMs, @mtimeMs, @ctimeMs)`
)
const insert_result = insert_stmt.run({
resourceName: item.resourceName,
fileName: item.fileName,
filePath: filePath,
fileExt: item.fileExt,
fileSize: stats.size,
url: item.url,
author: item.author || '',
link: item.link || '',
title: item.title || '',
desc: item.desc || '',
quality: item.quality || '',
width: item.width || 0,
height: item.height || 0,
isLandscape: item.isLandscape,
atimeMs: stats.atimeMs,
mtimeMs: stats.mtimeMs,
ctimeMs: stats.ctimeMs
})
if (insert_result.changes > 0) {
// 查询插入的记录
const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE id = ?`)
const query_result = query_stmt.get(insert_result.lastInsertRowid)
if (query_result) {
// 设置为壁纸
return await this.setAsWallpaper(query_result, true, true)
}
}
} catch (err) {
// 处理唯一键冲突
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
this.logger.info(`文件路径已存在,尝试查询现有记录: ${filePath}`)
// 查询已存在的记录
const query_stmt = this.db.prepare(`SELECT * FROM fbw_resources WHERE filePath = ?`)
const query_result = query_stmt.get(filePath)
if (query_result) {
// 设置为壁纸
return await this.setAsWallpaper(query_result, true, true)
}
} else {
throw err // 重新抛出非唯一键约束的错误
}
}
}
return {
success: false,
msg: t('messages.setWallpaperFail')
}
} catch (err) {
this.logger.error(`下载壁纸失败: error => ${err}`)
return {
success: false,
msg: t('messages.setWallpaperFail')
}
}
}
// 搜索并下载壁纸
async searchWallpaperWithDownload(params) {
const { resourceName, keywords, orientation, startPage, pageSize } = params
const { downloadFolder, remoteResourceSecretKeys } = this.settingData
let ret = {
success: false,
msg: t('messages.operationFail')
}
if (!downloadFolder || !fs.existsSync(downloadFolder)) {
ret.msg = t('messages.downloadFolderNotExistOrNotSet')
return ret
}
if (!keywords) {
ret.msg = t('messages.enterKeywords')
return ret
}
// 先获取资源数据
const resourceMapRes = await this.dbManager.getResourceMap()
if (!resourceMapRes.success) {
ret.msg = resourceMapRes.msg
return ret
}
const resourceMap = resourceMapRes.data
if (
resourceMap.remoteResourceKeyNames.includes(resourceName) &&
!remoteResourceSecretKeys[resourceName]
) {
ret.msg = t('messages.resourceSecretKeyUnset')
return ret
}
try {
// 确保下载目录存在
if (!fs.existsSync(downloadFolder)) {
fs.mkdirSync(downloadFolder, { recursive: true })
}
const res = await this.apiManager.call(resourceName, 'search', {
keywords,
orientation,
startPage,
pageSize,
secretKey: resourceMap.remoteResourceKeyNames.includes(resourceName)
? remoteResourceSecretKeys[resourceName]
: ''
})
if (res) {
if (res.list.length) {
const docs = []
const inserted_ids = []
const duplicate_filePaths = []
// 存储到本地
for (let i = 0; i < res.list.length; i++) {
const item = res.list[i]
const filePath = `${downloadFolder}/${item.fileName}.${item.fileExt}`
try {
if (fs.existsSync(filePath)) {
// 文件已存在,取消写入
this.logger.warn(`文件 ${filePath} 已存在,跳过写入`)
} else {
// 方式一:同步写入
const fileRes = await axios.get(item.url, { responseType: 'arraybuffer' })
fs.writeFileSync(filePath, fileRes.data)
}
const stats = fs.statSync(filePath)
docs.push({
...item,
filePath,
fileSize: stats.size,
atimeMs: stats.atimeMs,
mtimeMs: stats.mtimeMs,
ctimeMs: stats.ctimeMs
})
} catch (err) {
this.logger.error(`searchWallpaperWithDownload writeFileSync ERROR:: ${err}`)
}
}
if (docs.length) {
try {
const insert_stmt = this.db.prepare(
`INSERT OR IGNORE INTO fbw_resources
(resourceName, fileName, filePath, fileExt, fileSize, url, author, link, title, desc, quality, width, height, isLandscape, atimeMs, mtimeMs, ctimeMs) VALUES
(@resourceName, @fileName, @filePath, @fileExt, @fileSize, @url, @author, @link, @title, @desc, @quality, @width, @height, @isLandscape, @atimeMs, @mtimeMs, @ctimeMs)`
)
const transaction = this.db.transaction((docs) => {
for (let i = 0; i < docs.length; i++) {
const item = docs[i]
try {
const insert_result = insert_stmt.run({
resourceName: item.resourceName,
fileName: item.fileName,
filePath: item.filePath,
fileExt: item.fileExt,
fileSize: item.fileSize,
url: item.url,
author: item.author,
link: item.link,
title: item.title,
desc: item.desc,
quality: item.quality,
width: item.width,
height: item.height,
isLandscape: item.isLandscape,
atimeMs: item.atimeMs,
mtimeMs: item.mtimeMs,
ctimeMs: item.ctimeMs
})
const lastInsertedId = insert_result.lastInsertRowid
if (lastInsertedId) {
inserted_ids.push(lastInsertedId)
}
} catch (err) {
if (err.message.includes('UNIQUE constraint failed')) {
// 处理唯一约束失败错误
this.logger.warn(`跳过重复数据: ${item.filePath}`)
duplicate_filePaths.push(item.filePath)
} else {
throw err // 抛出其他类型的错误
}
}
}
})
transaction(docs)
} catch (err) {
this.logger.error(`searchWallpaperWithDownload insert ERROR:: ${err}`)
}
}
}
ret.success = true
ret.msg = t(res.list.length ? 'messages.querySuccess' : 'messages.queryEmpty')
}
return ret
} catch (err) {
this.logger.error(`搜索并下载壁纸失败: error => ${err}`)
return ret
}
}
// 下载壁纸
async downloadWallpaper() {
const { downloadSources, downloadKeywords, downloadOrientation, downloadFolder, autoDownload } =
this.settingData
if (
!autoDownload ||
!downloadSources ||
!downloadSources.length ||
!downloadKeywords ||
!downloadFolder
) {
return false
}
try {
// 确保下载目录存在
if (!fs.existsSync(downloadFolder)) {
fs.mkdirSync(downloadFolder, { recursive: true })
}
// 获取当前要使用的下载源
// 如果是数组,则按顺序轮流使用每个下载源
const currentSourceIndex =
(this.params.autoDownload.currentSourceIndex || 0) % downloadSources.length
const currentSource = downloadSources[currentSourceIndex]
const { startPage, pageSize } = this.params.autoDownload
const res = await this.searchWallpaperWithDownload({
resourceName: currentSource,
keywords: downloadKeywords,
orientation: downloadOrientation,
startPage,
pageSize
})
// 查询有结果时向下翻页,否则切换到下一个下载源
if (res.success) {
this.params.autoDownload.startPage = startPage + 1
return true
} else {
// 切换到下一个下载源,并重置页码
this.params.autoDownload.currentSourceIndex =
(currentSourceIndex + 1) % downloadSources.length
this.params.autoDownload.startPage = 1
return false
}
} catch (err) {
this.logger.error(`下载壁纸失败: error => ${err}`)
return false
}
}
// 清理所有下载的壁纸
async clearDownloadedAll() {
let ret = {
success: false,
msg: t('messages.operationFail')
}
try {
// 直接从数据库中查询所有非local的资源
const query_stmt = this.db.prepare(
`SELECT * FROM fbw_resources WHERE resourceName != 'local'`
)
const query_result = query_stmt.all()
let allCount = 0
let successCount = 0
let failCount = 0
if (Array.isArray(query_result) && query_result.length) {
allCount = query_result.length
for (let i = 0; i < query_result.length; i++) {
try {
const item = query_result[i]
const res = await this.fileManager.deleteFile(item)
if (res.success) {
successCount++
} else {
failCount++
}
} catch (err) {
this.logger.error(`处理文件时出错: ${err}`)
failCount++
}
}
}
this.logger.info(`清理文件完成,共 ${allCount},成功 ${successCount},失败 ${failCount}!`)
ret.success = true
ret.msg = t('messages.clearDownloadedDone', {
allCount,
successCount,
failCount
})
} catch (err) {
this.logger.error(`清理文件失败: ${err}`)
}
return ret
}
// 清理过期下载的壁纸
async clearDownloadedExpired() {
let ret = {
success: false,
msg: t('messages.operationFail')
}
try {
// 获取过期时间
const { clearDownloadedExpiredTime, clearDownloadedExpiredUnit } = this.settingData
const expiredTimeMs = handleTimeByUnit(clearDownloadedExpiredTime, clearDownloadedExpiredUnit)
const expiredTimestamp = Date.now() - expiredTimeMs
// 将时间戳转换为 SQLite 日期时间格式
const expiredDate = new Date(expiredTimestamp).toISOString()
// 直接从数据库中查询过期的非local资源
const query_stmt = this.db.prepare(
`SELECT * FROM fbw_resources WHERE resourceName != 'local' AND created_at < ?`
)
const query_result = query_stmt.all(expiredDate)
let allCount = 0
let successCount = 0
let failCount = 0
if (Array.isArray(query_result) && query_result.length) {
allCount = query_result.length
for (let i = 0; i < query_result.length; i++) {
try {
const item = query_result[i]
const res = await this.fileManager.deleteFile(item)
if (res.success) {
successCount++
} else {
failCount++
}
} catch (err) {
this.logger.error(`处理文件时出错: ${err}`)
failCount++
}
}
}
this.logger.info(
`清理过期文件完成,共 ${allCount},成功 ${successCount},失败 ${failCount}!`
)
ret.success = true
ret.msg = t('messages.clearDownloadedDone', {
allCount,
successCount,
failCount
})
} catch (err) {
this.logger.error(`清理过期文件失败: ${err}`)
}
return ret
}
}
词库管理类
WordsManager主要功能:
- 通过@node-rs/jieba对本地资源、远程资源的
fileName、title、desc进行分词记录
// main/store/WordsManager.mjs
import * as jieba from '@node-rs/jieba'
import { t } from '../../i18n/server.js'
export default class WordsManager {
// 单例实例
static _instance = null
// 获取单例实例
static getInstance(logger, dbManager, settingManager) {
if (!WordsManager._instance) {
WordsManager._instance = new WordsManager(logger, dbManager, settingManager)
}
return WordsManager._instance
}
constructor(logger, dbManager, settingManager) {
// 防止直接实例化
if (WordsManager._instance) {
return WordsManager._instance
}
this.logger = logger
this.dbManager = dbManager
this.db = dbManager.db
this.settingManager = settingManager
// 重置参数
this.resetParams()
// 保存实例
WordsManager._instance = this
}
// 使用 settingManager 获取设置
get settingData() {
return this.settingManager.settingData
}
resetParams() {
this.params = {
handleWords: {
startPage: 1,
pageSize: 20
}
}
}
/**
* 定时处理词库
* @param {Object} locks - 锁对象
*/
intervalHandleWords(locks) {
if (locks.handleWords) {
return
}
locks.handleWords = true
const { startPage, pageSize } = this.params.handleWords
// 查询未处理分词的图片
const query_stmt = this.db.prepare(
`SELECT r.id, r.title, r.desc, r.fileName
FROM fbw_resources r
WHERE
NOT EXISTS (SELECT 1 FROM fbw_resource_words rw WHERE rw.resourceId = r.id)
LIMIT ? OFFSET ?`
)
const query_result = query_stmt.all(pageSize, (startPage - 1) * pageSize)
if (Array.isArray(query_result) && query_result.length) {
// 如果没有更多数据,重置处理词库逻辑参数
if (query_result.length < pageSize) {
this.resetParams()
} else {
this.params.handleWords.startPage += 1
}
// 处理分词
this.handleWords(query_result)
locks.handleWords = false
} else {
locks.handleWords = false
this.resetParams()
}
}
/**
* 处理分词
* @param {Array} list - 资源列表
*/
handleWords(list) {
if (!Array.isArray(list) || !list.length) {
return
}
try {
// 插入分词
const insert_stmt = this.db.prepare(
`INSERT OR IGNORE INTO fbw_words (word, count, type) VALUES (?, ?, ?)`
)
// 更新分词计数
const update_word_stmt = this.db.prepare(
`UPDATE fbw_words SET count = count + 1 WHERE word = ?`
)
// 获取分词ID
const get_word_id_stmt = this.db.prepare(`SELECT id FROM fbw_words WHERE word = ?`)
// 插入资源与分词的关联
const insert_resource_word_stmt = this.db.prepare(
`INSERT OR IGNORE INTO fbw_resource_words (resourceId, wordId) VALUES (?, ?)`
)
const transaction = this.db.transaction(() => {
for (let i = 0; i < list.length; i++) {
const item = list[i]
// 处理标题和描述
const content = item.title ? `${item.title} ${item.desc}`.trim() : item.fileName
if (!content) continue
// 简单分词处理
const words = this.cutWords(content)
// 插入分词
for (const word of words) {
if (!word.trim()) continue
// 匹配中文、英文
let chinesePattern = /[\u4e00-\u9fa5]/
let englishPattern = /[a-zA-Z]/
// 类型 1:中文 2:英文
let type = 0
if (chinesePattern.test(word)) {
type = 1
} else if (englishPattern.test(word)) {
type = 2
}
// 先尝试插入
const insert_result = insert_stmt.run(word, 1, type)
// 如果插入失败,说明已存在,更新计数
if (!insert_result.changes) {
update_word_stmt.run(word)
}
// 获取分词ID
const wordRecord = get_word_id_stmt.get(word)
if (wordRecord && wordRecord.id) {
// 插入资源与分词的关联
insert_resource_word_stmt.run(item.id, wordRecord.id)
}
}
}
})
// 执行事务
transaction()
this.logger.info(`处理分词成功: count => ${list.length}`)
} catch (err) {
this.logger.error(`处理分词失败: error => ${err}`)
}
}
/**
* 处理删除资源时的分词计数更新
* @param {Object} resource - 被删除的资源
*/
handleDeletedResource(resource) {
if (!resource) {
return
}
try {
// 处理标题和描述
const content = resource.title
? `${resource.title} ${resource.desc}`.trim()
: resource.fileName
if (!content) return
// 获取分词
const words = this.cutWords(content)
// 更新分词计数
const update_word_stmt = this.db.prepare(
`UPDATE fbw_words SET count = count - 1 WHERE word = ?`
)
// 删除计数为0的分词
const delete_word_stmt = this.db.prepare(
`DELETE FROM fbw_words WHERE word = ? AND count <= 0`
)
// 删除资源与分词的关联
const delete_resource_word_stmt = this.db.prepare(
`DELETE FROM fbw_resource_words WHERE resourceId = ?`
)
const transaction = this.db.transaction(() => {
// 删除资源与分词的关联
delete_resource_word_stmt.run(resource.id)
for (const word of words) {
if (!word.trim()) continue
// 减少计数
update_word_stmt.run(word)
// 删除计数为0的分词
delete_word_stmt.run(word)
}
})
// 执行事务
transaction()
this.logger.info(
`更新删除资源的分词计数成功: 资源ID => ${resource.id},分词 => ${words.join(',')}`
)
} catch (err) {
this.logger.error(`更新删除资源的分词计数失败: error => ${err}`)
}
}
/**
* 分词处理
* @param {string} content - 内容
* @returns {Array} 分词结果
*/
cutWords(content) {
if (!content) return []
// 方式一:简单分词:按空格、标点符号分割
// return content
// .replace(/[^\w\s\u4e00-\u9fa5]/g, ' ') // 替换非字母、数字、中文、空格为空格
// .split(/\s+/) // 按空格分割
// .filter((word) => word.length > 1) // 过滤掉长度为1的词
// 方式二:结巴分词
return jieba.cutForSearch(content, true)
}
/**
* 获取词库
* @param {Object} params - 查询参数
* @returns {Object} 查询结果
*/
getWords(params = {}) {
const { types = [], size = 100 } = params
let ret = {
success: false,
msg: t('messages.operationFail'),
data: null
}
try {
if (types.length) {
const data = {}
for (let i = 0; i < types.length; i++) {
const type = types[i]
const query_stmt = this.db.prepare(
`SELECT word, count, type FROM fbw_words WHERE type = ? ORDER BY count DESC LIMIT ?`
)
const query_result = query_stmt.all(type, size)
if (query_result && query_result.length) {
data[type] = query_result
} else {
data[type] = []
}
}
ret = {
success: true,
msg: t(Object.keys(data).length ? 'messages.querySuccess' : 'messages.queryEmpty'),
data
}
} else {
const query_stmt = this.db.prepare(
`SELECT word, count, type FROM fbw_words ORDER BY count DESC LIMIT ?`
)
const query_result = query_stmt.all(size)
if (Array.isArray(query_result)) {
ret = {
success: true,
msg: t(query_result.length ? 'messages.querySuccess' : 'messages.queryEmpty'),
data: query_result
}
}
}
} catch (err) {
this.logger.error(`获取词库失败: error => ${err}`)
}
return ret
}
}