feat(core): 实现扫描并删除iPhone导出相册中的实况视频功能

- 新增文件扫描逻辑,遍历目录下所有文件并记录路径与元信息
- 新增核心处理逻辑,识别.HEIC图片及其对应的.MOV实况视频
- 自动将匹配的实况视频移动至 duplicates 目录以供用户确认删除
- 添加构建脚本build.sh用于编译和压缩可执行文件
- 更新.gitignore忽略.idea目录
- 重写README.md,明确工具用途、需求及效果预览
- 初始化模块依赖管理(go.mod, go.sum)及相关工具类和变量定义
This commit is contained in:
2025-12-20 10:01:08 +08:00
parent e7374097bc
commit 1364404123
14 changed files with 1260 additions and 69 deletions
+1
View File
@@ -13,3 +13,4 @@
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
.idea
-36
View File
@@ -1,36 +0,0 @@
# kenaito-iphone-export-photo-remove-duplicates
#### Description
IPhone导出的相册中存在图片和实况视频,这个工具用来删除图片对应的实况视频,从而减小占用空间。
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+8 -33
View File
@@ -1,37 +1,12 @@
# kenaito-iphone-export-photo-remove-duplicates ## IPhone导出相册删除实况工具
#### 介绍 #### 需求
IPhone导出的相册中存在图片和实况视频,这个工具用来删除图片对应的实况视频,从而减小占用空间。
#### 软件架构 - 扫描某个目录
软件架构说明 - 找到与照片对应的实况视频
- 删除实况视频
#### 效果预览
![1](./doc/QQ20251220-095717.png)
#### 安装教程 ![1](./doc/QQ20251220-095800.png)
1. xxxx
2. xxxx
3. xxxx
#### 使用说明
1. xxxx
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
+7
View File
@@ -0,0 +1,7 @@
rm -f kenaito-media-group-tool.exe
rm -f kenaito-media-group-tool-release.exe
# build
go build -o kenaito-media-group-tool.exe main.go
# upx compress
./upx -o kenaito-media-group-tool-release.exe kenaito-media-group-tool.exe
rm -f kenaito-media-group-tool.exe
+41
View File
@@ -0,0 +1,41 @@
package core
import (
"fmt"
"kenaito-media-group-tool/util"
"kenaito-media-group-tool/vars"
"log"
"os"
"path"
"path/filepath"
)
type FileScanner struct {
}
func (FileScanner) DoScan(rootDir string) {
fmt.Printf("=== 开始扫描文件 \n")
if err := filepath.Walk(rootDir, visitFile); err != nil {
log.Fatal(err)
}
}
// 定义walkFn回调函数visit
func visitFile(currentPath string, info os.FileInfo, err error) error {
if err != nil {
return err // 如果有错误,直接返回
}
if !info.IsDir() {
ext := path.Ext(currentPath)
if ".exe" != ext {
vars.GlobalFilePathList = append(vars.GlobalFilePathList, currentPath)
// filename, include ext
vars.GlobalFilePath2FileNameExtMap[currentPath] = filepath.Base(currentPath)
// filename, exclude ext
vars.GlobalFilePath2FileNameMap[currentPath] = util.GetFileNameWithoutExt(currentPath)
// file ext
vars.GlobalFilePath2FileExtMap[currentPath] = ext
}
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package core
import (
"fmt"
"github.com/redmask-hb/GoSimplePrint/goPrint"
uuid "github.com/satori/go.uuid"
"kenaito-media-group-tool/util"
"kenaito-media-group-tool/vars"
"os"
"strings"
)
func DoDeleteLiveVideo(rootDir string) {
pathSep := string(os.PathSeparator)
uid := strings.ReplaceAll(uuid.NewV4().String(), "-", "")
total := len(vars.GlobalFilePathList)
fmt.Printf("=== 文件总数: %d \n", total)
for _, filePath := range vars.GlobalFilePathList {
fileExt := vars.GlobalFilePath2FileExtMap[filePath]
if ".HEIC" == fileExt {
vars.GlobalHEICFilePathNameMap[filePath] = vars.GlobalFilePath2FileNameMap[filePath]
continue
}
if ".MOV" == fileExt {
vars.GlobalMOVFilePathPathNameMap[filePath] = vars.GlobalFilePath2FileNameMap[filePath]
}
}
var movFilePath []string
for _, heicName := range vars.GlobalHEICFilePathNameMap {
for movPath, movName := range vars.GlobalMOVFilePathPathNameMap {
if heicName == movName {
println(movPath)
movFilePath = append(movFilePath, movPath)
}
}
}
fmt.Printf("=== 发现实况视频文件数量: %d\n", len(movFilePath))
duplicatesDir := rootDir + pathSep + "duplicates-" + uid
util.CreateDir(duplicatesDir)
fmt.Printf("=== 开始移动实况视频文件")
doMoveFileToDir(movFilePath, duplicatesDir)
fmt.Printf("=== 实况视频已移动到: %v 中\n", duplicatesDir)
}
// 批量移动文件到目录
func doMoveFileToDir(filePathList []string, videoDirPath string) {
total := len(filePathList)
var count = 0
bar := goPrint.NewBar(100)
bar.SetNotice("=== 移动文件到目录:")
bar.SetGraph(">")
pathSeparator := string(os.PathSeparator)
for _, videoFilePath := range filePathList {
moveFileToDir(videoFilePath, videoDirPath+pathSeparator)
count = count + 1
bar.PrintBar(util.CalcPercentage(count, total))
}
bar.PrintEnd("=== Finish")
}
// 移动文件到目录
func moveFileToDir(sourceFilePath string, targetDirectory string) bool {
splits := strings.Split(sourceFilePath, string(os.PathSeparator))
fileName := splits[len(splits)-1]
targetFilePath := targetDirectory + fileName
err := os.Rename(sourceFilePath, targetFilePath)
//fmt.Printf("=== 移动文件, 源: %s, 目标: %s \n", sourceFilePath, targetFilePath)
return err == nil
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+15
View File
@@ -0,0 +1,15 @@
module kenaito-media-group-tool
go 1.21
require (
github.com/gabriel-vasile/mimetype v1.4.3
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d
github.com/satori/go.uuid v1.2.0
golang.org/x/image v0.16.0
)
require (
golang.org/x/net v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
+13
View File
@@ -0,0 +1,13 @@
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d h1:h/hohIqMUCML2Rp9BXXAu0I3ZR68d7eqMHLPNq7N2tg=
github.com/redmask-hb/GoSimplePrint v0.0.0-20210302075413-3a3af92bcb7d/go.mod h1:LiYo3EFlYfk46Re4zgQysMo6yO3/kAXslB2fyMdl+uw=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+24
View File
@@ -0,0 +1,24 @@
package main
import (
_ "embed"
"fmt"
"kenaito-media-group-tool/core"
"os"
"time"
)
func main() {
fmt.Println("=== 警告:该程序会改变目录结构,将在5秒后开始执行")
time.Sleep(time.Second * 5)
rootDir, err := os.Getwd()
if err != nil {
fmt.Println("=== 获取当前路径异常", err)
return
}
scanner := core.FileScanner{}
scanner.DoScan(rootDir)
core.DoDeleteLiveVideo(rootDir)
fmt.Println("=== 3s后自动退出")
time.Sleep(time.Second * 3)
}
+1042
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
package util
import (
"os"
"path/filepath"
)
// CreateDir 创建目录
func CreateDir(dirPath string) bool {
err := os.Mkdir(dirPath, 0755)
return err == nil
}
// CalcPercentage 计算percentage相对于total的百分比
func CalcPercentage(percentage int, total int) int {
return int(float64(percentage) / float64(total) * 100)
}
func GetFileNameWithoutExt(path string) string {
// 获取文件名(包含后缀)
filename := filepath.Base(path)
// 去除后缀
ext := filepath.Ext(filename)
name := filename[:len(filename)-len(ext)]
return name
}
+9
View File
@@ -0,0 +1,9 @@
package vars
var GlobalFilePathList []string // 所有文件的路径
var GlobalFilePath2FileNameExtMap = make(map[string]string) // 文件路径和文件名称(包含后缀)的映射关系
var GlobalFilePath2FileNameMap = make(map[string]string) // 文件路径和文件名称(不包含后缀)的映射关系
var GlobalFilePath2FileExtMap = make(map[string]string) // 文件路径和文件扩展名的映射关系
var GlobalHEICFilePathNameMap = make(map[string]string) // 文件路径和HEIC文件名称的映射关系
var GlobalMOVFilePathPathNameMap = make(map[string]string) // 文件路径和MOV文件名称的映射关系