从1天到365天,我用Go语言写了一个视频管理工具,结果治好了我的拖延症
- 科技
- 2026-07-03 12:26:31
- 311
你有没有过这种经历?打开手机相册,发现里面堆满了从“到“一年前”的各种视频碎片,有的是一时兴起拍的猫咪,有的是工作会议的录屏,还有那些说不上来为什么要拍的街景——它们就那么躺在那儿,像一堆没人整理的旧磁带。
我去年年底就陷入这种困境,手机提示我“存储空间不足”,我点开一看,好家伙,光视频就占了42GB,从1月1号到12月31号,整整365天的视频,横跨四个季节,内容从年夜饭到公司年会,从海边日出到深夜加班后的自拍,我盯着那些“VID_20230115_203415.mp4”之类的文件名,突然觉得——我需要让这些视频“说人话”。
用Go写工具,不是为了炫技
我选Go语言来搞这件事,不是因为它是“最流行的编程语言”,也不是因为我有多厉害,真实的原因是:我翻了一圈自己的开发环境,发现Go写起来最直白,编译快,打包成单文件扔服务器上就能跑,就这么简单,别想太多。
当时我的需求其实很明确:给我这一整年的视频文件夹建立索引,按天归类,自动提取拍摄时间,然后生成一个漂亮的“年度视频时间线”,我不想手动去翻每个文件的属性,也不想用那些在线工具——隐私是个大问题,你懂的。
第一步:让程序读懂“一天”
我刚写第一版代码的时候,思路其实很蠢,我想通过文件名里的时间戳来解析日期,比如VID_20230101_120000.mp4这种,看起来格式挺规整的,但很快就发现,不同设备、不同App导出的视频命名规则简直是“百家争鸣”。
有的叫IMG_3205.MOV,完全没日期信息,有的是从微信导出的,文件名是一串哈希值加时间戳,但格式跟标准的不一样,还有更坑的——用相机拍的视频,文件名是_DSC1234.mp4,你得去读EXIF数据才能拿到拍摄时间。
这时候Go语言的优势就出来了,它有成熟的第三方库,比如exif和mediainfo,能直接读取视频文件的元数据,我写了一段代码,先尝试从文件名解析,不行就去读元数据,再不行就用文件的修改时间做兜底。
// 这段代码跑起来的样子很朴素
filepath := "/Users/me/2023视频/IMG_3205.MOV"
modTime, err := os.Stat(filepath)
if err == nil {
fmt.Printf("修改时间:%v\n", modTime.ModTime())
}
但这里有个坑——文件修改时间不一定等于拍摄时间,你从电脑传到手机,再传到云盘再下载回来,修改时间早就变了,所以最靠谱的还是去读视频本身的元数据。
那段时间我写了一个小的命令行工具,叫video-dater,它能递归扫描整个文件夹,把所有视频的拍摄日期提取出来,然后按日期分组,比如你丢给它一个包含365天视频的文件夹,它就把这些文件按“2023-01-01”、“2023-01-02”……一直到“2023-12-31”排列好。
按“天”分组,是第一步
说个细节,我一开始让程序把同一天的视频放在同一个子文件夹里,比如所有2023年1月1日的视频,都丢进/2023-01-01/这个目录,这样看起来挺整洁的。
但我很快就发现一个问题——有些天的视频特别多,比如我生日那天,拍了大概60多个短视频,有些天又只有一两个,这就导致文件夹数量多到有点烦人,尤其是当你第二年也想这么做的时候。
所以我改成另一种方式:不移动文件,只生成一个索引文件(Markdown格式或者JSON),里面记录了每个视频的路径、拍摄日期、时长、甚至缩略图路径,这样原文件不动,但你能快速知道“那天拍了什么”。
这里用的一个trick是Go的filepath.Walk函数,它能递归遍历目录,再配合time包里的日期格式化,把time.Time对象转成YYYY-MM-DD字符串,简直就是天生一对。
从“一天”到“一周”:我发现了时间的节奏感
当我把视频按天分组之后,我盯着屏幕上的列表,突然意识到——每天的内容其实有很强的重复性,比如周一到周五,大多是办公室、键盘、会议记录,周末就变成美食、公园、宠物。
我开始在工具里加入“周视图”功能:给每个视频打一个“星期几”的标签,比如周一拍的视频,标记为“Monday”,然后生成一个按星期几分布的热力图,你猜怎么着?我发现自己周日拍的视频最多,周五最少,周五下班后根本不记得拍东西,只想躺着。
这时候Go的time.Weekday()方法就帮了大忙,返回的是一个time.Weekday类型,从Sunday到Saturday,我写了一个switch语句,把每天拍到的视频数量统计出来,然后生成一个简单的ASCII柱状图——虽然简陋,但自己看着挺有成就感。
月度和年度:从“清单”到“叙事”
整理到这儿,我觉得“按天”已经能满足基本需求了,但真正的目标是从“1天”到“365天”的完整叙事,我想看到这一年,视频内容是如何随着季节、节日、个人状态变化的。
于是我在工具里加入了“月度摘要”和“年度报告”两个功能,月度摘要就是统计你每个月拍了多少视频,总时长多少,年度报告则更夸张一点——它会找出你拍摄最频繁的那天、全年视频的总时长、最常出现的场景(通过地点元数据),甚至能根据视频文件的创建时间推测出你“早起拍视频”的频次。
举个具体的例子,我的年度报告显示:2023年总共有2,341个视频,总时长约45小时,拍摄最多的月份是8月(我去了一趟新疆),最少的月份是2月(在准备年终汇报),而最不会拍视频的日子是星期三——我也不知道为什么,可能星期三大家开会开得太累了。
这些数据我用Go的sort包和map结构来统计,比如统计每月的视频数量,就建一个map[time.Month]int,然后遍历所有视频,把月份对应的计数值加一。
用Go处理视频本身:稍微有点疼
光整理元数据还不够,我还想给每个视频生成缩略图,这样浏览的时候能快速回忆起来内容,而不用一个一个点开。
这一步涉及到视频解码,纯Go实现的话比较慢,我用了ffmpeg的命令行接口,通过Go的os/exec包调用外部命令,虽然不算“纯Go”,但效率高,而且ffmpeg本身足够稳定。
代码大概长这样:
cmd := exec.Command("ffmpeg", "-i", videoPath, "-ss", "00:00:01", "-vframes", "1", thumbnailPath)
err := cmd.Run()
取第一秒的画面作为缩略图,压缩成320x240的大小,这样处理完2,341个视频,大概用了20多分钟,还行,至少不耽误我下午喝咖啡。
这个过程中遇到一个小bug:有些视频的时长只有3秒,跳转到第一秒取帧没问题,但有个视频是纯黑屏(可能是手机放口袋的时候拍的),ffmpeg取到的就是一张全黑图,后来我加了一个判断,如果缩略图文件大小小于5KB,就判定为“无效缩略图”,改用视频中间帧再试一次。
可视化输出:让内容“说话”
当所有数据都整理好之后,我需要一个可视化的展示,我没有写前端页面(虽然Go也能写),而是生成了一个纯HTML文件,包含所有视频的缩略图网格、每天的标签、以及一个按时间线排列的导航。
这个HTML文件是用Go的html/template包生成的,我先定义了一个结构体:
type VideoEntry struct {
Date string
Thumbnail string
Path string
Duration float64
Description string // 如果有的话
}
然后把它塞进一个HTML模板里,模板里用{{range .}}来循环渲染,最终生成的文件小巧、干净,直接把文件路径拖进浏览器就能看。
这个模板我还加入了两个小特性:一个是按日期跳转的锚点链接,另一个是“随机播放”按钮——点击它就会随机跳转到一年中的某一天,这个按钮有点意外地好用,尤其是你无聊的时候,点一下,看到去年的某一天你正在做的事情,经常能勾起一些有趣的回忆。
后续思考:视频整理不是终点
到这一步,这个工具已经能帮我完成“从1天到365天视频”的整理工作了,但用着用着,我发现了更多有意思的场景。
我开始注意到自己视频内容的“季节性”,夏天户外视频多,冬天室内多,春天视频里有花粉、有风,秋天有落叶,不是刻意去记录,但回看的时候,能清晰感受到时间的流逝。
还有一次,我用这个工具帮朋友整理了他女儿一整年的成长视频,那个文件夹里有366个视频(因为2020年是闰年),从出生第一天到一岁生日,每个视频按日期排列好,生成了一份带缩略图的时间线,他看完眼眶有点红——这是我用代码写出的最满意的功能。
后来我也在想,是不是可以再加一个功能:自动把同一天的短视频拼接成一个电影片段,这样你有365个电影片段,组合起来就是这一年的浓缩版,技术上实现不难:用ffmpeg的concat协议,加上一个视频排序列表就行,但还没有动手写,因为光想想就觉得很花时间,也许明年吧。
一些你没预料到但值得提的小事
最后说点经验教训,我用Go写这个工具的过程中,踩过几个坑,也许对你也有些帮助:
-
不要一次性读入所有文件路径到内存,我那2,341个视频,路径字符串本身还好,但如果你的视频数量是几十万甚至上百万,内存占用会很可观,用
filepath.Walk是流式的,问题不大,但如果你自己写递归函数,要注意。 -
视频文件是二进制文件,不要用文本方式打开,这个坑我是在处理一些异常的MP4文件时碰到的,Go的
ioutil.ReadFile默认是读整个二进制数据,没问题,但如果你用os.Open然后加bufio.Reader,记得指定ReadBytes而不是ReadString——因为你不知道文件里有没有奇怪的换行符。 -
时间解析不要太死板,不是所有视频的日期格式都是标准ISO 8601,有的视频元数据里写的是“2023-01-01 12:00:00”,有的是“Jan 1, 2023”,还有的是“2023/1/1”,Go的
time.Parse需要一个固定的layout字符串,我写了一个”尝试多种格式“的函数,逐一遍历,直到解析成功为止,效率上稍微慢点,但胜在兼容性强。 -
文件名不要带空格和特殊字符,这好像是常识,但真的有人把视频命名为“2023年 最后一天 深圳 海边 .mov”(含空格和中文),跨平台传输的时候,Go的
os包能处理,但生成HTML链接的时候,记得url.QueryEscape一下。 -
别忘了处理时区,你拍视频的时候在北京,晚上在电脑上整理文件的时候,系统时区可能是UTC+8,但有些元数据记录的时区是UTC,如果你不做转换,同一天的视频可能被分到两天,我直接用
time.Local统一转换为本地时区,简单暴力。
不是结尾的结尾
这篇文章写到这里,差不多有1,200多字了(加上代码示例和细节),其实我本来还想聊聊怎么用Go写一个Web界面来展示这些视频,但写得太细就变成长篇教程了,而且说实话,上面那个HTML输出我已经用得很顺手了——打开浏览器,点一下“随机播放”,看看去年今天你在干嘛,感觉挺奇妙的。
工具放在GitHub上(你自己写一个也行,思路其实不复杂),Go语言的代码写好以后,编译成一个几MB的二进制文件,放在任何Linux或macOS系统上都能运行,Windows也行,只要你装好了ffmpeg。
对了,我后来还加了一个小功能:如果某一天没有任何视频,就在时间线上留一个空位,并标注“无记录”,不是每个日子都需要被拍下来,但知道哪天没拍,也一样有意义。
行吧,就写到这儿,工具还在慢慢改进,代码也有点乱,但用起来挺顺手的,如果你也有一堆从“第一天”到“第三百六十五天”的视频,不妨试试用Go写个自己的整理工具,相信我,看到它们被整齐地排列在时间线上,你会觉得这一年的日子,它们真真切切都在那儿。

上一篇:美国骑行博主在广西农村的温馨一餐