Wallpaper中动态壁纸的转移
背景
Wallpaper是Steam上的一个优秀的动态壁纸软件,其创意工坊中存在大量制作精良的壁纸(懂得都懂😉),但是订阅下载后会占用本机大量空间,尽管可以通过Steam的转移功能转移,但是依旧还是不太方便,最近在折腾家庭NAS,就想将Wallpaper的壁纸文件转移到NAS上并取消订阅,释放本地空间。
本次使用Python3来编写转移脚本,平常我用Python用的少,也算是提升一下熟练度吧。
分析
首先,Wallpaper中的壁纸结构基本上都是一个文件夹中三个文件的形式,这三个文件分别是视频文件、预览图、json文件,同时文件夹命名是一串很长的数字,我怀疑这串数字是唯一对应各个壁纸的,估计是Wallpaper数据库里壁纸的编号。
所以讲道理直接拷贝这个文件夹中的文件也问题不大,但是这样就很难直观的知道这个文件夹内部是什么,最好能拷贝到NAS中时再把文件夹名字改掉。
其次,要写脚本一次性转移这么多文件,很有可能中途就中断了,最好增加一些检查机制防止重复拷贝(最后我的脚本检查机制可以说是拉满了)
总之,我要实现一个拷贝的脚本,1、需要可以拷贝的时候重命名文件夹。2、有一些检查机制保证不重复拷贝。
实现
实现工具Python、Sqlite
编写两个脚本分别实现不同功能:
- 转移脚本,基本功能:
- 收集Wallpaper中有哪些文件
- 检查这些文件和Sqlite中的对不对的上
- 检查这些文件在目标文件夹中有没有
- 转移文件
- 检查脚本,基本功能:
- 收集目标文件夹中有哪些文件
- 检查目标文件夹文件是否与Sqlite中对的上
- 如果传入参数–update就更新数据库文件使数据库记录与目标文件夹文件一致,否则就报差别在哪里
脚本内容
使用了通义千问辅助开发(有一说一,通义千问挺强)
只要搭建好数据库,改一下脚本里的地址,也挺通用的,反正我用的感觉还行
脚本中有很多输出和log记录,还有文件名的防错,管控还比较到位
一、转移脚本
# 用来转移下载好的Wallpaper内容的脚本
import os
import json
import sqlite3
import chardet
import datetime
import shutil
import pprint
def detectEncoding(filePath):
"""
检测给定文件的编码。
:param filePath: 文件路径
:return: 文件的编码类型
"""
# """检测文件的编码"""
with open(filePath, 'rb') as f:
result = chardet.detect(f.read())
return result['encoding']
def getCurrentWallpaper():
"""
获取当前的壁纸信息。
:return: 壁纸信息列表,每个元素包含标题、索引和原始目录路径
"""
objects = []
# 原始地址
sourceDir = "F:/SteamLibrary/steamapps/workshop/content/431960/"
dirs = os.listdir(sourceDir)
for dir in dirs:
if not os.path.isdir(sourceDir + dir):
continue
files = os.listdir(sourceDir + dir)
if "project.json" not in files:
print("project.json 不存在于 " + dir)
continue
# 检测文件编码
jsonPath = os.path.join(sourceDir, dir, "project.json")
encoding = detectEncoding(jsonPath)
try:
with open(jsonPath, 'r', encoding=encoding, errors='ignore') as file:
jsonInfo = json.load(file)
title = jsonInfo["title"]
object = {
"title": title,
"index": dir,
"origDir": os.path.join(sourceDir, dir)
}
objects.append(object)
except Exception as e:
print(f"Failed to read {jsonPath}: {e}")
continue
return objects
def checkSqlInfo(objects):
"""
检查数据库中是否已存在相同的壁纸信息,将新信息添加到数据库。
:param objects: 壁纸信息列表
:return: 新的壁纸信息列表,不包含已存在于数据库中的信息
"""
newObjects = []
errorMessages = []
sqlFile = "Y:/WallpaperDate/transRecord.db"
conn = sqlite3.connect(sqlFile)
cursor = conn.cursor()
for object in objects:
index = object["index"]
cursor.execute("SELECT COUNT(*) FROM currentFiles WHERE title=?", (object["title"],))
result = cursor.fetchone()
if result is None:
newObjects.append(object)
else:
status = result[0]
if status == 0:
newObjects.append(object)
else:
message = f"{object['index']} {object['title']} 数据库中已存在\n"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
errorMessages.append((message,curTime))
print(f"{message} {curTime}")
if errorMessages:
cursor.executemany(
"INSERT INTO record (message, time) VALUES (?, ?)",
errorMessages
)
conn.commit()
message = "checkSqlInfo报错信息更新完毕"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{message} {curTime}")
cursor.close()
conn.close()
return newObjects
def transferFiles(objects):
"""
将壁纸文件从原始目录转移到目标目录,并在数据库中记录相关信息。
:param objects: 壁纸信息列表
"""
errorMessages = []
modifys = []
transferMessages = []
sqlFile = "Y:/WallpaperDate/transRecord.db"
destBaseDir = "Y:/WallpaperDate"
conn = sqlite3.connect(sqlFile)
cursor = conn.cursor()
for object in objects:
origDir = object["origDir"]
index = object["index"]
title = object["title"]
# 定义一个集合,包含所有非法的文件名字符
illegalChars = {'<', '>', ':', '"', '/', '\\', '|', '?', '*'}
# 使用列表推导式来构建一个只包含合法字符的新字符串
safeTitle = "".join(c for c in title if c not in illegalChars)
destDir = os.path.join(destBaseDir, safeTitle + "_" + index)
if os.path.exists(destDir):
message = f"{index} {title} 文件夹已存在"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
errorMessages.append((message, curTime))
print(f"{message} {curTime}")
continue
shutil.copytree(origDir, destDir)
if os.path.exists(destDir):
message = f"{index} {title} 已完成拷贝"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
modifys.append((f"{message} {curTime}"))
transferMessages.append((index, title))
print(f"{message} {curTime}")
cursor.executemany(
"INSERT INTO record (message, time) VALUES (?, ?)",
errorMessages
)
conn.commit()
message = "transferFiles报错信息更新完毕"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{message} {curTime}")
cursor.executemany(
"INSERT INTO record (modify, time) VALUES (?, ?)",
modifys
)
conn.commit()
message = "transferFiles操作信息更新完毕"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{message} {curTime}")
cursor.executemany(
"INSERT INTO currentFiles ([index], title) VALUES (?, ?)",
transferMessages
)
conn.commit()
message = "transferFiles转入信息记录完毕"
curTime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"{message} {curTime}")
cursor.close()
conn.close()
def main():
"""
主函数,程序的入口点。
"""
objects = getCurrentWallpaper()
objects = checkSqlInfo(objects)
transferFiles(objects)
main()
二、检查脚本
import os
import sqlite3
import argparse
def readDatabaseRecords(sqlFile):
"""
从数据库中读取所有记录,并清洗标题。
:param sqlFile: 数据库文件路径
:return: 清洗后的数据库记录列表
"""
records = []
illegalChars = {'<', '>', ':', '"', '/', '\\', '|', '?', '*'}
with sqlite3.connect(sqlFile) as connection:
cursor = connection.cursor()
cursor.execute("SELECT [index], title FROM currentFiles")
rawRecords = cursor.fetchall()
for index, title in rawRecords:
# 使用列表推导式来构建一个只包含合法字符的新字符串
safeTitle = "".join(c for c in title if c not in illegalChars)
records.append((index, safeTitle))
return records
def scanFileSystem(baseDirectory):
"""
扫描文件系统,获取所有子目录信息。
:param baseDirectory: 文件系统根目录
:return: 文件系统中的目录信息列表
"""
directories = []
for entry in os.scandir(baseDirectory):
if entry.is_dir():
directoryName = entry.name
parts = directoryName.rsplit('_', 1) # 从右向左分割,保留最后一段作为索引
if len(parts) == 2:
directories.append({"index": parts[1], "title": parts[0]})
return directories
def compareRecords(databaseRecords, fileSystemDirectories):
"""
比较数据库记录与文件系统中的目录,找出不一致的地方。
:param databaseRecords: 数据库记录列表
:param fileSystemDirectories: 文件系统目录信息列表
:return: 缺失和多余的条目列表
"""
missingInDb = []
missingInFs = []
dbSet = {(record[0], record[1]) for record in databaseRecords}
fsSet = {(directory["index"], directory["title"]) for directory in fileSystemDirectories}
missingInFs = list(dbSet - fsSet)
missingInDb = list(fsSet - dbSet)
return missingInDb, missingInFs
def logDifferences(missingInDb, missingInFs, logFile):
"""
将不一致的地方记录到日志文件中。
:param missingInDb: 文件系统中有但在数据库中没有的条目列表
:param missingInFs: 数据库中有的但在文件系统中没有的条目列表
:param logFile: 日志文件路径
"""
with open(logFile, 'w',encoding='utf-8') as log:
log.write("Missing in database:\n")
for item in missingInDb:
log.write(f"{item[0]} {item[1]}\n")
log.write("\nMissing in filesystem:\n")
for item in missingInFs:
log.write(f"{item[0]} {item[1]}\n")
def updateDatabaseToMatchFileSystem(sqlFile, databaseRecords ,fileSystemDirectories):
"""
更新数据库中的记录,使其与文件系统中的目录信息相匹配。
包括更新现有记录和插入新记录。
:param sqlFile: 数据库文件路径
:param baseDirectory: 文件系统根目录
"""
# # 首先读取数据库中的记录
# databaseRecords = readDatabaseRecords(sqlFile)
# # 然后扫描文件系统,获取所有子目录信息
# fileSystemDirectories = scanFileSystem(baseDirectory)
# 创建数据库连接
with sqlite3.connect(sqlFile) as connection:
cursor = connection.cursor()
# 创建一个集合,存储数据库中已有的索引
existingIndices = set(record[0] for record in databaseRecords)
# 遍历文件系统中的目录,检查并更新/插入记录
for fsDirectory in fileSystemDirectories:
index, title = fsDirectory["index"], fsDirectory["title"]
if index not in existingIndices:
# 如果数据库中没有这个索引,则插入新记录
cursor.execute("INSERT INTO currentFiles ([index], title) VALUES (?, ?)", (index, title))
existingIndices.add(index)
else:
# 如果数据库中有这个索引,检查标题是否需要更新
for dbRecord in databaseRecords:
if dbRecord[0] == index and dbRecord[1] != title:
cursor.execute("UPDATE currentFiles SET title=? WHERE index=?", (title, index))
# 清理数据库中不再存在的记录
for index in existingIndices:
if not any(directory["index"] == index for directory in fileSystemDirectories):
cursor.execute("DELETE FROM currentFiles WHERE index=?", (index,))
connection.commit()
def main():
parser = argparse.ArgumentParser(description="Check and optionally update the consistency between database records and filesystem.")
parser.add_argument('--update', action='store_true', help="Update database records to match filesystem directories.")
args = parser.parse_args()
sqlFile = "Y:/WallpaperDate/transRecord.db"
baseDirectory = "Y:/WallpaperDate"
logFile = "Y:/WallpaperDate/consistencyCheck.log"
databaseRecords = readDatabaseRecords(sqlFile)
fileSystemDirectories = scanFileSystem(baseDirectory)
missingInDb, missingInFs = compareRecords(databaseRecords, fileSystemDirectories)
logDifferences(missingInDb, missingInFs, logFile)
# 如果指定了 --update 参数,则更新数据库
if args.update:
updateDatabaseToMatchFileSystem(sqlFile, databaseRecords ,fileSystemDirectories)
if __name__ == "__main__":
main()